All incidents

Allbridge Pool Mispricing

Share
Apr 01, 2023 21:57 UTCAttackLoss: 290,278.55 USDT, 282,403.12 BUSDPending manual check1 exploit txWindow: Atomic
Estimated Impact
290,278.55 USDT, 282,403.12 BUSD
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Apr 01, 2023 21:57 UTC → Apr 01, 2023 21:57 UTC

Exploit Transactions

TX 1BSC
0x7ff1364c3b3b296b411965339ed956da5d17058f3164425ce800d64f1aef8210
Apr 01, 2023 21:57 UTCExplorer

Victim Addresses

0x179aad597399b9ae078acfe2b746c09117799ca0BSC
0xb19cd6ab3890f18b662904fd7a40c003703d2554BSC

Loss Breakdown

290,278.55USDT
282,403.12BUSD

Similar Incidents

Root Cause Analysis

Allbridge Pool Mispricing

1. Incident Overview TL;DR

On BNB Chain block 26982068, an unprivileged attacker used a flash-borrowed 7,500,000 BUSD position to dominate Allbridge's BUSD and USDT LP pools, skew pool accounting with a bridge swap, and then exit through Allbridge's one-sided withdrawal path in a way that broke the BUSD pool price. After the first BUSD-pool withdrawal, swapping only 40,000 BUSD through Allbridge returned 789,632.1174825 USDT, which is the concrete exploit predicate visible in the on-chain trace. The attacker repaid the flash loan and kept 549,874.391584307393758024 BUSD.

The root cause is an invariant-breaking implementation in Allbridge Pool.withdraw(uint256) and its helper _preWithdrawSwap(uint256,uint256). Instead of performing an invariant-preserving single-sided withdrawal, the code heuristically converts half of the token/vUSD imbalance against the full pool state and returns Math.min(...) of two derived values, without charging a swap fee. Once the attacker first imbalances the pool, this path over-credits actual token to the withdrawing LP and leaves the pool in a state where the next permissionless bridge swap is catastrophically mispriced.

2. Key Background

Allbridge maintains separate pools for each bridged token. Each pool tracks actual token liquidity in tokenBalance, virtual bridge inventory in vUsdBalance, and a stable-swap style invariant d. Bridge.swap bridges between pools by calling swapToVUsd on the source pool and swapFromVUsd on the destination pool.

Deposits and withdrawals change LP ownership and update the invariant, while bridge swaps are supposed to trade against the current pool state without allowing LP shares to withdraw more value than they represent. In this incident, the attacker used Wombat only as an external venue to source and settle stablecoins near parity; Wombat is not the vulnerable component. The loss-bearing contracts are Allbridge's BUSD pool at 0x179aad597399b9ae078acfe2b746c09117799ca0 and USDT pool at 0xb19cd6ab3890f18b662904fd7a40c003703d2554.

3. Vulnerability Analysis & Root Cause Summary

The exploit is an ATTACK-class ACT opportunity against Allbridge's pool accounting, not a mempool race and not a privileged compromise. The critical invariant is that burning LP shares must reduce the caller's claim by at most the invariant-backed value of those shares. A one-sided withdrawal must therefore be equivalent to an invariant-preserving conversion plus LP burn, and it must not let the caller withdraw more actual token than the burned share represents.

Allbridge breaks that invariant inside Pool.withdraw -> _preWithdrawSwap. The verified source shows that _preWithdrawSwap splits the difference between amountToken and amountVUsd, uses the full pool state with getY(...), and returns the smaller of two heuristic outcomes:

function _preWithdrawSwap(uint256 amountToken, uint256 amountVUsd) internal view returns (uint256) {
    if (amountToken > amountVUsd) {
        uint256 extraToken = (amountToken - amountVUsd) >> 1;
        uint256 extraVUsd = vUsdBalance - this.getY(tokenBalance + extraToken);
        return Math.min(amountToken - extraToken, amountVUsd + extraVUsd);
    } else {
        uint256 extraVUsd = (amountVUsd - amountToken) >> 1;
        uint256 extraToken = tokenBalance - this.getY(vUsdBalance + extraVUsd);
        return Math.min(amountVUsd - extraVUsd, amountToken + extraToken);
    }
}

That logic is heuristic rather than invariant-preserving, and it performs the internal conversion without charging the pool's public swap fee model. Once the attacker first makes the BUSD pool sufficiently imbalanced, burning LP shares through this path withdraws too much real BUSD and leaves the pool in a broken state where subsequent bridge swaps mint grossly inflated vUSD.

4. Detailed Root Cause Analysis

The seed trace shows the attacker helper contract 0x7d83fe202c51982a72e0a1146ec37b4643c725d7 receiving a Pancake flash swap and using it to set up dominant LP positions in both Allbridge pools. The observed sequence is:

0x179aaD...::deposit(5000000000000000000000000)
0xB19Cd6...::deposit(2000000000000000000000000)
0x7E6c25...::swap(495784250421615988577113, USDT, BUSD, helper)
0x179aaD...::withdraw(4830262616)

That first bridge swap pushes the BUSD pool away from the balanced state assumed by a safe one-sided withdrawal. The next call, Pool.withdraw(4830262616), triggers _preWithdrawSwap, and the trace shows the BUSD pool transferring 4,830,999.058 BUSD to the helper:

0x179aaD...::withdraw(4830262616)
emit Withdraw(param0: helper, param1: 4830262616)
BEP20Token::transfer(helper, 4830999058000000000000000)

This is the code-level breakpoint where the attacker converts the manipulated LP position into excess real token. Immediately afterward, the same BUSD pool is so distorted that a much smaller bridge swap produces a 19.7x effective price:

0x7E6c25...::swap(40000000000000000000000, BUSD, USDT, helper)
0x179aaD...::swapToVUsd(helper, 40000000000000000000000)
emit SwappedToVUsd(..., 40000000000000000000000, 789378078, ...)
0xB19Cd6...::swapFromVUsd(helper, 789378078)
BEP20USDT::transfer(helper, 789632117482500000000000)

That 40,000 BUSD to 789,632.1174825 USDT conversion is the decisive invariant break. The attacker then withdraws the USDT LP position, settles the remaining stablecoin exposure back through Wombat, repays the flash-loan venue, and transfers the residual BUSD to the sender EOA 0xc578d755cd56255d3ff6e92e1b6371ba945e3984.

The profit is fully quantified from public evidence. The seed balance diff shows the sender EOA ending with 549,874.391584307393758024 BUSD and paying 0.06220135 BNB in gas. Using the Pancake WBNB/BUSD pair 0x58F876857a02D6762E0101bb5C46A8c1ED44Dc16 at block 26982067, where reserves were 237118372705823977377220 WBNB and 74784468274423672828057561 BUSD, that gas spend values to 19.617606314599469358 BUSD and the sender's net portfolio gain is 549,854.773977992794288666 BUSD.

5. Adversary Flow Analysis

The attacker strategy is fully permissionless and fits in one transaction:

  1. Flash-borrow 7,500,000 BUSD from Pancake pair 0x7efaef62fddcca950418312c6c91aef321375a00.
  2. Swap part of the BUSD through Wombat to source USDT, then deposit 5,000,000 BUSD into the Allbridge BUSD pool and 2,000,000 USDT into the Allbridge USDT pool, becoming the dominant LP.
  3. Bridge-swap 495,784.250421615988577113 USDT to BUSD through Allbridge to skew the BUSD pool.
  4. Burn the BUSD-pool LP position via withdraw(4830262616), which exercises the broken _preWithdrawSwap path and over-withdraws BUSD.
  5. Swap only 40,000 BUSD back through Allbridge and receive 789,632.1174825 USDT because the BUSD pool is now mispriced.
  6. Burn the USDT-pool LP position, convert the remaining USDT back to BUSD on Wombat, repay the Pancake pair with 7,522,500 BUSD, and transfer the residual 549,874.391584307393758024 BUSD to the sender.

The attacker-controlled accounts are the sender EOA 0xc578d755cd56255d3ff6e92e1b6371ba945e3984 and helper contract 0x7d83fe202c51982a72e0a1146ec37b4643c725d7. No victim-observed transaction or privileged access is required; the exploit depends only on public pre-state, permissionless flash liquidity, and permissionless protocol entrypoints.

6. Impact & Losses

The incident depleted both loss-bearing Allbridge pools in the exploit transaction:

  • Allbridge USDT pool 0xb19cd6ab3890f18b662904fd7a40c003703d2554 lost 290,278.553842307020179247 USDT (290278553842307020179247 raw units, 18 decimals).
  • Allbridge BUSD pool 0x179aad597399b9ae078acfe2b746c09117799ca0 lost 282,403.115457925324692409 BUSD (282403115457925324692409 raw units, 18 decimals).

The attacker realized 549,874.391584307393758024 BUSD at the EOA level, and the remaining gas-adjusted value gain is 549,854.773977992794288666 BUSD using the public WBNB/BUSD spot valuation at the incident block. The pools were left heavily imbalanced, and the exploit path was executable by any unprivileged actor able to source temporary stablecoin liquidity.

7. References

  1. Incident transaction: 0x7ff1364c3b3b296b411965339ed956da5d17058f3164425ce800d64f1aef8210
  2. Allbridge BUSD pool verified source: 0x179aad597399b9ae078acfe2b746c09117799ca0
  3. Allbridge USDT pool verified source: 0xb19cd6ab3890f18b662904fd7a40c003703d2554
  4. Allbridge bridge router: 0x7e6c2522fee4e74a0182b9c6159048361bc3260a
  5. Seed trace artifact documenting deposits, withdrawals, swaps, and profit transfer: /workspace/session/artifacts/collector/seed/56/0x7ff1364c3b3b296b411965339ed956da5d17058f3164425ce800d64f1aef8210/trace.cast.log
  6. Seed balance diff artifact documenting pool losses and attacker profit: /workspace/session/artifacts/collector/seed/56/0x7ff1364c3b3b296b411965339ed956da5d17058f3164425ce800d64f1aef8210/balance_diff.json
  7. Auditor valuation artifact for the BUSD-denominated profit calculation: /workspace/session/artifacts/auditor/iter_1/profit_valuation.json
  8. Pancake WBNB/BUSD pair used to value gas at the incident block: 0x58F876857a02D6762E0101bb5C46A8c1ED44Dc16