xanoutas/solidity-security-findings huggingface dataset:
Vault owner can withdraw even when collateral ratio is less than `MIN_COLLATERIZATION_RATIO`
Audit: 2024-04-dyad-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

**Description:**
Vault owner can withdraw even when collateral ratio is less than `MIN_COLLATERIZATION_RATIO`

In `VaultManagerV2::withdraw` the check to make sure collateral ratio is less than `MIN_COLLATERIZATION_RATIO` is done after the asset has been withdrawn thereby bypassing check

**Impact:**

This can result in users withdrawing asset even when their collateral ratio is less than `MIN_COLLATERIZATION_RATIO` therefore causing an imbalance in the protocol.

**Recommended Mitigation:**

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

        if (getNonKeroseneValue(id) - value < dyadMinted)
            revert NotEnoughExoCollat();

        // Proceed with the withdrawal from the vault to the specified recipient address if collatRatio is less than min collatRatio.
+         if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow();
        _vault.withdraw(id, to, amount);

-        if (collatRatio(id) < MIN_COLLATERIZATION_RATIO) revert CrTooLow();
    }

```


## Assessed type

Other
Inflation of Kerosene's price by donation to vaults.
Audit: 2024-04-dyad-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Vulnerability Detail
- Parameters used for calculating the Kerosene token price is the TVL in USD of all vaults within the DYAD protocol. To determine this value, the balance of each vault of its asset is fetched in the UnboundedKerosineVault.assetPrice:
`tvl += vault.asset().balanceOf(address(vault)) * vault.assetPrice();`
- Problem is that this happens via the token’s balanceOf function rather than reading a storage variable that performs an internal tracking of deposits as it’s done in the BoundedKerosineVault. Therefore, an adversary may donate an arbitrary amount of assets to any vault which would increase the
price of Kerosene without having actual collateral provided to the protocol that can be withdrawn or
liquidated.


## Impact
Inflation of kerosene's price through donation.



## Tools Used
Manual Review

## Recommended Mitigation Steps
Track internally to avoid inflation attack.


## Assessed type

Other
Cannot add kerosine vault in VaultManagerV2
Audit: 2024-04-dyad-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Users won't be able to use kerosine vaults in their notes, because in `addKerosene` method, there is a check `if (!keroseneManager.isLicensed(vault)) revert VaultNotLicensed();`. This check will revert, because as per the DeployScriptV2.s.sol(https://github.com/code-423n4/2024-04-dyad/blob/cd48c684a58158de444b24854ffd8f07d046c31b/script/deploy/Deploy.V2.s.sol#L64) and also YouTube video https://youtu.be/64zoj0iLCy0 in KerosineManager won't go KerosineVaults.

## Proof of Concept
```solidity
    // addKerosene checks kerosineManager.isLicensed(), but have to check in vaultsLicensers.isLicensed
    // check also Deploy.V2.s.sol
    function testPOCUnableToAddUnboundedKerosineVault() public {
        vm.startPrank(USER1);
        vm.expectRevert(IVaultManager.VaultNotLicensed.selector);
        vaultManager.addKerosene(USER1_NFT, address(unboundedKerosineVault));
    }
```

## Tools Used
Manual review

## Recommended Mitigation Steps
Change `if (!keroseneManager.isLicensed(vault)) revert VaultNotLicensed();`
with `if (!vaultsLicenser.isLicensed(vault)) revert VaultNotLicensed();`


## Assessed type

Context
Oracle data feed is insufficiently validated causing wrong assetPrice
Audit: 2024-04-dyad-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

**Description:**
The `Vault::assetPrice()` does not include a check to ensure that the `updatedAt` and `answer` obtained from the oracle's `latestRoundData` is a valid non-zero value before proceeding with the staleness check.

**Impact:**

This omission leaves the system vulnerable to potential issues related to wrong data which can affect other part of the code that uses result from `Vault::assetPrice()`by returning wrong or invalid assetPrice

**Proof of Concept:**

Oracle data feed is insufficiently validated. There is no check for invalid price and updatedAt. This can lead to wrong answer return value.

```javascript
function assetPrice() public view returns (uint) {
        (, int256 answer, , uint256 updatedAt, ) = oracle.latestRoundData();
        if (block.timestamp > updatedAt + STALE_DATA_TIMEOUT)
            revert StaleData();
        return answer.toUint256();
    }

```

**Recommended Mitigation:**

```diff
function assetPrice() public view returns (uint) {
        (, int256 answer, , uint256 updatedAt, ) = oracle.latestRoundData();
+        require(updatedAt > 0, "Invalid updatedAt");
+        require(answer > 0, "Invalid price");
        if (block.timestamp > updatedAt + STALE_DATA_TIMEOUT)
            revert StaleData();
        return answer.toUint256();
    }

```


## Assessed type

Oracle
Recursion when calculating Kerosene price
Audit: 2024-04-dyad-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
In the Kerosene Manager only non-kerosene vaults are allowed, otherwise a recursion is created when calculating the price of Kerosene but then VaultManagerV2 requires the Kerosene vault to be added to the manager before being able to add it as a vault for an NFT.</description>

## Proof of Concept
In `VaultManagerV2.sol:74`, the `addKerosene` function allows adding Kerosene vaults to the `vaultsKerosene` array. However, the `assetPrice` function in `Vault.kerosine.unbounded.sol` is meant to get only non-Kerosene vaults from the `kerosineManager.getVaults()` function. If Kerosene vaults are added to the KeroseneManager, it will create an infinite recursion when `vault.assetPrice()` is called within the loop, as the Kerosene vault's `assetPrice` function will try to calculate the Kerosene price again, leading to a stack overflow.

```solidity
// VaultManagerV2.sol
function addKerosene(uint256 id, address vault) external isDNftOwner(id) {
    if (vaultsKerosene[id].length() >= MAX_VAULTS_KEROSENE) revert TooManyVaults();
	if (!keroseneManager.isLicensed(vault)) revert VaultNotLicensed(); // Needs Kerosene Manager to have Kerosene Vault added
	if (!vaultsKerosene[id].add(vault)) revert VaultAlreadyAdded();
    emit Added(id, vault);
}

// Vault.kerosine.unbounded.sol
function assetPrice() public view override returns (uint256) {
	uint256 tvl;
	address[] memory vaults = kerosineManager.getVaults();
	uint256 numberOfVaults = vaults.length;
	// @audit-info this is meant to get only Non-kerosene vaults, otherwise it will eventually revert when calling the vault.assetPrice() function because of infinite recursion.
	for (uint256 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());
	}
    // @audit-info C = tvl, D = dyad.totalSupply, K = kerosineDenominator.denominator
	// @audit numerator can be negative (revert) if tvl price falls below dyad.totalSupply
	// @audit-info given that in order to mint dyad a 150% collateral is required, even if nobody uses
	uint256 numerator = tvl - dyad.totalSupply();
	uint256 denominator = kerosineDenominator.denominator();
    // @audit-info this assetPrice goes higher if the TVL increases in vaults or if dyad.totalSupply decreases
    return numerator * 1e8 / denominator;
}

```

## Tools Used
Manual Review

## Recommended Mitigation Steps
The simplest way would be to use a VaultLicenser in VaultManagerV2 instead of KeroseneManager, but special attention must be put in making sure they are in sync regarding the vaults added.



## Assessed type

Other
The protocol allows creation of positions that can be liquidated the next block
Audit: 2024-04-dyad-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Summary

The protocol allows creation of positions that can be liquidated the next block. The only criteria to meet is just to have 1.5x more collateral than the minted DYAD, meaning you have to be 150% overcollateralized.

## Vulnerability Details

The problem is that if such positions are opened(where the provided collateral is exactly 1.5x the DYAD amount) it is really risky and highly likely that the user will be liquidated the next block if the price of the collateral falls slightly.
The user will then lose a majority of his collateral, meaning he will be in quite the loss.
No one can predict the prices and such positions shouldn't be allowed to be opened.

Most stablecoins have a treshold they need to meet so that they can be overcollateralized(200% for example) and if they fall below another threshold(150% for example) they can be liquidated.

## Impact

Allowing the users to be liquidated in the next block will both have a negative impact on the reputation of the protocol and lead to loss of funds from the user.

## Tools Used

Manual Review

## Recommended Mitigation Steps

Implement two tresholds. One so that the DYAD minted cannot exceed it(minting treshold) and another one for the liquidation of a user(when can a user be liquidated).


## Assessed type

Other
wrong may to implement "collatRatio" function
Audit: 2024-04-dyad-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Detailed description of the impact of this finding.
good collateral can also get liquidated.Here we are getting collatRatio by getTotalUsdValue(id).divWadDown(_dyad).Here we are rounding down so good collateral will also give less  collatRatio and can be liquidated.
## Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
 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);
  }
## Tools Used

## Recommended Mitigation Steps
we have to take into account that we should be taking 


## Assessed type

Context
Missing vault address validation in the `VaultManagerV2` for `deposit()` and `withdraw()`, exposes protocol to risk of reentrancy, unfair liquidation and phishing
Audit: 2024-04-dyad-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
There is no address validation for the vault parameter in the `VaultManagerV2`, for the vault address in the `deposit()`and `withdraw()` which exposes the protocol to different set of risks:
- Users could supply the address  of malicious contract that implements the IVault interface and make the vaults make calls to it openeing an attack surface for reentrancy attacks: we couldn't find any way of exploiting this to do some damage. But we still recommend closing unnecessary attack surfaces and avoiding external calls to user given adresses
- If a user deposits his fund to a vault that he didn't add in the `VaultManagerV2` through the `add()` or `addKerosene()`. The funds will still be deposited in the vault but they wouldn't be counted in when calculating the collateralisation ratio of his position, as the function will only iterate through the vaults the user adds and potentially this could lead to unfair liquidations.
- The VaultManagerV2 could be a target of fishing campaings by other Attackers. The VaultManagerV2 will be first not flagged and reported by web3 wallet like metamask. So the attackers will make the victims call deposit with their funds on address that belongs to a malicous contract the attacker deployed. So the user will deposit his funds to the VaultManagerV2 but his funds will get deposited to the malicious contract and the attacker will get the funds from the malicious vault.
## Recomendation 
To mitigate this attack vector, and protect both the users and the protocol from attack risks. We recommed that in the `deposit()` and `withdraw()` functions, that the protocol checks if the given vault address is an address that the user has already added else revert  
A possible fix would like this: 
```diff
  function deposit(
    uint    id,
    address vault,
    uint    amount
  ) 
    external 
      isValidDNft(id)
  {
    idToBlockOfLastDeposit[id] = block.number;
+   if (!(vaults[id].contains(vault) || vaultsKerosene[id].contains(vault))) revert NotAdded();
    Vault _vault = Vault(vault);
    _vault.asset().safeTransferFrom(msg.sender, address(vault), amount);
    _vault.deposit(id, amount);
  }

```

```diff
function withdraw(
    uint    id,
    address vault,
    uint    amount,
    address to
  ) 
    public
      isDNftOwner(id)
  {
    if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock();
+    if (!(vaults[id].contains(vault) || vaultsKerosene[id].contains(vault))) revert NotAdded();
    uint dyadMinted = dyad.mintedDyad(address(this), id);
    Vault _vault = Vault(vault);

```


## Assessed type

Invalid Validation
Liquidations are hardly profitable for the liquidator and lenient for the liquidated ones
Audit: 2024-04-dyad-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Users who liquidate undercollateralized accounts make insignificant profits or even suffer loss, while liquidated accounts do not suffer significant losses. This may lead to bad debt in the protocol as hardly anyone will be willing to liquidate other users.

## Proof of Concept
It might be that DYAD simply is wrongly burned off `msg.sender` instead of `owner(to)` in [this line](https://github.com/code-423n4/2024-04-dyad/blob/main/src/core/VaultManagerV2.sol#L215), however documentation confirms the code so it seems to be intended. 

Therefore, below explaining what happens in current state of the protocol. Assume following scenario. 

 - Bob deposits 2500 USD worth of WETH as a collateral and mints 1500 DYAD $. CR = 1.66
 - WETH drops in price on a sudden market drop. Now it happens that Bob's collateral is worth 1200USD. CR = 1.0 after cap, so account can be liquidated
- Alice, who has 1500DYAD minted, decides to liquidate Bob's account (ID) to herself. When Alice calls liquidate:
  - Alice is burned 1500$ DYAD
  - liquidationEquityShare = 0; so liquidationAssetShare is 100%
  - For each Bob's vault, `collateral` is equal to amount in the vault; since its 100% then just all Bob's collateral is moved to Alice's id.
  - Bob still has 1500DYAD minted (1500USD) which he can sell on the market
  - Alice lost 1500 USD DYAD but made 1200 USD from Bob's collateral. So Alice lost $300 on the liquidation + gas costs. 


Let's change the scenario, if Bob's collateral dropped only a bit, say to 2100$. then CR = 1.4, so account is liquidatable. Then liquidationEquityShare = 0.4*0.2=0.02, which causes liquidationAssetShare to be around 77%.
So Bob's collateral that will be liquidated is circa 2100 * 77% = 1617
Now Alice could do a small profit (=1617-1500-gas)assuming gas cost won't be high.

For collateral drop to 2000, where CR is circa 1.3 => 0.3 * 0.2 = 0.06 so circa 82% * 2000 = 1660. 

In the same time, Bob's DYAD is still there. While he won't be able to get remaining collateral, he can just market sell DYAD and will not lose much.

It clearly seems, that when DYAD is burned off liquidator and not liquidated account, there is no significant profit, thus it is doubtful that in current state anyone should be eager to liquidate undercollateralized accounts. 



## Tools Used
Manual approach

## Recommended Mitigation Steps
The burned DYAD should be burned off liquidated account, not off the liquidator's.


## Assessed type

Other
`assetPrice` function will return incorrect price due to missing `minAnswer` and `maxAnswer` check
Audit: 2024-04-dyad-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
In the `assetPrice` function of `Vault.kerosine.unbounded.sol` contract, the function calls the `vault.assetPrice()`, which in turn calls the Chainlink's `latestRoundData()` to get the price. 
But the answer returned by the `latestRoundData` is not checked if it is between the range of `minAnswer` and `maxAnswer`. Due to this, if the price of the asset drops below the `minAnswer`, the protocol will continue to value the asset at `minAnswer` instead of its actual value. This will lead to incorrect price. Such situation can happen in case of market crash.

Same issue also affects the `withdraw` function of VaultManager v2 contract.

Refer to this doc for more info : https://docs.chain.link/data-feeds#check-the-latest-answer-against-reasonable-limits

## Proof of Concept
The root problem is in the function `assetPrice` of `Vault.sol`
```
  function assetPrice() 
    public 
    view 
    returns (uint) {
      (
        ,
        int256 answer,
        , 
        uint256 updatedAt, 
      ) = oracle.latestRoundData();
      if (block.timestamp > updatedAt + STALE_DATA_TIMEOUT) revert StaleData();
      return answer.toUint256();
  }
```
It doesn't checks for min and max price

## Tools Used
Manual Review

## Recommended Mitigation Steps
It is recommended to add checks for `minAnswer` and `maxAnswer` limits. 
```
          require(answer < maxAnswer, "MaxAnswer triggered");
          require(answer > minAnswer, "MinAnswer triggered");
```



## Assessed type

Oracle
A malicious user can steal the asset of another user that deposit to a trove directly from non contract address
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/main/src/core/abbot.cairo#L131-L161


# Vulnerability details

## Impact
Loss of funds to the user that calls opens a trove by calling the abbot contract directly rather than through a contract.

## Proof of Concept
The root cause of this issue is that the `open_trove(...)` does not validate that the `get_caller_address()` is not zero.

If an innocent user creates a troves by directly calling the `open_trove(...)` function rather than through a contract, the `get_caller_address()` is zero and state will be updated as account zero is the owner of the trove.

This allows anyone that directly calls `close_trove(...)` function to close the trove and steal user asset for that trove id.

```
// create a new trove in the system with Yang deposits,
        // optionally forging Yin in the same operation (if `forge_amount` is 0, no Yin is created)
        fn open_trove(
            ref self: ContractState, mut yang_assets: Span<AssetBalance>, forge_amount: Wad, max_forge_fee_pct: Wad
        ) -> u64 {
            assert(yang_assets.len().is_non_zero(), 'ABB: No yangs');

            let troves_count: u64 = self.troves_count.read();
            self.troves_count.write(troves_count + 1);

           
            #######################################################
            #
            # NO VALIDATION TO ENSURE get_caller_address() IS NOT ZERO
            #######################################################
@>            let user = get_caller_address();//@audit no zero address check.
            let user_troves_count: u64 = self.user_troves_count.read(user);
            self.user_troves_count.write(user, user_troves_count + 1);

            let new_trove_id: u64 = troves_count + 1;
            self.user_troves.write((user, user_troves_count), new_trove_id);
@>          self.trove_owner.write(new_trove_id, user);//@audit trove owner is set as zero

            // deposit all requested Yangs into the system
            loop {
                match yang_assets.pop_front() {
                    Option::Some(yang_asset) => { self.deposit_helper(new_trove_id, user, *yang_asset); },
                    Option::None => { break; }
                };
            };

            // forge Yin
            self.shrine.read().forge(user, new_trove_id, forge_amount, max_forge_fee_pct);

            self.emit(TroveOpened { user, trove_id: new_trove_id });

            new_trove_id
        }
```

## Tools Used
Manual review

## Recommended Mitigation Steps
Consider Implementing a check to ensure troves are not created by direct call from non contract address by checking that the `get_caller_address()` is not zero.


## Assessed type

Other