背景資訊
2024 年 9月 3日,Penpie 合約遭受重入攻擊,攻擊者在重入階段向合約新增流動性來冒充獎勵金額,從而獲取合約內原有的獎勵代幣。資產損失高達 2734 萬美元。
2024 年 5月,Penpie 平臺新增了推出了無需許可的資產池功能,即允許 Pendle 上的使用者可以在該平臺上自建任何 PT 或 YT 代幣的 LP 資金池,使用者在 Penpie 平臺上存入 LP 後,可以額外多獲得一份代幣獎勵。
- X 告警:https://x.com/PeckShieldAlert/status/1831072230651941093
- 前置交易(Create Pool):https://app.blocksec.com/explorer/tx/eth/0xfda0dde38fa4c5b0e13c506782527a039d3a87f93f9208c104ee569a642172d2
- 其中一筆攻擊交易:https://app.blocksec.com/explorer/tx/eth/0x56e09abb35ff12271fdb38ff8a23e4d4a7396844426a94c4d3af2e8b7a0a2813
- 被攻擊合約:https://etherscan.io/address/0x6e799758cee75dae3d84e09d40dc416ecf713652
Trace 分析
攻擊者首先在前置交易中新建了一個 market,並將 SY
地址設定為攻擊合約 0x4af4
隨後在攻擊交易中,攻擊者閃電貸了四種資產(由於四種資產的操作類似,我們選擇其中一種資產 wstETH 進行分析)
閃電貸內進行了這幾類操作
在 batchHarvestMarketRewards
函式中進行了重入攻擊
代幣流向分析
- 0x6e79.batchHarvestMarketRewards:
- 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
漏洞分析
CreatePool
任何使用者都可以在 Pendle 上註冊 Pool(market)
https://etherscan.io/address/0x588f5e5d85c85cac5de4a1616352778ecd9110d3#code
其中的 onlyVerifiedMarket
檢查,會檢查 pool 地址是否在 allMarkets
中。而任何人都可以建立池子,繞過這個限制。
https://vscode.blockscan.com/ethereum/0x45cF29F501d218Ad045EB8d622B69968E2d4Ef5C
batchHarvestMarketRewards
在 batchHarvestMarketRewards
函式中,透過計算呼叫 market.redeemRewards
函式前後的 MarketToken 數量差值,來得到作為獎勵代幣的 wstETH 數量。
攻擊者利用這個設計缺陷,呼叫 0x6e79.batchHarvestMarketRewards
函式觸發重入攻擊,使得 bounsTokens
的值增大。
https://vscode.blockscan.com/ethereum/0xff51c6b493c1e4df4e491865352353eadff0f9f8
batchHarvestMarketRewards(Part1)
redeemRewards
由於 market 合約為攻擊者建立的合約,其 SY
在建立時被設為了攻擊合約的地址。
https://vscode.blockscan.com/ethereum/0x40789E8536C668c6A249aF61c81b9dfaC3EB8F32
函式呼叫流程
redeemRewards -> _redeemRewards -> _updateAndDistributeRewards -> _updateAndDistributeRewardsForTwo -> _updateRewardIndex -> _redeemExternalReward -> StandardizedYield.claimRewards
SY
為攻擊合約地址,在 claimRewards
函式中進行重入。
[Reentrancy] addLiquiditySingleTokenKeepYt & depositMarket
[Reentrancy] addLiquiditySingleTokenKeepYt:deposit 16010 wstETH to [Pendle: RouterV4], get 8860 [MarketToken]
[Reentrancy] depositMarket:deposite 1751 [MarketToken] to [0x6e79], received 1751 [StakingToken]
重入攻擊 trace,透過 addLiquiditySingleTokenKeepYt
和 depositMarket
操作將 wstETH 轉換為 MarketToken ,並質押到 0x6e79合約中。
batchHarvestMarketRewards(Part2)
透過重入攻擊,使得合約在計算 originalBonusBalance
獎勵數量時誤以為獲得了 1751 的獎勵(實際上並沒有獲得任何獎勵,餘額多出來的部分是重入的時候新增流動性那部分)。
originalBonusBalance
和 leftBonusBalance
的值會按照 _harvestBatchMarketRewards -> _sendRewards -> _queueRewarder
的呼叫路徑傳遞到 _queueRewarder
函式中。
此時合約會向 0xd128 地址傳送 1751
_rewardToken
。
攻擊者在透過重入新增流動性時,所新增的代幣數量 1751 等於 0x6e79 合約中代幣餘額的數量 1751
,其目的是構造“新增獎勵”的數量等於“賬戶餘額”,使得接下來的 queueRewarder
函式將 0x6e79 合約中的所有代幣轉移到 0xd128。
queueNewRewards
queueNewRewards:Claim 1751 [MarketToken] to 0xd128
0xd128 從 0x6e79 處轉移獎勵代幣。其中0xd128是rewardPool合約。
multiclaim
multiclaim:get 1751 [MarketToken] from 0xd128
攻擊者從 0xd128 合約中領取獎勵(完成獲利,這筆資金的來源是 0x6e79 合約的餘額)
withdrawMarket
withdrawMarket:burn 1751 [StakingToken], get 1751 [MarketToken]
取回在重入中透過 depositMarket
存入的 1751
[MarketToken]
removeLiquiditySingleToken
removeLiquiditySingleToken:burn 10611 [MarketToken], get 18733 wstETH
此時攻擊者手裡持有原來的 8860
,加上攻擊所得 1751
,一共持有 10611
[MarketToken]
最終攻擊者移除 10611
流動性,獲得 18733
的 wstETH。
Repay flashloan
向閃電貸歸還 16010
wstETH,獲利 2723
wstETH。
後記
這次的攻擊事件影響挺大的,涉及的金額也是巨大。在事件發生以後,許多安全從業人員都對這件事情進行了分析,我也第一時間嘗試著從 trace 去分析這個攻擊事件。由於當時事發不久,還沒有廠商公佈詳細的漏洞分析結果,再加上個人叛逆的心態想著難道不參考別人的分析報告我就分析不出來了嗎,這次的攻擊事件分析是在一天的時間內硬啃 trace 分析得來的。這樣的分析對我來說進行得並不容易,且最終輸出的分析文件,也會缺乏了一些對專案架構與設計的理解,有骨沒肉,讀起來很乾巴。我反思了一下我為何會落入如此窘境,歸根究底還是對專案的不熟悉。在不熟悉專案的前提下做的攻擊分析,有形無意,味如嚼蠟。這是一個不可忽視的問題,我需要想想辦法。