TIME ERC2771 Burn Exploit
Exploit Transactions
0xecdd111a60debfadc6533de30fb7f55dc5ceed01dfadd30e4a7ebdb416d2f6b6Victim Addresses
0x4b0e9a7da8bab813efae92a6651019b8bd6c0a29Ethereum0x760dc1e043d99394a10605b2fa08f123d60faf84EthereumLoss Breakdown
Similar Incidents
thirdweb Sender Spoof Burn
35%DominoTT Forwarder Burn Exploit
33%PLNTOKEN transferFrom burn hook drains WETH reserves
33%VINU Reserve Drain
31%GROK Tax-Swap Dump
31%WETH Drain via Unprotected 0xfa461e33 Callback on 0x03f9-62c0
30%Root Cause Analysis
TIME ERC2771 Burn Exploit
1. Incident Overview TL;DR
On Ethereum mainnet block 18730463, transaction 0xecdd111a60debfadc6533de30fb7f55dc5ceed01dfadd30e4a7ebdb416d2f6b6 exploited thirdweb TokenERC20 at 0x4b0e9a7da8bab813efae92a6651019b8bd6c0a29 and the TIME-WETH Uniswap V2 pair at 0x760dc1e043d99394a10605b2fa08f123d60faf84. The attacker used a public trusted forwarder at 0xc82bbe41f2cf04e3a8efa18f7032bdd7f6d98a81 together with delegatecall-based multicall to spoof _msgSender() as the pair address during burn(uint256), destroy most of the pair's TIME balance, call sync(), and then dump previously acquired TIME for WETH.
The root cause is a dangerous composition of modules that each behave as designed in isolation. TokenERC20 trusts ERC2771 meta-transactions and also exposes OpenZeppelin MulticallUpgradeable. Because multicall delegatecalls attacker-supplied calldata while msg.sender remains the trusted forwarder, ERC2771ContextUpgradeable._msgSender() reads the last 20 bytes of the delegated subcall payload instead of the real outer signer. In this incident, that let an unprivileged attacker make ERC20BurnableUpgradeable.burn(uint256) execute as if the Uniswap pair itself had called it.
2. Key Background
The victim token contract is thirdweb TokenERC20 at 0x4b0e9a7da8bab813efae92a6651019b8bd6c0a29. It inherits ERC2771ContextUpgradeable, MulticallUpgradeable, and ERC20BurnableUpgradeable. The relevant live liquidity venue is the TIME-WETH Uniswap V2 pair at 0x760dc1e043d99394a10605b2fa08f123d60faf84, with TIME as token0 and WETH as token1.
The exploit relies on four background facts:
ERC2771ContextUpgradeabletreats the last 20 bytes of calldata as the logical sender whenevermsg.senderis a trusted forwarder.MulticallUpgradeable.multicall(bytes[])re-enters the same contract withdelegatecall, so the outermsg.senderis preserved during inner calls.ERC20BurnableUpgradeable.burn(uint256)destroys tokens from_msgSender()rather than from an explicitaccountargument.- A Uniswap V2 pair can be repriced catastrophically if a token contract destroys the pair's token balance and the pair then calls
sync()to ratify the new reserve.
The ACT pre-state is Ethereum mainnet after block 18730462 and before the exploit transaction executes. In that pre-state, TokenERC20 already trusts forwarder 0xc82bbe41f2cf04e3a8efa18f7032bdd7f6d98a81, and the TIME-WETH pair already holds 65692658856160314505008470321 TIME and 89787809617954054780 WETH. No privileged key, whitelist position, or private orderflow is required to access the public forwarder, the Uniswap router, or the live token contract.
3. Vulnerability Analysis & Root Cause Summary
The broken invariant is straightforward: any path that relies on _msgSender() for balance ownership or authorization must bind that logical sender to the real signer of the outer call. TokenERC20 violates that invariant once it combines ERC2771 sender suffixing with delegatecall-based multicall. The victim contract trusts the forwarder in ERC2771ContextUpgradeable, and _msgSender() extracts the last 20 calldata bytes whenever the call comes from that forwarder. MulticallUpgradeable then delegatecalls attacker-supplied subcall bytes back into TokenERC20, preserving the forwarder as msg.sender while giving the attacker full control over the new calldata suffix. ERC20BurnableUpgradeable.burn(uint256) consumes _msgSender() directly, so the attacker can burn tokens from any address that can be encoded into the delegated subcall tail. In this incident, the attacker encoded the TIME-WETH pair address, burned 62227259510000000000000000000 TIME from the pair, forced the pair to sync(), and converted the resulting mispricing into 89495661608671096816 wei of net ETH-denominated profit after gas.
The vulnerable code path is visible in the collected source and verified forwarder source:
// ERC2771ContextUpgradeable
function _msgSender() internal view virtual override returns (address sender) {
if (isTrustedForwarder(msg.sender)) {
assembly {
sender := shr(96, calldataload(sub(calldatasize(), 20)))
}
} else {
return super._msgSender();
}
}
// MulticallUpgradeable
function multicall(bytes[] calldata data) external virtual returns (bytes[] memory results) {
results = new bytes[](data.length);
for (uint256 i = 0; i < data.length; i++) {
results[i] = _functionDelegateCall(address(this), data[i]);
}
}
// ERC20BurnableUpgradeable
function burn(uint256 amount) public virtual {
_burn(_msgSender(), amount);
}
4. Detailed Root Cause Analysis
The exploit hinges on how the public forwarder packages meta-transactions. Its verified execute function calls the target with abi.encodePacked(req.data, req.from), so the outer forwarded call into TokenERC20 legitimately carries the attacker's chosen signer address in the last 20 bytes:
(bool success, bytes memory result) = req.to.call{ gas: req.gas, value: req.value }(
abi.encodePacked(req.data, req.from)
);
In the collected trace, the attacker first buys TIME, then relays a forged-looking but fully valid ForwardRequest through forwarder 0xc82bbe41f2cf04e3a8efa18f7032bdd7f6d98a81. The forwarded target call is TokenERC20.multicall(bytes[]), and the single inner bytes payload is a burn(uint256) subcall whose calldata tail has been replaced with the pair address:
0xc82BbE41f2cF04e3a8efA18F7032BDD7f6d98a81::execute(...)
TokenERC20::ac9650d8(...) // multicall(bytes[])
TokenERC20::42966c68(...) [delegatecall] // burn(uint256)
emit Transfer(
from: 0x760dc1E043D99394A10605B2FA08F123D60faF84,
to: 0x0000000000000000000000000000000000000000,
value: 62227259510000000000000000000
)
That trace line is the decisive breakpoint. The burn event proves that the delegated subcall resolved _msgSender() to the pair address rather than to the attacker's signer. The collected balance diff corroborates the same state change at storage level: the pair's TIME balance drops from 65692658856160314505008470321 to 3465399346160314505008470321, a delta of -62227259510000000000000000000 TIME.
Once the pair's TIME balance is destroyed, the attacker calls pair.sync(). The trace shows the pair updating reserves to match the manipulated post-burn token balance:
0x760dc1E043D99394A10605B2FA08F123D60faF84::sync()
emit Sync(9999999891268803341558999, 94787809617954054780)
After the reserve ratification, the attacker sells the TIME acquired in the opening swap back into the now-mispriced pool. The trace shows the profitable exit and WETH withdrawal:
0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D::swapExactTokensForTokensSupportingFeeOnTransferTokens(
3455399346269045701666911322, 0, [TIME, WETH], 0x6980..., 1701904319
)
0x760dc1E043D99394A10605B2FA08F123D60faF84::swap(0, 94513462587046838316, 0x6980..., 0x)
WETH9::withdraw(94513462587046838316)
The exploit is ACT because the entire transaction sequence is permissionless. An unprivileged actor can fund the buy leg, sign a ForwardRequest for an address they control, relay it through the public forwarder, and use the public Uniswap V2 router and pair. The success predicate is objective and already realized in the incident: the attacker cluster's ETH-denominated value rises from 198257735908099592483 wei to 287753397516770689299 wei, with a net increase of 89495661608671096816 wei after 17800978375741500 wei of gas and fees.
5. Adversary Flow Analysis
The adversary strategy is a single-transaction, multi-stage exploit carried by transaction 0xecdd111a60debfadc6533de30fb7f55dc5ceed01dfadd30e4a7ebdb416d2f6b6.
- The top-level EOA
0xfde0d1575ed8e06fbf36256bcdfa1f359281455afunds and submits the transaction to attacker contract0x6980a47bee930a4584b09ee79ebe46484fbdbdd0. - The attacker contract wraps
5ETH into WETH and uses router0x7a250d5630b4cf539739df2c5dacb4c659f2488dto buy3455399346269045701666911322TIME from the TIME-WETH pair. - The attacker relays a signed
ForwardRequestwhose recovered signer is0xa16a5f37774309710711a8b4e83b068306b21724. That request targetsTokenERC20.multicall(bytes[])and contains a delegatedburn(uint256)payload with the pair address appended as the forged sender suffix. burn(uint256)executes as if the pair itself were calling it and destroys62227259510000000000000000000TIME from pair0x760dc1e043d99394a10605b2fa08f123d60faf84.- The attacker contract calls
pair.sync()so the Uniswap reserves now reflect the burned TIME balance. - The attacker immediately sells the TIME bought in step 2 back into the drained pool, receives
94513462587046838316WETH, unwraps it to ETH, and sends4923240442287576107wei to0xdafea492d9c6733ae3d56b7ed1adb60692c98bc5.
The identified adversary-related accounts are fully supported by the trace and balance deltas:
0xfde0d1575ed8e06fbf36256bcdfa1f359281455a: top-level sender and gas payer.0x6980a47bee930a4584b09ee79ebe46484fbdbdd0: attacker contract that orchestrates the exploit path.0xa16a5f37774309710711a8b4e83b068306b21724: recovered meta-transaction signer insideForwarder.execute.0xdafea492d9c6733ae3d56b7ed1adb60692c98bc5: direct ETH payout recipient at the end of the exploit.
6. Impact & Losses
The measurable losses are both economic and state-destructive.
- The TIME-WETH pair loses
89513462587046838316units of WETH-equivalent value from its pre-state balance sheet. - The pair's TIME balance is reduced by
62227259510000000000000000000TIME through the unauthorized burn. - The pair's TIME reserve collapses from
65692658856160314505008470321to3465399346160314505008470321. - The pair is left with only
274347030907216464WETH after the exit swap.
From the attacker perspective, the success predicate is net profit in ETH terms. Across the attacker contract, the top-level sender, and the direct payout recipient, the cluster's value rises by 89495661608671096816 wei after fees. This is consistent with the native balance deltas in the collected balance_diff.json, which show WETH/ETH extraction from the pair and gas payment by the top-level sender.
7. References
- Exploit transaction metadata for
0xecdd111a60debfadc6533de30fb7f55dc5ceed01dfadd30e4a7ebdb416d2f6b6, including block18730463, calldata, and gas data. - Full execution trace for the exploit transaction, showing the opening TIME buy,
Forwarder.execute, delegatedburn(uint256),pair.sync(), the exit swap, andWETH.withdraw. - Balance diff artifact for the exploit transaction, showing the pair TIME burn, WETH depletion, and the attacker's net ETH-denominated gain.
- thirdweb
TokenERC20verified source, especially its inheritance ofERC2771ContextUpgradeable,MulticallUpgradeable, andERC20BurnableUpgradeable. ERC2771ContextUpgradeablesource, specifically_msgSender()and_msgData()sender-suffix handling for trusted forwarders.- OpenZeppelin
MulticallUpgradeablesource, specificallymulticall(bytes[])and_functionDelegateCall. - OpenZeppelin
ERC20BurnableUpgradeablesource, specificallyburn(uint256). - Verified source for forwarder
0xc82bbe41f2cf04e3a8efa18f7032bdd7f6d98a81, especiallyexecute(ForwardRequest,bytes)and itsabi.encodePacked(req.data, req.from)call pattern. - Verified source for Uniswap V2 pair
0x760dc1e043d99394a10605b2fa08f123d60faf84, especiallysync(),swap(...), and reserve accounting.