xanoutas/solidity-security-findings huggingface dataset:
Inadequate Handling of Default Compensation Cap
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The function get_compensation(trove_value: Wad) -> (Ray, Wad) in code  does not adequately handle the case where default_compensation.val equals COMPENSATION_CAP. and it bypass the if condition and call else condition The current condition, if default_compensation.val < COMPENSATION_CAP,  its wrong
## Proof of Concept

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

        if default_compensation.val < COMPENSATION_CAP {
            (default_compensation_pct, default_compensation)
        } else {
            (wadray::rdiv_ww(COMPENSATION_CAP.into(), trove_value), COMPENSATION_CAP.into())
        }


## Recommended Mitigation Steps

 it is recommended to modify the conditional statement within the function from if default_compensation.val < COMPENSATION_CAP to if default_compensation.val <= COMPENSATION_CAP. This modification will ensure that the function accurately handles scenarios where the default compensation equals or slightly exceeds the compensation cap.


## Assessed type

Invalid Validation
There is no safety buffer between max LTV ratio and Liquidation threshold.
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/purger.cairo#L222-L257
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L849-L889


# Vulnerability details

## Vulnerability Detail
There are two phases we need to discuss here, first part is about forging yin (minting synthetic token which serves as loaned assets to user) and 2nd part is about on how the liquidation is being triggered. These two parts will show that the protocol allows user to take a loan at max LTV to be equal to the threshold and triggers liquidation when LTV is more than the threshold.

Similar C4 past finding:
https://github.com/code-423n4/2023-07-tapioca-findings/issues/1164

**1st Part: Forging Yin** 
In the forging of yin, it allows the user to take a loan at max ltv (loan-to-value) ratio equal to its liquidation threshold. Here's how to trace the code implementation to prove this statement.

Let's look on the function assert_valid_trove_action, this is part of checking made by function [forge](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L844-L878) before allowing minting of yin tokens.
 
Check line 1156, it asserts if the trove is healthy. Healthy trove means it will pass the checking in forge function and allows minting of certain number of yin tokens. To know the details more, let us check what function is_healthy_helper all about.
```
File: shrine.cairo
1151:         // Checks that:
1152:         // 1. the trove is healthy i.e. its LTV is equal to or lower than its threshold
1153:         // 2. the trove has at least the minimum value if it has non-zero debt
1154:         fn assert_valid_trove_action(self: @ContractState, trove_id: u64) {
1155:             let health: Health = self.get_trove_health(trove_id);
1156:             assert(self.is_healthy_helper(health), 'SH: Trove LTV is too high');
1157:             if health.debt.is_non_zero() {
1158:                 assert(health.value >= self.minimum_trove_value.read(), 'SH: Below minimum trove value');
1159:             }
1160:         }
```


Function is_healty_helper shows that the trove is healthy if the LTV is less than or equal to threshold. The key thing here is the symbol equal which prove there is no safety buffer between LTV ratio and liquidation threshold. This can be problematic even if LTV ratio exceed the threshold by very minimal percentage because it will trigger the liquidation. Considering the volatility of crypto market, this is highly possible to happen. You could get liquidated immediately right after the moment you take a loan.
```
File: shrine.cairo
1147:         fn is_healthy_helper(self: @ContractState, health: Health) -> bool {
1148:             health.ltv <= health.threshold
1149:         }
1150: 
```

**2nd Part: Liquidation** 
Let's discuss here on how the protocol triggers the liquidation. In the fu
Normalize function can be accessed by anyone which can cause yin burning without taking collateral.
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Vulnerability Detail
Normalize function is designed to burn yin tokens to address the debt deficit. Debt deficit happened when when there is bad debt in the shrine.

```
File: equalizer.cairo
202:         // Burn yin from the caller's balance to wipe off any budget deficit in Shrine.
203:         // Anyone can call this function.
204:         // Returns the amount of deficit wiped.
205:         fn normalize(ref self: ContractState, yin_amt: Wad) -> Wad {
206:             let shrine: IShrineDispatcher = self.shrine.read();
207:             let budget: SignedWad = shrine.get_budget();
208:             if budget.is_negative() {
209:                 let wipe_amt: Wad = min(yin_amt, budget.val.into());
210:                 let caller: ContractAddress = get_caller_address();
211:                 shrine.eject(caller, wipe_amt);
212:                 shrine.adjust_budget(wipe_amt.into());
213: 
214:                 self.emit(Normalize { caller, yin_amt: wipe_amt });
215: 
216:                 wipe_amt
217:             } else {
218:                 WadZeroable::zero()
219:             }
220:         }
221:     }
```

However, this function can be access by anyone, which can cause burning of yin tokens without taking collateral. This is quite dangerous when there is debt deficit in the shrine and then someone currently holding yin tokens inadvertently executed it, who mistakenly use the function to pay the debt and recover collateral assets. 

There is no way they can recover their collateral assets after execution unless they buy another yin tokens from another parties or exchanges and use correct function to recover assets.
   

## Impact
This can cause loss of yin tokens (without recovering any collateral) to users who use this function inadvertently.

## Proof of Concept
Let's illustrate this scenario.

**Assumed State Before Execution**
Debt Deficit in the Shrine: equivalent to 500 yin tokens
Alice : Holds a certain amount of Yin tokens. Let's say 500 Yin tokens.

**Execution Flow**
1. Function Accessibility: 
Normalize function is accessible by anyone, exposed to the public and waiting to be executed.

2. Mistaken Execution: 
Alice, who wants to pay their debt and recover their collateral assets mistakenly executes  thinking it will help recover their collateral assets.

3. Function Action:
Burns Alice's Yin tokens (say all 500 tokens) without taking any collateral.

**Post Execution State**
Shrine's Debt Deficit: Reduced by 500 units of yin.
Protocol Assets in custody: Unchanged, as no collateral is recovered.
Alice: Loses 500 Yin tokens without any collateral recovery.

## Tools Used
Manual Review

## Recommended Mitigation Steps
The function should be only accessed by the admin in order to prevent future mistakes in burning yin tokens inadvertently by users.





## Assessed type
Trove owners are not prioritized in the event of emergency shutdown.
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/caretaker.cairo#L193-L254
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/caretaker.cairo#L256-L310


# Vulnerability details

## Vulnerability Detail
Caretaker contract has been made for emergency shutdown of the protocol or particularly, the shrine. There are 3 main functions inside this contract (see the following)

1. [Shut](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/caretaker.cairo#L193-L255) - permanently disables user actions for the Shrine and transfers a percentage of the collateral backing the prevailing total troves' debt to the Caretaker.


2. [Release](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/caretaker.cairo#L262-L310)-  allow trove owners to withdraw all collateral from their trove after shut has occurred.

3. [Reclaim](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/caretaker.cairo#L329-L371)- allow yin holders to burn yin and receive a percentage of the collateral in the Caretaker, up to the total troves' debt, after shut has occurred. 

You can notice on these functions, shut is the one who initiates the emergency process. The other two functions can only be executed after shut is activated. In this sequence of events, it will show that the protocol started the emergency process by transferring first the collateral assets from gate contract to caretaker contract. The amount of collateral assets transferred is based on computation to make sure there is a collateral asset back support of 1:1 for all yin tokens.

**Part of Shut Function that transfer assets from gate to caretaker, this leaves no chance to trove owners to reclaim their assets in full (see below)*
```
File: caretaker.cairo
222:             // Loop through yangs and transfer the amount of each yang asset needed to back
223:             // yin to this contract. This is equivalent to a final redistribution enforced
224:             // on all trove owners.
225:             // Since yang assets are transferred out of the Gate and the total number of yang
226:             // is not updated in Shrine, the asset amount per yang in Gate will decrease.
227:             let sentinel: ISentinelDispatcher = self.sentinel.read();
228:             let yangs: Span<ContractAddress> = sentinel.get_yang_addresses();
229:             let caretaker = get_contract_address();
230: 
231:             let mut yangs_copy = yangs;
232:             loop {
233:                 match yangs_copy.pop_front() {
234:                     Option::Some(yang) => {
235:                         let backed_yang: Wad = wadray::rmul_rw(capped_backing_pct, shrine.get_yang_total(*yang));
236:                         sentinel.exit(*yang, caretaker, DUMMY_TROVE_ID, backed_yang);
237:
Protocol continues to operate with stale price data price leading to losses through arbitraging
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Description

The [seer::update_prices_internal(...)](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/seer.cairo#L193-L239) method is responsible for keeping the yangs' prices up to date and follows this procedure:
* If the oracle price data [becomes stale](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/external/pragma.cairo#L247) due to an outage, the `seer` module won't be able to update the price of a given yang and  [emits an event](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/seer.cairo#L212) to notify about the missed price update. However, the `shrine` module is **not** informed that the oracle is stalling.
* Valid price data is always [forwarded to the `shrine` module](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/seer.cairo#L224) via the [shrine::advance(...)](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L723-L745) method.

As a consequence, the `shrine` module has information about a yang's [latest valid price data](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L742) and its corresponding timestamp with a [30 min resolution](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L48).  

**The problem:** The `shrine` module does **not** enforce a **maximum price age** which should restrict protocol operations and user interactions when the price of a given yang is stale/outdated.  

The related method [shrine::get_current_yang_price(yang)](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L979-L981) only returns the most recent available price data **not matter** how old it is. Similarly, the [shrine::get_recent_price_from(yang_id, interval)](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1180-L1187) method returns the price for the given `interval` but actually recurses backwards (`interval - 1`, etc.) until price data is found. In both cases, **no error** is raised in case of outdated price data.

Essentially, the rest of the protocol relies on price data through those methods and takes those prices as given without further checks about staleness or a maximum price age.

## Impact
As a consequence, the whole protocol can operate on stale yang price data **without** restricting protocol and user interactions.  

This leads to an arbitrage opportun
Redistributed yang remains unclaimable during shutdown
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Loss of collateral for user, when system is shutdown. In case of redistributed yangs with exception which is attributed to a trove, where yang balance is increased, it won't be reflected in user's deposits during shutdown and claim process leading to loss of fund for user which will be locked up in system forever

## Tools Used
VS Code

## Recommended Mitigation Steps
Add following call in release function of caretaker
```
shrine.get_redistributions_attributed_to_trove(trove_id)
```
and use these yang balances in release function


## Assessed type

Other
Suspended collateral can be used to lower interest rate
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Loss of interest by using suspended yang if it has lower rate assigned, by depositing more of that collateral. In `get_avg_rate_over_era` it loops over all yangs of user to determine average rate, and this includes yangs which are suspended, if suspended yang has low interest rate, it will then encourage user's to put more of suspended collateral into system to lower their average interest rate. 
This will lead to loss of interest for protocol.

## Proof of Concept


## Tools Used
VS Code

## Recommended Mitigation Steps
Check if yang is suspended while looping over all yangs for calculating rate.


## Assessed type

Other
Loss of liquidation compensation assets in absorb
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
In absorb, since `shrine.melt` is being called after `free`, caller will end up receiving less compensation than actually intended. It is because in `free`, while checking user deposits, there are two cases when user has pending redistribution with exception:
1: User's yang 1 has zero balance
2: User's yang 1 has non zero balance

In both cases user's balances are due for an update, if first n yang balances are zero, caller will miss out on compensation from unaccounted yang updates from all yangs due for redistribution until it encounters one with non zero yang balance. When it encounters first non-zero yang balance, it will still miss out on unaccounted fund from that yang as well, but after that it will be okay, since charge() would be called inside seize function which will then update balances for all yangs. 


## Proof of Concept
This is inside free function, where only after first non-zero yang balance is seize called and redistribution updated for trove and all its yangs. All calls to deposit will return less than actual balance of trove.

```
            loop {
                match yangs_copy.pop_front() {
                    Option::Some(yang) => {
                        let deposited_yang_amt: Wad = shrine.get_deposit(*yang, trove_id);

                        let freed_asset_amt: u128 = if deposited_yang_amt.is_zero() {
                            0
                        } else {
                            let freed_yang: Wad = wadray::rmul_wr(deposited_yang_amt, percentage_freed);
                            let exit_amt: u128 = sentinel.exit(*yang, recipient, trove_id, freed_yang);
                            shrine.seize(*yang, trove_id, freed_yang);
                            exit_amt
                        };

                        freed_assets.append(AssetBalance { address: *yang, amount: freed_asset_amt });
                    },
                    Option::None => { break; }
                };
            };

```


## Tools Used
VS Code

## Recommended Mitigation Steps
`absorb` function should update order of melt and free
```
            shrine.melt(absorber.contract_address, trove_id, purge_amt);
            let compensation_assets: Span<AssetBalance> = self.free(shrine, trove_id, pct_value_to_compensate, caller);
```


## Assessed type

Other
Unexpected timelock multiplier when previous request has removed
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Unexpected timelock multiplier when previous request has removed

## Proof of Concept
- Alice provides Yin in absorber smart contract.
- Alice requests to remove Yin in timestampA.
- After REQUEST_BASE_TIMELOCK, Alice removes some amount of Yin and request.timestamp will be timestampA.
- Alice requests again. Considering last request has finished successfully, next request's timelock should still be REQUEST_BASE_TIMELOCK. Because of REQUEST_COOLDOWN condition in Line 449, the second request has to be locked more time.

## Tools Used
Manual

## Recommended Mitigation Steps
If previous requests has finished successfully, next request's timelock value should be taken as the initial value.



## Assessed type

Context
Improper rounding up in absorber::remove
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Improper rounding up in absorber::remove

## Proof of Concept
- Alice and Bob provide some yins into absorber to earn some shares.
- Alice sends one request and remove her shares to get her yins
- Variable 'shares_to_remove' is round up. Rounding up will be in favor of protocol.
- 'self.total_shares' updated value may be less because of 'shares_to_remove' rounding up.
- Bob can earn some profit because of less total_shares.

## Tools Used
Manual

## Recommended Mitigation Steps
total_shares value update should be in favor of protocol.


## Assessed type

Error
Manipulation of collateral-to-yang ratios due to reliance on ERC20 contract balance
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/gate.cairo#L231
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/gate.cairo#L202
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/gate.cairo#L220


# Vulnerability details

## Impact

The yang-to-assets and assets-to-yang conversions in the `Gate` contract implicitly assume that the following invariant holds:

```
1 yang (internal, as accounted by Shrine) == 1 asset token (external, in the contract's ERC20 balance)
```

The proof of concept below demonstrates that anyone holding asset tokens can violate this invariant, by transferring tokens into the Gate contract's ERC20 balance. This leads to payouts different from the ones requested by the user.

In particular, Gate's [`convert_to_assets_helper`](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/gate.cairo#L191-L204) and [`convert_to_yang_helper`](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/gate.cairo#L209-L223) functions [apply a conversion ratio](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/gate.cairo#L202) based on the Gate contract's ERC20 balance (in the respective collateral asset) by calling [`get_total_assets_helper`](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/gate.cairo#L230-L232) which in turn [queries the collateral asset's ERC20 balance](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/gate.cairo#L231) of the deployed Gate contract. A transfer of ERC20 tokens into the Gate contract's ERC20 balance brings the conversion ratios out of balance, resulting in payout amounts different from the ones requested by the user, e.g., when withdrawing from a trove through the Abbot.

## Proof of Concept

In the proof of concept below, Eve transfers ETH into the Gate contract’s ERC20 balance before Alice opens a trove backed by 10 ETH. When she attempts to withdraw the 10 ETH, only approx 9.91 ETH are returned.

Insert the code below into [`src/tests/abbot/test_abbot.cairo`](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/tests/abbot/test_abbot.cairo) and run

```sh
$ snforge test opus::tests::abbot::test_abbot::test_abbot::test_poc
```

```diff
diff --git a/src/tests/abbot/test_abbot.cairo b/src/tests/abbot/test_abbot.cairo
index e689697..321f2af 100644
--- a/src/tests/abbot/test_abbot.cairo
+++ b/src/tests/abbot/test_abbot.cairo
@@ -6,6 +6,7 @@ mod test_abbot {
     use opus::interfaces::IERC20::{IERC20Dispatcher, IERC20DispatcherTrait};
     use opus::interfaces::ISentinel::{ISentinelDispatcher, ISentinelDispatcherTrait};
     use opus::interfaces::IShrine::{IShrineDispatcher, IShrineDi