All incidents

Biswap Migrator Token Substitution

Share
Jun 30, 2023 13:10 UTCAttackLoss: 28.15 ETH, 53,553.74 USDTPending manual check1 exploit txWindow: Atomic
Estimated Impact
28.15 ETH, 53,553.74 USDT
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Jun 30, 2023 13:10 UTC → Jun 30, 2023 13:10 UTC

Exploit Transactions

TX 1BSC
0xebe5248820241d8de80bcf66f4f1bfaaca62962824efaaa662db84bd27f5e47e
Jun 30, 2023 13:10 UTCExplorer

Victim Addresses

0x839b0afd0a0528ea184448e890cbaaffd99c1dbfBSC
0x2978d920a1655abaa315bad5baf48a2d89792618BSC

Loss Breakdown

28.15ETH
53,553.74USDT

Similar Incidents

Root Cause Analysis

Biswap Migrator Token Substitution

1. Incident Overview TL;DR

Biswap V3 Migrator on BNB Chain exposed an anyone-can-take exploit in transaction 0xebe5248820241d8de80bcf66f4f1bfaaca62962824efaaa662db84bd27f5e47e at block 29554462. The attacker-controlled EOA 0xa7a98876c1dc2bffc4b2c8ccdebb847ff808662b called helper contract 0x76a40bd16b6d2b9bfbfe112199bae836c16dfc84, which then used V3Migrator.migrate at 0x839b0afd0a0528ea184448e890cbaaffd99c1dbf to pull LP tokens from victim 0x2978d920a1655abaa315bad5baf48a2d89792618.

The root cause is that V3Migrator.migrate burns a real Biswap V2 LP token but blindly trusts caller-supplied token0 and token1 when approving funds and minting the replacement V3 position. That lets an attacker burn a victim's approved LP, mint the victim an NFT over attacker-deployed fake tokens, and strand the real underlying assets on the migrator. Base.sweepToken and Base.unwrapWETH9 then make those stranded assets permissionlessly extractable.

2. Key Background

Biswap's V3Migrator is supposed to convert a UniswapV2-style LP into a Biswap V3 position for the LP owner. The intended flow is: transfer the LP in, burn it into the pair's real underlying assets, and mint a V3 position over those same assets for the recipient.

The relevant public protocol components are:

  • V3Migrator at 0x839b0afd0a0528ea184448e890cbaaffd99c1dbf
  • LiquidityManager at 0x24ba8d2a15fe60618039c398cf9fd093b1c1feb5
  • Victim LP pair 0x63b30de1a998e9e64fd58a21f68d323b9bcd8f85
  • Real pair underlyings 0x2170ed0880ac9a755fd29b2688956bd959f933f8 (ETH) and 0x55d398326f99059ff775485246999027b3197955 (USDT)

Base, which V3Migrator inherits, also exposes public cleanup helpers such as multicall, sweepToken, unwrapWETH9, and refundETH. Those helpers are only safe if the migrator never retains user assets after a call. Once that assumption fails, they become an extraction path.

The attacker first deployed helper contract 0x76a40bd16b6d2b9bfbfe112199bae836c16dfc84 in transaction 0x9fae2a38f26b4fec99b8d2fd57f838cd92b7f57cf843b4d9822ecfa4e0d067cb at block 29553055. Its CREATE nonces 1 and 2 produced 0x6919b2988d68128ed62644d7043c1799dd0f0d78 and 0xe26ade3a97f068603af72690746bae81b971d4f9, the fake token addresses later used in the malicious victim-facing migration.

3. Vulnerability Analysis & Root Cause Summary

This incident is an ATTACK-category ACT exploit, not a privileged-admin event. The safety invariant is straightforward: after pair.burn() returns the real assets from a V2 LP, those assets must either back the recipient's new V3 position or be refunded to the recipient. V3Migrator.migrate breaks that invariant by taking params.token0 and params.token1 from untrusted calldata instead of deriving them from pair.token0() and pair.token1(). The same function also calls transferFrom(params.recipient, params.pair, params.liquidityToMigrate), so any caller can pull LP tokens from an address that has already approved the migrator. In the seed transaction, the victim's approved LP was burned into real ETH and USDT, but the replacement NFT was minted over attacker-controlled fake tokens. The real assets remained on the migrator and were then reused by the attacker in the same transaction, while the verified Base code shows that a simpler generalized path exists through public sweepToken and unwrapWETH9.

The critical breakpoint is in V3Migrator.migrate:

IBiswapPair(params.pair).transferFrom(params.recipient, params.pair, params.liquidityToMigrate);
(uint256 amount0V2, uint256 amount1V2) = IBiswapPair(params.pair).burn(address(this));

safeApprove(params.token0, liquidityManager, amount0V2ToMigrate);
safeApprove(params.token1, liquidityManager, amount1V2ToMigrate);

ILiquidityManager(liquidityManager).mint(
    ILiquidityManager.MintParam({
        miner: params.recipient,
        tokenX: params.token0,
        tokenY: params.token1,
        fee: params.fee,
        ...
    })
);

The code above is taken from the verified V3Migrator source and shows exactly why the exploit works: the migrator burns canonical assets but mints with arbitrary caller-selected token addresses.

The violated security principles are equally direct:

  • Do not trust user-supplied token metadata when canonical pair state is already available on-chain.
  • Do not expose unrestricted residue-sweep helpers unless every call path enforces zero leftover user assets.
  • Enforce asset conservation across the burn, mint, and refund phases of migration.

4. Detailed Root Cause Analysis

The validated ACT opportunity exists in the BNB Chain pre-state immediately before block 29554462, specifically block 29554461, where the victim still held LP tokens and had already granted V3Migrator a public allowance. The attack requires only three conditions:

  • A victim holds a positive balance of a UniswapV2-compatible LP and has approved V3Migrator.
  • The attacker can deploy arbitrary ERC20-like contracts and create a Biswap V3 pool for them.
  • The attacker can call V3Migrator.migrate and then extract or reuse the real assets that remain on the migrator.

The exploit sequence is visible in the seed trace. The helper contract first checks the victim's LP balance and allowance, then calls V3Migrator.migrate with the real pair address but fake token0 and token1:

0x839b...::migrate(
  (0x63b30d..., 1094357092264218434277,
   0x6919b2988d68128ed62644d7043c1799dd0f0d78,
   0xe26ade3a97f068603af72690746bae81b971d4f9,
   150, 10000, 20000, 0, 0,
   0x2978d920a1655abaa315bad5baf48a2d89792618,
   1688130636, false)
)

The burn step consumes the victim's real LP and sends real ETH and USDT to the migrator:

BiswapPair::transferFrom(victim, pair, 1094357092264218434277)
BiswapPair::burn(0x839b...)
  -> amount0 = 28149267765302306856
  -> amount1 = 53553740618437724760070

Immediately after that, the replacement V3 NFT is minted for the victim against the attacker's fake token pair, not the real pair underlyings:

0x24Ba...::mint(
  (0x2978..., 0x6919..., 0xe26..., 150, 10000, 20000,
   28149267765302306856, 53553740618437724760070, 0, 0, 1688130636)
)
emit Transfer(..., 0x2978..., 1511)

That state transition satisfies the non-monetary success predicate MigratedAssetConservationBroken: a victim-approved LP is destroyed, the victim receives a bogus NFT over attacker-created tokens, and the real assets are left available to the adversary. The trace then shows the attacker reusing those real assets in a second migrate call that mints NFT 1512 to the attacker. The balance diff confirms the economic result: the victim's LP balance dropped to zero, the victim received one additional LiquidityManager NFT, the pair lost 28149267765302306856 wei-denominated ETH token units and 53553740618437724760070 USDT units, and the attacker-side state captured the same underlying value.

The generalized extraction path is visible in the verified Base implementation:

function sweepToken(address token, uint256 minAmount, address recipient) external payable {
    uint256 all = IERC20(token).balanceOf(address(this));
    require(all >= minAmount, 'WETH9 Not Enough');
    if (all > 0) {
        safeTransfer(token, recipient, all);
    }
}

Because sweepToken and unwrapWETH9 are permissionless, any caller can extract residual balances once migrate leaves real user assets on the migrator. That is why this incident is ACT even though the observed attacker used a helper contract and a same-transaction remint flow.

5. Adversary Flow Analysis

The adversary strategy was a single-transaction LP drain with three stages.

First, the attacker EOA 0xa7a98876c1dc2bffc4b2c8ccdebb847ff808662b deployed helper contract 0x76a40bd16b6d2b9bfbfe112199bae836c16dfc84 in tx 0x9fae2a38f26b4fec99b8d2fd57f838cd92b7f57cf843b4d9822ecfa4e0d067cb. That helper later produced child contracts 0x6919... and 0xe26..., which were the fake token addresses in the malicious migration, and pool 0x7b690b84e40849af013bd6c0db9d7a55122d25c5, which backed the victim's bogus replacement NFT.

Second, in exploit tx 0xebe5248820241d8de80bcf66f4f1bfaaca62962824efaaa662db84bd27f5e47e, the helper called V3Migrator.migrate against victim 0x2978d920a1655abaa315bad5baf48a2d89792618 and pair 0x63b30de1a998e9e64fd58a21f68d323b9bcd8f85. The victim had both the LP balance and a matching migrator allowance of 1094357092264218434277, so the helper could permissionlessly force the LP into the pair and burn it. The victim then received NFT 1511, but that NFT referenced fake tokens instead of ETH/USDT.

Third, the attacker realized value from the stranded real assets. In the observed transaction, the attacker used those assets to mint NFT 1512 for itself through a second migrate-like step. The trace and balance diff show the attacker ended with the drained ETH-side value and effectively all of the drained USDT-side value, while the verified Base code shows a simpler alternative: directly call sweepToken and unwrapWETH9 once the fake-token migration has stranded real funds on V3Migrator.

The adversary-related accounts established by the evidence are:

  • 0xa7a98876c1dc2bffc4b2c8ccdebb847ff808662b: attacker EOA and exploit sender
  • 0x76a40bd16b6d2b9bfbfe112199bae836c16dfc84: attacker helper/orchestrator
  • 0x6919b2988d68128ed62644d7043c1799dd0f0d78: fake token0 deployed from the helper
  • 0xe26ade3a97f068603af72690746bae81b971d4f9: fake token1 deployed from the helper
  • 0x7b690b84e40849af013bd6c0db9d7a55122d25c5: attacker-controlled fake-token Biswap V3 pool

6. Impact & Losses

The direct victim in the seed exploit was LP holder 0x2978d920a1655abaa315bad5baf48a2d89792618, which lost its full approved balance in pair 0x63b30de1a998e9e64fd58a21f68d323b9bcd8f85. The measurable underlying loss from that LP burn was:

  • 28149267765302306856 units of ETH token balance (18 decimals)
  • 53553740618437724760070 units of USDT token balance (18 decimals)

The broader impact is larger than this single address. Any LP holder that had publicly approved V3Migrator could be drained in the same way: the approved LP can be burned, the victim can be left with a worthless or attacker-controlled replacement NFT, and the real underlying assets can be extracted or reused by any unprivileged adversary. The seed transaction also cost the attacker 4927985000000000 wei in native BNB gas, which does not change the exploitability conclusion because the success predicate is the non-monetary asset-conservation break and the extracted underlying value is far larger.

7. References

  1. Seed exploit transaction: 0xebe5248820241d8de80bcf66f4f1bfaaca62962824efaaa662db84bd27f5e47e
  2. Helper deployment transaction: 0x9fae2a38f26b4fec99b8d2fd57f838cd92b7f57cf843b4d9822ecfa4e0d067cb
  3. Verified V3Migrator.sol for 0x839b0afd0a0528ea184448e890cbaaffd99c1dbf
  4. Verified base.sol for 0x839b0afd0a0528ea184448e890cbaaffd99c1dbf
  5. Seed trace showing the malicious victim-facing mint to fake tokens and attacker NFT 1512
  6. Balance diff for the seed transaction showing victim LP depletion and underlying-asset movement
  7. Attacker address overview linking EOA 0xa7a98876c1dc2bffc4b2c8ccdebb847ff808662b to helper 0x76a40bd16b6d2b9bfbfe112199bae836c16dfc84
  8. Derived evidence resolving helper child contracts 0x6919... and 0xe26... from CREATE nonces 1 and 2