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: 41/105
Findings: 4
Award: $281.43
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: JohnSmith
Also found by: Arz, Aymen0909, BowTiedOriole, DanielArmstrong, FastChecker, KupiaSec, deepplus, kennedy1030, kfx, shaka
38.4591 USDC - $38.46
https://github.com/code-423n4/2024-03-revert-lend/blob/457230945a49878eefdc1001796b10638c1e7584/src/V3Vault.sol#L44 https://github.com/code-423n4/2024-03-revert-lend/blob/457230945a49878eefdc1001796b10638c1e7584/src/V3Vault.sol#L1258-L1268 https://github.com/code-423n4/2024-03-revert-lend/blob/457230945a49878eefdc1001796b10638c1e7584/src/V3Vault.sol#L43 https://github.com/code-423n4/2024-03-revert-lend/blob/457230945a49878eefdc1001796b10638c1e7584/src/V3Vault.sol#L1246-L1256
The protocol imposes a daily limit on the amount deposited and borrowed of a 10% increase of the total lent assets. As described in the whitepaper, the daily limit "functions as an emergency backstop to protect in case of a catastrophic exploit, whether due to unforeseen errors in the protocol’s logic or the exploitation of counterparty risk in one of the collateral tokens."
The daily limit is updated in the _resetDailyLendIncreaseLimit
and _resetDailyDebtIncreaseLimit
functions, but
is calculated incorrectly.
File: V3Vault.sol 1250 uint256 lendIncreaseLimit = _convertToAssets(totalSupply(), newLendExchangeRateX96, Math.Rounding.Up) 1251 * (Q32 + MAX_DAILY_LEND_INCREASE_X32) / Q32;
The daily limit is not set to 10% of the total lent assets, but to 110% of the total lent assets.
lent * (Q32 + uint32(Q32 / 10)) / Q32 = lent * (Q32 + 0.1 * Q32) / Q32 = lent * 1.1
The daily limit protection is not functioning as intended, as it allows deposits of 110% of the total lent assets and borrows of 100% of the total lent assets. This breaks the intended protection mechanism against catastrophic exploits.
Add the following code to the contract V3Vault.t.sol
and run forge test --mt test_DailyBorrowIncreaseOver100PerCent
.
</details> <details> <summary>Deposit</summary>function test_DailyBorrowIncreaseOver100PerCent() external { // Setup vault.setLimits(0, 100e6, 10e6, 100e6, 0.5e6); skip(1 days); vm.startPrank(WHALE_ACCOUNT); USDC.approve(address(vault), type(uint256).max); vault.deposit(8e6, WHALE_ACCOUNT); vm.stopPrank(); // Add collateral vm.startPrank(TEST_NFT_ACCOUNT); NPM.approve(address(vault), TEST_NFT); vault.create(TEST_NFT, TEST_NFT_ACCOUNT); vm.stopPrank(); // Total lent is 8 USDC (, uint256 lent,,,,,) = vault.vaultInfo(); assertEq(lent, 8e6); // Daily debt increase should be max(0.5, 8 * 0.1) = 0.8 USDC, // but we borrow 8 USDC (all supply) vm.prank(TEST_NFT_ACCOUNT); vault.borrow(TEST_NFT, 8e6); }
Add the following code to the contract V3Vault.t.sol
and run forge test --mt test_DailyLendIncreaseOver100PerCent
.
</details>function test_DailyLendIncreaseOver100PerCent() external { // Setup vault.setLimits(0, 100_000e6, 100_000e6, 1_000e6, 1_000e6); uint256 amount = 1_000e6; vm.prank(WHALE_ACCOUNT); USDC.approve(address(vault), type(uint256).max); // Deposit 1_000 USDC per day for 20 days for (uint256 i = 0; i < 20; i++) { vm.prank(WHALE_ACCOUNT); vault.deposit(amount, WHALE_ACCOUNT); skip(1 days); } // Total lent is 20_000 USDC (, uint256 lent,,,,,) = vault.vaultInfo(); assertEq(lent, 20_000e6); // Daily lend increase should be max(1_000, 20_000 * 0.1) = 2_000 USDC, // but we deposit 21_000 USDC vm.prank(WHALE_ACCOUNT); vault.deposit(21_000e6, WHALE_ACCOUNT); }
Manual inspection.
File: V3Vault.sol function _resetDailyLendIncreaseLimit(uint256 newLendExchangeRateX96, bool force) internal { // daily lend limit reset handling uint256 time = block.timestamp / 1 days; if (force || time > dailyLendIncreaseLimitLastReset) { uint256 lendIncreaseLimit = _convertToAssets(totalSupply(), newLendExchangeRateX96, Math.Rounding.Up) - * (Q32 + MAX_DAILY_LEND_INCREASE_X32) / Q32; + * MAX_DAILY_LEND_INCREASE_X32 / Q32; dailyLendIncreaseLimitLeft = dailyLendIncreaseLimitMin > lendIncreaseLimit ? dailyLendIncreaseLimitMin : lendIncreaseLimit; dailyLendIncreaseLimitLastReset = time; } } function _resetDailyDebtIncreaseLimit(uint256 newLendExchangeRateX96, bool force) internal { // daily debt limit reset handling uint256 time = block.timestamp / 1 days; if (force || time > dailyDebtIncreaseLimitLastReset) { uint256 debtIncreaseLimit = _convertToAssets(totalSupply(), newLendExchangeRateX96, Math.Rounding.Up) - * (Q32 + MAX_DAILY_DEBT_INCREASE_X32) / Q32; + * MAX_DAILY_DEBT_INCREASE_X32 / Q32; dailyDebtIncreaseLimitLeft = dailyDebtIncreaseLimitMin > debtIncreaseLimit ? dailyDebtIncreaseLimitMin : debtIncreaseLimit; dailyDebtIncreaseLimitLastReset = time; } }
Error
#0 - c4-pre-sort
2024-03-21T14:12:16Z
0xEVom marked the issue as duplicate of #415
#1 - c4-pre-sort
2024-03-21T14:12:19Z
0xEVom marked the issue as sufficient quality report
#2 - c4-judge
2024-04-01T01:22:28Z
jhsagd76 marked the issue as satisfactory
221.1232 USDC - $221.12
https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L592 https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L703 https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L1270-L1279
When a loan is created by calling the borrow
function in V3Vault
the healthy check only requires that the collateral value is greater than the debt value. This means that a loan can be liquidated in the same block that it was created.
Given that the debt value is constantly increasing with the update of newDebtExchangeRateX96
, if a loan is created using the maximum loan to value ratio, it can be liquidated just in the next block after its creation.
Loans can be liquidated in the block just after their creation.
Add the following code to the contract V3Vault.t.sol
and run forge test --mt test_LiquidateImmediately
.
function test_LiquidateImmediately() external { _setupBasicLoan(true); (uint256 debtShares) = vault.loans(TEST_NFT); skip(12); vm.roll(block.number + 1); vm.startPrank(WHALE_ACCOUNT); USDC.approve(address(vault), type(uint256).max); vault.liquidate(IVault.LiquidateParams(TEST_NFT, debtShares, 0, 0, WHALE_ACCOUNT, "")); vm.stopPrank(); }
Manual inspection.
Require in the loan creation that the collateral value is higher than the debt value in a certain percentage to serve as a safety margin.
Invalid Validation
#0 - c4-pre-sort
2024-03-20T06:47:05Z
0xEVom marked the issue as duplicate of #412
#1 - c4-pre-sort
2024-03-20T06:47:08Z
0xEVom marked the issue as sufficient quality report
#2 - c4-pre-sort
2024-03-20T06:49:58Z
0xEVom marked the issue as duplicate of #363
#3 - c4-judge
2024-04-01T01:23:21Z
jhsagd76 marked the issue as satisfactory
🌟 Selected for report: Limbooo
Also found by: 0xDemon, 0xspryon, 14si2o_Flint, Aymen0909, Silvermist, alix40, btk, crypticdefense, erosjohn, falconhoof, jnforja, shaka, wangxx2026, y0ng0p3
18.5042 USDC - $18.50
https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L301-L309 https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L312-L320 https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L910-L911
The documentation states that V3Vault
: Should comply with ERC/EIP4626
, but the maxDeposit
and maxMint
functions do not comply with the EIP-4626 standard, which says the following for these functions:
MUST NOT be higher than the actual maximum that would be accepted (it should underestimate if necessary).
MUST factor in both global and user-specific limits.
However, the current implementations do not take into account the daily lend increase limit.
Integrations that use the values returned by maxDeposit
and maxMint
functions to call the deposit
and mint
functions will revert on their execution.
Manual inspection.
Account for the daily lend increase limit in the calculations of maxDeposit
and maxMint
functions.
Other
#0 - c4-pre-sort
2024-03-22T16:38:54Z
0xEVom marked the issue as duplicate of #249
#1 - c4-pre-sort
2024-03-22T16:38:57Z
0xEVom marked the issue as sufficient quality report
#2 - c4-judge
2024-04-01T01:22:59Z
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
In the liquidation process, the liquidator must send the value of the debt shares for the position to be liquidated and there is a check to ensure that this value matches the current debt of the position.
The owner of the position can front-run the liquidator by repaying the minimum amount of debt (1 share) and make the liquidation fail, effectively DoSing the liquidation process.
The amount of the repayment can be as low as 1 wei so the effective cost of preventing the liquidation is the gas cost of the transaction. Depending on the amount of debt, the owner of the position can be incentivized to prevent the liquidation by repaying the minimum amount. Note that the liquidator will also incur the gas cost of the failed liquidation so, if he is front-run multiple times, will be discouraged from continuing trying.
Another important remark is that the documentation states that "the protocol should be able to be deployed on any EVM compatible chain", so depending on the chain, the gas cost of the transaction can be very low, making the attack more feasible.
The owner of a position can prevent the liquidation of his position by repaying the minimum amount of debt, DoSing the liquidation process, and disincentivizing liquidators from trying to liquidate positions.
Manual inspection.
Consider removing the check, or checking instead that the amount of debt passed by the liquidator is not lower than the current debt of the position.
DoS
#0 - c4-pre-sort
2024-03-18T18:13:52Z
0xEVom marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-03-18T18:14:29Z
0xEVom marked the issue as duplicate of #231
#2 - c4-pre-sort
2024-03-22T12:02:44Z
0xEVom marked the issue as duplicate of #222
#3 - c4-judge
2024-03-31T16:06:01Z
jhsagd76 marked the issue as satisfactory