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: 52/105
Findings: 3
Award: $133.39
π Selected for report: 1
π Solo Findings: 0
π Selected for report: JecikPo
Also found by: KupiaSec, SpicyMeatball, kennedy1030, kfx, linmiaomiao, t4sk
119.7477 USDC - $119.75
The price calculation at the V3Oracle.sol will revert upon reaching certain level when referenceToken is used with high decimal value (e. g. 18). The revert (specifically happening when calling getValue()) would make the Chainlink price feed useless. Yet the TWAP price source would still be available. The protocol team would have to disable Chainlink and rely exclusively on the TWAP source reducing security of the pricing. The issue could manifest itself after certain amount of time once the project is already live and only when price returned by the feed reaches certain point.
The following code line has an issue:
chainlinkPriceX96 = (10 ** referenceTokenDecimals) * chainlinkPriceX96 * Q96 / chainlinkReferencePriceX96 / (10 ** feedConfig.tokenDecimals);
When referenceTokenDecimals is 18 chainlinkPriceX96 is higher than some threshold between 18 and 19 (in Q96 notation), it will cause arithmetic overflow.
Manual review.
Instead of calculating the price this way:
chainlinkPriceX96 = (10 ** referenceTokenDecimals) * chainlinkPriceX96 * Q96 / chainlinkReferencePriceX96 / (10 ** feedConfig.tokenDecimals);
It could be done the following way as per Chainlink's recommendation:
if (referenceTokenDecimals > feedConfig.tokenDecimals) chainlinkPriceX96 = (10 ** referenceTokenDecimals - feedConfig.tokenDecimals) * chainlinkPriceX96 * Q96 / chainlinkReferencePriceX96; else if (referenceTokenDecimals < feedConfig.tokenDecimals) chainlinkPriceX96 = chainlinkPriceX96 * Q96 / chainlinkReferencePriceX96 / (10 ** feedConfig.tokenDecimals - referenceTokenDecimals); else chainlinkPriceX96 = chainlinkPriceX96 * Q96 / chainlinkReferencePriceX96;
https://docs.chain.link/data-feeds/using-data-feeds#getting-a-different-price-denomination
Decimal
#0 - c4-pre-sort
2024-03-22T07:24:22Z
0xEVom marked the issue as primary issue
#1 - c4-pre-sort
2024-03-22T07:24:26Z
0xEVom marked the issue as sufficient quality report
#2 - c4-sponsor
2024-03-26T17:32:32Z
kalinbas (sponsor) confirmed
#3 - c4-judge
2024-03-31T15:57:15Z
jhsagd76 marked the issue as satisfactory
#4 - c4-judge
2024-04-01T15:34:00Z
jhsagd76 marked the issue as selected for report
#5 - kalinbas
2024-04-09T22:18:46Z
π Selected for report: kfx
Also found by: 0x175, 0xAlix2, 0xjuan, AMOW, Aymen0909, CaeraDenoir, Giorgio, JCN, JecikPo, JohnSmith, Norah, SpicyMeatball, alexander_orjustalex, atoko, erosjohn, falconhoof, givn, grearlake, jnforja, kinda_very_good, lanrebayode77, nmirchev8, shaka, web3Tycoon, zxriptor
3.3501 USDC - $3.35
An attacker by front-running any liquidate() call can prevent successful liquidation by making it revert. By preventing liquidations the gains of the protocol can be diminished using relatively low cost, especially during volatile price movements.
The problem lies in the following condition inside the liquidate() function in the V3Vault.sol:
uint256 debtShares = loans[params.tokenId].debtShares; if (debtShares != params.debtShares) { revert DebtChanged(); }
The liquidate() function requires that the caller puts the exact amount of debt that he intends to liquidate, that amount must equal the actual debt of a loan. An attacker can front-run the liquidate() transaction with a repay() and repay minimum amount of assets so that at least debtShares is reduced by 1.
A test to see the behaviour:
function testLiquidationFrontRun() external { address FRONT_RUNNER = 0xF977814e90Da44bfa03b6295a0616A897441bAcA; vm.prank(WHALE_ACCOUNT); USDC.transfer(FRONT_RUNNER, 1000); // approve front runner to vault for repay() vm.prank(FRONT_RUNNER); USDC.approve(address(vault), 100); _setupBasicLoan(true); // wait 7 days for interest to grow vm.warp(block.timestamp + 7 days); (,,, uint256 liquidationCost,) = vault.loanInfo(TEST_NFT); (uint256 debtShares) = vault.loans(TEST_NFT); vm.prank(WHALE_ACCOUNT); USDC.approve(address(vault), liquidationCost); vm.prank(FRONT_RUNNER); vault.repay(TEST_NFT, 1, true); vm.expectRevert(IErrors.DebtChanged.selector); vm.prank(WHALE_ACCOUNT); vault.liquidate(IVault.LiquidateParams(TEST_NFT, debtShares, 0, 0, WHALE_ACCOUNT, "")); }
Manual Review
Recommendation is to remove the above condition completely and the debtShares from LiquidateParams. liquidation should always liquidate the entire available debt.
DoS
#0 - c4-pre-sort
2024-03-18T18:13:51Z
0xEVom marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-03-18T18:14:27Z
0xEVom marked the issue as duplicate of #231
#2 - c4-pre-sort
2024-03-22T12:02:47Z
0xEVom marked the issue as duplicate of #222
#3 - c4-judge
2024-03-31T16:05:53Z
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
Anyone can repay any loan. So when an original loan owner wishes to repay the whole debt by specifying the attributes of the repay()
, the transaction could be front-run by an attacker where he repays the exact amount so that the loan.debtShares
decrements by 1. This way the debt owner's transaction gets reverted due to the below condition.
File: src/V3Vault.sol 973: if (shares > currentShares) { revert RepayExceedsDebt(); }
The fix should be the following: If the debt/loan owner specifies too high amount, and the above condition is hit, the shares/assets values should be recalculated, like this:
973: if (shares > currentShares) { shares = currentShares assets = _convertToAssets(shares, newDebtExchangeRateX96, Math.Rounding.Up); }
This way would protect the loan/debt owners from unnecessarily repeated transaction and hence save them gas.
GitHub : 973
Using the NonfungiblePositionManager
directly a user could potentially mint a position directly to V3Vault by having V3Vault as the receiver when calling NonfungiblePositionManager.mint()
.
Since NonfungiblePositionManager doesnβt use safeMint()
this would just transfer the token and liquidity to V3Vault without calling V3Vault.onERC721Received()
, thus the liquidity would be lost.
Consider adding a rescue function callable by owner that can transfer any accidentally minted tokens out of the contract. This could check that V3Vault is the owner of the token but has loans[tokenId].owner == address(0)
to prevent misuse.
#0 - c4-pre-sort
2024-03-24T20:32:38Z
0xEVom marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-03-24T20:32:41Z
0xEVom marked the issue as grade-c
#2 - c4-pre-sort
2024-03-25T20:52:21Z
0xEVom marked the issue as sufficient quality report
#3 - c4-pre-sort
2024-03-25T20:52:24Z
0xEVom removed the grade
#4 - c4-judge
2024-04-01T15:10:18Z
jhsagd76 marked the issue as grade-b