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: 77/183
Findings: 6
Award: $45.12
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Maroutis
Also found by: 0x486776, 0xShitgem, 0xabhay, 0xleadwizard, 0xlemon, 0xnilay, 0xtankr, 3docSec, AM, Aamir, Abdessamed, Al-Qa-qa, AlexCzm, Circolors, CodeWasp, Daniel526, Egis_Security, Emmanuel, Giorgio, Honour, Hueber, Infect3d, Krace, KupiaSec, LeoGold, Limbooo, PoeAudits, SBSecurity, SpicyMeatball, T1MOH, The-Seraphs, TheSavageTeddy, TheSchnilch, Topmark, VAD37, ZanyBonzy, adam-idarrha, bhilare_, btk, carlitox477, cinderblock, dimulski, falconhoof, grearlake, gumgumzum, iamandreiski, itsabinashb, josephdara, ke1caM, kennedy1030, ljj, n0kto, n4nika, neocrao, oakcobalt, petro_1912, pontifex, poslednaya, shaflow2, shikhar229169, web3km, ych18, zhaojohnson, zigtur
0.2831 USDC - $0.28
https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/VaultManagerV2.sol#L75-L76 https://github.com/code-423n4/2024-04-dyad/blob/main/script/deploy/Deploy.V2.s.sol#L93-L95
In VaultManagerV2
we have non-Kerosine vaults(WETH vault for example) and Kerosine vaults. We have two separate add functions to use them to add the desired vault. vaultLicenser
is suppossed to hold the non-Kerosine licensed vaults but if we look at the Deploy.V2.s.sol
(which is explicitly said to be in-scope) we can see that vaultLicenser
contains both non-Kerosine and Kerosine vaults:
... vaultLicenser.add(address(ethVault)); vaultLicenser.add(address(wstEth)); vaultLicenser.add(address(unboundedKerosineVault)); ...
This means that users can add a Kerosine vault in the add
function meant for adding vault for exogenous collateral.
By doing that the user can successfully bypass the exogenous collateral value check in the VaultManagerV2::withdraw()
because the deposited Keroine value will be counted as exogenous collateral even though it isn't. This is quite problematic because we have seen in the past how not having enough exogenous collateral might lead to price disruptions(terrausd - Luna crash).
Lets see the steps how a user can bypass this withdraw check: (Assuming the user has previouslt added other form of collateral)
VaultManagerV2::add
and adds a Kerosine vaultVaultManagerV2::deposit
depositing Kerosine tokensfunction withdraw( uint id, address vault, uint amount, address to ) public isDNftOwner(id) { ... if (getNonKeroseneValue(id) - value < dyadMinted) revert NotEnoughExoCollat(); ... }
Now look at the getNonKeroseneValue(id)
:
function getNonKeroseneValue( uint id ) public view returns (uint) { uint totalUsdValue; uint numberOfVaults = vaults[id].length(); for (uint i = 0; i < numberOfVaults; i++) { Vault vault = Vault(vaults[id].at(i)); uint usdValue; if (vaultLicenser.isLicensed(address(vault))) { usdValue = vault.getUsdValue(id); } totalUsdValue += usdValue; } return totalUsdValue; }
The loop is through exogenous vaults but since we added a Kerosine vault there and it is licensed by the vaultLicenser
, its value will be added to the totalUsdValue.
This also breaks the invariant that the maximum non-Kerosine vaults can be 5 and the Kerosine vaults can be 5.
Manual Review
To mitigate this in the deploy script when adding vaults to the vaultLicenser
add only exogenous vaults, not all of the vaults.
Also the vaultLicenser
should be used when calculating the TVL for example in Vault.kerosine.unbounded::assetPrice
Invalid Validation
#0 - c4-pre-sort
2024-04-28T18:47:21Z
JustDravee marked the issue as duplicate of #70
#1 - c4-pre-sort
2024-04-29T09:37:24Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-11T20:01:02Z
koolexcrypto marked the issue as satisfactory
#3 - c4-judge
2024-05-12T10:36:46Z
koolexcrypto marked the issue as not a duplicate
#4 - c4-judge
2024-05-12T10:36:51Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#5 - LyuboslavVeliev
2024-05-17T08:53:15Z
Hi @koolexcrypto,
I think this issue is a lot like #966, but I am pointing a different impact here with the same root cause. Here is why I believe this issue should be validated.
If vaultLicenser
contains both Kerosine and non-Kerosine vaults, we can add a Kerosine vault in the vaultManager::add()
function and when calculating the non-Kerosine value, this vault will be counted in:
function add( uint id, address vault ) external isDNftOwner(id) { if (vaults[id].length() >= MAX_VAULTS) revert TooManyVaults(); if (!vaultLicenser.isLicensed(vault)) revert VaultNotLicensed(); @-> if (!vaults[id].add(vault)) revert VaultAlreadyAdded(); emit Added(id, vault); }
We add the vault to vaults
and when calculating the non-Kerosine value we get this Enumerable set:
function getNonKeroseneValue( uint id ) public view returns (uint) { uint totalUsdValue; @ -> uint numberOfVaults = vaults[id].length(); for (uint i = 0; i < numberOfVaults; i++) { Vault vault = Vault(vaults[id].at(i)); uint usdValue; if (vaultLicenser.isLicensed(address(vault))) { usdValue = vault.getUsdValue(id); } totalUsdValue += usdValue; } return totalUsdValue; }
This means that by adding a Kerosine vault to exogenous vaults, we can bypass the checks that make sure that non-Kerosene value deposited >= dyadMinted. We can see such check in the vaultManager::withdraw
function withdraw( uint id, address vault, uint amount, address to ) public isDNftOwner(id) { if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock(); uint dyadMinted = dyad.mintedDyad(address(this), id); Vault _vault = Vault(vault); uint 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(); }
We can now mint DYAD without depositing any exogenous collater since the mint function has a similar check.
This is the impact I described and another impact is described in #966. Thank you for reviewing it again!!!
#6 - koolexcrypto
2024-05-24T10:48:58Z
Hi @LyuboslavVeliev
Thanks for the feedback.
should be duped with #966.
#7 - c4-judge
2024-05-24T10:49:39Z
koolexcrypto marked the issue as duplicate of #966
#8 - c4-judge
2024-05-29T10:26:56Z
koolexcrypto marked the issue as satisfactory
#9 - c4-judge
2024-05-29T11:20:21Z
koolexcrypto marked the issue as duplicate of #1133
#10 - c4-judge
2024-05-29T11:43:23Z
koolexcrypto changed the severity to 3 (High Risk)
🌟 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
https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/VaultManagerV2.sol#L143 https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/VaultManagerV2.sol#L127
The identified vulnerability in the VaultManagerV2::withdraw
function allows for a denial-of-service (DoS) attack on the owner due to the ability of a malicious user to front-run the owner's withdrawal transaction. This attack leverages the restriction that deposit and withdraw operations cannot occur in the same block, causing the owner's withdrawal transaction to revert and effectively denying access to their funds.
The vulnerability stems from the following key aspects of the withdraw function:
The function checks if the idToBlockOfLastDeposit[id]
is equal to the current block number (block.number
).
If this condition is true (indicating that a deposit occurred in the same block), the function reverts with a DepositedInSameBlock
error.
This check prevents immediate withdrawals following a deposit in the same block. This is generally done to prevent flash loan attacks.
However, a malicious user can exploit this behavior:
By monitoring the blockchain for the owner's withdraw transaction, the malicious user can front-run that transaction and do a deposit of 0 amount within the same block.
If the malicious user successfully executes their deposit transaction first, it will cause the owner's withdrawal attempt to revert due to the block restriction check. This sequence of events results in a denial-of-service (DoS) attack, where the owner is unable to withdraw their funds as intended.
To run add the test and run: forge test --fork-url <your mainnet rpc> --match-test testUserDoSWithdraw -vvv
// SPDX-License-Identifier: MIT pragma solidity =0.8.17; import "forge-std/console.sol"; import {Test} from "forge-std/Test.sol"; import {Parameters} from "../src/params/Parameters.sol"; import {DeployV2} from "../script/deploy/Deploy.V2.s.sol"; import {Contracts} from "../script/deploy/Deploy.V2.s.sol"; import {VaultManagerV2} from "../src/core/VaultManagerV2.sol"; import {DNft} from "../src/core/DNft.sol"; import {Dyad} from "../src/core/Dyad.sol"; import {Licenser} from "../src/core/Licenser.sol"; import {Vault} from "../src/core/Vault.sol"; import {VaultWstEth} from "../src/core/Vault.wsteth.sol"; import {IAggregatorV3} from "../src/interfaces/IAggregatorV3.sol"; import {KerosineManager} from "../src/core/KerosineManager.sol"; import {UnboundedKerosineVault} from "../src/core/Vault.kerosine.unbounded.sol"; import {BoundedKerosineVault} from "../src/core/Vault.kerosine.bounded.sol"; import {Kerosine} from "../src/staking/Kerosine.sol"; import {KerosineDenominator} from "../src/staking/KerosineDenominator.sol"; import {ERC20} from "@solmate/src/tokens/ERC20.sol"; interface IWETH { function deposit() external payable; function withdraw(uint) external; function approve(address, uint256) external returns (bool); } contract VaultManagerV2Test is Test, Parameters { Kerosine kerosene; Licenser vaultLicenser; VaultManagerV2 vaultManager; Vault ethVault; VaultWstEth wstEth; KerosineManager kerosineManager; UnboundedKerosineVault unboundedKerosineVault; BoundedKerosineVault boundedKerosineVault; KerosineDenominator kerosineDenominator; IWETH wETH; address user = makeAddr("userrrr"); uint256 userNFTId; function setUp() public { DeployV2 deployV2 = new DeployV2(); Contracts memory deployedContracts = deployV2.run(); kerosene = deployedContracts.kerosene; vaultLicenser = deployedContracts.vaultLicenser; vaultManager = deployedContracts.vaultManager; ethVault = deployedContracts.ethVault; wstEth = deployedContracts.wstEth; kerosineManager = deployedContracts.kerosineManager; unboundedKerosineVault = deployedContracts.unboundedKerosineVault; boundedKerosineVault = deployedContracts.boundedKerosineVault; kerosineDenominator = deployedContracts.kerosineDenominator; wETH = IWETH(MAINNET_WETH); vm.deal(user, 1000 ether); vm.prank(user); wETH.deposit{value: 100 ether}(); vm.prank(MAINNET_OWNER); kerosene.transfer(user, 10_000 ether); DNft dNFT = DNft(MAINNET_DNFT); vm.prank(user); userNFTId = dNFT.mintNft{value: 100 ether}(user); } function testUserDoSWithdraw() public { address randomUser = makeAddr("randomUser"); vm.startPrank(user); vaultManager.add(userNFTId, address(ethVault)); wETH.approve(address(vaultManager), 10 ether); vaultManager.deposit(userNFTId, address(ethVault), 10 ether); vm.roll(block.number + 1); vm.stopPrank(); // A malicious user front runs the owner's withdraw vm.prank(randomUser); vaultManager.deposit(userNFTId, address(ethVault), 0); // The owner tries to withdraw but cannot in the same block because he is fron ran vm.prank(user); vaultManager.withdraw( userNFTId, address(unboundedKerosineVault), 10 ether, user ); } }
We can see how by successfully front-running the owner's withdraw transaction the user DoSes the owner.
This attack disrupts the intended functionality of the withdrawal process and denies the owner access to their assets.
Manual Review
One way to mitigate this and still leave the flash loan protection is to allow only the owner of the NFT to deposit into his vaults. This way a random user cannot perform this attack.
DoS
#0 - c4-pre-sort
2024-04-27T11:22:26Z
JustDravee marked the issue as high quality report
#1 - c4-pre-sort
2024-04-27T11:22:29Z
JustDravee marked the issue as primary issue
#2 - c4-pre-sort
2024-04-27T11:51:54Z
JustDravee marked the issue as duplicate of #489
#3 - c4-judge
2024-05-05T20:38:08Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#4 - c4-judge
2024-05-05T21:11:16Z
koolexcrypto marked the issue as nullified
#5 - c4-judge
2024-05-05T21:11:22Z
koolexcrypto marked the issue as not nullified
#6 - c4-judge
2024-05-08T15:29:30Z
koolexcrypto marked the issue as duplicate of #1001
#7 - c4-judge
2024-05-11T19:44:54Z
koolexcrypto marked the issue as satisfactory
🌟 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
https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/VaultManagerV2.sol#L148
The VaultManagerV2::withdraw
function in the contract is designed to facilitate the withdrawal of assets from a specified vault to a designated recipient. However, the function erroneously attempts to calculate a value using the asset price obtained from the vault's oracle, which is not available for Kerosine vaults. This oversight prevents successful withdrawals of Kerosine tokens and leaves them trapped within the vault.
The specific vulnerability arises from the following aspects of the code:
The line _vault.assetPrice()
attempts to retrieve the current asset price from the vault's associated oracle.
Kerosine vaults do not have an oracle associated with them, as can be seen in Vault.kerosine
.
Accessing _vault.oracle() for a Kerosine vault will result in a failure (revert) due to the absence of this property. As a consequence:
The calculation of value in the function will fail due to the inability to retrieve the asset price from the nonexistent oracle.
The failure prevents users from withdrawing Kerosine tokens from the vault, effectively locking the tokens within the vault.
Add this test in the test folder and run forge test --fork-url <mainnet rpc url> --match-test testCannotWithdrawKerosene -vvv
// SPDX-License-Identifier: MIT pragma solidity =0.8.17; import "forge-std/console.sol"; import {Test} from "forge-std/Test.sol"; import {Parameters} from "../src/params/Parameters.sol"; import {DeployV2} from "../script/deploy/Deploy.V2.s.sol"; import {Contracts} from "../script/deploy/Deploy.V2.s.sol"; import {VaultManagerV2} from "../src/core/VaultManagerV2.sol"; import {DNft} from "../src/core/DNft.sol"; import {Dyad} from "../src/core/Dyad.sol"; import {Licenser} from "../src/core/Licenser.sol"; import {Vault} from "../src/core/Vault.sol"; import {VaultWstEth} from "../src/core/Vault.wsteth.sol"; import {IAggregatorV3} from "../src/interfaces/IAggregatorV3.sol"; import {KerosineManager} from "../src/core/KerosineManager.sol"; import {UnboundedKerosineVault} from "../src/core/Vault.kerosine.unbounded.sol"; import {BoundedKerosineVault} from "../src/core/Vault.kerosine.bounded.sol"; import {Kerosine} from "../src/staking/Kerosine.sol"; import {KerosineDenominator} from "../src/staking/KerosineDenominator.sol"; import {ERC20} from "@solmate/src/tokens/ERC20.sol"; interface IWETH { function deposit() external payable; function withdraw(uint) external; function approve(address, uint256) external returns (bool); } contract VaultManagerV2Test is Test, Parameters { Kerosine kerosene; Licenser vaultLicenser; VaultManagerV2 vaultManager; Vault ethVault; VaultWstEth wstEth; KerosineManager kerosineManager; UnboundedKerosineVault unboundedKerosineVault; BoundedKerosineVault boundedKerosineVault; KerosineDenominator kerosineDenominator; IWETH wETH; address user = makeAddr("userrrr"); uint256 userNFTId; function setUp() public { DeployV2 deployV2 = new DeployV2(); Contracts memory deployedContracts = deployV2.run(); kerosene = deployedContracts.kerosene; vaultLicenser = deployedContracts.vaultLicenser; vaultManager = deployedContracts.vaultManager; ethVault = deployedContracts.ethVault; wstEth = deployedContracts.wstEth; kerosineManager = deployedContracts.kerosineManager; unboundedKerosineVault = deployedContracts.unboundedKerosineVault; boundedKerosineVault = deployedContracts.boundedKerosineVault; kerosineDenominator = deployedContracts.kerosineDenominator; wETH = IWETH(MAINNET_WETH); vm.deal(user, 1000 ether); vm.prank(user); wETH.deposit{value: 100 ether}(); vm.prank(MAINNET_OWNER); kerosene.transfer(user, 10_000 ether); DNft dNFT = DNft(MAINNET_DNFT); vm.prank(user); userNFTId = dNFT.mintNft{value: 100 ether}(user); } function testCannotWithdrawKerosene() public { vm.startPrank(user); // Using add() here instead of addKerosene() because of a bug I described in another issue vaultManager.add(userNFTId, address(unboundedKerosineVault)); vaultManager.add(userNFTId, address(ethVault)); wETH.approve(address(vaultManager), 10 ether); vaultManager.deposit(userNFTId, address(ethVault), 10 ether); kerosene.approve(address(vaultManager), 1_000 ether); vaultManager.deposit(userNFTId, address(unboundedKerosineVault), 1_000 ether); vm.roll(block.number + 1); //vm.expectRevert(); // Withdraw() reverts because vault.oracle() is non-existant vaultManager.withdraw( userNFTId, address(unboundedKerosineVault), 1_000, user ); vm.stopPrank(); } }
We can clearly see how the withdraw reverts even when we have enough assets to withdraw.
The impact of this vulnerability is severe for users attempting to withdraw Kerosine tokens from Kerosine vaults. The inability to withdraw Kerosine tokens due to the code revert effectively locks these assets within the vault, rendering them inaccessible to their rightful owners.
This is the most important change from the VaultManager
to VaultManagerV2
and it cannot be used properly.
Manual Review
For the VaultManagerV2::withdraw
instead of calling the oracle's decimals you can check if that vault has been licensed in the keroseneManager
and if that is the case then divide the value by 1e8 (instead of the oracle call) and if not then leave the calculations as they were.
Other
#0 - c4-pre-sort
2024-04-26T21:33:32Z
JustDravee marked the issue as duplicate of #1048
#1 - c4-pre-sort
2024-04-28T18:39:27Z
JustDravee marked the issue as duplicate of #830
#2 - c4-pre-sort
2024-04-29T08:45:23Z
JustDravee marked the issue as sufficient quality report
#3 - c4-judge
2024-05-11T20:05:29Z
koolexcrypto marked the issue as satisfactory
🌟 Selected for report: 0xAlix2
Also found by: 0xfox, 0xlemon, 0xnev, 3docSec, Aamir, Abdessamed, Dudex_2004, Egis_Security, Evo, FastChecker, Honour, Jorgect, KupiaSec, Limbooo, MrPotatoMagic, SpicyMeatball, TheSchnilch, alix40, bhilare_, favelanky, forgebyola, ke1caM, kennedy1030, koo, oakcobalt, petro_1912, shikhar229169
32.4128 USDC - $32.41
https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/VaultManagerV2.sol#L150
The identified vulnerability in the VaultManagerV2::withdraw
function is related to potential arithmetic underflow during collateral value calculations, leading to unexpected behavior and potential reverts. Specifically, if the collateral value of the NFT (id) is insufficient compared to the value of Kerosine tokens (amount) to be withdrawn, the calculation can result in a negative value leading to an underflow, triggering a revert due to insufficient collateralization. This issue limits the flexibility and usability of the withdrawal process, particularly in scenarios where only Kerosine tokens are deposited or when exogenous collateral value is insufficient compared to Kerosine token value.
The problem is that a user cannot withdraw his Kerosine tokens if the value he wants to withdraw is > his exogenous collateral value. For example a user can only deposit into a Kerosine vault and he will not be able to get his tokens back because of this check in the withdraw function:
function withdraw( uint id, address vault, uint amount, address to ) public isDNftOwner(id) { if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock(); uint dyadMinted = dyad.mintedDyad(address(this), id); Vault _vault = Vault(vault); uint 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(); }
This leaves the Kerosine tokens stuck in the vault manager.
Another example is when a user gets liquidated most of his exogenous collateral is taken from him and when he wants to withdraw his Kerosine tokens, he most probably won't be able to because of the underflow that will happen when checking getNonKeroseneValue(id) - value
.
Leaves Kerosine tokens stuck in the contract and the user is denied of his funds.
Manual Review
I would recommend to construct a different withdraw function for the Kerosine vaults that doesn't include this check.
Another solution is to check if the keroseneManager
contains the inputed vault and if that is the case then remove the check and leave it as it is if it is another vault.
Other
#0 - c4-pre-sort
2024-04-26T21:33:09Z
JustDravee marked the issue as duplicate of #397
#1 - c4-pre-sort
2024-04-29T08:48:26Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-11T19:23:07Z
koolexcrypto marked the issue as satisfactory
🌟 Selected for report: dimulski
Also found by: 0xleadwizard, 0xlemon, Aamir, Al-Qa-qa, AvantGard, Bauchibred, Cryptor, DarkTower, Egis_Security, Giorgio, Maroutis, MrPotatoMagic, OMEN, Ocean_Sky, Ryonen, SBSecurity, Sabit, SpicyMeatball, Stefanov, T1MOH, Tigerfrake, WildSniper, atoko, bhilare_, darksnow, fandonov, grearlake, iamandreiski, igdbase, pontifex, web3km, xiao
4.8719 USDC - $4.87
https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/VaultManagerV2.sol#L205
Liquidators don't have incentive to liquidate very small positions where the collateral they get is worth less than the dyad value payed + gas used.
Liquidators liquidate users for the profit they can make. If there is no profit to be made than there will be no one to call the liquidate function. For example an account has 4$ worth of collateral and has 4 DYAD minted. This user is undercollateralized and must be liquidated in order to ensure that the protocol remains overcollateralized. Because the value of the account is so low, after gas costs, liquidators will not make a profit liquidating this user. In the end these low value accounts will never get liquidating, leaving the protocol with bad debt and can even cause the protocol to be undercollateralized with enough small value accounts being underwater.
This can totaly disrupt the protocol if enough small positions are opened. Therefore if there isn't enough collateral to account for the minted DYAD tokens, the price of the DYAD token will depeg and lose its value of 1$.
Manual Review
A potential fix could be to only allow users to mint DYAD tokens if their collateral value is past a certain threshold.
Oracle
#0 - c4-pre-sort
2024-04-27T17:32:42Z
JustDravee marked the issue as duplicate of #1258
#1 - c4-pre-sort
2024-04-29T09:16:52Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-03T14:07:47Z
koolexcrypto changed the severity to QA (Quality Assurance)
#3 - c4-judge
2024-05-12T09:33:23Z
koolexcrypto marked the issue as grade-c
#4 - c4-judge
2024-05-22T14:26:06Z
This previously downgraded issue has been upgraded by koolexcrypto
#5 - c4-judge
2024-05-28T16:52:31Z
koolexcrypto marked the issue as satisfactory
#6 - c4-judge
2024-05-28T20:06:33Z
koolexcrypto marked the issue as duplicate of #175
🌟 Selected for report: carrotsmuggler
Also found by: 0xAlix2, 0xSecuri, 0xleadwizard, 0xlemon, 0xtankr, 3th, Aamir, Abdessamed, Bauchibred, Circolors, Egis_Security, Evo, Hueber, Mahmud, SBSecurity, TheSavageTeddy, TheSchnilch, Tychai0s, alix40, bbl4de, btk, d3e4, ducanh2706, falconhoof, itsabinashb, ke1caM, lian886, n4nika, oakcobalt, pontifex, sashik_eth, steadyman, tchkvsky, zhuying
3.7207 USDC - $3.72
https://github.com/code-423n4/2024-04-dyad/blob/main/script/deploy/Deploy.V2.s.sol#L64-L65 https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/VaultManagerV2.sol#L88
In the DeployV2.s.sol
(which is explicitly said to be in scope) the wrong vaults are added to the kerosineManager
. Instead of Kerosine vaults, the non-Kerosine ones are added. This is a huge problem because the user cannot use the main functionallity of the VaultManagerV2
(the user cannot add Kerosine vaults).
Looking at the deploy script we can see that the ethVault
and wstEth
vaults are added to the kerosineManager
instead of the bounded and unbounded Kerosine vaults.
KerosineManager kerosineManager = new KerosineManager(); kerosineManager.add(address(ethVault)); kerosineManager.add(address(wstEth));
Looking at the VaultManagerV2::addKerosene
function we can see that it checks if the vault that the user wants to add is licensed by the kerosineManager
however we saw above that only the wETH and wstETH vaults were added, this means that if the user calls this function with the bounded Kerosine vault the call will revert.
function addKerosene( uint id, address vault ) external isDNftOwner(id) { if (vaultsKerosene[id].length() >= MAX_VAULTS_KEROSENE) revert TooManyVaults(); if (!keroseneManager.isLicensed(vault)) revert VaultNotLicensed(); if (!vaultsKerosene[id].add(vault)) revert VaultAlreadyAdded(); emit Added(id, vault); }
Add this test and run forge test --fork-url <your mainnet rpc> --match-test testCannotAddKerosineVault -vvv
// SPDX-License-Identifier: MIT pragma solidity =0.8.17; import "forge-std/console.sol"; import {Test} from "forge-std/Test.sol"; import {Parameters} from "../src/params/Parameters.sol"; import {DeployV2} from "../script/deploy/Deploy.V2.s.sol"; import {Contracts} from "../script/deploy/Deploy.V2.s.sol"; import {VaultManagerV2} from "../src/core/VaultManagerV2.sol"; import {DNft} from "../src/core/DNft.sol"; import {Dyad} from "../src/core/Dyad.sol"; import {Licenser} from "../src/core/Licenser.sol"; import {Vault} from "../src/core/Vault.sol"; import {VaultWstEth} from "../src/core/Vault.wsteth.sol"; import {IAggregatorV3} from "../src/interfaces/IAggregatorV3.sol"; import {KerosineManager} from "../src/core/KerosineManager.sol"; import {UnboundedKerosineVault} from "../src/core/Vault.kerosine.unbounded.sol"; import {BoundedKerosineVault} from "../src/core/Vault.kerosine.bounded.sol"; import {Kerosine} from "../src/staking/Kerosine.sol"; import {KerosineDenominator} from "../src/staking/KerosineDenominator.sol"; import {ERC20} from "@solmate/src/tokens/ERC20.sol"; interface IWETH { function deposit() external payable; function withdraw(uint) external; function approve(address, uint256) external returns (bool); } contract VaultManagerV2Test is Test, Parameters { Kerosine kerosene; Licenser vaultLicenser; VaultManagerV2 vaultManager; Vault ethVault; VaultWstEth wstEth; KerosineManager kerosineManager; UnboundedKerosineVault unboundedKerosineVault; BoundedKerosineVault boundedKerosineVault; KerosineDenominator kerosineDenominator; IWETH wETH; address user = makeAddr("userrrr"); uint256 userNFTId; function setUp() public { DeployV2 deployV2 = new DeployV2(); Contracts memory deployedContracts = deployV2.run(); kerosene = deployedContracts.kerosene; vaultLicenser = deployedContracts.vaultLicenser; vaultManager = deployedContracts.vaultManager; ethVault = deployedContracts.ethVault; wstEth = deployedContracts.wstEth; kerosineManager = deployedContracts.kerosineManager; unboundedKerosineVault = deployedContracts.unboundedKerosineVault; boundedKerosineVault = deployedContracts.boundedKerosineVault; kerosineDenominator = deployedContracts.kerosineDenominator; wETH = IWETH(MAINNET_WETH); vm.deal(user, 1000 ether); vm.prank(user); wETH.deposit{value: 100 ether}(); vm.prank(MAINNET_OWNER); kerosene.transfer(user, 10_000 ether); DNft dNFT = DNft(MAINNET_DNFT); vm.prank(user); userNFTId = dNFT.mintNft{value: 100 ether}(user); } function testCannotAddKerosineVault() public { vm.startPrank(user); // Using add() here instead of addKerosene() because of a bug I described in another issue vaultManager.addKerosene(userNFTId, address(unboundedKerosineVault)); kerosene.approve(address(vaultManager), 1_000 ether); vaultManager.deposit(userNFTId, address(unboundedKerosineVault), 1_000 ether); vm.stopPrank(); } }
We can see how the most important functionallity is blocked. The addKerosene
call reverts. This test is supposed to revert.
Manual Review
To resolve this issue, in the deploy script instead of adding the non-Kerosine vaults add the Kerosine ones in the kerosineManager
KerosineManager kerosineManager = new KerosineManager(); - kerosineManager.add(address(ethVault)); - kerosineManager.add(address(wstEth)); + kerosineManager.add(address(unboundedKerosineVault)); + //kerosineManager.add(address(boundedKerosineVault));
This of course should happen after the Kerosine vaults are deployed.
Now we need to change how the asset price is calculated since it should use only the exogenous collateral vaults. The best way is to calculate the TVL using vaultLicenser
and in that Licenser we should include only non-Kerosine vaults
Error
#0 - c4-pre-sort
2024-04-28T04:38:40Z
JustDravee marked the issue as duplicate of #70
#1 - c4-pre-sort
2024-04-29T09:37:28Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-12T08:54:22Z
koolexcrypto marked the issue as satisfactory
#3 - c4-judge
2024-05-13T18:36:27Z
koolexcrypto changed the severity to 2 (Med Risk)