Reentrancy in reward accounting: the Penpie $27M exploit

Reentrancy in Reward Accounting: How $27M Was Paid Out on a Deposit

Sep 1

| 6 min read

reentrancy_reward_accounting_penpie_27m_exploit

The Auditor’s Take is a weekly series by Jon Stephens, CEO of Veridise. This one is about a reentrancy bug, and about what a reward calculation measures when it subtracts two balances. There is a new one each week, and Jon posts them at @FormallyJon.

Penpie accepted Pendle market LP tokens and paid depositors the rewards those tokens earned, boosted above what Pendle paid directly. To determine the amount of rewards owed, its staking contract read its balance of the reward token, called the market to collect rewards, and read the balance again. It did not expect the caller to reenter and deposit the same token it was measuring. The deposit skewed the reward calculation, and the protocol boosted funds that had earned nothing. The attacker withdrew both the deposit and the reward, immediately and at no cost. That is how over $27 million left Penpie in September 2024.

TLDR:

  • Penpie measured rewards as the balance difference across a call to the market
  • The attacker’s malicious SY contract listed Penpie’s own deposit tokens as rewards
  • Depositing during the callback made the protocol interpret deposited funds as rewards
  • batchHarvestMarketRewards lacked a reentrancy guard, so the harvest never engaged it
  • One modifier fixed it; guards only help if every relevant entry point carries it

Measuring a Call by the Balance It Leaves Behind

The rewards were generated by Pendle, not by Penpie. Before distributing anything, Penpie had to know how much each market had paid. Pendle answers that directly as redeemRewards returns the reward amounts it paid.

Penpie did not rely on Pendle’s accounting. It recorded its balance of each reward token before collecting from the market, read it again afterwards, and treated the difference as the amount generated instead of trusting the number Pendle returned.

The balance difference answers a narrower question than it looks. It tells you how much the balance changed. It does not tell you what caused the change.

Those two answers coincide only when you know exactly what the call you made will do. Penpie could not know. The call ended up invoking untrusted code that could do anything, including reentering into Penpie to skew the balance difference.

The attacker specifically performed a deposit upon reentry. A deposit raises the same balance the harvest was watching. The protocol ended up treating those tokens two ways at once: once as a deposit, crediting the attacker a normal position they could withdraw, and once as a reward, crediting the harvest’s inflated balance difference. Both were there to be claimed. Rewards usually reflect a stake that was actually exposed to what the market did over time, not a balance that appeared and disappeared inside one transaction. Any payout upon transient funds enables such manipulations as a user can guarantee a payout with no risk.

Reentrancy has corrupted protocol accounting this way before. Conic Finance lost roughly $3.2 million in July 2023. Its reentrancy guards were conditionally enforced based on a faulty assumption about an external pool.

Penpie: A Market the Attacker Registered

Before a market could be harvested, it had to be registered with Penpie’s staking contract, and from May 2024 anyone could do it. registerPenpiePool only required that the provided address be created via Pendle’s factory. Creating a Pendle market is permissionless by design, so that requirement establishes provenance and nothing else.

On September 3, 2024, the attacker wrote a malicious Standardized Yield contract, the ERC-20 that a Pendle market wraps. They used Pendle’s own factory to create a market around it, then registered that market with Penpie.

So when Penpie asked the market to report its reward tokens, the answer came from the attacker’s SY contract underneath it. It named two real Pendle LP tokens that Penpie itself accepted as deposits. When Penpie called redeemRewards on the market, the call eventually reached claimRewards on the attacker’s contract, handing over control mid-calculation. The attacker used that control to add flash-loaned liquidity to the real markets and deposit the resulting LP tokens into Penpie. Over $27 million left across Ethereum and Arbitrum.

This is the problem with reviewing components in isolation. The staking contract was reviewed when only a team multisig could register markets. The harvest accounting held on the assumption that only trusted markets would ever be registered. A missing reentrancy guard on a function that hands control to an external contract is worth raising even under that assumption. Permissionless registration arrived more than a year later, and the engagement covering it reviewed the new contracts rather than the harvest function, which had not changed. This is a problem common to diff reviews and feature-scoped reviews. In both cases, a new feature can weaken or invalidate an assumption another part of the code relies on. Both when performing these reviews and when requesting a review, it is worth probing the impact a change can have on the protocol as a whole.

What the Harvest Code Assumed

The harvest code makes one assumption about the markets it calls. For a given market, a reward redemption call should only increase the contract’s reward token balance by the amount of rewards earned. Here is the loop that assumes it:

An attacker controls critical information used by this loop. Registering a market grants control of its parameters, such as the reward token list returned by getRewardTokens(). A real market names PENDLE and its own yield tokens. This one named the tokens Penpie accepted as deposits for other Pendle markets, causing their current balances to be cached in amountsBefore.

redeemRewards() eventually reaches the attacker’s SY contract through claimRewards, transferring control between the two reads. The attacker routed flash-loaned funds through PendleMarketDepositHelper into real pools, and each deposit moved that pool’s market tokens into the staking contract. When control returned, the difference between the current balance and the one cached in amountsBefore reported the attacker’s own redeemable deposit as rewards paid by their market. _sendRewards credited it to that market’s rewarder, where the attacker was the only depositor. The staking contract held the deposit tokens for every pool, so that reward was paid out of LP tokens belonging to depositors in the legitimate pools. The attacker withdrew their own deposits and repaid the loan, then claimed the reward.

The deposit path they reentered was guarded:

Penpie’s staking contract inherits OpenZeppelin’s reentrancy guard. While a function marked with the nonReentrant modifier is running, every other function marked with that modifier reverts. depositMarket carries it but batchHarvestMarketRewards did not, so the harvest never engaged the guard, and the deposit went through. The fix to close the attack vector later added the missing modifier:

With the modifier there, the guard is engaged for the whole harvest batch, and the reentrant deposit reverts.

How a Senior Auditor Reads a Call to Untrusted Code

Penpie built this calculation the way that implied a lack of trust in the target contract. A returned figure was available, and they measured the balance instead. So the question is what Penpie assumed that call would do, and whether the call could do more than that. Worth asking alongside it: what were they guarding against elsewhere, and why was reentry into the harvest left open?

The four steps below are worth running early in a smart contract audit, since they show where time is best spent.

First, identify external calls where the destination can be controlled by untrusted users. Most calls out of a contract go to addresses set by trusted admins. All calls matter, but these ones are worth extra attention.

Second, write down what the protocol concludes from each call. In this case, the protocol concludes that any increase in its balance of a reward token is directly proportional to the reward that market paid.

Third, specifically identify any other actions that may yield the same result. A deposit is the obvious one here.

Fourth, ask what prevents an attacker from performing those actions during the call. Where the answer is a reentrancy guard, check that it covers the entry point that starts the sequence and not only the one that finishes it.

From a Manual Trace to a Check on Every Commit

A review covers the code as it stood on the day it was performed. Penpie’s harvest function was initially reviewed, and funds remained safe until a seemingly irrelevant update a year later, when Penpie opened market registration to anyone in a different contract. That let markets with malicious configurations reach a calculation written on an assumption that no longer held.

Vanguard is the static analyzer Veridise auditors use, and it ships a detector for reentrancy of this kind. Here, it recognizes reads and writes to the same state that straddle a call capable of handing control to an untrusted party. It follows those accesses across contract boundaries, which matters here because the route from the harvest loop to the deposit runs through four contracts. It reasons about which state is reachable during the call rather than about which functions carry a modifier. In this case, the modifier was present on the deposit and absent on the harvest.

Pointed at the reproduced harvest path, it identifies the two balance reads and the deposit that lands between them. The guarantee stops there. It reports where the pattern is, not whether a given instance is exploitable, and the triage is still the auditors’ responsibility.

What Carries Beyond Penpie

Where your code draws a conclusion from an external call’s effect, write down what you are concluding, then ask what else could produce the same effect. Most protocols have already disallowed the alternatives, and the exercise confirms it. Where they have not, disallow the interleaving outright rather than reasoning about whether this ordering can be turned into a profit. That reasoning has to be redone every time the surrounding code changes, and it was the reasoning that expired here.

The reward design carries further than the reentrancy does, and in a DeFi audit it deserves the same scrutiny as the control flow. A reward has to reflect real exposure to what the market did, whether that is time in the pool or yield actually captured. Penpie paid out against a balance the recipient could move at no cost, and no reentrancy guard fixes a payout formula that cannot tell a deposit from a return. A previous review by our team found a similar issue. Rewards were computed from how much a user had deposited historically, with no weighting for how long the funds sat there. A large deposit earned the same reward in a moment as a depositor who left funds for months. Different bug, same missing link between the payout and the work.

Working on a similar protocol?

If your protocol calls out to contracts you do not control, the same class of vulnerability could be sitting in your code. Put the same question to each of those calls: what are you assuming it will do, and can the contract on the other end do more than that?

If you want a second pair of eyes before your next deploy, talk to us.

The Takeaway

A reentrancy bug does not need a protocol that forgot about external calls. Penpie expected the call to hand over control and measured a balance across it, then read the increase as rewards when it was really the attacker’s own deposit. Catching this class means asking what else could produce the change your code is measuring.

More by Veridise

An attacker supplied the target and payload of a call Arcadia’s Rebalancer made, and pointed a trusted contract’s authority at other users’ accounts.

Aug 5

| 4 min read

Aug 5

| 4 min read

Subscribe to our blog

Be the first to get the latest from Veridise — including educational articles on ZK and smart contracts, audit case studies, and updates on our tool development. Delivered twice a month.

smart contract audit cloud

Subscribe to our newsletter

A monthly round-up for protocol teams: new audit reports, research from our lab, and tooling releases from the Veridise team.

One email a month. Unsubscribe in one click

Contact us for a security audit quote

Secure an earlier audit slot by reaching out early.