xanoutas/solidity-security-findings huggingface dataset:
The `withdraw_helper()` function incorrectly updates `new_trove_balance` because it uses `trove_balance` before calling the `charge()` function
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/04583e0411dbf8027952d668a8678fda0cb5b160/src/core/shrine.cairo#L1379-L1397


# Vulnerability details

## Impact
The `withdraw_helper()` function is an internal function called by the external `withdraw()` function to handle trove accounting. This function reads the current `trove_balance` from storage and calculates the `new_trove_balance` and `new_total` values. However, these calculations occur before the `charge()` function is called.

The `charge()` function is used to accrue any interest from the last interacted timestamp to the current timestamp. It also pulls any redistribution debts and yangs. During this process, the yang balance of troves might be updated due to exceptional redistribution.

After calling `charge()`, `new_total` and `new_trove_balance` are written to storage. But, these values may be outdated if the trove balance was updated in the `charge()` function. As a result, the stored values could be incorrect, potentially causing user losses.
```rust
fn withdraw_helper(ref self: ContractState, yang: ContractAddress, trove_id: u64, amount: Wad) {
    ...
    let trove_balance: Wad = self.deposits.read((yang_id, trove_id));
    ...

    let new_trove_balance: Wad = trove_balance - amount;
    let new_total: Wad = self.yang_total.read(yang_id) - amount;
  
    // @audit Need to charge first, `trove_balance` might change after `charge()` 
    self.charge(trove_id); 
  
    // @audit `new_trove_balance` and `new_total` is calculated before `charge()`, it is outdated but still written to storage.
    self.yang_total.write(yang_id, new_total);
    self.deposits.write((yang_id, trove_id), new_trove_balance);
    ...
}
```

## Proof of Concept

Consider the following scenario:

1. Only Alice and Bob deposited yang X into Shrine. Alice's trove has `MIN_RECIPIENT_POOL_YANG - 1` yang X.
2. There is a redistribution of yang X from Bob's trove. Since only Alice and Bob deposited X, the `redistributed_yang_recipient_pool` will be `MIN_RECIPIENT_POOL_YANG - 1`, resulting in an exceptional redistribution.
3. Alice calls withdraw to remove `amount = MIN_RECIPIENT_POOL_YANG - 1` X that she deposited into her pool.
4. The `withdraw_helper` function calculates `new_trove_balance = 0`. The `charge()` function then pulls the exceptional distribution and increases Alice's yang X trove balance. However, after that, `new_trove_balance = 0` is still written to storage, meaning the amount of X redistributed to Alice's trove is lost forever.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Call the `charge()` function before any calculation in the `withdraw_helper()` function.



## Assessed type

Other
The `get_shrine_health()` function does not account for unaccrued interest
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/04583e0411dbf8027952d668a8678fda0cb5b160/src/core/shrine.cairo#L510


# Vulnerability details

## Impact
In Shrine, the function `get_shrine_health()` calculates the overall shrine health, including the threshold, LTV, total value, and total debt. This function is used to determine whether the shrine is in recovery mode. If it is, the threshold will be scaled down, and yin in the absorber cannot be removed. This behavior can be observed in the `shrine.scale_threshold_for_recovery_mode()` and `absorber.assert_can_remove()` functions.

However, `get_shrine_health()` does not account for unaccrued interest in troves, instead using `total_troves_debt` directly. As a result, it may return an incorrect result, indicating a healthier shrine than is actually the case. If any provider monitors the shrine and tracks its health, they would know the actual status of the shrine is recovery mode. In that case, they could remove their yin from the absorber when they should not be allowed to. This would result in later providers suffering the loss when the system in recovery mode.

```rust
fn get_shrine_health(self: @ContractState) -> Health {
    let (threshold, value) = self.get_threshold_and_value(self.get_shrine_deposits(), now());
    // @audit `total_troves_debt` has not accrued interest
    let debt: Wad = self.total_troves_debt.read(); 

    // If no collateral has been deposited, then shrine's LTV is
    // returned as the maximum possible value.
    let ltv: Ray = if value.is_zero() {
        BoundedRay::max()
    } else {
        wadray::rdiv_ww(debt, value)
    };

    Health { threshold, ltv, value, debt }
}
```

## Proof of Concept
In Shrine, each trove's debt is tracked in the `troves` mapping. Every time users interact with a trove, the interest is accrued using the `charge()` function. In this function, `total_troves_debt` is also increased by the charged amount. The code snippet below is from the function `charge()` where `total_troves_debt` is updated.

```rust
let charged: Wad = compounded_trove_debt - trove.debt;

// Add the interest charged on the trove's debt to the total troves' debt and
// budget only if there is a change in the trove's debt. This should not include
// redistributed debt, as that is already included in the total.
if charged.is_non_zero() {
    let new_total_troves_debt: Wad = self.total_troves_debt.read() + charged;
    self.total_troves_debt.write(new_total_troves_debt);
    self.adjust_budget_helper(charged.into());
    self.emit(TotalTrovesDebtUpdated { total: new_total_troves_debt });
}
```

Essentially, `total_troves_debt` is only updated when troves are interacted with.

Consider this scenario:

1. A whale's trove holds a large amount of debt, but no interaction has occurred with it for months. Naturally, the accrued interest would be substantial.
2. When it is charged, the amount added to `total_troves_debt` would set the system into recovery mode.
3
Budget Inaccuracy in Equalizer 
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/equalizer.cairo#L207-L222
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/equalizer.cairo#L182-L183
 https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/equalizer.cairo#L130-L136


# Vulnerability details

## Vulnerability details
The  contract occur a vulnerability due to its reliance on external data sources for budget adjustments without sufficient validation. Specifically, incorrect surplus reporting by the Shrine, misallocations by the Allocator, and inappropriate normalization actions can lead to significant discrepancies in the budget and the fuzz test id confirm that is present in clear as an issue .
Here is the Vulnerable Lines:
- the equalize Function Call: The call to shrine.get_budget() without verifying the accuracy of the returned budget value.

```
let budget: SignedWad = shrine.get_budget();
if !budget.is_positive() { ... }
```

- In the allocate Function Call: it's Using allocation percentages directly from allocator.get_allocation() without cross-verification.

```
let (recipients, percentages) = allocator.get_allocation();
```
- and in the normalize Function Logic: Burning Yin based on an unverified deficit reported by the Shrine.
```
let budget: SignedWad = shrine.get_budget();
if budget.is_negative() { ... }
```
## Impact
the issue  can lead to financial instability within the protocol, including inflation or deflation of assets In severe cases, it could enable malicious actors to manipulate the budget for financial gain.

## Proof of Concept
 in result of the fuzz test in the equalize Function it's Calls with Inaccurate Data When equalize is triggered based on incorrect surplus data from the Shrine, it can mint surplus debt unnecessarily or fail to mint debt when needed, deviation the budget. - in allocate Function Calls with Flawed Allocator Data the Calls is  to allocate that distribute resources based on incorrect Allocator data  it's result in the misallocation of Yin, and this affecting the protocol's financial , - in normalize Attempts Based on Error Deficit Reports calling normalize to adjust for a deficit that's incorrectly reported it's can lead to inappropriate burning of Yin  and about How the Budget Gets Into a Wrong State ? ==> in scenario of Incorrect Reporting from Shrine The fuzz test  show that  if the Shrine incorrectly reports a surplus   it directly impacts the budget state and ==>  in scenario of Allocator Misallocation the fuzz scenario show  that  incorrect data from the Allocator leads to misallocation of resources and ==> in scenario of overcorrection through the normalize Function it's show where attempts to correct perceived budget inaccuracies and this based on incorrect reports from the Shrine so this  lead to overcorrection

## Tools Used
- manual review
## Recommended Mitigation Steps
-
Incorrect interest accrual during withdraw
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
This would lead to users not having to pay for redistributed debt when they withdraw and also major discrepancies between the total debts of the trove and its deposits balance.

## Proof of Concept
When users perform actions that changes the balance of there trove, interest is accrued or accumulated on the trove as debt, The issue is that when a user tries to withdraw the interest is accrued after reading their balance.
https://github.com/code-423n4/2024-01-opus/blob/main/src/core/shrine.cairo#L1379
```solidity
fn withdraw_helper(ref self: ContractState, yang: ContractAddress, trove_id: u64, amount: Wad) {
            let yang_id: u32 = self.get_valid_yang_id(yang);

            // Fails if amount > amount of yang deposited in the given trove
            let trove_balance: Wad = self.deposits.read((yang_id, trove_id));<@
            assert(trove_balance >= amount, 'SH: Insufficient yang balance');

            let new_trove_balance: Wad = trove_balance - amount;
            let new_total: Wad = self.yang_total.read(yang_id) - amount;//<@

            self.charge(trove_id);//@audit charge trove after reading balance

            self.yang_total.write(yang_id, new_total);
            self.deposits.write((yang_id, trove_id), new_trove_balance);//<@

            // Emit events
            self.emit(YangTotalUpdated { yang, total: new_total });
            self.emit(DepositUpdated { yang, trove_id, amount: new_trove_balance });
        }
```
within `charge()` the interest accumulated is added as debt to the trove and the yang balance is also updated when their a redistribution but its immediately overridden by the cache variable defined before the call to `charge()` within `withdraw_helper()`.
https://github.com/code-423n4/2024-01-opus/blob/main/src/core/shrine.cairo#L1400
```solidity
fn charge(ref self: ContractState, trove_id: u64) {
            
            ...SNIP
            // Pull undistributed debt and update state
            let trove_yang_balances: Span<YangBalance> = self.get_trove_deposits(trove_id);
            let (updated_trove_yang_balances, compounded_trove_debt_with_redistributed_debt) = self
                .pull_redistributed_debt_and_yangs(trove_id, trove_yang_balances, compounded_trove_debt);

            // If there was any exceptional redistribution, write updated yang amounts to trove
            if updated_trove_yang_balances.is_some() {
                let mut updated_trove_yang_balances = updated_trove_yang_balances.unwrap();
                loop {
                    match updated_trove_yang_balances.pop_front() {
                        Option::Some(yang_balance) => {
                            self.deposits.write((*yang_balance.yang_id, trove_id), *yang_balance.amount);//@audit users deposits are updated when any redistributed debt
                        },
                        Opti
Rewards are incorrectly calculated in `Absorber` if the epoch changes
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

In the current implementation of `Absorber`, users can provide yin to the contract and get provider rewards and absorbed assets in return. Moreover, they can remove their yin back if it's still in the contract and wasn't absorbed. The problem is that the provider rewards are incorrectly calculated if the epoch changes. This may effectively lead to providers lose their rewards as they have more rewards to get than actually in the contract.

## Proof of Concept

Let's say there are 3 users who deposited yin in one epoch. 

1. User1 calls `provide()` with amount of 1000 and there are no currently any rewards distributed as `bestow()` can only be called to distribute rewards if the contract is operational:

https://github.com/code-423n4/2024-01-opus/blob/main/src/core/absorber.cairo#L921-924
```
  let total_shares: Wad = self.total_shares.read();
            if !is_operational_helper(total_shares) {
                return;
            }

```

So now `provider_last_reward_cumulative` = 0 as `asset_amt_per_share` for this particular epoch wasn't updated because `bestow()` is not called.


2. Now user2 calls `provide()` with amount of 2000 yin and at this time 1000 reward tokens are blessed from `Blesser`. The state is updated as following:

`asset_amount_per_share` = approximately 1 token per share as total shares are not updated for the user2 and we divide by the amount of shares of the user1. So it's updated to 1 here:

https://github.com/code-423n4/2024-01-opus/blob/main/src/core/absorber.cairo#L950-959

```
let epoch_reward_info: DistributionInfo = self
                        .cumulative_reward_amt_by_epoch
                        .read((reward.asset, epoch));
                    let total_amount_to_distribute: u128 = blessed_amt + epoch_reward_info.error;

                    let asset_amt_per_share: u128 = u128_wdiv(total_amount_to_distribute, total_recipient_shares.val);
                    let actual_amount_distributed: u128 = u128_wmul(asset_amt_per_share, total_recipient_shares.val);
                    let error: u128 = total_amount_to_distribute - actual_amount_distributed;

                    let updated_asset_amt_per_share: u128 = epoch_reward_info.asset_amt_per_share + asset_amt_per_share;

```

3. User3 provides 2000 yin and 2000 reward tokens are blessed again from the `Blesser`. So the `Absorber` contract has 3000 reward tokens available at the moment. The state is updated as following:

`asset_amount_per_share` = 2000 (total_amount_to_distribute) / 3000 (total_recipient_shares). Actually, it will be slightly less than 3000 as we substract the initial shares from it but it doesn't matter in this case => this variable is updated to 1.66 asset amount per share


4. Now the absorption happens and all the yin tokens are drained out.

5. User1 decides to claim his rewards and absorbed assets in a new e
Forced Liquidation through asset deposit
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The mechanics of the Opus Protocol revolves around troves. For any action to be taken in the protocol, the user first needs to at least open a trove from the `Abbot` module. Opening a trove is done by a the `openTrove` function which on a high level:
1. sets the owner of a trove ID
2. Deposit requested Yangs (collateral) by calling the `deposit_helper ()` function
3. Forges Yin (synthetic) if necessary. 

For step 2, the contract in the `deposit_helper` function, it calls `enter` function of the `sentinel` contract and then `deposit` of the shrine contract. 

The `sentinel` `enter` function performs important checks including ascertaining that a `Gate` has been deployed for the given `yang`(collateral token) by checking it is live via `yang_is_live` as seen here https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/sentinel.cairo#L250-L259 and here https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/sentinel.cairo#L281.  Basically, the `sentinel` contract is responsible for determining the types of `yang` (collateral) that is allowed by the protocol and it can as well add `yangs` collateral tokens by calling `add_yang`. So, it checks if the passed collateral token (yang) by user is valid or not.

The `shrine` `deposit` function on the other hand, updates the trove's deposits by the `amount` passed as seen here: https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L812-L832. 

Therefore, when opening a trove, the contract asserts that the given yang (collateral token) is valid via the sentinel and updates `deposits` of the `collateral token` (yang) in the trove. 

**It is important to note at this point, that, the type of collateral token (yang) is determined by the protocol, sentinel**

There is another function `deposit ()` function in the `abbot` module that allows **anyone** to deposit their yang (collateral) to **any trove id** as seen here https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/abbot.cairo#L191-L200. This action would increase the `deposits` of the `trove id` if the `yang` is valid. 

It is important to note that you do not need to forge a loan while opening a trove, this can be done later via the `forge` function. 

With the aforementioned context, let as examine what can possibly go wrong with this system design. 
1. Let's say the Opus Protocol through the sentinel validates three collateral types `ETH`, `WBTC` and `STARK`. 
2. A user Bob after evaluating the market conditions decides to open a trove with only `ETH` and `WBTC` collateral tokens without forging a loan. They have determined that  `STARK` collateral token is too volatile for them, hence their trove onl
Breaking of conversion rate properties in the gate module
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
According to the project documentation here:P https://demo-35.gitbook.io/untitled/smart-contracts/gate-module, there are certain properties that should hold true at all times. The property number 2 reads `The conversion rate of a yang to its underlying collateral asset should remain unchanged in both enter (if there is non-zero amount of the collateral asset before enter ) and exit (if there is non-zero amount of the collateral asset after exit)`. Basically, `enter` and `exit` actions are the only ones which can affect the conversion rate. 

Now, in calculating the conversion rate, the contract uses the `convert_to_yang_helper` which in turn uses `get_total_assets_helper` which is defined as follows:
```
    fn get_total_assets_helper(asset: IERC20Dispatcher) -> u128 {
        asset.balance_of(get_contract_address()).try_into().unwrap()
    }
```
So the contract uses the `gate_contract's` asset balance in calculating the conversion rate. 

`asset.balance_of(get_contract_address()` is easily manipulable. Because the contract directly reads its balance, an attacker can directly send the asset tokens to the `gate_contract` which would in turn increase its asset balances. Therefore, effectively changing the conversion rate before `enter` action or `exit` action.

## Proof of Concept
I have refactored the following test and after the first deposit and before the second deposit, I have directly sent asset tokens to the `gate_contract` which would in turn increase its asset balances effectively affecting the conversion rate. 
I have used `snforge` `PrintTrait` to print out the different conversion rates as a result of this manipulation.
```
 #[test]
    fn test_gate_multi_user_enter_exit_with_rebasing() {
        let (shrine, eth, gate) = gate_utils::eth_gate_deploy(Option::None);
        gate_utils::add_eth_as_yang(shrine, eth);

        let shrine = IShrineDispatcher { contract_address: shrine };
        let eth = IERC20Dispatcher { contract_address: eth };
        let gate = IGateDispatcher { contract_address: gate };

        let user1: ContractAddress = common::trove1_owner_addr();
        let trove1: u64 = common::TROVE_1;
        let enter1_amt = 50_u128 * WAD_SCALE;
        let enter2_amt = 30_u128 * WAD_SCALE;
        let asset_amt = 20_u128 * WAD_SCALE;

        gate_utils::approve_gate_for_token(gate.contract_address, eth.contract_address, user1);

        // fund user1
        start_prank(CheatTarget::One(eth.contract_address), common::eth_hoarder());
        eth.transfer(user1, (enter1_amt + enter2_amt).into());
        stop_prank(CheatTarget::One(eth.contract_address));
        //
        // first deposit to trove1
        //

        // simulate sentinel calling enter
        start_prank(CheatTarget::One(gate.contract_address), gate_utils::mock_sentinel());
        let