DYAD - KupiaSec'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: 33/183

Findings: 7

Award: $332.07

🌟 Selected for report: 0

🚀 Solo Findings: 0

Lines of code

https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/Deploy.V2.s.sol#L95

Vulnerability details

Impact

In the DeployV2.run() function, the unboundedKerosineVault is added into the vaultLicenser, so the malicious DNftOwner can add the unboundedKerosineVault into vaults[id]. If the unboundedKerosineVault is added into the vaults[id], the getNonKeroseneValue() function is miscalculated. The malicious DNftOwner can mint more dyad tokens and withdraw vault tokens than actual amount. Also, the liquidation cannot be done fairly. Consequently, dyad token cannot stay stablity.

  • DNftOwner can mint more dyad tokens and withdraw vault tokens than actual amount.
  • The liquidation cannot be done fairly.
  • Consequently, dyad token cannot stay stablility.

Proof of Concept

In the DeployV2.run() function, unboundedKerosineVault is added into vaultLicenser from L95.

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

File: script\deploy\Deploy.V2.s.sol
95:     vaultLicenser.add(address(unboundedKerosineVault));

So the malicious DNftOwner can add unboundedKerosineVault into vaults[id] from L76.

https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/VaultManagerV2.sol#L75~L76

File: src\core\VaultManagerV2.sol
75:     if (!vaultLicenser.isLicensed(vault))  revert VaultNotLicensed();
76:     if (!vaults[id].add(vault))            revert VaultAlreadyAdded();

If malicious DNftOwner adds unboundedKerosineVault into vaults[id], the getNonKeroseneValue() function calculates USD value of real non-kerosene vaults and unboundedKerosineVault.

https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/VaultManagerV2.sol#L250~L267

File: src\core\VaultManagerV2.sol
250:   function getNonKeroseneValue(
251:     uint id
252:   ) 
253:     public 
254:     view
255:     returns (uint) {
256:       uint totalUsdValue;
257:       uint numberOfVaults = vaults[id].length(); 
258:       for (uint i = 0; i < numberOfVaults; i++) {
259:         Vault vault = Vault(vaults[id].at(i));
260:         uint usdValue;
261:         if (vaultLicenser.isLicensed(address(vault))) {
262:           usdValue = vault.getUsdValue(id);        
263:         }
264:         totalUsdValue += usdValue;
265:       }
266:       return totalUsdValue;
267:   }

Thus, the return value of the getNonKeroseneValue() function is greater than real non-kerosene value. This makes the collatRatio greater than actual value. As a result, the malicious DNftOwner can mint more dyad tokens and withdraw vault tokens than actual amount. Also, the liquidation cannot be done fairly. Consequently, the dyad token cannot stay stable.

Tools Used

Manual Review

In the DeployV2.run() function, kerosine vaults must not be added into vaultLicenser.

Assessed type

Other

#0 - c4-pre-sort

2024-04-29T05:23:17Z

JustDravee marked the issue as duplicate of #966

#1 - c4-pre-sort

2024-04-29T08:37:10Z

JustDravee marked the issue as sufficient quality report

#2 - c4-judge

2024-05-04T09:46:24Z

koolexcrypto marked the issue as unsatisfactory: Invalid

#3 - c4-judge

2024-05-29T11:19:58Z

koolexcrypto marked the issue as duplicate of #1133

#4 - c4-judge

2024-05-29T14:03:55Z

koolexcrypto marked the issue as satisfactory

Lines of code

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

Vulnerability details

Impact

Attackers can delay any withdrawing and vault removing by depositing 1 wei.

Proof of Concept

At L143 of VaultManagerV2.withdraw(), there is a block.number check.

    function withdraw(
      uint    id,
      address vault,
      uint    amount,
      address to
    ) 
      public
        isDNftOwner(id)
    {
143   if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock();
      uint dyadMinted = dyad.mintedDyad(address(this), id);
      Vault _vault = Vault(vault);
      uint value = amount * _vault.assetPrice() 
                    * 1e18 
                    / 10**_vault.oracle().decimals() 
                    / 10**_vault.asset().decimals();
      if (getNonKeroseneValue(id) - value < dyadMinted) revert NotEnoughExoCollat();
      _vault.withdraw(id, to, amount);
      if (collatRatio(id) < MIN_COLLATERIZATION_RATIO)  revert CrTooLow(); 
    }

When someone tries withdrawing from his id, the attacker can set idToBlockOfLastDeposit[id] to the current block number by front-depositing 1 wei to that id, results in revert the withdrawal.

    function deposit(
      uint    id,
      address vault,
      uint    amount
    ) 
      external 
        isValidDNft(id)
    {
127   idToBlockOfLastDeposit[id] = block.number;
      Vault _vault = Vault(vault);
      _vault.asset().safeTransferFrom(msg.sender, address(vault), amount);
      _vault.deposit(id, amount);
    }

This DOS attack can be conducted to redeemDyad(), remove() and removeKerosene().

Tools Used

Manual review

Deposit should be allowed to only the owner of DNFT token.

    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:31:05Z

JustDravee marked the issue as duplicate of #1103

#1 - c4-pre-sort

2024-04-27T11:45:48Z

JustDravee marked the issue as duplicate of #489

#2 - c4-pre-sort

2024-04-29T09:29:33Z

JustDravee marked the issue as sufficient quality report

#3 - c4-judge

2024-05-05T20:38:11Z

koolexcrypto marked the issue as unsatisfactory: Invalid

#4 - c4-judge

2024-05-05T20:46:29Z

koolexcrypto marked the issue as nullified

#5 - c4-judge

2024-05-05T20:46:46Z

koolexcrypto marked the issue as not nullified

#6 - c4-judge

2024-05-08T15:29:16Z

koolexcrypto marked the issue as duplicate of #1001

#7 - c4-judge

2024-05-11T19:51:11Z

koolexcrypto marked the issue as satisfactory

#8 - c4-judge

2024-05-13T18:34:30Z

koolexcrypto changed the severity to 3 (High Risk)

Awards

32.4128 USDC - $32.41

Labels

bug
3 (High Risk)
satisfactory
sufficient quality report
:robot:_28_group
duplicate-397

External Links

Lines of code

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

Vulnerability details

Impact

Users can't withdraw kerosene and withdrawing from unadded vaults could be unfairly reverted.

Proof of Concept

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

    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 
148                 / 10**_vault.oracle().decimals() 
                    / 10**_vault.asset().decimals();
150   if (getNonKeroseneValue(id) - value < dyadMinted) revert NotEnoughExoCollat();
      _vault.withdraw(id, to, amount);
      if (collatRatio(id) < MIN_COLLATERIZATION_RATIO)  revert CrTooLow(); 
    }

If the vault is a kerosene vault, VaultManagerV2.withdraw() will be reverted at L148 because there is no oracle in kerosene vaults. And, if the vault was not added to id, value is not included to getNonKeroseneValue(id), so getNonKeroseneValue(id) should not be subtracted by value at L150. So L150 could result in a reversal of the transaction.

Tools Used

Manual review

The collateral check should be improved.

    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();
+       uint value;
+       if (vaults[id].contains(vault) && vaultLicenser.isLicensed(address(vault)))
+       {
+           value = amount * _vault.assetPrice()
+                   * 1e18 
+                   / 10**_vault.oracle().decimals() 
+                   / 10**_vault.asset().decimals();
+       }

        if (getNonKeroseneValue(id) - value < dyadMinted) revert NotEnoughExoCollat();
        _vault.withdraw(id, to, amount);
        if (collatRatio(id) < MIN_COLLATERIZATION_RATIO)  revert CrTooLow(); 
    }

Assessed type

Other

#0 - c4-pre-sort

2024-04-26T21:40:08Z

JustDravee marked the issue as duplicate of #397

#1 - JustDravee

2024-04-26T21:40:09Z

Double dup on 397 and 1048. Should these be grouped together? Duping with 397 for now

#2 - c4-pre-sort

2024-04-29T08:48:16Z

JustDravee marked the issue as sufficient quality report

#3 - koolexcrypto

2024-05-08T09:26:49Z

Will leave it as dup for 397

Advice for the warden, please report this as two issues next time. Generally speaking, Two different root causes => two separate issue

#4 - c4-judge

2024-05-11T19:22:53Z

koolexcrypto marked the issue as satisfactory

Findings Information

Labels

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

Awards

283.3687 USDC - $283.37

External Links

Lines of code

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

Vulnerability details

Impact

Anyone can prevent liquidation.

Proof of Concept

There is only collateral ratio check at L214 in VaultManagerV2.liquidate().

    function liquidate(
        uint id,
        uint to
    ) 
        external 
        isValidDNft(id)
        isValidDNft(to)
        {
        uint cr = collatRatio(id);
214     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);
    }

But, anyone who is facing liquidation can deposit a large amount of collateral which results in the increasing of the kerosene price. Consequently, he can increase collateral ratio increase enough to avoid liquidation. Then, even though there is a less collateral than dyad in id, that id avoids liquidation.

Tools Used

Manual review

The liquidation check should be improved.

    function liquidate(
      uint id,
      uint to
    ) 
      external 
        isValidDNft(id)
        isValidDNft(to)
      {
        uint cr = collatRatio(id);
-       if (cr >= MIN_COLLATERIZATION_RATIO) revert CrTooHigh();
+       if (cr >= MIN_COLLATERIZATION_RATIO && getNonKeroseneValue(id) >= dyad.mintedDyad(address(this), id)) 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);
    }

Assessed type

Other

#0 - c4-pre-sort

2024-04-28T10:22:23Z

JustDravee marked the issue as duplicate of #128

#1 - c4-pre-sort

2024-04-29T09:03:31Z

JustDravee marked the issue as sufficient quality report

#2 - c4-judge

2024-05-08T09:40:22Z

koolexcrypto marked the issue as not a duplicate

#3 - c4-judge

2024-05-08T09:40:34Z

koolexcrypto marked the issue as duplicate of #338

#4 - c4-judge

2024-05-11T12:20:17Z

koolexcrypto marked the issue as satisfactory

Awards

3.8221 USDC - $3.82

Labels

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

External Links

Lines of code

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

Vulnerability details

Impact

The asset price of unbounded kerosene vaults is calculated using the degree of DYAD’s overcollateralization, tvl - dyad.totalSupply(). If undercollateralization occurs, the subtract operation is reverted. This makes the calling of assetPrice() function reverted, so liquidation cannot be done.

In case of undercollateralization like sudden price drop of collateral assets, liquidation cannot be done.

Proof of Concept

In the Vault.kerosine.unbounded.assetPrice() function, the asset price of unbounded kerosene vaults is calculated using the degree of DYAD’s overcollateralization from L65.

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

File: src\core\Vault.kerosine.unbounded.sol
58:       for (uint i = 0; i < numberOfVaults; i++) {
59:         Vault vault = Vault(vaults[i]);
60:         tvl += vault.asset().balanceOf(address(vault)) 
61:                 * vault.assetPrice() * 1e18
62:                 / (10**vault.asset().decimals()) 
63:                 / (10**vault.oracle().decimals());
64:       }
65:       uint numerator   = tvl - dyad.totalSupply();
66:       uint denominator = kerosineDenominator.denominator();

Here, tvl is the total USD value of kerosineManager's vaults. If the asset price of kerosineManager's vaults drops, tvl becomes smaller than dyad.totalSupply(). This leads the subtract operation to be reverted from L65. Finally, in the case of undercollateralization, nobody can not call Vault.kerosine.unbounded.assetPrice() function. However anyone must be allowed to liquidate that DNft.

In the VaultManagerV2, calculating collatRatio needs to call Vault.kerosine.unbounded.assetPrice() function.

https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/VaultManagerV2.sol#L230

File: src\core\VaultManagerV2.sol
230:   function collatRatio(
231:     uint id
232:   )
233:     public 
234:     view
235:     returns (uint) {
236:       uint _dyad = dyad.mintedDyad(address(this), id);
237:       if (_dyad == 0) return type(uint).max;
238:       return getTotalUsdValue(id).divWadDown(_dyad);
239:   }
240: 
241:   function getTotalUsdValue(
242:     uint id
243:   ) 
244:     public 
245:     view
246:     returns (uint) {
247:       return getNonKeroseneValue(id) + getKeroseneValue(id);
248:   }
       [...]
269:   function getKeroseneValue(
270:     uint id
271:   ) 
272:     public 
273:     view
274:     returns (uint) {
275:       uint totalUsdValue;
276:       uint numberOfVaults = vaultsKerosene[id].length(); 
277:       for (uint i = 0; i < numberOfVaults; i++) {
278:         Vault vault = Vault(vaultsKerosene[id].at(i));
279:         uint usdValue;
280:         if (keroseneManager.isLicensed(address(vault))) {
281:           usdValue = vault.getUsdValue(id);        
282:         }
283:         totalUsdValue += usdValue;
284:       }
285:       return totalUsdValue;
286:   }

Calculating the collatRatio() function from L213 is always reverted.

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

File: src\core\VaultManagerV2.sol
205:   function liquidate(
206:     uint id,
207:     uint to
208:   ) 
209:     external 
210:       isValidDNft(id)
211:       isValidDNft(to)
212:     {
213:       uint cr = collatRatio(id);
           [...]
228:   }

Consequently, liquidation cannot be done.

Tools Used

Manual Review

File: src\core\Vault.kerosine.unbounded.sol
  function assetPrice() 
    public 
    view 
    override
    returns (uint) {
      uint tvl;
      address[] memory vaults = kerosineManager.getVaults();
      uint numberOfVaults = vaults.length;
      for (uint i = 0; i < numberOfVaults; i++) {
        Vault vault = Vault(vaults[i]);
        tvl += vault.asset().balanceOf(address(vault)) 
                * vault.assetPrice() * 1e18
                / (10**vault.asset().decimals()) 
                / (10**vault.oracle().decimals());
      }
+     if (tvl < dyad.totalSupply())
+         return 0;
      uint numerator = tvl - dyad.totalSupply();
      uint denominator = kerosineDenominator.denominator();
      return numerator * 1e8 / denominator;
  }

Assessed type

Other

#0 - c4-pre-sort

2024-04-28T19:29:35Z

JustDravee marked the issue as duplicate of #224

#1 - c4-pre-sort

2024-04-29T09:04:37Z

JustDravee marked the issue as sufficient quality report

#2 - c4-judge

2024-05-08T08:31:59Z

koolexcrypto marked the issue as duplicate of #308

#3 - c4-judge

2024-05-11T20:09:36Z

koolexcrypto marked the issue as satisfactory

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/Vault.kerosine.unbounded.sol#L60-L64

Vulnerability details

Impact

In liquidation, the liquidator burns a quantity of DYAD equal to the target Note’s DYAD minted balance, and in return receives an equivalent value plus a 20% bonus of the target Note’s backing colateral.(check here) However, since Kerosene used as collateral in the target Note are not moved to the liquidator, it discourages liquidation and the liqudator migth receive less incentives even when the target Note's collateral ratio is bigger than 1.

Proof of Concept

When a liqudator liquidates the target Note, he burns a quantity of DYAD equal to the target Note’s DYAD minted balance and in return receives an equal value plus a 20% bonus of the target Note's backing collateral.

But in the VaultManagerV2.liquidate() function, only the Non-Kerosine part of collateral is directed at L221.

File: 2024-04-dyad\src\core\VaultManagerV2.sol
205:   function liquidate(
206:     uint id,
207:     uint to
208:   ) 
209:     external 
210:       isValidDNft(id)
211:       isValidDNft(to)
212:     {
213:       uint cr = collatRatio(id);
214:       if (cr >= MIN_COLLATERIZATION_RATIO) revert CrTooHigh();
215:       dyad.burn(id, msg.sender, dyad.mintedDyad(address(this), id));
216: 
217:       uint cappedCr               = cr < 1e18 ? 1e18 : cr;
218:       uint liquidationEquityShare = (cappedCr - 1e18).mulWadDown(LIQUIDATION_REWARD);
219:       uint liquidationAssetShare  = (liquidationEquityShare + 1e18).divWadDown(cappedCr);
220: 
221:       uint numberOfVaults = vaults[id].length(); // @audit-info Only non-Kerosene collaterals are considered.
222:       for (uint i = 0; i < numberOfVaults; i++) {
223:           Vault vault      = Vault(vaults[id].at(i));
224:           uint  collateral = vault.id2asset(id).mulWadUp(liquidationAssetShare);
225:           vault.move(id, to, collateral);
226:       }
227:       emit Liquidate(id, msg.sender, to);
228:   }

As Kerosene is used as the collaterals, in the liquidation process, the asset of KeroseneVaults should be moved to the liquidator.

Scenario of the issue

Assume Alice is going to liquidate Bob's Note. Let Bob's Note is as follows.

Vaultid2Asset(1e18)Price(1e18)UsdValue(1e18)Sum(1e18)
wETH103111.1231111.2
wstETH103116.9431169.462280.6
Kerosine.unbound500105000
Kerosin.bound02005000
67280.6
mintedDYAD60000160000

In this case, his collateral ratio is larger than 100 % but smaller than 150%.

At L213-L219, the variables cr, cappedCr, liquidationEquityShare, liquidationAssetShare are calculated as below.

  • cr = 62280.6 / 6000 = 1.1213 * 1e18
  • cappedCr = max(1, cr) = 1.1213 * 1e18
  • liquidationEquityShare = (cappedCr - 1e18).mulWadDown(0.2e18) = 0.0242 * 1e18
  • liquidationAssetShare = (liquidationEquityShare + 1e18).divWadDown(cappedCr) = 0.9134 * 1e18

Then, the UsdValue of insentive of liquidator is calculated like below.

VaultMoved Asset(1e18)Price(1e18)UsdValue(1e18)Sum(1e18)
wETH9.13433111.1228417.90413
wstETH9.13433116.9428471.06576
Kerosine.unbound0100
Kerosin.bound0200
56888.96989

To sum up, the liquidator burned DYAD of $60000 but received only $56888.97 as incentive. Although Alice liquidated Bob's Note, she received less incentive than her waste. This is not fair for liquidators who contributes to enhance the security of the protocol.

The reason behind this is that the protocol allowed to move Non-Kerosene assets only in liquidation process although Kerosene was used to calculate Bob's collateral ratio. If the protocol allows to move Kerosene, it will be fair. It is revealed below.

VaultMoved Asset(1e18)Price(1e18)UsdValue(1e18)Sum(1e18)
wETH9.13433111.1228417.90413
wstETH9.13433116.9428471.06576
Kerosine.unbound456.715104567.15
Kerosin.bound0200
61456.12

But still, if collateral ratio drops below 1, the liqudator always receive less incentives than the amount she wasted to liquidate.

Tools Used

Manual Review

In VaultManagerV2.liquidate(), assets of vaultsKerosene should be moved to the liquidator.

File: 2024-04-dyad\src\core\VaultManagerV2.sol
205:   function liquidate(
206:     uint id,
207:     uint to
208:   ) 
209:     external 
210:       isValidDNft(id)
211:       isValidDNft(to)
212:     {
213:       uint cr = collatRatio(id);
214:       if (cr >= MIN_COLLATERIZATION_RATIO) revert CrTooHigh();
215:       dyad.burn(id, msg.sender, dyad.mintedDyad(address(this), id));
216: 
217:       uint cappedCr               = cr < 1e18 ? 1e18 : cr;
218:       uint liquidationEquityShare = (cappedCr - 1e18).mulWadDown(LIQUIDATION_REWARD);
219:       uint liquidationAssetShare  = (liquidationEquityShare + 1e18).divWadDown(cappedCr);
220: 
221:       uint numberOfVaults = vaults[id].length();
222:       for (uint i = 0; i < numberOfVaults; i++) {
223:           Vault vault      = Vault(vaults[id].at(i));
224:           uint  collateral = vault.id2asset(id).mulWadUp(liquidationAssetShare);
225:           vault.move(id, to, collateral);
226:       }

+          uint 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);
+          }
227:       emit Liquidate(id, msg.sender, to);
228:   }

Assessed type

Other

#0 - c4-pre-sort

2024-04-28T10:22:10Z

JustDravee marked the issue as duplicate of #128

#1 - c4-pre-sort

2024-04-29T09:03:32Z

JustDravee marked the issue as sufficient quality report

#2 - c4-judge

2024-05-11T19:39:44Z

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#L60-L64

Vulnerability details

Impact

A malicious attacker can manipulate the price of Kerosene by depositing or transfering large amount of Non-Kerosene tokens to the vaults.

  • Depositing or transfering Non-Kerosene tokens to the vaults increases the tvl of the protocol and also the price of Kerosene. It makes it difficult to liquidate the Note with KerosenVaults due to the rise of collateral ratio.
  • The attacker withdraws all deposited tokens when the price of the tokens decreases. As a result, the price of Kerosene decreases and he can place the Notes with KeroseneVaults in the liquidation state. The attacker liquidates the Notes and can earn yields from the liquidation.

Proof of Concept

The price of Kerosene is calculated from the Vault.kerosine.unbounded.assetPrice() function,

File: 2024-04-dyad\src\core\Vault.kerosine.unbounded.sol
50:   function assetPrice() 
51:     public 
52:     view 
53:     override
54:     returns (uint) {
55:       uint tvl;
56:       address[] memory vaults = kerosineManager.getVaults();
57:       uint numberOfVaults = vaults.length;
58:       for (uint i = 0; i < numberOfVaults; i++) {
59:         Vault vault = Vault(vaults[i]);
60:         tvl += vault.asset().balanceOf(address(vault)) //@audit-info avoid to use balanceOf(address(vault))
61:                 * vault.assetPrice() * 1e18
62:                 / (10**vault.asset().decimals()) 
63:                 / (10**vault.oracle().decimals());
64:       }
65:       uint numerator   = tvl - dyad.totalSupply();
66:       uint denominator = kerosineDenominator.denominator();
67:       return numerator * 1e8 / denominator;
68:   }

First, it calculates tvl of the protocol. Here, it uses the balance of vault at L60.

An attacker can manipulate the balance of vault by transfering large amount of Non-Kerosine tokens to it directly via ERC20.Safetransfer or depositing via VaultManagerV2.deposit()

File: 2024-04-dyad\src\core\VaultManagerV2.sol
119:   function deposit(
120:     uint    id,
121:     address vault,
122:     uint    amount
123:   ) 
124:     external 
125:       isValidDNft(id)
126:   {
127:     idToBlockOfLastDeposit[id] = block.number;
128:     Vault _vault = Vault(vault);
129:     _vault.asset().safeTransferFrom(msg.sender, address(vault), amount);
130:     _vault.deposit(id, amount);
131:   }

It makes it difficult to liquidate the Note with KeroseneVaults. When tvl decreases due to price fluctuation, he withdraws deposited assets and place Note with KeroseneVaults in the liqudation state. Then he can earns yields by liquidating all available Notes

Scenario of the issue

Alice is going to manipulate the price of Kerosene and wants to earn yields.

  • She mints Note by calling DNft.mintNft().
  • She never mints Dyad from the Note, as a result, her Note's collateral ratio is uint.max and she can withdraw her Note's collateral assets at anytime as much as she want.
  • She deposits 10000 ether of WETH to ethVault via the minted Note. This increases the balance of WETH of ethVault, tvl of the protocol. Consequencely, the price of Kerosene increases.
  • First, this makes it difficult to liquidate the Notes with KeroseneVaults balance as their collateral ratios rise.
  • When the price of WETH decreases, she decided to withdraw all deposited WETH. It results the decrease of the price of Kerosene, the decrease of the collateral ratio of the Notes with KeroseneVaults. Then, the Notes with KeroseneVaults are placed in the liquidation state.
  • She liquidates the Notes and earn yields from liquidation.

Tools Used

Manual Review

We recommend the to avoid using KeroseneVaults in the Notes.

And we also recommed to use internal accounting for collaterals.

Assessed type

Other

#0 - c4-pre-sort

2024-04-28T05:50:09Z

JustDravee marked the issue as duplicate of #67

#1 - c4-pre-sort

2024-04-29T09:06:12Z

JustDravee marked the issue as sufficient quality report

#2 - c4-judge

2024-05-05T09:59:12Z

koolexcrypto changed the severity to 2 (Med Risk)

#3 - c4-judge

2024-05-08T11:50:03Z

koolexcrypto marked the issue as unsatisfactory: Invalid

#4 - c4-judge

2024-05-08T12:02:35Z

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