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: 54/105
Findings: 2
Award: $102.40
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Aymen0909
Also found by: KupiaSec, Topmark, befree3x, kennedy1030, linmiaomiao, pynschon
92.1136 USDC - $92.11
https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol#L906 https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol#L301-L320
Deposit would Not Revert when Total Supply is too Excessive to handle globalLendLimit and in addition It would Revert when Total Supply is within accurate range, this problem is due to oversight in validation implementation
function _deposit(address receiver, uint256 amount, bool isShare, bytes memory permitData) internal returns (uint256 assets, uint256 shares) { (, uint256 newLendExchangeRateX96) = _updateGlobalInterest(); _resetDailyLendIncreaseLimit(newLendExchangeRateX96, false); if (isShare) { shares = amount; assets = _convertToAssets(shares, newLendExchangeRateX96, Math.Rounding.Up); } else { assets = amount; shares = _convertToShares(assets, newLendExchangeRateX96, Math.Rounding.Down); } if (permitData.length > 0) { (ISignatureTransfer.PermitTransferFrom memory permit, bytes memory signature) = abi.decode(permitData, (ISignatureTransfer.PermitTransferFrom, bytes)); permit2.permitTransferFrom( permit, ISignatureTransfer.SignatureTransferDetails(address(this), assets), msg.sender, signature ); } else { // fails if not enough token approved SafeERC20.safeTransferFrom(IERC20(asset), msg.sender, address(this), assets); } _mint(receiver, shares); >>> if (totalSupply() > globalLendLimit) { revert GlobalLendLimit(); }
The code above shows how _deposit(...) function is implemented in the v3Vault contract, the point of interest from the pointer is the validation that is done to ensure that total supply is not above the global lend limit. However there is an oversight in this implementation, Total supply should be first converted to Asset before the validation is done, a reference to similar circumstance where totalsupply() and globalLendLimit are used can be noted from the maxDeposit(...) & maxMint(...) functions provided below. totalSupply() was first converted to asset and the resulting value was used for the validation, total supply was not used directly as they represent different variables.
function maxDeposit(address) external view override returns (uint256) { (, uint256 lendExchangeRateX96) = _calculateGlobalInterest(); >>> uint256 value = _convertToAssets(totalSupply(), lendExchangeRateX96, Math.Rounding.Up); >>> if (value >= globalLendLimit) { return 0; } else { return globalLendLimit - value; } } /// @inheritdoc IERC4626 function maxMint(address) external view override returns (uint256) { (, uint256 lendExchangeRateX96) = _calculateGlobalInterest(); >>> uint256 value = _convertToAssets(totalSupply(), lendExchangeRateX96, Math.Rounding.Up); >>> if (value >= globalLendLimit) { return 0; } else { return _convertToShares(globalLendLimit - value, lendExchangeRateX96, Math.Rounding.Down); } }
Manual Review
The correct implementation should be adjusted to the lend limit validation as provided in the code below. Total supply should be converted to asset before validation
function _deposit(address receiver, uint256 amount, bool isShare, bytes memory permitData) internal returns (uint256 assets, uint256 shares) { (, uint256 newLendExchangeRateX96) = _updateGlobalInterest(); _resetDailyLendIncreaseLimit(newLendExchangeRateX96, false); ... _mint(receiver, shares); --- if (totalSupply() > globalLendLimit) { +++ if (_convertToAssets(totalSupply(), lendExchangeRateX96, Math.Rounding.Up) > globalLendLimit) { revert GlobalLendLimit(); }
Invalid Validation
#0 - c4-pre-sort
2024-03-18T19:02:45Z
0xEVom marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-03-18T19:03:56Z
0xEVom marked the issue as duplicate of #324
#2 - c4-judge
2024-04-01T07:41:35Z
jhsagd76 marked the issue as satisfactory
#3 - c4-judge
2024-04-01T15:41:48Z
jhsagd76 changed the severity to 2 (Med Risk)
🌟 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
10.2896 USDC - $10.29
https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol#L1025
Wrong and Inconsistent lent value Calculation in contract which would cause wrong data updated in relation to functions calling _getAvailableBalance(...) function in V3Vault contract.
function _getAvailableBalance(uint256 debtExchangeRateX96, uint256 lendExchangeRateX96) internal view returns (uint256 balance, uint256 available, uint256 reserves) { balance = totalAssets(); uint256 debt = _convertToAssets(debtSharesTotal, debtExchangeRateX96, Math.Rounding.Up); >>> uint256 lent = _convertToAssets(totalSupply(), lendExchangeRateX96, Math.Rounding.Down); reserves = balance + debt > lent ? balance + debt - lent : 0; available = balance > reserves ? balance - reserves : 0; }
The function above shows how _getAvailableBalance(...) function is implemented in the V3Vault.sol contract, as noted from the pointer lent is calculated by converting totalSupply() to Assets, the problem is that Math.Rounding.Down
was used as the third parameter instead of Math.Rounding.Up
which can be proven by other similar scenarios, which appeared in 9 different places in the contract, Math.Rounding.Up
was used all through in similar situations except in the _getAvailableBalance(...) function as noted above. As can be confirmed from L213, L303, L314, L1134, L1225, L1250 & L1262, this mistake is totally wrong and inconsistent with lent calculation.
Manual Review
The necessary adjustment should be done to the _getAvailableBalance(...) function as provided below, Math.Rounding.Up should be used instead of Math.Rounding.Down
function _getAvailableBalance(uint256 debtExchangeRateX96, uint256 lendExchangeRateX96) internal view returns (uint256 balance, uint256 available, uint256 reserves) { balance = totalAssets(); uint256 debt = _convertToAssets(debtSharesTotal, debtExchangeRateX96, Math.Rounding.Up); --- uint256 lent = _convertToAssets(totalSupply(), lendExchangeRateX96, Math.Rounding.Down); +++ uint256 lent = _convertToAssets(totalSupply(), lendExchangeRateX96, Math.Rounding.Up); reserves = balance + debt > lent ? balance + debt - lent : 0; available = balance > reserves ? balance - reserves : 0; }
Error
#0 - c4-pre-sort
2024-03-21T09:34:05Z
0xEVom marked the issue as duplicate of #254
#1 - c4-pre-sort
2024-03-21T09:34:08Z
0xEVom marked the issue as sufficient quality report
#2 - c4-judge
2024-03-31T15:42:59Z
jhsagd76 changed the severity to QA (Quality Assurance)
#3 - c4-judge
2024-03-31T15:43:29Z
jhsagd76 marked the issue as grade-a
#4 - c4-judge
2024-04-01T07:46:14Z
jhsagd76 marked the issue as grade-c
#5 - Topmark1
2024-04-02T13:50:48Z
Please I would implore Judge to have a look at the Judgement and grading of this issue as it is a duplicate of https://github.com/code-423n4/2024-03-revert-lend-findings/issues/254 , as can be noted from C4 ruling at
https://docs.code4rena.com/awarding/judging-criteria/supreme-court-decisions-fall-2023#verdict-similar-exploits-under-a-single-issue
Verdict: Similar exploits under a single issue
The findings are duplicates if they share the same root cause.
More specifically, if fixing the Root Cause (in a reasonable manner) would cause the finding to no longer be exploitable, then the findings are duplicates.
Would implore Judge to please reconsider, thanks
#6 - c4-judge
2024-04-04T08:45:32Z
jhsagd76 marked the issue as grade-b
#7 - jhsagd76
2024-04-04T08:46:02Z
This is because I can only retain a rating for one QA report.