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: 111/183
Findings: 3
Award: $7.65
π 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
Liquidation can be blocked by the NFT owner. This is because the of a miscalculation in the collateralratio.
The collatRatio
gets the getTotalUsdValue()
of an ID which is then used to calculate the actual collateralratio. However the getTotalUsdValue()
add all vaults in both vaults types:
function getTotalUsdValue( uint id ) public view returns (uint) { return getNonKeroseneValue(id) + getKeroseneValue(id); }
This is expected except for the fact whereby a vault is present in both vaults[]
and vaultsKerosene
.
If we look at the DeployV2
which is listed in scope, we see that the same vaults are licensed on both sides.
In DeployV2
:
kerosineManager.add(address(ethVault)); kerosineManager.add(address(wstEth)); . . . vaultLicenser.add(address(ethVault)); vaultLicenser.add(address(wstEth)); vaultLicenser.add(address(unboundedKerosineVault));
Hence by adding the same vault to both vaults[]
and vaultsKerosene
, Liquidations will revert, even though the collateral ratio is adding the same vault twice
function liquidate( uint id, uint to ) external isValidDNft(id) isValidDNft(to) { uint cr = collatRatio(id); if (cr >= MIN_COLLATERIZATION_RATIO) revert CrTooHigh();
Since collatRatio()
will return a high value even though there isn't enough collateral, liquidation will revert, the nft owner simply has to call the functions below using the same vault and 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); }
Manual Review, JosephDara
Prevent additon of a vault to either of the arrays if they are already contained in the other array
Other
#0 - c4-pre-sort
2024-04-29T07:17:18Z
JustDravee marked the issue as duplicate of #70
#1 - c4-pre-sort
2024-04-29T12:03:00Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-06T18:26:48Z
koolexcrypto marked the issue as not a duplicate
#3 - c4-judge
2024-05-06T18:26:52Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#4 - Josephdara
2024-05-16T04:19:01Z
Hi @koolexcrypto , I believe this has been judged wrongly. The deployment script in scope: https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/script/deploy/Deploy.V2.s.sol shows that some vaults are added to both Licensers:
KerosineManager kerosineManager = new KerosineManager(); kerosineManager.add(address(ethVault)); kerosineManager.add(address(wstEth));
and
vaultLicenser.add(address(ethVault)); vaultLicenser.add(address(wstEth)); vaultLicenser.add(address(unboundedKerosineVault));
However, when calculating the Collateral ratio, the value in the both licensed vaults is added together.
function collatRatio( uint id ) public view returns (uint) { uint _dyad = dyad.mintedDyad(address(this), id); if (_dyad == 0) return type(uint).max; return getTotalUsdValue(id).divWadDown(_dyad); } function getTotalUsdValue( uint id ) public view returns (uint) { return getNonKeroseneValue(id) + getKeroseneValue(id); } function getNonKeroseneValue( uint id ) public view returns (uint) { uint totalUsdValue; uint numberOfVaults = vaults[id].length(); for (uint i = 0; i < numberOfVaults; i++) { Vault vault = Vault(vaults[id].at(i)); uint usdValue; if (vaultLicenser.isLicensed(address(vault))) { usdValue = vault.getUsdValue(id); } totalUsdValue += usdValue; } return totalUsdValue; } 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; }
By having the same vault addresses licensed in both licensers, liquidations is blocked because the collateral ratio would be higher than it's actual figure.
To avoid liquidation, a user simply need to add the other vault using either add
or addKerosene
. And their CR is increased automatically
#5 - koolexcrypto
2024-05-22T09:48:31Z
Hi @Josephdara
Thank you for your feedback.
addKerosene
adds to vaultsKerosene
which is not used on liquidation, vaults
is used instead.
#6 - Josephdara
2024-05-22T09:57:41Z
Thanks for your reply. Pardon my comment however, liquidation uses collateral ratio Collateral ratio gets total value from both VaultsKerosene and Vaults https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L213
So the collateral ratio for an ID is larger since itβs adding from both arrays that contain the same address.
Please kindly confirm this, I would however respect your decision on this.
#7 - c4-judge
2024-05-29T08:50:42Z
koolexcrypto marked the issue as duplicate of #1133
#8 - koolexcrypto
2024-05-29T08:51:35Z
Thank you for the further clarification.
This is a dup of #1133
#9 - c4-judge
2024-05-29T08:51:50Z
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
Allowing any user to deposit funds for a particular ID opens up the smart contract to frontrunning, causing withdrawals to fail.
This is due to the check:
if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock();
The state variable idToBlockOfLastDeposit[]
is updated in the deposit()
, hence by depositing 1 wei of collateral in a block, withdrawals can be blocked.
Therefore frontrunning a withdrawal transaction with a small deposit causes DOS
Anyone can deposit to any ID
function deposit( uint id, address vault, uint amount ) external isValidDNft(id) { idToBlockOfLastDeposit[id] = block.number;
Manual Review, JosephDara
Remove the block check or restrict deposits to only the isDNftOwner(id)
DoS
#0 - c4-pre-sort
2024-04-27T11:23:56Z
JustDravee marked the issue as duplicate of #1103
#1 - c4-pre-sort
2024-04-27T11:51:48Z
JustDravee marked the issue as duplicate of #489
#2 - c4-pre-sort
2024-04-29T09:33:05Z
JustDravee marked the issue as sufficient quality report
#3 - c4-judge
2024-05-05T20:38:07Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#4 - c4-judge
2024-05-05T21:10:02Z
koolexcrypto marked the issue as nullified
#5 - c4-judge
2024-05-05T21:10:07Z
koolexcrypto marked the issue as not nullified
#6 - c4-judge
2024-05-08T15:30:02Z
koolexcrypto marked the issue as duplicate of #1001
#7 - c4-judge
2024-05-11T19:45:04Z
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
In the VaultManagerV2, vaults can be added if they are licensed, and removed if they have no funds for an ID. However, addition and removal of vaults can be blocked whenever a Vault which was previously licensed is delicensed.
This is because the withdraw function does not allow withdrawals from unlicensed vaults. Although the check is not done in that function, it is done in getNonKeroseneValue(id)
which skips every unlicensed vaults when adding all collateral.
Since users can not withdraw collateral from an unlicensed vault, it is impossible to remove the vault from the vaults / vaultsKerosene
array using remove() & removeKerosene()
.
Since the protocol already enforces a strict cap using MAX_VAULTS/MAX_VAULTS_KEROSENE
, a user cannot add new licensed vault using add() & addKerosene()
if they have their array populated with delicensed and licensed vaults.
One more issue here is, since a malicious user can deposit for any other user, immediately a vault is delicensed, they can simply deposit 1 wei value to all users who currently have that vault in their array, hence prevent removal from the vault from the vaults
array for users.
The check in the withdrawal & getNonKeroseneValue function
function withdraw( uint id, address vault, uint amount, address to ) public isDNftOwner(id) { if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock(); uint dyadMinted = dyad.mintedDyad(address(this), id); Vault _vault = Vault(vault); uint value = amount * _vault.assetPrice() * 1e18 / 10**_vault.oracle().decimals() / 10**_vault.asset().decimals(); if (getNonKeroseneValue(id) - value < dyadMinted) revert NotEnoughExoCollat(); //@audit getNonKeroseneValue called here _vault.withdraw(id, to, amount); if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); } 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))) { //@audit excluded all unlicensed vaults }
Withdrawal fails
//@audit cannot remove vaults when it is unlicensed. This is because, you cannot withdraw from unlicensed vaults. Hence id2asset will never be zero if (Vault(vault).id2asset(id) > 0) revert VaultHasAssets();
Addition fails here
function add( uint id, address vault ) external isDNftOwner(id) { if (vaults[id].length() >= MAX_VAULTS) revert TooManyVaults();
Manual Review, JosephDara
If a vault is delicensed, allow withdrawals from that vaults since it's funds are no more calculated as collateral.
DoS
#0 - c4-pre-sort
2024-04-29T07:44:33Z
JustDravee marked the issue as duplicate of #489
#1 - c4-pre-sort
2024-04-29T09:33:00Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-05T20:23:11Z
koolexcrypto marked the issue as not a duplicate
#3 - c4-judge
2024-05-06T08:57:11Z
koolexcrypto marked the issue as duplicate of #118
#4 - c4-judge
2024-05-11T12:24:26Z
koolexcrypto marked the issue as satisfactory
#5 - c4-judge
2024-05-13T18:33:43Z
koolexcrypto changed the severity to 2 (Med Risk)