thirdweb Sender Spoof Burn
Exploit Transactions
Victim Addresses
0x0dabdc92af35615443412a336344c591faed3f90BSC0x4f34b914d687195a73318ccc58d56d242b4dccf6BSC0x256d3bc542ff4edb5959b584cc98741d28165bbcBSC0x7e3f53af12b2c84c35700be68cd316518546ca34BSCLoss Breakdown
Similar Incidents
DominoTT Forwarder Burn Exploit
48%UN Burn-Skim Exploit
36%TIME ERC2771 Burn Exploit
35%SafeMoon LP Burn Drain
35%CS Pair Balance Burn Drain
35%GPT Public LP-Burn Exploit
35%Root Cause Analysis
thirdweb Sender Spoof Burn
1. Incident Overview TL;DR
On BNB Chain, EOA 0x835b45d38cbdccf99e609436ff38e31ac05bc502 exploited two thirdweb TokenERC20 deployments in tx 0x67af906c1efc05067a01f197bd780ebf4e0a76729d54288a400e715f87ea50c7 and tx 0x1ee617cd739b1afcc673a180e60b9a32ad3ba856226a68e8748d58fcccc877a8. In each case the adversary acquired a small token position, used the public thirdweb forwarder at 0x7C4717039B89d5859c4Fbb85EDB19A6E2ce61171 to route a meta-transaction into TokenERC20.multicall(bytes[]), forged a Pancake pair address as _msgSender() for burn(uint256), called sync() on the pair, and then sold into the manipulated pool to extract WBNB.
The root cause is the unsafe composition of thirdweb's trusted-forwarder support with OpenZeppelin's multicall batching. Forwarder.execute authenticates only the outer req.from and appends that address to calldata once. MulticallUpgradeable.multicall then delegatecalls attacker-controlled inner payloads, and ERC2771ContextUpgradeable._msgSender() reads the last 20 bytes of each delegated calldata buffer. Because ERC20BurnableUpgradeable.burn(uint256) burns _msgSender(), the attacker can burn arbitrary holder balances, including AMM-pair reserves, without approval or ownership.
2. Key Background
The affected contracts are thirdweb TokenERC20 deployments that inherit ERC2771ContextUpgradeable, MulticallUpgradeable, and ERC20BurnableUpgradeable. The DominoTT deployment is 0x0dabdc92af35615443412a336344c591faed3f90, its Pancake pair is 0x4f34b914d687195a73318ccc58d56d242b4dccf6, the HALVING NETWORK deployment is 0x256d3bc542ff4edb5959b584cc98741d28165bbc, and its Pancake pair is 0x7e3f53af12b2c84c35700be68cd316518546ca34.
The forwarder is public and permissionless. Any EOA can sign its own ForwardRequest and submit it through the forwarder. The collected on-chain reads confirm that both victim tokens still trusted the forwarder immediately before exploitation and that the attacker EOA did not hold MINTER_ROLE or DEFAULT_ADMIN_ROLE.
Pancake v2 pairs keep reserves as raw token balances in the pair contract. sync() copies current token balances into the stored reserve variables that the AMM uses for pricing. If a token balance is reduced externally and the pair is then synced, the AMM starts quoting against the reduced reserve.
The attack therefore needs only four public conditions: the token trusts the forwarder, the token exposes multicall, a valuable third-party holder such as a Pancake pair owns tokens, and public liquidity exists so the attacker can buy before the burn and sell after sync().
3. Vulnerability Analysis & Root Cause Summary
This is an ACT exploit against the thirdweb token composition itself, not a mere MEV trade. The invariant that should hold is straightforward: authenticated signer identity must remain stable across a batched meta-transaction, and burn(uint256) must only burn the real caller's balance. That invariant fails because multicall delegatecalls raw user-supplied bytes while the trusted-forwarder context stays active.
The critical code relationship is visible in the verified sources. TokenERC20 opts into all three components, ERC2771ContextUpgradeable._msgSender() reads the final 20 bytes of calldata when msg.sender is a trusted forwarder, MulticallUpgradeable.multicall delegatecalls each subpayload into address(this), and ERC20BurnableUpgradeable.burn(uint256) destroys _msgSender()'s balance:
// Verified thirdweb TokenERC20 inheritance
contract TokenERC20 is
ERC2771ContextUpgradeable,
MulticallUpgradeable,
ERC20BurnableUpgradeable
{ ... }
function _msgSender() internal view override returns (address sender) {
if (isTrustedForwarder(msg.sender)) {
assembly { sender := shr(96, calldataload(sub(calldatasize(), 20))) }
} else {
return super._msgSender();
}
}
function multicall(bytes[] calldata data) external returns (bytes[] memory results) {
for (uint256 i = 0; i < data.length; i++) {
results[i] = _functionDelegateCall(address(this), data[i]);
}
}
function burn(uint256 amount) public virtual {
_burn(_msgSender(), amount);
}
The trusted forwarder completes the exploit path because it verifies the outer signature and then appends req.from to the calldata that reaches the victim token:
// Verified thirdweb Forwarder source
function verify(ForwardRequest calldata req, bytes calldata signature) public view returns (bool) {
address signer = _hashTypedDataV4(
keccak256(abi.encode(TYPEHASH, req.from, req.to, req.value, req.gas, req.nonce, keccak256(req.data)))
).recover(signature);
return _nonces[req.from] == req.nonce && signer == req.from;
}
function execute(ForwardRequest calldata req, bytes calldata signature)
public
payable
returns (bool, bytes memory)
{
require(verify(req, signature), "MinimalForwarder: signature does not match request");
_nonces[req.from] = req.nonce + 1;
(bool success, bytes memory result) = req.to.call{ gas: req.gas, value: req.value }(
abi.encodePacked(req.data, req.from)
);
...
}
The bug is therefore not that the attacker can fake the outer signer. The outer signer remains legitimate. The bug is that inner delegated subcalls can carry a different trailing 20-byte suffix than the authenticated req.from, so the logical sender used inside burn(uint256) becomes attacker-chosen.
4. Detailed Root Cause Analysis
The exploit chain is deterministic and fully visible in the collected artifacts.
The ACT opportunity existed in publicly reconstructible BNB Chain state immediately before block 34141660, with block 34141659 as the relevant pre-state for the DominoTT incident. At that point the victim token already trusted the public thirdweb forwarder, the Pancake pair already held both DominoTT and WBNB liquidity, and all routing and flash-loan venues used by the attacker were public. The same controlling EOA had already executed the identical reserve-manipulation pattern five blocks earlier against HNet, which confirms that the opportunity was not dependent on any victim-specific off-chain secret or privileged setup.
- The attacker signs a valid
ForwardRequestas an unprivileged EOA and targets the public victim token. - The forwarder verifies the EIP-712 signature and calls the token with
abi.encodePacked(req.data, req.from). - The victim token enters
multicall(bytes[]). multicalldelegatecalls an attacker-supplied inner payload. That inner payload isabi.encodePacked(abi.encodeWithSelector(burn.selector, burnAmount), pairAddress).- During the delegated call,
msg.senderis still the trusted forwarder, soERC2771ContextUpgradeable._msgSender()reads the last 20 bytes of the delegated calldata, not the outer request'sreq.from. burn(uint256)therefore executes_burn(pairAddress, burnAmount)and destroys the Pancake pair's token balance directly.- The attacker calls
pair.sync()so the pair reserves are updated to the now-reduced token balance. - The attacker sells the tokens acquired earlier into the distorted pool and withdraws excess WBNB.
The collected tx2 trace shows the exact breakpoint and post-breakpoint state transition:
// Collected trace for tx 0x1ee617cd...
0x7C4717039B89d5859c4Fbb85EDB19A6E2ce61171::execute(
(0x835B45D38cbDccf99E609436FF38E31Ac05bc502, 0x0DaBDC92aF35615443412A336344c591FaEd3f90, ..., 0xac9650d8...)
)
TokenERC20::ac9650d8(...835b45d38cbdccf99e609436ff38e31ac05bc502)
TokenERC20::42966c68(...4f34b914d687195a73318ccc58d56d242b4dccf6)
emit Transfer(
from: PancakePair: [0x4f34b914D687195A73318ccC58D56D242b4dCcF6],
to: 0x0000000000000000000000000000000000000000,
value: 1999999999999999966445568
)
PancakePair::sync()
emit Sync(reserve0: 132214347064234223478933, reserve1: 5343852821823074662)
The tx2 balance diff independently confirms the pair-side token destruction:
{
"token": "0x0dabdc92af35615443412a336344c591faed3f90",
"holder": "0x4f34b914d687195a73318ccc58d56d242b4dccf6",
"before": "2172773910489531517026547",
"after": "172773910489531550580979",
"delta": "-1999999999999999966445568"
}
The same structure appears five blocks earlier in tx 0x67af906c1efc05067a01f197bd780ebf4e0a76729d54288a400e715f87ea50c7 against HNet. The trace shows Forwarder.execute, delegated burn(uint256) with pair 0x7e3f53af12b2c84c35700be68cd316518546ca34 embedded in calldata, the burn event from the pair, sync(), the manipulated sale, and a final WBNB transfer to the same controlling EOA. The HNet pair lost 17999999999999999794973293674496 HNet according to the state diff.
This establishes the code-level breakpoint precisely: once a trusted-forwarder call is allowed to reach multicall, any state-changing function whose authority or asset subject depends on _msgSender() becomes forgeable inside the delegatecall frame.
5. Adversary Flow Analysis
The adversary strategy is a single-transaction reserve-manipulation loop repeated against two separate thirdweb tokens.
For tx 0x67af906c1efc05067a01f197bd780ebf4e0a76729d54288a400e715f87ea50c7:
- Controlling EOA:
0x835b45d38cbdccf99e609436ff38e31ac05bc502 - Helper contract:
0x18d76da50c3524e5e2b8df8b6c50b63022bc5feb - Victim token: HNet
0x256d3bc542ff4edb5959b584cc98741d28165bbc - Victim pair:
0x7e3f53af12b2c84c35700be68cd316518546ca34 - Flow: flash-loan
0.1WBNB, buy426274610346314935516141063331HNet, forwarder-based forged burn of17999999999999999794973293674496HNet from the pair balance,sync(), sell back into the manipulated pool for2535061113462309793wei WBNB, repay the flash loan, and transfer2435061113462309793wei WBNB to the EOA.
For tx 0x1ee617cd739b1afcc673a180e60b9a32ad3ba856226a68e8748d58fcccc877a8:
- Controlling EOA:
0x835b45d38cbdccf99e609436ff38e31ac05bc502 - Helper contract:
0xaed80b8a821607981e5e58b7a753a3336c0bfd6f - Victim token: DominoTT
0x0dabdc92af35615443412a336344c591faed3f90 - Victim pair:
0x4f34b914d687195a73318ccc58d56d242b4dccf6 - Flow: flash-loan
0.1WBNB, swap into40559563425297327102046DominoTT, burn1999999999999999966445568DominoTT from the pair via forged sender context,sync(), sell the purchased DominoTT back for1252095510970497300wei WBNB, repay the0.1WBNB flash loan, transfer1152095510970497300wei WBNB to the EOA.
The tx2 post-sync and profit realization are visible directly in the collected trace:
// Collected trace for tx 0x1ee617cd...
PancakeRouter::swapExactTokensForTokensSupportingFeeOnTransferTokens(
40559563425297327102046,
0,
[DominoTT, WBNB],
0xaed80b8a821607981e5e58b7a753a3336c0bfd6f,
...
)
PancakePair::swap(0, 1252095510970497300, 0xaed80b8a821607981e5e58b7a753a3336c0bfd6f, 0x)
WBNB::transfer(0x835B45D38cbDccf99E609436FF38E31Ac05bc502, 1152095510970497300)
The on-chain reads corroborate the realized EOA profit across both incidents: the EOA's WBNB balance increased by 2435061113462309793 wei in tx1 and 1152095510970497300 wei in tx2 before subtracting native gas costs, for a combined 3587156624432807093 wei WBNB gained at the EOA level.
For the DominoTT exploit specifically, the success predicate is direct WBNB profit at the controlling EOA; there is no separate non-monetary oracle. The validated on-chain reads show the EOA's WBNB balance moving from 29414467853864843185 before block 34141660 to 30566563364835340485 after the block, while the tx-level native balance diff shows 1080303000000000 wei of BNB gas paid. That yields a net tx2 profit of 1151015207970497300 wei in WBNB-equivalent terms after gas.
6. Impact & Losses
The exploit permanently destroyed the token-side reserves of two Pancake liquidity pools and extracted WBNB from both pairs.
Measured losses from the validated root cause are:
3587156624432807093wei WBNB drained across both incidents17999999999999999794973293674496HNet burned from the HNet/WBNB pair1999999999999999966445568DominoTT burned from the DominoTT/WBNB pair
The damage is broader than the immediate WBNB outflow. Because the pair balances themselves were burned, the pools' token reserves were permanently reduced and their AMM state was resynchronized to the depleted balances. That means LPs suffered both direct reserve loss and a distorted price surface that the attacker immediately monetized.
7. References
- Exploit tx
0x67af906c1efc05067a01f197bd780ebf4e0a76729d54288a400e715f87ea50c7on BNB Chain - Exploit tx
0x1ee617cd739b1afcc673a180e60b9a32ad3ba856226a68e8748d58fcccc877a8on BNB Chain - thirdweb Forwarder
0x7C4717039B89d5859c4Fbb85EDB19A6E2ce61171, functionexecute((address,address,uint256,uint256,uint256,bytes),bytes) - DominoTT thirdweb
TokenERC200x0dabdc92af35615443412a336344c591faed3f90 - HALVING NETWORK thirdweb
TokenERC200x256d3bc542ff4edb5959b584cc98741d28165bbc - Pancake pair
0x4f34b914d687195a73318ccc58d56d242b4dccf6 - Pancake pair
0x7e3f53af12b2c84c35700be68cd316518546ca34 - Verified code paths:
ERC2771ContextUpgradeable._msgSender(),MulticallUpgradeable.multicall(bytes[]),ERC20BurnableUpgradeable.burn(uint256), and thirdweb Forwarderverify/execute - Collected evidence types used for validation: tx metadata, full traces, balance diffs, and validator-reviewed on-chain reads for trusted-forwarder status, role checks, and WBNB deltas