Compound cTUSD Legacy-TUSD Double-Entry sweepToken() Exchange-Rate Collapse Near Miss (2022-02-23)
- Name: Compound cTUSD legacy-TUSD double-entry
sweepToken()exchange-rate collapse near miss - Date: 2022-02-23
- Protocol: Compound V2
cTUSDmarket / TrueUSD - Chain: Ethereum
- Category: asset-identity aliasing / multi-entry token surface / stray-token rescue bypass / exchange-rate collapse / whitehat-coordinated near miss
- Loss: no realized theft; at patch time Compound’s TUSD market held roughly
$88M, OpenZeppelin said a maximal exploit could have roughly halved the redemption value of pre-existingcTUSD, and ChainSecurity estimated one borrow-side variant at roughly$3.1Mprofit when the issue was first found - Summary: Compound’s
cTUSDmarket treated the upgraded TUSD implementation at0x0000000000085d4780b73119b644ae5ecd22b376as its underlying asset. But legacy TUSD address0x8dd5fbce2f6a956c3022ba3663759011dd51e73estill forwarded token logic into the same balance sheet. That meant one economic asset could be reached through two contract addresses. Compound’s genericsweepToken(token)rescue hook only blockedtoken == underlying. Anyone could therefore pass the legacy TUSD alias, satisfy thetoken != underlyingcheck, and still makebalanceOf(cTUSD)andtransfer(admin, balance)operate on the very same TUSD reserves backingcTUSD. Sweeping the market’s cash into Compound’s Timelock would collapse thecTUSDexchange rate by drivingtotalCashtoward zero. An attacker could then mint discountedcTUSDor repay TUSD debt more cheaply before the funds were returned, socializing the loss onto earlier suppliers. TrustToken patched the root cause on 2022-02-23 before public exploitation. - Trigger / entry point:
- The vulnerable path was Compound V2’s standard
CErc20::sweepToken(EIP20NonStandardInterface token)helper on thecTUSDmarket. sweepToken()was intended to rescue stray ERC-20 balances accidentally sent to the cToken contract.- The only asset-identity guard was
require(address(token) != underlying, "CErc20::sweepToken: can not sweep underlying token"). - For most markets that exact-address check is enough. For TUSD it was not, because the legacy TUSD address and the newer configured TUSD implementation both mutated the same balances.
- The vulnerable path was Compound V2’s standard
- Root cause:
- Compound modeled
token addressas if it were a canonical asset identity. - TUSD’s legacy forwarding contract and newer implementation contract were economically the same asset surface but presented two distinct addresses to integrators.
sweepToken()used exact address inequality to distinguishunderlying assetfromstray asset.- Once that identity boundary failed, a housekeeping helper inherited authority to move the market’s actual reserves.
- The downstream economic damage came from Compound’s exchange-rate formula: if
totalCashis swept away, newly mintedcTUSDprices against a temporarily broken backing ratio.
- Compound modeled
- Exploit path:
- The attacker identifies that
cTUSDis configured with the newer TUSD address asunderlying, while legacy TUSD address0x8dd5...e73estill forwards to the same balances. - They call
cTUSD.sweepToken(legacyTUSD)instead of passing the configuredunderlyingaddress. - The
require(address(token) != underlying)check passes because the alias address differs from the configured address. legacyTUSD.balanceOf(address(cTUSD))still returns the market’s real TUSD cash balance, because both addresses expose the same balance sheet.legacyTUSD.transfer(admin, balance)moves that balance to Compound’s admin / Timelock destination as if it were stray inventory.cTUSDnow reports far less or zerototalCash, so the exchange rate(cash + borrows - reserves) / totalSupplycollapses.- Before the swept TUSD is returned, the attacker can either mint fresh
cTUSDat the depressed exchange rate or repay TUSD borrows more cheaply than the pre-sweep market state should allow. - When the swept TUSD is later restored, the attacker exits against the recovered exchange rate and the loss is borne by pre-existing
cTUSDsuppliers.
- The attacker identifies that
- What the attacker needed to be true:
- The same economic asset had to be reachable through at least two contract addresses.
- Compound had to key the
cannot sweep underlyingrule to exact address equality rather than canonicalized asset identity. - The market had to keep
sweepToken()permissionless. - The swept funds had to be returnable later, so the attacker could monetize a temporary exchange-rate dislocation rather than merely grief the market.
- TUSD did not need to be enabled as collateral for the core mint-at-a-discount path; the exploit primarily targeted supplier exchange-rate fairness.
- Technical anatomy worth remembering:
- This was not a bug in Compound’s interest model alone and not a direct bug in either TUSD address in isolation. The exploit lived in the integration seam between a two-address token surface and a generic cToken helper written for one-address asset identity.
- The dangerous helper was not a core mint or borrow function. It was a maintenance function intended for token rescue. That is exactly why the note matters: low-frequency admin or rescue code can still become solvency-critical when asset identity is modeled too narrowly.
- The attack did not permanently steal funds out to the attacker in one step. First it relocated market cash to Compound’s Timelock, then monetized the temporary accounting distortion through cheaper minting or repayment.
- ChainSecurity’s write-up also preserved a liquidation-theory branch, but TUSD was not collateral-enabled on Compound, so the most credible live monetization paths were the cheap-repayment and discounted-mint branches.
- OpenZeppelin concluded the issue was systemic, not Compound-specific: TUSD was listed across dozens of DeFi protocols, and variants of the same
double-entry tokenassumption failure appeared elsewhere.
- Representative contracts and artifacts:
- Legacy TUSD forwarding address:
0x8dd5fbce2f6a956c3022ba3663759011dd51e73e - Newer TUSD implementation used by Compound as underlying:
0x0000000000085d4780b73119b644ae5ecd22b376 - Compound
cTUSDmarket:cTUSD/ Compound V2 TUSD market - Vulnerable helper shape:
function sweepToken(EIP20NonStandardInterface token) override external { require(address(token) != underlying, "CErc20::sweepToken: can not sweep underlying token"); uint256 balance = token.balanceOf(address(this)); token.transfer(admin, balance); } - Root-cause patch transaction cited by OpenZeppelin:
0x125823f2914e4f14e06b9b1b30fe9dd9512b36354cc1f6c063118c4fe03d8287
- Legacy TUSD forwarding address:
- Response / mitigation:
- ChainSecurity found the issue during an audit of Compound’s cToken contracts and privately disclosed it.
- OpenZeppelin later reviewed the same issue during Compound’s comprehensive audit and concluded variants could affect other TUSD integrations too.
- Proposal 84, which would have modified the TUSD market through normal Compound governance, was discouraged and ultimately defeated because publicly changing
cTUSDrisked signaling the vulnerability before a fix was in place. - OpenZeppelin coordinated with Compound’s multisig / Pause Guardian and TrustToken on a short-fuse plan: if TrustToken did not commit to a root-cause patch within 24 hours, Compound would pause the TUSD market.
- TrustToken instead patched the issue at the token layer on 2022-02-23, eliminating the dangerous legacy entry point before public disclosure.
- OpenZeppelin disclosed the retrospective publicly on 2022-03-21 after the fix was live and no funds had been lost.
- Key transaction hashes:
- Root-cause TUSD patch tx:
0x125823f2914e4f14e06b9b1b30fe9dd9512b36354cc1f6c063118c4fe03d8287 - No public exploit transactions exist because the issue was fixed before a live attack.
- Root-cause TUSD patch tx:
Pattern tags:
- Asset-identity aliasing / multi-entry token surface: Compound special-cased only one TUSD address even though legacy and upgraded TUSD entrypoints still touched one economic balance sheet.
- Housekeeping-helper solvency crossover: a generic
sweepToken()rescue verb quietly sat on the market’s reserve boundary oncenot underlyingstopped meaningnot the same asset. - Temporary reserve-removal monetization: the exploit did not need to steal reserves directly on the first call; it only needed to relocate
cTUSDcash long enough for exchange-rate math to publish a cheaper mint / repayment state. - Canonical-asset versus raw-address policy drift: Compound’s configured
underlyingfield described one preferred entrypoint, but the protocol had no canonical asset object spanning all TUSD aliases. - Design companions: asset-identity-aliasing-multi-entry-token-surface, reserve-backed-claim-truth-security-object, entrypoint-equivalence-under-shared-standards, and unsupported-standard-verb-revocation-and-fail-closed-surface.
Why the exploit worked
- Compound treated
underlying addressas a full proof of asset identity. That was true for ordinary ERC-20 listings, but TUSD’s legacy forwarding surface meant one economic reserve could still be reached through two addresses. sweepToken()was written as ifdifferent token addressimpliedstray inventory. OncTUSD, that assumption failed: the legacy address satisfied the guard while still exposing the market’s real TUSD balance.- The dangerous power sat in a helper that looked operational, not in a bespoke admin drain path. Once the alias bypass succeeded, the helper inherited authority to move the same cash input that
exchangeRateStoredInternal()used to price all livecTUSDclaims. - The near miss also depended on reversibility. Swept funds went to Compound’s Timelock rather than directly to the attacker, but that was still enough because a temporary reserve hole was economically actionable: minting or repaying against the depressed exchange rate would have crystallized loss onto incumbent suppliers.
- TrustToken’s token-layer patch mattered because the root cause was not merely
Compound forgot an extra denylist address; it was that the TUSD asset family itself still exposed a second production entry surface that downstream protocols could misclassify.
Detection and audit pivots
- For every listed asset, ask whether
underlyingreally means one callable address or one economic asset family. If the token has forwarders, deprecated facades, migration shims, or proxy-era aliases, raw address equality is already too weak for policy. - Re-review every rescue, sweep, arbitrary-token, and
unsupported tokenhelper against alias surfaces. The sharp question is notcan this call move the configured underlying address?butcan this call move the same reserve through any other live entrypoint? - Trace how reserve movement propagates into pricing or claim issuance. On Compound the important follow-on question was
which math uses totalCash next, and who can mint / repay against it before reserves are restored? - Test housekeeping verbs under temporary-balance-distortion scenarios, not only direct theft scenarios. A helper that can evacuate reserves to a safe admin wallet can still be exploitable if exchange-rate, collateral, or repayment logic honors the interim state.
- Treat token-upgrade backward-compatibility as integration state, not token-team trivia. If a token preserves old entrypoints for UX continuity, downstream listings need that alias inventory before markets go live.
Failure-prevention checklist
- Model listed assets as canonical asset objects, not single addresses.
underlying,listed,paused, andcannot sweeplogic should collapse every known alias or legacy forwarding surface into one protected identity. - Treat
sweepToken()-style helpers as reserve code whenever they can touch balances that back live cTokens, vault shares, or collateral claims. - Require rescue helpers to prove an invariant stronger than
token != underlying, such asthis transfer cannot reduce the reserve that prices live claims. - Maintain an alias dossier for nontrivial integrated assets: canonical address, legacy addresses, forwarding behavior, shared balances, and which surfaces remain callable in production.
- Re-test listed markets whenever an integrated token upgrades, adds compatibility forwarding, or changes how legacy entrypoints route into current logic.
- Prefer root-cause deauthorization upstream when possible. If a deprecated token surface can still move real balances, either disable it or treat it as part of every downstream protocol’s live attack surface.
- Bound the blast radius of reserve-moving helpers with permissioning, pauses, or market-specific safeguards when a token family is known to have unusual identity or upgrade semantics.
- Monitor for reserve movements initiated through unexpected token addresses, not just unexpected EOAs or admin paths.
Audit prompts
- Which listed assets in the protocol can currently be reached through more than one contract address that shares balances, allowances, or transfer effects?
- Where does the code still equate
different addresswithdifferent asset, especially in rescue, sweep, listing, pause, and unsupported-token paths? - Can any helper move the same reserve that prices exchange rate, collateral value, or claim redemption through an alias the protocol does not classify as the underlying?
- If a reserve-moving helper sends funds to a trusted destination rather than the attacker, does any mint, repay, redeem, liquidation, or oracle path still treat the temporary post-transfer state as economically final?
- Who owns the alias inventory for integrated tokens, and what operational trigger forces that inventory to be updated when a token team adds or preserves a new compatibility surface?
- What test proves that every protected asset policy stays true if the caller supplies any documented alternate entrypoint for the same token family?
- If the token team promises that a legacy surface is deprecated, what onchain fact proves it no longer reaches production balances?
- Could pausing the market or helper fast enough actually stop the monetization leg, or would a temporary reserve distortion already be enough to let attackers mint or repay at a bad rate before operators react?
Lessons that should already be standard
- Asset identity is not always address identity. When one balance sheet answers through multiple token addresses, address-level deny rules become naming conventions, not security boundaries.
- Rescue code is solvency code the moment it can move reserves that price live claims.
Funds only move to the timelockis not a safety argument if other protocol math treats the interim reserve hole as a real market state.- Backward-compatibility token surfaces are part of the downstream protocol’s threat model whether integrators asked for them or not.
- Token upgrades should trigger integration re-certification, not just a note that the preferred address still works.
- If a token family cannot present one unambiguous production entrypoint, downstream protocols need canonicalization and stronger invariant checks before they expose generic helper authority around it.
Why this incident matters
- It is a clean example of asset identity aliasing: the protocol did not misprice a fake token or admit a malicious wrapper; it mis-modeled the fact that the same asset could answer to two addresses.
- It shows how seemingly harmless rescue hooks can become balance-sheet critical once
underlyingandnot underlyingare separated by address equality alone. - It also preserves an important integration lesson for upgradeable or legacy-token ecosystems:
same balances, different addressis not benign metadata. It can invalidate deny-lists, allowlists, and housekeeping guards across every downstream protocol.
Internal linkages
- Core recurring pattern: asset-identity-aliasing-multi-entry-token-surface.
- Adjacent integration pattern: interface-conformance-semantic-compatibility-failure — that note is broader and about unsupported token behavior generally; this incident is sharper because the token’s key failure mode was identity aliasing across two addresses, not exotic transfer semantics.
- Adjacent asset-truth pattern: asset-identity-registry-poisoning — both families are about asset identity lying to downstream systems, but bridge registry poisoning is about false denomination mapping while this case is the same economic asset appearing through multiple contract addresses.
Control surface
-
The decisive leverage sat in the combination of TUSD’s dual-address surface and Compound’s permissionless
sweepToken()helper, which treatedtoken address != underlying addressas sufficient proof that a token was unrelated stray inventory. -
Once that assumption broke, a housekeeping function became an exchange-rate attack primitive.
-
Sources:
-
Last reviewed: 2026-07-24 UTC