DYAD - josephdara'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: 111/183

Findings: 3

Award: $7.65

🌟 Selected for report: 0

πŸš€ Solo Findings: 0

Lines of code

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L205-L239

Vulnerability details

Impact

Liquidation can be blocked by the NFT owner. This is because the of a miscalculation in the collateralratio. The collatRatio gets the getTotalUsdValue() of an ID which is then used to calculate the actual collateralratio. However the getTotalUsdValue() add all vaults in both vaults types:


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

This is expected except for the fact whereby a vault is present in both vaults[] and vaultsKerosene. If we look at the DeployV2 which is listed in scope, we see that the same vaults are licensed on both sides. In DeployV2:

    kerosineManager.add(address(ethVault));
    kerosineManager.add(address(wstEth));

.
.
.


    vaultLicenser.add(address(ethVault));
    vaultLicenser.add(address(wstEth));
    vaultLicenser.add(address(unboundedKerosineVault));

Hence by adding the same vault to both vaults[] and vaultsKerosene, Liquidations will revert, even though the collateral ratio is adding the same vault twice

Proof of Concept

  function liquidate(
    uint id,
    uint to
  ) 
    external 
      isValidDNft(id)
      isValidDNft(to)
    {
      uint cr = collatRatio(id);
      if (cr >= MIN_COLLATERIZATION_RATIO) revert CrTooHigh();

Since collatRatio() will return a high value even though there isn't enough collateral, liquidation will revert, the nft owner simply has to call the functions below using the same vault and id:

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

Tools Used

Manual Review, JosephDara

Prevent additon of a vault to either of the arrays if they are already contained in the other array

Assessed type

Other

#0 - c4-pre-sort

2024-04-29T07:17:18Z

JustDravee marked the issue as duplicate of #70

#1 - c4-pre-sort

2024-04-29T12:03:00Z

JustDravee marked the issue as sufficient quality report

#2 - c4-judge

2024-05-06T18:26:48Z

koolexcrypto marked the issue as not a duplicate

#3 - c4-judge

2024-05-06T18:26:52Z

koolexcrypto marked the issue as unsatisfactory: Invalid

#4 - Josephdara

2024-05-16T04:19:01Z

Hi @koolexcrypto , I believe this has been judged wrongly. The deployment script in scope: https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/script/deploy/Deploy.V2.s.sol shows that some vaults are added to both Licensers:

    KerosineManager kerosineManager = new KerosineManager();

    kerosineManager.add(address(ethVault));
    kerosineManager.add(address(wstEth));

and

    vaultLicenser.add(address(ethVault));
    vaultLicenser.add(address(wstEth));
    vaultLicenser.add(address(unboundedKerosineVault));

However, when calculating the Collateral ratio, the value in the both licensed vaults is added together.

  function collatRatio(
    uint id
  )
    public 
    view
    returns (uint) {
      uint _dyad = dyad.mintedDyad(address(this), id);
      if (_dyad == 0) return type(uint).max;
      return getTotalUsdValue(id).divWadDown(_dyad);
  }


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


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

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

By having the same vault addresses licensed in both licensers, liquidations is blocked because the collateral ratio would be higher than it's actual figure.

To avoid liquidation, a user simply need to add the other vault using either add or addKerosene. And their CR is increased automatically

#5 - koolexcrypto

2024-05-22T09:48:31Z

Hi @Josephdara

Thank you for your feedback.

addKerosene adds to vaultsKerosene which is not used on liquidation, vaults is used instead.

#6 - Josephdara

2024-05-22T09:57:41Z

Thanks for your reply. Pardon my comment however, liquidation uses collateral ratio Collateral ratio gets total value from both VaultsKerosene and Vaults https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L213

So the collateral ratio for an ID is larger since it’s adding from both arrays that contain the same address.

Please kindly confirm this, I would however respect your decision on this.

#7 - c4-judge

2024-05-29T08:50:42Z

koolexcrypto marked the issue as duplicate of #1133

#8 - koolexcrypto

2024-05-29T08:51:35Z

Thank you for the further clarification.

This is a dup of #1133

#9 - c4-judge

2024-05-29T08:51:50Z

koolexcrypto marked the issue as satisfactory

Lines of code

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

Vulnerability details

Impact

Allowing any user to deposit funds for a particular ID opens up the smart contract to frontrunning, causing withdrawals to fail.

This is due to the check:

    if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock();

The state variable idToBlockOfLastDeposit[] is updated in the deposit(), hence by depositing 1 wei of collateral in a block, withdrawals can be blocked. Therefore frontrunning a withdrawal transaction with a small deposit causes DOS

Proof of Concept

Anyone can deposit to any ID


  function deposit(
    uint    id,
    address vault,
    uint    amount
  ) 
    external 
      isValidDNft(id)
  {
    idToBlockOfLastDeposit[id] = block.number;

Tools Used

Manual Review, JosephDara

Remove the block check or restrict deposits to only the isDNftOwner(id)

Assessed type

DoS

#0 - c4-pre-sort

2024-04-27T11:23:56Z

JustDravee marked the issue as duplicate of #1103

#1 - c4-pre-sort

2024-04-27T11:51:48Z

JustDravee marked the issue as duplicate of #489

#2 - c4-pre-sort

2024-04-29T09:33:05Z

JustDravee marked the issue as sufficient quality report

#3 - c4-judge

2024-05-05T20:38:07Z

koolexcrypto marked the issue as unsatisfactory: Invalid

#4 - c4-judge

2024-05-05T21:10:02Z

koolexcrypto marked the issue as nullified

#5 - c4-judge

2024-05-05T21:10:07Z

koolexcrypto marked the issue as not nullified

#6 - c4-judge

2024-05-08T15:30:02Z

koolexcrypto marked the issue as duplicate of #1001

#7 - c4-judge

2024-05-11T19:45:04Z

koolexcrypto marked the issue as satisfactory

Awards

7.3512 USDC - $7.35

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
sufficient quality report
:robot:_39_group
duplicate-118

External Links

Lines of code

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L67-L116

Vulnerability details

Impact

In the VaultManagerV2, vaults can be added if they are licensed, and removed if they have no funds for an ID. However, addition and removal of vaults can be blocked whenever a Vault which was previously licensed is delicensed.

This is because the withdraw function does not allow withdrawals from unlicensed vaults. Although the check is not done in that function, it is done in getNonKeroseneValue(id) which skips every unlicensed vaults when adding all collateral. Since users can not withdraw collateral from an unlicensed vault, it is impossible to remove the vault from the vaults / vaultsKerosene array using remove() & removeKerosene().

Since the protocol already enforces a strict cap using MAX_VAULTS/MAX_VAULTS_KEROSENE, a user cannot add new licensed vault using add() & addKerosene() if they have their array populated with delicensed and licensed vaults.

One more issue here is, since a malicious user can deposit for any other user, immediately a vault is delicensed, they can simply deposit 1 wei value to all users who currently have that vault in their array, hence prevent removal from the vault from the vaults array for users.

Proof of Concept

The check in the withdrawal & getNonKeroseneValue 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(); //@audit getNonKeroseneValue called here
    _vault.withdraw(id, to, amount);
    if (collatRatio(id) < MIN_COLLATERIZATION_RATIO)  revert CrTooLow(); 
  }

  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))) { //@audit excluded all unlicensed vaults
    
  }

Withdrawal fails

  //@audit cannot remove vaults  when it is unlicensed. This is because, you cannot withdraw from unlicensed vaults. Hence id2asset will never be zero

    if (Vault(vault).id2asset(id) > 0) revert VaultHasAssets();
 

Addition fails here

  function add(
      uint    id,
      address vault
  ) 
    external
      isDNftOwner(id)
  {
    if (vaults[id].length() >= MAX_VAULTS) revert TooManyVaults();

Tools Used

Manual Review, JosephDara

If a vault is delicensed, allow withdrawals from that vaults since it's funds are no more calculated as collateral.

Assessed type

DoS

#0 - c4-pre-sort

2024-04-29T07:44:33Z

JustDravee marked the issue as duplicate of #489

#1 - c4-pre-sort

2024-04-29T09:33:00Z

JustDravee marked the issue as sufficient quality report

#2 - c4-judge

2024-05-05T20:23:11Z

koolexcrypto marked the issue as not a duplicate

#3 - c4-judge

2024-05-06T08:57:11Z

koolexcrypto marked the issue as duplicate of #118

#4 - c4-judge

2024-05-11T12:24:26Z

koolexcrypto marked the issue as satisfactory

#5 - c4-judge

2024-05-13T18:33:43Z

koolexcrypto changed the severity to 2 (Med Risk)

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