xanoutas/solidity-security-findings huggingface dataset:
after shut,  no pull redistribution yang will be locked
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Vulnerability details

in `caretaker.release()`
We can release the remaining yang.
```rust
        fn release(ref self: ContractState, trove_id: u64) -> Span<AssetBalance> {
            let shrine: IShrineDispatcher = self.shrine.read();
...
            loop {
                match yangs_copy.pop_front() {
                    Option::Some(yang) => {
@>                      let deposited_yang: Wad = shrine.get_deposit(*yang, trove_id);
                        let asset_amt: u128 = if deposited_yang.is_zero() {
                            0
                        } else {
                            let exit_amt: u128 = sentinel.exit(*yang, trove_owner, trove_id, deposited_yang);
                            // Seize the collateral only after assets have been
                            // transferred so that the asset amount per yang in Gate
                            // does not change and user receives the correct amount
                            shrine.seize(*yang, trove_id, deposited_yang);
                            exit_amt
                        };
                        released_assets.append(AssetBalance { address: *yang, amount: asset_amt });
                    },
                    Option::None => { break; },
                };
            };
```
As above, we can only release the yang that already exists in the trove: `shrine.get_deposit(*yang, trove_id);`

When `shire.get_live() == false`, we can no longer perform `shire.pull_redistributed_debt_and_yangs()`.
So if there is any yang that hasn't been pull, it can't be retrieved through `caretaker.release()`
This part of the yang will be locked in the contract.

## Impact

After `shut()`, the `redistributed yang` that hasn't been pull will be locked in the contract.

## Recommended Mitigation
add pull_when_shut()
```rust
        fn pull_when_shut(ref self: ContractState, trove_id: u64) {
            if self.is_live.read() {
                return;
            }
           let trove: Trove = self.troves.read(trove_id);

            let current_interval: u64 = now();
            let trove_yang_balances: Span<YangBalance> = self.get_trove_deposits(trove_id);
            let (updated_trove_yang_balances, _) = self
                .pull_redistributed_debt_and_yangs(trove_id, trove_yang_balances,Wad { val: 0 });

            // 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);
when add new yang , get_recent_asset_absorption_error() may reach recursion limit
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Vulnerability details


In `update_absorbed_asset()`
it will use `get_recent_asset_absorption_error()` to get the most recent `last_error`. 
`get_recent_asset_absorption_error()` internally is a recursive call to the next `absorption`.
Example:
absorption_id = 1000

get_recent_asset_absorption_error(1000) -> get_recent_asset_absorption_error(999) .....get_recent_asset_absorption_error(0)

```rust
        fn get_recent_asset_absorption_error(self: @ContractState, asset: ContractAddress, absorption_id: u32) -> u128 {
            if absorption_id == 0 {
                return 0;
            }

            let absorption: DistributionInfo = self.asset_absorption.read((asset, absorption_id));
            // asset_amt_per_share is checked because it is possible for the error to be zero.
            // On the other hand, asset_amt_per_share may be zero in extreme edge cases with
            // a non-zero error that is spilled over to the next absorption.
            if absorption.asset_amt_per_share.is_non_zero() || absorption.error.is_non_zero() {
                return absorption.error;
            }

            self.get_recent_asset_absorption_error(asset, absorption_id - 1)
        }
```

Normally, it will not recursively call to `absorption_id=0`, because in most cases `absorption.asset_amt_per_share` or `absorption.error` will have values.

But there is one exception: for the newly added `yang`, the old `absorption`'s `asset_amt_per_share/error` will both be 0, causing it to recurse from 1000 to 0. 

`get_recent_asset_absorption_error(1000) -> .....get_recent_asset_absorption_error(0) `

Most languages have a `recursion limit`.

Python's default is 1000, I'm not sure what `cairo`'s is.

I tried testing to 15000 and it gave an error.
>Could not reach the end of the program. RunResources has no remaining steps

## Proof of Concept

```rust
add to test_absorber.cairo
    #[test]
    fn test_recall_recent() {      
        let absorption_id:u32 = 15000;
        let (_, _, _, absorber, _, _) = absorber_utils::absorber_deploy(
            Option::None, Option::None, Option::None, Option::None, Option::None, Option::None
        );        
        absorber.get_absorption_error(absorption_id);
    }

add to absorber.cairo

    impl IAbsorberImpl of IAbsorber<ContractState> {
        fn get_absorption_error(ref self: ContractState, absorption_id: u32) -> u128 {
            return self.get_recent_asset_absorption_error(get_contract_address(),absorption_id);
        }    
```

```console
$ scarb test -vvv test_recall_recent

[FAIL] opus::tests::absorber::test_absorber::test_absorber::test_recall_recent

Failure data:
    Got an exception while executing a hint: Hint Error: Error in the called contract (0x041f7a65be670b7019cd46fcc710d8eea72e610b66c3cf9bdfddced306e6adf8):
Error at pc=0:16838:
Coul
Variation of inflation attack in `absorber` makes it possible for an attacker to start griefing users
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

It is possible to start griefing users who provide their yin to the `absorber`, making them lose all of their funds in exchange of `0` shares (a variation of the vanilla first-depositor front-running attack).

## Proof of Concept

Like any other ERC4626 vault, the `absorber` implements a deposit function called `provide` that, in a nutshell, lets users deposit their yin and receive shares of the underlying vault. To prevent the vanilla front-running attack, the first provider must deposit a value higher or equal than `INITIAL_SHARES`, so that he will receive the difference:

[**absorber, function convert_shares**](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/absorber.cairo#L670C1-L676C14)

```rs
        fn convert_to_shares(self: @ContractState, yin_amt: Wad, round_up: bool) -> (Wad, Wad) {
            let total_shares: Wad = self.total_shares.read();

            if INITIAL_SHARES > total_shares.val {
                // By subtracting the initial shares from the first provider's shares, we ensure that
                // there is a non-removable amount of shares. This subtraction also prevents a user
                // from providing an amount less than the minimum shares.
                assert(yin_amt.val >= INITIAL_SHARES, 'ABS: provision < minimum');
                return ((yin_amt.val - INITIAL_SHARES).into(), yin_amt);
            }

            ...

        }
```

and if we go to the definition of `INITIAL_SHARES`:

```rs
    const INITIAL_SHARES: u128 = 1000; // 10 ** 3 (Wad);
```

we see this amount is negligible taking into account the decimals of the yin token (18 as seen [here](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L417)). If we add the fact that `convert_to_shares` can return `0` shares and `provide` does not revert if that happens, the next situation arises:

1. Bob, our average user, calls `provide` with his own yin to participate in absorptions by being credited a certain number of shares. Note that he is the first depositor and lets say the amount provided by Bob is `X`
2. Alice, our attacker, sees this, front-runs Bob and provides exactly `INITIAL_SHARES`, so that she receives `0` shares AND `self.total_shares = self.yin_erc20().balance_of(absorber) = INITIAL_SHARES`
3. After that, Alice transfers directly to the `absorber` `X * INITIAL_SHARES`, so that `self.yin_erc20().balance_of(absorber) = INITIAL_SHARES + X * INITIAL_SHARES` whilst `self.total_shares` remains unchanged
4. Bob transaction gets executed but receives `0` shares in exchange due to:
   1. `self.convert_to_shares` nor `self.provide` do not revert in that situation
   2. `self.convert_to_shares` does not round up if called from `self.provide`, as the flag is false:
Improper rounding down in Abbot::withdraw()
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Improper rounding down in Abbot::withdraw()

## Proof of Concept
In Abbot::withdraw(), we will decrease yang amount according to asset amount. The calculation should be round up. Because this yang amount will be decreased from user's account. We should round up to profit protocol.

## Tools Used
Manual

## Recommended Mitigation Steps
Round up yang amount when we decrease yang amount from user's account.


## Assessed type

Other
malicious donations increase yang price, leading to cumulative_price underflow
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Vulnerability details

In `sentinel.add_yang()`, `INITIAL_DEPOSIT_AMT` was added to prevent attacks.
>Require an initial deposit when adding a yang to prevent first depositor from front-running

However, this does not prevent malicious users from donating to artificially increase the price, leading to an underflow in `yang_prices.cumulative_price`.

For example:
after add_yang(eth):
totalShares: totalAsset : = 1000:1000

A malicious user donates an asset to the gate, suppose the donation is (1e18-1000 ETH):
totalShares: totalAsset : = 1000 : 1e18
exchangeRate = 1e15

Assuming eth = 2000 USD, then the price of yang "sentinel.get_asset_amt_per_yang()" becomes:
> get_asset_amt_per_yang() -> convert_to_assets_helper(WAD_ONE.into())

get_asset_amt_per_yang = WAD_ONE * totalAsset / totalShares = 1e18 * 1e15 = 1e33

price = 2000 USD  * get_asset_amt_per_yang() = 2e36

`cumulative_price` is Wad (uint128) and accumulates every TIME_INTERVAL = 30 minutes:

type(uint128).max / 2e36 = 170 (TIME_INTERVAL) = 170 * 30 minutes  =  3.5 days

As stated above,
if a malicious user maliciously donates to increase the price
eth = 1e18   overflow =  3.5 days
eth = 0.1e18   overflow =  35 days
eth = 0.01e18   overflow =  350 days

## Impact

Malicious donations will not cause errors at the beginning, but after a few days, when updating the price, `shrine.advance()` will fail, making it impossible to update the price, which will cause many problems.

## Recommended Mitigation

`INITIAL_DEPOSIT_AMT` should not be fixed at 1000, it should be passed in by `add_yang()`, and different values should be specified for different assets to increase the cost of donation.
```diff
        fn add_yang(
            ref self: ContractState,
            yang: ContractAddress,
            yang_asset_max: u128,
            yang_threshold: Ray,
            yang_price: Wad,
            yang_rate: Ray,
            gate: ContractAddress,
+          initial_deposit_amt:u128
        ) {
```



## Assessed type

DoS
No return value is determined for token transfers
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

It is possible that the transfer fails without a rollback, and the user fails to get the reward.

## Proof of Concept
The erc20 transfer calls an external contract via the low-level method starknet::call_contract_syscall. If you don't judge the return value, it is possible that the transfer fails without a revert and the user fails to get the reward.

## Tools Used

Manual Review

## Recommended Mitigation Steps

Add the judgment that the return value is true. If the return value is not true, revert


## Assessed type

Token-Transfer
yang Pair ID Validation Flaw for Tokens with Zero Decimal Values
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/external/pragma.cairo#L149


# Vulnerability details

## Impact
During the process of setting the Yang Pair ID, a validation check is implemented to ensure that the decimal value returned for a specific asset is always non-zero. In cases where the decimal value is zero, the corresponding function is reverted, leading to the exclusion of that pair ID from the system.

 certain tokens have the potential to return zero as their decimal value. One such instance is observed with the Gol token, a prominent ERC-20 token on the StarkNet platform. The token with the address 0x06a05844a03bb9e744479e3298f54705a35966ab04140d3d8dd797c1f6dc49d0 is found to have a decimal value of 0, which triggers the reversion of the associated function.

## Proof of Concept
https://starkscan.co/token/0x06a05844a03bb9e744479e3298f54705a35966ab04140d3d8dd797c1f6dc49d0

## Tools Used
manual review

## Recommended Mitigation Steps
Add the whitelisting mechanism for checking such type of tokens


## Assessed type

Decimal
No way to take out initial_yang_amts after closing shrine
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/main/src/core/sentinel.cairo#L201-L206


# Vulnerability details

## Impact
When a new yang is added, to avoid deposit front running, an initial token amount of 1000, will be added. But there is no mechanism implementation to takeout those tokens back. This will make the tokens to be stuck inside the shrine.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Add a mechanism to get the initial token amounts out even after closing shrine.


## Assessed type

Token-Transfer
The remainder does not require further precision reduction
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

Will cause trove_debt to be lower than expected

## Proof of Concept
The main effect of the method [pull_redistributed_debt_and_yangs](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L2040-L2198) is to pull the redistributed debt and yangs. instead of rounding down the precision when distributing the debt, the remainder is added to the [cumulative_r](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L2138).
Then it is added to trove_debt

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

```solidity
           let cumulative_r: u128 = cumulative_r.try_into().unwrap();
 -->          trove_debt += (cumulative_r / WAD_SCALE).into(); // shouldn't then divide by WAD_SCALE
```
The problem occurs when adding to trove_debt. shouldn't then divide by WAD_SCALE

Let's take an example to illustrate the problem.
Suppose 1089 and 1091 with a precision of 1000. the arithmetic procedure is as follows:
```math
1089*1091= 1188099
1089*1091/1000 = 1188…99
1188*1000 + 99  = 1089*1091 = 1188099
99/1000 =  0
```
The 99 is added to the precision 1000 and does not need to be divided by 1000; if it were divided by 1000, the result would be less than the original value 1188099
## Tools Used

Manual Review

## Recommended Mitigation Steps
Change
```solidity
trove_debt += (cumulative_r / WAD_SCALE).into();
```
to 
```solidity
trove_debt += (cumulative_r).into();
```


## Assessed type

Error
`shrine.update_rates's check` is not sufficient
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
At the end of `shrine.update_rates`, the function verifies that all rates were updated correctly, but this check is not sufficient. There might be a case that the rates aren't updated correctly.


## Proof of Concept
At the end of [shrine.update_rates](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L652-L720), the function verifies that all rates were updated correctly by check if `yang_rates` [is zero](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L715)
```rust
 703             // Verify that all rates were updated correctly
 704             // This is necessary because we don't enforce that the `yangs` array really contains
 705             // every single yang, only that its length is the same as the number of yangs.
 706             // For all we know, `yangs` could contain one yang address 10 times.
 707             // Even though this is an admin/governance function, such a mistake could break
 708             // interest rate calculations, which is why it's important that we verify that all yangs'
 709             // rates were correctly updated.
 710             let mut idx: u32 = num_yangs;
 711             loop {
 712                 if idx == 0 {
 713                     break ();
 714                 }
 715                 assert(self.yang_rates.read((idx, rate_era)).is_non_zero(), 'SH: Incorrect rate update'); <<<--- Here checks yang_rates is zero
 716                 idx -= 1;
 717             };
 718 
 719             self.emit(YangRatesUpdated { rate_era, current_interval, yangs, new_rates });
```

But such check is not sufficient.
According to [shrine.cairo#L670-L674](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L670-L674), `rate_era` will be increased only if `latest_rate_era_interval != current_interval`, and `current_interval` will be updated [every 30mins](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L48), so if `shrine.update_rates` is called twice within the same era, there might be problem.

So in a special case that:
1. at era T0, `shrine.update_rates` is called, and everything is updated correctly.
1. at the same era T0, `shrine.update_rates` is called again, at this time, **yangs could contain one yang address n times.**
1. in such case, because step1 has saved the stale price, the call in step2 will be successfully executed. **But this is against the design**

## Tools Used
VIM

## Recommended Mitigation Steps
Please consider using a LegacyMap::<ContractAddress, bool> local variables to save the updated yangs, and at the end of the function, check if all yangs has been updated.


## Assessed
`purger.get_liquidation_penalty_internal` doesn't conform with the document
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
`purger.get_liquidation_penalty_internal` doesn't conform with the document, which is against the design.

## Proof of Concept
Quoting from the [Liquidation penalty](https://demo-35.gitbook.io/untitled/smart-contracts/purger-module#liquidation-penalty)
>The liquidation penalty for liquidate is given by the below equations:
>MAX_PENALTY=0.125
>max_possible_penalty=min(MAX_PENALTY,(1−ltv)/ltv)
>penalty_at_ltv = MIN_PENALTY + ( ltv / threshold ) − 1
> penalty=min(penalty_at_ltv,max_possible_penalty)

But the function [purger.get_liquidation_penalty_internal](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/purger.cairo#L549-L576) doesn't follow the document in [purger.cairo#L572](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/purger.cairo#L572):
```rust
548     // Returns `Option::None` if the trove is not liquidatable, otherwise returns the liquidation penalty
549     fn get_liquidation_penalty_internal(threshold: Ray, ltv: Ray) -> Option<Ray> {
550         if ltv <= threshold {
551             return Option::None;
552         }
553
554         // Handling the case where `ltv > 1` to avoid underflow
555         if ltv >= RAY_ONE.into() {
556             return Option::Some(RayZeroable::zero());
557         }
558
559         // If the threshold is below the given minimum, we automatically
560         // return the minimum penalty to avoid division by zero/overflow, or the largest possible penalty,
561         // whichever is smaller.
562         if threshold < MIN_THRESHOLD_FOR_PENALTY_CALCS.into() {
563             // This check is to avoid overflow in the event that the
564             // trove's LTV is also extremely low.
565             if ltv >= MIN_THRESHOLD_FOR_PENALTY_CALCS.into() {
566                 return Option::Some(min(MAX_PENALTY.into(), (RAY_ONE.into() - ltv) / ltv));
567             }
568             return Option::Some(MAX_PENALTY.into());
569         }
570
571         let penalty = min(
572             min(MIN_PENALTY.into() + ltv / threshold - RAY_ONE.into(), MAX_PENALTY.into()), (RAY_ONE.into() - ltv) / ltv <<<--- Here, the code doesn't follow the design
573         );
574
575         Option::Some(penalty)
576     }
```

## Tools Used
VIM

## Recommended Mitigation Steps
```diff
diff --git a/src/core/purger.cairo b/src/core/purger.cairo
index 6a36bbc..b651035 100644
--- a/src/core/purger.cairo
+++ b/src/core/purger.cairo
@@ -569,7 +569,7 @@ mod purger {
         }

         let penalty = min(
-            min(MIN_PENALTY.into() + ltv / threshold - RAY_ONE.into(), MAX_PENALTY.into()), (RAY_ONE.into() - ltv) / ltv
+            min(MIN_PENALT