Palm PLP AUM Inflation
Exploit Transactions
0x62dba55054fa628845fecded658ff5b1ec1c5823f1a5e0118601aa455a30eac9Victim Addresses
0x806f709558cdbba39699fbf323c8fda4e364ac7aBSC0x6876b9804719d8d9f5aeb6ad1322270458fa99e0BSC0xd990094a611c3de34664dd3664ebf979a1230fc1BSCLoss Breakdown
Similar Incidents
Helio Plugin Donation Inflation
33%BankrollNetworkStack self-buy dividend inflation exploit
31%SellToken Short Oracle Manipulation
31%Local Traders Price Takeover
31%GGGTOKEN Treasury Drain via receive()
31%APE2 Pair Burn Exploit
30%Root Cause Analysis
Palm PLP AUM Inflation
1. Incident Overview TL;DR
Palm Protocol on BNB Smart Chain exposed a permissionless accounting exploit at the public state immediately after block 30248637. In transaction 0x62dba55054fa628845fecded658ff5b1ec1c5823f1a5e0118601aa455a30eac9 at block 30248638, the adversary used a 3,000,000 USDT flash loan, bought PLP through Palm's LiquidityEvent, then sent another 2,000,000 USDT directly into Palm's Vault.buyUSDP path. That direct USDP mint increased the PLP AUM used for redemption without minting proportional PLP, so the attacker redeemed the already-purchased PLP against an overstated share price, sold additional USDP, repaid the flash loan, and kept 901455921547703167737871 USDT while the helper contract also retained 40108136663147844828665 USDP.
The root cause is a net-asset accounting failure in Palm's historical PLP pricing logic. Vault.buyUSDP increased both poolAmount and outstanding USDP, but PlpManager.getAum() added the increased poolAmount into PLP AUM without subtracting the matching USDP liability. That let any unprivileged actor who bought PLP before the direct USDP mint redeem the PLP at an inflated price.
2. Key Background
Palm's LP flow spans three public components:
LiquidityEventproxy0xd990094A611c3De34664dd3664ebf979A1230FC1PlpManagerproxy0x6876B9804719d8D9F5AEb6ad1322270458fA99E0Vaultproxy0x806f709558CDBBa39699FBf323C8fDA4e364Ac7A
At block 30248637, the proxies pointed to these historical implementations:
Vaultimplementation0xea625e24a40b07f5094b390257f772580e757055PlpManagerimplementation0xa68f4b2c69c7f991c3237ba9b678d75368ccff8fLiquidityEventimplementation0x02a4b53926bad267df700ff8e9f5d4a2516bb1fc
Independent on-chain reads at block 30248637 confirm the exploit preconditions used in the analysis:
Vault.inManagerMode() = falsePlpManager.inPrivateMode() = truePlpManager.cooldownDuration() = 0LiquidityEvent.endWhitelistTime() = 1689350400, which was already before the transaction timestampPlpManager.getAumInUsdp(true) = 1060591303897471446528544
Palm routes LP entry and exit through LiquidityEvent.purchasePlp() and LiquidityEvent.unstakeAndRedeemPlp(), but PLP redemption is ultimately priced inside PlpManager._removeLiquidity() with:
uint256 aumInUsdp = getAumInUsdp(false);
uint256 plpSupply = IERC20Upgradeable(plp).totalSupply();
uint256 usdpAmount = (_plpAmount * aumInUsdp) / plpSupply;
That formula is safe only if aumInUsdp reflects net assets actually backing PLP. Palm's historical implementation did not satisfy that requirement.
3. Vulnerability Analysis & Root Cause Summary
This incident is an ATTACK-class ACT opportunity, not a privileged abuse. Palm left the direct buyUSDP and sellUSDP paths public whenever inManagerMode was disabled, and at the exploit block that flag was disabled. The PLP pricing path in PlpManager.getAum() counted vault.poolAmount() as PLP backing, but it did not subtract the outstanding USDP debt created by Vault.buyUSDP(). As a result, a direct USDP mint increased the AUM used in PLP redemption while leaving PLP supply unchanged. The attacker exploited that mismatch by buying PLP before the direct mint, then redeeming the PLP after the mint had inflated AUM. The same public transaction also sold most of the freshly minted USDP back into the vault, turning the accounting distortion into realized USDT profit.
4. Detailed Root Cause Analysis
The code breakpoint is visible in Palm's historical Vault and PlpManager implementations.
Historical Palm Vault implementation, behind the Vault proxy at block 30248637:
function buyUSDP(address _receiver) external override nonReentrant returns (uint256) {
_validateManager();
address _collateralToken = collateralToken;
uint256 tokenAmount = _transferIn(_collateralToken);
uint256 price = getMinPrice(_collateralToken);
uint256 _usdpAmount = (tokenAmount * price) / PRICE_PRECISION;
_usdpAmount = adjustForDecimals(_usdpAmount, _collateralToken, usdp);
uint256 feeBasisPoints = vaultUtils.getBuyUsdpFeeBasisPoints(_collateralToken, _usdpAmount);
uint256 amountAfterFees = _collectSwapFees(_collateralToken, tokenAmount, feeBasisPoints);
uint256 mintAmount = (amountAfterFees * price) / PRICE_PRECISION;
mintAmount = adjustForDecimals(mintAmount, _collateralToken, usdp);
_increaseUsdpAmount(mintAmount);
_increasePoolAmount(tokenAmount);
IUSDP(usdp).mint(_receiver, mintAmount);
}
Historical Palm Vault manager gate:
function _validateManager() private view {
if (inManagerMode) {
_validate(isManager[msg.sender], 45);
}
}
Historical Palm PlpManager implementation:
function getAum(bool maximise) public view returns (uint256) {
uint256 aum = aumAddition;
uint256 collateralTokenPrice = maximise ? _vault.getMaxPrice(collateralToken) : _vault.getMinPrice(collateralToken);
uint256 collateralDecimals = _vault.tokenDecimals(collateralToken);
uint256 currentAmmDeduction = (vault.permanentPoolAmount() * collateralTokenPrice) / (10**collateralDecimals);
aum += (vault.poolAmount() * collateralTokenPrice) / (10**collateralDecimals);
...
aum = currentAmmDeduction > aum ? 0 : aum - currentAmmDeduction;
return aumDeduction > aum ? 0 : aum - aumDeduction;
}
function _removeLiquidity(address _account, uint256 _plpAmount, uint256 _minOut, address _receiver)
private
returns (uint256)
{
uint256 aumInUsdp = getAumInUsdp(false);
uint256 plpSupply = IERC20Upgradeable(plp).totalSupply();
uint256 usdpAmount = (_plpAmount * aumInUsdp) / plpSupply;
...
IERC20Upgradeable(usdp).safeTransfer(address(vault), usdpAmount);
uint256 amountOut = vault.sellUSDP(_receiver);
}
The invariant is straightforward: PLP should be priced from Palm's net assets, not from gross collateral alone. Vault.buyUSDP() mints a new USDP liability and also increases poolAmount. Because PlpManager.getAum() adds poolAmount but never subtracts the matching USDP liability, a direct USDP mint makes PLP appear more valuable even though Palm's net equity did not improve.
The exploit required the same concrete conditions listed in the validated root-cause record:
Vault.inManagerModehad to remainfalseso an unprivileged caller could invokebuyUSDP()andsellUSDP()directly.PlpManager.cooldownDurationhad to remain0so PLP could be bought and redeemed in the same transaction.- The public LiquidityEvent route had to remain callable by an unprivileged address after the whitelist window.
- The attacker had to source enough collateral, here via a public flash loan, to buy PLP first and then inflate AUM with a direct USDP mint.
The security principles violated were likewise concrete:
- Net-asset accounting: Palm omitted the USDP liability from PLP backing.
- Share-issuance equivalence: a non-PLP collateral path changed PLP redemption value without minting proportional PLP.
- Permission minimization: a value-moving stablecoin mint and burn path remained public even though PLP pricing depended on it.
The exploit path in the seed transaction is fully visible in the collected trace:
0xd50Cf00b6e600Dd036Ba8eF475677d816d6c4281::flashLoan(... [3000000000000000000000000], ...)
LiquidityEvent::purchasePlp(1000000000000000000000000, 0, 0)
emit BuyUSDP(account: 0x6876B9804719d8D9F5AEb6ad1322270458fA99E0, tokenAmount: 1000000000000000000000000, usdpAmount: 996769453720000000000000, feeBasisPoints: 30)
emit AddLiquidity(..., 1000000000000000000000000, 1060591303897471446528544, 1060117814431969258921599, 996769453720000000000000, 996324456826157454206069)
emit BuyUSDP(account: 0x55252A6D50BFAd0E5F1009541284c783686F7f25, tokenAmount: 2000000000000000000000000, usdpAmount: 1993538907440000000000000, feeBasisPoints: 30)
LiquidityEvent::unstakeAndRedeemPlp(996311162765970954648048, 0, 0x55252A6D50BFAd0E5F1009541284c783686F7f25)
emit RemoveLiquidity(..., 996311162765970954648048, 4050654353532854041808248, 2056442271258126713127668, 1962472861668186127414294, 1956585443083181569032051)
emit SellUSDP(account: 0x55252A6D50BFAd0E5F1009541284c783686F7f25, usdpAmount: 1953430770776852155171335, tokenAmount: 1947570478464521598705820, feeBasisPoints: 30)
Those numbers show the exploit mechanics directly:
- The attacker borrowed
3,000,000USDT from the public lending pool. 1,000,000USDT went throughpurchasePlp, which minted996324456826157454206069PLP exposure before the AUM inflation step.- The remaining
2,000,000USDT was sent directly to theVaultand passed intobuyUSDP, minting1993538907440000000000000USDP while leaving PLP supply unchanged. PlpManager._removeLiquidity()then used the inflated AUM value4050654353532854041808248against PLP supply2056442271258126713127668, so the attacker redeemed the previously acquired PLP for1962472861668186127414294USDP worth of claims and withdrew1956585443083181569032051USDT.- A second direct
sellUSDPconverted another1953430770776852155171335USDP into1947570478464521598705820USDT.
The LiquidityEvent public-access condition was also concrete at the exploit block. Palm's historical LiquidityEvent implementation only enforced a whitelist while block.timestamp <= endWhitelistTime:
function _checkEligible(address account) internal view {
if (endWhitelistTime == 0) return;
else if (block.timestamp <= endWhitelistTime) {
require(isWhitelisted[account], "!whitelist");
}
}
By block 30248637, endWhitelistTime had already elapsed, so purchasePlp() remained available to any unprivileged caller.
5. Adversary Flow Analysis
The adversary cluster contained EOA 0xf84efa8a9f7e68855cf17eaac9c2f97a9d131366 and helper contract 0x55252a6d50bfad0e5f1009541284c783686f7f25. The EOA submitted the seed transaction and ultimately received the realized USDT profit. The helper contract received the flash loan, executed the Palm call sequence, repaid the loan plus fee, then forwarded surplus USDT to the EOA while retaining leftover USDP.
Operationally, the sequence was:
- Borrow
3,000,000USDT with a public flash loan. - Buy PLP first, while Palm AUM still reflected the lower pre-inflation state.
- Inflate AUM with a direct public
Vault.buyUSDP()call that increasedpoolAmountand minted USDP without minting PLP. - Redeem the already-acquired PLP through
LiquidityEvent.unstakeAndRedeemPlp(), which callsPlpManager._removeLiquidity()and prices redemption from the inflated AUM. - Sell most of the directly minted USDP through
Vault.sellUSDP(). - Repay the flash loan plus
2700000000000000000000USDT premium and keep the remaining value.
The final balance deltas confirm that the adversary realized value instead of merely moving accounting entries:
- Attacker EOA
0xf84efa8a9f7e68855cf17eaac9c2f97a9d131366:+901455921547703167737871USDT - Helper contract
0x55252a6d50bfad0e5f1009541284c783686f7f25:+40108136663147844828665USDP - Vault
0x806f709558cdbba39699fbf323c8fda4e364ac7a:-904155921547703167737871USDT
6. Impact & Losses
Palm's vault lost 904155921547703167737871 USDT in a single transaction. The attacker EOA realized 901455921547703167737871 USDT after the flash loan was repaid, and the attacker-controlled helper still retained a positive USDP balance at transaction end. Gas was paid separately in BNB, with the EOA's native balance delta showing -3550824356598799500 wei.
The incident demonstrates that Palm's historical PLP pricing was exploitable by any unprivileged actor whenever the same public conditions held: direct buyUSDP access, zero PLP cooldown, a callable LiquidityEvent path, enough flash liquidity to buy PLP first, and enough remaining collateral to inflate AUM before redeeming.
7. References
- Seed transaction:
0x62dba55054fa628845fecded658ff5b1ec1c5823f1a5e0118601aa455a30eac9 - Seed metadata:
/workspace/session/artifacts/collector/seed/56/0x62dba55054fa628845fecded658ff5b1ec1c5823f1a5e0118601aa455a30eac9/metadata.json - Seed trace:
/workspace/session/artifacts/collector/seed/56/0x62dba55054fa628845fecded658ff5b1ec1c5823f1a5e0118601aa455a30eac9/trace.cast.log - Seed balance diff:
/workspace/session/artifacts/collector/seed/56/0x62dba55054fa628845fecded658ff5b1ec1c5823f1a5e0118601aa455a30eac9/balance_diff.json - Historical Palm Vault source:
https://bscscan.com/address/0xea625e24a40b07f5094b390257f772580e757055#code - Historical Palm PlpManager source:
https://bscscan.com/address/0xa68f4b2c69c7f991c3237ba9b678d75368ccff8f#code - Historical Palm LiquidityEvent source:
https://bscscan.com/address/0x02a4b53926bad267df700ff8e9f5d4a2516bb1fc#code - Palm Vault proxy:
0x806f709558CDBBa39699FBf323C8fDA4e364Ac7A - Palm PlpManager proxy:
0x6876B9804719d8D9F5AEb6ad1322270458fA99E0 - Palm LiquidityEvent proxy:
0xd990094A611c3De34664dd3664ebf979A1230FC1