All audits
FailedEthereumIndependent

Penpie Pendle Reentrancy (2024)

Smart contract security audit by TierZero · 2024-09-15

Independent research. This is an unsolicited security analysis published by TierZero — not a commissioned audit, and no certificate is issued. It reflects our own review of public code and on-chain events.

Findings summary

1
Critical
1
High
1
Medium
1
Low
1
Informational

What the protocol did

Penpie, part of the Magpie ecosystem, was a yield-boosting layer sitting on top of Pendle Finance. Users deposited Pendle LP tokens (PT/YT positions on various underlying yield-bearing assets) into Penpie, which staked them into Pendle markets on the user's behalf, auto-compounded PENDLE emissions and market rewards, and issued boosted receipt tokens plus PNP incentives back to depositors. The core contracts tracked, per market, how much each user had deposited and how much reward was owed, then periodically harvested rewards directly from the underlying Pendle market contracts and distributed them pro rata.

The critical design point: Penpie's staking contract trusted the market address it was told to interact with. Pendle's architecture allows anyone to permissionlessly deploy a market for a given PT/YT pair, and Penpie's registration flow accepted new markets without independently verifying, on-chain, that the address was a genuine market deployed by Pendle's official factory. That trust boundary is where the exploit lived.

On September 3, 2024, an attacker drained Penpie for roughly $27 million across several pools in a single transaction sequence.

The vulnerability

This is a classic reentrancy, but the entry point is unusual: instead of reentering through a token's transfer callback, the attacker reentered through a market contract they controlled and had gotten registered as "trusted" inside Penpie's accounting.

The flawed pattern, reduced to its essence:

// simplified — illustrates the pattern, not the exact production source
function harvestAndDistribute(address market) external {
    uint256 rewardBefore = IERC20(rewardToken).balanceOf(address(this));

    // external call into a contract whose code Penpie does not control
    IPendleMarket(market).redeemRewards(address(this));

    uint256 rewardReceived = IERC20(rewardToken).balanceOf(address(this)) - rewardBefore;

    // state (accRewardPerShare, user balances) is updated AFTER the external call
    rewardManager.updateRewards(market, rewardReceived);
}

Two separate weaknesses compound here. First, market is attacker-influenceable because market registration lacked a strict factory-provenance check — a market-shaped contract deployed by the attacker was accepted as legitimate. Second, and more damaging, the external call (redeemRewards) happens before Penpie finalizes its internal reward/share accounting, violating checks-effects-interactions. A contract the attacker fully controls gets execution mid-function, while Penpie's books are in an inconsistent, still-mutable state.

How the exploit worked

  1. The attacker deployed a malicious contract mimicking the interface of a Pendle market and registered it with Penpie's staking contract as a legitimate market — Penpie's registration path did not reject it.
  2. The attacker called into Penpie's deposit/harvest flow against this fake market. Because Penpie treated the market as trusted, it invoked callback-style functions on the attacker's contract expecting standard Pendle market behavior (reward redemption, share accounting hooks).
  3. Inside that callback, the attacker's contract reentered Penpie's core staking/reward contracts — calling deposit, withdraw, or reward-claim functions again before the first call had finished updating accRewardPerShare and per-user balances.
  4. Each reentrant call let the attacker manipulate the reward-per-share math relative to a real, high-value market (with legitimate rewards already accrued from other users' deposits), effectively crediting the attacker's position with rewards and withdrawable principal it never earned.
  5. The attacker repeated the pattern across multiple pools, then withdrew the inflated balances as real assets (stETH, USDe, and other underlyings held by Penpie on behalf of depositors), draining the shared liquidity backing every other user's position.

The mechanics are conceptually identical to any cross-contract reentrancy risk we flag in call-graph review — the same class of "who executes when your accounting is half-updated" question we walk through for cross-program invocation patterns in our analysis of cross-program invocation risk in Solana DeFi, just expressed through Solidity's external-call semantics instead of a CPI.

How an audit catches this

This bug class is caught by a specific, repeatable set of checks, not general code reading:

  • Trust-boundary mapping: for every external call, ask "whose code runs here, and what can it do to me before it returns." Any contract address that is registrable by a non-privileged actor (governance-less addMarket, addPool, addStrategy functions) gets flagged as attacker-controllable by default until proven otherwise.
  • CEI ordering audit: mechanical check of every function that both makes an external call and mutates shared accounting state (accRewardPerShare, total supply, per-user balances) — state must be finalized before the call, or the function must be reentrancy-guarded on the full call graph, not just the immediate function.
  • Reentrancy fuzzing across contracts: single-function reentrancy guards (nonReentrant on one entrypoint) are insufficient when the reentrant call lands in a different function of the same protocol. We test cross-function, cross-contract reentrancy paths explicitly, including through attacker-supplied "market"/"pool"/"strategy" contracts registered via permissionless paths.
  • Provenance validation for pluggable integrations: any integration that accepts an externally supplied contract address (a market, a price feed, a strategy) needs an on-chain provenance check against the canonical factory or a curated allowlist — not just an interface check, which any contract can satisfy.

This is standard coverage in a smart contract audit engagement and in the deeper manual pass we run under code review and audit for protocols that compose on top of external DeFi primitives like Pendle, Aave, or Curve — composability is exactly where these trust-boundary bugs hide.

Remediation

  • Enforce checks-effects-interactions strictly: finalize all internal accounting (reward-per-share, balances, total deposits) before any external call, or restructure to a pull-based reward claim model that never needs to trust the timing of an external redemption call.
  • Validate market/pool addresses against the canonical factory at registration time and again at call time — an interface match is not proof of authenticity.
  • Apply reentrancy guards at the protocol level (a shared lock across all state-mutating entrypoints touching the same accounting structures), not just per-function.
  • Add circuit breakers: anomaly-based pausing when a single address's claimable balance grows disproportionately relative to its historical deposits in one block.
  • For teams building new integrations rather than patching existing ones, get the trust-boundary model reviewed before mainnet — it's substantially cheaper through smart contract development support than after funds are live.

If you're integrating with third-party markets, vaults, or reward routers and want the call graph checked for this exact failure mode, talk to us about a smart contract audit.

Need your contracts audited?

Manual review + tooling across TON, Solana and EVM — certificate and full report included.

Get an audit