Platypus PoolSAvax JIT LP Exploit
Exploit Transactions
0x4425f757715e23d392cda666bc0492d9e5d5848ff89851a1821eab5ed12bb867Victim Addresses
0x4658ea7e9960d6158a261104aaa160cc953bb6baAvalanche0xc73eed4494382093c6a7c284426a9a00f6c79939Avalanche0xa2a7ee49750ff12bb60b407da2531db3c50a1789AvalancheLoss Breakdown
Similar Incidents
Platypus LP Cross-Asset Mispricing
45%Platypus Stale Collateral Withdrawal
38%Stars Arena Callback Weight Reentrancy
27%Public Curve Treasury Drain
19%Metalend Empty-Market Donation Exploit
19%Conic crvUSD Oracle Exploit
19%Root Cause Analysis
Platypus PoolSAvax JIT LP Exploit
1. Incident Overview TL;DR
At Avalanche block 36346398, transaction 0x4425f757715e23d392cda666bc0492d9e5d5848ff89851a1821eab5ed12bb867 deployed an attacker contract, borrowed WAVAX and sAVAX from Aave V3, deposited both assets into Platypus PoolSAvax, oscillated pool coverage with self-swaps, withdrew the fresh LP positions, repaid Aave, and retained residual WAVAX and sAVAX. The measured post-repayment residue was 23564213231457049370005 WAVAX plus 20873980832851904983452 sAVAX, worth 46722526443451349008651 wei of AVAX-equivalent before the external gas spend borne by the sender.
The root cause is deterministic and protocol-side: the PoolSAvax fork removed impairment loss/gain from LP mint and burn accounting. Both pool legs were already under-covered before the exploit, but fresh deposits still minted LP against raw liability, and same-tx withdrawals redeemed against the same raw liability share. Because swaps in the same pool still paid coverage-based rebalancing premia, the attacker could insert just-in-time liquidity, harvest those premia, and exit before internalizing the legacy impairment that incumbent LPs were already carrying.
2. Key Background
PoolSAvax is a dedicated Platypus pool for WAVAX and sAVAX. Each token has its own asset proxy that tracks cash, liability, and LP totalSupply, while the pool contract handles deposit, swap, and withdrawal logic. The collected pre-state snapshot immediately before the exploit shows that both legs were already impaired:
{
"wavax_asset": {
"cash": "324928728985689853475724",
"liability": "492988996699770702359279",
"total_supply": "492988996699770702359279"
},
"savax_asset": {
"cash": "401999492918738285450055",
"liability": "417291207895764839925361",
"total_supply": "417291207895764839925361"
}
}
Those numbers matter for two reasons. First, both cash / liability ratios were below 1, so each leg was already carrying impairment. Second, totalSupply == liability on both legs, so if deposit and withdrawal logic use only raw liability share, the LP is minted and redeemed at stale par instead of current net asset value.
The sAVAX leg also depends on the staking exchange rate exposed by the verified StakedAvax contract. The collected source shows:
function getPooledAvaxByShares(uint shareAmount) public view returns (uint) {
if (totalShares == 0) return 0;
return shareAmount.mul(totalPooledAvax).div(totalShares);
}
At block 36346397, getPooledAvaxByShares(1e18) returned 1109445934083492461, so each sAVAX represented more than one AVAX. That rate is relevant to profit valuation and confirms that the residual sAVAX balance carried material AVAX-equivalent value.
3. Vulnerability Analysis & Root Cause Summary
This incident is an ATTACK-class ACT exploit against Platypus PoolSAvax, not a privileged compromise. The core invariant is that when an asset leg is impaired, newly minted and burned LP must internalize that impairment; otherwise a same-block entrant can obtain a full legacy LP claim without paying for the embedded deficit. The collected verified PoolSAvax excerpts show that this fork explicitly removed impairment loss/gain from deposit and withdrawal accounting. As a result, deposits minted LP from amount, totalSupply, and liability, and withdrawals burned liability share from the same raw accounting base. The same code path still allowed swaps to change coverage and pay rebalancing premia, so temporary LP could capture value that belonged to existing LPs. Because the full sequence used only public flashLoan, deposit, swap, and withdraw entrypoints, any unprivileged actor with temporary liquidity could realize the opportunity.
4. Detailed Root Cause Analysis
The verified PoolSAvax notes collected during analysis identify the implementation behind proxy 0x4658ea7e9960d6158a261104aaa160cc953bb6ba as 0xe5c84c7630a505b6adf69b5594d0ff7fedd5f447 and record the customization comment:
// Changes: removed impairment loss/gain on withdrawals/deposits
The same notes extract the deposit path:
function _deposit(Asset asset, uint256 amount, address to) private returns (uint256 liquidity) {
uint256 totalSupply = asset.totalSupply();
uint256 liability = asset.liability();
uint256 fee = _depositFee(..., asset.cash(), liability, amount);
if (liability == 0) {
liquidity = amount - fee;
} else {
liquidity = ((amount - fee) * totalSupply) / liability;
}
asset.addCash(amount);
asset.addLiability(amount - fee);
asset.mint(to, liquidity);
}
Because both impaired legs had totalSupply == liability immediately before the attack, that formula minted LP one-for-one with the raw deposited amount. No term in the formula charged the entrant for the pre-existing shortfall between cash and liability.
The withdrawal path carried the same flaw:
function _withdrawFrom(Asset asset, uint256 liquidity) private view returns (...) {
liabilityToBurn = (asset.liability() * liquidity) / asset.totalSupply();
fee = _withdrawalFee(..., asset.cash(), asset.liability(), liabilityToBurn);
...
}
Here again, withdrawal value is derived from liability share rather than from impaired net asset value. The fork therefore let the attacker redeem the fresh LP against the same stale accounting basis that was used on entry.
Swaps remained valuable because the pool still used coverage-sensitive pricing:
(actualToAmount, haircut) = _quoteFrom(fromAsset, toAsset, fromAmount);
fromAsset.addCash(fromAmount);
toAsset.removeCash(actualToAmount);
toAsset.addLiability(_dividend(haircut, _retentionRatio));
toAsset.transferUnderlyingToken(to, actualToAmount);
That combination is the breakpoint. Deposits and withdrawals ignored impairment, while swaps still paid coverage-driven premia. The attacker could therefore become a temporary LP, move pool coverage back and forth with self-swaps, and exit with more value than was economically justified by the impaired pool state.
The exploit trace and decoded sequence show the exact realization:
1. flashLoan(WAVAX=1054969334265171860090656, sAVAX=950996381159237599003939)
2. deposit(WAVAX, 1054969334265171860090656) -> LP minted: 1054969334265171860090656
3. deposit(sAVAX, 316998793719745866334646) -> LP minted: 316998793719745866334646
4. swap(sAVAX -> WAVAX, 600000000000000000000000) -> 658669609416142389471788 WAVAX
5. withdraw(WAVAX LP, 1020000000000000000000000) -> 721228453834719324094592 WAVAX
6. swap(WAVAX -> sAVAX, 1200000000000000000000000) -> 1221350685348699013961361 sAVAX
7. withdraw(WAVAX LP, 34969334265171860090656) -> 34969334265171860090656 WAVAX
8. swap(sAVAX -> WAVAX, 600000000000000000000000) -> 864193634647727921733670 WAVAX
9. withdraw(sAVAX LP, 316998793719745866334646) -> 316997587394478376156239 sAVAX
This is fully consistent with the measured state changes. The seed balance diff shows that the attacker contract 0x44e251786a699518d6273ea1e027cec27b49d3bd finished with positive balances in both assets, while the victim asset proxies lost cash:
{
"attacker_wavax_delta": "23564213231457049370005",
"attacker_savax_delta": "20873980832851904983452",
"wavax_asset_cash_delta": "-24091697898589635300050",
"savax_asset_cash_delta": "-21349479023431523782954"
}
The exploit is ACT by construction. The sender EOA 0x0cd4fd0eecd2c5ad24de7f17ae35f9db6ac51ee7 used only public Avalanche state and public protocol contracts: Aave V3 flash liquidity and Platypus deposit, swap, and withdraw entrypoints. No privileged key, admin action, or private attacker-only artifact was required.
5. Adversary Flow Analysis
The adversary flow is a single public transaction with three stages.
First, the attacker contract received a dual-asset Aave V3 flash loan. The trace shows AaveV3Pool::flashLoan transferring 1054969334265171860090656 WAVAX and 950996381159237599003939 sAVAX into the freshly created attacker contract.
Second, the attacker inserted just-in-time liquidity into both impaired legs. The trace shows deposits of all borrowed WAVAX and 316998793719745866334646 sAVAX into PoolSAvax, and the pool minted exactly the same raw LP amounts as the deposits. That one-to-one minting is the observable manifestation of the stale liability accounting.
Third, the attacker harvested rebalancing premia and exited. The two sAVAX -> WAVAX swaps and the intermediate WAVAX -> sAVAX swap moved coverage across the two legs, while two WAVAX withdrawals and one sAVAX withdrawal burned the temporary LP and extracted underlying tokens. After approving Aave for amount + premium, the attacker contract repaid the flash loan and retained the residual balances.
The concrete adversary-related accounts are:
- EOA
0x0cd4fd0eecd2c5ad24de7f17ae35f9db6ac51ee7, which submitted the exploit transaction and paid239951150000000000wei of native AVAX gas. - Contract
0x44e251786a699518d6273ea1e027cec27b49d3bd, created and executed within the transaction, which held the flash-loaned assets, interacted with Platypus, and kept the profit residue after repayment.
6. Impact & Losses
The incident directly depleted both Platypus asset proxies in the pool:
- WAVAX asset proxy
0xc73eed4494382093c6a7c284426a9a00f6c79939lost24091697898589635300050wei of WAVAX. - sAVAX asset proxy
0xa2a7ee49750ff12bb60b407da2531db3c50a1789lost21349479023431523782954wei of sAVAX.
Part of the extracted value paid Aave flash-loan premiums and part remained as attacker profit. The post-repayment residue retained by the attacker contract was 23564213231457049370005 WAVAX plus 20873980832851904983452 sAVAX. Using the collected pre-state staking rate 1109445934083492461, the combined residue equaled 46722526443451349008651 wei of AVAX-equivalent value. The transaction sender separately paid 239951150000000000 wei of native AVAX for gas.
The affected public protocol components were:
- Platypus
PoolSAvaxproxy0x4658ea7e9960d6158a261104aaa160cc953bb6ba - Platypus WAVAX asset proxy
0xc73eed4494382093c6a7c284426a9a00f6c79939 - Platypus sAVAX asset proxy
0xa2a7ee49750ff12bb60b407da2531db3c50a1789
7. References
- Exploit transaction:
0x4425f757715e23d392cda666bc0492d9e5d5848ff89851a1821eab5ed12bb867on Avalanche block36346398 - Exploit sender EOA:
0x0cd4fd0eecd2c5ad24de7f17ae35f9db6ac51ee7 - Exploit contract:
0x44e251786a699518d6273ea1e027cec27b49d3bd - Platypus
PoolSAvaxproxy:0x4658ea7e9960d6158a261104aaa160cc953bb6ba - Verified
PoolSAvaximplementation identified in the collected trace and code notes:0xe5c84c7630a505b6adf69b5594d0ff7fedd5f447 - Platypus WAVAX asset proxy:
0xc73eed4494382093c6a7c284426a9a00f6c79939 - Platypus sAVAX asset proxy:
0xa2a7ee49750ff12bb60b407da2531db3c50a1789 - WAVAX token:
0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7 - sAVAX token / StakedAvax proxy:
0x2b2c81e08f1af8835a78bb2a90ae924ace0ea4be - Aave V3 pool:
0x794a61358d6845594f94dc1db02a252b5b4814ad - Evidence reviewed: pre-state snapshot, verified victim code excerpts, verified StakedAvax source, full seed transaction trace, decoded call sequence, and ERC-20/native balance diffs collected for the seed transaction