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: 108/183
Findings: 4
Award: $7.94
🌟 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/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L80 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L106
The system is designed with the vaultsKerosene mapping storing vaultsKerosene, and vaults storing other vaults (currently only WETH and wstETH).
However, incorrect implementation of the add and remove functions in the VaultManagerV2 contract results in the system deviating from its design.
From Deploy.V2.s.sol, it can be seen that all vaults will be added to the Licenser contract. The KerosineManager contract is used to store regular vaults used for LVT valuation (WETH and wstETH).
However, in the VaultManagerV2 contract, it mistakenly assumes that WETH and wstETH vaults will be added to the Licenser contract, and vaultsKerosene will be added to the KerosineManager contract. GitHub:https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L67
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); } 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); }
This leads to the following consequences:
Below is part of the test code:
function testValutAddError() public { testLicenseVaultManager(); address addr1 = makeAddr("addr1"); deal(MAINNET_WETH, addr1, 10e18); assertEq(IERC20(MAINNET_WETH).balanceOf(addr1), 10e18); vm.startPrank(MAINNET_OWNER); uint256 id1 = DNft(MAINNET_DNFT).mintInsiderNft(addr1); IERC20(MAINNET_KEROSENE).transfer(addr1, 10e18); assertEq(IERC20(MAINNET_KEROSENE).balanceOf(addr1), 10e18); vm.stopPrank(); vm.startPrank(addr1); //deposit weth contracts.vaultManager.add(id1, address(contracts.ethVault)); IERC20(MAINNET_WETH).approve(address(contracts.vaultManager), 1e18); contracts.vaultManager.deposit(id1, address(contracts.ethVault), 1e18); console.log("AfterDepositInWETHVault noKerosineVaultValue:"); console.log(contracts.vaultManager.getNonKeroseneValue(id1)); //repeat add weth console.log("Before repeat add wethVault totalValue:"); console.log(contracts.vaultManager.getTotalUsdValue(id1)); contracts.vaultManager.addKerosene(id1, address(contracts.ethVault)); console.log("After repeat add wethVault totalValue:"); console.log(contracts.vaultManager.getTotalUsdValue(id1)); vm.stopPrank(); }
Manual audit, foundry
Add a mapping in the licenser contract to store the Kerosine vaults:
contract Licenser is Owned(msg.sender) { mapping(address => bool) public isLicensed; mapping(address => bool) public isKerosineLicensed; constructor() {} function add(address vault) external onlyOwner { isLicensed[vault] = true; } function addKerosine(address vault) external onlyOwner { isKerosineLicensed[vault] = true; } function remove(address vault) external onlyOwner { isLicensed[vault] = false; } function removeKerosine(address vault) external onlyOwner { isKerosineLicensed[vault] = false; } }
VaultManager Contract
function addKerosene(uint id, address vault) external isDNftOwner(id) { if (vaultsKerosene[id].length() >= MAX_VAULTS_KEROSENE) revert TooManyVaults(); if (!vaultLicenser.isKerosineLicensed(vault)) revert VaultNotLicensed(); if (!vaultsKerosene[id].add(vault)) revert VaultAlreadyAdded(); emit Added(id, vault); } function removeKerosene(uint id, address vault) external isDNftOwner(id) { if (Vault(vault).id2asset(id) > 0) revert VaultHasAssets(); if (!vaultLicenser.isKerosineLicensed(vault)) revert VaultNotLicensed(); emit Removed(id, vault); }
Error
#0 - c4-pre-sort
2024-04-28T07:02:15Z
JustDravee marked the issue as duplicate of #966
#1 - c4-pre-sort
2024-04-29T08:37:38Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-04T09:46:28Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2024-05-28T15:28:33Z
koolexcrypto marked the issue as duplicate of #1133
#4 - c4-judge
2024-05-29T07:07:00Z
koolexcrypto marked the issue as satisfactory
🌟 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
Attackers can monitor the txpool, front-run based on MEV, and perform DoS on a withdrawal operation of a specific NFT user.
Since anyone can deposit for an NFT, regardless of whether they are the owner of the NFT.
However, the contract specifies that deposit and withdrawal operations for an NFT cannot occur in the same block.
github:https://github.com/code-423n4/2024-04-dyad/blob/4a987e536576139793a1c04690336d06c93fca90/src/core/VaultManagerV2.sol#L143
if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock();
There is no limit on the amount of deposit, which means the attacker only needs to spend gas to perform a DoS attack on the target NFT.
POC:
Manual audit
function deposit( uint id, address vault, uint amount ) external isDNftOwner(id) {
Access Control
#0 - c4-pre-sort
2024-04-27T11:54:23Z
JustDravee marked the issue as duplicate of #489
#1 - c4-pre-sort
2024-04-29T09:31:46Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-05T20:38:19Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2024-05-05T20:39:24Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#4 - c4-judge
2024-05-05T20:39:57Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#5 - c4-judge
2024-05-05T21:44:28Z
koolexcrypto marked the issue as nullified
#6 - c4-judge
2024-05-05T21:44:33Z
koolexcrypto marked the issue as not nullified
#7 - c4-judge
2024-05-08T15:27:53Z
koolexcrypto marked the issue as duplicate of #1001
#8 - c4-judge
2024-05-11T19:49:41Z
koolexcrypto marked the issue as satisfactory
#9 - c4-judge
2024-05-13T18:34:30Z
koolexcrypto changed the severity to 3 (High Risk)
🌟 Selected for report: Circolors
Also found by: 0x175, 0x486776, 0xAlix2, 0xSecuri, 0xShitgem, 0xfox, 0xlemon, 0xnilay, 3th, 4rdiii, Aamir, Al-Qa-qa, AlexCzm, Egis_Security, Evo, Honour, Infect3d, Josh4324, Limbooo, Mahmud, SBSecurity, TheSchnilch, ahmedaghadi, alix40, amaron, bbl4de, bhilare_, btk, carrotsmuggler, cinderblock, d3e4, dimulski, dinkras, ducanh2706, iamandreiski, itsabinashb, ke1caM, ljj, sashik_eth, shaflow2, steadyman, web3km, y4y
3.8221 USDC - $3.82
The design of UnboundedKerosineVault allows users to deposit and withdraw Kerosine through VaultManagerV2. In the implementation of VaultManagerV2, users can deposit Kerosine into UnboundedKerosineVault by calling the deposit method in VaultManagerV2. However, when attempting to withdraw through a call to withdraw, the transaction will revert.
The reason for this is that Vault and KerosineVault have different valuation methods, and VaultManagerV2 does not consider the valuation method of KerosineVault.
Vault obtains prices through an oracle, hence it has an oracle field.
However, the withdraw function calls the oracle to get the price decimals, and since KerosineVault does not have an oracle field, it reverts during withdrawal.
github:https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L146C1-L150C1
uint value = amount * _vault.assetPrice() * 1e18 / 10**_vault.oracle().decimals() / 10**_vault.asset().decimals();
POC: Create test functions in v2.t.sol.
function testKerosineWithdraw() public { testLicenseVaultManager(); address addr1 = makeAddr("addr1"); deal(MAINNET_KEROSENE, addr1, 100e18); assertEq(IERC20(MAINNET_KEROSENE).balanceOf(addr1), 100e18); vm.startPrank(MAINNET_OWNER); uint id1 = DNft(MAINNET_DNFT).mintInsiderNft(addr1); //id 645 vm.stopPrank(); vm.startPrank(addr1); IERC20(MAINNET_KEROSENE).approve( address(contracts.vaultManager), 100e18 ); vm.roll(1); contracts.vaultManager.deposit( id1, address(contracts.unboundedKerosineVault), 100e18 ); vm.roll(2); vm.expectRevert(); contracts.vaultManager.withdraw( id1, address(contracts.unboundedKerosineVault), 100e18, addr1 ); vm.stopPrank(); }
Manual audit, foundry
function withdraw( uint id, address vault, uint amount, address to ) public isDNftOwner(id) { if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock(); + if (vaultsKerosene[id].contains(vault)) { 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(); }
Error
#0 - c4-pre-sort
2024-04-26T20:55:19Z
JustDravee marked the issue as duplicate of #1048
#1 - c4-pre-sort
2024-04-28T18:38:34Z
JustDravee marked the issue as duplicate of #830
#2 - c4-pre-sort
2024-04-29T08:44:20Z
JustDravee marked the issue as sufficient quality report
#3 - c4-judge
2024-05-11T20:04:24Z
koolexcrypto marked the issue as satisfactory
🌟 Selected for report: 0xAlix2
Also found by: 0x486776, 0xabhay, 0xlucky, 0xtankr, Abdessamed, Circolors, CodeWasp, DarkTower, Egis_Security, Giorgio, Infect3d, Krace, KupiaSec, Limbooo, Maroutis, NentoR, Ryonen, SpicyMeatball, T1MOH, TheFabled, TheSavageTeddy, TheSchnilch, VAD37, XDZIBECX, btk, carrotsmuggler, cu5t0mpeo, dimulski, gumgumzum, iamandreiski, imare, itsabinashb, ke1caM, kennedy1030, lian886, n4nika, oakcobalt, sashik_eth, shaflow2, steadyman, web3km, windhustler, zhaojohnson
3.8221 USDC - $3.82
https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/script/deploy/Deploy.V2.s.sol#L42 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/script/deploy/Deploy.V2.s.sol#L74
During the system migration using the DeployV2 script, the previously deployed Dyad contract is used. Violating the condition LVT > Dyad TotalSupply can lead to a system DoS. Github:https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/script/deploy/Deploy.V2.s.sol#L42
VaultManagerV2 vaultManager = new VaultManagerV2( DNft(MAINNET_DNFT), Dyad(MAINNET_DYAD), vaultLicenser );
The main reason for this issue is that the Dyad contract on the mainnet already has a total supply of 632967400000000000000000.
However, in V2, each vault is redeployed, resulting in LVT being set to 0. If the old Dyad contract is used to migrate to V2, the system will underflow when calculating the assertPrice in the UnboundedKerosineVault contract.
This will cause users who have deposited into the UnboundedKerosineVault and users who attempt to interact with it to be unable to perform any operations.
uint numerator = tvl - dyad.totalSupply();
Below is the test function I added in v2.t.sol:
function testgetNonKeroseneValueRevert() public { testLicenseVaultManager(); address addr1 = makeAddr("addr1"); deal(MAINNET_WETH, addr1, 10e18); assertEq(IERC20(MAINNET_WETH).balanceOf(addr1), 10e18); vm.startPrank(MAINNET_OWNER); uint256 id1 = DNft(MAINNET_DNFT).mintInsiderNft(addr1); IERC20(MAINNET_KEROSENE).transfer(addr1, 10e18); assertEq(IERC20(MAINNET_KEROSENE).balanceOf(addr1), 10e18); vm.stopPrank(); vm.startPrank(addr1); contracts.vaultManager.add( id1, address(contracts.unboundedKerosineVault) ); IERC20(MAINNET_KEROSENE).approve(address(contracts.vaultManager), 1e18); contracts.vaultManager.deposit( id1, address(contracts.unboundedKerosineVault), 1e18 ); vm.expectRevert(); contracts.vaultManager.getNonKeroseneValue(id1); vm.stopPrank(); }
Manual audit
Deploy a new Dyad contract during migration.
DoS
#0 - c4-pre-sort
2024-04-29T07:43:49Z
JustDravee marked the issue as duplicate of #308
#1 - c4-pre-sort
2024-04-29T09:04:55Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-11T20:08:50Z
koolexcrypto marked the issue as satisfactory
#3 - c4-judge
2024-05-13T18:34:04Z
koolexcrypto changed the severity to 3 (High Risk)