DYAD - pontifex'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: 47/183

Findings: 6

Award: $259.07

🌟 Selected for report: 0

🚀 Solo Findings: 0

Lines of code

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/script/deploy/Deploy.V2.s.sol#L64-L65

Vulnerability details

Impact

Users can receive more dyad than expected by including collateral vaults (ethVault and wstEth) in both the vaults and the vaultsKerosene mappings. This lets receive dyad in 1:1 usd value ration. Also this can prevent the user from liquidation while the collateral usd value not less than 75% of the initial. So the dyad token can be undercollateralized.

Proof of Concept

The DeployV2 contract adds ethVault and wstEth vaults to the KerosineManager contract and to the Licenser contract.

contract DeployV2 is Script, Parameters {
  function run() public returns (Contracts memory) {
//...
    KerosineManager kerosineManager = new KerosineManager();

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

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/script/deploy/Deploy.V2.s.sol#L62-L94 So users can add ethVault and wstEth vaults as collateral vault in the vaults mapping and as kerosene vault in the vaultsKerosene mapping because these vaults are licensed in both contracts.

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

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L67-L91 The mintDyad function checks the usd value of collateral vaults and then the total usd value. The issue lets pass the second check without having any kerosene vaults because value from ethVault and wstEth vaults will be counted in the getTotalUsdValue function twice

  function mintDyad(
    uint    id,
    uint    amount,
    address to
  )
    external 
      isDNftOwner(id)
  {
    uint newDyadMinted = dyad.mintedDyad(address(this), id) + amount;
    if (getNonKeroseneValue(id) < newDyadMinted)     revert NotEnoughExoCollat();
    dyad.mint(id, to, amount);
    if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); 
    emit MintDyad(id, amount, to);
  }

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

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

There is the same for the liquidate function.

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

Tools Used

Manual review

Consider adding ethVault and wstEth vaults only in the Licenser contract. It also demands fixing Kerosene vaults can break the protocol functionality issue in the UnboundedKerosineVault contract.

    KerosineManager kerosineManager = new KerosineManager();

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

    vaultManager.setKeroseneManager(kerosineManager);

It can be useful to implement a check in the KerosineManager contract which lets adding only vaults with kerosene token as an asset. The alternative recommendation is checking if the vault asset equals the kerosine token address in the VaultManagerV2.addKerosene function.

Assessed type

Other

#0 - c4-pre-sort

2024-04-28T07:00:36Z

JustDravee marked the issue as duplicate of #966

#1 - c4-pre-sort

2024-04-29T08:37:44Z

JustDravee marked the issue as sufficient quality report

#2 - c4-judge

2024-05-04T09:46:20Z

koolexcrypto marked the issue as unsatisfactory: Invalid

#3 - c4-judge

2024-05-29T11:20:23Z

koolexcrypto marked the issue as duplicate of #1133

#4 - c4-judge

2024-05-29T11:43:36Z

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#L95

Vulnerability details

Impact

Users can receive more dyad than expected by including unboundedKerosineVault vault in the vaults mapping. It will increase the getNonKeroseneValue value by the kerosene token usd value. So users can receive dyad tokens in ratio even less than 1:1 rate in usd value and the dyad token can be undercollateralized.

Proof of Concept

The DeployV2 contract adds the unboundedKerosineVault vault to the Licenser contract.

contract DeployV2 is Script, Parameters {
  function run() public returns (Contracts memory) {
//...
    vaultLicenser.add(address(unboundedKerosineVault));

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/script/deploy/Deploy.V2.s.sol#L95 So users can add the unboundedKerosineVault vault as a collateral vault in the vaults mapping because the vault is licensed in the Licenser contract.

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

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L67-L78 The mintDyad function checks the usd value of collateral vaults which should not less than the amount of DYAD to be minted but it should not include kerosene tokens. In case the user has enough kerosene (usd value more than collateral usd value) the dyad token can be undercollateralized.

  function mintDyad(
    uint    id,
    uint    amount,
    address to
  )
    external 
      isDNftOwner(id)
  {
    uint newDyadMinted = dyad.mintedDyad(address(this), id) + amount;
    if (getNonKeroseneValue(id) < newDyadMinted)     revert NotEnoughExoCollat();
    dyad.mint(id, to, amount);
    if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow(); 
    emit MintDyad(id, amount, to);
  }

Tools Used

Manual review

Consider not adding kerosene vaults to the Licenser contract but only to the KerosineManager contract. It also demands fixing Kerosene vaults can break the protocol functionality issue in the UnboundedKerosineVault contract.

    vaultLicenser.add(address(ethVault));
    vaultLicenser.add(address(wstEth));
-   vaultLicenser.add(address(unboundedKerosineVault));
-   // vaultLicenser.add(address(boundedKerosineVault));

The alternative recommendation is checking if the vault asset does not equal the kerosine token address in the VaultManagerV2.add function.

Assessed type

Other

#0 - c4-pre-sort

2024-04-28T19:45:32Z

JustDravee marked the issue as duplicate of #966

#1 - c4-pre-sort

2024-04-29T08:37:40Z

JustDravee marked the issue as sufficient quality report

#2 - c4-judge

2024-05-04T09:46:20Z

koolexcrypto marked the issue as unsatisfactory: Invalid

#3 - c4-judge

2024-05-29T11:20:24Z

koolexcrypto marked the issue as duplicate of #1133

#4 - c4-judge

2024-05-29T11:43:38Z

koolexcrypto marked the issue as satisfactory

Findings Information

Labels

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

Awards

238.0297 USDC - $238.03

External Links

Lines of code

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

Vulnerability details

Impact

An attacker can deposit a low amount, or even zero, of assets to prevent another user withdrawal. Actually attackers can create vaults which are under their control. So they can deposit any amounts of any tokens through the VaultManagerV2.deposit with the result of idToBlockOfLastDeposit[id] updating of the victim for the gas cost only.

Proof of Concept

There is only restriction isValidDNft(id) in 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);
  }

So the id owner can't withdraw in the block.number

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

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

Tools Used

Manual review

Consider restriction of donating or/and implementing trusted addresses mapping, who can deposit on behalf of a particular id owner.

Assessed type

MEV

#0 - c4-pre-sort

2024-04-27T11:24:21Z

JustDravee marked the issue as duplicate of #1103

#1 - c4-pre-sort

2024-04-27T11:51:51Z

JustDravee marked the issue as duplicate of #489

#2 - c4-pre-sort

2024-04-29T09:32:04Z

JustDravee marked the issue as sufficient quality report

#3 - c4-judge

2024-05-05T20:14:04Z

koolexcrypto marked the issue as not a duplicate

#4 - c4-judge

2024-05-05T20:16:25Z

koolexcrypto marked the issue as duplicate of #1266

#5 - c4-judge

2024-05-08T15:37:48Z

koolexcrypto changed the severity to 3 (High Risk)

#6 - c4-judge

2024-05-11T12:19:06Z

koolexcrypto marked the issue as satisfactory

#7 - c4-judge

2024-05-28T19:12:59Z

koolexcrypto marked the issue as duplicate of #930

Awards

7.3026 USDC - $7.30

Labels

bug
3 (High Risk)
satisfactory
sufficient quality report
:robot:_97_group
duplicate-128

External Links

Lines of code

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

Vulnerability details

Impact

Liquidators are not rewarded with Kerosene tokens since only assets from vaults are moved to liquidators. So liquidators receive less than expected and can even have losses in case getNonKeroseneValue(id) < dyad.mintedDyad(address(this), id)).

Proof of Concept

Users add collateral vaults to their dNFT (Note) via the VaultManagerV2.add function and Kerosene vaults via the VaultManagerV2.addKerosene function. The Kerosene vaults are added to vaultsKerosene mapping.

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

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L67-L91 In case of liquidation only assets from vaults added to the vaults mapping are moved to the liquidator and all Kerosene tokens stay on the liquidated Note because only vaults mapping are included in the liquidate function as a source of assets.

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

In the protocol documentation is mentioned The liquidating Note absorbs the liquidated Note’s DYAD balance and captures its collateral, including Kerosene https://dyadstable.notion.site/DYAD-design-outline-v5-ed2d075eb691482d90cae262f822a2ff.

Tools Used

Manual review

Consider adding the vaultsKerosene mapping as a source of assets too.

  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);
      }
+     numberOfVaults = vaultsKerosene[id].length();
+     for (uint i = 0; i < numberOfVaults; i++) {
+         Vault vault      = Vault(vaultsKerosene[id].at(i));
+         uint  collateral = vault.id2asset(id).mulWadUp(liquidationAssetShare);
+         vault.move(id, to, collateral);
+     }
      emit Liquidate(id, msg.sender, to);
  }

Assessed type

Other

#0 - c4-pre-sort

2024-04-28T10:21:53Z

JustDravee marked the issue as duplicate of #128

#1 - c4-pre-sort

2024-04-29T09:06:53Z

JustDravee marked the issue as sufficient quality report

#2 - c4-judge

2024-05-11T19:41:30Z

koolexcrypto marked the issue as satisfactory

Awards

4.8719 USDC - $4.87

Labels

bug
2 (Med Risk)
satisfactory
sponsor acknowledged
sufficient quality report
:robot:_11_group
duplicate-175

External Links

Lines of code

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L119-L131 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L156-L181 https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L184-L202

Vulnerability details

Impact

The protocol allows to create vaults and provide collateral for minting DYAD with no lower limit. As such, multiple low value positions can exist. However, there is no incentive to liquidate low value positions because of gas cost. This can lead to the DYAD undercollaterization.

Proof of Concept

Liquidators liquidate users for the profit they can make. If there is no profit to be made then there will be no one to call the liquidate function. For example a position could exist with a relatively low collateral value. This user is undercollateralized and must be liquidated in order to ensure that the protocol remains overcollateralized. If a liquidator wishes to liquidate this user, they will first need to mint some DYAD which involves gas cost. Because the value of the collateral is low, after gas costs, liquidators will not make a profit liquidating this user. In the end these low value positions will never get liquidated, leaving the protocol with bad debt and can even cause the protocol to be undercollateralized with enough small value accounts being underwater.

Tools Used

Manual review

A potential fix would be to set a minimum threshold for collateral value which has to be exceeded in order for a user to mint DYAD.

Assessed type

Other

#0 - JustDravee

2024-04-27T10:02:56Z

Will let the judge decide on the severity. 2022 it was accepted as medium on C4: https://code4rena.com/reports/2022-08-frax#m-13-no-incentives-to-write-off-bad-debt-when-remaining-collateral-is-very-small But it was also a low from ToB in 2022: https://solodit.xyz/issues/collateral-dust-prevents-the-designation-of-defaulted-loans-as-bad-debt-trailofbits-umee-pdf Known issue: https://dacian.me/lending-borrowing-defi-attacks#heading-no-incentive-to-liquidate-small-positions

To avoid bad debt, the protocol may still chose to liquidate those positions but the difficulty of the attack is too high and costly, so I'd downgrade to QA

#1 - c4-pre-sort

2024-04-27T10:03:00Z

JustDravee marked the issue as primary issue

#2 - c4-pre-sort

2024-04-29T09:17:03Z

JustDravee marked the issue as sufficient quality report

#3 - shafu0x

2024-04-29T23:26:53Z

This is a know issue. We normally would just liquidate small position ourselves.

#4 - koolexcrypto

2024-05-03T14:06:23Z

I agree, the attack seems to me too expensive, and the small positions can still be liquidated by the protocol. Marking it as QA for now. I will reconsider on PJQA If the warden provides a sufficient proof (with numbers and estimates)

#5 - c4-judge

2024-05-03T14:07:49Z

koolexcrypto changed the severity to QA (Quality Assurance)

#6 - c4-judge

2024-05-10T10:38:32Z

koolexcrypto marked the issue as grade-b

#7 - c4-judge

2024-05-12T09:31:00Z

koolexcrypto marked the issue as grade-c

#8 - c4-judge

2024-05-12T09:31:20Z

koolexcrypto marked the issue as grade-b

#9 - c4-judge

2024-05-12T09:33:45Z

koolexcrypto marked the issue as grade-c

#10 - AtanasDimulski

2024-05-15T20:40:06Z

Hello @koolexcrypto, thank you for judging the protocol so quickly. First of all, I would like to point out that it wasn't stated anywhere that this is a known vulnerability prior or during the audit. Second, I don't know why a report from ToB from 2022 was used, as a measure to determine the severity of this issue. On all other platforms like Sherlock and CodeHawks this type of issues are accepted as mediums. A few examples:

Recently such issues have been judged as valid mediums in C4 as well:

Finally, In my report 175 I have described a scenario, how this vulnerability can be exploited by a malicious actor or a group of malicious actors to place the DYAD protocol underwater and profit in the same time.

#11 - mcgrathcoutinho

2024-05-16T19:31:23Z

Hi @koolexcrypto, I agree with @AtanasDimulski's comments above.

This same issue was judged as a valid Medium in the recent Wise Lending contest as well (see here).

Thank you for your time!

#12 - koolexcrypto

2024-05-21T10:36:58Z

Hi @AtanasDimulski @mcgrathcoutinho

Thank you for your input.

After reading #175 , I believe there is a value in fixing this. Additionally, I couldn't find in the docs where it mentions this is a known issue.

According to severity-categorization, the severity should be medium.

Assets not at direct risk, but the function of the protocol or its availability could be impacted, or leak value with a hypothetical attack path with stated assumptions, but external requirements

#13 - c4-judge

2024-05-21T10:38:08Z

koolexcrypto removed the grade

#14 - c4-judge

2024-05-22T14:26:09Z

This previously downgraded issue has been upgraded by koolexcrypto

#15 - c4-judge

2024-05-22T14:28:29Z

koolexcrypto marked the issue as satisfactory

#16 - c4-judge

2024-05-28T20:06:36Z

koolexcrypto marked the issue as duplicate of #175

Awards

3.7207 USDC - $3.72

Labels

bug
2 (Med Risk)
satisfactory
sufficient quality report
:robot:_08_group
duplicate-70

External Links

Lines of code

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

Vulnerability details

Impact

Users can't use kerosene vaults in the usd total value calculation because it breaks the protocol core functionality due to the UnboundedKerosineVault.assetPrice() function invokes itself. But it can't cause asset locking since users are able to burn dyad and bypass the function while withdrawal.

Proof of Concept

Users can use kerosene tokens to overcollaterize their position when dyad minting. It can be counted as part of total usd value in the collatRatio function.

  function mintDyad(
    uint    id,
    uint    amount,
    address to
  )
    external
      isDNftOwner(id)
  {
    uint newDyadMinted = dyad.mintedDyad(address(this), id) + amount;
    if (getNonKeroseneValue(id) < newDyadMinted)     revert NotEnoughExoCollat();
    dyad.mint(id, to, amount);
    if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow();
    emit MintDyad(id, amount, to);
  }

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L156-L169 The kerosene vault has to be added to the KerosineManager contract before users will be able to add it to the vaultsKerosene mapping.

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

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L80-L91 But when a kerosine vault is added to the vaultsKerosene mapping for a dNFT the getKeroseneValue becomes DoSed due to self invoke in the UnboundedKerosineVault.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

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/Vault.kerosine.unbounded.sol#L56-L61

Tools Used

Manual review

Consider receiving collateral vaults from the Licenser contract instead of the KerosineManager contract. It also demands a similar with the KerosineManager implementation of the Licenser contract. The alternative recommendation is skipping the vault asset when it equals the kerosine token address in the UnboundedKerosineVault.assetPrice() function.

// SPDX-License-Identifier: MIT
pragma solidity =0.8.17;

import {KerosineVault}        from "./Vault.kerosine.sol";
import {IVaultManager}        from "../interfaces/IVaultManager.sol";
import {Vault}                from "./Vault.sol";
import {Dyad}                 from "./Dyad.sol";
-import {KerosineManager}      from "./KerosineManager.sol";
+import {Licenser}      from "./Licenser.sol";
import {BoundedKerosineVault} from "./Vault.kerosine.bounded.sol";
import {KerosineDenominator}  from "../staking/KerosineDenominator.sol";

import {ERC20}           from "@solmate/src/tokens/ERC20.sol";
import {SafeTransferLib} from "@solmate/src/utils/SafeTransferLib.sol";

contract UnboundedKerosineVault is KerosineVault {
  using SafeTransferLib for ERC20;

  Dyad                 public immutable dyad;
  KerosineDenominator  public kerosineDenominator;

  constructor(
      IVaultManager   _vaultManager,
      ERC20           _asset, 
      Dyad            _dyad, 
-     KerosineManager _kerosineManager
+     Licenser _licenser
- ) KerosineVault(_vaultManager, _asset, _kerosineManager) {
+ ) KerosineVault(_vaultManager, _asset, _licenser) {
      dyad = _dyad;
  }

<...>

  function assetPrice() 
    public 
    view 
    override
    returns (uint) {
      uint tvl;
-     address[] memory vaults = kerosineManager.getVaults();
+     address[] memory vaults = licenser.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;
  }
}

// SPDX-License-Identifier: MIT
pragma solidity =0.8.17;

import {KerosineVault}          from "./Vault.kerosine.sol";
import {IVaultManager}          from "../interfaces/IVaultManager.sol";
import {Dyad}                   from "./Dyad.sol";
-import {KerosineManager}      from "./KerosineManager.sol";
+import {Licenser}      from "./Licenser.sol";
import {UnboundedKerosineVault} from "./Vault.kerosine.unbounded.sol";

import {ERC20} from "@solmate/src/tokens/ERC20.sol";

contract BoundedKerosineVault is KerosineVault {
  error NotWithdrawable(uint id, address to, uint amount);

  UnboundedKerosineVault public unboundedKerosineVault;

  constructor(
    IVaultManager   _vaultManager,
    ERC20           _asset, 
-     KerosineManager _kerosineManager
+     Licenser _licenser
- ) KerosineVault(_vaultManager, _asset, _kerosineManager) {}
+ ) KerosineVault(_vaultManager, _asset, _licenser) {}

<...>
}

// SPDX-License-Identifier: MIT
pragma solidity =0.8.17;

import {IVaultManager}   from "../interfaces/IVaultManager.sol";
-import {KerosineManager}      from "./KerosineManager.sol";
+import {Licenser}      from "./Licenser.sol";
import {IVault}          from "../interfaces/IVault.sol";

import {SafeTransferLib} from "@solmate/src/utils/SafeTransferLib.sol";
import {ERC20}           from "@solmate/src/tokens/ERC20.sol";
import {Owned}           from "@solmate/src/auth/Owned.sol";

abstract contract KerosineVault is IVault, Owned(msg.sender) {
  using SafeTransferLib for ERC20;

  IVaultManager   public immutable vaultManager;
  ERC20           public immutable asset;
- KerosineManager public immutable kerosineManager;
+ Licenser public immutable licenser;

<...>

  constructor(
    IVaultManager   _vaultManager,
    ERC20           _asset, 
-     KerosineManager _kerosineManager
+     Licenser _licenser
  ) {
    vaultManager    = _vaultManager;
    asset           = _asset;
-   kerosineManager = _kerosineManager;
+   licenser = _licenser;
  }

<...>
}

Assessed type

DoS

#0 - c4-pre-sort

2024-04-27T17:22:18Z

JustDravee marked the issue as duplicate of #70

#1 - c4-pre-sort

2024-04-29T12:02:07Z

JustDravee marked the issue as sufficient quality report

#2 - c4-judge

2024-05-11T20:01:19Z

koolexcrypto marked the issue as satisfactory

Awards

4.8719 USDC - $4.87

Labels

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

External Links

Lines of code

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

Vulnerability details

Impact

Kerosene token price can be manipulated via relatively small asset withdrawal to push particular positions under MIN_COLLATERIZATION_RATIO for their liquidation.

Proof of Concept

A malicious liquidator (dNFT owner) can keep in controlled vault assets without minting dyad token. The usd value of these assets can consist of a sufficient percentage of the difference between non kerosene usd value and dyad total supply.

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

In case of these assets instant withdrawal the kerosene price will be decreased by the same percent. For the boundedKerosineVault the decrease will be doubled. The current dyad.totalSupply() is approx 622,967.4 DYAD (https://etherscan.io/token/0x305b58c5f6b5b6606fb13edd11fbdd5e532d5a26#readContract). The min overcollateralized rate is 1.5. The non kerosene usd value except for the attacker assets can be 1,000,000.0 USD. So the difference is approx 400,000.0 USD. Assume the attacker keeps 200,000.0 USD in assets. So to push the kerosene price to 1% it is enough to withdraw 6,000.0 USD in asset equivalents. The attacker can choose users with relatively low collateralization ratio and sufficient amount of kerosene token amounts, push the kerosene price down and liquidate victims positions.

Tools Used

Manual review

Consider tracking time weighted average kerosene price for the position health estimation.

Assessed type

Other

#0 - c4-pre-sort

2024-04-28T05:18:22Z

JustDravee marked the issue as duplicate of #67

#1 - c4-pre-sort

2024-04-28T05:18:25Z

JustDravee marked the issue as sufficient quality report

#2 - c4-judge

2024-05-05T09:59:11Z

koolexcrypto changed the severity to 2 (Med Risk)

#3 - c4-judge

2024-05-08T11:50:00Z

koolexcrypto marked the issue as unsatisfactory: Invalid

#4 - c4-judge

2024-05-08T12:22:56Z

koolexcrypto marked the issue as satisfactory

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