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: 25/105
Findings: 5
Award: $560.56
🌟 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/main/src/V3Vault.sol#L1250 https://github.com/code-423n4/2024-03-revert-lend/blob/435b054f9ad2404173f36f0f74a5096c894b12b7/src/V3Vault.sol#L1262
_resetDailyLendIncreaseLimit() function will return incorrect dailyLendIncreaseLimitLeft
and _resetDailyDebtIncreaseLimit function will return incorrect dailyDebtIncreaseLimitLeft
and users deposit and lend more allowed within a day than intended
function _resetDailyLendIncreaseLimit(uint256 newLendExchangeRateX96, bool force) internal { // daily lend limit reset handling uint256 time = block.timestamp / 1 days; if (force || time > dailyLendIncreaseLimitLastReset) { 1250 uint256 lendIncreaseLimit = _convertToAssets(totalSupply(), newLendExchangeRateX96, Math.Rounding.Up) * (Q32 + MAX_DAILY_LEND_INCREASE_X32) / Q32; dailyLendIncreaseLimitLeft = dailyLendIncreaseLimitMin > lendIncreaseLimit ? dailyLendIncreaseLimitMin : lendIncreaseLimit; dailyLendIncreaseLimitLastReset = time; } }
As you can see in the L1250, the limit becomes 110%
, not 10%
and I guess it is not intended.
This goes the same for _resetDailyDebtIncreaseLimit() function
Manual Review
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; } }
Other
#0 - c4-pre-sort
2024-03-21T14:14:11Z
0xEVom marked the issue as duplicate of #415
#1 - c4-pre-sort
2024-03-21T14:14:14Z
0xEVom marked the issue as sufficient quality report
#2 - c4-judge
2024-04-01T06:46:26Z
jhsagd76 marked the issue as satisfactory
🌟 Selected for report: JecikPo
Also found by: KupiaSec, SpicyMeatball, kennedy1030, kfx, linmiaomiao, t4sk
92.1136 USDC - $92.11
In the _getReferenceTokenPriceX96() function of the contract, at L304 overflow could occur and this will the function revert.
function _getReferenceTokenPriceX96(address token, uint256 cachedChainlinkReferencePriceX96) internal view returns (uint256 priceX96, uint256 chainlinkReferencePriceX96) { if (token == referenceToken) { return (Q96, chainlinkReferencePriceX96); } TokenConfig memory feedConfig = feedConfigs[token]; if (feedConfig.mode == Mode.NOT_SET) { revert NotConfigured(); } uint256 verifyPriceX96; bool usesChainlink = ( feedConfig.mode == Mode.CHAINLINK_TWAP_VERIFY || feedConfig.mode == Mode.TWAP_CHAINLINK_VERIFY || feedConfig.mode == Mode.CHAINLINK ); bool usesTWAP = ( feedConfig.mode == Mode.CHAINLINK_TWAP_VERIFY || feedConfig.mode == Mode.TWAP_CHAINLINK_VERIFY || feedConfig.mode == Mode.TWAP ); if (usesChainlink) { uint256 chainlinkPriceX96 = _getChainlinkPriceX96(token); chainlinkReferencePriceX96 = cachedChainlinkReferencePriceX96 == 0 ? _getChainlinkPriceX96(referenceToken) : cachedChainlinkReferencePriceX96; 304 chainlinkPriceX96 = (10 ** referenceTokenDecimals) * chainlinkPriceX96 * Q96 / chainlinkReferencePriceX96 / (10 ** feedConfig.tokenDecimals); if (feedConfig.mode == Mode.TWAP_CHAINLINK_VERIFY) { verifyPriceX96 = chainlinkPriceX96; } else { priceX96 = chainlinkPriceX96; } } if (usesTWAP) { uint256 twapPriceX96 = _getTWAPPriceX96(feedConfig); if (feedConfig.mode == Mode.CHAINLINK_TWAP_VERIFY) { verifyPriceX96 = twapPriceX96; } else { priceX96 = twapPriceX96; } } if (feedConfig.mode == Mode.CHAINLINK_TWAP_VERIFY || feedConfig.mode == Mode.TWAP_CHAINLINK_VERIFY) { _requireMaxDifference(priceX96, verifyPriceX96, feedConfig.maxDifference); } }
At L304, if the referenceToken = ETH
and token = BTC
for example, the chainlinkPriceX96
is 70,000 * 2 ** 96
, and referenceTokenDecimal = 18
and this is enough for overflow and this function will revert.
Plus other functions which is calling this function directly or indirectly will revert.
Manual Review
You can use FullMath
library to prevent overflow here.
Under/Overflow
#0 - c4-pre-sort
2024-03-22T07:25:45Z
0xEVom marked the issue as duplicate of #409
#1 - c4-pre-sort
2024-03-22T07:25:48Z
0xEVom marked the issue as sufficient quality report
#2 - c4-judge
2024-03-31T14:51:09Z
jhsagd76 changed the severity to 2 (Med Risk)
#3 - c4-judge
2024-04-01T13:28:25Z
jhsagd76 marked the issue as satisfactory
🌟 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
In the overall codebase totalSupply
is for shares
and globalLendLimit
is for assets
. But in the _deposit() function at L906 totalSupply
is compared to the globalLendLimit
and this should not happen and assets can be lent more than the globalLendLimit
.
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); 906 if (totalSupply() > globalLendLimit) { revert GlobalLendLimit(); } if (assets > dailyLendIncreaseLimitLeft) { revert DailyLendIncreaseLimit(); } else { dailyLendIncreaseLimitLeft -= assets; } emit Deposit(msg.sender, receiver, assets, shares); }
totalSupply
stands for shares
and globalLendLimit
stands for assets
and they are completely different terms. They must not be compared together or assets could be lent more than globalLendLimit
.
Manual Review
Should convert totalSupply
to assets and then compare with globalLendLimit
function _deposit(address receiver, uint256 amount, bool isShare, bytes memory permitData) internal returns (uint256 assets, uint256 shares) { /// lines of code _mint(receiver, shares); - if (totalSupply() > globalLendLimit) { + if (_convertToAssets(totalSupply(), lendExchangeRateX96, Math.Rounding.Down) > globalLendLimit) { revert GlobalLendLimit(); } if (assets > dailyLendIncreaseLimitLeft) { revert DailyLendIncreaseLimit(); } else { dailyLendIncreaseLimitLeft -= assets; } emit Deposit(msg.sender, receiver, assets, shares); }
Invalid Validation
#0 - c4-pre-sort
2024-03-18T19:02:43Z
0xEVom marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-03-18T19:03:48Z
0xEVom marked the issue as duplicate of #324
#2 - c4-judge
2024-04-01T07:41:32Z
jhsagd76 marked the issue as satisfactory
🌟 Selected for report: kennedy1030
327.5899 USDC - $327.59
If onlyFee is set as false, the protocol should collect reward from only the fees. However if the borrowers(token owners) decrease the liquity for some reason, the decreased amount is added to the feeAmount0
and feeAmount1
and it is treated and fee.
And this can be included in the calculation for reward, if the borrowers doesn't take the amount in time.
When execute function of the AutoExit contract is called, at [L143] some values are calculated and later used for reward.
(state.amount0, state.amount1, state.feeAmount0, state.feeAmount1) = _decreaseFullLiquidityAndCollect( params.tokenId, state.liquidity, params.amountRemoveMin0, params.amountRemoveMin1, params.deadline );
If onlyFee
is set True, reward is calculated as follows.
if (config.onlyFees) { state.amount0 -= state.feeAmount0 * params.rewardX64 / Q64; state.amount1 -= state.feeAmount1 * params.rewardX64 / Q64; }
Above state.feeAmount0
and state.feeAmount1
is calculated like following.
function _decreaseFullLiquidityAndCollect( uint256 tokenId, uint128 liquidity, uint256 amountRemoveMin0, uint256 amountRemoveMin1, uint256 deadline ) internal returns (uint256 amount0, uint256 amount1, uint256 feeAmount0, uint256 feeAmount1) { if (liquidity > 0) { // store in temporarely "misnamed" variables - see comment below (feeAmount0, feeAmount1) = nonfungiblePositionManager.decreaseLiquidity( INonfungiblePositionManager.DecreaseLiquidityParams( tokenId, liquidity, amountRemoveMin0, amountRemoveMin1, deadline ) ); } (amount0, amount1) = nonfungiblePositionManager.collect( INonfungiblePositionManager.CollectParams(tokenId, address(this), type(uint128).max, type(uint128).max) ); // fee amount is what was collected additionally to liquidity amount feeAmount0 = amount0 - feeAmount0; feeAmount1 = amount1 - feeAmount1; }
feeAmount0
and feeAmount1
are the amounts for the uncollected ones. But if the liquidity owner decreases the liquidity for some reason, the feeAmount0
and feeAmount1
contains not only but also the decreased the liquidity amount. So in case of the owner doesn't collect this amount Automator.execute is called, the liquidity owner should pay more reward than expected without even knowing
Manual Review
feeAmount0
and feeAmount1
should be calculated correctly not to include the decreased liquidity. Maybe feeGrowthInside
in the UniswapV3, could be used
Other
#0 - c4-pre-sort
2024-03-22T10:46:52Z
0xEVom marked the issue as duplicate of #216
#1 - c4-pre-sort
2024-03-22T10:47:01Z
0xEVom marked the issue as sufficient quality report
#2 - c4-judge
2024-03-31T15:50:44Z
jhsagd76 changed the severity to 2 (Med Risk)
#3 - c4-judge
2024-04-01T13:28:16Z
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
There are mainly two ways for borrowers to repay debt fully.
Borrowers are able to call LeverageTransformer.leverageDown function using V3Vault.transform function.
And if they try to repay debt fully using the LeverageTransformer.leverageDown function, [V3Vault.repay] function is called.
SafeERC20.safeApprove(IERC20(token), msg.sender, amount); IVault(msg.sender).repay(params.tokenId, amount, false);
_cleanupLoan() function is called and the NFT token
is return to the owner and vault is no longer the owner of the token
. However in the transform function, after making the call there is a check for ownership of the issued NFT token
.
(bool success,) = transformer.call(data); if (!success) { revert TransformFailed(); } // may have changed in the meantime tokenId = transformedTokenId; // check owner not changed (NEEDED because token could have been moved somewhere else in the meantime) 531 address owner = nonfungiblePositionManager.ownerOf(tokenId); 532 if (owner != address(this)) { revert Unauthorized(); }
At L531, the owner is not the vault itself and the owner is the original token owner. And this will make revert in the following check at L532
Manual Review
After call in the transform function, it would be good to add check for the NFT token
debt and skip the check at At L531-534, if possible.
But I am not sure it is not good way and it would be good for borrowers to use V3Vault.repay
for repaying debt fully.
ERC721
#0 - c4-pre-sort
2024-03-22T10:48:42Z
0xEVom marked the issue as duplicate of #172
#1 - c4-pre-sort
2024-03-22T10:48:47Z
0xEVom marked the issue as sufficient quality report
#2 - c4-pre-sort
2024-03-23T07:53:00Z
0xEVom marked the issue as insufficient quality report
#3 - c4-judge
2024-03-31T13:43:29Z
jhsagd76 changed the severity to QA (Quality Assurance)
#4 - c4-judge
2024-04-01T13:38:17Z
jhsagd76 marked the issue as grade-b