All incidents

NEU Convert LP Mispricing Drain

Share
Aug 01, 2023 19:55 UTCAttackLoss: 23.93 aeWETHPending manual check2 exploit txWindow: 3m 45s
Estimated Impact
23.93 aeWETH
Label
Attack
Exploit Tx
2
Addresses
2
Attack Window
3m 45s
Aug 01, 2023 19:55 UTC → Aug 01, 2023 19:59 UTC

Exploit Transactions

TX 1Arbitrum
0x4778f44af159252de89c998f6cb2bf824e2bdab23201152c14b2b4df091249e4
Aug 01, 2023 19:55 UTCExplorer
TX 2Arbitrum
0x6301d4c9f7ac1c96a65e83be6ea2fff5000f0b1939ad24955e40890bd9fe6122
Aug 01, 2023 19:59 UTCExplorer

Victim Addresses

0xdbd3d6040f87a9f822839cb31195ad25c2d0d54dArbitrum
0x2ea3ca79413c2ec4c1893d5f8c34c16acb2288a4Arbitrum

Loss Breakdown

23.93aeWETH

Similar Incidents

Root Cause Analysis

NEU Convert LP Mispricing Drain

1. Incident Overview TL;DR

On Arbitrum block 117189139, attacker EOA 0x39b842bb7287afc516db176b76c5cbbd8caa1e84 called its previously deployed helper contract 0x44d1cce6841502b832cf5ba636a38cadf9b70c69, borrowed 1000 aeWETH from the Balancer Vault, manipulated the reserve composition of Camelot pair 0x65ebc8cfd2af1d659ef2405a47172830180ba440, converted the resulting LP through Convert at 0xdbd3d6040f87a9f822839cb31195ad25c2d0d54d, burned the overpriced output LP from Camelot pair 0x2ea3ca79413c2ec4c1893d5f8c34c16acb2288a4, and exited with 23.572400220627017010 ETH net profit after gas.

The root cause is a deterministic accounting bug in Convert. Its getAmountOut function values LP tokens from only the shared pairToken reserve side and ignores the opposite reserve entirely. Because Camelot reserves are spot-state and can be moved cheaply with a flash-loan-funded swap, an unprivileged attacker could temporarily inflate the input LP's aeWETH-per-LP ratio, obtain far too many output LP tokens from Convert, redeem those LP tokens for both reserves of the output pool, and realize profit immediately.

2. Key Background

Convert is configured with one shared pairToken, here aeWETH at 0x82af49447d8a07e3bd95bd0d56f35241523fbab1, and two Camelot LP tokens: input pair 0x65ebc8cfd2af1d659ef2405a47172830180ba440 and output pair 0x2ea3ca79413c2ec4c1893d5f8c34c16acb2288a4. Both LPs share aeWETH on one side but pair it against different NEU tokens: 0xda51015b73ce11f77a115bb1b8a7049e02ddecf0 for the input pool and 0x6609be1547166d1c4605f3a243fdcff467e600c3 for the output pool.

Camelot LP tokens represent claims on both reserves of their pool. Any exchange rate that tracks only one reserve side is incomplete: it can diverge from the LP's true redeemable value whenever the pool becomes imbalanced. That matters here because the attacker can move spot reserves within one transaction by borrowing aeWETH from Balancer and swapping against the input pool.

The profit path is fully public. Balancer flash loans, Camelot swaps and liquidity adds, Convert.convert, LP burns, and aeWETH withdrawal are all permissionless. No privileged key, whitelist, or private orderflow was required.

3. Vulnerability Analysis & Root Cause Summary

The vulnerable contract is the verified Convert contract at 0xdbd3d6040f87a9f822839cb31195ad25c2d0d54d. Its convert function transfers input LP from the caller, computes amountOut = getAmountOut(_amount), and transfers output LP to the caller if inventory is available. The pricing error is entirely inside getAmountOut.

The verified source shows that getAmountOut first computes the input LP's pro-rata claim on only the pairToken reserve. It then converts that single-reserve quantity into output LP units using only the matching pairToken reserve in the output pool:

function getAmountOut(uint256 _amount) view public returns(uint256) {
    (uint256 reserves0, uint256 reserves1,,) = inputToken.getReserves();
    uint256 inputTotalSupply = inputToken.totalSupply();

    uint256 pairTokenAmount;
    if (pairToken == inputToken.token0()) {
        pairTokenAmount = reserves0 * _amount / inputTotalSupply;
    } else {
        pairTokenAmount = reserves1 * _amount / inputTotalSupply;
    }

    (uint256 reserves2, uint256 reserves3,,) = outputToken.getReserves();
    uint256 outputTotalSupply = outputToken.totalSupply();

    if (pairToken == outputToken.token0()) {
        return outputTotalSupply * pairTokenAmount / reserves2;
    } else {
        return outputTotalSupply * pairTokenAmount / reserves3;
    }
}

The violated invariant is straightforward: converting one LP token into another should conserve full economic value, not just one reserve leg. Convert breaks that invariant by ignoring the non-aeWETH reserve on both the input and output side. Once the attacker skewed the input pool so each input LP appeared to contain much more aeWETH than normal, Convert over-issued output LP that still entitled the attacker to both aeWETH and NEU when burned.

4. Detailed Root Cause Analysis

The trace for tx 0x6301d4c9f7ac1c96a65e83be6ea2fff5000f0b1939ad24955e40890bd9fe6122 shows the decisive pre-conversion state. Right before Convert.convert(7390794092629250793), the input pair reserves were 858676390114419906659 aeWETH and 282138757097417557333 NEU, with total supply 476774712637538175318. The output pair reserves were 340914987794845576869846 NEU and 66560708590370878620 aeWETH, with total supply 4759121671008920535185.

Applying the verified Convert formula to that traced state reproduces the exploit outcome exactly:

pairTokenAmount = 858676390114419906659 * 7390794092629250793 / 476774712637538175318
                = 13310899725427673116

amountOut = 4759121671008920535185 * 13310899725427673116 / 66560708590370878620
          = 951735531148986388193

That is the exact amountOut emitted by Convert in the trace:

Convert::convert(7390794092629250793)
...
emit Converted(
  receiver: 0x44d1cCE6841502b832cf5ba636A38CaDf9B70C69,
  amountInt: 7390794092629250793,
  amountOut: 951735531148986388193
)

This output LP amount was grossly favorable because the output pool had relatively little aeWETH reserve per LP unit, while the input pool had just been manipulated to show a very high aeWETH reserve per LP unit. Convert treated that temporary aeWETH concentration as if it represented the whole value of the input LP, even though the attacker had simultaneously stripped most of the other side of the pool. The bug is therefore not “flash loans are dangerous”; the flash loan only makes the transient reserve distortion capital-efficient. The underlying flaw is the one-sided LP valuation rule.

5. Adversary Flow Analysis

The adversary sequence contains two transactions:

  1. 0x4778f44af159252de89c998f6cb2bf824e2bdab23201152c14b2b4df091249e4
  2. 0x6301d4c9f7ac1c96a65e83be6ea2fff5000f0b1939ad24955e40890bd9fe6122

The first transaction deployed helper contract 0x44d1cce6841502b832cf5ba636a38cadf9b70c69. Etherscan contract-creation metadata identifies the deploying EOA as 0x39b842bb7287afc516db176b76c5cbbd8caa1e84, so the EOA and helper contract form one adversary-controlled cluster.

The second transaction executed the exploit. The helper contract borrowed 1000 aeWETH from the Balancer Vault, swapped 0.15 aeWETH into the input NEU token, added liquidity to mint 7390794092629250793 input LP, and then spent 849 aeWETH in another Camelot swap that pushed the input pool into a reserve state favorable to the bug. The trace then shows Convert.convert(7390794092629250793), followed by transfer of 951735531148986388193 output LP to the attacker helper.

The helper next burned those output LP tokens and received both sides of the output pool: 68157547064697183305504 units of output NEU and 13307172728756127953 aeWETH. It then swapped the output NEU back into aeWETH, repaid the Balancer flash loan principal, withdrew the remaining aeWETH into ETH, and forwarded the ETH to the attacker EOA.

Representative trace fragment:

Convert::convert(7390794092629250793)
...
CamelotPair::transfer(0x44d1..., 951735531148986388193)
...
CamelotPair::burn(0x44d1...)
...
NEU::transfer(0x44d1..., 68157547064697183305504)
aeWETH::transfer(0x44d1..., 13307172728756127953)

Everything in this flow was permissionless and executed in one transaction after contract deployment. That is sufficient to classify the incident as ACT.

6. Impact & Losses

The balance-diff artifact shows that the output Camelot aeWETH/NEU pool lost 23928334462162097885 units of aeWETH during the exploit transaction:

{
  "token": "0x82af49447d8a07e3bd95bd0d56f35241523fbab1",
  "holder": "0x2ea3ca79413c2ec4c1893d5f8c34c16acb2288a4",
  "before": "66560708590370878620",
  "after": "42632374128208780735",
  "delta": "-23928334462162097885"
}

The attacker EOA's native balance increased by 23572400220627017010 wei after gas, matching the reported 23.572400220627017010 ETH realized profit. Convert also lost nearly all of its output LP inventory, falling from 951992684839607319881 output LP to 257153690620931688, which is the mechanism by which protocol-held inventory was monetized.

7. References

  • Seed exploit transaction metadata: 0x6301d4c9f7ac1c96a65e83be6ea2fff5000f0b1939ad24955e40890bd9fe6122
  • Seed exploit trace: Balancer flash loan, Camelot swaps, Convert.convert, output LP burn, and unwind sequence for tx 0x6301d4c9f7ac1c96a65e83be6ea2fff5000f0b1939ad24955e40890bd9fe6122
  • Seed balance diff: attacker profit and output-pool aeWETH loss for tx 0x6301d4c9f7ac1c96a65e83be6ea2fff5000f0b1939ad24955e40890bd9fe6122
  • Verified Convert source from Etherscan V2 for 0xdbd3d6040f87a9f822839cb31195ad25c2d0d54d
  • Etherscan V2 contract creation metadata linking attacker EOA 0x39b842bb7287afc516db176b76c5cbbd8caa1e84 to helper contract 0x44d1cce6841502b832cf5ba636a38cadf9b70c69