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: 83/105
Findings: 1
Award: $24.06
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: Limbooo
Also found by: 0xDemon, 0xspryon, 14si2o_Flint, Aymen0909, Silvermist, alix40, btk, crypticdefense, erosjohn, falconhoof, jnforja, shaka, wangxx2026, y0ng0p3
24.0555 USDC - $24.06
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#L323-L326 https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L329-L331
Protocols that try to integrate with Revert Land, expecting V3Vault
to be ERC-4626 compliant, will multiple issues that may damage Revert Land's brand and limit Revert Land's growth in the market.
All official ERC-4626 requirements are on their official page. Non-compliant methods are listed below along with why they are not compliant and coded POCs demonstrating the issues.
V3Vault::maxDeposit
and V3Vault::maxMint
As specified in ERC-4626, both maxDeposit
and maxMint
must return the maximum amount that would allow to be used and not cause a revert. Also, if the deposits or mints are disabled at current moment, it MUST return 0.
ERC4626::maxDeposit
Non-compliant RequirementsMUST return the maximum amount of assets
deposit
would allow to be deposited forreceiver
and not cause a revert, which 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, like if deposits are entirely disabled (even temporarily) it MUST return 0.
ERC4626::maxMint
Non-compliant RequirementsMUST return the maximum amount of shares
mint
would allow to be deposited toreceiver
and not cause a revert, which 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, like if mints are entirely disabled (even temporarily) it MUST return 0.
However, in both V3Vault::maxDeposit
or V3Vault::maxMint
return the amount that cause a revert in deposit
or mint
. This is because they do not check if the amount exceeded the daily lend limit and if this is a case, it will cause a revert inside _deposit
function (where it used in both deposit
and mint
):
src/V3Vault.sol: 915: if (assets > dailyLendIncreaseLimitLeft) { 916: revert DailyLendIncreaseLimit(); 917: } else {
Furthermore, when dailyLendIncreaseLimitLeft == 0
that means the deposits and mints are temporarily disabled, while both V3Vault::maxDeposit
and V3Vault::maxMint
could return amounts that is more than 0. Based on ERC4626 requirements, it MUST return 0 in this case.
To run the POC, copy-paste it into V3Vault.t.sol
:
function testPOC_MaxDepositDoesNotReturnZeroWhenExceedsDailyLimit() public { uint256 dailyLendIncreaseLimitMin = vault.dailyLendIncreaseLimitMin(); uint256 depositAmount = dailyLendIncreaseLimitMin; vm.startPrank(WHALE_ACCOUNT); USDC.approve(address(vault), depositAmount); vault.deposit(depositAmount, WHALE_ACCOUNT); //Should return 0 to comply to ERC-4626. assertNotEq(vault.maxDeposit(address(WHALE_ACCOUNT)), 0); USDC.approve(address(vault), 1); vm.expectRevert(IErrors.DailyLendIncreaseLimit.selector); vault.deposit(1, WHALE_ACCOUNT); vm.stopPrank(); }
V3Vault::maxWithdraw
and V3Vault::maxRedeem
As specified in ERC-4626, both maxWithdraw
and maxRedeem
must return the maximum amount that would allow to be transferred from owner and not cause a revert. Also, if the withdrawals or redemption are disabled at current moment, it MUST return 0.
ERC4626::maxWithdraw
Non-compliant RequirementsMUST return the maximum amount of assets that could be transferred from
owner
throughwithdraw
and not cause a revert, which 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, like if withdrawals are entirely disabled (even temporarily) it MUST return 0.
ERC4626::maxRedeem
Non-compliant RequirementsMUST return the maximum amount of shares that could be transferred from
owner
throughredeem
and not cause a revert, which 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, like if redemption is entirely disabled (even temporarily) it MUST return 0.
However, in both V3Vault::maxWithdraw
or V3Vault::maxRedeem
return the amount that cause a revert in withdraw
or redeem
. This is because they do not check if the amount exceeded the current available balance in the vault and if this is a case, it will cause a revert inside _withdraw
function (where it used in both withdraw
and redeem
):
src/V3Vault.sol: 962: (, uint256 available,) = _getAvailableBalance(newDebtExchangeRateX96, newLendExchangeRateX96); 963: if (available < assets) { 964: revert InsufficientLiquidity(); 965: }
To run the POC, copy-paste it into V3Vault.t.sol
:
function testPOC_MaxWithdrawDoesNotReturnZeroWhenExceedsAvailableBalance() external { // maximized collateral loan _setupBasicLoan(true); uint256 amount = vault.maxRedeem(address(WHALE_ACCOUNT)); (,,, uint256 available,,,) = vault.vaultInfo(); //Should return available balance if it is less than owner balance to comply to ERC-4626. assertNotEq(vault.maxRedeem(address(WHALE_ACCOUNT)), available); vm.expectRevert(IErrors.InsufficientLiquidity.selector); vm.prank(WHALE_ACCOUNT); vault.redeem(amount, WHALE_ACCOUNT, WHALE_ACCOUNT); }
Manual review.
To address the non-compliance issues, the following changes are recommended:
diff --git a/src/V3Vault.sol b/src/V3Vault.sol index 64141ec..a25cebd 100644 --- a/src/V3Vault.sol +++ b/src/V3Vault.sol @@ -304,7 +304,12 @@ contract V3Vault is ERC20, Multicall, Ownable, IVault, IERC721Receiver, IErrors if (value >= globalLendLimit) { return 0; } else { - return globalLendLimit - value; + uint256 maxGlobalDeposit = globalLendLimit - value; + if (maxGlobalDeposit > dailyLendIncreaseLimitLeft) { + return dailyLendIncreaseLimitLeft; + } else { + return maxGlobalDeposit; + } } } @@ -315,19 +320,37 @@ contract V3Vault is ERC20, Multicall, Ownable, IVault, IERC721Receiver, IErrors if (value >= globalLendLimit) { return 0; } else { - return _convertToShares(globalLendLimit - value, lendExchangeRateX96, Math.Rounding.Down); + uint256 maxGlobalDeposit = globalLendLimit - value; + if (maxGlobalDeposit > dailyLendIncreaseLimitLeft) { + return _convertToShares(dailyLendIncreaseLimitLeft, lendExchangeRateX96, Math.Rounding.Down); + } else { + return _convertToShares(maxGlobalDeposit, lendExchangeRateX96, Math.Rounding.Down); + } } } /// @inheritdoc IERC4626 function maxWithdraw(address owner) external view override returns (uint256) { - (, uint256 lendExchangeRateX96) = _calculateGlobalInterest(); - return _convertToAssets(balanceOf(owner), lendExchangeRateX96, Math.Rounding.Down); + uint256 ownerBalance = balanceOf(owner); + (uint256 debtExchangeRateX96, uint256 lendExchangeRateX96) = _calculateGlobalInterest(); + (, uint256 available, ) = _getAvailableBalance(debtExchangeRateX96, lendExchangeRateX96); + if (available > ownerBalance) { + return _convertToAssets(ownerBalance, lendExchangeRateX96, Math.Rounding.Down); + } else { + return _convertToAssets(available, lendExchangeRateX96, Math.Rounding.Down); + } } /// @inheritdoc IERC4626 function maxRedeem(address owner) external view override returns (uint256) { - return balanceOf(owner); + uint256 ownerBalance = balanceOf(owner); + (uint256 debtExchangeRateX96, uint256 lendExchangeRateX96) = _calculateGlobalInterest(); + (, uint256 available, ) = _getAvailableBalance(debtExchangeRateX96, lendExchangeRateX96); + if (available > ownerBalance) { + return ownerBalance; + } else { + return available; + } }
The modified maxDeposit
function now correctly calculates the maximum deposit amount by considering both the global lend limit and the daily lend increase limit. If the calculated maximum global deposit exceeds the daily lend increase limit, the function returns the daily lend increase limit to comply with ERC-4626 requirements.
Similarly, the modified maxMint
ensures compliance with ERC-4626 by calculating the maximum mints amount for a given owner. It considers both the global lend limit and the daily lend increase limit as mentioned in maxDeposit
.
The modified maxWithdraw
function now correctly calculates the maximum withdrawal amount for a given owner. It ensures that the returned value does not exceed the available balance in the vault. If the available balance is greater than the owner's balance, it returns the owner's balance, otherwise it return the available balance to prevent potential reverts during withdrawal transactions. This adjustment aligns with ERC-4626 requirements by ensuring that withdrawals do not cause unexpected reverts and accurately reflect the available funds for withdrawal.
Similarly, the modified maxRedeem
function ensures compliance with ERC-4626 by calculating the maximum redemption amount for a given owner. It considers both the owner's balance and the available liquidity in the vault as mentioned in maxWithdraw
.
Other
#0 - c4-pre-sort
2024-03-20T15:48:06Z
0xEVom marked the issue as high quality report
#1 - c4-pre-sort
2024-03-20T15:48:10Z
0xEVom marked the issue as primary issue
#2 - c4-sponsor
2024-03-26T16:02:12Z
kalinbas (sponsor) confirmed
#3 - c4-judge
2024-03-31T00:14:11Z
jhsagd76 marked the issue as satisfactory
#4 - c4-judge
2024-04-01T15:34:07Z
jhsagd76 marked the issue as selected for report
#5 - kalinbas
2024-04-09T18:08:55Z