Calculated from recorded token losses using historical USD prices at the incident time.
0x62dba55054fa628845fecded658ff5b1ec1c5823f1a5e0118601aa455a30eac90x806f709558cdbba39699fbf323c8fda4e364ac7aBSC0x6876b9804719d8d9f5aeb6ad1322270458fa99e0BSC0xd990094a611c3de34664dd3664ebf979a1230fc1BSCPalm 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.
Palm's LP flow spans three public components:
LiquidityEvent proxy 0xd990094A611c3De34664dd3664ebf979A1230FC1PlpManager proxy 0x6876B9804719d8D9F5AEb6ad1322270458fA99E0Vault proxy 0x806f709558CDBBa39699FBf323C8fDA4e364Ac7AAt block 30248637, the proxies pointed to these historical implementations:
Vault implementation 0xea625e24a40b07f5094b390257f772580e757055PlpManager implementation 0xa68f4b2c69c7f991c3237ba9b678d75368ccff8fLiquidityEvent implementation 0x02a4b53926bad267df700ff8e9f5d4a2516bb1fcIndependent 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) = 1060591303897471446528544Palm 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.
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.
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.inManagerMode had to remain false so an unprivileged caller could invoke buyUSDP() and sellUSDP() directly.PlpManager.cooldownDuration had to remain 0 so PLP could be bought and redeemed in the same transaction.The security principles violated were likewise concrete:
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:
3,000,000 USDT from the public lending pool.1,000,000 USDT went through purchasePlp, which minted 996324456826157454206069 PLP exposure before the AUM inflation step.2,000,000 USDT was sent directly to the Vault and passed into buyUSDP, minting 1993538907440000000000000 USDP while leaving PLP supply unchanged.PlpManager._removeLiquidity() then used the inflated AUM value 4050654353532854041808248 against PLP supply 2056442271258126713127668, so the attacker redeemed the previously acquired PLP for 1962472861668186127414294 USDP worth of claims and withdrew 1956585443083181569032051 USDT.sellUSDP converted another 1953430770776852155171335 USDP into 1947570478464521598705820 USDT.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.
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:
3,000,000 USDT with a public flash loan.Vault.buyUSDP() call that increased poolAmount and minted USDP without minting PLP.LiquidityEvent.unstakeAndRedeemPlp(), which calls PlpManager._removeLiquidity() and prices redemption from the inflated AUM.Vault.sellUSDP().2700000000000000000000 USDT premium and keep the remaining value.The final balance deltas confirm that the adversary realized value instead of merely moving accounting entries:
0xf84efa8a9f7e68855cf17eaac9c2f97a9d131366: +901455921547703167737871 USDT0x55252a6d50bfad0e5f1009541284c783686f7f25: +40108136663147844828665 USDP0x806f709558cdbba39699fbf323c8fda4e364ac7a: -904155921547703167737871 USDTPalm'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.
0x62dba55054fa628845fecded658ff5b1ec1c5823f1a5e0118601aa455a30eac9/workspace/session/artifacts/collector/seed/56/0x62dba55054fa628845fecded658ff5b1ec1c5823f1a5e0118601aa455a30eac9/metadata.json/workspace/session/artifacts/collector/seed/56/0x62dba55054fa628845fecded658ff5b1ec1c5823f1a5e0118601aa455a30eac9/trace.cast.log/workspace/session/artifacts/collector/seed/56/0x62dba55054fa628845fecded658ff5b1ec1c5823f1a5e0118601aa455a30eac9/balance_diff.jsonhttps://bscscan.com/address/0xea625e24a40b07f5094b390257f772580e757055#codehttps://bscscan.com/address/0xa68f4b2c69c7f991c3237ba9b678d75368ccff8f#codehttps://bscscan.com/address/0x02a4b53926bad267df700ff8e9f5d4a2516bb1fc#code0x806f709558CDBBa39699FBf323C8fDA4e364Ac7A0x6876B9804719d8D9F5AEb6ad1322270458fA99E00xd990094A611c3De34664dd3664ebf979A1230FC1