xanoutas/solidity-security-findings huggingface dataset:
An attacker can perform a DoS attack by depositing into a dNFT’s vault
Audit: 2024-04-dyad-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L127


# Vulnerability details

## Impact

Users subject to this attack will be unable to call the following functions in `VaultManagerV2`:

1. `withdraw()`
2. `redeemDyad()`
3. `remove()`
4. `removeKerosene()`

This will persist for as long as the attacker wishes to repeatedly perform the exploit—given the low cost of the attack, this extends the likelihood of a user being denied service for longer.

## Proof of Concept

This vulnerability can be divided into two parts: 1. a DoS attack on withdrawing collateral and redeeming DYAD, and 2. a DoS attack on removing vaults.

### DoS attack on withdrawing collateral and redeeming DYAD

When a user calls `VaultManagerV2::deposit()` the `idToBlockOfLastDeposit` mapping is updated:

```solidity
function deposit(
	...
		isValidDNft(id)
) 
	...
{
  idToBlockOfLastDeposit[id] = block.number;
	...
}
```

`idToBlockOfLastDeposit` maps a dNFT’s token ID to the `block.number` in which the last deposit was made for that dNFT.

```solidity
mapping (uint => uint) public idToBlockOfLastDeposit;
```

This is done so that the following check can be made in `VaultManagerV2::withdraw()` to prevent flash loan attacks:

```solidity
function withdraw(
	...
) 
	...
{
  if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock();
	...
}
```

The issue arises due to the fact that anyone can deposit collateral into a dNFT’s vault, even if they do not own that dNFT. An attacker can front run a user’s withdrawal and deposit into their dNFT’s vault—updating `idToBlockOfLastDeposit` and causing the above check to fail.

Note: this also affects `VaultManagerV2::redeemDyad()` as `withdraw()` is called within the function. An attacker does not have to deposit any assets when front running as they can pass 0 as `amount` to `deposit()`.

### DoS attack on removing vaults

When removing a vault from a dNFT, a check is made to ensure there are no assets in the vault:

```solidity
function remove(
		...
) 
	...
{
  if (Vault(vault).id2asset(id) > 0) revert VaultHasAssets();
	...
}

function removeKerosene(
		...
) 
	...
{
  if (Vault(vault).id2asset(id) > 0) revert VaultHasAssets();
	...
}
```

An attacker can front run a user’s call to `remove()` or `removeKerosene()` and deposit a dust amount of collateral or Kerosene into the vault they’re trying to remove—causing the above checks to fail.

### Test

The following test demonstrates both parts of the vulnerability. 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`

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.17;

import { Test } from "forge-std/Test.sol";
import { ERC20 } from "@s
User token withdraws from vaults can be griefed
Audit: 2024-04-dyad-findings
Severity: MEDIUM

# 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#L119-L131


# Vulnerability details

## Impact
The [withdraw()](https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/VaultManagerV2.sol#L134-L153) function in implements the following check:
```solidity
if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock()
```
not allowing withdraws in the same block, in which someone has already deposited into a so called Note. As can be seen from the [deposit()](https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/VaultManagerV2.sol#L119-L131) function:

```solidity
  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);
  }
```
There are no minimum deposits required, so a malicious actor can frontrun the Note owner call to [withdraw()](https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/VaultManagerV2.sol#L134-L153), deposit as little as **1 WEI** and the withdraw function will revert, thus griefing the Note owner. A malicious actor can do this as many times as he wants, and the Note owner won't be able to withdraw his deposited tokens. In a turbulent market conditions (which are a typical occurrence in crypto), a Note owner may have decided that he would like to convert his crypto assets to fiat currency, already burned all of his minted DYAD tokens in order to free all of his collateral. However as he is trying to withdraw all of his deposited collateral, a malicious actor can grief him as described above, while the deposited collateral falls in value. Thus resulting in immense losses for the owner of the Note. The same griefing attack can be performed when a user is trying to remove a vault from his ``vaults``, or ``vaultsKerosene`` lists. For example he has liquidated an undercollateralized position of somebody, he already has 5 vaults in his ``vaults`` list but a big portion of the collateral he received as a reward are in a vault he hasn't added to his ``vaults`` list. In order to withdrawn them he will have to remove one vault and add another, however a malicious user can keep depositing 1 WEI, thus restricting this functionality. Considering when a positions gets liquidated, it is typically because the collateral asset fell in dollar value, and is probably going to continue falling in price due to some turbulent market conditions, this may result in a big loss for users. 

## Tools Used
Manual review

## Recommended Mitigation Steps
Consider allowing only the DNFT Owner to be able to deposit assets via the [deposit()](https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/VaultManagerV2.sol#L119-L131) function, set the mod
Attacker can make 0 value deposit() calls to deny user from redeeming or withdrawing collateral 
Audit: 2024-04-dyad-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L125


# Vulnerability details

## Summary

**Root cause:**
Function [deposit()](https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L119) uses modifier [isValidDNft()](https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L42) instead of [isDNftOwner()](https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L39), which allows anyone to call [deposit()](https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L119) on behalf of any DNft id.

**Impacts:**
1. Attacker can make user devoid of withdrawing or redeeming collateral by making 0 value [deposit()](https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L119) calls. Other than denying users from temporarily withdrawing their collateral, this is also an issue since it could force users into liquidations when they try to take preventative measures on their collateral ratio (especially through [redeemDyad()](https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L184)) in high collateral price volatility situations. If successful, the attacker could then perform the liquidation to profit from the situation.
2. Attacker can make user devoid of removing vault due to id2asset > 0 by depositing 1 wei of collateral.

## Proof of Concept

**Let's understand the first impact:**

1. User calls [redeemDyad()](https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L184) (or [withdraw()](https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L134) to directly withdraw collateral) to burn DYAD and withdraw their collateral asset.

2. Attacker frontruns the call with a 0 value [deposit()](https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/VaultManagerV2.sol#L119) call. This would set the idToBlockOfLastDeposit[id] to the current block.number. The call does not revert since 0 value transfers are allowed.
```solidity
File: VaultManagerV2.sol
127:   function deposit(
128:     uint    id,
129:     address vault,
130:     uint    amount
131:   ) 
132:     external 
133:       isValidDNft(id)
134:   {
135:     idToBlockOfLastDeposit[id] = block.number;
136:     Vault _vault = Vault(vault);
137:     _vault.asset().safeTransferFrom(msg.sender, address(vault), amount);
138:     _vault.deposit(id, amount);
139:   }
```

3. When the user's [redeemDyad()](https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58
Upgraded Q -> 2 from #1011 [1716984547522]
Audit: 2024-04-dyad-findings
Severity: MEDIUM

Judge has assessed an item in Issue #1011 as 2 risk. The relevant finding follows:

 [L-06] Deployment script deploys bounded kerosine without calling setUnboundedKerosineVault
Issue Description:

Deployment of the BoundedKerosineVault happens without calling setUnboundedKerosineVault, that will eventually make the price of the asset 0, when admin license it and users begin to use it. The consequences will be that it won’t support depositor’s collateral rate and will limit their minting capabilities.

DeployV2.sol#L78-L82

 BoundedKerosineVault boundedKerosineVault     = new BoundedKerosineVault(
      vaultManager,
      Kerosine(MAINNET_KEROSENE), 
      kerosineManager
    );
Recommendation:

Despite that it is not going to be used for now, call the setUnboundedKerosineVault in the script as well.
Upgraded Q -> 2 from #1121 [1716977938594]
Audit: 2024-04-dyad-findings
Severity: MEDIUM

Judge has assessed an item in Issue #1121 as 2 risk. The relevant finding follows:

 [L-02] No minimum threshold for positions makes so small positions unprofitable to get liquidated
the owners of the dNFT can mint DYAD by providing collateral equal to 150%. and if his position gets undercollateralized. people can liquidate him and get a reward for that.

But there is no minimum minting limit for the position. So if the NFT owner made a position with a small amount, and after time this position gets undercollateralized. it can be unprofitable to liquidate that position as the return may be too little compared to the gas that the liquidator will pay for the calling.

POC
The owner deposited an amount of ETH.
The user minted some small DYAD tokens.
The user removed most of his collateral but made an amount to make his position overcollateralized.
since he mints small DYAD amounts he only needs to keep a small amount as collateral.
The price of the collateral changes and the position is undercollateralized now.
No one wants to liquidate that position, as he will pay a lot for gas compared to the value he will gain.
VaultManagerV2.sol#L156-L169

  // @audit no minimum amount to mint
  function mintDyad( ... ) 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);
  }
This can introduce issues where some positions can be undercollateralized, which is not a good thing for the protocol and coin stability, and there is no incentivize to liquidate the position.

Recommendations
Make a minimum amount for minting new DYAD when calling VaultManagerV2::mintDyad().
Make the burning failed if the position goes below the threshold you configured for minting when calling VaultManagerV2::burnDyad() .
Upgraded Q -> 2 from #1121 [1716977907927]
Audit: 2024-04-dyad-findings
Severity: MEDIUM

Judge has assessed an item in Issue #1121 as 2 risk. The relevant finding follows:

 [L-01] BoundedKerosineVault::setUnboundedKerosineVault is not fired when deploying, leading to DoS when they are added.
For BoundedKerosineVault to return the assetPrice, it depends on the UnBoundedkerosineVault asset price, and multiply it by 2.

Vault.kerosine.bounded.sol#L44-L50

  function assetPrice() 
    public 
    view 
    override
    returns (uint) {
      return unboundedKerosineVault.assetPrice() * 2;
  }
And unboundedKerosineVault should be set by the owner of the contract.

Vault.kerosine.bounded.sol#L23-L30

  function setUnboundedKerosineVault(
    UnboundedKerosineVault _unboundedKerosineVault
  )
    external
    onlyOwner
  {
    unboundedKerosineVault = _unboundedKerosineVault;
  }
In Deploy.V2.s.sol, the vault is not set before transferring the ownership from the Deployer to the MAINNET_OWNER (multi-sig). which makes adding this vault leads to reverting when calling different functions in the VaultManagerV2, and DoS the protocol.

Deploy.V2.s.sol#L78-L91

  function run() public returns (Contracts memory) {
    ...

    UnboundedKerosineVault unboundedKerosineVault = new UnboundedKerosineVault( ... );

    BoundedKerosineVault boundedKerosineVault     = new BoundedKerosineVault( ... );

    KerosineDenominator kerosineDenominator       = new KerosineDenominator( ... );

    unboundedKerosineVault.setDenominator(kerosineDenominator);

    // @audit we did not set the unBoundedVault in `boundedKerosineVault` before transfering the ownership

    unboundedKerosineVault.transferOwnership(MAINNET_OWNER);
    boundedKerosineVault.  transferOwnership(MAINNET_OWNER);
}
PoC
The protocol Deployed into the mainnet.
The ownership of the boundedKerosineVault transferred to multi-sig.
boundedKerosineVault was added as licensed vaults.
The owner of the dNFT owners added that vault.
VaultManagerV2::getNonKeroseneValue, VaultManagerV2::getKeroseneValue, and VaultManagerV2::getTotalUsdValue will revert when getting called, as they call getUsdValue which calls assetPrice, which will make a call to the address(0), and it will revert.
If the vault is added as a NonKerosene vault getNonKeroseneValue() and getTotalUsdValue() will revert.
If the vault is added as Kerosene vault getKeroseneValue() and getTotalUsdValue() will revert
If the vault is added to both then all functions will get reverted (if it is a licensed vault as kerosene vault and normal vault).
This will make all the protocols in DoS, as these functions are called when minting, withdrawing, and liquidating.

In the Deployment script, all settings and configurations occur to all newly deployed contracts before transferring the ownership to MAINNET_OWNER, So we believe that this is an issue.

Recommendations
Set the unBoundedKeroseneVault before transferring the ownership.

Deploy.V2.s.sol L:89

  function run() public returns (Contracts memory) {
    ...

    unboundedKerosineVault.setDenominator(ker
Upgraded Q -> 2 from #389 [1716976587182]
Audit: 2024-04-dyad-findings
Severity: MEDIUM

Judge has assessed an item in Issue #389 as 2 risk. The relevant finding follows:

 13. No incentives to liquidate undercollateralized positions
Lines of code*

https://github.com/code-423n4/2024-04-dyad/blob/4a987e536576139793a1c04690336d06c93fca90/src/core/VaultManagerV2.sol#L217

Impact
When liquidators liquidate a distressed NFT, they perform a expect to receive a significant profit for their efforts while factoring gas costs and cost of liquidation. However, if a position lacks sufficient collateral to cover the short amount being liquidated, liquidators receive the remaining collateral which, which could be minimal when compared to the price of dyad needed to be burned to liquidate position. This scenario causes protocol to accumulate bad debts as multiple distressed/liquidatable nfts won’t be liquidated cause there are no incentives to do so (if there assets are relatively low in value), discouraging liquidators from participating in the protocol and impacting the protocol’s liquidity and overall efficiency.
Upgraded Q -> 2 from #980 [1716971346189]
Audit: 2024-04-dyad-findings
Severity: MEDIUM

Judge has assessed an item in Issue #980 as 2 risk. The relevant finding follows:

 L04 - Missing setUnboundedKerosenVault initialization in deploy
https://github.com/code-423n4/2024-04-dyad/blob/49fb7174576e5147dc73d3222d16954aa641a2e0/script/deploy/Deploy.V2.s.sol#L89-L89

Vulnerability details
boundedKerosineVault is deployed, but the setUnboundedKerosenVault is not called, which will cause a revert when BounderKerosineVault::assetPrice() will be called to price users kerosine collateral:

File: src/core/Vault.kerosine.bounded.sol
22: 
23:   function setUnboundedKerosineVault(
24:     UnboundedKerosineVault _unboundedKerosineVault
25:   )
26:     external
27:     onlyOwner
28:   {
29:     unboundedKerosineVault = _unboundedKerosineVault;
30:   }
31: 
...:
...:	/// ... some code ...
...:
44:   function assetPrice() 
45:     public 
46:     view 
47:     override
48:     returns (uint) {
49:       return unboundedKerosineVault.assetPrice() * 2;
50:   }
Upgraded Q -> 2 from #1150 [1715597404521]
Audit: 2024-04-dyad-findings
Severity: MEDIUM

Judge has assessed an item in Issue #1150 as 2 risk. The relevant finding follows:

 remove() and removeKerosene() could be Dossed by depositing 1 wei of collateral
The VaultManagerV2 allows any one to deposit collateral assocciated with a DNft and doesn’t force the sender to be theDNft owner

  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);
  }
remove() and removeKerosene() however checks that the removed Vaults from the MarketManagerV2 must have 0 tokens posted as collateral. And in this case it is possible for an attacker to deposit 1 wei to the vault in the name of the dnft which will make vault removal revert
Upgraded Q -> 2 from #892 [1715425448738]
Audit: 2024-04-dyad-findings
Severity: MEDIUM

Judge has assessed an item in Issue #892 as 2 risk. The relevant finding follows:

 [L-1] Griefing is possible on vault removal
In VaultManagerV2.sol line 101 Since anyone can deposit into a vault, someone can deny removal of a vault if will be able to frontrun removal transaction with a 1 wei deposit. However, this is rated as low at best, since this doesn’t look like efficient and impactful attack vector.

Remediation:
Either allow owner to pause deposits or implement minimum deposit to make attack more expensive.
Upgraded Q -> 2 from #892 [1715425381801]
Audit: 2024-04-dyad-findings
Severity: MEDIUM

Judge has assessed an item in Issue #892 as 2 risk. The relevant finding follows:

 [L-1] Griefing is possible on vault removal
In VaultManagerV2.sol line 101 Since anyone can deposit into a vault, someone can deny removal of a vault if will be able to frontrun removal transaction with a 1 wei deposit. However, this is rated as low at best, since this doesn’t look like efficient and impactful attack vector.

Remediation:
Either allow owner to pause deposits or implement minimum deposit to make attack more expensive.
Upgraded Q -> 2 from #1211 [1715337786912]
Audit: 2024-04-dyad-findings
Severity: MEDIUM

Judge has assessed an item in Issue #1211 as 2 risk. The relevant finding follows:

 [L-03] Kerosine price can be manipulated by a coordinated attack, leading to undesirable liquidations
The price of kerosene is deterministically calculated.

It takes (TVL - DYAD total supply) / Kerosene supply.

When minting DYAD, users need to have at least a 100% collateral ratio between exogeneous collateral (WSTETH/WETH).

    uint newDyadMinted = dyad.mintedDyad(address(this), id) + amount;
    if (getNonKeroseneValue(id) < newDyadMinted)     revert NotEnoughExoCollat();
    dyad.mint(id, to, amount);
Users then need to have at least 150% collateral ratio to prevent liquidation.

Kerosene can be used as extra collateral to mint more DYAD.

When minting 1000 DYAD, users can have

1500 USD worth of exogenous collateral (WETH)
1000 USD worth of exogeneous collateral and 500 USD worth of kerosene
Users cannot have

1500 USD worth of kerosene
With that being said, this is how the attack works.

Let’s say there are a bunch of people that has both exogenous collateral and kerosene as collateral.

The price of kerosene is (TVL - DYAD total supply) / Kerosene supply.

Assume TVL is 10M and DYAD total supply is ~4M. The whales can come together and purposely burn their DYAD to bring down the DYAD total supply, leaving some DYAD to liquidate a particular user that has a lot of kerosene as collateral.

Having a deterministic price may not be a good idea because it brings about many potential issues. Consider get the price through a TWAP on a liquidity pool instead.

https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/src/core/Vault.kerosine.unbounded.sol#L57-L65
Upgraded Q -> 2 from #1288 [1715337295488]
Audit: 2024-04-dyad-findings
Severity: MEDIUM

Judge has assessed an item in Issue #1288 as 2 risk. The relevant finding follows:

 [L-04] Attacker can grief users from redeeming collateral
The burnDyad() function allows anyone to burn DYAD on behalf of any id. This allows an attacker to burn DYAD and thus prevent users from possible redeeming their collateral back due to mintedDyad underflowing during redeem.

  function burnDyad(
    uint id,
    uint amount
  ) 
    external 
      isValidDNft(id)
  {
    dyad.burn(id, msg.sender, amount);
    emit BurnDyad(id, amount, msg.sender);
  }
Upgraded Q -> 2 from #1011 [1714930106885]
Audit: 2024-04-dyad-findings
Severity: MEDIUM

Judge has assessed an item in Issue #1011 as 2 risk. The relevant finding follows:

 [L-05] VaultManagerV2::remove and removeKerosene can be blocked for 1 wei
Issue Description:

Anyone can block removal of vault in the VaultManagerV2 contract by simply depositing at least 1 wei, this will make the id2asset of this nftId non-zero and grief the removal.

The issue is possible because everyone can deposit to any dNft, even without having one.

VaultManager.sol#L94-L116

 function remove(
      uint    id,
      address vault
  ) 
    external
      isDNftOwner(id)
  {
    if (Vault(vault).id2asset(id) > 0) revert VaultHasAssets();//ISSUE
    if (!vaults[id].remove(vault))     revert VaultNotAdded();
    emit Removed(id, vault);
  }

  function removeKerosene(
      uint    id,
      address vault
  ) 
    external
      isDNftOwner(id)
  {
    if (Vault(vault).id2asset(id) > 0)     revert VaultHasAssets();//ISSUE
    if (!vaultsKerosene[id].remove(vault)) revert VaultNotAdded();
    emit Removed(id, vault);
  }
Recommendation:

Instead of reverting when id2asset is a non-zero, just execute the withdraw function once again with the amount, this will send the donated tokens to the owner of that nft.
Upgraded Q -> 2 from #601 [1714928245780]
Audit: 2024-04-dyad-findings
Severity: MEDIUM

Judge has assessed an item in Issue #601 as 2 risk. The relevant finding follows:

 DOS remove() vault. Anyone can deposit dust token to prevent remove vault from active list
VaultManagerV2 allow 5 max normal vaults and 5 max Kerosene vault. To remove vault from active vault list. User must have no deposit inside these vault. https://github.com/code-423n4/2024-04-dyad/blob/49fb7174576e5147dc73d3222d16954aa641a2e0/src/core/VaultManagerV2.sol#L101

This can be prevented by other user. Other user can deposit into any DNFT id. https://github.com/code-423n4/2024-04-dyad/blob/49fb7174576e5147dc73d3222d16954aa641a2e0/src/core/VaultManagerV2.sol#L125

By frontrun depositing 1 wei token, remove() vault transaction will fail.