All incidents

LeetSwap Base Pair Drain

Share
Aug 01, 2023 01:20 UTCAttackLoss: 119.45 WETHPending manual check1 exploit txWindow: Atomic
Estimated Impact
119.45 WETH
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Aug 01, 2023 01:20 UTC → Aug 01, 2023 01:20 UTC

Exploit Transactions

TX 1Base
0xbb837d417b76dd237b4418e1695a50941a69259a1c4dee561ea57d982b9f10ec
Aug 01, 2023 01:20 UTCExplorer

Victim Addresses

0x94dac4a3ce998143aa119c05460731da80ad90cfBase
0xfcd3842f85ed87ba2889b4d35893403796e67ff1Base

Loss Breakdown

119.45WETH

Similar Incidents

Root Cause Analysis

LeetSwap Base Pair Drain

1. Incident Overview TL;DR

On Base block 2031747, transaction 0xbb837d417b76dd237b4418e1695a50941a69259a1c4dee561ea57d982b9f10ec drained the LeetSwap WETH/axlUSDC pair at 0x94dAC4a3Ce998143aa119c05460731dA80ad90cf. The attacker bought a small axlUSDC position, used a public helper on the pair to move almost all axlUSDC from the pair into the fee vault, called sync() to ratchet reserves down to the manipulated balance, and then swapped axlUSDC back into the pair to extract WETH.

The root cause is a contract bug in LeetSwapV2Pair: _transferFeesSupportingTaxTokens(address,uint256) is externally callable even though it is an invariant-critical fee-moving primitive. Because it transfers pair-held assets to the fees contract without access control and without updating reserves, any unprivileged caller can desynchronize balances from stored reserves and then make the manipulated balances authoritative via sync().

2. Key Background

LeetSwap uses a Uniswap-V2-style pair design with separately stored reserves (reserve0, reserve1) and live ERC-20 balances. Pricing and invariant checks are performed against the stored reserves until _update() or sync() refreshes them.

The attacked pair is the verified LeetSwap WETH/axlUSDC pair at 0x94dAC4a3Ce998143aa119c05460731dA80ad90cf. In the verified source, the pair deploys a dedicated fee vault contract and stores its address in immutable fees.

axlUSDC on Base is the verified token at 0xEB466342C4d449BC9f53A865D5Cb90586f405215. The pair held deep WETH liquidity and large axlUSDC reserves immediately before the exploit, which made reserve manipulation economically meaningful.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability class is an access-control and reserve-accounting failure in the pair contract. The key bug is that the helper below is public, not internal, and it directly transfers pair-held tokens to the fee vault:

function _transferFeesSupportingTaxTokens(address token, uint256 amount)
    public
    returns (uint256)
{
    uint256 balanceBefore = IERC20(token).balanceOf(fees);
    _safeTransfer(token, fees, amount);
    uint256 balanceAfter = IERC20(token).balanceOf(fees);
    return balanceAfter - balanceBefore;
}

The contract's reserve model requires economically available balances and stored reserves to stay aligned. That invariant is broken here because _transferFeesSupportingTaxTokens moves assets out of the pair without calling _update(). The pair therefore keeps stale reserves until somebody later calls sync():

function sync() external lock {
    _update(
        IERC20Metadata(token0).balanceOf(address(this)),
        IERC20Metadata(token1).balanceOf(address(this)),
        reserve0,
        reserve1
    );
}

In effect, the attacker can first shrink the live balance of one side of the pool, then use sync() to overwrite the stored reserves with the manipulated balance, and then trade against a price curve that now vastly overprices the depleted asset. That is exactly what happened here on the axlUSDC side of the pair.

4. Detailed Root Cause Analysis

Immediately before the exploit, the pair held about 120.187521209079354818 WETH and 216584888040 raw axlUSDC units. The attacker-funded exploit contract first wrapped 0.001 ETH into WETH and swapped it into the pair for a starter axlUSDC position.

The crucial breakpoint was the direct call to the public helper:

LeetSwapV2Pair::_transferFeesSupportingTaxTokens(
  0xEB466342C4d449BC9f53A865D5Cb90586f405215,
  216583081404
)

The collected trace shows the pair transferring 216583081404 raw axlUSDC units to its fee vault 0xE659e3044B4720B4f107b12a45bcd9bc44A4AC02, and returning the same amount. The balance-diff artifact confirms the effect:

{
  "token": "0xeb466342c4d449bc9f53a865d5cb90586f405215",
  "holder": "0x94dac4a3ce998143aa119c05460731da80ad90cf",
  "before": "216584888040",
  "after": "1622117",
  "delta": "-216583265923"
}

At that point the pair's live axlUSDC balance had been almost fully stripped, but the stored reserves were still stale. The attacker then called sync(). The trace shows the exact reserve update:

emit Sync(reserve0: 120188518209079354818, reserve1: 10000)

This is the invariant failure made authoritative: the pair still held roughly 120.1885 WETH, but reserve1 had been collapsed to only 10000 raw axlUSDC units. With reserves now reflecting the manipulated balance, the pair priced axlUSDC as if it were nearly absent.

The attacker then sold the axlUSDC accumulated from the seed swap back into the pair. The trace and balance diffs show a WETH transfer of 119446585023779038288 wei to the exploit contract, after which the contract unwrapped WETH to ETH and paid the profit recipient. The exploit did not require privileged keys, governance, or non-public data. Any unprivileged actor could have reproduced the same sequence on the public pair contract.

5. Adversary Flow Analysis

The adversary cluster consisted of:

  • EOA 0x705f736145bb9d4a4a186f4595907b60815085c3, which sent the exploit transaction.
  • Exploit contract 0xea8f89f47f3d4293897b4fe8cb69b5c233b9f560, which executed the calls.
  • Profit-recipient EOA 0x5b030f90db67190373dbf3422436df4c62f60a60, which received the final ETH payout.

The on-chain sequence was:

  1. The exploit contract received 0.001 ETH of seed capital and wrapped it into WETH.
  2. It swapped that WETH into the target pair to acquire a small axlUSDC inventory.
  3. It called the public pair helper _transferFeesSupportingTaxTokens(axlUSDC, 216583081404), pushing almost all pair-held axlUSDC into the pair's fee vault.
  4. It called sync(), causing the pair to emit Sync(reserve0=120188518209079354818,reserve1=10000).
  5. It transferred its axlUSDC back into the pair and called swap(...) to pull out WETH against the manipulated reserves.
  6. It unwrapped WETH to ETH and transferred the proceeds to 0x5b030f90db67190373dbf3422436df4c62f60a60.

This is a single-transaction ACT exploit: all calls were made through public interfaces, and the profit condition was realized immediately in the same transaction.

6. Impact & Losses

The measurable loss was 119446585023779038288 wei of WETH-equivalent value, or 119.446585023779038288 WETH, drained from the LeetSwap WETH/axlUSDC pair.

The balance-diff artifact shows:

{
  "token": "0x4200000000000000000000000000000000000006",
  "holder": "0x94dac4a3ce998143aa119c05460731da80ad90cf",
  "before": "120187521209079354818",
  "after": "740936185300316530",
  "delta": "-119446585023779038288"
}

The profit recipient 0x5b030f90db67190373dbf3422436df4c62f60a60 ended with 119447582023779038288 wei of native ETH-equivalent balance increase. After subtracting the exploit contract's 0.001 ETH seed capital and the sender EOA's 987419792502374 wei gas cost, the net profit was 119445594603986535914 wei.

7. References

  1. Base tx 0xbb837d417b76dd237b4418e1695a50941a69259a1c4dee561ea57d982b9f10ec metadata and trace.
  2. Collected balance diff for the same transaction, showing WETH loss from the pair and ETH gain by the profit recipient.
  3. Verified LeetSwapV2Pair source for 0x94dAC4a3Ce998143aa119c05460731dA80ad90cf, specifically _transferFeesSupportingTaxTokens, _update, and sync.
  4. Verified axlUSDC source for 0xEB466342C4d449BC9f53A865D5Cb90586f405215.