Penpie Malicious Pendle Market batchHarvestMarketRewards Reentrancy (2024-09-03)

  • Name: Penpie malicious Pendle market batchHarvestMarketRewards reentrancy
  • Date: 2024-09-03
  • Protocol: Penpie
  • Chain: Ethereum + Arbitrum
  • Category: balance-delta provenance failure / reentrancy / reward-accounting manipulation / permissionless-market abuse / fake-market registration
  • Loss: about 11,113.6 ETH, roughly 621k drain on Arbitrum before pausing completed
  • Summary: Penpie let anyone register any Pendle-valid market, and its PendleStakingBaseUpg.batchHarvestMarketRewards() path trusted that a market’s getRewardTokens() and redeemRewards() behavior would be honest. The attacker created a fake Pendle market with a malicious SY contract, became its only depositor, then called batchHarvestMarketRewards() on that fake market. During the harvest, Penpie first recorded rewardTokens and amountsBefore, then called the fake market’s redeemRewards(). The malicious SY contract used that callback window to re-enter depositMarket() and push flash-loaned real Pendle market tokens into Penpie. Because batchHarvestMarketRewards() lacked reentrancy protection, Penpie misread those fresh deposits as harvested rewards, credited them to the fake market’s reward bucket, and let the attacker claim essentially all of them as the sole fake-market depositor.
  • Trigger / entry point:
    • Penpie had introduced permissionless pool registration for markets that passed Pendle-side validity checks.
    • Pendle market creation itself was permissionless, so the attacker could create a syntactically valid market whose SY contract was malicious.
    • batchHarvestMarketRewards() trusted the target market’s getRewardTokens() output, read Penpie’s token balances before and after redeemRewards(), and treated the delta as harvested rewards.
    • That harvest function lacked a nonReentrant guard even though the re-entered depositMarket() path was externally callable and moved the same token types whose balances were being measured.
  • Root cause:
    • Penpie measured rewards as amountAfter - amountsBefore around an external redeemRewards() call without sealing the accounting state against re-entry.
    • The protocol also assumed that isValidMarket() meant economically honest market, but Pendle validity only proved that the market was factory-created, not that its SY / reward behavior was safe for Penpie’s reward-accounting assumptions.
    • Those two design choices combined badly: permissionless market admission let the attacker supply the hostile callback surface, and the missing reentrancy guard let that hostile market rewrite the balance sheet Penpie was treating as reward output.
    • Penpie’s own postmortem adds an important lifecycle / audit-scope detail: the 2023 Zokyo and WatchPug reviews saw batchHarvestMarketRewards() while pool registration was still owner-gated, so the hostile-market assumption was muted. WatchPug had noted an incorrect-distribution issue around pendleMarket.redeemRewards() claiming on behalf of any user, but the fix path stayed inside a trusted-market framing and neither 2023 audit called for reentrancy protection here. When permissionless registration was introduced in May 2024, the incremental AstraSec review focused on the new registration feature and did not re-scope the largely unchanged PendleStakingBaseUpg harvest path. The unchanged code therefore inherited a new hostile-input surface without a matching re-review of its callback and accounting assumptions.
  • Exploit path:
    1. The attacker deployed a malicious Pendle market and malicious SY contract on Ethereum, then registered that market on Penpie through the protocol’s permissionless pool-registration flow.
    2. They minted or assembled large quantities of fake PT / YT / market-token positions for the malicious market and deposited into Penpie so they would be the effective sole depositor in that fake pool.
    3. The attacker flash-loaned large real positions tied to valuable Pendle markets, including wstETH-, sUSDe-, egETH-, and rswETH-related market tokens.
    4. They invoked PendleStakingBaseUpg.batchHarvestMarketRewards() on the malicious market.
    5. Inside the harvest flow, Penpie queried the malicious market’s getRewardTokens() and accepted attacker-chosen real Pendle market tokens as the assets whose reward deltas should be measured.
    6. Penpie recorded amountsBefore for those tokens, then called the malicious market’s redeemRewards() path.
    7. During redeemRewards(), the malicious SY contract re-entered Penpie and called depositMarket() with the flash-loaned real Pendle market tokens. That moved valuable legitimate market tokens into Penpie while the harvest frame was still live.
    8. Control returned to batchHarvestMarketRewards(), which measured the new higher balances as if they were freshly redeemed rewards from the fake market rather than attacker-supplied deposits.
    9. Penpie forwarded those inflated amountAfter - amountsBefore values to its reward distributor and attributed the rewards to the fake market.
    10. Because the attacker was effectively the only depositor in that fake market, they claimed nearly all of the misattributed rewards, withdrew the legitimate Pendle market tokens, redeemed them back into their underlying assets, repaid the flash loans, and kept the spread.
    11. The attacker repeated the drain across multiple Ethereum transactions and then performed a final Arbitrum-side extraction before the full pause closed every path.
  • What the attacker needed to be true:
    • Penpie had to accept permissionlessly created Pendle markets as stakeable pools.
    • The fake market’s SY / reward flow had to be able to run arbitrary callback logic during redeemRewards().
    • batchHarvestMarketRewards() had to measure token-balance deltas across that callback window without a reentrancy lock.
    • The reward-distribution logic had to attribute harvested amounts to the market currently being processed, rather than proving that the balance increase actually came from that market’s own reward redemption.
    • The attacker needed to be the dominant or sole depositor in the fake market so the misattributed rewards would flow almost entirely back to them.
  • Technical anatomy worth remembering:
    • This was not a vanilla withdraw() reentrancy. The attacker monetized a reward-accounting window: first choose which token balances the protocol will watch, then increase those balances through a re-entrant side path, then let the protocol book the delta as harvested rewards.
    • The permissionless-market design was not independently fatal, and the missing reentrancy guard was not independently enough without a hostile market surface. The exploit required both the open admission surface and the callback-sensitive accounting assumption.
    • The attack is a strong example of balance delta != earned reward. Penpie treated a raw token-balance increase inside the staking contract as proof that a market had produced rewards, even though the increase actually came from attacker-controlled deposits during the measurement window.
    • The sharper operational lesson is that unchanged legacy code can become newly dangerous when admission policy changes. Penpie’s postmortem says the original V1 audits did not flag this as a reentrancy issue while market registration was still multisig-gated, and the later incremental audit for permissionless registration did not pull the old harvest path back into scope.
    • Although depositMarket() itself had reentrancy protection in some flows, that did not save Penpie because the vulnerable harvest path lacked the corresponding lock. A single guarded sibling entrypoint is not a protocol-wide reentrancy defense.
  • Representative transactions and contracts:
    • Penpie PendleStaking contract highlighted in the protocol postmortem: 0x6E799758CEE75DAe3d84e09D40dc416eCf713652
    • Penpie’s published preparation window (5:44 PM–5:51 PM UTC) included setup / fake-market staging transactions 0xfda0dde38fa4c5b0e13c506782527a039d3a87f93f9208c104ee569a642172d2, 0x7e7f9548f301d3dd863eac94e6190cb742ab6aa9d7730549ff743bf84cbd21d1, 0x7961b0d3382bddff4c777e432902ea0b6940414ec16aa1a1dcebc4ebcbd8f867, and 0xca87f257280e19378dc1890a478514195f068857affacde0b92c851b897dff9e
    • First major Ethereum drain transaction: 0x56e09abb35ff12271fdb38ff8a23e4d4a7396844426a94c4d3af2e8b7a0a2813
    • Follow-on Ethereum drain transactions: 0x42b2ec27c732100dd9037c76da415e10329ea41598de453bb0c0c9ea7ce0d8e5, 0x663b55a1ee992603f7636ef23ff5cf19d3b261ab81494d06e218c86482df5342
    • Final Arbitrum drain transaction cited by Penpie: 0x67c5400da117b906f8c0fc5f5149e4ea10ed6358cd9ea2ec0ed8f559d757b7df
    • Exploiter address cited in Penpie’s timeline: 0x7a2f4d625fb21f5e51562ce8dc2e722e12a61d1b
  • Response / mitigation:
    • Pendle paused its own contracts on Ethereum at about 6:45 PM UTC and on Arbitrum at about 7:19 PM UTC to prevent further Penpie-linked drains through shared market infrastructure; Penpie’s postmortem says those pauses likely protected roughly $70M more TVL on Penpie, excluding vePENDLE positions.
    • Penpie paused deposits, withdrawals, and pool activity across chains at about 7:38 PM UTC, then restored frontend access for unaffected functions while the protocol remained in incident mode.
    • Penpie, Pendle, and SEAL 911 coordinated an incident war room; the team also contacted law enforcement, Hypernative, SlowMist, Binance Security, Chainalysis, and other response partners.
    • Penpie sent onchain and public messages seeking a whitehat-style return, but public reporting indicates the attacker instead moved funds through fresh wallets and later Tornado Cash flows.

Pattern tags

  • permissionless market admission turned a factory-valid market object into attacker-authored reward-harvest authority
  • batchHarvestMarketRewards() treated watched-balance growth as reward truth even though sibling entrypoints could inject the same tokens during the callback window`
  • sole fake-market depositor captured almost the entire misattributed reward bucket once publication authority blessed the balance delta
  • unchanged legacy harvest code inherited a new hostile-input surface after registration policy widened

Why the exploit worked

  • Penpie authenticated the wrong object twice. First it treated Pendle-valid market as if that implied safe reward producer for Penpie, even though Pendle validity only proved factory shape, not honest getRewardTokens() and redeemRewards() behavior under hostile control. Then it treated token balances increased during harvest as if that implied this market just emitted rewards, even though the increase actually came from attacker-driven depositMarket() inflows.
  • batchHarvestMarketRewards() therefore behaved like a publication-authority surface, not just a housekeeping helper. Once it observed higher balances after the callback, it canonized those deltas into the fake market’s reward ledger and forwarded them to the distributor as if source provenance had already been proven.
  • The attacker only needed one fake market and one dominant depositor position because Penpie’s reward accounting was market-local but not source-local. After the malicious market redirected real Pendle market tokens into the watched balances, the protocol still credited the resulting reward bucket to the fake market whose callback was in flight.
  • Penpie’s own postmortem makes the rollout failure sharper: the harvest code itself was not newly written for the exploit. What changed was the admission boundary. Once permissionless market registration arrived, old trusted-market assumptions around batchHarvestMarketRewards() stopped being safe, but the harvest path did not get re-reviewed as if hostile SY / market callbacks were now first-class inputs.
  • Put differently: this was not just missing nonReentrant. It was harvest publication authority + permissionless hostile market admission + unproven reward provenance.

Detection and audit pivots

  • Audit any harvest, claim, sync, or redeemRewards() path that measures amountAfter - amountBefore across an external call. The core reviewer question is whether any sibling entrypoint can move the same watched token set during that window.
  • Treat permissionless market admission as a re-scope event for every old helper that previously assumed honest markets. If registration policy changes from owner-gated to permissionless, unchanged harvest code should be reviewed as if it had just accepted a brand-new hostile input class.
  • Bind reward provenance to source-specific events or escrow buckets, not to ambient contract balances. A market-local reward path should prove that the watched increase came from that market’s own redemption flow, not merely that the contract balance rose while the market callback was active.
  • Fuzz fake market -> attacker-chosen rewardTokens -> redeemRewards() callback -> sibling deposit path for the same assets -> reward distribution traces directly. Penpie is not a generic withdraw twice case; it is a publish attacker-supplied deposits as rewards case.
  • Review whether guarded sibling entrypoints actually compose into protocol-wide safety. A nonReentrant on depositMarket() did not help once the caller entered through an unguarded harvest path that could still invoke the same deposit-side balance movements.
  • Ask whether one actor can become the overwhelming or sole beneficiary of a newly published reward bucket. Concentrated entitlement is what turned Penpie’s accounting mistake into near-total extraction rather than noisy, partially socialized misdistribution.

Failure-prevention checklist

  • Put the same reentrancy and frame-consumption discipline on harvest and reward-publication paths that already exists on deposit and withdrawal paths. If a callback can move watched balances, harvest is a money-moving function.
  • Treat market admission as a semantic boundary, not just a factory check. If downstream accounting assumes honest reward semantics, permissionless registration needs either stricter market screening or a reward path that remains safe under hostile markets.
  • Commit the market, watched reward-token set, and allowed reward-source action before calling redeemRewards(), then reject any sibling path that can alter those balances without being the committed source for this frame.
  • Publish harvested rewards from frame-local deltas or market-scoped escrow, not from ambient token balances sitting in the staking contract after nested execution.
  • Require explicit rescoping of older accounting paths when governance opens a formerly gated integration surface. Same code, wider hostile input set should be treated as a material security change.
  • Add adversarial tests where fake markets return real high-value tokens from getRewardTokens() and use redeemRewards() callbacks to route flash-loaned assets through sibling deposit helpers.
  • Limit blast radius when one market’s reward path goes wrong: caps on harvestable amounts, quarantined new markets, staged activation, or delayed reward publication do not fix provenance bugs, but they reduce how quickly a fake market can steal from unrelated real markets.
  • Separate factory-valid, interface-valid, and economically safe for this downstream accounting path as three different admission claims. Only the last one should unlock direct reward publication into a shared distributor.

Audit prompts

  • What exactly proves that a balance increase observed after redeemRewards() came from harvested rewards for this market rather than from a sibling deposit, transfer, or callback side path?
  • If getRewardTokens() is attacker-influenced, what prevents the market from choosing high-value tokens already used by other pools and then manipulating Penpie’s accounting to relabel those balances as its own rewards?
  • Which state object binds market being harvested, reward tokens being watched, and the only action allowed to increase those balances before external control leaves the protocol?
  • If permissionless registration is introduced tomorrow for a previously trusted integration surface, which unchanged legacy helpers would immediately become unsafe under hostile callback assumptions?
  • Does the system have a path where a fake or newly listed market can become the sole depositor or dominant claimant for a reward bucket that is funded from shared contract balances?
  • Are harvest, claim, redeem, sync, or cache write helpers still treated as low-risk bookkeeping in reviews even though they publish balances into user-entitlement state?
  • If a sibling entrypoint is nonReentrant but the current caller entered through a different unguarded path, does the protocol still prevent cross-entrypoint balance pollution during the same frame?
  • What telemetry would reveal reward-provenance drift early: new markets harvesting unusually large values immediately after registration, watched-token sets that overlap major real markets, or reward deltas that far exceed the market’s prior emission history?

Lessons that should already be standard

  • Permissionless market and safe downstream integration object are different claims. Factory validity does not prove reward honesty.
  • Harvest code is not bookkeeping when it decides when a token-balance increase becomes user-entitlement truth.
  • balanceAfter - balanceBefore can prove that tokens moved; it cannot by itself prove why they moved or which market earned them.
  • Opening one formerly trusted surface should trigger a fresh review of every unchanged helper that now accepts hostile callbacks through that surface.
  • A guarded sibling function is not a protocol-wide reentrancy defense. The economically decisive question is whether any unguarded entrypoint can still let the same balances change during its frame.
  • Reward provenance needs the same rigor as reserve provenance. If the system can tell two coherent stories about how a watched balance increase arrived, the attacker will claim the one that pays them.

Why this incident matters

  • It is a clean modern reminder that reentrancy is not just about calling withdraw() twice. Any accounting flow that measures balances around an external call can become reentrancy-sensitive if the attacker can influence those same balances during the callback.
  • It also shows how permissionless market and factory-valid market are not equivalent to safe integration surface for downstream protocols.
  • The exploit preserved a reusable lesson for reward systems: a harvested reward should be proven by source-specific accounting, not inferred from a generic token-balance increase inside a contract that an attacker can touch through other entrypoints.
  • It also preserves a quieter rollout lesson: widening market admission can reactivate dormant assumptions inside unchanged legacy code. If a harvest, router, or accounting path was originally reviewed under trusted-market or owner-gated assumptions, making the upstream market surface permissionless should force a fresh review of every old callback and provenance boundary that now accepts hostile inputs.
  • More specifically, it shows that harvest is not passive bookkeeping. batchHarvestMarketRewards() was a publication-authority surface deciding when one market’s callback-time balance changes became canonical reward truth for that market’s depositors.

Internal linkages

  • Core recurring pattern: reentrancy.
  • Sharper positive-design companion: callback-frame-truth-security-object — Penpie needed one market-local harvest frame binding the in-flight market, the watched reward-token set, the only permitted reward-source path, and the sibling state transitions that were still allowed before reward deltas became final.
  • Publication / harvest-authority companion: sync-harvest-and-cache-write-authority-discipline — batchHarvestMarketRewards() was the moment Penpie turned an observed balance increase into canonical reward truth, so the protocol needed to treat harvest timing and harvest consumers as a write-authority surface rather than as harmless bookkeeping.
  • Admission / endpoint-semantics companion: interface-conformance-semantic-compatibility-failure — Penpie treated Pendle-valid market as if it also meant safe reward-emission endpoint, but the exploit path lived in the gap between factory-valid interface shape and semantically safe callback behavior.
  • Accounting lens: balance-delta-provenance-failure — Penpie is the reward-accounting / semantic-source-confusion variant: amountAfter - amountBefore showed a real balance increase, but the increase came from attacker-supplied depositMarket() inflows during the callback window rather than from the market’s own harvested rewards.
  • Routing shorthand inside that sibling pattern: Mirror is the ambient-parked-balance cousin, Akropolis is the clean one outer unwind remint cousin, Grim is the repeated checkpoint-reset remints vault cousin, and Origin Dollar is the explicit mint first, later rebasing reread of the same backing cousin.
  • Adjacent admission/control lesson: permissionless integration surfaces can still be too open when downstream accounting assumes honest callback behavior.

Control surface