All incidents

Paribus Redeem Reentrancy

Share
Apr 11, 2023 10:15 UTCAttackLoss: 13.08 ETH, 20,122.07 USDT +1 morePending manual check1 exploit txWindow: Atomic
Estimated Impact
13.08 ETH, 20,122.07 USDT +1 more
Label
Attack
Exploit Tx
1
Addresses
3
Attack Window
Atomic
Apr 11, 2023 10:15 UTC → Apr 11, 2023 10:15 UTC

Exploit Transactions

TX 1Arbitrum
0x0e29dcf4e9b211a811caf00fc8294024867bffe4ab2819cc1625d2e9d62390af
Apr 11, 2023 10:15 UTCExplorer

Victim Addresses

0x375ae76f0450293e50876d0e5bdc3022cab23198Arbitrum
0xd3e323a672f6568390f29f083259debb44c41f41Arbitrum
0x367351f854506da9b230cbb5e47332b8e58a1863Arbitrum

Loss Breakdown

13.08ETH
20,122.07USDT
0.755231WBTC

Similar Incidents

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 pETH market (0x375ae76f0450293e50876d0e5bdc3022cab23198) pays out native ETH rather than an ERC-20 token.
  • Each market has its own 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.

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.

  1. The attacker contract takes an Aave flash loan of 200 WETH and 30,000 USDT.
  2. The attacker uses the flash-loaned assets to mint Paribus pETH and pUSDT, then enters those markets in the Comptroller.
  3. After collateral is in place, the attacker borrows 13.075471156463824220 ETH from pETH.
  4. The attacker then redeems all of its pETH. Paribus computes the post-redeem balances but has not written them yet.
  5. PEther.doTransferOut sends 99.999999999994845004 ETH back to the attacker contract. The attacker's payable fallback executes before accountTokens[attacker] is reduced.
  6. Inside that callback, the attacker borrows 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.
  7. Control returns to 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:

  • 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 pETH in 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 second pETH position, and returned value to the main attacker contract during the same transaction.

The execution flow is:

  1. Flash-loan funding
    • Aave L2Pool (0x794a61358d6845594f94dc1db02a252b5b4814ad) transfers 200 WETH and 30,000 USDT to the attacker contract.
  2. Collateral setup
    • The helper contract receives 100 WETH, unwraps it, and mints pETH.
    • The attacker contract unwraps another 100 WETH, mints its own pETH, mints pUSDT, and enters the pETH and pUSDT markets.
  3. Initial extraction
    • With both positions counted as collateral, the attacker borrows 13.075471156463824220 ETH from pETH.
  4. Reentrant cross-market borrow
    • The attacker redeems all of its pETH.
    • During the raw ETH callback, the attacker drains all immediately available cash from pUSDT and pWBTC by borrowing 50,122,071,155 USDT units and 75,523,112 WBTC units.
  5. Profit realization
    • The helper contract redeems its own pETH back 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 ETH gross to the EOA.

The balance-diff artifact confirms the final economic result:

  • EOA 0x014abff04e5c441b2ceaa62d843bbc5ae49e5504: +35.228645913352377951 ETH net after gas.
  • pETH market: -13.075471156453514228 ETH.
  • pUSDT market: -20,122.071155 USDT.
  • pWBTC market: -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 / AssetRaw amountHuman amount
ETH1307547115645351422813.075471156453514228 ETH
USDT2012207115520,122.071155 USDT
WBTC755231120.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: 0x0e29dcf4e9b211a811caf00fc8294024867bffe4ab2819cc1625d2e9d62390af on Arbitrum block 79308098
  • Attacker EOA: 0x014abff04e5c441b2ceaa62d843bbc5ae49e5504
  • Attacker contracts: 0xcd31e27f0a811de7139938b1972b475697f8c50b, 0xec05281d0345f5142acd197bdbc6c4e1fc29dfe7
  • Victim markets:
    • pETH 0x375ae76f0450293e50876d0e5bdc3022cab23198
    • pUSDT 0xd3e323a672f6568390f29f083259debb44c41f41
    • pWBTC 0x367351f854506da9b230cbb5e47332b8e58a1863
  • 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 borrowAllowed and getHypotheticalAccountLiquidityInternalImpl