All audits
FailedOptimismIndependent

Exactly Protocol Market (2023)

Smart contract security audit by TierZero · 2023-11-15

Independent research. This is an unsolicited security analysis published by TierZero — not a commissioned audit, and no certificate is issued. It reflects our own review of public code and on-chain events.

Findings summary

1
Critical
2
High
1
Medium
1
Low
1
Informational

Independent research — not a commissioned audit. This report is based solely on public post-incident disclosures and third-party writeups; TierZero was not engaged by Exactly Protocol and did not review non-public code.

On August 18, 2023, Exactly Protocol — a fixed-rate, fixed-term lending market on Optimism — lost roughly $7.3M in ETH and USDC across a burst of transactions. Public writeups converge on the same root cause: the protocol's DebtManager periphery contract accepted a caller-supplied market address without checking it against the set of markets the protocol actually deployed, and combined that with a permit-based transfer path. An attacker supplied a contract they controlled in place of a real market, and DebtManager treated it as trustworthy.

What the protocol did

Exactly Protocol offers Compound/Aave-style variable-rate pools plus fixed-rate, fixed-term borrowing and lending, settled through per-asset Market contracts. DebtManager was a newer periphery contract layered on top, giving users one-transaction "leverage" and "deleverage" flows: roll collateral and debt between markets, optionally using an off-chain permit signature so the flow could execute without a separate approval transaction. That convenience — take a market address and a signature as arguments, then act on the caller's behalf — is exactly what a lending-protocol audit from our smart contract audit service is built to stress-test, because it concentrates trust in a single entry point.

The vulnerability

The bug class is missing allowlist validation on an externally-supplied contract address that is subsequently treated as a trusted, privileged callee. DebtManager's leverage/deleverage entry points took a Market market parameter and called into it (and used a permit signature to authorize pulling funds) without first confirming that market was one of the addresses the protocol itself had deployed and registered.

// Simplified pattern of the flaw
function crossDeleverage(
    Market market,        // attacker-controlled address accepted here
    Market marketToRepay,
    address borrower,
    uint256 maxRepay,
    Permit calldata permit
) external {
    // signature checked, but not bound tightly enough to a
    // specific, allowlisted market or execution context
    _permit(borrower, permit);

    // no `require(isMarket[market], "unknown market")`
    market.deposit(...);   // external call into an untrusted contract
    // ... state assumed consistent after this call returns
}

Two things compound the same underlying trust failure: the missing market allowlist check, and the fact that crossDeleverage made an external call into that unverified contract before it had finished updating its own accounting — leaving a reentrancy window. A permit signature authorizes who can move funds; it says nothing about which contract is allowed to receive the callback, and here the two were conflated.

How the exploit worked

  1. The attacker deployed a contract implementing the Market interface but with a deposit() function written to serve the attack, not to hold collateral.
  2. They called DebtManager.leverage()/crossDeleverage(), passing their fake contract as the market argument alongside a permit signature.
  3. Because DebtManager never validated market against the protocol's real market registry, it treated the attacker's contract as legitimate and invoked it.
  4. Inside the fake market's deposit(), the attacker re-entered DebtManager.crossDeleverage(), this time targeting a genuine market (reports point to USDC) and a real victim's position, exploiting the fact that DebtManager's own state hadn't settled from the outer call.
  5. The reentrant call let the attacker strip a victim's real collateral, which was then routed out through an attacker-controlled Uniswap V3 position. This repeated across roughly 117 accounts, with losses concentrated in a handful of large wallets.

The exploit didn't require a flash loan or price-oracle manipulation — it was a straightforward confused-deputy attack: get a privileged contract to trust an address it should have verified.

How an audit catches this

This is a class of finding we look for explicitly, not one that requires guessing at novel attack paths:

  • Address-parameter provenance check. For every external function that accepts a contract address and then calls into it, trace whether that address is validated against a canonical registry (isMarket[market], a factory-deployed check, or an immutable allowlist) before use. Any "trust the parameter" pattern on a privileged periphery contract is flagged by default.
  • Checks-effects-interactions and reentrancy tests. We write PoC tests that make the external call itself reenter the calling function, confirming state is fully settled (debt, collateral, and interest indices) before any external call, and that reentrancy guards exist on every state-mutating entry point reachable from an unauthenticated address.
  • Permit-scope invariant. A permit signature should authorize a specific action against a specific counterparty/context, not be reusable to bootstrap trust in an unrelated address supplied in the same call. We test that signature verification and address validation are independent, both-must-pass checks, not substitutes for one another.
  • Periphery-contract scope discipline. Exactly's original audits reportedly did not cover DebtManager because it was added later. Our code review and audit process treats any contract with transfer authority over user funds as in-scope by definition, regardless of when it was added to the deployment.

This same "unvalidated address treated as trusted" pattern shows up outside EVM lending too — see our notes on Anchor account discriminators and type confusion and on cross-program invocation risk in Solana DeFi, where an unchecked account or program ID plays the same role as the unchecked market address here.

Remediation

The fix is a registry check, not a redesign: require market (and any other externally supplied contract reference) to match a mapping populated only by governance or a factory at deployment time, reject the call otherwise, and apply checks-effects-interactions plus a reentrancy guard around any function that both moves funds and calls out to a caller-supplied contract. Permit signatures should be scoped narrowly enough that they can't be replayed against a substituted counterparty. None of this requires new cryptography or an architecture change — it requires treating every address argument on a privileged contract as untrusted input until proven otherwise, which is the baseline standard our smart contract audit engagements are scoped around.

Need your contracts audited?

Manual review + tooling across TON, Solana and EVM — certificate and full report included.

Get an audit