Paribus Redeem Reentrancy
Exploit Transactions
0x0e29dcf4e9b211a811caf00fc8294024867bffe4ab2819cc1625d2e9d62390afVictim Addresses
0x375ae76f0450293e50876d0e5bdc3022cab23198Arbitrum0xd3e323a672f6568390f29f083259debb44c41f41Arbitrum0x367351f854506da9b230cbb5e47332b8e58a1863ArbitrumLoss Breakdown
Similar Incidents
dForce Oracle Reentrancy Liquidation
40%Sentiment Balancer Oracle Overborrow
31%DEI burnFrom Allowance Inversion
29%Midas LP Oracle Read-Only Reentrancy via Curve stMATIC/WPOL
27%Cream Finance cAmp / Amp Reentrancy Exploit
26%BSC staking pool reentrancy drain
24%Root Cause Analysis
Paribus Redeem Reentrancy
1. Incident Overview TL;DR
On 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.
2. Key Background
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:
- The
pETHmarket (0x375ae76f0450293e50876d0e5bdc3022cab23198) pays out native ETH rather than an ERC-20 token. - Each market has its own
nonReentrantguard, but the Comptroller performs cross-market borrow checks. Reentering frompETHintopUSDTorpWBTCtherefore 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.
3. Vulnerability Analysis & Root Cause Summary
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);
4. Detailed Root Cause Analysis
The exploit path is fully visible in the seed trace for transaction 0x0e29dcf4e9b211a811caf00fc8294024867bffe4ab2819cc1625d2e9d62390af.
- The attacker contract takes an Aave flash loan of
200 WETHand30,000 USDT. - The attacker uses the flash-loaned assets to mint Paribus
pETHandpUSDT, then enters those markets in the Comptroller. - After collateral is in place, the attacker borrows
13.075471156463824220 ETHfrompETH. - The attacker then redeems all of its
pETH. Paribus computes the post-redeem balances but has not written them yet. PEther.doTransferOutsends99.999999999994845004 ETHback to the attacker contract. The attacker's payable fallback executes beforeaccountTokens[attacker]is reduced.- Inside that callback, the attacker borrows
50,122,071,155USDT base units frompUSDTand75,523,112WBTC base units frompWBTC. Those borrow checks call the Comptroller, which still reads the stalepETHsnapshot and therefore approves the borrows. - Control returns to
redeemFresh, which finally burns the attacker'spETH. The transaction ends with zeropETHbalance but outstanding debt inpETH,pUSDT, andpWBTC.
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:
- The attacker controls a contract with a payable fallback.
- At least one collateral market (
pETH) uses the vulnerable redeem ordering. - Other Paribus markets (
pUSDT,pWBTC) still have borrowable cash during the callback. - The attacker enters the collateral markets before redeeming, so the Comptroller includes stale
pETHin its liquidity walk.
5. Adversary Flow Analysis
The exploit is a single adversary-crafted transaction. The attacker cluster consists of:
- EOA
0x014abff04e5c441b2ceaa62d843bbc5ae49e5504, which submitted the transaction and received the final ETH profit. - Main attacker contract
0xcd31e27f0a811de7139938b1972b475697f8c50b, which executed the flash-loan callback, Paribus interactions, reentrant borrows, swaps, and final payout. - Helper contract
0xec05281d0345f5142acd197bdbc6c4e1fc29dfe7, which temporarily held WETH, minted and redeemed a secondpETHposition, and returned value to the main attacker contract during the same transaction.
The execution flow is:
- Flash-loan funding
- Aave L2Pool (
0x794a61358d6845594f94dc1db02a252b5b4814ad) transfers200 WETHand30,000 USDTto the attacker contract.
- Aave L2Pool (
- Collateral setup
- The helper contract receives
100 WETH, unwraps it, and mintspETH. - The attacker contract unwraps another
100 WETH, mints its ownpETH, mintspUSDT, and enters thepETHandpUSDTmarkets.
- The helper contract receives
- Initial extraction
- With both positions counted as collateral, the attacker borrows
13.075471156463824220 ETHfrompETH.
- With both positions counted as collateral, the attacker borrows
- Reentrant cross-market borrow
- The attacker redeems all of its
pETH. - During the raw ETH callback, the attacker drains all immediately available cash from
pUSDTandpWBTCby borrowing50,122,071,155USDT units and75,523,112WBTC units.
- The attacker redeems all of its
- Profit realization
- The helper contract redeems its own
pETHback to WETH. - The attacker swaps residual USDT and WBTC into WETH using public DEX liquidity, repays the Aave flash loan plus fees, unwraps the remaining WETH, and transfers
35.228921031652377951 ETHgross to the EOA.
- The helper contract redeems its own
The balance-diff artifact confirms the final economic result:
- EOA
0x014abff04e5c441b2ceaa62d843bbc5ae49e5504:+35.228645913352377951 ETHnet after gas. pETHmarket:-13.075471156453514228 ETH.pUSDTmarket:-20,122.071155 USDT.pWBTCmarket:-0.75523112 WBTC.
6. Impact & Losses
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.
7. References
- Exploit transaction:
0x0e29dcf4e9b211a811caf00fc8294024867bffe4ab2819cc1625d2e9d62390afon Arbitrum block79308098 - Attacker EOA:
0x014abff04e5c441b2ceaa62d843bbc5ae49e5504 - Attacker contracts:
0xcd31e27f0a811de7139938b1972b475697f8c50b,0xec05281d0345f5142acd197bdbc6c4e1fc29dfe7 - Victim markets:
pETH0x375ae76f0450293e50876d0e5bdc3022cab23198pUSDT0xd3e323a672f6568390f29f083259debb44c41f41pWBTC0x367351f854506da9b230cbb5e47332b8e58a1863
- Supporting evidence used for validation:
- Seed opcode trace for the exploit transaction
- Seed balance diff for the exploit transaction
- Paribus
PToken.redeemFresh - Paribus
PEther.doTransferOut - Paribus Comptroller
borrowAllowedandgetHypotheticalAccountLiquidityInternalImpl