background information
On September 3, 2024, Penpie contracts were subject to a reentry attack in which the attacker added liquidity to the contract during the reentry phase to impersonate the reward amount, thereby accessing the original reward tokens within the contract. Asset losses amounted to $27.34 million.
In May 2024, the Penpie platform added the launch of a license-free asset pooling feature, which allowed users on Pendle to create their own LP pools of any PT or YT tokens on the platform, with users receiving an additional token bonus for depositing LPs on the Penpie platform.
- X Warning:/PeckShieldAlert/status/1831072230651941093
- Pre-transactions (Create Pool):/explorer/tx/eth/0xfda0dde38fa4c5b0e13c506782527a039d3a87f93f9208c104ee569a642172d2
- One of the attack deals:/explorer/tx/eth/0x56e09abb35ff12271fdb38ff8a23e4d4a7396844426a94c4d3af2e8b7a0a2813
- Attacked contract:/address/0x6e799758cee75dae3d84e09d40dc416ecf713652
Trace Analysis
The attacker first creates a new market in the front-loaded transaction, and puts theSY
Address set to attack contract 0x4af4
Subsequently, in the attack transaction, the attacker flash-lent four assets (we chose one of them, wstETH, to analyze due to the similarity of the operation of the four assets)
These types of operations are carried out within the flash loans
existbatchHarvestMarketRewards
A reentrant attack was performed in the function
Token flow analysis
- :
- redeemRewards:
- [Reentrancy] addLiquiditySingleTokenKeepYt:deposit
16010
wstETH to [Pendle: RouterV4], get8860
[MarketToken] - [Reentrancy] depositMarket:deposite
1751
[MarketToken] to [0x6e79], received1751
[StakingToken]
- [Reentrancy] addLiquiditySingleTokenKeepYt:deposit
- queueNewRewards:Claim
1751
[MarketToken] to 0xd128
- redeemRewards:
- multiclaim:Claim
1751
[MarketToken] from 0xd128 - withdrawMarket:burn
1715
[StakingToken], get1715
[MarketToken] - removeLiquiditySingleToken:burn
10611
[MarketToken], get18733
wstETH - transfer:Repay flashloan
Vulnerability Analysis
CreatePool
Any user can register a Pool on Pendle (market)
/address/0x588f5e5d85c85cac5de4a1616352778ecd9110d3#code
includedonlyVerifiedMarket
check, which checks to see if the pool address is in theallMarkets
center. And anyone can create pools to bypass this restriction.
/ethereum/0x45cF29F501d218Ad045EB8d622B69968E2d4Ef5C
batchHarvestMarketRewards
existbatchHarvestMarketRewards
function by computing the call to The difference in the number of MarketTokens before and after the function is used to get the number of wstETH as reward tokens.
An attacker exploits this design flaw by calling function triggers a reentry attack that makes the
bounsTokens
The value of is increased.
/ethereum/0xff51c6b493c1e4df4e491865352353eadff0f9f8
batchHarvestMarketRewards(Part1)
redeemRewards
Since the market contract is an attacker-created contract, itsSY
is set to the address of the attack contract when it is created.
/ethereum/0x40789E8536C668c6A249aF61c81b9dfaC3EB8F32
function call flow
redeemRewards -> _redeemRewards -> _updateAndDistributeRewards -> _updateAndDistributeRewardsForTwo -> _updateRewardIndex -> _redeemExternalReward ->
SY
is the address of the attack contract in theclaimRewards
function for reentry.
[Reentrancy] addLiquiditySingleTokenKeepYt & depositMarket
[Reentrancy] addLiquiditySingleTokenKeepYt:deposit 16010 wstETH to [Pendle: RouterV4], get 8860 [MarketToken]
[Reentrancy] depositMarket:deposite 1751 [MarketToken] to [0x6e79], received 1751 [StakingToken]
Reentry Attack trace, byaddLiquiditySingleTokenKeepYt
cap (a poem)depositMarket
The operation converts wstETH to a MarketToken and pledges it to the 0x6e79 contract.
batchHarvestMarketRewards(Part2)
With the reentry attack, the contract is made to compute theoriginalBonusBalance
The number of rewards was incorrectly assumed to be 1751 (in fact, no rewards were earned, and the excess balance was the portion of the reentry that added mobility).
originalBonusBalance
cap (a poem)leftBonusBalance
The value of the_harvestBatchMarketRewards -> _sendRewards -> _queueRewarder
The call path is passed to the_queueRewarder
function.
At this point, the contract sends the address 0xd128 to the1751
_rewardToken
。
The number of tokens added by an attacker when adding liquidity via reentry 1751 equals the number of tokens in the 0x6e79 contract balance1751
The purpose is to construct the number of "New Rewards" equal to the "Account Balance", so that the followingqueueRewarder
The function transfers all tokens in contract 0x6e79 to 0xd128.
queueNewRewards
queueNewRewards:Claim 1751 [MarketToken] to 0xd128
0xd128 transfers reward tokens from 0x6e79. Where 0xd128 is the rewardPool contract.
multiclaim
multiclaim:get 1751 [MarketToken] from 0xd128
The attacker receives the reward from the 0xd128 contract (to complete the profit, the source of this money is the balance of the 0x6e79 contract)
withdrawMarket
withdrawMarket:burn 1751 [StakingToken], get 1751 [MarketToken]
Retrieval is done on reentry via thedepositMarket
incoming1751
[MarketToken]
removeLiquiditySingleToken
removeLiquiditySingleToken:burn 10611 [MarketToken], get 18733 wstETH
At this point the attacker is holding the original8860
Plus the proceeds of the attack.1751
The total number of holdings10611
[MarketToken]
Final Attacker Removal10611
Mobility, access to18733
The wstETH.
Repay flashloan
Return to Lightning Loans16010
wstETH, take profit2723
wstETH。
postscript
The impact of this attack is quite large, and the amount of money involved is also huge. After the incident, many security practitioners have analyzed this matter, and I also tried to analyze this attack from trace at the first time. As the incident occurred soon, there is no vendor to publish detailed vulnerability analysis results, coupled with personal rebellious mentality thinking that I can not analyze it without referring to other people's analysis reports, the analysis of this attack was in a day's time to gnaw on the trace analysis came. This analysis is not easy for me to carry out, and the final output of the analysis of the document, but also lacks some understanding of the project architecture and design, there is a bone without flesh, read very dry. I reflected on why I fell into such a dilemma, and it all boils down to unfamiliarity with the project. Attack analysis done without familiarity with the project is tangible and tasteless. This was a problem that could not be ignored and I needed to think of something.