Exactly Protocol DebtManager Malicious-Market Permit Account-Rebinding Reentrancy Drain (2023-08-18)

  • Name: Exactly Protocol DebtManager malicious-market permit account-rebinding reentrancy drain
  • Date: 2023-08-18
  • Protocol: Exactly Protocol
  • Chain: Optimism
  • Category: account confusion / fake integration endpoint / permit-account rebinding / malicious-market reentrancy / delegated-position drain
  • Loss: about $7.61M total across 117 affected accounts; the main exploiter extracted about 4,330 ETH, while a copycat attacker extracted about 140 ETH
  • Summary: Exactly Protocol’s peripheral DebtManager helper (0x16748Cb753A68329cA2117a7647aA590317EbF41) was meant to let users leverage and deleverage lending positions in one transaction. The fatal mistake was that leverage() trusted a caller-supplied market address both as the contract that would verify permit-style authorization and as the external endpoint to call during execution. By supplying a malicious fake market and setting the permit’s account field to a victim, the attacker made DebtManager stop operating on behalf of msg.sender and start operating on behalf of the victim instead. The fake market’s deposit() then re-entered crossDeleverage() against a real Exactly market, withdrew victim funds into an attacker-controlled Uniswap pool, and converted existing DebtManager allowances on leveraged accounts into direct theft. A second accounting quirk in market share-value accrual let the main attacker nudge some drained accounts just below health factor 1 and repeatedly liquidate them to keep extracting more.
  • Trigger / entry point:
    • Exactly launched the DebtManager periphery helper on 2023-03-30 to wrap leverage / deleverage flows around its core markets.
    • The missing input validation became practically exploitable after a permit scheme was added on 2023-07-12, because leverage() now accepted both a caller-supplied market and a caller-supplied permit payload describing which account should authorize the action.
    • The vulnerable surface lived in DebtManager.leverage(), which accepted an arbitrary market argument and later performed an external deposit() call against it.
    • The app and multisig flows also tended to leave DebtManager with larger-than-necessary approvals or signed permit capacity, which widened the set of victims the exploit could monetize once the helper began acting on behalf of the wrong account.
  • Root cause:
    • DebtManager treated a user-supplied market address as if it were the canonical Exactly market the helper was supposed to trust.
    • Permit verification was performed against the supplied market, so a malicious market could bypass the intended authorization boundary.
    • After that check, the helper rebound its internal acting-account context to the permit’s account field, meaning later operations could execute on behalf of a victim rather than the transaction sender.
    • leverage() then made an external call to deposit() on the malicious market before the helper had safely constrained the rest of the flow, creating a re-entrant path into crossDeleverage() while the helper still believed it was acting for the victim.
    • Exactly’s frontend approval logic amplified the bug by commonly granting DebtManager more allowance than a single leverage / deleverage action strictly needed.
  • Exploit path:
    1. The main attacker deployed a malicious market contract that implemented the interface DebtManager expected, including a hostile deposit() path.
    2. They called DebtManager.leverage() and supplied that fake market as the market argument while placing a victim address in the permit payload’s account field.
    3. Because the permit check was performed against the attacker-controlled market rather than a canonical Exactly market, the authorization boundary was bypassed and DebtManager’s acting-account context shifted to the victim.
    4. DebtManager.leverage() then called deposit() on the malicious market.
    5. Inside that fake deposit(), the attacker created or used an attacker-controlled Uniswap pool and re-entered DebtManager.crossDeleverage(), this time pointing marketIn at a real Exactly market such as USDC while using the malicious market as the fake repayment destination.
    6. crossDeleverage() withdrew the victim’s funds from the real Exactly market and routed them into the attacker-controlled swap path as if it were deleveraging the victim’s position.
    7. The attacker removed liquidity or otherwise unwound the fake market / Uniswap setup so the real tokens ended up in attacker-controlled addresses.
    8. For some targets, the attacker then exploited a separate share-value accrual quirk: by taking large fixed-pool borrows and repaying them immediately, they slightly reduced collateral valuation enough to push a previously drained victim just below health factor 1, liquidated the account at a discount, restored the health factor to roughly 1.25, and repeated the withdrawal cycle in subsequent transactions.
  • What the attacker needed to be true:
    • Victims needed live DebtManager approvals or permit capacity on leveraged positions.
    • The helper needed to accept an arbitrary market address instead of proving it was one of Exactly’s canonical markets.
    • The helper needed to let the permit payload define which account it was acting for after the attacker-controlled market had satisfied the permit check.
    • The malicious market needed a reachable external-call hook such as deposit() from which it could re-enter crossDeleverage().
    • A real Exactly market with victim liquidity and debt had to remain reachable so the fake deleverage could pull valuable assets out.
    • The attacker-controlled swap path had to be able to accept the victim’s withdrawn assets while returning worthless or attacker-controlled settlement on the other side.
  • Technical anatomy worth remembering:
    • This was not a failure of Exactly’s core market contracts alone; it was a periphery-helper exploit where a convenience layer became a privileged execution surface over real user positions.
    • The fake market was both the trust-anchor substitution and the reentrancy trampoline. It let the attacker satisfy the helper’s local expectations while still controlling the next external call.
    • The important preserved invariant is not just validate inputs. It is never let a user-supplied integration endpoint both certify authority and receive the next privileged call.
    • Exactly’s own postmortem makes the sequence unusually clear: bypassed permit verification changed the helper’s acting account from msg.sender to the permit’s account, then the malicious deposit() used that rebound context to enter crossDeleverage() against a real market. The counterfeit endpoint, delegated victim identity, re-entrant resume path, and leftover approval scope were one joined helper authority object, not four separate mistakes.
    • The oversized frontend approvals mattered because they turned a helper bug into a broad multi-victim drain rather than a single-account misuse. In the web-app’s leverage and deleverage flows, users often approved or signed for the final borrow amount while the helper only needed the delta, leaving DebtManager with a larger residual spend surface than the immediate action required.
    • That detail is worth preserving as design-layer evidence, not only as UX sloppiness: the fake market decided which account DebtManager would act for, while residual approvals decided how much approved wallet-funded authority that mistaken acting-account context could still exercise once the flow reached real markets.
    • The post-exploit share-value manipulation is worth preserving because it made the incident more than a one-shot withdrawal bug: once an account was left barely solvent, the attacker could use a small collateral-valuation distortion to reopen a fresh withdrawal window through liquidation mechanics.
  • Representative transactions and contracts:
    • Vulnerable DebtManager: 0x16748Cb753A68329cA2117a7647aA590317EbF41
    • Main exploiter contract / primary draining address named in Exactly’s postmortem: 0x3747DbBCb5C07786a4c59883E473A2e38F571af9
    • Copycat exploiter (tiffa.eth): 0x00000000002088951336D7972746a135F2956417
    • Attacker contract deployment tx highlighted by Exactly: 0xeb330e1d745b4e3af3012f080a262704a3461b1620c809212d56da696c5baa22
    • Representative exploit tx analyzed by Exactly / Phalcon: 0xe8999fb57684856d637504f1f0082b69a3f7b34dd4e7597bea376c9466813585
    • Copycat transaction cited in the postmortem: 0x3cc452c70c81c1300c65169d261f5c4ad037b48da2139bf0743429d6e100de96
    • Emergency pause tx: 0x50ea2ea398151327ee1914e7d35a2d0e1625359a693da3a212933dac10ae112c
    • Treasury-fee-to-zero mitigation tx for the share-value issue: 0x7aaef51ba9695127abb156297734ef66a36f1aa6fbaca641141d03cbb9707b8f
  • Response / mitigation:
    • Exactly paused the protocol on 2023-08-18 once the team understood the exploit path, though withdrawals remained live by design.
    • The protocol later set treasury fees for all markets to 0% as a precaution while the share-value accounting issue was fixed and audited.
    • Exactly published a wallet-by-wallet loss analysis, investigation repository, and postmortem, then committed to auditing the periphery helper flows that its web app exposed rather than relying only on core-market audits.
    • The user-facing lesson was also operational: leverage helpers and permit flows should not keep broader or longer-lived allowance than the exact action requires.

Pattern tags

  • Permit-surface root substitution: leverage(..., Permit calldata marketPermit, ...) verified authority against the caller-supplied market before checkMarket(market) ran, so the fake market became the object that certified whether the helper should trust the action at all.
  • Acting-account rebinding through attacker-certified permit data: once the malicious market accepted a permit whose account field named a victim, DebtManager stopped acting for msg.sender and carried the victim identity forward as _msgSender into later privileged steps.
  • Counterfeit-endpoint callback trampoline: the same fake market that bypassed authorization also owned the next external deposit() call, letting the attacker re-enter crossDeleverage() while the helper still believed it was operating for the victim.
  • Residual-approval amplification: the exploit family was not only wrong account chosen; the web app’s oversized permit / approval practice meant that once account context had been rebound, DebtManager still had enough leftover spend authority to turn that mistake into multi-victim withdrawals.

Why the exploit worked

  • Exactly split one security object into separate caller-controlled pieces. The helper let the attacker choose the market that defined permit truth, the permit payload that defined the acting account, and the callback endpoint that received the next privileged external call, instead of binding those into one canonical periphery route.
  • The order of operations mattered. The permit modifier ran against the supplied market before noTransferLeverage() called checkMarket(market), so canonical-market validation happened too late to protect the authorization boundary that had already been decided.
  • The fake market did not merely bypass a check and disappear; it remained live as the next deposit() target. That turned the counterfeit root object into a reentrancy trampoline, allowing crossDeleverage() to resume with victim context against a real Exactly market.
  • The losses scaled because approval scope and identity confusion composed cleanly. Once _msgSender had been rebound to a victim, residual allowances and signed permit capacity determined how much real market collateral the helper could keep moving before it hit a hard stop.
  • The later share-value quirk was a profit extender, not the primary door. It mattered because it let the attacker reopen withdrawable headroom on already-drained accounts, but only after the fake-market / permit-account confusion had already converted helper authority into unauthorized victim withdrawals.

Detection and audit pivots

  • In every helper that accepts a user-supplied market, pool, vault, or router, ask whether any authorization modifier or signature check runs before canonical-endpoint validation. If yes, the trust anchor may already be attacker-chosen even if a later allowlist exists.
  • Treat _msgSender overrides, permit account fields, meta-tx principals, and delegate identities as first-class security objects. Review whether any attacker-controlled endpoint can rewrite who the helper believes it is acting for before value-moving calls complete.
  • Trace periphery flows end-to-end, not function-by-function: can the same supplied contract both certify authority and receive the next callback-capable external call? That combined shape is much more dangerous than either property alone.
  • Audit allowance discipline as part of exploitability, not only UX hygiene. If a helper bug misbinds user identity, how much residual approval or permit capacity remains available to monetize that bug across many accounts?
  • Fuzz helper flows with fake market -> accepted permit(account=victim) -> callback reentry into real market sequences and assert that canonical-market checks, acting-account identity, and spender scope remain mutually consistent at every edge, not only at function exit.

Failure-prevention checklist

  • Run canonical-market validation before any permit, meta-tx, or delegated-identity modifier consumes caller-supplied market data. In this family, checkMarket(market) belonged ahead of permit(market, ...), not inside the later execution body.
  • Do not let the same user-supplied endpoint both certify authority and receive the next privileged external call. If a route must support dynamic endpoints, split who is allowed to authenticate this action? from who receives execution next? and pin both to governance-controlled registries or immutable route config.
  • Treat acting-account context as a first-class security object. Once a helper decides whose position it is operating on, freeze that identity through the rest of the route or recompute it from a stronger canonical source before every value-moving step.
  • Bind approvals and permits to route-local deltas, expiries, and canonical spender meaning. A leverage helper should not emerge from a completed action still holding broad residual approval that a later counterfeit-endpoint bug can reuse.
  • Collapse helper authority to one explicit route object: canonical market set, acting account, allowed callback / resume target, permitted swap venue, and maximum spend scope. If those fields can drift independently, attackers will compose them.
  • Reentrancy-harden periphery helpers even when the core markets already look disciplined. A helper that can withdraw, deleverage, or swap on behalf of users needs frame-local locks or staged settlement around every external call boundary.
  • Test fake-endpoint flows directly: malicious market accepted by permit path, victim account rebound, callback into real market, and residual-approval spending against live user balances.
  • Audit frontend approval policy as part of exploitability, not only UX hygiene. Overbroad or long-lived helper approvals are protocol-side blast-radius multipliers.

Audit prompts

  • Can any caller-supplied market, vault, router, or module both satisfy a permission / permit check and receive the next external call while the helper still believes it is acting for a user?
  • Does canonical-endpoint validation happen before or after delegated-identity state like _msgSender, account, or permit.account is established?
  • After a permit or meta-tx modifier runs, what exact state variable or stack value defines whose position this helper now controls, and can an attacker rewrite it without proving the same identity to the canonical market?
  • If the first external call re-enters another helper path, which fields from the original frame remain live: acting account, market set, spender scope, or swap-path assumptions?
  • Are residual approvals sized to this transaction's exact delta, or to a larger target state like final borrow size that leaves reusable post-transaction authority behind?
  • Could a counterfeit market still monetize without the later share-value quirk? If yes, counterfeit-root admission is the primary bug and any accounting edge is only a profit extender.
  • Do monitoring and incident-response tooling tell operators which helper, market, account context, and approval surface each deleverage or withdrawal actually relied on?
  • If one helper route goes wrong, what concrete cap stops it from draining every account that ever approved that helper?

Lessons that should already be standard

  • A permit is only as trustworthy as the object asked to verify it. If the caller chooses that verifier, the signature boundary is already suspect.
  • Who the helper is acting for is not a convenience variable; it is money-moving authority. Rebinding that field mid-route is as dangerous as swapping the spender or collateral contract.
  • Canonical endpoint identity has to be fixed before delegated execution starts. Later allowlist checks do not save a flow that already authenticated the wrong object.
  • Periphery helpers deserve core-grade review. If they can spend approvals, choose routes, or act over user positions, they are part of the live solvency and custody boundary.
  • Excess approvals turn localized logic bugs into portfolio-wide drains. The exploit scales when stale spender rights outlive the exact action that supposedly needed them.
  • Counterfeit-root bugs and callback bugs often compose. The fake endpoint is dangerous not just because it is fake, but because it can keep steering execution after the protocol has already accepted its version of reality.
  • The healthy design target is one delegated authority object, not a pile of individually plausible fields. Market identity, acting account, callback target, and spend scope either stay fused or they become an exploit kit.

Why this incident matters

  • It is a clean EVM example of account confusion through a fake integration endpoint rather than through a fake token alone: the attacker supplied a counterfeit market that looked locally valid enough to become both the permit-verification surface and the next privileged execution target.
  • It also shows how delegated helper contracts can become more dangerous than the markets they wrap. Users were not interacting directly with a malicious market; they were trusting a helper that was allowed to pick markets, spend approvals, and act across real positions on their behalf.
  • Exactly’s own postmortem also preserves the second half of the authority story: the helper bug decided which victim account DebtManager would act for, while the web app’s oversized approvals decided how much residual approval-funded spend power that mistaken acting-account context still had once the fake route reached real markets.
  • The healthier lesson is therefore broader than validate the market input: the helper needed one delegated authority object binding canonical market identity, acting account, next callback endpoint, and remaining approval-backed spend scope before any periphery flow could move value on behalf of a user.
  • The incident preserves a nasty composition: fake-endpoint trust, acting-account rebinding, reentrancy, and excess approvals each look survivable alone, but together they turned a leverage convenience feature into a scalable victim-drain primitive.

Internal linkages

  • Core recurring pattern: account-confusion-root-of-trust-substitution.
  • Adjacent pattern: callback-frame-authority-reuse — after the fake market became the trust anchor, the helper’s still-live acting-account frame could be rebound and reused against real markets.
  • Adjacent pattern: reentrancy — the fake market needed a callback window to jump from authorization confusion into a live crossDeleverage() theft path.
  • Sharper positive-design companion: callback-frame-truth-security-object — a healthy helper should have bound canonical market, acting account, target position set, resume path, and approval-spending meaning into one authenticated in-flight execution frame instead of letting a fake market redefine those fields mid-flow.
  • Broader positive-design companion: root-object-authenticity-and-trust-anchor-recomputation-discipline — the first failure was letting a caller-supplied fake market become the root object that defined permit truth at all; a healthier design would have recomputed or loaded the canonical market before delegated identity and callback-capable execution began.
  • Adjacent positive-design companion: approval-funded-route-authority-truth-security-object — the web app and periphery helper together failed to keep one route-local authority object binding spender surface, acting account, canonical market set, and deleverage-stage meaning before allowance-backed value moved.
  • End-to-end routing companion: route-commitment-across-callback-frame-and-settlement-envelope — useful for the broader design question of how Exactly should have kept fake-market rejection, acting-account identity, callback-stage authority, and later allowance-backed settlement meaning pinned across one deleverage lifecycle instead of letting each stage rediscover those facts from attacker-controlled inputs.
  • Broader spender-boundary companion: spender-authority-truth-security-object — useful when the sharper question is why residual DebtManager approvals were still broad enough that a fake-endpoint / acting-account confusion bug could turn the helper into a victim-wallet spender at all.
  • Adjacent pattern: arbitrary-call-token-approval-drain — both families monetize standing approval surfaces, but Exactly’s decisive failure was a fake trusted market plus delegated-position execution rather than a generic arbitrary external call router.
  • Adjacent protocol context: exactly-protocol.

Control surface