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: 22/183
Findings: 4
Award: $464.92
🌟 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
Function getNonKeroseneValue() implementation is incorrect, which could lead to exogenous collateral is less than dyad debt. This is not allowed.
In VaultManagerV2, two kinds of assets can be taken as the collateral, exogenous collateral(WETH/WSTETH) and kerosine(unbounded and bounded kerosine). Function getNonKeroseneValue() aims to calculate one NFT position's exogenous collateral's value.
Users can add collateral asset through add(). From deploy.v2.s.sol, permited vaults include ethVault, wstETHVault, and unboundedKerosineVault. Users can add unboundedKerosineVault
into vaults[id]
.
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); }
When we calculate users' getNonKeroseneValue(), unbounded kerosine assets are calculated as one part of non-kerosine value.
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; }
Manual
Only weth and wsteth should be calculated for the getNonKeroseneValue() calculation.
Context
#0 - c4-pre-sort
2024-04-29T08:15:02Z
JustDravee marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-04-29T08:16:57Z
JustDravee marked the issue as duplicate of #966
#2 - c4-judge
2024-05-04T09:46:32Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2024-05-28T15:28:15Z
koolexcrypto marked the issue as duplicate of #1133
#4 - c4-judge
2024-05-29T14:02:52Z
koolexcrypto marked the issue as satisfactory
🌟 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
Function getKeroseneValue() is expected to calculate kerosene collateral value. Actual calculation includes exogenous collateral value.When we calculate getTotalUsdValue(), some assets might be calculated twice.
From Deploy.V2.s.sol, ethVault
and wstEth
are added into both vaultLicenser
and kerosineManager
. Then users can add ethVault
and wstEth
vault into both vaults[id]
and vaultsKerosene[id]
.
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); }
When VaultManagerV2 tries to calculate getTotalUsdValue(), value for weth and wsteth will be calculated for twice. Because weth and wstvault exist both in vaults[id] and vaultsKerosene[id], they will be taken as nonkerosene and kerosene at the same time.
function getTotalUsdValue( uint id ) public view returns (uint) { return getNonKeroseneValue(id) + getKeroseneValue(id); }
Manual
For vaultsKerosene(), we need calculate vaults which belong to vaults
, and not belong to vaultsKerosene
. In current design, vaults
includes all possible vaults and vaultsKerosene
include exogenous vaults.
Error
#0 - c4-pre-sort
2024-04-29T08:15:21Z
JustDravee marked the issue as duplicate of #966
#1 - c4-pre-sort
2024-04-29T08:16:31Z
JustDravee marked the issue as not a duplicate
#2 - c4-pre-sort
2024-04-29T08:16:40Z
JustDravee marked the issue as duplicate of #966
#3 - c4-pre-sort
2024-04-29T08:37:33Z
JustDravee marked the issue as sufficient quality report
#4 - c4-judge
2024-05-04T09:46:32Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#5 - c4-judge
2024-05-28T15:28:16Z
koolexcrypto marked the issue as duplicate of #1133
#6 - c4-judge
2024-05-29T14:02:55Z
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
Hacker can block normal user's withdraw() via deposit() function.
In VaultManagerV2, we will revert withdraw operation if idToBlockOfLastDeposit[id] == block.number
. This aims to prevent flashloan attack. However, this could block normal withdraw case if hacker deposit 0 amount for withdrawer via frontrun.
For example, Alice wants to withdraw some assets. Bob monitors and call one deposit() with alice's NFT id and 0 amount. And idToBlockOfLastDeposit[id]
will be updated to the latest.
function deposit( uint id, address vault, uint amount ) external isValidDNft(id) { idToBlockOfLastDeposit[id] = block.number; Vault _vault = Vault(vault); _vault.asset().safeTransferFrom(msg.sender, address(vault), amount); _vault.deposit(id, amount); }
When Alice withdraws her assets, the transaction will be reverted because of idToBlockOfLastDeposit[id]
has already updated to the latest.
function withdraw( uint id, address vault, uint amount, address to ) public isDNftOwner(id) { if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock(); ... }
Manual
Add one modifier for function deposit(), only NFT owner or owner's delegator can deposit() for the related NFT id.
DoS
#0 - c4-pre-sort
2024-04-27T11:58:02Z
JustDravee marked the issue as duplicate of #489
#1 - c4-pre-sort
2024-04-29T09:31:27Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-05T20:42:46Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2024-05-05T20:43:20Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#4 - c4-judge
2024-05-05T20:45:38Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#5 - c4-judge
2024-05-05T21:56:43Z
koolexcrypto marked the issue as nullified
#6 - c4-judge
2024-05-05T21:56:46Z
koolexcrypto marked the issue as not nullified
#7 - c4-judge
2024-05-08T15:26:28Z
koolexcrypto marked the issue as duplicate of #1001
#8 - c4-judge
2024-05-11T19:48:29Z
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
Function assetPrice() might be reverted when tvl < dyad.totalSupply(), which leads to some key actions' failure, such as withdraw(), liquidate().
In VaultManagerV2, kerosine is brought in the system. System will make sure NonKeroseneValue of each NFT position is larger than minted Dyad amount for this NFT position.
The vulnerability is when some exogenous asset price drop down a lot suddenly. The whole system's tvl may be less than dyad.totalSupply(). When liquidators want to liquidate some NFT's position with some kerosine assets, liquidate() will revert because of kerosine vault's assetPrice() will be reverted because of tvl is less than dyad.totalSupply().
function assetPrice() public view override returns (uint) { uint tvl; address[] memory vaults = kerosineManager.getVaults(); uint numberOfVaults = vaults.length; for (uint i = 0; i < numberOfVaults; i++) { Vault vault = Vault(vaults[i]); tvl += vault.asset().balanceOf(address(vault)) * vault.assetPrice() * 1e18 / (10**vault.asset().decimals()) / (10**vault.oracle().decimals()); } @==> uint numerator = tvl - dyad.totalSupply(); uint denominator = kerosineDenominator.denominator(); return numerator * 1e8 / denominator; }
Manual
According to doc, Kerosene is thus as valuable as the degree of DYAD’s overcollateralization. When tvl equals to dyad.totalSupply(), the value of kerosene is 0. It makes sense to mark kerosense's value as 0 when tvl is less than dyad.totalSupply().
DoS
#0 - c4-pre-sort
2024-04-27T18:26:52Z
JustDravee marked the issue as duplicate of #958
#1 - c4-pre-sort
2024-04-29T08:38:50Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-05T13:48:31Z
koolexcrypto marked the issue as duplicate of #308
#3 - c4-judge
2024-05-11T20:08:22Z
koolexcrypto marked the issue as satisfactory
#4 - c4-judge
2024-05-13T18:34:04Z
koolexcrypto changed the severity to 3 (High Risk)
🌟 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
UnboundedKerosineVault & BoundedKerosineVault's assetPrice() will be reverted when tvl < dyad. This would cause withdraw()/liquidate() failure.
VaultManagerV2 is one mitigation from VaultManager. From description in the code4rena webpage, The only transaction that needs to be done by the multi-sig after the deployment is licensing the new Vault Manager. We can consider that existing VaultManger will run at the same time.
The vulnerability is that VaultManager and VaultMangerV2 share the same DYAD Token and share to DYAD's totalSupply().
In UnboundedKerosineVault::assetPrice(), if tvl is less than dyad.totalSupply(), assetPrice() will be reverted. And tvl is exogenous collateral USD values in VaultManagerV2. dyad.totalSupply() will contain DYAD minted in VaultMangerV2 and VaultManger. It's easy to meet that condition: tvl is less than dyad.totalSupply().
function assetPrice() public view override returns (uint) { uint tvl; address[] memory vaults = kerosineManager.getVaults(); uint numberOfVaults = vaults.length; for (uint i = 0; i < numberOfVaults; i++) { Vault vault = Vault(vaults[i]); tvl += vault.asset().balanceOf(address(vault)) * vault.assetPrice() * 1e18 / (10**vault.asset().decimals()) / (10**vault.oracle().decimals()); } uint numerator = tvl - dyad.totalSupply(); uint denominator = kerosineDenominator.denominator(); return numerator * 1e8 / denominator; }
Manual
Calculated minted DYAD amount for each VaultManager.
DoS
#0 - c4-pre-sort
2024-04-27T18:27:00Z
JustDravee marked the issue as duplicate of #958
#1 - c4-pre-sort
2024-04-29T08:38:52Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-05T13:48:32Z
koolexcrypto marked the issue as duplicate of #308
#3 - c4-judge
2024-05-11T20:08:25Z
koolexcrypto marked the issue as satisfactory
460.7972 USDC - $460.80
Some bounded kerosine profits from liquidation will be locked in contract.
When liquidator liquidate one NFT position, all collateral assets will be moved to liquidator. If liquidator has no debt, liquidator can withdraw all profits. However, bounded kerosine vault is not allowed to withdraw. Liquidator may lose some profits.
For example, Alice borrow 100 DYAD via collateral WETH(100$) and bounded kerosine(50$). Now collateral price drops down, Bob wants to liquidate Alice's position. Bob will pay 100 DYAD and get WETH and bounded kerosine. Considering these bounded kerosine are not withdraw-able and transferable, Bob will lose some profit.
Manual
It's not easy to mitigate this case. Even if we allow liquidator to withdraw related bounded kerosine, it's also unfair for liquidators. Liquidator liquidate bounded kerosine, valued 50$(doubled because of bounded), liquidator will get nearly 25$ value kerosine even if we allow liquidator to withdraw.
Error
#0 - c4-pre-sort
2024-04-29T08:17:41Z
JustDravee marked the issue as duplicate of #1040
#1 - c4-pre-sort
2024-04-29T09:24:28Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-03T21:16:06Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2024-05-28T17:45:27Z
koolexcrypto removed the grade
#4 - c4-judge
2024-05-28T17:45:32Z
koolexcrypto marked the issue as not a duplicate
#5 - c4-judge
2024-05-28T17:45:39Z
koolexcrypto marked the issue as duplicate of #343
#6 - c4-judge
2024-05-28T17:45:45Z
koolexcrypto marked the issue as satisfactory
#7 - c4-judge
2024-05-28T17:47:09Z
koolexcrypto changed the severity to 2 (Med Risk)