Calculated from recorded token losses using historical USD prices at the incident time.
0xbe163f651d23f0c9e4d4a443c0cc163134a31a1c2761b60188adcfd33178f50f0x9215748657319b17fecb2b5d086a3147bfbc8613Arbitrum0x7b8b944ab2f24c829504a7a6d70fce5298f2147cArbitrumOn Arbitrum block 211107442, transaction 0xbe163f651d23f0c9e4d4a443c0cc163134a31a1c2761b60188adcfd33178f50f drained all incident-block WETH and USDC balances from PredyPool proxy 0x9215748657319b17fecb2b5d086a3147bfbc8613. The attacker used one transaction to create a fresh callback contract, register a new WETH/USDC pair, convert the pool’s globally custodied reserves into attacker-owned pair-local supply claims, and immediately redeem those claims for the underlying assets.
The root cause is an access-control and accounting composition failure in the PredyPool implementation 0x7b8b944ab2f24c829504a7a6d70fce5298f2147c. registerPair, trade, supply, and withdraw were callable by arbitrary users, while the trade path temporarily assigned msg.sender as the settlement locker and allowed that locker to call take. That let an attacker move globally held WETH and USDC into a newly created pair’s supply accounting without decrementing the older pairs’ local accounting, then withdraw the same reserves a second time.
PredyPool tracks lending and trading state per pair, but the ERC-20 reserves themselves are held at the proxy contract address. Registering a new pair deploys fresh pair-local supply tokens, so a newly added pair can create new redeemable claims if the contract accepts deposits for assets it already holds globally.
The exploit depended on the interaction between three public paths:
registerPair, which creates a new pair and deploys fresh supply-token contracts for the pair’s quote and base assets.trade, whose callback flow assigns the caller as the active locker during settlement.take, which lets the active locker move quote or base reserves from the pool address during that callback window.The pre-state documented in the collector artifacts shows the victim pool still held 83910994929830029848 WETH units and 219585737814 USDC units immediately before the exploit transaction. The targeted state diff also shows pairCount = 3 and vaultCount = 185 before exploitation.
This is an ATTACK-category ACT exploit, not a benign MEV unwind. The safety invariant is that each unit of WETH or USDC custodied by PredyPool should back exactly one set of redeemable supply claims, and only trusted settlement logic should be able to move those reserves during callback settlement.
That invariant failed because PredyPool exposed both pair creation and locker-controlled settlement to arbitrary callers. registerPair had no caller restriction, so the attacker could create a duplicate WETH/USDC pair with brand-new supply-token contracts. trade then delegated control to the attacker’s callback contract by recording msg.sender as the current locker. During that callback, the attacker could call take to move WETH and USDC out of the pool, approve the pool, and immediately call supply into the new pair, minting attacker-owned pTokens against reserves that already backed older pairs. After callback finalization restored the token balances at the pool address, withdraw burned the newly minted pTokens and transferred the same underlying assets out to the attacker.
The key breakpoint is the locker handoff and unrestricted reserve movement:
function trade(TradeParams memory tradeParams, bytes memory settlementData)
external
returns (TradeResult memory tradeResult)
{
tradeParams.vaultId = globalData.createOrGetVault(tradeParams.vaultId, tradeParams.pairId);
return TradeLogic.trade(globalData, tradeParams, settlementData);
}
function take(bool isQuoteAsset, address to, uint256 amount) external onlyByLocker {
globalData.take(isQuoteAsset, to, amount);
}
PredyPool implementation snippet.
The settlement path makes the callback caller the privileged locker for the duration of trade settlement:
function initializeLock(GlobalDataLibrary.GlobalData storage globalData, uint256 pairId) internal {
globalData.lockData.quoteReserve = ERC20(globalData.pairs[pairId].quotePool.token).balanceOf(address(this));
globalData.lockData.baseReserve = ERC20(globalData.pairs[pairId].basePool.token).balanceOf(address(this));
globalData.lockData.locker = msg.sender;
globalData.lockData.pairId = pairId;
}
function callTradeAfterCallback(...) internal {
globalData.initializeLock(tradeParams.pairId);
IHooks(msg.sender).predyTradeAfterCallback(tradeParams, tradeResult);
(int256 marginAmountUpdate, int256 settledBaseAmount) = globalData.finalizeLock();
if (settledBaseAmount != 0) revert IPredyPool.BaseTokenNotSettled();
}
Trade callback and lock-management snippets.
Once the attacker controls that callback, take can move pool reserves without checking pair ownership or deposit provenance. The new pair’s supply path then mints redeemable supply tokens solely from a transfer back into the pool:
function supply(GlobalDataLibrary.GlobalData storage globalData, uint256 _pairId, uint256 _amount, bool _isStable)
external
returns (uint256 mintAmount)
{
DataType.PairStatus storage pair = globalData.pairs[_pairId];
if (_isStable) {
mintAmount = receiveTokenAndMintBond(pair.quotePool, _amount);
} else {
mintAmount = receiveTokenAndMintBond(pair.basePool, _amount);
}
}
function receiveTokenAndMintBond(Perp.AssetPoolStatus storage _pool, uint256 _amount)
internal
returns (uint256 mintAmount)
{
mintAmount = _pool.tokenStatus.addAsset(_amount);
ERC20(_pool.token).safeTransferFrom(msg.sender, address(this), _amount);
ISupplyToken(_pool.supplyTokenAddress).mint(msg.sender, mintAmount);
}
Supply minting snippet.
The state diff for the exploit transaction confirms the semantic milestones claimed by the analysis:
pairCount changes from 3 to 4.vaultCount changes from 185 to 186.3: 0x3dd636919d4180b59d9225370cb84f1ba849aba2 for pWETH and 0x0b9f4dfb6eb2a8c2f26e98c4538422e6b8c4599f for pUSDC.The balance diff proves realized loss and profit. PredyPool’s WETH and USDC balances both fell to zero, while helper contract 0x8affdd350eb754b4652d9ea5070579394280cad9 gained the same 83910994929830029848 WETH units and 219585737814 USDC units. The sender EOA paid 43831070000000 wei in gas.
The exploit is a single-transaction ACT sequence executed by EOA 0x76b02ab483482740248e2ab38b5a879a31c6d008 through helper contract 0x8affdd350eb754b4652d9ea5070579394280cad9.
0xb79714634895f52a4f6a75eceb58c96246370149.registerPair and creates pair 3 for the WETH/USDC Uniswap V3 pool 0xc6962004f452be9203591991d15f6b388e09e8d0, which deploys new pWETH and pUSDC contracts.trade on pair 3. During callTradeAfterCallback, PredyPool records the callback contract as the active locker.predyTradeAfterCallback, the callback calls take(true, ...) and take(false, ...) to pull all incident-block WETH and USDC reserves from the pool, then approves the pool and calls supply for both assets into pair 3.withdraw twice, burns the pair-3 pTokens, receives the underlying WETH and USDC, and forwards them to helper contract 0x8aff....The validator’s independent Forge replay on an Arbitrum fork reproduces the same sequence: the test creates pair 3, creates vault 185, drains PredyPool’s WETH and USDC reserves to zero, burns the pair-3 pTokens, and transfers the drained assets to a clean attacker address.
The measurable loss in the incident transaction is:
83910994929830029848 smallest units (83.910994929830029848 WETH)219585737814 smallest units (219585.737814 USDC)The direct victim is PredyPool proxy 0x9215748657319b17fecb2b5d086a3147bfbc8613, which lost all incident-block reserves for the affected assets. Existing pairs retained their local accounting even though the shared reserves were reassigned and later withdrawn, so the exploit created duplicate redeemable claims before cashing them out.
0xbe163f651d23f0c9e4d4a443c0cc163134a31a1c2761b60188adcfd33178f50fpairCount and vaultCount increments plus pair-3 storage writes.0x7b8b944ab2f24c829504a7a6d70fce5298f2147c.TradeLogic.sol and GlobalData.sol snippets showing locker assignment and callback execution.AddPairLogic.sol and SupplyLogic.sol snippets showing duplicate-pair creation and supply-token minting.