All incidents

BTC20 Presale Price Manipulation

Share
Aug 19, 2023 13:48 UTCAttackLoss: 62,516 BTC20Pending manual check1 exploit txWindow: Atomic
Estimated Impact
62,516 BTC20
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Aug 19, 2023 13:48 UTC → Aug 19, 2023 13:48 UTC

Exploit Transactions

TX 1Ethereum
0xcdd93e37ba2991ce02d8ca07bf6563bf5cd5ae801cbbce3dd0babb22e30b2dbe
Aug 19, 2023 13:48 UTCExplorer

Victim Addresses

0x1f006f43f57c45ceb3659e543352b4fae4662df7Ethereum
0x58178a119781df139307572066d5e74704809861Ethereum

Loss Breakdown

62,516BTC20

Similar Incidents

Root Cause Analysis

BTC20 Presale Price Manipulation

1. Incident Overview TL;DR

On Ethereum mainnet block 17949215, transaction 0xcdd93e37ba2991ce02d8ca07bf6563bf5cd5ae801cbbce3dd0babb22e30b2dbe used a Balancer flash loan and public BTC20 liquidity venues to buy 62,516 BTC20 from the BTC20 presale at a manipulated discount, unwind the position, repay the flash loan, and keep net profit. The root cause is that the BTC20 presale implementation priced primary-sale inventory from a live Uniswap V2 spot quote on the same public BTC20/WETH market that any unprivileged trader could move immediately before calling the public purchase path.

2. Key Background

The victim sale endpoint was the BTC20 Presale proxy at 0x1f006f43f57c45ceb3659e543352b4fae4662df7, which delegated to PresaleV4 implementation 0xdb0618e0b850cab3f756d030398be22929226d5c at the relevant block. PresaleV4 exposed public buyWithEthDynamic(uint256) and buyWithUSDTDynamic(uint256) functions and transferred sale-token inventory immediately to the buyer when dynamicSaleState was enabled.

The pricing dependency was hard-coded. fetchPrice(uint256 amountOut) built the path [WETH, BTC20], called router.getAmountsIn(amountOut, path), and then added a configurable percentage premium:

function fetchPrice(uint256 amountOut) public view returns (uint256) {
  address[] memory path = new address[](2);
  path[0] = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
  path[1] = 0xE86DF1970055e9CaEe93Dae9B7D5fD71595d0e18;
  uint256[] memory amounts = router.getAmountsIn(amountOut, path);
  return amounts[0] + ((amounts[0] * percent) / 100);
}

Because the quote came from the public WETH/BTC20 Uniswap V2 pair at 0xd50c5b8f04587d67298915e099e170af3cd6909a, any attacker with temporary capital could move the reserves intra-transaction and make BTC20 appear cheaper in WETH terms before purchasing from the presale.

3. Vulnerability Analysis & Root Cause Summary

This incident is an ATTACK, not passive MEV. The presale sold fixed inventory using a manipulable AMM spot quote as the authoritative sale price. That violates a core issuance invariant: the amount of ETH required to buy a fixed amount of BTC20 should not be reducible by first moving a public trading pool that the presale itself reads as its oracle.

The code-level breakpoint is PresaleV4.fetchPrice() feeding directly into buyWithEthDynamic(). buyWithEthDynamic() computes ethAmount = fetchPrice(amount * baseDecimals), accepts any msg.value >= ethAmount, forwards only the quoted ETH amount to the payment wallet, refunds the excess, and then transfers the BTC20 inventory to the buyer. The function therefore trusts a reserve-dependent spot quote at the exact moment of execution.

The seed trace confirms that the attacker manipulated the WETH/BTC20 market before the presale buy, causing getAmountsIn(62516000000000000000000, [WETH, BTC20]) to return only 11614285593821339252 wei before the premium was added. The subsequent payment transfer to the presale payment wallet was 12194999873512406214 wei, exactly matching the amount claimed in the root cause.

4. Detailed Root Cause Analysis

The ACT opportunity exists in the Ethereum pre-state immediately before block 17949215, with dynamic sale mode enabled, sale inventory present in the presale, public liquidity in the hard-coded WETH/BTC20 path, and permissionless flash liquidity available from Balancer. No privileged keys or private flow were required.

The seed trace shows the end-to-end exploit mechanism:

0xBA1222...::flashLoan(... [300 WETH] ...)
  0xDb81...::flash(... 76301042059171907852637 BTC20 ...)
    0x7234...::flash(... 47676018750296374476872 BTC20 ...)
      UniswapV2Router02::swapExactTokensForTokens(123977060809468282329509 BTC20 -> WETH)
      0x1F006F...::buyWithEthDynamic{value: 300 ETH}(62516)
        UniswapV2Router02::getAmountsIn(62516000000000000000000, [WETH, BTC20])
        0x58178A...::fallback{value: 12194999873512406214}()
        BTC20Token::transfer(attacker, 62516000000000000000000)

This shows the crucial sequence. First, the attacker borrowed 300 WETH from Balancer and nested BTC20 flash borrows from the public concentrated-liquidity pools at 0xdb81... and 0x7234.... Next, the attacker sold 123977060809468282329509 BTC20 into the public Uniswap V2 market, materially changing the WETH/BTC20 reserves that fetchPrice() would read. Then the attacker called the public presale function and bought 62,516 BTC20 while the spot quote was distorted.

The relevant victim code path confirms why this underpayment was accepted:

function buyWithEthDynamic(uint256 amount) external payable whenNotPaused nonReentrant returns (bool) {
  require(dynamicSaleState, "dynamic sale not active");
  require(amount <= maxTokensToSell - directTotalTokensSold, "amount exceeds max tokens to be sold");
  directTotalTokensSold += amount;
  uint256 ethAmount = fetchPrice(amount * baseDecimals);
  require(msg.value >= ethAmount, "Less payment");
  uint256 excess = msg.value - ethAmount;
  sendValue(payable(paymentWallet), ethAmount);
  if (excess > 0) sendValue(payable(_msgSender()), excess);
  bool success = IERC20Upgradeable(saleToken).transfer(_msgSender(), (amount * baseDecimals));
  require(success, "Token transfer failed");
  emit TokensBought(_msgSender(), amount, address(0), ethAmount, 0, block.timestamp);
  return true;
}

The trace and balance diff align with the exploit result. The presale lost exactly 62516000000000000000000 BTC20, and the payment wallet 0x58178a119781df139307572066d5e74704809861 gained exactly 12194999873512406214 wei. After receiving the underpriced BTC20, the attacker unwound the position across the public pools at 0xd50c..., 0xdb81..., and 0xc7cb..., repaid the flash loans, and transferred the remaining profit back to the originating EOA.

5. Adversary Flow Analysis

The adversary cluster consisted of EOA 0x6ce9fa08f139f5e48bc607845e57efe9aa34c9f6 and helper contract 0xb7fbf984a50cd7c66e6da3448d68d9f3b7f24f33. The entire exploit was realized in a single adversary-crafted transaction.

  1. Flash-loan funding: Balancer vault 0xba12222222228d8ba445958a75a0704d566bf2c8 transferred 300 WETH to the attacker helper.
  2. Market manipulation: the attacker borrowed additional BTC20 from the two concentrated-liquidity pools and sold 123977060809468282329509 BTC20 into the public Uniswap V2 pair 0xd50c5b8f04587d67298915e099e170af3cd6909a, making BTC20 appear cheaper in WETH terms.
  3. Underpriced primary purchase: the attacker called buyWithEthDynamic(62516) on the presale proxy. The trace shows the manipulated getAmountsIn quote, the payment-wallet transfer of 12194999873512406214 wei, the refund of the excess ETH, and the transfer of 62516000000000000000000 BTC20 to the attacker.
  4. Unwind and profit extraction: the attacker bought back enough BTC20 on V2 to repay the two nested BTC20 flash loans, then sold the residual presale inventory through public V2 and V3 liquidity, repaid the 300 WETH Balancer flash loan, unwrapped the remaining WETH, and transferred 18348059243787394323 wei to the EOA.

The ACT framing is satisfied because every component in the sequence was permissionless: Balancer flash loans, the public Uniswap V2/V3 pools, and the presale's public purchase function.

6. Impact & Losses

The presale transferred 62,516 BTC20 to the attacker at a manipulated price. The direct token loss recorded in the incident artifacts is:

  • BTC20: 62516000000000000000000 raw units (62,516 BTC20, 18 decimals)

The payment wallet received only 12.194999873512406214 ETH for that inventory, while the attacker extracted net profit. The balance-diff artifact shows the attacker EOA increased from 2.020951833551046263 ETH to 20.355808020094199306 ETH, for a net delta of 18.334856186543153043 ETH after gas and miner payment.

7. References

  • Seed transaction: 0xcdd93e37ba2991ce02d8ca07bf6563bf5cd5ae801cbbce3dd0babb22e30b2dbe
  • Presale proxy: 0x1f006f43f57c45ceb3659e543352b4fae4662df7
  • Presale implementation: 0xdb0618e0b850cab3f756d030398be22929226d5c
  • Payment wallet: 0x58178a119781df139307572066d5e74704809861
  • Manipulated Uniswap V2 pool: 0xd50c5b8f04587d67298915e099e170af3cd6909a
  • Balancer vault: 0xba12222222228d8ba445958a75a0704d566bf2c8
  • Verified victim source: https://repo.sourcify.dev/contracts/full_match/1/0xdb0618e0b850cab3f756d030398be22929226d5c/sources/contracts/ETH/PresaleV4.sol
  • Collected evidence: seed transaction metadata, trace, and balance diff under /workspace/session/artifacts/collector/seed/1/0xcdd93e37ba2991ce02d8ca07bf6563bf5cd5ae801cbbce3dd0babb22e30b2dbe/