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: 21/105
Findings: 3
Award: $715.48
π Selected for report: 0
π Solo Findings: 0
π Selected for report: alix40
Also found by: 0xPhantom, Norah, ktg, lanrebayode77
159.2087 USDC - $159.21
https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol#L954
A borrower can use the liquidity of the vault without paying any fees. A liquidator can use the liquidity of the protocol to make a liquidation. The functionality to do a liquidation with an Uniswap flashLoan is useless.
A borrower can use the liquidity of the vault without paying any fees. A liquidator can use the liquidity of the protocol to make a liquidation. The functionality to do a liquidation with an Uniswap flashLoan is useless.
When a user borrows the protocol increase his debt and he receive the amount that he borrowed. The problem is that a user can repay at any time, he can repay exactly the same amount that he borrowed if he repay in the same block, and basically give him a free flashloan. users could use this to do arbitrages or worse liquidation in the protocol itself. If a position is liquidable and the lend amount is bigger than the debt a liquidator could borrow pay the liquidation cost get the liquidation value repay exactly the same amount. He then performed a liquidation without taking any risk or paying any fees.
You can run this test in the VaultV3.t.sol
function testFreeFlashloan() external { _setupBasicLoan(true); (, uint256 fullValue, uint256 collateralValue,,) = vault.loanInfo(TEST_NFT); vm.startPrank(TEST_NFT_ACCOUNT); USDC.approve(address(vault), collateralValue); vault.repay(TEST_NFT,collateralValue,false); vm.stopPrank(); (uint256 debt, , ,,) = vault.loanInfo(TEST_NFT); assertEq(debt,0); }
Echidna
The protocol should add a variable in the Loan struct, increment it in the borrow function(L567-L570) add require in the _repay function(L957-L960).
struct Loan { uint256 debtShares; uint256 timeOfBorrow; }
borrow function:
uint256 loanDebtShares = loan.debtShares + shares; loan.debtShares = loanDebtShares; loan.timeOfBorrow = block.timestamp; debtSharesTotal += shares;
in the _repay function:
Loan storage loan = loans[tokenId]; require(loan.timeOfBorrow> block.timestamp,"Could not repay in the same block"); uint256 currentShares = loan.debtShares;
MEV
#0 - c4-pre-sort
2024-03-22T10:01:47Z
0xEVom marked the issue as duplicate of #435
#1 - c4-pre-sort
2024-03-22T10:01:50Z
0xEVom marked the issue as sufficient quality report
#2 - jhsagd76
2024-03-31T03:32:41Z
he needs to swap coll and debt
#3 - c4-judge
2024-03-31T03:43:33Z
jhsagd76 changed the severity to 2 (Med Risk)
#4 - c4-judge
2024-04-01T01:24:34Z
jhsagd76 marked the issue as satisfactory
545.9832 USDC - $545.98
https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol#L954 https://github.com/code-423n4/2024-03-revert-lend/blob/main/src/V3Vault.sol#L1082-L1083
A user can get out of the protocol by another user. It could be a loss for the user specially if the user use the transform mode.
If a user want to borrow he must first create a position with the create function(L400) and after that he can borrow with the borrow function(L550).
But if in the meantime another user repay with 0 assets the internal function _cleanupLoan(L1082) will be called and it will delete the position, the user will be out of the protocol. It could be a problem especially if the user use the protocol to manage his position on Uniswap.
You can run this test in V3Vault.t.sol :
function testUserCanGetAnotherUserOutOfTheProtocol() external { _setupBasicLoan(false); vault.repay(TEST_NFT,0,false); assertEq(NPM.ownerOf(TEST_NFT),TEST_NFT_ACCOUNT); }
Echidna
The problem is that the protocol allows repaying any amount. The best way to prevent that is a minimum repay amount like 10% of the debt, add a require in case the user have no debt and add another external function to _cleanupLoan if the user have no debt.
Other
#0 - c4-pre-sort
2024-03-18T18:06:08Z
0xEVom marked the issue as duplicate of #232
#1 - c4-pre-sort
2024-03-18T18:06:11Z
0xEVom marked the issue as sufficient quality report
#2 - c4-judge
2024-04-01T10:53:23Z
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/main/src/V3Vault.sol#L968-L969
The user will repay but his debt will not decrease.
Because of a rounding error in the _repay function (L954) a user may repay his loan and his debt don't decrease. It happen whenever the user repay an amount above the debt exchange rate. Because of this lines in the _repay function (L968-L969)
assets = amount; shares = _convertToShares(amount, newDebtExchangeRateX96, Math.Rounding.Down);
Here we can see that the rounding is down so it will round down to zero. Moreover the protocol give absolutely no view function to convert assets in debt shares, so a user or a protocol build on top of this one can make a mistake a repay without having the debt decrease. You can run this test in V3Vault.t.sol :
function testBorrowerDontRepayAnything() external { _setupBasicLoan(true); (,,,,, uint256 _debtExchangeRateX96,uint256 _lendExchangeRateX96)= vault.vaultInfo(); //We let two year pass in order to let the debt exchange rate increase. vm.warp(block.timestamp+ 2*60*60*24*365); ( uint256 debtBefore)= vault.loans(TEST_NFT); _repay(_debtExchangeRateX96/Q96,TEST_NFT_ACCOUNT,TEST_NFT,false); ( uint256 debtAfter)= vault.loans(TEST_NFT); assertEq(debtAfter,debtBefore); }
Echidna
The _repay function should have a require in case that the shares repayed round down to zero.
assets = amount; shares = _convertToShares(amount, newDebtExchangeRateX96, Math.Rounding.Down); require(shares>0);
Math
#0 - c4-pre-sort
2024-03-22T10:10:50Z
0xEVom marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-03-22T10:10:53Z
0xEVom marked the issue as primary issue
#2 - 0xEVom
2024-03-22T10:15:07Z
+ ideally only shares amount converted back to assets should be transferred from caller.
Both #316 and this issue likely only represent dust amounts, so low severity seems more appropriate.
#3 - c4-sponsor
2024-03-26T17:31:45Z
kalinbas marked the issue as disagree with severity
#4 - kalinbas
2024-03-26T17:31:50Z
low severity
#5 - c4-sponsor
2024-03-26T17:32:14Z
kalinbas (sponsor) confirmed
#6 - jhsagd76
2024-04-01T08:12:06Z
This is reasonable rounding, marked as N instead of L.
#7 - jhsagd76
2024-04-01T08:12:10Z
N
#8 - c4-judge
2024-04-01T08:12:20Z
jhsagd76 changed the severity to QA (Quality Assurance)
#9 - jhsagd76
2024-04-01T08:14:14Z
It should be noted that the check for 0 mint shares helps to prevent rounding attacks, hence it retains an L-B rating.
#10 - c4-judge
2024-04-01T08:14:19Z
jhsagd76 marked the issue as grade-b
#11 - kalinbas
2024-04-09T22:00:01Z