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: 85/183
Findings: 6
Award: $26.03
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: MrPotatoMagic
Also found by: 0x175, 0x486776, 0x77, 0xAkira, 0xAsen, 0xDemon, 0xabhay, 0xblack_bird, 0xlemon, 0xloscar01, 0xtankr, 3docSec, 4rdiii, Abdessamed, AlexCzm, Angry_Mustache_Man, BiasedMerc, Circolors, Cryptor, DMoore, DPS, DedOhWale, Dinesh11G, Dots, GalloDaSballo, Giorgio, Honour, Imp, Jorgect, Krace, KupiaSec, Mrxstrange, NentoR, Pechenite, PoeAudits, Ryonen, SBSecurity, Sabit, T1MOH, TheFabled, TheSavageTeddy, Tychai0s, VAD37, Vasquez, WildSniper, ZanyBonzy, adam-idarrha, alix40, asui, blutorque, btk, c0pp3rscr3w3r, caglankaan, carrotsmuggler, d_tony7470, dimulski, dinkras, djxploit, falconhoof, forgebyola, grearlake, imare, itsabinashb, josephdara, kartik_giri_47538, ke1caM, kennedy1030, koo, lionking927, ljj, niser93, pep7siup, poslednaya, ptsanev, sashik_eth, shaflow2, steadyman, turvy_fuzz, ubl4nk, valentin_s2304, web3km, xyz, y4y, zhaojohnson, zigtur
0.0234 USDC - $0.02
https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/VaultManagerV2.sol#L143
Users could withdraw collateral tokens using the withdraw
function:
File: VaultManagerV2.sol 133: /// @inheritdoc IVaultManager 134: function withdraw( 135: uint id, 136: address vault, 137: uint amount, 138: address to 139: ) 140: public 141: isDNftOwner(id) 142: { 143: if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock(); 144: uint dyadMinted = dyad.mintedDyad(address(this), id); 145: Vault _vault = Vault(vault); 146: uint value = amount * _vault.assetPrice() 147: * 1e18 148: / 10**_vault.oracle().decimals() 149: / 10**_vault.asset().decimals(); 150: if (getNonKeroseneValue(id) - value < dyadMinted) revert NotEnoughExoCollat(); 151: _vault.withdraw(id, to, amount); 152: if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); 153: }
As we can see in line 143 - this function checks if there were deposits on behalf of the current note in the same block.
At the same time, the deposit
function allows anybody to deposit any amount on another's notes behalf and set idToBlockOfLastDeposit
to the latest block:
File: VaultManagerV2.sol 119: function deposit( 120: uint id, 121: address vault, 122: uint amount 123: ) 124: external 125: isValidDNft(id) 126: { 127: idToBlockOfLastDeposit[id] = block.number; 128: Vault _vault = Vault(vault); 129: _vault.asset().safeTransferFrom(msg.sender, address(vault), amount); 130: _vault.deposit(id, amount); 131: }
All this opens a griefing vector when the attacker deposits dust amount (or even 0 amount since the deposit function fails to check if the vault address is a real vault), resets the idToBlockOfLastDeposit
value for desired note, and blocks withdrawing calls for this note in the current block. The attack could be repeated multiple times DOSing any note withdrawals.
Withdrawing could be DOSed using dust deposits
Consider the next scenario:
Consider disallowing deposits to notes that the user doesn't own.
Access Control
#0 - c4-pre-sort
2024-04-28T06:28:54Z
JustDravee marked the issue as duplicate of #489
#1 - c4-pre-sort
2024-04-29T09:31:52Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-05T20:38:08Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2024-05-05T21:12:42Z
koolexcrypto marked the issue as nullified
#4 - c4-judge
2024-05-05T21:12:49Z
koolexcrypto marked the issue as not nullified
#5 - c4-judge
2024-05-08T15:29:25Z
koolexcrypto marked the issue as duplicate of #1001
#6 - c4-judge
2024-05-11T19:44:43Z
koolexcrypto marked the issue as satisfactory
🌟 Selected for report: Circolors
Also found by: 0x175, 0x486776, 0xAlix2, 0xSecuri, 0xShitgem, 0xfox, 0xlemon, 0xnilay, 3th, 4rdiii, Aamir, Al-Qa-qa, AlexCzm, Egis_Security, Evo, Honour, Infect3d, Josh4324, Limbooo, Mahmud, SBSecurity, TheSchnilch, ahmedaghadi, alix40, amaron, bbl4de, bhilare_, btk, carrotsmuggler, cinderblock, d3e4, dimulski, dinkras, ducanh2706, iamandreiski, itsabinashb, ke1caM, ljj, sashik_eth, shaflow2, steadyman, web3km, y4y
3.8221 USDC - $3.82
https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/VaultManagerV2.sol#L148
The protocol includes two vaults that hold Kerosene tokens - UnboundedKerosineVault
and BoundedKerosineVault
. While withdrawing from the bounded tokens is impossible by design, withdrawing from the unbounded vault should be available for the users.
Two functions could be used for withdrawing assets from the vaults - withdraw
and redeemDyad
. The last one uses withdraw
inside, so we are interested only in the first one:
File: VaultManagerV2.sol 134: function withdraw( 135: uint id, 136: address vault, 137: uint amount, 138: address to 139: ) 140: public 141: isDNftOwner(id) 142: { 143: if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock(); 144: uint dyadMinted = dyad.mintedDyad(address(this), id); 145: Vault _vault = Vault(vault); 146: uint value = amount * _vault.assetPrice() 147: * 1e18 148: / 10**_vault.oracle().decimals() 149: / 10**_vault.asset().decimals(); 150: if (getNonKeroseneValue(id) - value < dyadMinted) revert NotEnoughExoCollat(); 151: _vault.withdraw(id, to, amount); 152: if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); 153: }
As we can see in line 148 - the contract executes the call to vault.oracle()
function, however, UnboundedKerosineVault
lacks implementation of such a function. This means that any call to withdraw
and redeemDyad
with the UnboundedKerosineVault
address as a vault
parameter would always revert.
Users would not be able to withdraw Kerosene from the UnboundedKerosineVault
, while they should.
Consider the next scenario:
UnboundedKerosineVault
to use them as a part of the collateral for a future position.oracle()
on the UnboundedKerosineVault
address.Consider skipping the evaluation value
inside the withdraw
function for the UnboundedKerosineVault
vault.
Token-Transfer
#0 - c4-pre-sort
2024-04-26T21:35:01Z
JustDravee marked the issue as duplicate of #1048
#1 - c4-pre-sort
2024-04-28T18:39:25Z
JustDravee marked the issue as duplicate of #830
#2 - c4-pre-sort
2024-04-29T08:44:24Z
JustDravee marked the issue as sufficient quality report
#3 - c4-judge
2024-05-11T20:05:22Z
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/main/src/core/Vault.kerosine.unbounded.sol#L65
UnboundedKerosineVault#assetPrice function does not count TVL from V1 vaults
UnboundedKerosineVault#assetPrice
function calculates the price of the Kerosene token as a collateral asset:
File: Vault.kerosine.unbounded.sol 50: function assetPrice() 51: public 52: view 53: override 54: returns (uint) { 55: uint tvl; 56: address[] memory vaults = kerosineManager.getVaults(); 57: uint numberOfVaults = vaults.length; 58: for (uint i = 0; i < numberOfVaults; i++) { 59: Vault vault = Vault(vaults[i]); 60: tvl += vault.asset().balanceOf(address(vault)) 61: * vault.assetPrice() * 1e18 62: / (10**vault.asset().decimals()) 63: / (10**vault.oracle().decimals()); 64: } 65: uint numerator = tvl - dyad.totalSupply(); 66: uint denominator = kerosineDenominator.denominator(); 67: return numerator * 1e8 / denominator; 68: }
As we can see numerator value depends on two values - the totalSupply
of DYAD that could be minted by vaultManagerV2 and already minted by vaultManagerV1, and the tvl
value, which here is equal to the sum of all collateral value from vaults that added to kerosineManager
list. From the deployment script it's clear that only V2 vaults Vault.sol
and Vault.wsteth.sol
would be added to this list:
File: Deploy.V2.s.sol 64: kerosineManager.add(address(ethVault)); 65: kerosineManager.add(address(wstEth));
However, current vaults from V1 already hold some amount of collateral tokens and there is some amount of DYAD token that minted against this collateral. The protocol does not have a way to force the burning of these tokens and returning collateral to initial minters.
This means that the numerator
value would be incorrectly calculated because of counting TVL only from V2 vaults but DYAD tokens(total supply) from both V1 and V2 versions.
Kerosene price would be incorrectly calculated, potentially blocking minting against Kerosene token, which is the core functionality of the protocol.
Consider the next scenario:
vaultManager
V1 using a $150 collateral value of WETH, minting 100 DYAD tokens. Current total supply of DYAD = 100.vaultManager
V2 using a $150 collateral value of WETH, minting 100 DYAD tokens. Current total supply of DYAD = 200.vaultManager
V2 using the $100 collateral value of WETH and
1000 Kerosene tokens, Charlie currently holds 100 percent of Kerosene free supply, this means he should be able to utilize all surplus collateral, however, his tx reverts since assetPrice()
encounters only Bob's collateral ($150) while the total supply of DYAD (200).Should note that users of V1 manager in this way intentionally or accidentally could DOS using Kerosene by opening/increasing positions in V1.
Consider updating the UnboundedKerosineVault#assetPrice
function so it's counting collateral value from V1 vaults in total TVL.
Math
#0 - c4-pre-sort
2024-04-27T18:18:31Z
JustDravee marked the issue as high quality report
#1 - c4-pre-sort
2024-04-27T18:18:36Z
JustDravee marked the issue as duplicate of #958
#2 - c4-pre-sort
2024-04-29T08:38:58Z
JustDravee marked the issue as sufficient quality report
#3 - c4-judge
2024-05-05T13:48:45Z
koolexcrypto marked the issue as duplicate of #308
#4 - c4-judge
2024-05-11T20:09:54Z
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/main/src/core/Vault.kerosine.unbounded.sol#L65
The assetPrice()
function calculates price of Kerosene in both Kerosene vaults:
File: Vault.kerosine.unbounded.sol 50: function assetPrice() 51: public 52: view 53: override 54: returns (uint) { 55: uint tvl; 56: address[] memory vaults = kerosineManager.getVaults(); 57: uint numberOfVaults = vaults.length; 58: for (uint i = 0; i < numberOfVaults; i++) { 59: Vault vault = Vault(vaults[i]); 60: tvl += vault.asset().balanceOf(address(vault)) 61: * vault.assetPrice() * 1e18 62: / (10**vault.asset().decimals()) 63: / (10**vault.oracle().decimals()); 64: } 65: uint numerator = tvl - dyad.totalSupply(); 66: uint denominator = kerosineDenominator.denominator(); 67: return numerator * 1e8 / denominator; 68: }
This function is called during the execution of liquidation in the case the user previously added any Kerosene vault to his note independently if there is any Kerosene deposit for this note or not.
As we can see in line 65 - if the tvl
value becomes less than the dyad.totalSupply()
execution of the function would be reverted due to overflow. All this could lead to a scenario when after a rapid drop in the prices of collateral execution of the liquidations would be DOSed, resulting in locked collateral inside of the protocol.
Collateral tokens would be locked if TVL would drop less than the totalSupply
of DYAD, liquidations would revert in this case.
Consider the next scenario:
assetPrice
on Kerosine vaults (both depositors added Kerosine vaults to their notes). Since positions were "unhealthy", depositors also could not withdraw collateral without additional deposits.Consider returning 0 price for the Kerosene token in case TVL is less than the totalSupply
of DYAD. This would allow the execution of liquidations even in these conditions, preventing locking collateral assets inside the protocol.
Math
#0 - c4-pre-sort
2024-04-29T07:25:50Z
JustDravee marked the issue as duplicate of #224
#1 - c4-pre-sort
2024-04-29T09:03:59Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-08T08:32:00Z
koolexcrypto marked the issue as duplicate of #308
#3 - c4-judge
2024-05-11T20:10:02Z
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/main/src/core/Vault.kerosine.unbounded.sol#L65
The assetPrice()
function calculates the Kerosene price:
File: Vault.kerosine.unbounded.sol 50: function assetPrice() 51: public 52: view 53: override 54: returns (uint) { 55: uint tvl; 56: address[] memory vaults = kerosineManager.getVaults(); 57: uint numberOfVaults = vaults.length; 58: for (uint i = 0; i < numberOfVaults; i++) { 59: Vault vault = Vault(vaults[i]); 60: tvl += vault.asset().balanceOf(address(vault)) 61: * vault.assetPrice() * 1e18 62: / (10**vault.asset().decimals()) 63: / (10**vault.oracle().decimals()); 64: } 65: uint numerator = tvl - dyad.totalSupply(); 66: uint denominator = kerosineDenominator.denominator(); 67: return numerator * 1e8 / denominator; 68: }
It uses the tvl
value to evaluate how much surplus collateral exists in the protocol (L65). The problem is that not all values deposited to the vaults are actually locked
. Users can deposit collateral and withdraw it later freely in case it's not back a minimal percent (150%) for minted DYAD. This means that the price of Kerosene would be inflated by this collateral, potentially opening doors for risk-free trades or trades with hedged risk. In bad cases, this would affect holders of DYAD tokens as shown in the POC lower.
TVL wrongly includes collateral that is not used for minting, this potentially allows users to create risk-free long positions for collateral tokens.
Consider the next scenario (for simplicity let's assume that Alice is the only depositor, she holds all "free" Kerosene, and price of WETH and WSTETH could move independently for some percent):
As a result, Alice received the possibility of a risk-free position using Bob's liquidity.
Consider updating protocol logic, so it separately encounters collateral that is used as a minimum (150%) collateral for minted positions as TVL.
Other
#0 - c4-pre-sort
2024-04-29T07:26:21Z
JustDravee marked the issue as duplicate of #224
#1 - c4-pre-sort
2024-04-29T09:04:02Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-08T08:32:02Z
koolexcrypto marked the issue as duplicate of #308
#3 - c4-judge
2024-05-09T11:55:06Z
koolexcrypto marked the issue as not a duplicate
#4 - c4-judge
2024-05-09T12:13:25Z
koolexcrypto marked the issue as duplicate of #308
#5 - c4-judge
2024-05-11T20:10:06Z
koolexcrypto marked the issue as satisfactory
🌟 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
https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/VaultManagerV2.sol#L221
The liquidate
function allows the liquidator to receive a portion of the liquidated position, this is done by iterating through positions vaults and moving the appropriate part of its assets to the liquidator's note (L222):
File: VaultManagerV2.sol 205: function liquidate( 206: uint id, 207: uint to 208: ) 209: external 210: isValidDNft(id) 211: isValidDNft(to) 212: { 213: uint cr = collatRatio(id); 214: if (cr >= MIN_COLLATERIZATION_RATIO) revert CrTooHigh(); 215: dyad.burn(id, msg.sender, dyad.mintedDyad(address(this), id)); 216: 217: uint cappedCr = cr < 1e18 ? 1e18 : cr; 218: uint liquidationEquityShare = (cappedCr - 1e18).mulWadDown(LIQUIDATION_REWARD); 219: uint liquidationAssetShare = (liquidationEquityShare + 1e18).divWadDown(cappedCr); 220: 221: uint numberOfVaults = vaults[id].length(); 222: for (uint i = 0; i < numberOfVaults; i++) { 223: Vault vault = Vault(vaults[id].at(i)); 224: uint collateral = vault.id2asset(id).mulWadUp(liquidationAssetShare); 225: vault.move(id, to, collateral); 226: } 227: emit Liquidate(id, msg.sender, to); 228: }
While it's working correctly for the V1 version, in V2 each note could also have assets in keroseneVaults
. A portion of Kerosene in collateral value could go up to 50% of the needed minimum of 150%. As Kerosene vaults take part in the collateral ratio of the position - it's also should be moved proportionally to the liquidator's note
But as we can see liquidate
functions lack to iterate through Kerosene vaults and move part of liquidated position assets to the liquidator's note, which means that liquidators would miss part of its assets.
Liquidators would not receive part of their expected value or would have a lack of economic motivation to liquidate positions.
Consider the next scenario:
Consider updating the liquidate
function so it iterates through the Kerosene vaults of the liquidated notes in the way it is already done with non-Kerosene vaults.
Token-Transfer
#0 - c4-pre-sort
2024-04-28T10:21:37Z
JustDravee marked the issue as duplicate of #128
#1 - c4-pre-sort
2024-04-29T09:06:50Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-11T19:40:02Z
koolexcrypto marked the issue as satisfactory
🌟 Selected for report: TheSavageTeddy
Also found by: 0x175, 0x486776, 0xnev, AamirMK, AlexCzm, ArmedGoose, BiasedMerc, CaeraDenoir, Egis_Security, Jorgect, KYP, MrPotatoMagic, PoeAudits, SBSecurity, SovaSlava, VAD37, adam-idarrha, alix40, carrotsmuggler, d_tony7470, dimulski, grearlake, josephdara, ljj, n0kto, okolicodes, sashik_eth, sil3th, turvy_fuzz
7.3512 USDC - $7.35
https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/VaultManagerV2.sol#L101
Users can add and later remove vault addresses from their notes. Removing could be done using remove
and removeKerosene
functions:
File: VaultManagerV2.sol 094: function remove( 095: uint id, 096: address vault 097: ) 098: external 099: isDNftOwner(id) 100: { 101: if (Vault(vault).id2asset(id) > 0) revert VaultHasAssets(); 102: if (!vaults[id].remove(vault)) revert VaultNotAdded(); 103: emit Removed(id, vault); 104: } 105: 106: function removeKerosene( 107: uint id, 108: address vault 109: ) 110: external 111: isDNftOwner(id) 112: { 113: if (Vault(vault).id2asset(id) > 0) revert VaultHasAssets(); 114: if (!vaultsKerosene[id].remove(vault)) revert VaultNotAdded(); 115: emit Removed(id, vault); 116: }
As we can see each function during removing checks if the note position balance in the desired vault is equal to 0 and reverts if opposite (L101 and L114). Since anyone could deposit an arbitrary amount of collateral on behalf of other's notes, this opens a griefing vector when attacker DOS removes vault addresses by sending dust amount deposits to other users' notes.
Anyone could DOS execution of remove
and removeKerosene
functions
Consider the next scenario:
Vault
address to her note with id 1.remove
function.id2asset()
function returns a value greater than 0, blocking the execution of Alice tx.Consider disallowing deposits to notes that the user doesn't own.
Access Control
#0 - c4-pre-sort
2024-04-28T06:28:58Z
JustDravee marked the issue as duplicate of #489
#1 - c4-pre-sort
2024-04-29T09:31:50Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-05T20:38:09Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2024-05-05T21:13:28Z
koolexcrypto marked the issue as nullified
#4 - c4-judge
2024-05-05T21:13:35Z
koolexcrypto marked the issue as not nullified
#5 - c4-judge
2024-05-05T21:13:44Z
koolexcrypto marked the issue as not a duplicate
#6 - c4-judge
2024-05-06T08:55:46Z
koolexcrypto marked the issue as duplicate of #118
#7 - c4-judge
2024-05-11T12:24:20Z
koolexcrypto marked the issue as satisfactory
🌟 Selected for report: carrotsmuggler
Also found by: 0xAlix2, 0xSecuri, 0xleadwizard, 0xlemon, 0xtankr, 3th, Aamir, Abdessamed, Bauchibred, Circolors, Egis_Security, Evo, Hueber, Mahmud, SBSecurity, TheSavageTeddy, TheSchnilch, Tychai0s, alix40, bbl4de, btk, d3e4, ducanh2706, falconhoof, itsabinashb, ke1caM, lian886, n4nika, oakcobalt, pontifex, sashik_eth, steadyman, tchkvsky, zhuying
3.7207 USDC - $3.72
https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/VaultManagerV2.sol#L75
The add
function allows users to add vaults to a list of non-kerosene collateral vaults per their notes, it checks if an added vault is licensed in vaultLicenser
(L75):
File: VaultManagerV2.sol 67: function add( 68: uint id, 69: address vault 70: ) 71: external 72: isDNftOwner(id) 73: { 74: if (vaults[id].length() >= MAX_VAULTS) revert TooManyVaults(); 75: if (!vaultLicenser.isLicensed(vault)) revert VaultNotLicensed(); 76: if (!vaults[id].add(vault)) revert VaultAlreadyAdded(); 77: emit Added(id, vault); 78: }
If we check the deployment script, we can see that both Kerosene and non-kerosene types of vaults would be added to the licenser:
File: Deploy.V2.s.sol 93: vaultLicenser.add(address(ethVault)); 94: vaultLicenser.add(address(wstEth)); 95: vaultLicenser.add(address(unboundedKerosineVault));
All this means that users would be able to add an unboundedKerosineVault
vault address to their vaults[id]
list. The problem is that the getNonKeroseneValue
function iterates through this list and calculates the sum of non-kerosene collateral for the given note id:
File: VaultManagerV2.sol 250: function getNonKeroseneValue( 251: uint id 252: ) 253: public 254: view 255: returns (uint) { 256: uint totalUsdValue; 257: uint numberOfVaults = vaults[id].length(); 258: for (uint i = 0; i < numberOfVaults; i++) { 259: Vault vault = Vault(vaults[id].at(i)); 260: uint usdValue; 261: if (vaultLicenser.isLicensed(address(vault))) { 262: usdValue = vault.getUsdValue(id); 263: } 264: totalUsdValue += usdValue; 265: } 266: return totalUsdValue; 267: }
This allows users to inflate their non-kerosene collateral amount and total collateral ratio by adding the Kerosene vault to the non-kerosene vault list.
Users could inflate their collateral ratio by adding Kerosene vaults as non-kerosene
Consider the next scenario:
unboundedKerosineVault
address using the add
function.Here one of the main protocols invariant (1 DYAD should be minted with at least $1 of exogenous collateral) is broken.
There are also scenarios with this bug - e.g. when the same Kerosene tokens are counted two times (as non-kerosene and kerosene vault values) or when liquidation of such position is blocked since the liquidator part would be tried to move twice, etc.
But the main idea is clear - that the Kerosene vault should not ever be added to the non-kerosene vaults[id]
list.
Consider updating the add
function in the way it checks if the added vault is not a Kerosene vault.
Access Control
#0 - c4-pre-sort
2024-04-29T05:26:12Z
JustDravee marked the issue as duplicate of #70
#1 - c4-pre-sort
2024-04-29T12:01:58Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-11T20:00:54Z
koolexcrypto marked the issue as satisfactory
#3 - c4-judge
2024-05-13T18:36:27Z
koolexcrypto changed the severity to 2 (Med Risk)
🌟 Selected for report: carrotsmuggler
Also found by: 0xAlix2, 0xSecuri, 0xleadwizard, 0xlemon, 0xtankr, 3th, Aamir, Abdessamed, Bauchibred, Circolors, Egis_Security, Evo, Hueber, Mahmud, SBSecurity, TheSavageTeddy, TheSchnilch, Tychai0s, alix40, bbl4de, btk, d3e4, ducanh2706, falconhoof, itsabinashb, ke1caM, lian886, n4nika, oakcobalt, pontifex, sashik_eth, steadyman, tchkvsky, zhuying
3.7207 USDC - $3.72
https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/VaultManagerV2.sol#L88
Users should be able to add Kerosene vaults to their notes using addKerosene
function:
File: VaultManagerV2.sol 80: function addKerosene( 81: uint id, 82: address vault 83: ) 84: external 85: isDNftOwner(id) 86: { 87: if (vaultsKerosene[id].length() >= MAX_VAULTS_KEROSENE) revert TooManyVaults(); 88: if (!keroseneManager.isLicensed(vault)) revert VaultNotLicensed(); 89: if (!vaultsKerosene[id].add(vault)) revert VaultAlreadyAdded(); 90: emit Added(id, vault); 91: }
At line 88 this function includes checking if the given vault is licensed in the keroseneManager
contract. However, the keroseneManager
contract actually contains addresses of the non-kerosene vault. The purpose of its contract is to provide list of non-kerosene vaults for assetPrice()
. We could ensure this by checking the deployment script:
File: Deploy.V2.s.sol 64: kerosineManager.add(address(ethVault)); 65: kerosineManager.add(address(wstEth));
This means that the Kerosene vaults would not be possible to add per user's notes, breaking using the Kerosene token as collateral, which is one of the core functionalities of the protocol.
Also, Kerosene vaults were wrongly checked using the keroseneManager
inside the getKeroseneValue
function (L280):
File: VaultManagerV2.sol 269: function getKeroseneValue( 270: uint id 271: ) 272: public 273: view 274: returns (uint) { 275: uint totalUsdValue; 276: uint numberOfVaults = vaultsKerosene[id].length(); 277: for (uint i = 0; i < numberOfVaults; i++) { 278: Vault vault = Vault(vaultsKerosene[id].at(i)); 279: uint usdValue; 280: if (keroseneManager.isLicensed(address(vault))) { 281: usdValue = vault.getUsdValue(id); 282: } 283: totalUsdValue += usdValue; 284: } 285: return totalUsdValue; 286: } 287:
Users would not be able to add Kerosene vaults to their notes.
Consider creating a separate licenser for Kerosene vaults and check vault addresses using it.
Access Control
#0 - c4-pre-sort
2024-04-29T05:26:07Z
JustDravee marked the issue as duplicate of #70
#1 - c4-pre-sort
2024-04-29T12:02:02Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-11T20:00:57Z
koolexcrypto marked the issue as satisfactory
#3 - c4-judge
2024-05-13T18:36:27Z
koolexcrypto changed the severity to 2 (Med Risk)