DYAD - 0xabhay's results

The first capital efficient overcollateralized stablecoin.

General Information

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

DYAD

Findings Distribution

Researcher Performance

Rank: 64/183

Findings: 4

Award: $123.13

🌟 Selected for report: 0

πŸš€ Solo Findings: 0

Lines of code

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L156-L169 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/Vault.kerosine.unbounded.sol#L50 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/Vault.kerosine.sol#L60

Vulnerability details

Impact

The protocol's vulnerability allows for the minting of dyad tokens using only kerosene as collateral, which contravenes the intended design that prohibits using kerosene alone for this purpose. Additionally, this flaw enables users to bypass the critical 150% collateralization ratio requirement, threatening the protocol's financial stability. Exploitation of this bug could lead to the issuance of dyad tokens that are not sufficiently backed by collateral, potentially causing a devaluation of the dyad currency and jeopardizing the protocol's solvency.

<details> <summary>Proof of Concept</summary>

The deploymentscript adds the unboundedKerosineVault to the vaultLicenser, which could be problematic because the intention is that users should not be able to mint dyad solely with kerosene as collateral

a user adds the unboundedKerosineVault using VaultManagerV2::add and inside there is check

 function add(
      uint    id,
      address vault
  ) 
    external
      isDNftOwner(id)
  {
  ///......
    // @audit this check will passed for the unbounded vault because of the deployment script
    if (!vaultLicenser.isLicensed(vault))  revert VaultNotLicensed();
    //...
  }

and the check is passed now the user deposits 1 million kerosene using the VaultManagerV2::deposit function. As of now when i am writing this report the cost of 1 KEROSENE = 0.03456 USDC ($0.0317) 1m kerosene = 34560.3 usdc. also this can be huge if the user is taking a flash loan to exploit.

User calls mintDyad with 36500 dyad as the amount and with their address

now inside the mintdyad function it calls the getNonKeroseneValue()

 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;
  }

since there is only one vault which is the unboundedkerosine vault so it will call the getUsdValue inside this function it will call the Vault.kerosine.unbounded::assetprice():

  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();
      // 1000000000e18 - 950841953.47e18 = 49158046.53e18
      uint denominator = kerosineDenominator.denominator();
      return numerator * 1e8 / denominator;
    }
}  

KerosineManager has only two vault, weth & wstETH. so we will only calculating the assetprice() of weth ans wstETH now lets take an example , suppose

tvl of weth = 500 weth, tvl of wstETH = 500 stEth dyad totalsupply (as of the time of writing this report) = 632967400000000000000000 (in 18 decimal)

return value of assetPrice() for Weth = 318542608000

return value of assetPrice() for wstETH = 368088673998

now calculating the tvl using the Tvl formula

tvlforWeth=500Γ—1018Γ—318542608000Γ—1018108Γ—1018=1592713.04e18\text{tvlforWeth} = \frac{500 \times 10^{18} \times 318542608000 \times 10^{18}}{10^8 \times 10^{18}} = {1592713.04e18}
tvlforWstETH=500Γ—1018Γ—368088673998Γ—1018108Γ—1018=1840443.36999e18\text{tvlforWstETH} = \frac{500 \times 10^{18} \times 368088673998 \times 10^{18}}{10^8 \times 10^{18}} = 1840443.36999e18
totalTVL=1592713.04e18+1840443.36999e18=3433156.40999e18\text{totalTVL} = 1592713.04e18 + 1840443.36999e18 = 3433156.40999e18

numetor = 3433156409990000000000000 - 632967400000000000000000 = 2800189009990000000000000

denominator = 1000000000e18 - 950841953.47e18 = 49158046.53e18

return = 2800189009990000000000000 * 1e8 / 49158046.53e18 = 5696298 (return value of assetPrice() inside unbounded kerosine vault contract)

now, calculating the getUsdValue for Unbounded Vault,

1000000e18βˆ—5696298/1e18=56962.98e18.1000000e18 * 5696298 / 1e18 = 56962.98e18.

since there is a only one vault added by the user the getNonKeroseneValue will only run for one vault,so the totalUsdValue = 56962.98e18.

now back to mintdyad the check will pass because the newDyadminted is 0 + 37000e18 (attacker is minting for the first time). and the getNonKeroseneValue(id) is 56962.98e18

now the user minted the 36500 dyad,

  dyad.mint(id, to, amount);
  if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); 
    emit MintDyad(id, amount, to);

after minting dyad it will call, the collatRatio(id) inside this function it will return the getTotalUsdValue because the user _dyad is non zero it is 37000e18

now inside the getTotalUsdValue it return

  function getTotalUsdValue(
    uint id
  ) 
    public 
    view
    returns (uint) {
      return getNonKeroseneValue(id) + getKeroseneValue(id);
  }

so, the getnonkerosene is 56962.98e18 and getKeroseneValue is zero becuase user didnt add any exogenous collateral.

so the return value is 56962.98e18 returned to collatration for the ratio calculation = 56962.98e18.divwadDown(36500e18) = 1.5606e18 (18 decimal)

now the check in mintDyad :This check will pass, allowing the user to successfully mint dyad. Since the collatRatio returns 1.5606 is greater than the MIN_COLLATERIZATION_RATIO, the check will pass, enabling the user to mint dyad using only kerosene tokens.

  dyad.mint(id, to, amount);
  if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); 
    emit MintDyad(id, amount, to);

Due to the addition of the unboundedKerosineVault to the vaultLicenser, an attacker can mint dyad tokens using only kerosene as collateral.

This attack is profitable as 1 million kerosene costs $34,560.3, and the user minted 36,500 dyad, with dyad being pegged 1:1 to stablecoins such as USDC, USDT, or DAI. Therefore, the profit from the attack would be $36,500 - $34,560.3 = $1,939.7 in stablecoin. The profitability could be significantly amplified if the exploiter utilizes a flash loan to scale the attack.

</details>

Tools Used

Manual Review

Modify the VaultManagerV2::add function to include additional checks that prevent unbounded vaults from being used as collateral sources for minting dyad.

Assessed type

Context

#0 - c4-pre-sort

2024-04-28T19:57:05Z

JustDravee marked the issue as duplicate of #70

#1 - c4-pre-sort

2024-04-29T09:37:43Z

JustDravee marked the issue as sufficient quality report

#2 - c4-judge

2024-05-11T20:00:24Z

koolexcrypto marked the issue as satisfactory

#3 - c4-judge

2024-05-12T11:07:54Z

koolexcrypto marked the issue as not a duplicate

#4 - c4-judge

2024-05-12T11:07:58Z

koolexcrypto marked the issue as unsatisfactory: Invalid

#5 - 0xAbhay

2024-05-16T04:07:34Z

@koolexcrypto The identified vulnerability allows an attacker to exploit the minting mechanism of the protocol by bypassing the intended collateralization constraints. This is due to an error in the deployment script, which incorrectly licenses the unboundedKerosineVault, enabling users to use this vault as valid collateral despite it not being intended as such. An attacker can then add this vault, deposit kerosene, and mint dyad tokens solely with kerosene tokens. The flawed script allows the vault to pass the collateral check, leading to the successful minting of dyad tokens with insufficient collateral, undermining the financial stability of the protocol. This exploit can be further amplified using flash loans, significantly increasing the attacker's profit.

#6 - 0xAbhay

2024-05-17T04:01:55Z

@koolexcrypto The main invariant is that the user cannot mint dyad tokens using only kerosene. If they are trying to mint using solely kerosene, then there is a problem. #679 #1098 and #872 has similar issue please relook in to it

Thank you πŸ™

#7 - koolexcrypto

2024-05-22T10:16:39Z

Hi @0xAbhay

Thank you for your feedback.

The issue seems to be in the condition if (vaultLicenser.isLicensed(address(vault))) { in getNonKeroseneValue function, as it should ensure that only non-kerosene vaults only are used. Probably using keroseneManager.isLicensed instead.

Could you please provide a PoC (coded) that demonstrates the issue above?

#8 - shafu0x

2024-05-22T13:35:37Z

Hi @0xAbhay

Thank you for your feedback.

The issue seems to be in the condition if (vaultLicenser.isLicensed(address(vault))) { in getNonKeroseneValue function, as it should ensure that only non-kerosene vaults only are used. Probably using keroseneManager.isLicensed instead.

Could you please provide a PoC (coded) that demonstrates the issue above?

exactly. this is the problem here.

#9 - 0xAbhay

2024-05-22T16:51:36Z

@koolexcrypto

Sure, let's break down the finding and the potential exploit step-by-step, focusing on how the minting of dyad tokens using only kerosene as collateral happens and why it is problematic for the protocol. We'll use the provided example to illustrate this process.

Step-by-Step Explanation

<details>
  1. Deployment Script Issue:

    • The deployment script incorrectly adds the unboundedKerosineVault to the vaultLicenser. This setup was not intended because the protocol design prohibits using kerosene alone as collateral for minting dyad tokens.
  2. Adding the Vault:

    • A user adds the unboundedKerosineVault using the VaultManagerV2::add function.
    • Inside this function, a check ensures the vault is licensed:
      if (!vaultLicenser.isLicensed(vault)) revert VaultNotLicensed();
    • Because of the deployment script error, this check passes for the unboundedKerosineVault.
  3. Depositing Kerosene:

    • The user deposits 1 million kerosene tokens into the unboundedKerosineVault using the VaultManagerV2::deposit function.
    • The value of 1 million kerosene is calculated as: [ 1,000,000 \times 0.03456 \text{ USDC} = 34,560.3 \text{ USDC} ]
  4. Minting Dyad Tokens:

    • The user calls the mintDyad function to mint 36,500 dyad tokens to their address.
    • Inside the mintDyad function, the getNonKeroseneValue() function is called to calculate the collateral value excluding kerosene:
      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;
      }
  5. Calculating the Asset Price:

    • The getUsdValue function for the unboundedKerosineVault calculates the total value of the assets. This involves a detailed calculation using the assetPrice() function:
      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;
      }
  6. Example Calculation: Total Value Locked (TVL) in WETH and wstETH vaults is calculated:

TVLΒ forΒ WETH=500Γ—1018Γ—318,542,608,000Γ—1018108Γ—1018=1,592,713.04Β USDC\text{TVL for WETH} = \frac{500 \times 10^{18} \times 318,542,608,000 \times 10^{18}}{10^8 \times 10^{18}} = 1,592,713.04 \text{ USDC}
TVLΒ forΒ wstETH=500Γ—1018Γ—368,088,673,998Γ—1018108Γ—1018=1,840,443.36999Β USDC\text{TVL for wstETH} = \frac{500 \times 10^{18} \times 368,088,673,998 \times 10^{18}}{10^8 \times 10^{18}} = 1,840,443.36999 \text{ USDC}

Total TVL:

totalTVL=1,592,713.04+1,840,443.36999=3,433,156.40999Β USDC\text{totalTVL} = 1,592,713.04 + 1,840,443.36999 = 3,433,156.40999 \text{ USDC}

Adjusted numerator and denominator for asset price calculation:

numerator=3,433,156.40999Β USDCβˆ’632,967.4Β USDC=2,800,189.00999Β USDC\text{numerator} = 3,433,156.40999 \text{ USDC} - 632,967.4 \text{ USDC} = 2,800,189.00999 \text{ USDC}
denominator=1,000,000,000βˆ’950,841,953.47=49,158,046.53\text{denominator} = 1,000,000,000 - 950,841,953.47 = 49,158,046.53

Asset price calculation:

assetPrice=2,800,189.00999Γ—10849,158,046.53=56,962.98\text{assetPrice} = \frac{2,800,189.00999 \times 10^8}{49,158,046.53} = 56,962.98
  1. USD Value Calculation for Unbounded Vault:

    • Using the calculated assetPrice:
      getUsdValueΒ forΒ UnboundedΒ Vault=1,000,000Γ—56,962.98/1e18=56,962.98Β USDC\text{getUsdValue for Unbounded Vault} = 1,000,000 \times 56,962.98 / 1e18 = 56,962.98 \text{ USDC}
  2. Collateral Ratio Check:

    • The mintDyad function checks the collateralization ratio after minting:
      collatRatio=56,962.9836,500=1.5606>1.50Β (minimumΒ ratio)\text{collatRatio} = \frac{56,962.98}{36,500} = 1.5606 > 1.50 \text{ (minimum ratio)}
    • This check passes, allowing the user to mint the dyad tokens.

Profit Calculation

  • The user effectively minted 36,500 dyad tokens, pegged 1:1 to stablecoins like USDC.
  • The cost of 1 million kerosene is $34,560.3.
  • The profit from the attack:
    36,500Β USDCβˆ’34,560.3Β USDC=1,939.7Β USDC36,500 \text{ USDC} - 34,560.3 \text{ USDC} = 1,939.7 \text{ USDC}

Exploit Amplification

  • The profitability can be significantly increased if the attacker uses a flash loan to deposit larger amounts of kerosene, thereby scaling the attack.

Summary

This vulnerability arises due to the improper licensing of the unboundedKerosineVault, allowing users to use kerosene alone as collateral. This bypasses the intended 150% collateralization ratio requirement, enabling the minting of undervalued dyad tokens and threatening the protocol's financial stability.

</detail>

This is not a duplicate #1029. and for coded poc -> please refers to #679

#10 - 0xAbhay

2024-05-22T16:54:46Z

@koolexcrypto Also, why would a kerosene vault be added using the add function, which is used to add exogenous collateral? This finding shows how, due to a development script error, we can add unbounded kerosene using the add function and mint the DYAD using only kerosene thank you for all the hard work you have put in i appreciate your work and #832 is a similar finding and also has a good poc please check.

#11 - c4-judge

2024-05-28T14:47:44Z

koolexcrypto removed the grade

#12 - c4-judge

2024-05-28T14:47:55Z

koolexcrypto marked the issue as duplicate of #1133

#13 - c4-judge

2024-05-28T14:47:59Z

koolexcrypto marked the issue as satisfactory

Lines of code

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/script/deploy/Deploy.V2.s.sol#L64-L65 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/script/deploy/Deploy.V2.s.sol#L93-L94 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L67 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L80 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L156-L169

Vulnerability details

Impact

The duplication bug that allows the same exogenous vault to be counted in both getNonKeroseneValue and getKeroseneValue could lead to a significantly inflated collateral ratio, enabling users to mint more DYAD than the actual collateral should permit. potentially resulting in under-collateralization and jeopardizing user funds.

Proof of Concept

If you look at the deployment script, the team has added the exogenous vault to the Kerosene Manager and also into the Vault License. Thus, the user can add the same exogenous vault using add and addKerosene.

<details> <summary>Case 1: Using inflated collateral to mint Dyad</summary>

as per docs provide $1500 worth of wETH or wstETH, mint $1000 worth of Dyad stablecoins.

here how the user can mint more than 150% of their deposit collateral

Deposit: User deposits 10 WETH into a vault, with WETH priced at $3000 each, totaling $30000 worth of WETH.

Mint DYAD: User attempts to mint 29998 DYAD, which is just under the USD value of the deposited WETH.

The mintDyad function calls getNonKeroseneValue to ensure the vault's USD value is sufficient to cover the new DYAD minting.

  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;
        // @audit-> this check will pass for exogenous vaults becuase of the deployment script 
        if (vaultLicenser.isLicensed(address(vault))) {
          usdValue = vault.getUsdValue(id);        
        }
        totalUsdValue += usdValue;
      }
      return totalUsdValue;
  }

Calculate USD Value: getUsdValue calculates the USD value of the WETH in the vault:

For instance, if the asset price of WETH in terms of USD is $3000, the assetPrice function would return 3000e8, because the ETH/USD price is conventionally expressed with 8 decimal places.

10Γ—1018Γ—3000Γ—108Γ—1Γ—1018108Γ—1018=30000Γ—1018\frac{10 \times 10^{18} \times 3000 \times 10^{8} \times 1 \times 10^{18}}{10^{8} \times 10^{18}} = 30000 \times 10^{18}

Verify Collateralization: The contract checks whether the vault's USD value exceeds the amount of DYAD requested for minting. This check passes because getKeroseneValue returns 30000e18, which is greater than the newDyadMinted amount of 29998e18.

Minting Allowed: Since the vault value is sufficient, DYAD is minted.

Post-minting, the collatRatio is assessed: getTotalUsdValue aggregates the values from getNonKeroseneValue and getKeroseneValue. If the same WETH vault is referenced in both cases due to the user adding the same vault to both kerosene and non-kerosene sets, the total value would be 60000e18. This duplication could lead to an inflated collateral ratio calculation.

Calculate Collateral Ratio: The collatRatio function divides the total USD value by the minted DYAD amount:

60000e18 / 29998e18 β‰ˆ 2.00013334e18

Collateral Ratio Check: The minted DYAD amount is checked against the minimum collateralization ratio (MIN_COLLATERIZATION_RATIO = 1.5e18). Since 2.00013334e18 > 1.5e18, the minting process is successful.

The user successfully mints 29998 DYAD with a collateral ratio above the required minimum, effectively allowing nearly 1:1 minting of DYAD to the USD value of the collateral.

This process relies on the assumption that both getNonKeroseneValue and getKeroseneValue reference the same Vault, which may not be the intended design. The system is designed to prevent under-collateralization, but if the same Vault is counted twice, it could lead to overestimation of collateral and potential system risk.

</details> <details> <summary>Case 2: Excessive Withdrawal</summary>

A user deposits 10 WETH into a vault. The current price of WETH is $3000, so the total collateral value is $30,000. The user mints DYAD worth $20,000, expecting to maintain a collateral ratio above the minimum of 150%.

Due to the bug, the same vault is counted in both getNonKeroseneValue and getKeroseneValue, doubling the perceived collateral value to $60,000. The user now sees a collateral ratio of 300% ($60,000 / $20,000) instead of the actual 150% ($30,000 / $20,000).

The user decides to withdraw assets based on the inflated collateral ratio.

They request a withdrawal equivalent to $10,000, expecting the system to maintain the minimum collateral ratio after the withdrawal.

However, because the actual collateral is only $30,000, after withdrawing $10,000 worth of WETH, only $20,000 worth of WETH remains to back the $20,000 of minted DYAD.

The actual collateral ratio post-withdrawal is now 100% ($20,000 / $20,000), which is below the required 150%.

This leaves the system under-collateralized and vulnerable to price fluctuations that could further diminish the collateral's value.

</details>

Tools Used

Manual Review

Implement a check in add and addKerosene functions to verify that a vault is not already present in the other set before adding.

For the add function:

function add(
    uint id,
    address vault
) 
  external
    isDNftOwner(id)
{
  if (vaults[id].length() >= MAX_VAULTS) revert TooManyVaults();
  if (!vaultLicenser.isLicensed(vault))  revert VaultNotLicensed();
++  if (vaultsKerosene[id].contains(vault)) revert VaultAlreadyInKerosene();
  if (!vaults[id].add(vault))            revert VaultAlreadyAdded();
  emit Added(id, vault);
}

For the addKerosene function:

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 (vaults[id].contains(vault))                         revert VaultAlreadyInNonKerosene();
  if (!vaultsKerosene[id].add(vault))                     revert VaultAlreadyAdded();
  emit Added(id, vault);
}

Assessed type

Context

#0 - c4-pre-sort

2024-04-28T07:01:57Z

JustDravee marked the issue as duplicate of #966

#1 - c4-pre-sort

2024-04-29T08:37:29Z

JustDravee marked the issue as sufficient quality report

#2 - c4-judge

2024-05-04T09:46:23Z

koolexcrypto marked the issue as unsatisfactory: Invalid

#3 - 0xAbhay

2024-05-16T04:34:57Z

@koolexcrypto The vulnerability stems from a duplication bug that allows the same exogenous vault to be counted twice, both in calculations for non-Kerosene collateral (getNonKeroseneValue) and Kerosene collateral (getKeroseneValue). This oversight could lead to an inflated perception of the collateral ratio, enabling users to mint more DYAD stablecoins than their actual collateral value permits. Consequently, users could exploit this bug to effectively bypass the system's intended safeguards against under-collateralization. This occurs when users deposit assets into a vault, with the same vault mistakenly counted twice in collateral valuation calculations. As a result, the system erroneously assesses the collateral value, potentially allowing users to mint more DYAD than they should, posing risks of insolvency and financial loss.

#4 - koolexcrypto

2024-05-22T10:42:39Z

Hi @0xAbhay

Thank you for your input.

Could you please provide a PoC (coded) that demonstrates the issue above?

#5 - 0xAbhay

2024-05-22T17:17:21Z

@koolexcrypto Summary of the Duplication Bug in the DYAD Minting Process Issue Description

A duplication bug in the DYAD minting process allows the same exogenous vault to be counted in both getNonKeroseneValue and getKeroseneValue. This bug results in a significantly inflated collateral ratio, enabling users to mint more DYAD than the actual collateral should permit. This could lead to under-collateralization and jeopardize user funds.

<details>

Proof of Concept Adding Vaults:

The deployment script adds the exogenous vault to both the Kerosene Manager and the Vault License.

Users can add the same exogenous vault using both add and addKerosene. Minting DYAD with Inflated Collateral:

A user deposits 10 WETH (worth $30,000) into a vault. The user mints 29,998 DYAD, just under the USD value of the deposited WETH. The mintDyad function calls getNonKeroseneValue to check the vault's USD value. getNonKeroseneValue calculates the USD value, which includes the exogenous vault.

Collateral Value Calculation:

The USD value of the WETH in the vault is correctly calculated based on the asset price (e.g., $3000 per WETH). The getTotalUsdValue function aggregates values from getNonKeroseneValue and getKeroseneValue, potentially doubling the perceived value if the same vault is counted in both.

Inflated Collateral Ratio:

If the vault value is duplicated, the perceived total value is $60,000 instead of $30,000. The collateral ratio appears as 200% instead of 150%.

Impact on Collateralization:

The inflated collateral ratio allows users to mint DYAD at almost a 1:1 ratio to the USD value of their collateral. Post-minting, the collateral ratio is incorrectly calculated, making it seem higher than it actually is.

Minting More DYAD than Permitted:

A user deposits $30,000 worth of WETH and mints 29,998 DYAD. Due to the duplication bug, the system thinks the collateral is worth $60,000. This falsely high collateral value allows the user to pass the collateralization check.

Excessive Withdrawal:

A user deposits $30,000 worth of WETH and mints $20,000 worth of DYAD, expecting a collateral ratio above 150%. The system erroneously calculates the collateral as $60,000, showing a 300% ratio. The user withdraws $10,000, reducing the actual collateral to $20,000. The true collateral ratio drops to 100%, below the required 150%, leaving the system under-collateralized.

Conclusion The duplication bug in the minting and collateral calculation process can lead to severe under-collateralization. Users can exploit this by minting more DYAD than their actual collateral should allow or by withdrawing excessive amounts, putting the system at significant risk. Fixing this bug is crucial to maintaining the integrity and security of the DYAD stablecoin system.

</details>

#6 - 0xAbhay

2024-05-22T17:21:47Z

@koolexcrypto Sorry, I won't be able to provide the coded POC because I am on vacation. Additionally, this seems similar to issue #966. Thank you for your hard work. I hope you understand my situation.

This is my greatest attempt at the POC, as it was very difficult to write it on my phone.

#7 - 0xAbhay

2024-05-29T01:40:06Z

@koolexcrypto hey sir this is still marked as unsatisfactory please check whenever you are free thank you.

#8 - c4-judge

2024-05-29T09:07:59Z

koolexcrypto removed the grade

#9 - c4-judge

2024-05-29T09:08:33Z

koolexcrypto marked the issue as satisfactory

#10 - c4-judge

2024-05-29T11:20:15Z

koolexcrypto marked the issue as duplicate of #1133

Lines of code

https://github.com/code-423n4/2024-04-dyad/blob/49fb7174576e5147dc73d3222d16954aa641a2e0/src/core/VaultManagerV2.sol#L119-L131 https://github.com/code-423n4/2024-04-dyad/blob/49fb7174576e5147dc73d3222d16954aa641a2e0/src/core/VaultManagerV2.sol#L134-L153

Vulnerability details

Impact

The exploitation of the block.number dependency bug can lead to a denial of service where legitimate users are unable to withdraw their funds. This undermines the trust in the system and can lead to financial losses and reputational damage for the platform.

Proof of Concept

The VaultManagerV2::deposit lacks a check to ensure that the caller is the DNft holder. This omission could potentially result in a serious problem.

This can be manupilated by miner or non-miner lets go through the one by one example

Here's how a miner could execute this attack:

Monitor Mempool: The miner monitors the mempool for withdraw transactions related to a specific id.

Prepare Deposit: Upon detecting a withdraw transaction, the miner prepares a deposit transaction for the same id.

// @audit no check anyone can deposit for a valid id even if they are not the owner of DNft
  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);
  }

The miner can use any account to perform this deposit, including their own.

Construct Block: When constructing the next block, the miner includes their own deposit transaction before the user's withdraw transaction.

Mine Block: The miner mines the block, ensuring that their deposit transaction is included first.

Publish Block: The miner publishes the block to the blockchain.

Withdrawal Fails: When the block is confirmed, the withdraw transaction is processed after the deposit transaction. The withdraw transaction fails because the check in VaultManagerV2::withdraw

  function withdraw(
    uint    id,
    address vault,
    uint    amount,
    address to
  ) 
    public
      isDNftOwner(id)
  {
    if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock();
    // ...
    // ..
  }

now matches the current block number, triggering the DepositedInSameBlock revert condition.

Here's a step-by-step explanation of how a non-miner might carry out this attack:

Monitor Mempool: The attacker monitors the mempool for withdraw transactions related to a specific id using a Web3 provider or blockchain explorer API.

Craft Deposit Transaction: Upon detecting a withdraw transaction, the attacker quickly creates a deposit transaction for the same id.

Increase Gas Price: The attacker sets a higher gas price for their deposit transaction to incentivize miners to prioritize and include it in the next block before the pending withdraw transaction.

Broadcast Deposit Transaction: The attacker broadcasts their deposit transaction to the network.

Miners Include Deposit First: Miners, motivated by the higher gas fees, include the attacker's deposit transaction in the next block before the withdraw transaction.

Withdrawal Fails: When the withdraw transaction is processed, it fails because the check in VaultManagerV2::withdraw

  function withdraw(
    uint    id,
    address vault,
    uint    amount,
    address to
  ) 
    public
      isDNftOwner(id)
  {
    if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock();
    // ...
    // ..
  }

matches the current block number due to the inclusion of the attacker's deposit transaction, triggering the DepositedInSameBlock revert condition.

Repeat: The miner and non-miner can continue this strategy for subsequent blocks, effectively preventing the user from withdrawing by ensuring that a deposit transaction for the same id always precedes any withdraw transaction in the block order

Tools Used

Manual Review

allow deposits only from the owner of the DNft, ensure that the VaultManagerV2::deposit function includes a check to verify the caller's ownership status.

  function deposit(
    uint    id,
    address vault,
    uint    amount
  ) 
    external 
    isValidDNft(id)
++  isDNftOwner(id)
    
  {

    idToBlockOfLastDeposit[id] = block.number;
    Vault _vault = Vault(vault);
    _vault.asset().safeTransferFrom(msg.sender, address(vault), amount);
    _vault.deposit(id, amount);
  }

Assessed type

DoS

#0 - c4-pre-sort

2024-04-27T11:56:24Z

JustDravee marked the issue as duplicate of #489

#1 - c4-pre-sort

2024-04-29T09:26:15Z

JustDravee marked the issue as sufficient quality report

#2 - c4-judge

2024-05-05T20:39:59Z

koolexcrypto marked the issue as unsatisfactory: Invalid

#3 - c4-judge

2024-05-05T20:45:36Z

koolexcrypto marked the issue as unsatisfactory: Invalid

#4 - c4-judge

2024-05-05T21:52:18Z

koolexcrypto marked the issue as nullified

#5 - c4-judge

2024-05-05T21:52:22Z

koolexcrypto marked the issue as not nullified

#6 - c4-judge

2024-05-08T15:26:39Z

koolexcrypto marked the issue as duplicate of #1001

#7 - c4-judge

2024-05-11T19:49:04Z

koolexcrypto marked the issue as satisfactory

#8 - c4-judge

2024-05-13T18:34:30Z

koolexcrypto changed the severity to 3 (High Risk)

Findings Information

Labels

bug
3 (High Risk)
partial-50
sufficient quality report
upgraded by judge
edited-by-warden
:robot:_49_group
duplicate-930

Awards

119.0149 USDC - $119.01

External Links

Lines of code

https://github.com/code-423n4/2024-04-dyad/blob/49fb7174576e5147dc73d3222d16954aa641a2e0/src/core/VaultManagerV2.sol#L119-L131

Vulnerability details

Impact

The lack of proper ownership verification in the deposit function could lead to significant security vulnerabilities. An attacker could exploit this oversight to deposit assets into unauthorized or malicious vaults using any DNft ID, regardless of ownership. This could result in the unauthorized use of the platform, manipulation of vault balances, and potential loss of user funds.

Proof of Concept

Identify or deploy a malicious vault contract that could, for example, redirect deposited assets to an attacker-controlled address.

Without owning a particular DNft, call the deposit function with its ID and the address of the malicious vault, along with the amount to deposit. Since there is no ownership check,

//@audit-> Anyone can deposit for any other ID, even if they do not hold the corresponding NFT
// attacker can cause issue by creating a non lincensed vault and deposit with the valid Dnft
  function deposit(
    uint    id,
    address vault,
    uint    amount
  ) 
    external 
      isValidDNft(id)
  {

    idToBlockOfLastDeposit[id] = block.number;
    Vault _vault = Vault(vault);
    //@audit-> lacks the Licensed  vault check
    _vault.asset().safeTransferFrom(msg.sender, address(vault), amount);
    _vault.deposit(id, amount);
  }

the transaction could succeed, allowing the attacker to deposit assets into the malicious vault.

Tools Used

Manual Review

Modify the deposit function to include the isDNftOwner modifier and Licensed vaults check for both kerosene and non kerosene.

function deposit(
  uint id,
  address vault,
  uint amount
) 
  external 
    isValidDNft(id)
++  isDNftOwner(id)
{
++  if (!vaultLicenser.isLicensed(vault) && !keroseneManager.isLicensed(vault)) revert NotLicensed();

  idToBlockOfLastDeposit[id] = block.number;
  Vault _vault = Vault(vault);
  _vault.asset().safeTransferFrom(msg.sender, address(vault), amount);
  _vault.deposit(id, amount);
}

Assessed type

Context

#0 - c4-pre-sort

2024-04-28T03:46:29Z

JustDravee marked the issue as duplicate of #463

#1 - c4-pre-sort

2024-04-29T08:50:20Z

JustDravee marked the issue as sufficient quality report

#2 - c4-judge

2024-05-03T21:57:41Z

koolexcrypto marked the issue as unsatisfactory: Invalid

#3 - 0xAbhay

2024-05-16T04:41:10Z

@koolexcrypto hey how this finding is duplicate of #463 this finding describes

The vulnerability in the deposit function of the VaultManagerV2 contract stems from the lack of proper ownership verification and the absence of a check to ensure that the vault is licensed. This flaw allows an attacker to deposit assets into any vault using any DNft ID, regardless of whether they own the DNft. The attacker could exploit this by creating or identifying a malicious vault contract that redirects deposited assets to an attacker-controlled address. By calling the deposit function with the ID of a valid DNft and the address of the malicious vault, the attacker can deposit assets without restriction. This loophole can lead to unauthorized use of the platform, manipulation of vault balances, and potential loss of user funds. To mitigate this, the deposit function should include checks for DNft ownership and verify that the vault is licensed by adding the isDNftOwner modifier and validating the vault's license status.

#4 - c4-judge

2024-05-22T10:46:18Z

koolexcrypto marked the issue as not a duplicate

#5 - koolexcrypto

2024-05-22T10:49:27Z

Hi @0xAbhay

Thank you for your feedback.

significant security vulnerabilities. to deposit assets into unauthorized or malicious vaults using any DNft ID This could result in the unauthorized use of the platform, manipulation of vault balances, and potential loss of user funds

Could you please explain what's the impact? Those are general statements. This could possibly be a dup of #1266 . But no sufficient proof is provided nor specific impact.

#6 - 0xAbhay

2024-05-22T17:43:05Z

Hey @koolexcrypto vulnerability in the deposit function of the VaultManagerV2.sol contract has significant security implications, primarily due to the lack of proper ownership verification for the DNft and the absence of a check to ensure that the vault is licensed. Here's a detailed explanation of the impact:

<details> <summary>IMPACT</summary>

Exploitation by Attackers: An attacker can call the deposit function with any DNft ID, even if they do not own the DNft associated with that ID. This allows the attacker to manipulate the vaults without needing to hold the corresponding DNft.

Unauthorized Use of Platform: This vulnerability enables attackers to interact with the platform in ways that are not intended by its design, allowing them to deposit assets into vaults they shouldn't have access to.

Redirection of Funds: An attacker could create a malicious vault contract that redirects any deposited assets to an address under their control. By exploiting the lack of ownership checks, the attacker can use any valid DNft ID to deposit assets into this malicious vault.

Manipulation of Vault Balances: Without ownership verification and vault licensing checks, the platform's vault balances can be manipulated. This can disrupt the normal functioning and integrity of the platform.

Systemic Risk: The broader platform might face systemic risks due to such unauthorized and potentially fraudulent activities, impacting all users and stakeholders.

</details>

#7 - 0xAbhay

2024-05-22T17:43:39Z

@koolexcrypto Thank you for judging this. It seems similar to #1266. In summary, anyone can deposit into any vault even if they don't hold the NFT, and even if the vault is not an exogenous or endogenous vault.

#8 - koolexcrypto

2024-05-29T09:12:27Z

Thank you.

dup of #930 with a partial credit due to the fact that, it doesn't describe a specific impact although it mentions the bug .

#9 - c4-judge

2024-05-29T09:12:31Z

koolexcrypto removed the grade

#10 - c4-judge

2024-05-29T09:12:57Z

koolexcrypto changed the severity to 3 (High Risk)

#11 - c4-judge

2024-05-29T09:13:48Z

koolexcrypto marked the issue as duplicate of #930

#12 - c4-judge

2024-05-29T09:13:52Z

koolexcrypto marked the issue as satisfactory

#13 - c4-judge

2024-05-29T09:13:57Z

koolexcrypto marked the issue as partial-50

Awards

3.8221 USDC - $3.82

Labels

bug
3 (High Risk)
satisfactory
sufficient quality report
upgraded by judge
:robot:_52_group
duplicate-308

External Links

Lines of code

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L134-L153 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/Vault.kerosine.unbounded.sol#L65

Vulnerability details

Impact

The potential underflow in the Vault.kerosine.unbounded::assetPrice() function can lead to a denial of service where users are unable to withdraw their assets from the Unbounded kerosine vault. This can be exploited by an attacker to manipulate the TVL, potentially causing financial loss to users.This could lead to loss of confidence in the system and financial loss for users if they are unable to access their assets when needed.

Proof of Concept

In Deployment Script a new WETH or wstETH vault is created without any initial assets, resulting in a TVL of zero. Additionally, the deployment script grants an unbounded vault to the vault licenser, so the unbounded vault can be added to the VaultManagerV2 using the add function. This also satisfies the check of !vaultLicenser.isLicensed(vault).

<details> <summary>Case 1: When the TVL < dyad.totalSupply()</summary> user add the Unbouded kerosine vault using `VaultManagerV2::add`

A user deposits assets into the Unbounded kerosine vault.

The user attempts to withdraw, triggering the vault.assetPrice() function

 function withdraw(
    uint    id,
    address vault,
    uint    amount,
    address to
  ) 
    public
      isDNftOwner(id)
  {
///........
    uint value = amount * _vault.assetPrice() 
                  * 1e18 
                  / 10**_vault.oracle().decimals() // oracle decimal is 8
                  / 10**_vault.asset().decimals();
                  ///......
  }                  

Since the user is withdrawing from the Unbounded Kerosene vault, the assetPrice() function specific to the Unbounded vault is called. This function's calculation depends on the TVL of the exogenous vaults (e.g., WETH, wstETH), which can be influenced by external users.

  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-> underflow reverts when tvl < totalsupply
      uint numerator   = tvl - dyad.totalSupply();
      uint denominator = kerosineDenominator.denominator();
      return numerator * 1e8 / denominator;
    }

Since the TVL is zero and the dyad total supply is greater than zero, the subtraction in the assetPrice calculation results in an underflow. Even if a user deposits WETH and wstETH, for a withdrawal to succeed, it must be ensured that the TVL is greater than dyad.totalSupply(). If the TVL is less than dyad.totalSupply() after a user's deposit, the transaction will fail, and the user will not be able to withdraw.

</details> <details> <summary> Case 2: When the TVL > dyad.totalSupply() </summary>

User Prepares Withdrawal: A user initiates a withdrawal from a unbounded vaultwith a significant TVL in exogenous Vault.

Attacker Monitors: An attacker, holding a large portion of the assets in theexogenous Vault, monitors the network for pending withdrawal transactions.

Attacker Front-Runs: Upon detecting a user's withdrawal transaction, the attacker submits their own withdrawal from the exogenousVault. and submit a transaction with a higher gas fee to ensure it gets processed first.

Attacker Withdraws Assets: The attacker's withdrawal transaction is confirmed first due to the higher gas fee, significantly reducing the TVL in the vault. now TVL < dyad.totalSupply()

User's Withdrawal Transaction Processes: When the user's withdrawal transaction is processed, it triggers the Vault.assetPrice() ex(unbounded vault) function.

TVL Falls Below Dyad Total Supply: The attacker's withdrawal may have caused the TVL to fall below the dyad total supply.

Underflow in Asset Price Calculation: The assetPrice function calculates a negative value due to underflow, as the TVL is now less than the dyad total supply.

Transaction Reverts: The smart contract reverts the user's withdrawal transaction because of the underflow, preventing the user from retrieving their assets and effectively locking them in the unboundedVault.

In both scenarios, the attacker exploits the underflow vulnerability in the assetPrice function, which assumes that the TVL will always be greater than the dyad total supply. This vulnerability can lead to a denial of service for users trying to withdraw their assets.

</details> <details> <summary>Case 3: When the TVL == dyad.totalSupply() </summary>

here's a example of how the TVL could be manipulated to equal dyad.totalSupply(), causing the assetPrice to return zero and triggering a failure in the withdrawal process due to the NotEnoughExoCollat check, we would follow these steps:

Initial State: Assume the contract has a certain tvl (total value locked) and dyad.totalSupply() that are not equal. The assetPrice is therefore greater than zero.

Deposit/Withdraw Manipulation: An actor (could be a user or a group) starts interacting with the exogenous vaults that contribute to the tvl calculation. They could either deposit more assets into the exogenous vaults or withdraw assets from them.

Equalizing TVL and Dyad Supply: The actor continues to adjust their deposits and withdrawals until the tvl exactly equals dyad.totalSupply(). This could be done by monitoring the contract state off-chain and performing transactions accordingly.

TVL Equals Dyad Supply: Once tvl is equal to dyad.totalSupply(), the numerator in the assetPrice calculation becomes zero tvl - dyad.totalSupply() = 0.

Asset Price Calculation: When the assetPrice function is called, it returns zero because the numerator is zero, making the price calculation 0 * 1e8 / denominator = 0.

Withdrawal Attempt: A user tries to withdraw their funds from the unbounded vault. The withdrawal function calls getNonKeroseneValue(id), which relies on assetPrice of vault whhich is unbounded vault here and to determine the USD value of the assets that are not part of the Kerosene collateral.

Zero Asset Price Impact: Since assetPrice is zero, getNonKeroseneValue(id) also returns zero. This means that the system believes there is no non-Kerosene collateral value.

NotEnoughExoCollat Check: The withdrawal function then performs a check to ensure that the user has enough exogenous collateral to cover the withdrawal after accounting for the Kerosene collateral (dyadMinted). The check is if (getNonKeroseneValue(id) - value < dyadMinted) revert NotEnoughExoCollat;.

Revert Condition Met: Because getNonKeroseneValue(id) returns zero, any non-zero value requested for withdrawal will cause the subtraction to underflow (which is prevented by Solidity 0.8.x), or it will simply be less than dyadMinted, thus triggering the NotEnoughExoCollat revert.

Withdrawal Failure: The transaction is reverted, and the user is unable to withdraw their funds, effectively locking assets in the vault until the issue is resolved.

</details>

NOTE: This could also affect the redeemDyad function in the same way it affects the withdraw function. Users may not be able to redeemDyad because the underflow could cause the transaction to revert.

Tools Used

Manual Review

Validation Checks: Implement validation checks in the assetPrice function to ensure that the TVL is always greater than or equal to dyad.totalSupply(). If not, the function should return a meaningful error rather than allowing an underflow.

Case 3 solution:

  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();
++    require(tvl > 0 , "tvl should be greater than zero");  
      uint denominator = kerosineDenominator.denominator();
      return numerator * 1e8 / denominator;
  }

Assessed type

DoS

#0 - c4-pre-sort

2024-04-27T18:21:02Z

JustDravee marked the issue as duplicate of #958

#1 - c4-pre-sort

2024-04-29T08:39:36Z

JustDravee marked the issue as sufficient quality report

#2 - c4-judge

2024-05-05T13:48:42Z

koolexcrypto marked the issue as duplicate of #308

#3 - c4-judge

2024-05-11T20:09:38Z

koolexcrypto marked the issue as satisfactory

#4 - c4-judge

2024-05-13T18:34:04Z

koolexcrypto changed the severity to 3 (High Risk)

#5 - 0xAbhay

2024-05-16T04:25:39Z

@koolexcrypto i think this finding has more detail than #308

here is the explanation

Finding #308 describes a specific scenario related to the assetPrice() calculation in the Unbounded Kerosene vault, where the numerator calculation (tvl - dyad.totalSupply()) can lead to assets getting temporarily stuck in the Kerosene vaults until the TVL surpasses the Dyad's total supply. However, it focuses only on this particular situation without exploring other potential issues.

On the other hand, Finding #873 elaborates on the vulnerability in the assetPrice() function of the Vault.kerosine.unbounded contract in more detail. It outlines three distinct cases where this vulnerability can cause problems: when the TVL is less than, greater than, or equal to the Dyad's total supply. Each case describes a scenario where the underflow in the numerator calculation (tvl - dyad.totalSupply()) can lead to a denial of service situation, potentially causing financial loss and loss of confidence in the system.

In summary, Finding 308 provides a single scenario, while Finding 873 expands on the issue by presenting three distinct cases where the vulnerability can manifest due to the numerator calculation.

#6 - koolexcrypto

2024-05-29T09:01:45Z

Thank you for your feedback.

Case 1 and 2 are the same. The root cause is the underflow, the attacker in case 2 used the same bug in case 1.

While I appreciate your thoroughness in this report, I believe #308 is much easier to understand and comprehend.

Awards

3.8221 USDC - $3.82

Labels

bug
3 (High Risk)
satisfactory
sufficient quality report
upgraded by judge
:robot:_52_group
duplicate-308

External Links

Lines of code

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L205 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L213 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/Vault.kerosine.unbounded.sol#L65

Vulnerability details

Impact

This vulnerability can be exploited to avoid liquidations, allowing under-collateralized positions to persist and potentially leading to insolvency risks for the vault. It undermines the integrity of the liquidation mechanism, which is crucial for maintaining the platform's financial health.

Proof of Concept

In Deployment Script a new WETH or wstETH vault is created without any initial assets, resulting in a TVL of zero. Additionally, the deployment script grants an unbounded vault to the vault licenser, so the unbounded vault can be added to the VaultManagerV2 using the add function. This also satisfies the check of !vaultLicenser.isLicensed(vault).

a user has minted dyad worth $2000 using 1 ETH as collateral at a price of $3000 per ETH. Additionally, the user has deposited 1 Kerosene into the unbounded vault. If the user's collateralization ratio (CR) falls below the minimum required CR, their position becomes eligible for liquidation.

Here's how the vulnerability can prevent liquidation:

Liquidation Trigger: The user's position becomes under-collateralized, triggering eligibility for liquidation.

Liquidation Process: Another party or the system itself initiates the liquidation process by calling the liquidate function.

Collateral Valuation: The VaultManagerV2::liquidate function calls collatRatio -> getTotalUsdValue(id) to determine the total USD value of the user's collateral.

Calling getNonKeroseneValue(id): Within getTotalUsdValue(id), the getNonKeroseneValue(id) function is called to assess the value of non-Kerosene collateral, which is a unbounded vault here.

Underflow in assetPrice: If the TVL of the unbounded vault is less than dyad.totalSupply, the assetPrice calculation for the unbounded vault underflows, potentially reverting the transaction.

The underflow causes the transaction to revert, preventing liquidation

If the TVL Total Value Locked of the UnboundedKerosineVault is less than dyad.totalSupply(), the calculation of assetPrice() could underflow when subtracting dyad.totalSupply() from tvl. This underflow would cause the transaction to revert. Since TVL is influenced by external users through deposits and withdrawals, it can be manipulated by any user.

Tools Used

Manual Review

If tvl is less than dyad.totalSupply(), return a minimum asset price value that represents an under-collateralized position, triggering liquidation.

Assessed type

Context

#0 - c4-pre-sort

2024-04-27T18:18:10Z

JustDravee marked the issue as duplicate of #958

#1 - c4-pre-sort

2024-04-29T08:39:38Z

JustDravee marked the issue as sufficient quality report

#2 - c4-judge

2024-05-05T13:48:46Z

koolexcrypto marked the issue as duplicate of #308

#3 - c4-judge

2024-05-11T20:09:58Z

koolexcrypto marked the issue as satisfactory

#4 - c4-judge

2024-05-13T18:34:04Z

koolexcrypto changed the severity to 3 (High Risk)

#5 - 0xAbhay

2024-05-16T04:31:16Z

@koolexcrypto Finding #308 focuses on the withdraw function and how this can be the same issue please explain and also this is a duplicate of #224. this can be a separate finding from #308.

#6 - koolexcrypto

2024-05-22T10:33:26Z

Hi @0xAbhay

Please clarify what's the difference, otherwise, I can't guess what you mean.


also this is a duplicate of https://github.com/code-423n4/2024-04-dyad-findings/issues/224

Not sure what does that mean. If you have a comment on different issue, please post it there separately.

#7 - 0xAbhay

2024-05-22T17:28:22Z

@koolexcrypto The two vulnerabilities described in the findings are different in their nature and impact, although both relate to issues with the calculation of asset prices in the context of under-collateralization and potential liquidation problems. Here is an explanation of each vulnerability with examples:

<details>

Finding 1008: Avoiding Liquidations Due to Underflow in Asset Price Calculation

<details>

Vulnerability Details:

  • Impact: This vulnerability allows users to avoid liquidation even when they are under-collateralized, risking the financial health of the vault.

  • Proof of Concept: When the total value locked (TVL) of the unbounded vault is less than the total supply of DYAD, the asset price calculation (assetPrice()) underflows, causing the liquidation transaction to revert.

  • Example:

    1. A user deposits 1 ETH worth $3000 as collateral and mints $2000 worth of DYAD.
    2. If the price of ETH drops, making the collateral insufficient, the position should be liquidated.
    3. However, if the TVL in the unbounded vault (holding Kerosene and other assets) is less than the total DYAD supply, the calculation of asset price leads to an underflow.
    4. This underflow reverts the liquidation process, allowing the under-collateralized position to persist.

Finding 308: TVL Dependency Leading to Stuck Collateral

Vulnerability Details:

  • Impact: Users' collateral can get stuck in the vault due to the TVL being insufficient to cover the total DYAD supply, preventing proper price calculation and operation of the vault.
  • Proof of Concept: Until the TVL in the V2 vaults exceeds the total DYAD supply, the asset price calculation in UnboundedKerosineVault::assetPrice will revert, leading to stuck Kerosene deposits.
  • Example:
    1. Users are expected to migrate their collateral from V1 to V2 vaults, increasing TVL in V2.
    2. The asset price calculation depends on tvl - dyad.totalSupply(). If TVL is less than DYAD's total supply, the calculation reverts.
    3. A user deposits Kerosene into the unbounded vault when the TVL is still insufficient.
    4. The asset price calculation fails, and the user's Kerosene becomes temporarily stuck in the vault.

Comparison and Example Clarification:

Finding 1008: Avoids liquidation due to an underflow error during asset price calculation. It involves a scenario where the collateral ratio check fails due to the asset price calculation reverting, preventing liquidation.

Example: A user’s position should be liquidated because their collateral value falls below the required threshold. However, due to the underflow error in calculating the asset price, the liquidation process reverts, allowing the user to avoid liquidation despite being under-collateralized.

Finding 308: Involves the practical inability to interact with the vault due to insufficient TVL relative to DYAD's total supply, leading to stuck collateral.

Example: Users migrate their collateral from V1 to V2, but because the TVL in V2 is less than the total DYAD supply, any interaction with the Kerosene vault (such as depositing or withdrawing) fails, resulting in stuck collateral until the TVL exceeds the DYAD supply.

Both findings highlight critical issues in the liquidation and asset management processes but differ in the specifics of their causes and impacts.

</details>

#8 - 0xAbhay

2024-05-22T17:29:26Z

@koolexcrypto thanks for judging

#9 - koolexcrypto

2024-05-29T09:06:49Z

Thank you for your further clarification.

All mentioned issues share the same root cause, some describe variant impact.

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter