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: 99/183
Findings: 5
Award: $11.66
🌟 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/main/script/deploy/Deploy.V2.s.sol#L95 https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/VaultManagerV2.sol#L75
Each DYAD stablecoin is backed by at least $1.50 of exogenous collateral. This surplus absorbs the collateral’s volatility, keeping DYAD fully backed in all conditions. Here is the keypoint to understand the Kerosene token:
The problem lies in the current implementation, which permits users to mint Dyad tokens solely against Kerosene. This undermines the intended design and the above key points. Consequently:
In the VaultManagerV2 there is two functions to add a vault:
The Deploy.V2.s will license the unboundedKerosineVault:
vaultLicenser.add(address(unboundedKerosineVault));
Consequently, users can include the unboundedKerosineVault along with their regular vaults:
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); }
The call succeeds because the unboundedKerosineVault is licensed during deployment. However, the issue arises as users can not only add it but also mint Dyad solely against Kerosene by merely (a) adding a Kerosene vault (bound or unbounded), (b) depositing some Kerosene to their position, and (c) minting Dyad against Kerosene.
function testUseKerosineAsCollateral() public { uint id = mintDNft(); vaultManager.add(id, address(_unboundedKerosineVault)); deposit(weth, id, address(wethVault), 1e22); vaultManager.mintDyad(id, 100e18, address(this)); address user = _setupKerosine(); uint256 userId = dNft.mintNft(user); vm.startPrank(user); vaultManager.add(userId, address(_unboundedKerosineVault)); kerosine.approve(address(vaultManager), 1000000e18); // 1 million vaultManager.deposit(userId, address(_unboundedKerosineVault), 1000000e18); vaultManager.mintDyad(userId, 100e18, address(this)); vm.stopPrank(); assertEq(dyad.collatRatio(id)); }
VaultManagerTest
forge test --mt testUseKerosineAsCollateral
Manual review
Consider introducing a new KeroseneLicenser specifically for Kerosene vaults, while maintaining the Current Licenser for regular vaults. Then, update the add() & addKerosene() & getKeroseneValue() licensing checks accordingly.
Other
#0 - c4-pre-sort
2024-04-28T18:13:10Z
JustDravee marked the issue as duplicate of #70
#1 - c4-pre-sort
2024-04-29T09:36:38Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-11T19:58:51Z
koolexcrypto marked the issue as satisfactory
#3 - c4-judge
2024-05-12T10:58:33Z
koolexcrypto marked the issue as not a duplicate
#4 - c4-judge
2024-05-12T10:58:50Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#5 - 0xbtk
2024-05-16T21:47:18Z
Hey @koolexcrypto, I believe that this issue is valid, can you please elaborate on why it is marked "unsatisfactory"?
#6 - koolexcrypto
2024-05-24T06:48:14Z
Hi @0xbtk
Thanks for the feedback.
I believe this should be a dup of #872
#7 - c4-judge
2024-05-28T14:48:11Z
koolexcrypto removed the grade
#8 - c4-judge
2024-05-28T14:48:20Z
koolexcrypto marked the issue as satisfactory
#9 - c4-judge
2024-05-28T14:48:32Z
koolexcrypto marked the issue as duplicate of #1133
🌟 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#L119-L131
Leaving the deposit function accessible to anyone introduces two issues:
Below is the implementation of the deposit() function:
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); }
As long as the id is a valid NFT, the deposit will succeed. An attacker can exploit this design to grief users.
The withdraw function prevents deposits and withdrawals in the same block to protect against flash loan attacks:
function withdraw(...) public { if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock(); // code }
The issue is that this flash loan check can be used against users to prevent them from withdrawing or redeeming their collateral. Let's consider this scenario:
if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock();
because her idToBlockOfLastDeposit[AliceId] == block.number
.
Kerosene whales are incentivized to launch this attack because the kerosene is as valuable as the degree of DYAD’s overcollateralization. Therefore, locking users' funds will prevent the kerosene price from dropping.
Below is the implementation of the remove() function:
function remove(...) external isDNftOwner(id) { if (Vault(vault).id2asset(id) > 0) revert VaultHasAssets(); // code }
There is a sanity check to prevent users from removing a vault that still contains assets. The issue is that any user can deposit assets on behalf of another user. An attacker can exploit this vulnerability by:
function testGriefUsers() public { uint id = mintDNft(); vaultManager.add(id, address(wethVault)); address attacker = makeAddr("Attacker"); weth.mint(attacker, 1 wei); vm.startPrank(attacker); weth.approve(address(vaultManager), 1 wei); vaultManager.deposit(id, address(wethVault), 1 wei); vm.stopPrank(); vm.expectRevert(); // VaultHasAssets vaultManager.remove(id, address(wethVault)); }
Manual reveiw
To fix these two issues, consider allowing only DNft owners or their approved operators to call the deposit function:
function deposit( uint id, address vault, uint amount ) external isValidDNft(id) { address idOwner = dNft.ownerOf(id); require(msg.sender == idOwner || dNft.isApprovedForAll(idOwner, msg.sender)); idToBlockOfLastDeposit[id] = block.number; Vault _vault = Vault(vault); _vault.asset().safeTransferFrom(msg.sender, address(vault), amount); _vault.deposit(id, amount); }
Other
#0 - JustDravee
2024-04-27T11:44:39Z
@0xbtk, you even said yourself that this here is 2 issues 🥲 Why submit as 1? These 2 root causes were submitted as different everywhere (1. the state update, 2. the leftover balance) But indeed, adding permissions fixes both. I'll try to make this issue as primary of both the others (which then will need to either have a 50% partial or the full amount depending on the impact they mentioned)
#1 - c4-pre-sort
2024-04-27T11:44:47Z
JustDravee marked the issue as primary issue
#2 - c4-pre-sort
2024-04-27T11:44:52Z
JustDravee marked the issue as high quality report
#3 - shafu0x
2024-04-30T11:39:10Z
Good find! We should restrict it to only owner.
#4 - c4-judge
2024-05-05T13:45:12Z
koolexcrypto marked the issue as satisfactory
#5 - c4-judge
2024-05-05T20:38:16Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#6 - c4-judge
2024-05-05T20:39:25Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#7 - c4-judge
2024-05-05T21:03:55Z
koolexcrypto marked the issue as satisfactory
#8 - c4-judge
2024-05-08T15:26:12Z
koolexcrypto marked the issue as nullified
#9 - c4-judge
2024-05-08T15:26:17Z
koolexcrypto marked the issue as not nullified
#10 - c4-judge
2024-05-08T15:30:11Z
koolexcrypto marked the issue as duplicate of #1001
#11 - c4-judge
2024-05-11T19:50:12Z
koolexcrypto marked the issue as satisfactory
#12 - c4-judge
2024-05-13T18:34:30Z
koolexcrypto changed the severity to 3 (High Risk)
#13 - 0xbtk
2024-05-16T21:51:40Z
Hey @koolexcrypto, this issue contains both #118 and #1001 with a detailed explanation. Could you please divide this into two issues?
#14 - koolexcrypto
2024-05-24T06:49:59Z
Hi @0xbtk
Thank you for your feedback.
Sure, this should be divided.
🌟 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
Users Kerosing will get permanently locked in the UnboundedKerosineVault.
Currently, there is only one exit point for users which is the withdraw 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(); _vault.withdraw(id, to, amount); if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); }
withdraw() will get the value of the amount as follows:
uint value = amount * _vault.assetPrice() * 1e18 / 10**_vault.oracle().decimals() / 10**_vault.asset().decimals();
The issue is that this will not work with UnboundedKerosineVault and the call will simply revert because the vault it have no oracle and the code will attampt to call a non-existent variable, see here. Thus, all funds deposited in UnboundedKerosineVault will get permanently locked.
Manual review
Consider adding a withdrawKerosine() function that does not call a non-existent variable.
Other
#0 - c4-pre-sort
2024-04-26T21:19:49Z
JustDravee marked the issue as duplicate of #1048
#1 - c4-pre-sort
2024-04-28T18:38:36Z
JustDravee marked the issue as duplicate of #830
#2 - c4-pre-sort
2024-04-29T08:44:30Z
JustDravee marked the issue as sufficient quality report
#3 - c4-judge
2024-05-11T20:04:33Z
koolexcrypto marked the issue as satisfactory
#4 - c4-judge
2024-05-13T18:39:30Z
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
https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/Vault.kerosine.unbounded.sol#L65
During lunchtime, the total supply of dyad tokens will exceed the total value locked (TVL) in all vaults. Consequently, an underflow will occur, leading to a temporary locking of funds for users of UnboundedKerosineVault.
The function assetPrice() in UnboundedKerosineVault calculates the price of Kerosene as follows:
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()); } // @audit-issue underflow uint numerator = tvl - dyad.totalSupply(); uint denominator = kerosineDenominator.denominator(); return numerator * 1e8 / denominator; }
The issue arises from the calculation of the numerator, where the total supply of dyad tokens is subtracted from the TVL. Currently, dyad stablecoin is already deployed with 625967.4e18 tokens minted source.
During lunchtime, users depositing Kerosene will be unable to withdraw it, triggering a DoS in the withdraw() function as it calls vault.assetPrice():
uint value = amount * _vault.assetPrice() * 1e18 / 10**_vault.oracle().decimals() / 10**_vault.asset().decimals();
which will revert due to an underflow here:
// = 0 - 625967400000000000000000 uint numerator = tvl - dyad.totalSupply();
Manual review
There isn't a direct fix for this issue since dyad is already deployed. However, to prevent users' funds from getting locked, consider temporarily disabling the UnboundedKerosineVault. You can re-enable it once the TVL exceeds the total supply of DYAD tokens. Also, consider updating the assetPrice() function to:
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()); } if (tvl <= dyad.totalSupply()) return 0; uint numerator = tvl - dyad.totalSupply(); uint denominator = kerosineDenominator.denominator(); return numerator * 1e8 / denominator; }
Under/Overflow
#0 - c4-pre-sort
2024-04-27T18:30:28Z
JustDravee marked the issue as duplicate of #958
#1 - c4-pre-sort
2024-04-29T08:39:28Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-05T13:48:44Z
koolexcrypto marked the issue as duplicate of #308
#3 - c4-judge
2024-05-11T20:09:43Z
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#L88 https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/Vault.kerosine.unbounded.sol#L56
The current deployment script overlooks licensing the unboundedKerosineVault in the keroseneManager, preventing users from executing the addKerosene() function.
This omission poses a significant issue: if the owner licenses the unboundedKerosineVault after deployment, it introduce a critical problem. This action leads to an incorrect inflation of the Kerosene price, as the Kerosene vault's total value locked (TVL) is erroneously added to all exogenous collateral in the protocol.
Presently, there exist two licensors:
Users employ the addKerosene() function to add a Kerosene vault:
function addKerosene(...) external { if (!keroseneManager.isLicensed(vault)) revert VaultNotLicensed(); if (!vaultsKerosene[id].add(vault)) revert VaultAlreadyAdded(); }
Each Kerosene vault necessitates licensing in the keroseneManager. However, the deployment script fails to license the unboundedKerosineVault, necessitating manual intervention by the owner:
function add( address vault ) external onlyOwner { if (vaults.length() >= MAX_VAULTS) revert TooManyVaults(); if (!vaults.add(vault)) revert VaultAlreadyAdded(); }
Yet, licensing the unboundedKerosineVault in the keroseneManager leads to a critical flaw. Kerosene's calculation involves:
Here's how it's implemented in the unboundedKeroseneVault:
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; }
Initially, it retrieves licensed vaults from the kerosineManager, which should exclusively include exogenous collateral. However, as per Deploy.V2.s.sol, only ethVault and wstEth vaults are licensed:
kerosineManager.add(address(ethVault)); kerosineManager.add(address(wstEth));
However, as explained above, the owner must license the unboundedKerosineVault in the kerosineManager for users to call addKerosene().
Consequently, the owner must manually license the unboundedKerosineVault in the kerosineManager for addKerosene() to function properly. As a result, assetPrice() incorporates the unboundedKerosineVault's USD value into the total USD value of exogenous collateral, inflating the Kerosene token price allowing users to abuse this to mint more dyad.
Manual review
To address this issue, consider utilizing a single licenser for all vaults (This approach ensures KerosineManager alone calculates the Kerosene price):
// SPDX-License-Identifier: MIT pragma solidity =0.8.17; import {Owned} from "@solmate/src/auth/Owned.sol"; contract Licenser is Owned(msg.sender) { mapping (address => bool) public isLicensed; mapping (address => bool) public isKeroseneLicensed; constructor() {} function add (address vault) external onlyOwner { isLicensed[vault] = true; } function remove (address vault) external onlyOwner { isLicensed[vault] = false; } function addKerosene (address vault) external onlyOwner { isKeroseneLicensed[vault] = true; } function removeKerosene (address vault) external onlyOwner { isKeroseneLicensed[vault] = false; } }
(A) You can then remove the keroseneManager from the VaultManagerV2, as it becomes useless. (B) Update the addKerosene() and getKeroseneValue() functions to call Licenser.isKeroseneLicensed directly.
Other
#0 - c4-pre-sort
2024-04-28T18:13:55Z
JustDravee marked the issue as duplicate of #966
#1 - c4-pre-sort
2024-04-29T08:37:57Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-04T09:46:26Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#3 - 0xbtk
2024-05-16T21:41:36Z
@koolexcrypto, I believe this is a valid issue. Could you please take another look?
if (!keroseneManager.isLicensed(vault)) revert VaultNotLicensed();
#4 - koolexcrypto
2024-05-24T06:44:04Z
Hi @0xbtk
Thanks for the input.
kerosene vault shouldn’t be added to keroseneManager. The issue is in this condition if (!keroseneManager.isLicensed(vault))
.
Since this already stated in the issue, this should be a dup of #70
#5 - c4-judge
2024-05-24T06:45:08Z
koolexcrypto removed the grade
#6 - c4-judge
2024-05-24T06:45:13Z
koolexcrypto marked the issue as not a duplicate
#7 - c4-judge
2024-05-24T06:45:25Z
koolexcrypto marked the issue as duplicate of #70
#8 - c4-judge
2024-05-29T11:22:55Z
koolexcrypto marked the issue as satisfactory
#9 - c4-judge
2024-05-29T11:25:43Z
koolexcrypto changed the severity to 2 (Med Risk)