code-423n4 / 2024-04-dyad-findings

8 stars 6 forks source link

Kerosene's price can be manipulated #828

Closed c4-bot-7 closed 5 months ago

c4-bot-7 commented 6 months ago

Lines of code

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/Vault.kerosine.unbounded.sol#L50-L68 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/VaultManagerV2.sol#L230-L239 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L250-L267

Vulnerability details

Impact

A user deposit too much collateral can raise the price of Kerosene, But he can also lower the price of Kerosene by withdrawing collateral or mint DYAD.

A malicious user can manipulate the price of Kerosene to influence the user to meet the liquidation criteria, and the malicious user can profit from the liquidation.

Proof of Concept

1、Casting DYAD process

With VaultManagerV2.mintDyad we can see the restrictions on mintDyad https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L156-L169

  function mintDyad(
    uint    id,
    uint    amount,
    address to
  )
    external 
      isDNftOwner(id)
  {
    uint newDyadMinted = dyad.mintedDyad(address(this), id) + amount;
     // @audit If user is casting DYAD for the first time, newDyadMinted is 0, getNonKeroseneValue if also 0 can be checked by
    if (getNonKeroseneValue(id) < newDyadMinted)     revert NotEnoughExoCollat();
    dyad.mint(id, to, amount);
     // @audit The collateralization rate calculation includes getNonKeroseneValue and getKeroseneValue two parts, if getNonKeroseneValue is 0, completely getKeroseneValue is non-zero, as long as the collateralization rate is greater than 1.5 can be casting normally.
    if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); 
    emit MintDyad(id, amount, to);
  }

The main limitation is:

  1. getNonKeroseneValue(id) < newDyadMinted With the getNonKeroseneValue function, we can see that the collateral value is calculated excluding the KeroseneValue value. If it is the first time the user calls mintDyad, newDyadMinted=0, getNonKeroseneValue returns 0 which is passable. https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L250-L267

    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;
    }
  2. collatRatio(id) < MIN_COLLATERIZATION_RATIO Through the collatRatio method we can see that the calculation of the pledge rate includes getNonKeroseneValue and getKeroseneValue 2 parts, the sum of the 2 collateralization rate is greater than or equal to 1.5 can be. getNonKeroseneValue is equal to 0, also can be passed. https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L230-L239

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

The collatRatio calls the getTotalUsdValue method. https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L241-L248

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

Through the getKeroseneValue method, we can see the calculation process https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L269-L286

  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);  // @audit Calculating Collateral value      
        }
        totalUsdValue += usdValue;
      }
      return totalUsdValue;
  }

With vault.getUsdValue we can see that the value depends on the price and quantity https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/Vault.sol#L79-L89)

  function getUsdValue(
    uint id
  )
    external
    view 
    returns (uint) {
      return id2asset[id] * assetPrice() 
              * 1e18 
              / 10**oracle.decimals() 
              / 10**asset.decimals();
  }

Through the UnboundedKerosineVault.assetPrice we can see how the Kerosene price is calculated.

  function assetPrice() 
    public 
    view 
    override
    returns (uint) {
...
      for (uint i = 0; i < numberOfVaults; i++) {
        Vault vault = Vault(vaults[i]);
        tvl += vault.asset().balanceOf(address(vault))  // @audit Get locked value
                * vault.assetPrice() * 1e18
                / (10**vault.asset().decimals()) 
                / (10**vault.oracle().decimals());
      }
      uint numerator   = tvl - dyad.totalSupply();
      uint denominator = kerosineDenominator.denominator();
      return numerator * 1e8 / denominator; // @audit Calculate Kerosine price
  }

We can see that a malicious user can reduce the value of getKeroseneValue by reducing the pledge to reduce the tvl. This is when users who mainly rely on KeroseneValue pledges are affected. This is when malicious users can profit by liquidating these users

2、Liquidation process

Through the Liquidation method, we can see that the pledge rate is lower than 1.5 will be liquidated, the liquidator can get rewards. https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L205-L228

  function liquidate(
    uint id,
    uint to
  ) 
    external 
      isValidDNft(id)
      isValidDNft(to)
    {
      uint cr = collatRatio(id);
      if (cr >= MIN_COLLATERIZATION_RATIO) revert CrTooHigh();
      dyad.burn(id, msg.sender, dyad.mintedDyad(address(this), id));

      uint cappedCr               = cr < 1e18 ? 1e18 : cr;
      uint liquidationEquityShare = (cappedCr - 1e18).mulWadDown(LIQUIDATION_REWARD);
      uint liquidationAssetShare  = (liquidationEquityShare + 1e18).divWadDown(cappedCr);

      uint numberOfVaults = vaults[id].length();
      for (uint i = 0; i < numberOfVaults; i++) {
          Vault vault      = Vault(vaults[id].at(i));
          uint  collateral = vault.id2asset(id).mulWadUp(liquidationAssetShare);
          vault.move(id, to, collateral);
      }
      emit Liquidate(id, msg.sender, to);
  }

collatRatio Through the above analysis, we can know that users with more KeroseneValue collateralization are vulnerable to price manipulation

Tools Used

Manual Review

Recommended Mitigation Steps

When calling withdraw to extract collateral and calling mintDyad, we should not only consider whether the collateralization rate is greater than or equal to 1.5, but also consider the impact on KeroseneValue, for example, one day to limit the range of fluctuation of KeroseneValue, so as to protect the system.

Assessed type

Oracle

c4-pre-sort commented 5 months ago

JustDravee marked the issue as duplicate of #67

c4-pre-sort commented 5 months ago

JustDravee marked the issue as sufficient quality report

c4-judge commented 5 months ago

koolexcrypto changed the severity to 2 (Med Risk)

c4-judge commented 5 months ago

koolexcrypto marked the issue as unsatisfactory: Invalid

c4-judge commented 5 months ago

koolexcrypto marked the issue as satisfactory