xanoutas/solidity-security-findings huggingface dataset:
Underflow in `UnboundedKerosineVault::assetPrice` will render the system inoperable upon deployment
Audit: 2024-04-dyad-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

The system will become inoperable upon deployment due to underflow in Kerosine asset price calculation.

## Description

[UnboundedKerosineVault::assetPrice()](https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/Vault.kerosine.unbounded.sol#L55-L65) contains the following fragment:

```solidity
      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();
```

In the fragment above, the total value locked (`tvl`) is calculated over all vaults licensed with `KerosineManager`. As can be seen [from the deployment script](https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/script/deploy/Deploy.V2.s.sol#L64-L65) those are the freshly created `ethVault` and `wstEth`, which balances wrt. the corresponding assets are initially zero, while the total supply of DYAD is non-zero. As a result, any attempt to calculate `assetPrice()` for the `UnboundedKerosineVault` will underflow and revert.

This function is used in particular to calculate the USD value of particular note in a Kerosine vault via [KerosineVault::getUsdValue(id)](https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/Vault.kerosine.sol#L60-L67), which in turn is used in [VaultManagerV2::getKeroseneValue()](https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L269-L286). The latter is employed via `getTotalUsdValue()` in [collatRatio()](https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L230-L239). This function reverting will in turn make functions `mintDyad()`, `withdraw`, and `liquidate` inoperable.

What's worth noticing is that the deployment uses [MAINNET_DYAD address](https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/script/deploy/Deploy.V2.s.sol#L44-L44) for the `Dyad` contract, which is already [in operation on Mainnet](https://etherscan.io/address/0x305b58c5f6b5b6606fb13edd11fbdd5e532d5a26), with [625,967.4 DYAD total supply](https://etherscan.io/token/0x305b58c5f6b5b6606fb13edd11fbdd5e532d5a26) at the time of writing.

## Tools Used

Manual audit


## Assessed type

Other
Unbounded/Bounded Kerosine vault can be used to calculate NonKeroseneValue
Audit: 2024-04-dyad-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Using kerosine vault, where normal vaults(wETH, wstETH) are, will artificially inflate protocol TVL

## Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
POC
```solidity
contract VaultManagerV2Test is Test, Parameters{
    // users
    address internal USER1 = makeAddr("user1");
    address internal USER2 = makeAddr("user2");

    uint256 internal USER1_NFT;
    uint256 internal USER2_NFT;

    // core
    DNft         dNft;
    Licenser     vaultLicenser;
    Licenser     vaultManagerLicenser;
    Dyad         dyad;
    VaultManagerV2 vaultManager;
    KerosineManager kerosineManager;
    KerosineDenominator kerosineDenominator;

    // weth
    Vault        wethVault;
    ERC20Mock    weth;
    OracleMock   wethOracle;

    // dai
    Vault        daiVault;
    ERC20Mock    dai;
    OracleMock   daiOracle;

    // kerosine
    Kerosine    kerosine;
    UnboundedKerosineVault unboundedKerosineVault;

    function setUp() public{
        // create erc20 tokens
        weth       = new ERC20Mock("WETH-TEST", "WETHT");
        wethOracle = new OracleMock(1000e8);

        dai       = new ERC20Mock("DAI-TEST", "DAIT");
        daiOracle = new OracleMock(1e8);

        vm.startPrank(Parameters.MAINNET_OWNER);
        kerosine = new Kerosine();
        kerosineDenominator = new KerosineDenominator(kerosine);

        vaultLicenser = new Licenser();
        vaultManagerLicenser = new Licenser();
        dNft = new DNft();
        dyad = new Dyad(vaultManagerLicenser);
        kerosineManager = new KerosineManager();

        vaultManager = new VaultManagerV2(dNft, dyad, vaultLicenser);
        vaultManagerLicenser.add(address(vaultManager));
        vaultManager.setKeroseneManager(kerosineManager);

        // create vaults
        wethVault = new Vault(vaultManager, ERC20(address(weth)), IAggregatorV3(address(wethOracle)));
        daiVault = new Vault(vaultManager, ERC20(address(dai)), IAggregatorV3(address(daiOracle)));
        unboundedKerosineVault = new UnboundedKerosineVault(vaultManager, ERC20(address(kerosine)), dyad, kerosineManager);
        unboundedKerosineVault.setDenominator(kerosineDenominator);

        // add vaults to licenser to be used as collateral
        vaultLicenser.add(address(wethVault));
        vaultLicenser.add(address(daiVault));
        vaultLicenser.add(address(unboundedKerosineVault));

        // add vaults used to calculate the price of unbounded kerosine vault
        kerosineManager.add(address(wethVault));
        kerosineManager.add(address(daiVault));

        USER1_NFT = dNft.mintInsiderNft(USER1);
        USER2_NFT = dNft.mintInsiderNft(USER2);

        assertEq(dNft.ownerOf(USER1_NFT), USER1);
        assertEq(dNft.ownerOf(USER2_NFT),
Wrong implement in add vault in deploy function lead to wrong price return
Audit: 2024-04-dyad-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Vulnerability details
From `run()` function, it can be seen that `ethVault` and `wstEth` are added to both `kerosineManager` and `vaultLicenser`:


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

    .  .  .  .  .  .

    vaultLicenser.add(address(ethVault));    // <--
    vaultLicenser.add(address(wstEth));      // <--
    vaultLicenser.add(address(unboundedKerosineVault));
In `VaultManagerV2()` contract, to calculate valuable of dNFT, `getTotalUsdValue()` function is used:

    function getTotalUsdValue(uint id) public view returns (uint) {
        return getNonKeroseneValue(id) + getKeroseneValue(id);  // <---
    }
Function `getNonKeroseneValue()` get value of token that added in `vaultLicenser` contract:

    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;
    }
Similar with it, `getKeroseneValue()` get value of token that added in `keroseneManager` contract:

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

Since the WETH and WstETH added 2 time, it will be counted twice, which will return incorrect price

## Impact
Price will be incorrectly returned,

## Tools Used
Manual review

## Recommended Mitigation Steps
Only add `WETH` and `WstETH` vault once.


## Assessed type

Other
Misalignment in Liquidation Handling Between Kerosene and Non-Kerosene Vaults
Audit: 2024-04-dyad-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

`VaultManagerV2` contains separate handling for kerosene and non-kerosene vaults but does not appropriately manage these distinctions during liquidations. Specifically, the liquidation function fails to account for vaults tagged as "kerosene," which means assets in these vaults are effectively exempt from liquidation. This misalignment is due to the `collatRatio` calculation considering the `totalUSDValue` from both kerosene and non-kerosene vaults, yet the actual liquidation process only targets non-kerosene vaults. This discrepancy allows users to circumvent liquidation by strategically classifying their vaults as kerosene.

## Impact
This vulnerability could lead to significant financial and operational risks:
1. **Avoidance of Asset Liquidation**: Users can prevent their assets from being liquidated by assigning them to kerosene vaults, thus exploiting a loophole to maintain control over assets that should otherwise be subject to liquidation under distressed conditions.
2. **System Integrity and Risk Management Failures**: The system’s ability to manage risk and maintain its integrity is compromised, as it fails to enforce its risk controls consistently across different asset categories.
3. **Economic Disincentives**: This loophole might encourage strategic behavior that undermines the economic mechanisms designed to stabilize and secure the platform's operations.

## Proof of Concept
Consider a scenario where a user wants to avoid liquidation:

1. The user places a significant amount of collateral into a vault classified as kerosene.
2. The user then engages in high-risk activities that would typically lead to a liquidation scenario due to low collateralization ratios.
3. Upon triggering the `liquidate` function, only non-kerosene vaults are considered for asset movement, leaving the substantial collateral in kerosene vaults untouched, despite potentially contributing to an overall risky collateralization ratio.

## Tools Used
manual review

## Recommended Mitigation Steps
Either do not account for the kerosene vault's USD value, or allow for liquidation of them.


## Assessed type

Error
Liquidation blocked
Audit: 2024-04-dyad-findings
Severity: HIGH

# 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:
```solidity

  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``:
```solidity
    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
```solidity
  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:

```solidity
  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

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


## Assessed type

Other
Withdrawal Frontrun
Audit: 2024-04-dyad-findings
Severity: HIGH

# 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:
```solidity
    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
```solidity

  function deposit(
    uint    id,
    address vault,
    uint    amount
  ) 
    external 
      isValidDNft(id)
  {
    idToBlockOfLastDeposit[id] = block.number;
```
## Tools Used
Manual Review, JosephDara

## Recommended Mitigation Steps
Remove the block check or restrict deposits to only the `` isDNftOwner(id)``


## Assessed type

DoS
`collatRatio()` may be forced to return large number, leading to forced liquidations
Audit: 2024-04-dyad-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

The `collatRatio` function calculates the collateral ratio based on the number of dyads minted for a given ID and their corresponding USD value. However, due to the exploitable behavior of the `burn` function, the dyad count (`mintedDyad`) for any user can be manipulated by any other user, potentially driving the count to zero. When it is zero, `collatRatio` returns `type(uint).max`, which represents an extremely high collateral ratio, effectively setting up the user for liquidation.

## Impact
By exploiting this vulnerability, a malicious actor (like Bob in the given scenario) can deliberately burn dyads to reduce Alice's dyad count to zero. This action would inadvertently cause the `collatRatio` function to return `type(uint).max`, allowing for Alice to be liquidated.

## Proof of Concept
Consider the scenario with Alice and Bob:

1. **Initial Setup**:
   - Alice has a substantial amount of dyads minted under her ID, properly collateralized.

2. **Bob's Malicious Action**:
   - Bob exploits the vulnerability in the `burn` function to set Alice’s dyad count to zero by burning dyads, using her ID but his own tokens.

Within `Dyad.sol`:

```
  function burn(
      uint    id,
      address from,
      uint    amount
  ) external
      licensedVaultManager
    {
      _burn(from, amount);
      mintedDyad[msg.sender][id] -= amount;
  }
```

When we burn the mintedDyad mapping is altered. If Bob burns with Alice's `id`, we can make the mintedDyad mapping 0.

3. **System's Faulty Response**:
   - With Alice’s dyad count at zero, any call to `collatRatio` for her ID returns `type(uint).max`.

Within `VaultManagerV2.sol`:

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

We are using the `mintedDyad` mapping and not her balance. This leads to an improper collatRatio.

4. **Liquidation**
   - Now, Bob may liquidated Alice


## Tools Used
manual review

## Recommended Mitigation Steps
Do not allow unauthorized users to burn dyad for the NFT owners. Simply change the modifier from isValidDNFT to isDNFTOwner.


## Assessed type

Error
Possible Reentrancy issue with the withdraw function
Audit: 2024-04-dyad-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
It is possible to re-enter the function vault.kerosine.unbounded.sol.withdraw() from the malicious attack contract’s fallback function as soon as it recieves the transfered amount.
This will again hit the attacker contract’s fallback function (with the withdrawn token amount) and repeat the flow until kerosine from contract are completely drained.
```solidity
  function withdraw(
    uint    id,
    address to,
    uint    amount
  ) 
    external 
      onlyVaultManager
  {
    id2asset[id] -= amount;
    asset.safeTransfer(to, amount); 
    emit Withdraw(id, to, amount);
  }
```

## Proof of Concept
https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/Vault.kerosine.unbounded.sol#L30C3-L42C1

## Tools Used
Vs code
## Recommended Mitigation Steps
This could be prevented by using the Re-entrancy guard from OpenZeppelin library.



## Assessed type

Reentrancy
UnboundedKerosineVault::assetPrice() will always revert
Audit: 2024-04-dyad-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

Users who deposit Kerosene into a Kerosene vault will be unable to withdraw their tokens, and the tokens will be locked in the vault in which they were deposited.

## Proof of Concept

The value of Kerosene is defined within the protocol. Part of calculating the value of Kerosene requires getting the total USD value of all exogenous collateral in the protocol (TVL)—excluding Kerosene itself. `UnboundedKerosineVault::assetPrice()` intends to calculate the asset price:

```solidity
function assetPrice() 
...
  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)) 
							...
              / (10**vault.oracle().decimals());
    }
...
}
```

The issue is that the `vaults` retrieved using `kerosineManager.getVaults()` are Kerosene vaults, not exogenous collateral vaults. The function then attempts to retrieve the oracle decimals by calling `vault.oracle().decimals()`, however since each `vault` is wrongly a Kerosene vault—and the `KerosineVault` contract does not declare an oracle as exogenous vaults do—this call will always revert.

To withdraw Kerosene from the vault manager users must call `VaultManagerV2::withdraw()`:

```solidity
function withdraw(
  uint    id,
  address vault,
  uint    amount,
  address to
) 
	...
{
	...
  Vault _vault = Vault(vault);
  uint value = amount * _vault.assetPrice() 
	...
}
```

`_vault.assetPrice()` is called within this function, and since `UnboundedKerosineVault::assetPrice()` always reverts users will be unable to withdraw their Kerosene.

Important notes:

1. If a dNFT’s position faces liquidation it will not be possible to liquidate this position if the dNFT has assets deposited in a Kerosene vault. This is because during `VaultManagerV2::liquidate()`, `vault.getUsdValue` is called—which calls `assetPrice()`:

```solidity
function getUsdValue(
..
)
..
  returns (uint) {
    return id2asset[id] * assetPrice() / 1e8;
}
```

2. Users will be unable to redeem their DYAD for Kerosene as `assetPrice()` is called in `VaultManagerV2::redeemDyad()`.

### Test

Steps to run:

1. Create a file in `2024-04-dyad/test` and name it `VaultManagerV2.t.sol`
2. Pass your RPC URL to `createSelectFork()`
3. Run `forge t --mp test/VaultManagerV2.t.sol -vvvvv`

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.17;

import { Test } from "forge-std/Test.sol";
import { VaultManagerV2 } from "../src/core/VaultManagerV2.sol";
import { DNft } from "../src/core/DNft.sol";
import { Dyad } from "../src/core/Dyad.sol";
import { Licenser } from "../src/core/Licenser.sol";
import { KerosineManager } from "../src/core/KerosineMan