xanoutas/solidity-security-findings huggingface dataset:
Overly Strict Liquidation Conditions in Purger Causing False Positives
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/roles.cairo#L204-L227


# Vulnerability details

## Bug Description
The access control roles defined in[ src/core/roles.cairo](https://github.com/code-423n4/2024-01-opus/blob/main/src/core/roles.cairo). The permissions seem appropriately scoped to authorized roles, with some **caveats**

- Roles like `shrine_roles.abbot`, `shrine_roles.purger`, `shrine_roles.caretaker` grant just the necessary subset of Shrine permissions to those modules. This follows the principle of least privilege.

- External calls to Shrine functions check that the caller has the appropriate role. This prevents unauthorized external access.

**Potential ways to escalate privileges include:**

1. `shrine_roles.all_roles()` combines all Shrine permissions. This should only be used behind access control guards that restrict it to test environments. Accidentally deploying to prod could enable a full privileged user.

2. The `sentinel_roles.purger` role has the EXIT permission to withdraw assets from Gates. This unnecessary permission for the Purger could allow it to manipulate collateral asset prices if hacked. Removing EXIT would follow least privilege better.

3. Admin roles that can update other roles/permissions are not protected by TIMELOCKS. An attacker that compromises the admin could add backdoors by creating new over-privileged roles instantly. 

## Impact
The Purger contract contains the `preview_liquidate()` function to check if a collateral position (trove) can be liquidated. This is used to identify undercollateralized/risky troves.

However, the liquidation conditions are too strict: /[src](https://github.com/code-423n4/2024-01-opus/tree/main/src)/[core](https://github.com/code-423n4/2024-01-opus/tree/main/src/core)
/purger.cairo

```cairo
fn preview_liquidate(self: @ContractState, trove_id: u64) -> Option<(Ray, Wad)> {
    let trove_health: Health = self.shrine.read().get_trove_health(trove_id);
    preview_liquidate_internal(trove_health)
}

//

fn preview_liquidate(self: @ContractState, trove_id: u64) 
  -> Option<(Ray, Wad)> {

  // Fetch trove health metrics
  let trove = getTrove(trove_id)

  // Check if ltv > threshold
  if (trove.ltv > trove.threshold) {
    return getLiquidationParams(trove) 
  }

  return None
}
```

It only checks if the LTV (debt/collateral ratio) exceeds the liquidation threshold for the collateral type. No other risk metrics are considered.

**Impact** - False Positives

This causes false positives by marking some risky troves as "healthy":

```
Trove A 
  - Debt: $1000
  - Collateral: $100 
  - LTV: 1000/100 = 10x  
  - Threshold: 5x

// LTV > Threshold 
// Marked unsafe, can liquidate

If collateral price doubles:

- Collateral: $200
- LTV: 1000/200 = 5x < Threshold 

// Marked safe, no liquidation 
// Despite 10x leverage
```

The sudden price spike makes the LTV look good temporarily. Preventing liquidation allows un
Griefing: if pending forge transaction can be observed and frontrun, DoS can be created by a whale attacker who forges all available yin
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/main/src/core/shrine.cairo#L857


# Vulnerability details

Assume that an attacker can observe and frontun pending transactions from other Opus users, and has substantial amounts of collateral tokens under their control. By forging all available yin just before the user, the whale attacker whale can block user's from opening new troves with non-zero debt, or forging new debt for existing ones.

Since the success of a `forge` call depends on the debt ceiling and the amount of yin already minted by the system, the attacker knows exactly how much collateral they need and how much yin to forge in order to succeed, but make the subsequent user transactions fail.

## Proof of Concept

Attack idea:
- Attacker observes a user trying to forge a valid amount of yin for their trove.
- Attacker deposits a large amount of collateral token to the system and opens a whale trove.
- Attacker forges all permitted yin for the whale trove, so that the debt ceiling of the shrine is reached.
- The user's forge transaction fails due to the debt ceiling check.
- Attacker returns yin to the whale trove and withdraws the deposited assets.

Assuming that forging fee is zero, there's no loss for the attacker at the end, only transaction costs.

## Tools Used

Manual review.

## Recommended Mitigation Steps

Either specify a minimum duration of at least one interval before debt can be repaid, or ask the users to always pay some fee even for a sequences of `forge` and `melt` called in the same block.



## Assessed type

MEV
Anyone can forge and use yin in interest-free way as long as they repay all debt in the same interval
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/main/src/core/shrine.cairo#L845


# Vulnerability details

## Impact

Charging the interest only happens on interval boundaries, meaning that forging yin essentially is free as long as the debt is repaid in the same inteval. Essentially this behavior can be used to create a multi-block flash mints, where yin is minted, used for some purposes, and returned to the system with no fee.

This is unfair to the other users of the system, who pay interest on their outstanding yin.

This can also be used by the whales to grief the smaller users, always keeping the shrine debt close to the debt ceiling, except for the brief period while the block number advances from interval `t` to interval `t+1`. (If the whale is able to ensure their transactions get added to blocks on demand, with priority, them it returning the debt could be the last tx in the block `t*INTERVAL`, and forging new debt the first tx in the block `t*INTERVAL+1`, giving other uses no opportunity to forge any yin at all.)

## Tools Used

Manual review.

## Recommended Mitigation Steps

Either specify a minimum duration of at least one interval before debt can be repaid, or ask the users to always pay some fee even for a sequences of `forge` and `melt` called in the same block.



## Assessed type

Other
Overly Strict Validation in Purger Leading to False Positives
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/purger.cairo#L165-L168
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/roles.cairo#L111-L114
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L914


# Vulnerability details

## Bug Description
Access control roles defined in [src/core/roles.cairo](https://github.com/code-423n4/2024-01-opus/blob/main/src/core/roles.cairo), the permissions seem properly restricted with some exceptions:

- For `shrine_roles`, the permissions seem appropriately scoped. Roles like `abbot`, `purger`, `caretaker`, etc are given only the necessary subset of permissions.

- For `sentinel_roles`, `purger` is given `EXIT` permission which allows withdrawing collateral assets from Gates. This seems unnecessary for the Purger's intended functionality.

- For `sentinel_roles`, `caretaker` is also given `EXIT` permission. This makes sense for the Caretaker to liquidate and release collateral assets.

- For `shrine_roles`, `all_roles()` combines all possible permissions. This should only be used for testing and not production.

- Other roles like `absorber_roles`, `transmuter_roles`, etc also seem appropriately scoped.

These couple exceptions should be addressed to restrict permissions.

## Impact
Purger contract is responsible for identifying and liquidating unhealthy collateral positions to maintain system solvency. However, the liquidation conditions in the `preview_liquidate()` view function are too strict: [src/core/purger.cairo#L165-L168](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/purger.cairo#L165-L168)

```cairo
fn preview_liquidate(self: @ContractState, trove_id: u64) -> Option<(Ray, Wad)> {
    let trove_health: Health = self.shrine.read().get_trove_health(trove_id);
    preview_liquidate_internal(trove_health)
}
```

This only checks if the Loan-to-Value ratio (LTV) exceeds the liquidation threshold. But in certain edge cases - like when the collateral value suddenly spikes - an unhealthy trove may have an LTV lower than the threshold.

For example:

```
Trove A
  - Collateral Value: $100  
  - Debt: $90 
  - Threshold: 0.95

Original LTV = Debt / Value = 90 / 100 = 0.9

If collateral value doubles to $200, the new LTV = 90 / 200 = 0.45 < Threshold

So Trove A will be marked as healthy even if it has unsafe leverage.
```

**Impact**

This code strictly checks `ltv > threshold` to determine if a trove can be liquidated. But this ignores scenarios where sudden price spikes artificially improve the LTV.

As a result, unhealthy troves with spiked collateral values may be marked as falsely "healthy". This prevents the system from proactively liquidating and reducing risk.

Over time, leverage could accumulate across falsely healthy troves. During sudden market crashes, mass liquidations get trig
Immediate Lowering of Yang Threshold Could Lead to Arbitrage Opportunities for Bots and Lost of User Funds
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L620-L624
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1343-L1349


# Vulnerability details

## Impact
Shrine admins have the capability to adjust the liquidation threshold for a yang asset using the `set_threshold()` function. However, there's no time delay mechanism in place for these changes, posing a risk particularly when the threshold is lowered. Such adjustments can immediately render numerous troves eligible for liquidation. Opportunistic bots could exploit this by either back-running the adjustment transaction or waiting for the subsequent block to liquidate these troves, which, before the threshold change, were deemed healthy. As a consequence, trove owners may lose collateral amounting to the liquidation fees for troves that were considered healthy before the adjustment.

## Proof of Concept
`set_threshold()` method is used by the shrine admin to change the liquidation threshold of a yang.
```rust
fn set_threshold(ref self: ContractState, yang: ContractAddress, new_threshold: Ray) {
        self.access_control.assert_has_role(shrine_roles::SET_THRESHOLD);

        self.set_threshold_helper(yang, new_threshold);
    }
```
The method `set_threshold_helper()` doesn't implement any timelock functionality when lowering the threshold of the target yang.

```rust
fn set_threshold_helper(ref self: ContractState, yang: ContractAddress, threshold: Ray) {
    assert(threshold.val <= MAX_THRESHOLD, 'SH: Threshold > max');
    self.thresholds.write(self.get_valid_yang_id(yang), threshold);

    // Event emission
    self.emit(ThresholdUpdated { yang, threshold });
}
```
## Tools Used
Manual Review


## Recommended Mitigation Steps
To mitigate this issue, we recommend implementing a time delay and a two-step yang threshold change mechanism, especially when the change lowers the threshold. For instance, the admin will first request to change the threshold, and this request event will be broadcasted. After a 30-minute period, the admin would be eligible to enforce the threshold change. During this 30-minute time frame, trove owners will have adequate time to respond to the proposed alterations and adjust their positions to avoid liquidations.





## Assessed type

MEV
`shrine.get_threshold_and_value` function might use invalid assets prices
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1306-L1337
https://github.com/code-423n4/2024-01-opus/blob/4720e9481get_recent_price_froma4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1180-L1187


# Vulnerability details

## Impact

- `shrine.get_threshold_and_value` function is used by:

  - `shrine.get_trove_health` function: to check the healthiness of the trove by calculating its assets value to its yin debt if passing liquidation threshold, and the trove is considered unhealthy and can be liquidated if its LTV is above lqiquidation threshold.
  - `shrine.get_shrine_health` function: to get the health of the shrine contract.
  - `shrine.redistribute_helper` function: to redistribute bad debt and yang of between troves.

- Knowing that the collateral tokens prices (that are used to calculate the trove value) are updated in the following cases:

  1.  when the time of the prices update comes, where `seer.execute_task` function can be invoked by anyone.

  2.  automatically when an absorbtion happens (liquidation of an unhealthy trove using the yin balance of the `absorber` contract), where the price update is **enforced** regardless of the price being invalid/stale, and this is done via `seer.update_prices` function.

- But it was noticed that `get_threshold_and_value` might be using **stale prices** as there's no check if the prices are old and need to be updated or not (prices are extracted from the saved prices by calling the `get_recent_price_from` function):

```javascript
   fn get_recent_price_from(self: @ContractState, yang_id: u32, interval: u64) -> (Wad, Wad, u64) {
            let (price, cumulative_price) = self.yang_prices.read((yang_id, interval));

            if price.is_non_zero() {
                return (price, cumulative_price, interval);
            }
            self.get_recent_price_from(yang_id, interval - 1)
        }
```

- So as can be noticed; the collateral price is returned once a price > 0 is catched, regardless of this price being stale or not, which will result in using stale prices leading to an inaccuracy calculations of health and redistributed yang and yin amounts (from bad debts)

- Also, if the actual asset price drops while the protocol hasn't updated it; this will lead users to borrow more yin against their collaterals as their trove values will be calculated based on the old high price instead of the new unfetched price.

## Proof of Concept

[shrine.get_threshold_and_value function](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1306-L1337)

```javascript
      fn get_threshold_and_value(
            self: @ContractState, mut yang_balances: Span<YangBalance>, interval: u64
        ) -> (Ray, Wad) {
            let mut weighted_threshold_sum: Ray = RayZeroable::zero();
            let mut total_value: Wad = WadZeroable::zero();

            loop {
`seer.update_prices` function might enforce updating to an invalid collateral prices and risking users troves health
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/seer.cairo#L168-L171
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/seer.cairo#L193-L239
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/external/pragma.cairo#L186-L214


# Vulnerability details

## Impact

- The yang price is set in the shrine by one of the following operations:

  1.  when the yang is initially added, where this is done manually without directly fetching the price from the pragma oracle as the pprice will be entered manually.

  2.  externally by anyone via `seer.execute_task` function when the time of the prices update comes.

  3.  automatically when an absorbtion happens (liquidation of an unhealthy trove using the yin balance of the `absorber` contract), where the price update is **enforced** regardless of the price being invalid/stale, and this is done via `seer.update_prices` function.

- The problem with `seer.update_prices` is that it **enforces** updating the collaterals prices reagrdless of the returned prices being stale/invalid or zero, as it will fetch the price from pragma oracle where this check is made when fetching it:

  ```javascript
  //@notice: pragma.fetchprice function
  fn fetch_price(ref self: ContractState, yang: ContractAddress, force_update: bool) -> Result<Wad, felt252> {
              let response: PragmaPricesResponse = self.oracle.read().get_data_median(DataType::SpotEntry(pair_id));

          //some code...
              if force_update || self.is_valid_price_update(response) {
                  return Result::Ok(price);
              }
          //rest of the function...
  }
  ```

- So even if the price is invalid (not fresh and/or not having enough sources), it will be used since `force_update` is set to true when `seer.update_prices` is called:
  ```javascript
          fn update_prices(ref self: ContractState) {
              self.access_control.assert_has_role(seer_roles::UPDATE_PRICES);
              self.update_prices_internal(true);
          }
  ```
- This will risk the health of the users troves (positions) if the updated assets prices are invalid/zero, since users positions might be liquidated based on an invalid/stale asset price as there's no way to ensure that the price used to calculate the collateral value is up-to-date/fresh/has enough sources because it is **enforced** regardless of its validity.

## Proof of Concept

[seer.update_prices function](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/seer.cairo#L168-L171)

```javascript
        fn update_prices(ref self: ContractState) {
            self.access_control.assert_has_role(seer_roles::UPDATE_PRICES);
            self.update_prices_internal(true);
        }
```

[seer.update_prices_internal function](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4
`abbot.open_trove` function lacks slippage protection
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/abbot.cairo#L190-L191


# Vulnerability details

## Impact

- `abbot.open_trove` function is meant to enable users from depositing their collaterals and receives yangs (shares), where the deposited collateral is converted into share (yang) based on the total deposited collaterals balance, total minted shares and the amount of deposited collateral by the user:

  ```javascript
  //@notice: gate.enter function
  let yang_amt: Wad = self.convert_to_yang_helper(asset_amt);
              if yang_amt.is_zero() {
                  return WadZeroable::zero();
              }
              let success: bool = self.asset.read().transfer_from(user, get_contract_address(), asset_amt.into());
  ```

- As can be noticed; there's no check on the `yang_amt` , if it's zero then the function will continue smoothly without reverting.

- The other issue is that the user might get `yang_amt` (shares) less than he wishes for due to the collateral price fluctuations, as there's no mechanism to protect user from slippage.

- Same issue in `abbot.open_trove` function, where the user can open a trove with multiple collaterals and minted a yang shares for each collateral, but there's no check on the minted yang shares if it's greater than zero and greater than a minimum amount required by the user.

## Proof of Concept

[abbot.deposit function](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/abbot.cairo#L190-L191)

```javascript
        // add Yang (an asset) to a trove
        fn deposit(ref self: ContractState, trove_id: u64, yang_asset: AssetBalance) {
```

## Tools Used

Manual Review.

## Recommended Mitigation Steps

Update `abbot.deposit` function to check if the returned shares amount is >= the amount required by the user:

```diff
-fn deposit_helper(ref self: ContractState, trove_id: u64, user: ContractAddress, yang_asset: AssetBalance) {
+fn deposit_helper(ref self: ContractState, trove_id: u64, user: ContractAddress, yang_asset: AssetBalance) -> Wad {
            // reentrancy guard is used as a precaution
            self.reentrancy_guard.start();

            let yang_amt: Wad = self.sentinel.read().enter(yang_asset.address, user, trove_id, yang_asset.amount);
            self.shrine.read().deposit(yang_asset.address, trove_id, yang_amt);

            self.reentrancy_guard.end();

+           return yang_amt;
        }
```

```diff
-      fn deposit(ref self: ContractState, trove_id: u64, yang_asset: AssetBalance) {
+      fn deposit(ref self: ContractState, trove_id: u64, yang_asset: AssetBalance, min_yang: Wad) {
            // There is no need to check the yang address is non-zero because the
            // Sentinel does not allow a zero address yang to be added.

            assert(trove_id != 0, 'ABB: Trove ID cannot be 0');
            assert(trove_id <= self.troves_count.read(), 'ABB: Non-ex
Return value of yin transfer not checked during yin allocation
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/equalizer.cairo#L192


# Vulnerability details

## Impact
Return value of transfer is not checked causing failed transfers to fail silently and consequently loss of funds.
## Proof of Concept
The yin token returns a bool on transfer showing success or failure, hence the return value must always be checked. For the most part, the protocol does this.
```
        fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool {
            self.transfer_helper(get_caller_address(), recipient, amount);
            true
        }
```
However, in the `allocate` function in the allocator contract, yin balance is distributed to a number of recipient addresses at certain percentages. The issue here is that the return value of this transfer is not checked. 

```
        fn allocate(ref self: ContractState) {
            let shrine: IShrineDispatcher = self.shrine.read();

            let yin = IERC20Dispatcher { contract_address: shrine.contract_address };
            let balance: Wad = shrine.get_yin(get_contract_address());

            if balance.is_zero() {
                return;
            }

            // Loop over equalizer's balance and transfer to recipients
            let allocator: IAllocatorDispatcher = self.allocator.read();
            let (recipients, percentages) = allocator.get_allocation();

            let mut amount_allocated: Wad = WadZeroable::zero();
            let mut recipients_copy = recipients;
            let mut percentages_copy = percentages;
            loop {
                match recipients_copy.pop_front() {
                    Option::Some(recipient) => {
                        let amount: Wad = wadray::rmul_wr(balance, *(percentages_copy.pop_front().unwrap()));

                        yin.transfer(*recipient, amount.into()); //@note
                        amount_allocated += amount;
                    },
                    Option::None => { break; }
                };
            };

            self.emit(Allocate { recipients, percentages, amount: amount_allocated });
        }

```

This causes that any failed transfer fails silently and recipients will be marked as having been allocated amounts when in reality they received nothing, leading to loss of funds. It also causes accounting issues as there's now a discrepancy in the amount allocated and the contract balance. 
## Tools Used
Manual code review

## Recommended Mitigation Steps
Consider including an assertion for success.
```
      assert(success, "GA: Asset transfer failed");
```



## Assessed type

Token-Transfer