Zokyo Security Research
$27M Reentrancy Exploit on Penpie
A detailed post-mortem analyzing the reentrancy vulnerability in Penpie's reward harvesting mechanism, the attack flow across Ethereum and Arbitrum, and the lessons for cross-contract security.
$ $ $ $

Incident Overview

On September 3, 2024, at 6:23 PM UTC, a sophisticated attacker exploited a reentrancy vulnerability in the Penpie protocol - a yield farming platform built on top of Pendle Finance. Over the course of three transactions across Ethereum and Arbitrum, the attacker drained approximately $27 million worth of crypto assets, including wstETH, sUSDe, agETH, rswETH, and various Pendle-related YT tokens.

Penpie functions as an intermediary layer that allows users to deposit Pendle LP tokens and earn boosted rewards. The platform integrates deeply with Pendle's market infrastructure, trusting Pendle-created markets as valid sources for reward distribution. This trust assumption became the foundation of the exploit.

Disclosure

The smart contract exploited in this attack was outside the scope of Zokyo's original audit. The Penpie team introduced "permissionless pool registration" approximately one year after Zokyo performed its audit. At that time, they hired a separate security firm (AstraSec) to audit only the new registration contracts. Since the exploit resulted from an interaction between contracts audited by two different teams at two different points in time, neither audit caught the cross-contract vulnerability.

Key Addresses and Transactions

The following on-chain identifiers are central to understanding this exploit:

  • Attacker address: 0x7a2f4d625fb21f5e51562ce8dc2e722e12a61d1b
  • Victim contract (Ethereum): 0x6E799758CEE75DAe3d84e09D40dc416eCf713652
  • Exploit contract: 0x4aF4C234B8CB6e060797e87AFB724cfb1d320Bb7

The attack was executed across three primary transactions:

Transactions Ethereum Mainnet
// Transaction 1
0x56e09abb35ff12271fdb38ff8a23e4d4a7396844426a94c4d3af2e8b7a0a2813

// Transaction 2
0x42b2ec27c732100dd9037c76da415e10329ea41598de453bb0c0c9ea7ce0d8e5

// Transaction 3
0x7e7f9548f301d3dd863eac94e6190cb742ab6aa9d7730549ff743bf84cbd21d1

Root Cause Analysis

The exploit was made possible by the convergence of two independent design weaknesses:

  1. Missing reentrancy protection on the batchHarvestMarketRewards() function in the PendleStakingBaseUpg contract
  2. Permissionless market registration in Pendle's factory contracts, which Penpie treated as inherently trustworthy

The Vulnerable Function

The core vulnerability resided in PendleStakingBaseUpg.sol. The batchHarvestMarketRewards() function was responsible for iterating through registered Pendle markets, claiming their accumulated rewards, and distributing those rewards to depositors. Critically, this function lacked a nonReentrant modifier.

Solidity PendleStakingBaseUpg.sol
function batchHarvestMarketRewards(
    address[] calldata _markets,
    uint256 _minEthToRecieve
) external {
    // No nonReentrant modifier - VULNERABLE

    for (uint256 i = 0; i < _markets.length; i++) {
        address market = _markets[i];

        // Snapshot balances BEFORE claiming
        uint256[] memory amountsBefore = _getRewardBalances(market);

        // External call to market's claimRewards()
        // This is where the reentrancy occurs
        IPendleMarket(market).redeemRewards(address(this));

        // Snapshot balances AFTER claiming
        uint256[] memory amountsAfter = _getRewardBalances(market);

        // Difference is attributed as "rewards"
        for (uint256 j = 0; j < amountsBefore.length; j++) {
            uint256 reward = amountsAfter[j] - amountsBefore[j];
            // Distribute reward to depositors...
        }
    }
}

The critical flaw is the pattern of snapshot-before, external-call, snapshot-after. The function measures the "reward" as the difference in token balances before and after calling redeemRewards() on the market. If an attacker can inject tokens into the contract during that external call, the balance delta will be inflated - and the contract will treat the injected tokens as legitimate rewards.

Permissionless Market Registration

The second enabling factor was Pendle's permissionless market creation system. Anyone can call createNewMarket() on the Pendle factory contract to deploy a new market. Penpie's registration helper then allows any Pendle-created market to be registered as a valid pool:

Solidity PendleMarketRegisterHelper.sol
function registerPenpiePool(
    address _market
) external {
    // Only checks that _market was created by PendleMarketFactoryV3
    // Does NOT verify the underlying SY token is legitimate
    require(
        IPendleMarketFactory(factory).isValidMarket(_market),
        "Invalid market"
    );

    // Creates PenpieReceiptToken and BaseRewardPoolV2
    // for the (potentially malicious) market
    _registerPool(_market);
}

This meant the attacker could deploy a malicious SY (Standardized Yield) token, use it to create a Pendle market through the official factory, and then register that market with Penpie. The system would treat it as a legitimate pool because it came from Pendle's factory - even though the underlying SY contract was entirely attacker-controlled.

Step-by-Step Attack Flow

The attack unfolded in three distinct phases: setup, exploitation, and extraction.

Phase 1: Malicious Market Setup

  1. Deploy malicious SY contract: The attacker deployed a crafted SY (Standardized Yield) token contract designed to act as both a yield source and the reentrancy attack vector. This contract's claimRewards() callback was weaponized to re-enter Penpie's staking contract.
  2. Create fake Pendle Market: Using Pendle's PendleYieldContractFactory.createYieldContract() and PendleMarketFactoryV3.createNewMarket(), the attacker created a legitimate-looking market backed by the malicious SY token.
  3. Mint PT/YT tokens: The attacker called PendleYieldToken.mintPY() to mint PT (Principal Token) and YT (Yield Token) for the fake market.
  4. Register with Penpie: The attacker called registerPenpiePool() to register the fake market with Penpie. The system created a corresponding PenpieReceiptToken (PRT) and a BaseRewardPoolV2 rewarder for the malicious market.
Solidity Attack Setup (simplified)
// Step 1: Deploy malicious SY token
MaliciousSY maliciousSY = new MaliciousSY();

// Step 2: Create yield contract through Pendle factory
IPendleYieldContractFactory(yieldFactory).createYieldContract(
    address(maliciousSY),
    expiry,
    doCacheIndex
);

// Step 3: Create market through Pendle market factory
address fakeMarket = IPendleMarketFactory(marketFactory).createNewMarket(
    PT_address,
    scalarRoot,
    initialAnchor,
    lnFeeRateRoot
);

// Step 4: Register the fake market in Penpie
IPendleMarketRegisterHelper(registerHelper).registerPenpiePool(
    fakeMarket
);

Phase 2: Reentrancy Exploitation

  1. Flash loan: The attacker obtained flash loans from Balancer Vault, borrowing large quantities of agETH, rswETH, egETH, and wstETH.
  2. Deposit into fake SY: The borrowed tokens were deposited into the malicious SY contract, giving it control over significant token balances.
  3. Trigger batchHarvestMarketRewards(): The attacker called batchHarvestMarketRewards() on the Penpie staking contract, passing the fake market as one of the target markets.
  4. Reentrancy via redeemRewards(): When the staking contract called redeemRewards() on the fake market, the malicious SY contract's callback fired. Inside this callback, the attacker re-entered the depositMarket() function.
  5. Inflate balances: During the reentrant call, the attacker deposited flash-loaned tokens as liquidity into real Pendle markets, received genuine LP tokens, and deposited those LP tokens back into Penpie pools. The malicious SY contract's getRewardTokens() function was crafted to return these LP tokens as "reward tokens."
  6. Balance delta manipulation: When execution returned to batchHarvestMarketRewards(), the amountsAfter snapshot reflected the inflated balances. The contract calculated a massive "reward" delta and attributed it entirely to the attacker (who was the sole depositor in the fake market).
Solidity Malicious SY Contract (simplified)
contract MaliciousSY is IStandardizedYield {
    IPendleStaking penpieStaking;
    address[] rewardTokens;
    bool reentered;

    // Called by Penpie during batchHarvestMarketRewards
    function claimRewards(
        address /*user*/
    ) external returns (uint256[] memory) {
        if (!reentered) {
            reentered = true;

            // Re-enter: deposit flash-loaned tokens
            // into legitimate Pendle markets
            _addLiquidityToRealMarkets();

            // Deposit resulting LP tokens into Penpie
            penpieStaking.depositMarket(
                realMarket,
                address(this),
                lpAmount
            );
        }
        return new uint256[](0);
    }

    // Returns LP tokens as "reward tokens"
    function getRewardTokens()
        external view returns (address[] memory)
    {
        return rewardTokens; // Points to real LP tokens
    }
}

Phase 3: Fund Extraction

  1. Claim inflated rewards: The attacker called multiclaim() on the MasterPenpie contract to withdraw the "rewards" - which were actually real LP tokens that had been injected during the reentrancy.
  2. Convert PRT to market tokens: The attacker called withdrawMarket() to convert PenpieReceiptTokens back into the underlying market LP tokens.
  3. Redeem and repay: LP tokens were redeemed from legitimate Pendle markets back into the original assets (wstETH, sUSDe, agETH, rswETH). Flash loans were repaid, and the attacker retained the profit.
Key Insight

The depositMarket() function had a nonReentrant modifier, but batchHarvestMarketRewards() did not. Because these functions used different reentrancy locks (or the harvesting function had none at all), the attacker could re-enter depositMarket() from within batchHarvestMarketRewards() without triggering the guard.

Stolen Assets Breakdown

The attacker drained approximately $27 million in assets across Ethereum and Arbitrum. The primary stolen tokens included:

  • wstETH - Wrapped staked Ether (Lido)
  • sUSDe - Staked USDe (Ethena)
  • agETH - Kelp DAO restaked ETH
  • rswETH - Restaked Swell ETH
  • egETH - EigenLayer restaked ETH
  • Various Pendle YT (Yield Token) positions

Following the exploit, approximately 10,110 ETH (worth roughly $24.7 million) was consolidated at address 0x2f2d...1C39. The attacker subsequently laundered the funds through Tornado Cash in batches of 100 ETH, completing the transfer of approximately 11,261 ETH ($27 million total) within five days of the exploit.

Why the Vulnerability Was Not Caught

This exploit illustrates a critical challenge in DeFi security: the gap between sequential audits and evolving codebases.

Audit Scope Fragmentation

The Penpie protocol underwent its initial audit with Zokyo, which covered the core staking and reward distribution logic at a time when pool registration was admin-controlled. Approximately one year later, the Penpie team introduced permissionless pool registration and engaged a separate firm (AstraSec) to audit only the new registration system.

The critical gap: neither audit examined the interaction between the new permissionless registration and the existing reward harvesting logic. The vulnerability did not exist in either codebase in isolation - it only manifested when the two systems interacted.

Plaintext Audit Timeline
Timeline of Events:

[Year 1]  Zokyo audits Penpie core contracts
           - Pool registration: admin-only (registerPool)
           - batchHarvestMarketRewards: safe (only trusted markets)
           - No reentrancy risk from trusted admin-registered pools

[Year 2]  Penpie introduces permissionless pool registration
           - AstraSec audits ONLY the new registration contracts
           - Existing staking/harvesting contracts NOT re-audited
           - New trust assumption: any Pendle market is valid

[Exploit] Attacker combines both systems:
           - Registers malicious market (new system)
           - Exploits reward harvesting (old system)
           - Neither audit scope covered this interaction

The Trust Boundary Shift

When pool registration was admin-controlled, the batchHarvestMarketRewards() function operated under a reasonable trust assumption: all registered markets were vetted by the team. The lack of a nonReentrant modifier was not a critical risk because the markets calling back into the contract were known entities.

When registration became permissionless, this trust boundary silently shifted. The harvesting function now interacted with arbitrary, potentially malicious markets - but its code was never updated to reflect this new threat model. The redeemRewards() call became an unguarded external call to an attacker-controlled contract.

The Fix

The immediate remediation is straightforward. Adding a nonReentrant modifier to batchHarvestMarketRewards() would have prevented the exploit entirely:

Solidity PendleStakingBaseUpg.sol (patched)
function batchHarvestMarketRewards(
    address[] calldata _markets,
    uint256 _minEthToRecieve
) external nonReentrant {   // <-- Added reentrancy guard
    for (uint256 i = 0; i < _markets.length; i++) {
        // ... same logic, now protected
    }
}

However, the deeper fix involves re-evaluating the trust model. Additional defensive measures include:

  • Whitelisting markets: Only allow admin-approved markets to participate in reward harvesting, even if registration is permissionless
  • Validating reward tokens: Verify that getRewardTokens() returns expected tokens, not arbitrary addresses controlled by the market deployer
  • Global reentrancy locks: Use a single, contract-wide reentrancy guard rather than per-function modifiers, preventing cross-function reentrancy
  • Checks-effects-interactions pattern: Complete all state updates before making external calls, regardless of reentrancy guards

Post-Incident Response

Immediately after the exploit was detected, the Pendle Finance team paused all protocol operations, successfully protecting approximately $70 million in additional assets that could have been at risk. The Penpie team engaged multiple security partners for incident response and fund tracking:

  • Hypernative - Real-time threat detection
  • Binance Security Team - Exchange-level fund tracking
  • SlowMist - On-chain investigation
  • Chainalysis - Transaction tracing and compliance

Despite these efforts, the attacker did not respond to on-chain messages requesting fund return. Within one week, the entirety of the stolen funds had been laundered through Tornado Cash in sequential 100 ETH batches. The PNP governance token dropped approximately 40% in the immediate aftermath.

Lessons Learned

This exploit carries several important takeaways for protocol teams and security auditors alike:

1. Re-audit When Trust Assumptions Change

Any modification that changes who can interact with a contract - such as moving from admin-only to permissionless access - constitutes a fundamental shift in the security model. The entire codebase that relies on the old trust assumption must be re-evaluated, not just the new code.

2. Cross-Contract Interactions Are the Highest-Risk Surface

The vulnerability did not exist within any single contract. It emerged from the interaction between the registration system and the reward harvesting system. Audits must explicitly examine cross-contract call chains, especially when contracts have been audited by different teams at different times.

3. Reentrancy Guards Should Be Applied Broadly

Any function that makes an external call to an address that could be attacker-controlled must have reentrancy protection. The fact that depositMarket() had a nonReentrant modifier but batchHarvestMarketRewards() did not created an asymmetric defense that the attacker exploited. A global reentrancy lock across all state-mutating functions would have prevented this.

4. Permissionless Does Not Mean Trustless

Allowing anyone to create markets is a valid design choice for composability. But the system that consumes those markets must treat them as untrusted input. The balance-delta pattern for reward calculation is inherently vulnerable when the "reward source" is attacker-controlled, because the attacker can inject arbitrary tokens between the before and after snapshots.

5. Flash Loans Amplify Every Vulnerability

Without flash loans, the attacker would have needed significant capital to inflate the reward balances. Flash loans reduced the attack cost to near zero while enabling $27 million in extraction. Any reentrancy or oracle manipulation vulnerability should be evaluated under the assumption that the attacker has access to effectively unlimited capital through flash loans.

Conclusion

The Penpie exploit is a textbook case of how individually secure components can create catastrophic vulnerabilities when composed together under changed assumptions. The original staking contract was safe under admin-only registration. The new registration system was safe in isolation. But when combined, the permissionless registration opened an attack vector in the reward harvesting logic that neither audit scope covered.

For protocol teams, the lesson is clear: security is not a point-in-time exercise. Every change to access control, trust boundaries, or external integrations requires a holistic re-evaluation of the entire system's security properties. For auditors, this incident reinforces the need to map complete trust chains across all contracts in scope, especially where external calls delegate control to potentially adversarial addresses.

At Zokyo, this incident has reinforced our commitment to comprehensive audit scoping that explicitly includes cross-contract interaction analysis, trust boundary documentation, and follow-up review recommendations whenever client codebases evolve post-audit.