This is a lower bound: only assets with reliable historical USD prices are counted, so the actual loss may be higher.
0x395675b56370a9f5fe8b32badfa80043f5291443bd6c8273900476880fb5221e0x051ebd717311350f1684f89335bed4abd083a2b6EthereumAt Ethereum mainnet block 12000165, transaction 0x395675b56370a9f5fe8b32badfa80043f5291443bd6c8273900476880fb5221e drained a DODO Vending Machine pool at 0x051ebd717311350f1684f89335bed4abd083a2b6. The attacker-controlled helper contract first deposited attacker-controlled ERC20s, then borrowed the pool's real wCRES and USDT through flashLoan, and finally reinitialized the pool during the flash-loan callback so the repayment checks pointed at the fake tokens instead of the real assets.
The root cause is a code bug in the verified DVM implementation at 0x2bbd66fc4898242bdbd2583bbe1d76e8b8f71445: init is externally callable after deployment and rewrites _BASE_TOKEN_ and _QUOTE_TOKEN_, while flashLoan trusts those same mutable storage pointers after the borrower callback when deciding whether the loan was repaid.
The victim is a DODO Vending Machine pool proxy that delegates to the verified DVM implementation at 0x2bbd66fc4898242bdbd2583bbe1d76e8b8f71445. Before the exploit, the pool tracked wCRES at 0xa0afaa285ce85974c3c881256cb7f225e3a1178a as base and USDT at as quote, with reserves base units and quote units.
0xdac17f958d2ee523a2206206994597c13d831ec71348979177623485321037541150965863028The DVM design matters because flashLoan transfers the configured base and quote tokens to a borrower, executes an optional callback, and only then checks balances again. That repayment check is safe only if the pool's asset identity is immutable for the full duration of the loan. In this implementation, that assumption is false because init can be called again and directly rewrites the token pointers.
The attacker relied on two attacker-controlled replacement tokens observed in the trace at 0x7f4e7fb900e0ec043718d05caee549805cab22c8 and 0xf2df8794f8f99f1ba4d8adc468ebff2e47cd7010. Those tokens were deposited into the pool immediately before flashLoan so that, after reinitialization, the pool would appear solvent under the wrong asset pair.
This is an ACT-style attack caused by mutable core asset identity inside the pool. The verified DVM source exposes init as a plain external function and does not apply notInitialized, onlyOwner, or any equivalent one-time guard before assigning _BASE_TOKEN_ and _QUOTE_TOKEN_. The same implementation also uses _BASE_TOKEN_.balanceOf(address(this)) and _QUOTE_TOKEN_.balanceOf(address(this)) after the borrower callback inside flashLoan, so any in-callback mutation of those storage pointers changes what the repayment logic measures.
The broken invariant is straightforward: once a pool is deployed, the base-token and quote-token addresses must remain fixed across reserve accounting and across every flash-loan lifecycle. The code-level breakpoint is the combination of DVM.init mutating token-pointer storage and DVM.flashLoan rereading those same pointers after control returns from the borrower callback. The fake-token deposits are not the root cause by themselves; they only satisfy the repayment checks after the asset identity has already been corrupted.
The relevant verified source demonstrates the issue directly:
function flashLoan(
uint256 baseAmount,
uint256 quoteAmount,
address assetTo,
bytes calldata data
) external preventReentrant {
_transferBaseOut(assetTo, baseAmount);
_transferQuoteOut(assetTo, quoteAmount);
if (data.length > 0)
IDODOCallee(assetTo).DVMFlashLoanCall(msg.sender, baseAmount, quoteAmount, data);
uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this));
uint256 quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this));
...
}
function init(
address maintainer,
address baseTokenAddress,
address quoteTokenAddress,
uint256 lpFeeRate,
address mtFeeRateModel,
uint256 i,
uint256 k,
bool isOpenTWAP
) external {
require(baseTokenAddress != quoteTokenAddress, "BASE_QUOTE_CAN_NOT_BE_SAME");
_BASE_TOKEN_ = IERC20(baseTokenAddress);
_QUOTE_TOKEN_ = IERC20(quoteTokenAddress);
...
}
The exploit begins from Ethereum mainnet state immediately before block 12000165, with the victim pool still configured for the real wCRES and USDT pair and holding the reserve balances recorded above. The attacker transaction is permissionless: the sender EOA 0x368a6558255bccac517da5106647d8182c571b23 calls a helper contract at 0x910fd17b9bfc42a6eea822912f036ef5a080be8a, and neither the seed metadata nor the trace shows any privileged role, signature, or governance dependency.
First, the helper contract stages the fake accounting assets. The balance-diff artifact shows attacker-controlled token 0x7f4e7fb900e0ec043718d05caee549805cab22c8 increasing the pool balance by 134897917762348532113754, and attacker-controlled token 0xf2df8794f8f99f1ba4d8adc468ebff2e47cd7010 increasing the pool balance by 1150965873028. The trace records the corresponding transferFrom calls into the pool before the flash loan starts.
Second, the victim pool sends out the real assets. The seed trace shows the proxy calling flashLoan, delegating into the verified DVM implementation, and transferring 133548938584725046782716 wCRES and 1139456204397 USDT to the helper contract:
0x051EBD717311350f1684f89335bed4ABd083a2b6::flashLoan(...)
0x2BBD66fC4898242BDBD2583BBe1d76E8b8f71445::flashLoan(...) [delegatecall]
0xa0afAA285Ce85974c3C881256cB7F225e3A1178a::transfer(0x910FD17B..., 133548938584725046782716)
0xdAC17F958D2ee523a2206206994597C13D831ec7::transfer(0x910FD17B..., 1139456204397)
Third, the borrower callback corrupts the repayment context. Inside DVMFlashLoanCall, the helper invokes init on the victim pool. The trace shows the proxy delegatecalling into the DVM implementation and rewriting storage slot 1 from wCRES to the attacker token 0x7f4e...22c8, and storage slot 2 from USDT to attacker token 0xf2df...7010:
0x051EBD717311350f1684f89335bed4ABd083a2b6::init(..., 0x7f4E7fB900E0EC043718d05caEe549805CaB22C8, 0xf2dF8794f8F99f1Ba4D8aDc468EbfF2e47Cd7010, ...)
0x2BBD66fC4898242BDBD2583BBe1d76E8b8f71445::init(...) [delegatecall]
storage @ 1: 0x...a0afaa285ce85974c3c881256cb7f225e3a1178a -> 0x...7f4e7fb900e0ec043718d05caee549805cab22c8
storage @ 2: 0x...dac17f958d2ee523a2206206994597c13d831ec7 -> 0x...f2df8794f8f99f1ba4d8adc468ebff2e47cd7010
Finally, flashLoan resumes and queries balances using the mutated pointers. The trace immediately shows balanceOf(address(this)) calls against the attacker-controlled replacement tokens, not against wCRES and USDT. Those fake-token balances satisfy the solvency branches, so the function completes without demanding that the borrowed real assets be returned. The balance-diff artifact confirms the resulting real-asset loss: the pool loses 133548938584725046782716 wCRES and the helper gains the same amount, while the helper also gains 1139456204397 USDT.
The adversary strategy is a single-transaction callback-based drain using one EOA and one helper contract. The EOA 0x368a6558255bccac517da5106647d8182c571b23 originates transaction 0x395675b56370a9f5fe8b32badfa80043f5291443bd6c8273900476880fb5221e and pays the transaction gas. The helper contract 0x910fd17b9bfc42a6eea822912f036ef5a080be8a receives the real assets and performs the callback reinitialization.
The execution flow is:
flashLoan entrypoint to borrow real wCRES and USDT.init entrypoint and replace the tracked asset pair with the fake tokens.flashLoan finish its repayment checks against the fake tokens already sitting in the pool.This flow is fully ACT-compliant. An unprivileged adversary can deploy a callback contract, mint or control substitute ERC20s, call public pool functions, and realize the same state transition using only public on-chain information and public contract interfaces.
The measurable loss is the permanent removal of the pool's real wCRES and USDT while the pool's internal configuration is rewritten to attacker-controlled assets. The pool ends the transaction accounting against the wrong pair, so its stored configuration and its remaining real holdings no longer match.
The direct token losses recorded in the seed artifacts are:
wCRES: 133548938584725046782716 raw units (18 decimals)USDT: 1139456204397 raw units (6 decimals)The exploit helper contract holds those drained assets at the end of the transaction, while the attacker EOA pays 90874800000000000 wei in gas according to the balance-diff artifact.
0x395675b56370a9f5fe8b32badfa80043f5291443bd6c8273900476880fb5221e, including sender, block, and call target.flashLoan, callback, init delegatecall, storage mutations, and post-callback balanceOf reads.wCRES / USDT outflows to the helper contract.0x2bbd66fc4898242bdbd2583bbe1d76e8b8f71445, which exposes the unguarded init function and the mutable-token flashLoan repayment logic.0x051ebd717311350f1684f89335bed4abd083a2b6, which delegates to the vulnerable DVM implementation.