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: 82/183
Findings: 3
Award: $37.30
🌟 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
By providing empty deposits to ids of other users, legit users of the protocol can get their withdrawal and redeem in the VaultManagerV2 reverted by a malicious user. Apart from the grief this would cause to users and the protocol, users may also be external protocols or smart contract systems which rely on smooth functionality of the dyad system. Note that the malicious user can DOS withdrawal and redeem for every single user available and for every block. The user only has to worry about gas fees. This would be especially easier in the early adoption of the protocol with relatively less users.
Due to lack of access control on the VaultManager.deposit
function, users can make deposits into any DyadNFT for any vault. This seems to be a system design that allows users to make payments for other users.
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); }
Additionally, the block.number at that deposit is set to idToBlockOfLastDeposit[id]
for that id. This is to prevent users from withdrawing within the same block as their last deposit, also this check is indirectly enforced during redeem as withdrawal happens during the redeem process.
function withdraw( uint id, address vault, uint amount, address to ) public isDNftOwner(id) { @> if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock
function redeemDyad( uint id, address vault, uint amount, address to ) external isDNftOwner(id) returns (uint) { ............... withdraw(id, vault, asset, to);
This is where the malicious user can grief legit users The malicious user can frontrun any withdrawal or redeem in the mempool with a 0 amount deposit into the id of the user, causing the lastdeposit for that id to be reset to that block.number. This would subsequently cause the withdrawal or redeem for the unsuspecting user to revert due to the check. The malicious user can do this for the next block and every single block as long as they intend. Apart from griefing,the bad actor may have other legitimate reasons for doing this and create more attack paths. For example, if the legit user is an external protocol, the bad actor can prevent their withdrawal in order to build an attack on that protocol.
Manual review
function deposit( uint id, address vault, uint amount ) external isValidDNft(id) { + require(amount > 0 /** || amount > minDeposit*/); idToBlockOfLastDeposit[id] = block.number; Vault _vault = Vault(vault); _vault.asset().safeTransferFrom(msg.sender, address(vault), amount); _vault.deposit(id, amount); }
DoS
#0 - c4-pre-sort
2024-04-27T11:30:23Z
JustDravee marked the issue as duplicate of #1103
#1 - c4-pre-sort
2024-04-27T11:45:42Z
JustDravee marked the issue as duplicate of #489
#2 - c4-pre-sort
2024-04-29T09:28:32Z
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:30:15Z
koolexcrypto marked the issue as nullified
#5 - c4-judge
2024-05-05T21:30:19Z
koolexcrypto marked the issue as not nullified
#6 - c4-judge
2024-05-08T15:29:10Z
koolexcrypto marked the issue as duplicate of #1001
#7 - c4-judge
2024-05-11T19:50:56Z
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: 0xAlix2
Also found by: 0xfox, 0xlemon, 0xnev, 3docSec, Aamir, Abdessamed, Dudex_2004, Egis_Security, Evo, FastChecker, Honour, Jorgect, KupiaSec, Limbooo, MrPotatoMagic, SpicyMeatball, TheSchnilch, alix40, bhilare_, favelanky, forgebyola, ke1caM, kennedy1030, koo, oakcobalt, petro_1912, shikhar229169
32.4128 USDC - $32.41
Users of the VaultManagerV2 can add both non kerosene Vaults and kerosene Vaults to their DNfts and make deposits into these vaults. Kerosene vaults may be unbounded where users can freely deposit and withdraw kerosene, while deposits in bounded kerosene Vaults cannot be withdrawn. When users make deposits into unbounded Kerosene Vaults they may not be able to withdraw their assets from these vaults. This may cause loss of funds for users of the system.
https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L150 Users can add make deposits into any vault added to their dnft. Consider a scenario where a user has added an unbounded non-kerosene vault to their id and makes a deposit of 100e18 kerosene to that vault. Now the user wants to withdraw let's say 80e18 assets from that vault. Note that the only way for users to withdraw is through the VaultManagerV2 contract. When the user makes the attempt, since the user does not have a non kerosene vault attached to their id or even if they have, let us assume they do not have any assets in their non-kerosene vaults. Due to the check on L150, the amount to be withdrawn which is 80e18 is substracted from the non kerosene value of the user and reverts if it is less than the dyadMinted for that user. Since the user does not have any value in their non-kerosene vault, their transaction reverts.
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(); }
Essentially, users are expected to have assets in their non-kerosene vaults which is more than the assets in their unbounded kerosene vault in order to withdraw their unbounded kerosene assets. This breaks core protocol functionality as users should be able to withdraw their unbounded kerosene vault asset at any time. Especially considering that users cannot mint Dyad based on their kerosene vault assets, this check should not apply to users when withdrawing from unbounded kerosene vaults
Manual review
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(); + if(_vault.isNonKeroseneVault) { if (getNonKeroseneValue(id) - value < dyadMinted) revert NotEnoughExoCollat(); } _vault.withdraw(id, to, amount); if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); }
Invalid Validation
#0 - c4-pre-sort
2024-04-26T21:43:18Z
JustDravee marked the issue as duplicate of #397
#1 - c4-pre-sort
2024-04-29T08:48:13Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-11T19:22:49Z
koolexcrypto marked the issue as satisfactory
🌟 Selected for report: carrotsmuggler
Also found by: 0xAlix2, 0xSecuri, 0xblack_bird, 0xnev, AM, Al-Qa-qa, AlexCzm, Dudex_2004, Egis_Security, GalloDaSballo, Infect3d, Jorgect, KupiaSec, Ryonen, SpicyMeatball, T1MOH, VAD37, adam-idarrha, amaron, cu5t0mpeo, d3e4, darksnow, forgebyola, foxb868, itsabinashb, jesjupyter, nnez, peanuts, pontifex, wangxx2026, windhustler, zhuying
4.8719 USDC - $4.87
Unsuspecting users of the protocol who rely on deposited assets in the kerosene vaults to avoid liquidation can get maliciously liquidated by a user with significant portions of the deposited assets in the kerosene vaults. Essentially, users of the protocol would lose funds in this scenario.
The asset price of kerosene is determined by the tvl of kerosene assets in kerosene vaults (total value deposited into kerosene vaults) and dyad totalsupply which is controlled by minting or burning dyad tokens.
assetprice of kerosene = tvl - dyadTotalSupply/denominator
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;
The denominator is determined by the kerosene totalSupply (1billion*WAD) and the value owned by the mainnet owner. For most scenarios this value can be expected to be constant unless the mainnet owner makes large moves. Therefore, this asset price is mostly determined by how much is deposited in kerosene vaults and how much dyad is in circulation. There are 2 ways to drop the assetprice
To increase the assetprice, the opposite can be done.
Now why would a user who has enough value to shift asset price make such moves. This is because users rely on this asset price to maintain their collatRatio against liquidation.
function liquidate( uint id, uint to ) external isValidDNft(id) isValidDNft(to) { uint cr = collatRatio(id); @> if (cr >= MIN_COLLATERIZATION_RATIO) revert CrTooHigh(); .................................
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 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; }
Users are incentivized to deposit assets into kerosene vaults especially bounded kerosene vaults (since this doubles their assets) to hedge against liquidation. If the total value of that user goes below a certain range (collatRatio), they would get liquidated. If users are liquidated by another user, this user gets to burn dyad tokens for legit deposited assets of the liquidatee. Therefore, this malicious user who controls 40% of the total assets in kerosene vaults (for example), can maliciously withdraw all their assets in one transaction, causing the asset price of kerosene to tank. Since the total usd value of users who have deposited and minted dyad, and rely on their kerosene assets to maintain their collatRatio is now reduced. This whale can then liquidate unfairly all users whose positions are now liquidatable.
After mass liquidations such as this, the user now has even more assets and can deposit again to raise the price up even more, making massive profits
Manual review
Oracle
#0 - c4-pre-sort
2024-04-28T06:10:23Z
JustDravee marked the issue as duplicate of #67
#1 - c4-pre-sort
2024-04-29T09:17:52Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-05T09:59:11Z
koolexcrypto changed the severity to 2 (Med Risk)
#3 - c4-judge
2024-05-08T11:50:03Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#4 - c4-judge
2024-05-08T12:05:10Z
koolexcrypto marked the issue as satisfactory