ElasticSwap Rebase Exploit
Exploit Transactions
0xb36486f032a450782d5d2fac118ea90a6d3b08cac3409d949c59b43bcd6dbb8fVictim Addresses
0xa0c5aa50ce3cc69b1c478d8235597bc0c51dfdabEthereumLoss Breakdown
Similar Incidents
HATE Rebase-Before-Burn Exploit
37%QWAStaking Rebase Capture
35%QUATERNION Pair-Rebase Accounting Drift Enables Permissionless Drain
35%EFVault Withdraw Under-Burn
34%Kyber Elastic Tick Rounding Exploit
31%Orion Pool Double-Count Exploit
31%Root Cause Analysis
ElasticSwap Rebase Exploit
1. Incident Overview TL;DR
ElasticSwap's AMPL/USDC exchange on Ethereum mainnet was exploited in transaction 0xb36486f032a450782d5d2fac118ea90a6d3b08cac3409d949c59b43bcd6dbb8f at block 16172820. The attacker used an EOA at 0xbeadedbabed6a353c9caa4894aa7e5f883e32967 and an attacker-controlled helper contract at 0xe911519dc7f35996c6ad5c8a53e82b101af790d6 to borrow AMPL and USDC via nested Uniswap V2 flash swaps, route those assets through ElasticSwap's liquidity functions, and monetize the drained inventory into ETH.
The root cause is a stale-accounting bug in ElasticSwap's rebase-down liquidity-addition path. During AMPL negative-rebase decay handling, the protocol mints LP for the decay-fixing base-token contribution without updating internalBalances, then immediately prices a paired liquidity addition against those stale reserves. That over-mints LP shares, and removeLiquidity later redeems those inflated shares against the pool's real token balances, allowing extraction of excess AMPL and USDC.
2. Key Background
ElasticSwap is designed for elastic-supply base assets such as AMPL. Instead of relying only on raw ERC-20 balances, the pool tracks intended reserves in internalBalances. That design matters because an AMPL negative rebase reduces the pool's external AMPL balance while leaving the internally tracked reserve untouched until the decay is explicitly repaired.
In the incident pre-state, the ElasticSwap AMPL/USDC pool at 0xa0c5aa50ce3cc69b1c478d8235597bc0c51dfdab had internalBalances.baseTokenReserveQty = 347616080868225 while the external AMPL balance was 164339211948719. The gap was 183276868919506 raw AMPL units. That mismatch was publicly observable through on-chain reads, so any unprivileged searcher could detect it and construct a transaction to repair the decay and then exploit the stale reserve state before accounting caught up.
The attack required no privileged keys, no attacker-only contracts from the victim side, and no non-public data. It relied on public pool state, public Uniswap V2 liquidity, and ordinary transaction inclusion rules, which makes the opportunity ACT rather than a privileged compromise.
3. Vulnerability Analysis & Root Cause Summary
The vulnerable logic is in ElasticSwap's AMPL rebase-down branch inside MathLib.calculateAddLiquidityQuantities. When the pool is short base token because of a negative rebase, the code first calls calculateAddBaseTokenLiquidityQuantities to determine the base-token-only contribution needed to repair the decay and to mint LP for that contribution. The crucial flaw is that this helper explicitly does not update internalBalances, even though control later returns to the paired-liquidity path with a comment assuming those balances were already updated.
That assumption is false in the rebase-down branch. As a result, the follow-on paired AMPL and USDC deposit is quoted against stale internal reserves instead of the economically correct repaired state. The attacker can then receive more LP than the deposit should buy, redeem those LP tokens against actual pool balances, and withdraw excess AMPL and USDC. The exploit remains economically viable because temporary capital can be sourced permissionlessly through flash swaps, and the loss can be realized in a single transaction.
4. Detailed Root Cause Analysis
The core invariant is that every LP mint must be priced from one coherent reserve state. The reserve state used for LP issuance and the reserve state reflected in internal accounting must match before any further pricing step occurs.
The first relevant source snippet is the decay-repair helper in MathLib.sol. It documents that no internal state is updated while still calculating LP to mint:
// Origin: ElasticSwap MathLib.calculateAddBaseTokenLiquidityQuantities
// we are not changing anything about our internal accounting here. We are simply adding tokens
// to make our internal account "right"...or rather getting the external balances to match our internal
// quoteTokenReserveQty += quoteTokenQtyDecayChange;
// baseTokenReserveQty += baseTokenQty;
liquidityTokenQty = calculateLiquidityTokenQtyForSingleAssetEntryWithQuoteTokenDecay(
_baseTokenReserveQty,
_totalSupplyOfLiquidityTokens,
baseTokenQty,
_internalBalances.baseTokenReserveQty
);
The second relevant snippet is the caller in calculateAddLiquidityQuantities. It reuses the same internalBalances for paired-liquidity pricing and includes an incorrect comment:
// Origin: ElasticSwap MathLib.calculateAddLiquidityQuantities
(
tokenQtys.baseTokenQty,
tokenQtys.quoteTokenQty,
tokenQtys.liquidityTokenQty
) = calculateAddTokenPairLiquidityQuantities(
_baseTokenQtyDesired - baseTokenQtyFromDecay,
_quoteTokenQtyDesired - quoteTokenQtyFromDecay,
0,
0,
_totalSupplyOfLiquidityTokens + liquidityTokenQtyFromDecay,
_internalBalances // NOTE: these balances have already been updated when we did the decay math.
);
That comment is wrong for the rebase-down branch shown above. The stale-accounting effect becomes exploitable because Exchange.removeLiquidity later calculates redemption against actual token balances:
// Origin: ElasticSwap Exchange.removeLiquidity
uint256 baseTokenReserveQty = IERC20(baseToken).balanceOf(address(this));
uint256 quoteTokenReserveQty = IERC20(quoteToken).balanceOf(address(this));
uint256 baseTokenQtyToReturn =
(_liquidityTokenQty * baseTokenReserveQty) / totalSupplyOfLiquidityTokens;
uint256 quoteTokenQtyToReturn =
(_liquidityTokenQty * quoteTokenReserveQty) / totalSupplyOfLiquidityTokens;
The seed transaction trace shows the attacker driving exactly this path after borrowing flash liquidity:
Origin: seed transaction trace
UniswapV2Pair::swap(0, 944686398050649, attacker, ...)
attacker::uniswapV2Call(...)
UniswapV2Pair::swap(2834059194151, 0, attacker, ...)
attacker::uniswapV2Call(...)
Exchange::internalBalances()
Exchange::addLiquidity(183277868919506, 0, 0, 0, attacker, 1000000000000)
Exchange::addLiquidity(164204588455430, 249072677422, 0, 0, attacker, 1000000000000)
Exchange::removeLiquidity(16657566971382, 1, 1, attacker, 1000000000000)
Exchange::swapQuoteTokenForBaseToken(20470265200, 1, 1000000000000)
Exchange::addLiquidity(1106478567741571, 2564311548877, 0, 0, attacker, 1000000000000)
Exchange::removeLiquidity(2069682864518885, 1, 1, attacker, 1000000000000)
The first addLiquidity call closely matches the observed AMPL decay gap. The later paired adds and removals are the extraction steps: they mint LP against stale internal reserves and then redeem against actual balances. The trace also shows a follow-on swapQuoteTokenForBaseToken, which amplifies the extraction before final liquidation.
5. Adversary Flow Analysis
The attacker flow is a single transaction with two nested flash swaps. First, the helper contract borrows 944686398050649 raw AMPL from the Uniswap V2 AMPL/WETH pair at 0xc5be99a02c6857f9eac67bbce58df5572498f40c. Inside that callback, it borrows 2834059194151 raw USDC from the Uniswap V2 USDC/USDT pair at 0x3041cbd36888becc7bbcbc0045e3b1f144466f5f.
With those temporary balances, the helper reads Exchange::internalBalances(), performs a base-only liquidity addition to repair the rebase-down decay, then immediately performs a paired AMPL/USDC add while the protocol still prices against stale internalBalances. The helper transfers extra USDC directly into the exchange, removes inflated LP shares, swaps quote token for base token, and repeats the add/remove pattern to extract more inventory.
The final realization phase converts the drained assets into ETH-denominated profit. The root cause artifacts report that the attacker sold 152345306710415 raw AMPL and 472054335900 raw USDC for WETH/ETH, paid a builder 78534059333097925500 wei, and still retained 445014074247614272344 wei net.
The balance-diff evidence confirms the realized losses at the victim pool and the final profit at the attacker EOA:
// Origin: seed transaction balance diff
{
"address": "0xbeadedbabed6a353c9caa4894aa7e5f883e32967",
"delta_wei": "445014074247614272344"
}
{
"token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"holder": "0xa0c5aa50ce3cc69b1c478d8235597bc0c51dfdab",
"delta": "-500394927842"
}
{
"token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"holder": "0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc",
"delta": "472054335900"
}
Those diffs line up with the exploit story: USDC leaves ElasticSwap, USDC reaches the monetization venue, and the attacker EOA finishes with a large positive ETH delta.
6. Impact & Losses
The directly measured victim-side losses in the collected artifacts are:
500394927842raw USDC units (500394.927842 USDC,decimal=6)161792170690922raw AMPL units (161792.170690922 AMPL,decimal=9)
The pool affected is ElasticSwap's AMPL/USDC exchange at 0xa0c5aa50ce3cc69b1c478d8235597bc0c51dfdab. The incident drained both sides of the pool, not just the rebasing token. Because the exploit monetized the drained assets in the same transaction, the attacker's realized profit was approximately 445.014074247614272344 ETH net to the profit-taking EOA, with an additional builder payment of 78.5340593330979255 ETH visible in the native balance diff.
7. References
- Seed transaction metadata for
0xb36486f032a450782d5d2fac118ea90a6d3b08cac3409d949c59b43bcd6dbb8f, including sender0xbeadedbabed6a353c9caa4894aa7e5f883e32967and target0xe911519dc7f35996c6ad5c8a53e82b101af790d6. - Seed opcode trace for the same transaction, showing nested Uniswap flash swaps and the exact
Exchange::addLiquidity,Exchange::removeLiquidity, andExchange::swapQuoteTokenForBaseTokensequence. - Seed balance diff for the same transaction, showing
-500394927842USDC at the ElasticSwap exchange and+445014074247614272344wei at the attacker EOA. - ElasticSwap
MathLib.sol, especiallycalculateAddBaseTokenLiquidityQuantitiesandcalculateAddLiquidityQuantities. - ElasticSwap
Exchange.sol, especiallyaddLiquidity,removeLiquidity, andswapQuoteTokenForBaseToken.