Calculated from recorded token losses using historical USD prices at the incident time.
0x0e29dcf4e9b211a811caf00fc8294024867bffe4ab2819cc1625d2e9d62390af0x375ae76f0450293e50876d0e5bdc3022cab23198Arbitrum0xd3e323a672f6568390f29f083259debb44c41f41Arbitrum0x367351f854506da9b230cbb5e47332b8e58a1863ArbitrumOn Arbitrum block 79308098, transaction 0x0e29dcf4e9b211a811caf00fc8294024867bffe4ab2819cc1625d2e9d62390af let attacker EOA 0x014abff04e5c441b2ceaa62d843bbc5ae49e5504 use contract 0xcd31e27f0a811de7139938b1972b475697f8c50b to flash-loan 200 WETH and 30,000 USDT, build collateral in Paribus, redeem pETH, and borrow additional assets from other Paribus markets during the redeem callback. The exploit ended with a net gain of 35.228645913352377951 ETH to the originating EOA after gas.
The root cause is a cross-market reentrancy bug in Paribus. PToken.redeemFresh transfers underlying before it burns the redeemer's pTokens, and PEther.doTransferOut sends native ETH with a raw call. That ordering lets an attacker reenter other Paribus markets while the Comptroller still sees the pre-redeem pETH balance as valid collateral.
Paribus is a Compound-style lending protocol. The shared Comptroller determines account liquidity by iterating the borrower's entered markets and reading each market's live getAccountSnapshot(account) result.
Two implementation details make this incident exploitable:
pETH market (0x375ae76f0450293e50876d0e5bdc3022cab23198) pays out native ETH rather than an ERC-20 token.nonReentrant guard, but the Comptroller performs cross-market borrow checks. Reentering from pETH into pUSDT or pWBTC therefore crosses contract instances and bypasses a single-market lock.The pre-state immediately before the exploit transaction was fully public and reconstructible: the attacker contract bytecode was already deployed, Aave flash-loan liquidity was available, Paribus had borrowable cash in pETH, pUSDT, and pWBTC, and the Comptroller price feeds were on-chain. No privileged key, private orderflow, or off-chain secret was needed.
This is an ATTACK-category bug caused by violating checks-effects-interactions in Paribus redemption logic. PToken.redeemFresh computes the post-redeem totalSupply and accountTokens[redeemer], but it does not commit those values before transferring the underlying asset out. In the pETH market, PEther.doTransferOut executes to.call.value(amount)(""), which hands control to the redeemer during the critical window. While that callback is running, ComptrollerNoNFTPart2.borrowAllowed recomputes account liquidity by reading each entered market's current snapshot, and pETH still reports the old token balance. As a result, fresh borrow checks in pUSDT and pWBTC succeed even though the attacker is in the middle of redeeming away the pETH collateral that justified them. When control returns, redeemFresh finally burns the attacker's pETH, leaving zero pETH collateral but positive debt across multiple Paribus markets.
The violated invariant is: once a redeem has been approved, the redeemed pETH must stop contributing to account liquidity before any later borrow check can observe the account in the same transaction. The code-level breakpoint is the external call in redeemFresh that occurs before the accountTokens and totalSupply writeback.
// Paribus PToken.redeemFresh
vars.totalSupplyNew = sub_(totalSupply, vars.redeemTokens, "REDEEM_TOO_MUCH");
vars.accountTokensNew = sub_(accountTokens[redeemer], vars.redeemTokens, "REDEEM_TOO_MUCH");
doTransferOut(redeemer, vars.redeemAmount);
totalSupply = vars.totalSupplyNew;
accountTokens[redeemer] = vars.accountTokensNew;
// Paribus PEther.doTransferOut
(bool success, ) = to.call.value(amount)("");
require(success, "Transfer failed");
// Comptroller liquidity walk
(vars.pTokenBalance, vars.borrowBalance, vars.exchangeRateMantissa) = asset.getAccountSnapshot(account);
vars.sumCollateral = mul_ScalarTruncateAddUInt(vars.tokensToDenom, vars.pTokenBalance, vars.sumCollateral);
vars.sumBorrowPlusEffects = mul_ScalarTruncateAddUInt(vars.oraclePrice, vars.borrowBalance, vars.sumBorrowPlusEffects);
The exploit path is fully visible in the seed trace for transaction 0x0e29dcf4e9b211a811caf00fc8294024867bffe4ab2819cc1625d2e9d62390af.
200 WETH and 30,000 USDT.pETH and pUSDT, then enters those markets in the Comptroller.13.075471156463824220 ETH from pETH.pETH. Paribus computes the post-redeem balances but has not written them yet.PEther.doTransferOut sends 99.999999999994845004 ETH back to the attacker contract. The attacker's payable fallback executes before accountTokens[attacker] is reduced.50,122,071,155 USDT base units from pUSDT and 75,523,112 WBTC base units from pWBTC. Those borrow checks call the Comptroller, which still reads the stale pETH snapshot and therefore approves the borrows.redeemFresh, which finally burns the attacker's pETH. The transaction ends with zero pETH balance but outstanding debt in pETH, pUSDT, and pWBTC.The decisive trace segment is:
L2Pool::flashLoan(... [200 WETH, 30000000000 USDT], ...)
...
enterMarkets([pETH, pUSDT])
emit MarketEntered(param0: PEtherDelegator, param1: 0xcd31...c50b)
emit MarketEntered(param0: PErc20Delegator, param1: 0xcd31...c50b)
PEtherDelegate::borrow(13075471156463824220)
PEtherDelegate::redeem(499999973671)
0xcd31...c50b::fallback{value: 99999999999994845004}()
PErc20Delegate::borrow(50122071155)
0x367351f854506da9b230cbb5e47332b8e58a1863::borrow(75523112)
emit Redeem(redeemer: 0xcd31...c50b, redeemAmount: 99999999999994845004, redeemTokens: 499999973671)
The trace also shows the stale-liquidity dependency directly. During the reentrant borrowAllowed path, the Comptroller reads:
PEtherDelegate::getAccountSnapshot(0xcd31...c50b)
← [Return] 499999973671, 13075471156463824220, ...
That snapshot still contains the pre-redeem pETH token balance. Only after the callback returns does the Redeem event fire and the storage change zero out the attacker's pETH balance. This is why the exploit is cross-market: the local nonReentrant modifier on pETH does not prevent the attacker from entering pUSDT and pWBTC while pETH state is half-updated.
The exploit conditions are exactly the ones listed in the analysis and are all satisfied by public state:
pETH) uses the vulnerable redeem ordering.pUSDT, pWBTC) still have borrowable cash during the callback.pETH in its liquidity walk.The exploit is a single adversary-crafted transaction. The attacker cluster consists of:
0x014abff04e5c441b2ceaa62d843bbc5ae49e5504, which submitted the transaction and received the final ETH profit.0xcd31e27f0a811de7139938b1972b475697f8c50b, which executed the flash-loan callback, Paribus interactions, reentrant borrows, swaps, and final payout.0xec05281d0345f5142acd197bdbc6c4e1fc29dfe7, which temporarily held WETH, minted and redeemed a second pETH position, and returned value to the main attacker contract during the same transaction.The execution flow is:
0x794a61358d6845594f94dc1db02a252b5b4814ad) transfers 200 WETH and 30,000 USDT to the attacker contract.100 WETH, unwraps it, and mints pETH.100 WETH, mints its own pETH, mints pUSDT, and enters the pETH and pUSDT markets.13.075471156463824220 ETH from pETH.pETH.pUSDT and pWBTC by borrowing 50,122,071,155 USDT units and 75,523,112 WBTC units.pETH back to WETH.35.228921031652377951 ETH gross to the EOA.The balance-diff artifact confirms the final economic result:
0x014abff04e5c441b2ceaa62d843bbc5ae49e5504: +35.228645913352377951 ETH net after gas.pETH market: -13.075471156453514228 ETH.pUSDT market: -20,122.071155 USDT.pWBTC market: -0.75523112 WBTC.The exploit leaves Paribus with zero pETH collateral for the attacker but non-zero borrow balances across multiple markets. That is the protocol-level invariant break: the redeemer exits with the collateral removed while the debt created during the callback remains.
Measured losses from the collected balance diff are:
| Market / Asset | Raw amount | Human amount |
|---|---|---|
| ETH | 13075471156453514228 | 13.075471156453514228 ETH |
| USDT | 20122071155 | 20,122.071155 USDT |
| WBTC | 75523112 | 0.75523112 WBTC |
The transaction also satisfies the non-monetary exploit predicate from the analysis: after the redeem completes, the attacker has zero pETH yet still owes Paribus debt. The economic predicate is also satisfied: the attacker realized 35.228645913352377951 ETH net profit to the submitting EOA, with approximately 0.0002751183 ETH attributable to transaction fees.
0x0e29dcf4e9b211a811caf00fc8294024867bffe4ab2819cc1625d2e9d62390af on Arbitrum block 793080980x014abff04e5c441b2ceaa62d843bbc5ae49e55040xcd31e27f0a811de7139938b1972b475697f8c50b, 0xec05281d0345f5142acd197bdbc6c4e1fc29dfe7pETH 0x375ae76f0450293e50876d0e5bdc3022cab23198pUSDT 0xd3e323a672f6568390f29f083259debb44c41f41pWBTC 0x367351f854506da9b230cbb5e47332b8e58a1863PToken.redeemFreshPEther.doTransferOutborrowAllowed and getHypotheticalAccountLiquidityInternalImpl