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: 98/183
Findings: 5
Award: $12.81
🌟 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/4a987e536576139793a1c04690336d06c93fca90/script/deploy/Deploy.V2.s.sol#L95 https://github.com/code-423n4/2024-04-dyad/blob/4a987e536576139793a1c04690336d06c93fca90/src/core/VaultManagerV2.sol#L75
Malicious user can add UnboundedKeroseneVault to both vaults
and vaultsKerosene
, causing VaultManagerV2::getTotalUsdValue()
to return more than the user actually has, possibly creating bad debt for the protocol.
The function VaultManager::add()
performs a very crucial check to see if the vault is licensed before adding it.
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); }
However inside DeployV2.s.sol
at line 95, we are licensing the unbounded kerosene vault. Which will cause this check to pass and add the kerosene vault in the list of exogenous vaults.
Paste this inside of test/fork/v2.sol.
function testUserCanAddNonKeroseneVaultsToVaultsList() public { DNft dNft = DNft(MAINNET_DNFT); address user = makeAddr("user"); vm.deal(user, 200 ether); vm.startPrank(user); uint id = dNft.mintNft{value: 5 ether}(user); VaultManagerV2 vaultManager = contracts.vaultManager; vaultManager.add(id, address(contracts.ethVault)); vaultManager.add(id, address(contracts.unboundedKerosineVault)); vm.stopPrank(); }
and run a forked test using:
forge test --fork-url <MAINNET_FORK_URL> --match-test testUserCanAddNonKeroseneVaultsToVaultsList
Foundry, Manual review
Consider not licensing any Kerosene vaults through the licenser as the users will be able to add them to their exogenous collateral.
Other
#0 - c4-pre-sort
2024-04-29T05:44:17Z
JustDravee marked the issue as duplicate of #966
#1 - c4-pre-sort
2024-04-29T08:37:35Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-04T09:46:29Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2024-05-28T15:28:29Z
koolexcrypto marked the issue as duplicate of #1133
#4 - c4-judge
2024-05-29T07:06:51Z
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
https://github.com/code-423n4/2024-04-dyad/blob/4a987e536576139793a1c04690336d06c93fca90/src/core/KerosineManager.sol#L44-L52 https://github.com/code-423n4/2024-04-dyad/blob/4a987e536576139793a1c04690336d06c93fca90/src/core/VaultManagerV2.sol#L88
The current implmentation has 2 problems.
VaultManagerV2::vaultsKerosene
VaultManagerV2::vaultsKerosene
The current role of KerosineManager
is to store a list of vaults that will be used for calculating kerosene's usd price.
UnboundedKerosineVault::assetPrice()
calls KerosineManager::getVaults()
to get the total usd value of the vaults and then calculates kerosene's usd value In line 50-69.
However, the problem occurs with how the KerosineManager::isLicensed()
, is used inside of VaultManagerV2::addKerosene()
.
function addKerosene(uint id, address vault) external isDNftOwner(id) { if (vaultsKerosene[id].length() >= MAX_VAULTS_KEROSENE) revert TooManyVaults(); //@review - `isLicensed` returns true for the vaults used for exogenous collateral instead of the actual kerosene vaults. if (!keroseneManager.isLicensed(vault)) revert VaultNotLicensed(); if (!vaultsKerosene[id].add(vault)) revert VaultAlreadyAdded(); emit Added(id, vault); }
Since the isLicensed checks for exogenous vaults, no user will be able to add kerosene as collateral
A malicious actor will be able to double the return of VaultManagerV2::getTotalUsdValue()
by by adding the same vaults he has in vaults
to vaultsKerosene
, which can lead to bad debt, since 1/2 of the usd value returned does not exist.
Coded PoC
PoC of user doubling his collateral Paste this chunk of code inside v2.t.sol and in the terminal run
forge test --fork-url <MAINNET_FORK_URL> --match-test testUserCanDoubleHisCollateralByAddingHisVaultToKeroseneSet
Foundry
Consider adding another storage variable for licensed kerosene vaults and use it within the isLicensed function.
contract KerosineManager is Owned(msg.sender) { ... + EnumerableSet.AddressSet private licensedKeroseneVaults; + function addKerosene(address vault) external onlyOwner { + if (licensedKeroseneVaults.length() >= MAX_VAULTS) + revert TooManyVaults(); + if (!licensedKeroseneVaults.add(vault)) revert VaultAlreadyAdded(); + } + function removeKerosene(address vault) external onlyOwner { + if (!licensedKeroseneVaults.remove(vault)) revert VaultNotFound(); + } ... function isLicensed(address vault) external view returns (bool) { - return vaults.contains(vault); + return licensedKeroseneVaults.contains(vault); } }
Other
#0 - c4-pre-sort
2024-04-29T05:44:10Z
JustDravee marked the issue as duplicate of #966
#1 - c4-pre-sort
2024-04-29T08:37:36Z
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:32Z
koolexcrypto marked the issue as duplicate of #1133
#4 - c4-judge
2024-05-29T07:06:57Z
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
https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L119-L131 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L94-L104 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L106-L116
Users will not be able to remove their vault, because of malicious actor depositing as much as 1 wei before they remove it.
Within the VaultManagerV2::deposit()
, we are only checking if the id of the note sent is valid.
function deposit( uint id, address vault, uint amount ) external isValidDNft(id) { //@review - Anyone can deposit into any note Vault _vault = Vault(vault); _vault.asset().safeTransferFrom(msg.sender, address(vault), amount); _vault.deposit(id, amount); }
A malicious actor can monitor the mempool and wait for a user to execute VaultManagerV2::remove()
or VaultManagerV2::removeKerosene
. The griefer can front-run the transaction and deposit 1 wei into the vault the user wants to remove. Making his transaction revert due to check in VaultManagerV2::remove()
and VaultManagerV2::removeKerosene
function remove(uint id, address vault) external isDNftOwner(id) { @> if (Vault(vault).id2asset(id) > 0) revert VaultHasAssets(); //@review - Since the griefer deposited 1 wei into his vault the function will revert if (!vaults[id].remove(vault)) revert VaultNotAdded(); emit Removed(id, vault); }
function removeKerosene(uint id, address vault) external isDNftOwner(id) { @> if (Vault(vault).id2asset(id) > 0) revert VaultHasAssets(); //@review - Since the griefer deposited 1 wei into his vault the function will revert if (!vaultsKerosene[id].remove(vault)) revert VaultNotAdded(); emit Removed(id, vault); }
Now the user will have to withdraw the remaining 1 wei, before attempting to remove the vault again.
Foundry, Manual review
Consider only allowing the owner of the note to deposit into it.
DoS
#0 - c4-pre-sort
2024-04-27T11:30:31Z
JustDravee marked the issue as duplicate of #1103
#1 - c4-pre-sort
2024-04-27T11:45:43Z
JustDravee marked the issue as duplicate of #489
#2 - c4-pre-sort
2024-04-29T09:31:30Z
JustDravee marked the issue as sufficient quality report
#3 - c4-judge
2024-05-05T20:38:12Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#4 - c4-judge
2024-05-05T21:02:19Z
koolexcrypto marked the issue as nullified
#5 - c4-judge
2024-05-05T21:02:26Z
koolexcrypto marked the issue as not nullified
#6 - c4-judge
2024-05-08T15:29:11Z
koolexcrypto marked the issue as duplicate of #1001
#7 - c4-judge
2024-05-11T19:53:08Z
koolexcrypto marked the issue as satisfactory
#8 - 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
https://github.com/code-423n4/2024-04-dyad/blob/4a987e536576139793a1c04690336d06c93fca90/src/core/VaultManagerV2.sol#L146-L149 https://github.com/code-423n4/2024-04-dyad/blob/4a987e536576139793a1c04690336d06c93fca90/src/core/VaultManagerV2.sol#L195-L198
Users will not be able to withdraw or reedem for Kerosene vaults.
Both functions VaultManagerV2::withdraw()
and VaultManagerV2::redeemDyad()
denominate a value based on the decimals of the vault's oracle.
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(); }
function redeemDyad( uint id, address vault, uint amount, address to ) external isDNftOwner(id) returns (uint) { dyad.burn(id, msg.sender, amount); Vault _vault = Vault(vault); uint asset = (amount * @> (10 ** (_vault.oracle().decimals() + _vault.asset().decimals()))) / _vault.assetPrice() / 1e18; withdraw(id, vault, asset, to); emit RedeemDyad(id, vault, amount, to); return asset; }
However, after the upgrade there are 2 new vaults, Bounded and unbounded kerosene vaults. One of the main differences between the two vaults is that the Kerosene Vault does not implement an oracle to get its value, but is caclulated deterministically depending on the total tvl and the total supply of dyad.
When any of the functions that depend on the vault's oracle are called for any kerosene vault they will inevitably revert.
Coded PoC of Unbounded Kerosene vault reverting when trying to call oracle method Please run the coded poc with:
forge test --fork-url <MAINET_FORK_URL> --match-test testKeroseneVaultsDontHaveOracle -vv
Consider adding some type of storage variable/function to get Kerosene vault's price decimals, then a helper function for the manager to exctract the decimals from the vault.
abstract contract KerosineVault is IVault, Owned(msg.sender) { using SafeTransferLib for ERC20; IVaultManager public immutable vaultManager; ERC20 public immutable asset; KerosineManager public immutable kerosineManager; + uint8 public decimals = 8; ... }
function _vaultPriceDecimals( address vault, uint256 id ) internal view returns (uint256) { return vaults[id].contains(vault) ? Vault(vault).oracle().decimals() : KerosineVault(vault).decimals(); }
function redeemDyad( uint id, address vault, uint amount, address to ) external isDNftOwner(id) returns (uint) { dyad.burn(id, msg.sender, amount); Vault _vault = Vault(vault); +. uint priceDecimals = _vaultPriceDecimals(address(_vault), id); - uint asset = (amount * - (10 ** (_vault.oracle().decimals() + _vault.asset().decimals()))) / - _vault.assetPrice() / - 1e18; + uint asset = (amount * + (10 ** (priceDecimals + _vault.asset().decimals()))) / + _vault.assetPrice() / + 1e18; withdraw(id, vault, asset, to); emit RedeemDyad(id, vault, amount, to); return asset; }
DoS
#0 - c4-pre-sort
2024-04-26T21:28:47Z
JustDravee marked the issue as duplicate of #1048
#1 - c4-pre-sort
2024-04-28T18:38:29Z
JustDravee marked the issue as duplicate of #830
#2 - c4-pre-sort
2024-04-29T08:43:52Z
JustDravee marked the issue as sufficient quality report
#3 - c4-judge
2024-05-11T20:06:07Z
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
Early adopter can manipulate prices in order to liquidate positions that depend on kerosene, and possibly receiving the collateral.
Users who have added any kerosene vault to their note, will not be able to mint/redeem/withdraw until the TVL of the new vaults deployed exceeds the total supply of DYAD.
The team intents to deploy two separate vaults for V2. However since dyad as of the time writting has ~ 632,967 total Supply, but the TVL of the V2 system will initially be 0, causing UnboundedKerosineVault::assetPrice()
to revert due to underflow.
2 issues can occur by having a completely new tvl for the new system.
VaultManagerV2::collatRatio()
always reverting.A malicious actor can take advantage of this and intentionally add a kerosene vault to his vaultsKerosene
to make sure he can never get liquidated, which can account for bad debt.
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(); //@review -> Total supply will initially be far greater than the tvl of the V2 system. uint denominator = kerosineDenominator.denominator(); return (numerator * 1e8) / denominator; }
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(); }
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 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); //@review - If the note passed, has Unbounded or bounded kerosene vaults added this function will revert due to underflow } totalUsdValue += usdValue; } return totalUsdValue; }
Marking the issue as Medium, as the team can act quick and add the old vaults to KerosineManager::vaults
, to pump the tvl over the total supply.
Foundry, Manual Analysis
Consider adding the V1 vaults to KerosineManager::vaults
.
DoS
#0 - c4-pre-sort
2024-04-27T18:23:10Z
JustDravee marked the issue as duplicate of #958
#1 - c4-pre-sort
2024-04-29T08:38:55Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-05T13:48:38Z
koolexcrypto marked the issue as duplicate of #308
#3 - c4-judge
2024-05-11T20:09:25Z
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: 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 can be undercollateralized potentially, which breaks the invariant tvl > totalSupply of dyad, which leads to UnboundedKerosineVault::assetPrice()
always reverting.
Liquidators liquidate users to turn a profit, but it becomes tricky when the value is too low. For example, if an account has only $5.50 worth of collateral and owes 4 DYAD, it's undercollateralized and needs liquidation to keep the protocol safe. However, with such a small value, after considering gas costs, liquidators may not make any profit. Consequently, these low-value accounts might never get liquidated, leaving the protocol with bad debt and risking undercollateralization.
If too many of those small positions never get liquidated users that have added UnboundedKeroseneVault
to their vaultsKerosene
will never be able to mint/redeem/withdraw as UnboundedKerosineVault::assetPrice()
will always revert.
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]); //@note - Seems correct tvl += (vault.asset().balanceOf(address(vault)) * //@audit - balanceOf() can be manipulatable vault.assetPrice() * 1e18) / (10 ** vault.asset().decimals()) / (10 ** vault.oracle().decimals()); } // @review - tvl will be smaller than total supply, if too many small positions do not get liquidated. uint numerator = tvl - dyad.totalSupply(); uint denominator = kerosineDenominator.denominator(); return (numerator * 1e8) / denominator; }
Foundry, Manual review
Consider only allowing users to mint Dyad if their collateral value is past a certain threshold.
DoS
#0 - c4-pre-sort
2024-04-27T17:34:31Z
JustDravee marked the issue as duplicate of #1258
#1 - c4-pre-sort
2024-04-29T09:20:54Z
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:43Z
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:43Z
koolexcrypto marked the issue as satisfactory
#6 - c4-judge
2024-05-28T20:06:05Z
koolexcrypto marked the issue as duplicate of #175