All incidents

thirdweb Sender Spoof Burn

Share
Dec 07, 2023 08:37 UTCAttackLoss: 3.59 WBNB, 18,000,000,000,000 HNet +1 morePending manual check2 exploit txWindow: 22m
Estimated Impact
3.59 WBNB, 18,000,000,000,000 HNet +1 more
Label
Attack
Exploit Tx
2
Addresses
4
Attack Window
22m
Dec 07, 2023 08:37 UTC → Dec 07, 2023 08:59 UTC

Exploit Transactions

TX 1BSC
0x67af906c1efc05067a01f197bd780ebf4e0a76729d54288a400e715f87ea50c7
Dec 07, 2023 08:37 UTCExplorer
TX 2BSC
0x1ee617cd739b1afcc673a180e60b9a32ad3ba856226a68e8748d58fcccc877a8
Dec 07, 2023 08:59 UTCExplorer

Victim Addresses

0x0dabdc92af35615443412a336344c591faed3f90BSC
0x4f34b914d687195a73318ccc58d56d242b4dccf6BSC
0x256d3bc542ff4edb5959b584cc98741d28165bbcBSC
0x7e3f53af12b2c84c35700be68cd316518546ca34BSC

Loss Breakdown

3.59WBNB
18,000,000,000,000HNet
2,000,000DominoTT

Similar Incidents

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.

  1. The attacker signs a valid ForwardRequest as an unprivileged EOA and targets the public victim token.
  2. The forwarder verifies the EIP-712 signature and calls the token with abi.encodePacked(req.data, req.from).
  3. The victim token enters multicall(bytes[]).
  4. multicall delegatecalls an attacker-supplied inner payload. That inner payload is abi.encodePacked(abi.encodeWithSelector(burn.selector, burnAmount), pairAddress).
  5. During the delegated call, msg.sender is still the trusted forwarder, so ERC2771ContextUpgradeable._msgSender() reads the last 20 bytes of the delegated calldata, not the outer request's req.from.
  6. burn(uint256) therefore executes _burn(pairAddress, burnAmount) and destroys the Pancake pair's token balance directly.
  7. The attacker calls pair.sync() so the pair reserves are updated to the now-reduced token balance.
  8. 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.1 WBNB, buy 426274610346314935516141063331 HNet, forwarder-based forged burn of 17999999999999999794973293674496 HNet from the pair balance, sync(), sell back into the manipulated pool for 2535061113462309793 wei WBNB, repay the flash loan, and transfer 2435061113462309793 wei WBNB to the EOA.

For tx 0x1ee617cd739b1afcc673a180e60b9a32ad3ba856226a68e8748d58fcccc877a8:

  • Controlling EOA: 0x835b45d38cbdccf99e609436ff38e31ac05bc502
  • Helper contract: 0xaed80b8a821607981e5e58b7a753a3336c0bfd6f
  • Victim token: DominoTT 0x0dabdc92af35615443412a336344c591faed3f90
  • Victim pair: 0x4f34b914d687195a73318ccc58d56d242b4dccf6
  • Flow: flash-loan 0.1 WBNB, swap into 40559563425297327102046 DominoTT, burn 1999999999999999966445568 DominoTT from the pair via forged sender context, sync(), sell the purchased DominoTT back for 1252095510970497300 wei WBNB, repay the 0.1 WBNB flash loan, transfer 1152095510970497300 wei 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:

  • 3587156624432807093 wei WBNB drained across both incidents
  • 17999999999999999794973293674496 HNet burned from the HNet/WBNB pair
  • 1999999999999999966445568 DominoTT 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 0x67af906c1efc05067a01f197bd780ebf4e0a76729d54288a400e715f87ea50c7 on BNB Chain
  • Exploit tx 0x1ee617cd739b1afcc673a180e60b9a32ad3ba856226a68e8748d58fcccc877a8 on BNB Chain
  • thirdweb Forwarder 0x7C4717039B89d5859c4Fbb85EDB19A6E2ce61171, function execute((address,address,uint256,uint256,uint256,bytes),bytes)
  • DominoTT thirdweb TokenERC20 0x0dabdc92af35615443412a336344c591faed3f90
  • HALVING NETWORK thirdweb TokenERC20 0x256d3bc542ff4edb5959b584cc98741d28165bbc
  • Pancake pair 0x4f34b914d687195a73318ccc58d56d242b4dccf6
  • Pancake pair 0x7e3f53af12b2c84c35700be68cd316518546ca34
  • Verified code paths: ERC2771ContextUpgradeable._msgSender(), MulticallUpgradeable.multicall(bytes[]), ERC20BurnableUpgradeable.burn(uint256), and thirdweb Forwarder verify / 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