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: 72/105
Findings: 3
Award: $46.11
π Selected for report: 0
π Solo Findings: 0
π Selected for report: 0xjuan
Also found by: 0rpse, 0x175, 0xAlix2, 0xBugSlayer, 0xloscar01, Ali-_-Y, Arz, CaeraDenoir, JohnSmith, Ocean_Sky, SpicyMeatball, alix40, ayden, falconhoof, givn, iamandreiski, kinda_very_good, nmirchev8, nnez, novamanbg, stackachu, wangxx2026
17.3162 USDC - $17.32
Can be blocked from being liquidated
With the liquidate method. we can see that the logic of liquidation is: 1.Check if the tokenId is in an unhealthy state 2.Calculate the cost of liquidation 3.If there is a bad debt, let the lender share it 4.from the liquidator to pull the liquidation needs of the fee 5.Send the reward to the liquidator 6.Return the liquidated tokenId to the owner
The problem occurs when the tokenId is returned to the owner. the return code is implemented in the _cleanupLoan method with the following code:
function _cleanupLoan(uint256 tokenId, uint256 debtExchangeRateX96, uint256 lendExchangeRateX96, address owner) internal { _removeTokenFromOwner(owner, tokenId); _updateAndCheckCollateral(tokenId, debtExchangeRateX96, lendExchangeRateX96, loans[tokenId].debtShares, 0); delete loans[tokenId]; nonfungiblePositionManager.safeTransferFrom(address(this), owner, tokenId); // @audit-issue If the owner is a contract and does not support onERC721Received, it will revert, resulting in a failure of the liquidation. emit Remove(tokenId, owner); }
And the owner can be set by the recipient parameter in the create method. Then without any checking, set the recipient to owner in onERC721Received method. the implementation is as follows: https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L429-L477
function onERC721Received(address, address from, uint256 tokenId, bytes calldata data) external override returns (bytes4) { ... if (transformedTokenId == 0) { address owner = from; if (data.length > 0) { owner = abi.decode(data, (address)); // @audit-issue Get the specified parameter as owner } loans[tokenId] = Loan(0); _addTokenToOwner(owner, tokenId); // @audit-issue Set as owner emit Add(tokenId, owner, 0); } else { ... } return IERC721Receiver.onERC721Received.selector; }
_addTokenToOwner method: https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L1297-L1301
function _addTokenToOwner(address to, uint256 tokenId) internal { ownedTokensIndex[tokenId] = ownedTokens[to].length; ownedTokens[to].push(tokenId); tokenOwner[tokenId] = to; // @audit-issue Store the correspondence between tokenid and owner. }
The implementation of getting the owner of the liquidation return tokenId is as follows: https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L741-L744
address owner = tokenOwner[params.tokenId]; // @audit-issue The owner may be a contract. // disarm loan and send remaining position to owner _cleanupLoan(params.tokenId, state.newDebtExchangeRateX96, state.newLendExchangeRateX96, owner);
Manual Review
It is recommended that only the EOA address can be used as the owner. If the owner is a contract address, it is undesirable to check whether it supports onERC721Received. If the owner is a contract, the controller of the contract can control when to return normally and when to return abnormally, which also prevents it from being liquidated.
ERC721
#0 - c4-pre-sort
2024-03-18T18:41:21Z
0xEVom marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-03-20T08:35:47Z
0xEVom marked the issue as duplicate of #54
#2 - c4-judge
2024-03-31T16:09:02Z
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#L910-L914
maxDeposit, maxMint, maxWithdraw, maxRedeem methods are not fully implemented according to the ERC4642 standard, which may cause issues with integration in third-party dapps.
The maxDeposit implementation is as follows:
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; } }
Global deposit limits are supported, but not daily limits.
In the _deposit method, we can see the implementation of the daily limit https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L906-L914
function _deposit(address receiver, uint256 amount, bool isShare, bytes memory permitData) internal returns (uint256 assets, uint256 shares) { ... if (totalSupply() > globalLendLimit) { // @audit-issue Global Limit revert GlobalLendLimit(); } if (assets > dailyLendIncreaseLimitLeft) { // @audit-issue Daily Limit revert DailyLendIncreaseLimit(); } else { dailyLendIncreaseLimitLeft -= assets; } emit Deposit(msg.sender, receiver, assets, shares); }
Required by the ERC4642 standard: https://eips.ethereum.org/EIPS/eip-4626#maxdeposit
MUST return the maximum amount of assets deposit would allow to be deposited for receiver and not cause a revert, which MUST NOT be higher than the actual maximum that would be accepted (it should underestimate if necessary). This assumes that the user has infinite assets, i.e. MUST NOT rely on balanceOf of asset.
Only global limits, not daily limits
function maxMint(address) external view override returns (uint256) { (, uint256 lendExchangeRateX96) = _calculateGlobalInterest(); uint256 value = _convertToAssets(totalSupply(), lendExchangeRateX96, Math.Rounding.Up); if (value >= globalLendLimit) { // @audit-issue global limits return 0; } else { return _convertToShares(globalLendLimit - value, lendExchangeRateX96, Math.Rounding.Down); } }
Limit on available balance in _withdraw method _withdraw
function _withdraw(address receiver, address owner, uint256 amount, bool isShare) internal returns (uint256 assets, uint256 shares) { ... (, uint256 available,) = _getAvailableBalance(newDebtExchangeRateX96, newLendExchangeRateX96); if (available < assets) { // @audit-issue limit available revert InsufficientLiquidity(); } ... }
function maxRedeem(address owner) external view override returns (uint256) { return balanceOf(owner); // @audit-issue Limit only own balance }
Manual Review
The maximum limit returned by these four methods is the same as in the implementation.
ERC4626
#0 - c4-pre-sort
2024-03-20T13:22:19Z
0xEVom marked the issue as primary issue
#1 - c4-pre-sort
2024-03-20T15:49:00Z
0xEVom marked the issue as duplicate of #249
#2 - c4-pre-sort
2024-03-24T09:46:30Z
0xEVom marked the issue as sufficient quality report
#3 - c4-judge
2024-04-01T01:34:21Z
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
10.2896 USDC - $10.29
https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L827-L828 https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L1246-L1256 https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L1258-L1268
Forced limit updates can result in unexpected increases in the day's limit
In the setLimits method, we can see that updating the dailyLendIncreaseLimitMin, dailyDebtIncreaseLimitMin will do a limit force update limit.
function setLimits( uint256 _minLoanSize, uint256 _globalLendLimit, uint256 _globalDebtLimit, uint256 _dailyLendIncreaseLimitMin, uint256 _dailyDebtIncreaseLimitMin ) external { ... // force reset daily limits with new values _resetDailyLendIncreaseLimit(newLendExchangeRateX96, true); // @audit-issue Updating Deposit Limits _resetDailyDebtIncreaseLimit(newLendExchangeRateX96, true); // @audit-issue Update Debt Limit ... ); }
However, the forced limit update does not take into account the amount already used on that day, which will result in the updated limit being equal to the amount used on that day + the updated limit.
Take the _resetDailyLendIncreaseLimit method as an example, which is implemented as follows:
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; dailyLendIncreaseLimitLeft = dailyLendIncreaseLimitMin > lendIncreaseLimit ? dailyLendIncreaseLimitMin : lendIncreaseLimit; dailyLendIncreaseLimitLastReset = time; } }
For example, in the case where lendIncreaseLimit is less than dailyLendIncreaseLimitMin, and the deposit limit for today was originally 1 million, with 500,000 already used. Now, if dailyLendIncreaseLimitMin is changed to 800,000, the updated limit for today would be 1.3 million. However, the correct result should be that there is only 300,000 remaining.
_resetDailyDebtIncreaseLimit has the same problem
Manual Review
Record the amount already used for the day in the variable todayUsed, and when forcing an update
dailyLendIncreaseLimitLeft -= todayUsed.
Then set todayUsed to 0 when updating the limit normally.
Math
#0 - c4-pre-sort
2024-03-21T14:25:35Z
0xEVom marked the issue as primary issue
#1 - c4-pre-sort
2024-03-21T14:25:39Z
0xEVom marked the issue as sufficient quality report
#2 - c4-pre-sort
2024-03-23T20:02:33Z
0xEVom marked the issue as insufficient quality report
#3 - 0xEVom
2024-03-23T20:02:34Z
QA
#4 - jhsagd76
2024-03-31T09:02:05Z
agree, QA
#5 - c4-judge
2024-03-31T09:02:13Z
jhsagd76 changed the severity to QA (Quality Assurance)
#6 - jhsagd76
2024-03-31T09:02:26Z
L-B
#7 - c4-judge
2024-04-01T06:45:04Z
jhsagd76 marked the issue as grade-b