xanoutas/solidity-security-findings huggingface dataset:
By forcing the system to enter recovery mode, attacker can profit from liquidation previously healthy troves
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

A whale can force the system to enter recovery mode simply by creating a trove with large deposits and minting a lot of yang.

In the recovery mode, the threshold for all troves is scaled down. If there are some troves in the system with already reasonably high LTV, entering the recovery mode will make these troves' LTV to cross their thresholds. The whale can now liquidate these troves and collect liquidation profits.

## Proof of Concept

The PoC for this is in the existing shrine test `test_recovery_mode_previously_healthy_trove_now_unhealthy`. The user does not get the option to add to their position once the protocol enters the recovery mode, because both making the trove unhealthy and liquidating it can be done in a single transaction.

## Tools Used

Manual review and Starknet foundry.

## Recommended Mitigation Steps

Allow some grace period after triggering recovery mode.



## Assessed type

Other
Redistribution can be used to force invalid oracle price updates to enter the system
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/main/src/core/purger.cairo#L362
https://github.com/code-423n4/2024-01-opus/blob/main/src/core/seer.cairo#L170
https://github.com/code-423n4/2024-01-opus/blob/main/src/external/pragma.cairo#L198


# Vulnerability details

## Impact

Redistribution reduces the number of yang shares in the system. Especially ordinary redistribution can reduce it by several orders of magnitude. This can lead to incorrect calculation of trove's health, making de facto healthy troves eligible for liquidation. To deal with that, the price of the yang in terms of underlying asset must be updated after the redistribution. This is done in https://github.com/code-423n4/2024-01-opus/blob/main/src/core/purger.cairo#L362, where `self.seer.read().update_prices()` is called. It in turn calls `update_prices_internal` which asks the Oracle for a new price.

However, this approach leads to two problems:
1. If the oracle update fails for whatever reason, the price of yang is not updated, and then healthy troves that are recipients of the redistributed debt can be liquidated (and potentially other problems created in the system, due to the discrepancy in the yang price and the real amount of assets backing each unit of yang).
2. Normally, we can expect the oracle update to not fail, however, this can be used to bypass the `is_valid_price_update` check.


Let's assume that the attacker can manipulate one of the sources of the oracle, and create updates with price data just from this source. Normally `is_valid_price_update` would cause such updates to be ignored by the system, however, redistribution can be used to force these updates to be accepted. This will allow the attacker to manipulate the price of the redistributed yang much more easily than they could otherwise.


## Tools Used

Manual review.

## Recommended Mitigation Steps

Do not fetch a new oracle price after redistributions. Simply use the old oracle price, with the new yang-per-backing-asset ratio, to compute the new yang price.

Additionally, do not allow to create positions with `ltv` very close to `threshold`. Require that some safety margin is left when minting `debt`. This will make it less quick and  more difficult to execute attacks that requires triggering redistributions.






## Assessed type

Oracle
A user can steal from the shrine by forcing redistribution of their trove; due to incorrect logic trove debt will be reset but yangs kept
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

Let's assume two yangs in the system, yang A and yang B, and two users:
* User U1 with trove #1 with zero A units, 1000 B units, and 500 yin debt;
* User U2 with trove #2 10000 A unit, 1000 B units, and 500 yin debt.

If the user U1 can force redistribution of their position, then they can steal from the shrine due to a bug in the code. The function `redistribute_helper` loops through all yangs in order, including those not in the trove #1. Since `trove_yang_amt.is_zero()` returns `true` for yang A, the `updated_trove_yang_balances` array is updated early and then `continue` statement is executed.

However, since the `new_yang_totals` array is not updated in the iteration of the loop, some values of `updated_trove_yang_balances` end up never being used.

Let's assume 100% redistribution. After the all loop is fully executed, the two arrays contain:
`updated_trove_yang_balances = [(A, 0), (B, 0)];`
`new_yang_totals = [(B, 1000)];`

The final loop of the function is executed just once. Its first and only iteration writes the new total B value. However, it does not update the amount of B in the trove #1, since `(B, 0)` is the second element of the first array.
The final state is that trove #1 still has 1000 units of B, but no more debt. The user U1 can now withdraw all 1000 units from the trove #1.


This bug violates the shrine invariant "The total amount of a yang is equal to the sum of all troves' deposits of that yang (this includes any exceptionally redistributed yangs and their accompanying errors) and the initial amount seeded at the time of add_yang."


## Proof of Concept

    #[test]
    fn test_shrine_redistribution_bug() {
        let shrine: IShrineDispatcher = shrine_utils::shrine_setup_with_feed(Option::None);

        // Manually set up troves so that all troves uses just yang1
        let yangs: Span<ContractAddress> = shrine_utils::three_yang_addrs_reversed();
        let yang_addr = *yangs.at(1); // select the middle one

        let forge_amt: u128 = 1_000_000_000_000_000_000_000;

        // Set up trove1 with some yang and some debt
        let trove1_owner = common::trove1_owner_addr();
        let redistributed_trove: u64 = common::TROVE_1;
        start_prank(CheatTarget::All, shrine_utils::admin());
        shrine.deposit(yang_addr, redistributed_trove, shrine_utils::TROVE1_YANG1_DEPOSIT.into());
        shrine.forge(trove1_owner, redistributed_trove, forge_amt.into(), 0_u128.into());

        // Set up trove1 with some yang and some debt
        let trove2_owner = common::trove2_owner_addr();
        let recipient_trove: u64 = common::TROVE_2;
        shrine.deposit(yang_addr, recipient_trove, shrine_utils::TROVE1_YANG1_DEPOSIT.into());
        shrine.forge(trove2_owner, recipient_trove, forge_amt.into(), 0_u128.into());

        println!("before:");
        println!(" trove1 yang={}", shr
yang can be permanently suspended even if the SUSPENSION GRACE PERIOD has not elapsed
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
A `yang` (collateral) can be suspended and if the `SUSPENSION_GRACE_PERIOD` elapses, then the `yang` will be permanently suspended. This is what the docs here https://demo-35.gitbook.io/untitled/smart-contracts/shrine-module state
```
From the time of the suspension and before the SUSPENSION_GRACE_PERIOD elapses, the suspension can be reversed. However, once the SUSPENSION_GRACE_PERIOD elapses, the yang will be permanently suspended i.e. delisted.
```
Therefore, it is clear that for a `yang` to be suspended, the `SUSPENSION_GRACE_PERIOD` has to elapse. 

This logic is implemented here: https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1235-L1246 as
```
        fn get_yang_suspension_status_helper(self: @ContractState, yang_id: u32) -> YangSuspensionStatus {
            let suspension_ts: u64 = self.yang_suspension.read(yang_id);
            if suspension_ts.is_zero() {
                return YangSuspensionStatus::None;
            }

            if get_block_timestamp() - suspension_ts < SUSPENSION_GRACE_PERIOD {
                return YangSuspensionStatus::Temporary;
            }

            YangSuspensionStatus::Permanent
        }
```
We note that this line of code `if get_block_timestamp() - suspension_ts < SUSPENSION_GRACE_PERIOD` determines if a `yang` is Temporary suspended or Permanently. 
So the function calculates the difference between now(`get_block_timestamp`) and when the `yang` was suspended and stored as `suspension_ts`. If this difference is less than `SUSPENSION_GRACE_PERIOD` then it is regarded as a temporary suspension.

For `SUSPENSION_GRACE_PERIOD` to elapse it means that it is greater than the difference(`get_block_timestamp() - suspension_ts`).

Interestingly, the function does not factor in when difference between now(`get_block_timestamp`) and `suspension_ts` is equal to `SUSPENSION_GRACE_PERIOD`. It simply checks if the difference(`get_block_timestamp() - suspension_ts`) is less than `SUSPENSION_GRACE_PERIOD` and if it is anything else (equal to || greater than) it is regarded as a permanent suspension.

Because if the difference(`get_block_timestamp() - suspension_ts`) == `SUSPENSION_GRACE_PERIOD` then `SUSPENSION_GRACE_PERIOD` has not elapsed and it should therefore not be regarded as a Permanent Suspension.


## Proof of Concept
I have refactored the following test case as 
```
 #[test]
    fn test_yang_suspend_and_unsuspend() {
        let shrine_addr: ContractAddress = shrine_utils::shrine_deploy(Option::None);
        let mut spy = spy_events(SpyOn::One(shrine_addr));

        shrine_utils::shrine_setup(shrine_addr);
        let shrine = shrine_utils::shrine(shrine_addr);
        let yang = shrine_utils::yang1_addr();
        let start_ts = shrine_utils::DEPLOYMENT_TIMESTAMP;
Under-Collateralization of Yins Prior to Complete User Reclamation
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/caretaker.cairo#L341-L363
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/caretaker.cairo#L215-L240


# Vulnerability details

## Summary 
When the caretaker shuts down the shrine, it calculates the backing percentage (backing_pct). This percentage represents the value of collaterals used to support Yin assets. Users can reclaim their assets by burning minted Yin. However, a problem arises if the price of assets falls before everyone has reclaimed their assets. In such cases, users might not be able to reclaim their assets in full(this is not a problem but they will use a 1 USD pegged stablecoin(yin) instead of claim some collateral and this makes yin under collateralized), as they receive a proportion of assets relative to their Yin. This leads to under-collateralized Yin, and users may not be able to reclaim their assets due to losses. also price of yangs are not updated before calculating backing_pct.
## Vulnerability Details 
The caretaker's shut function computes the backing percentage (backing-pct). This ensures that for every Yin minted as debt, there are corresponding assets available for users to reclaim, equivalent to their value. For instance, if the total value of shrine yangs is 1000 USD and the total debt is 800 USD, 800 USD worth of assets would be set aside for users to reclaim. This means that for every Yin they burn, users would receive $1 worth of assets.
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/caretaker.cairo#L211-L240

then users can reclaim their assets through the reclaim function which would burn yin and transfer underlying collateral to user.
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/caretaker.cairo#L341-L363

If the value of the underlying collateral decreases, the assets earmarked for reclamation would have a lower value than the Yin tokens that users provide for reclamation. As a result, Yin tokens are under-collateralized which would be de-pegged from 1 USD
## Impact
yins become under-collateralized 

## Tools Used
Manual Review 
## Recommended Mitigation Steps
You might want to consider increasing the backing percentage by an additional 1% to incentivize users to reclaim their assets promptly, mitigating potential risks associated with price fluctuations.




## Assessed type

Other
Absorber is Susceptible to Flash Loan Attacks to Steal Yield
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/types.cairo#L226-L231
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/absorber.cairo#L894-L908


# Vulnerability details

## Impact

The identified vulnerability within the absorber contract allows an attacker to disproportionately claim yields without committing any capital at risk. This is achieved by exploiting the contract's failure to track the original share ownership before and after liquidity provision. An attacker can inflate their share temporarily through a flash loan, claim a majority of the yields, and exit, leaving genuine liquidity providers at a disadvantage.

## Vulnerabilty Description

The `Request` struct only tracks the time and not the owned shares prior to the request.

https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/types.cairo#L226-L231

```rust
#[derive(Copy, Debug, Drop, Serde)]
struct Request {
    timestamp: u64, // Timestamp of request
    timelock: u64, // Amount of time that needs to elapse after the timestamp before removal
    has_removed: bool, // Whether provider has called `remove`
}

```

In `request()`, the `assert_provider()` will check if the request issuer has at least one share, which can be easily bypassed by depositing a small amount of yin.

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

```rust
fn assert_provider(provision: Provision) {
        assert(provision.shares.is_non_zero(), 'ABS: Not a provider');
    }

```

In the `remove()` function, the absorber contract verifies the liquidity provider's eligibility for withdrawal by utilizing the `assert_can_remove()` helper function.
The `assert_can_remove()` function, however, does not check the amount of shares the provider held prior to issuing a withdrawal request.

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

```rust
fn assert_can_remove(self: @ContractState, request: Request) {
            let shrine = self.shrine.read();
            // Removal is not allowed if Shrine is live and in recovery mode.
            if shrine.get_live() {
                assert(!shrine.is_recovery_mode(), 'ABS: Recovery Mode active');
            }

            assert(request.timestamp.is_non_zero(), 'ABS: No request found');
            assert(!request.has_removed, 'ABS: Only 1 removal per request');

            let current_timestamp: u64 = starknet::get_block_timestamp();
            let removal_start_timestamp: u64 = request.timestamp + request.timelock;
            assert(removal_start_timestamp <= current_timestamp, 'ABS: Request is not valid yet');
            assert(current_timestamp <= removal_start_timestamp + REQUEST_VALIDITY_PERIOD, 'ABS: Request has expired');
        }
```


## Proof of Concept
The
acess control mechanism absent for set_threshold(...) function
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
new_threshold can be altered & threshold set to MAX_THRESHOLD to increase the debt to collateral ratio

## Proof of Concept


## Tools Used
Visual inspection

## Recommended Mitigation Steps


## Assessed type

Access Control
Incorrect break condition in the `redistribute_helper()` could cause liquidation to fail
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The `redistribute_helper()` function iterates over each yang in a trove for redistribution. After each iteration, this condition is used to check and break the loop:

```rust
// If debt was rounded up, meaning it is now fully redistributed, skip the remaining yangs
// Otherwise, continue the iteration
if debt_to_distribute_for_yang != raw_debt_to_distribute_for_yang {
    break;
}
```

This condition works under the assumption that if these two values are different, it indicates the function `round_distributed_debt()` has rounded up, and all debt has been fully redistributed. This is because the function `round_distributed_debt` checks if the remaining debt is too small, then adds it to the current yang's `debt_to_distribute`, which mean all debt are redistributed.

```rust
fn round_distributed_debt(
    total_debt_to_distribute: Wad, debt_to_distribute: Wad, cumulative_redistributed_debt: Wad
) -> (Wad, Wad) {
    let updated_cumulative_redistributed_debt = cumulative_redistributed_debt + debt_to_distribute;
    let remaining_debt: Wad = total_debt_to_distribute - updated_cumulative_redistributed_debt;
    
    // @audit If the remaining_debt = 0
    //        then the check `debt_to_distribute_for_yang != raw_debt_to_distribute_for_yang` won't break 
    //        even though there is no remaining debt
    if remaining_debt.val <= ROUNDING_THRESHOLD { 
        return (debt_to_distribute + remaining_debt, updated_cumulative_redistributed_debt + remaining_debt);
    }

    (debt_to_distribute, updated_cumulative_redistributed_debt)
}
```

However, it does not account for the case where `remaining_debt = 0` in the function `round_distributed_debt()`. In this instance, even though all debt has been redistributed, `debt_to_distribute_for_yang` is still equal to `raw_debt_to_distribute_for_yang`, so the loop doesn't break. The iteration of the loop for the next yang will cause an overflow and revert the transactions. As liquidation is time-sensitive, reverting a liquidation transaction could cause the trove to accrue more bad debt and negatively impact the overall shrine health.


## Proof of Concept
Consider the scenario:

1. There are 2 yangs [A, B, C]. After looping through yang B, all debt has been redistributed and `remaining_debt = 0` in the function `round_distributed_debt()`. Now we have `redistributed_debt = debt_to_redistribute`.
2. During the next iteration with yang C, it doesn't consider the current `redistributed_debt` when calculating `raw_debt_to_distribute_for_yang` but only calculates using the total `debt_to_redistribute` and `yang_debt_pct`.
3. Since all debt has now been redistributed, any `raw_debt_to_distribute_for_yang != 0` will cause an overflow when trying to calculate the `remaining_debt` in the function `round_distributed_debt()`.
```rust
// `cumul
When `redistribute_helper()` skip over a zero yang, `new_yang_totals` was not appended cause wrong update for trove yang balances
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

The function `redistribute_helper()` in the Opus codebase may skip over an empty yang during the redistribution process. This can cause an incorrect update to the yang balances within a trove.

## Impact
The `redistribute_helper()` function iterates over each yang in a trove for redistribution. During this process, it maintains two lists: `new_yang_totals` and `updated_trove_yang_balances`. These lists record the new total yang and new trove yang balance after each distribution, respectively. Both lists should have the same index, meaning index `i` in each list should refer to the same yang.

At the end of the function, the values in these two lists are used to write to storage:

```rust
let mut new_yang_totals: Span<YangBalance> = new_yang_totals.span();
let mut updated_trove_yang_balances: Span<YangBalance> = updated_trove_yang_balances.span();
loop {
    match new_yang_totals.pop_front() {
        Option::Some(total_yang_balance) => {
            let updated_trove_yang_balance: YangBalance = *updated_trove_yang_balances.pop_front().unwrap();
            self
                .deposits
                .write((updated_trove_yang_balance.yang_id, trove_id), updated_trove_yang_balance.amount);

            self.yang_total.write(*total_yang_balance.yang_id, *total_yang_balance.amount);
        },
        Option::None => { break; },
    };
};
```

As you can see, each time `new_yang_totals` pops an element, `updated_trove_yang_balances` also pops one element to write to storage. If `new_yang_totals.len() < updated_trove_yang_balances.len()`, some of the last `updated_trove_yang_balances` won't be updated. This can cause incorrect accounting for the trove following liquidation.

Specifically, if there are three yangs A, B, and C, and the balance of B is zero, `new_yang_totals` will only contain values for [A, C], while `updated_trove_yang_balances` contains all values for all [A, B, C]. During the writing to storage, `updated_trove_yang_balances` of C won't be processed.

## Proof of Concept
The scenario described in the Impact section can occur when skipping an empty yang:
```rust
// Skip over this yang if it has not been deposited in the trove
// @audit `new_yang_totals` is not appended
if trove_yang_amt.is_zero() { 
    updated_trove_yang_balances.append(*yang_balance);
    continue;
}
```

During the loop through the yangs of the trove, if `trove_yang_amt` is zero, the function will skip over it and append the same trove balance to `updated_trove_yang_balances`. However, nothing is added to `new_yang_totals`. This can cause a mismatch between these two lists. In the end, the length of `new_yang_totals` would be smaller than `updated_trove_yang_balances`, causing some of the last values in `updated_trove_yang_balances` not to be written to storage.

## Tools Used
Manual Review

## Rec