xanoutas/solidity-security-findings huggingface dataset:
Debt from a liquidated trove may not be redistributed due to an issue inside the function `shrine.redistribute_helper()`
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1881-L1887
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1995-L2005
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/types.cairo#L92-L95
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L2086


# Vulnerability details

## Impact

The function `shrine.redistribute_helper()` may wrongly calculate a liquidated trove's debt amount to be redistributed to other troves to be 0. Thus no debt may be redistributed to other troves upon liquidations.

**Details:**

* Inside the function `shrine.redistribute_helper()` on line 1881 in shrine.cairo, `redistributed_yang_unit_debt` may be computed to be 0 in the case where the denominator `redistributed_yang_recipient_pool` is bigger than the numerator `adjusted_debt_to_distribute_for_yang` in the division, due to rounding down to 0 in fixed-point arithmetic.

* `redistributed_yang_unit_debt` (`YangRedistribution.unit_debt`) is the amount of debt in wad to be distributed to each wad unit of yang, quoting the comment on line 93-94 inside types.cairo.

* `redistributed_yang_unit_debt` is subsequently assigned to `YangRedistribution.unit_debt` upon defining the variable `redistributed_yang_info` (line 1995 shrine.cairo).

* Subsequently a new redistribution entry is written into `self.yang_redistributions` (line 1998-2000 shrine.cairo), where `redistributed_yang_info` is wrongly holding the value 0 for `unit_debt`, thus no debt will be redistributed.

* Note: The calculation for debt to redistribute relies on `unit_debt`. Debt is redistributed whenever a deposit/withdrawal/forge/melt tx happens in the shrine, for example in this trace: `shrine.deposit()->shrine.charge()->shrine.pull_redistributed_debt_and_yangs()`. There inside `shrine.pull_redistributed_debt_and_yangs()` on line 2086 in shrine.cairo the computation of the debt to redistribute is done by using `unit_debt`.

**Example:**

* total troves: 100
* 1 trove is liquidated/redistributed
* each trove has 10 yang deposited, total yang = 1000 yang
* liquidated user's debt: 10 yin (exchange rate yang/yin 1:1)
* yang pool of all other troves: 990 yang
* The shrine then calculates (line 1881 shrine.cairo):
* `adjusted_debt_to_distribute_for_yang` = 10 yin
* `redistributed_yang_recipient_pool` = 990 yang
* `redistributed_yang_unit_debt` = 10 / 990 = 0

**Impact:**

* Debt from a liquidated trove may not be distributed to other troves, leading to an issue, where other troves may receive yang from the liquidated trove without being accounted with additional debt. This may lead to a loss for the protocol, and to a skewed system state that may introduce further issues.

* An attacker may intentionally set up an unhealthy trove together with multiple other troves to make pro
The last user will get 1% of collateral instead of 100% in `Caretaker`
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

After `Shrine` smart contract is shut down, the user can reclaim their collateral by putting `yin` amount as the argument in `reclaim() function. This function uses another function - `preview_reclaim()` to determine the percentage that the user is eligible to claim. However, due to a miscalculated division operation, the last user will eventually get much less than he should.

## Proof of Concept

Let's say there are 3 users who want to withdraw from `Caretaker`. 
There are also 3000 yang in the contract and 1000 yin as `reclaimable_yin`.

User1 withdraws 300 yin, now the balances change as following:

`reclaimable_yin` = 1000 - 300 = 700 yin
`yang assets` = (300 / 1000) * 3000 = 30% * 3000 = 900 yang

These calculations are made inside of `preview_reclaim()`:

yin calculations:

https://github.com/code-423n4/2024-01-opus/blob/main/src/core/caretaker.cairo#L167-169
```
  let remaining_reclaimable_yin: Wad = self.reclaimable_yin.read();
            let capped_yin: Wad = min(yin, remaining_reclaimable_yin);
            let pct_to_reclaim: Ray = wadray::rdiv_ww(capped_yin, remaining_reclaimable_yin);
```

yang calculations:

https://github.com/code-423n4/2024-01-opus/blob/main/src/core/caretaker.cairo#L180-181
```
  let caretaker_balance: u128 = asset.balance_of(caretaker).try_into().unwrap();
                        let asset_amt: Wad = wadray::rmul_rw(pct_to_reclaim, caretaker_balance.into());

```

Now the user2 withdraws 400 yin:

`reclaimable_yin` = 700 - 400 = 300 yin
`yang assets` = (400 / 700) * 2100 = 1200 yang asset amount

The problem is that when the last user withdraws, `reclaimable_yin` = 300 and
`capped_yin` = 300 so when the percentage is calculated for him he gets:

https://github.com/code-423n4/2024-01-opus/blob/main/src/core/caretaker.cairo#L169
```
  let pct_to_reclaim: Ray = wadray::rdiv_ww(capped_yin, remaining_reclaimable_yin);
```

`pct_to_reclaim` = 300 / 300 = 1% - which is wrong as he should have 100% as he's the last one to reclaim

## Tools Used

Manual review.

## Recommended Mitigation Steps

Add a conditional check to see if the user is the last one to reclaim. It can be done by substracting the remaining balance from the amount that user wants to reclaim and if it's equal to 0, it means that there is no more collateral in the contract to reclaim and the user is the last one.

If so, bypass the normal percentage calculation and directly assign the remaining yang assets to this user




## Assessed type

Other
User can lose funds
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
User can only withdraw if health check pass. But health check is skipped when shrine is killed. This allows for an attack vector where one User can steal funds of other Users

## Proof of Concept

1. For sake of simplicity, lets say threshold is at 100% 

2. User A creates trove1 with yang Y1, having value of 5
3. User A takes amount 5 as debt (100% threshold), which increases yin balance of User A to 5
4. User B creates trove2 with yang Y1, having value of 5 and no debt
5. Shrine is killed by calling [shut](https://github.com/code-423n4/2024-01-opus/blob/main/src/core/caretaker.cairo#L194)
6. Since total debt is 5 and total yang value is 10 (from User A & B), so debt amount 5 is send back to Caretaker contract (capped_backing_pct, ltv,  being 50%)

```
Option::Some(yang) => {
                        let backed_yang: Wad = wadray::rmul_rw(capped_backing_pct, shrine.get_yang_total(*yang));
                        sentinel.exit(*yang, caretaker, DUMMY_TROVE_ID, backed_yang);
                    },
```

7. Now User A simply calls [reclaim](https://github.com/code-423n4/2024-01-opus/blob/main/src/core/caretaker.cairo#L328) which melts his 5 yin and send him the 5 yang assets

```
let success: bool = IERC20Dispatcher { contract_address: *reclaimable_asset.address }
                            .transfer(caller, (*reclaimable_asset.amount).into());
```

8. Now gate is left with 10-5=5 assets (5 asset sent to caretaker). Yang minted is 10 so 1 yang = 0.5 asset
9. User A calls [release](https://github.com/code-423n4/2024-01-opus/blob/main/src/core/caretaker.cairo#L262)
10. Since User A has 5 yang so he gets 5*0.5=2.5 assets from `gate`
11. Now User A has got total 5+2.5 =7.5 assets which is more than he deposited
12. User B had no debt so cannot reclaim and on calling release, will get 2.5 assets for his 5 yang
13. So User A stole 2.5 assets from User B

Note:
The scale of this attack can be increased when:
1. User deposits huge amount of collateral and hence own more yang
2. User takes loan very near to threshold for this huge collateral
3. Once shrine is killed, first uses his huge yin balance to get equivalent assets and then release to gets remaining+alreadyRepaid assets


## Recommended Mitigation Steps
[release](https://github.com/code-423n4/2024-01-opus/blob/main/src/core/caretaker.cairo#L262) should consider trove debt and only release remaining deposited yang assets after deducting trove debt.


## Assessed type

Other
Function `compound()` should take the redistribution event into account when calculating the debt after interest charged 
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Description
The `shrine::compound()` function calculates the debt owed by the `trove_id` after interest has been charged over a given time. As indicated in the function's comment, it assumes that:

https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1461-L1462
* > the trove hasn't minted or paid back any additional debt during the given time period
* > the trove hasn't deposited or withdrawn any additional collateral during the given time period

These assumptions are somewhat correct since no functions related to minting, burning, depositing, or withdrawing are executed during the period `[start_interval, end_interval]`. However, when considering the redistribution process, a flaw becomes apparent. The issue arises because the redistribution process could alter the trove's yang/trove's debt during the period (contrary to the assumptions of the `compound()` function) but the function `shrine::pull_redistributed_debt_and_yangs()` is always triggered after the `shrine::compound()` function in the `shrine::charge()` function.

https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1413-L1418
```rust=
fn charge(ref self: ContractState, trove_id: u64) { 
    ... 
    let compounded_trove_debt: Wad = self.compound(trove_id, trove, current_interval);


    // 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);
    ...
}
```

Take an example as follows: 
Consider the following example:

1. At `interval = 1`, there are two troves with two yangs each:
   - Trove 1:
     - Yang: `20, 40`
     - Debt: `100`
   - Trove 2:
     - Yang: `20, 0`
     - Debt: `10`

2. At `interval = 2`, Trove 1 is fully redistributed, which should modify Trove 2 to:
   - Trove 2:
     - Yang: `20 (+ 20), 0 (+ 40)`
     - Debt: `10 (+ 100)`

    However, this modification isn't immediately applied to Trove 2 because no actions have been applied to Trove 2 yet to trigger the redistribution.

3. At `interval = 4`, someone triggers the `charge()` function on Trove 2, which consequently triggers the `compound()` function. The function takes the initial `compounded_debt = trove.debt = 10` instead of `10 + 100` because the redistribution hasn't been applied yet. 
    --> This results in the interest charged to Trove 2 from interval 2 to interval 4 being applied to just the 10 debt instead of the expected 110 debt.

## Impact
T
Purger uses stale asset price for liquidations
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/purger.cairo#L227
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L509 https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1318 https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1181
 https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/seer.cairo#L181-L184


# Vulnerability details

## Summary
The protocol uses a liquidation mechanism that utilizes asset prices that are seconds old. This temporal inconsistency enables liquidators to initiate liquidations based on outdated price information, potentially causing substantial financial losses for users, it enables liquidators to earn profit from arbitrage in addition to reward penalty. The protocol's liquidation process relies on asset prices that are only updated every few seconds or minutes. The liquidation process should use the most recent and accurate asset prices, ensuring fair liquidations.
## Vulnerability Details 
liquidate function at purger module gets trove information(value,debt,ltv) from get_trove_helth 
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/purger.cairo#L227

In the following line of code, get_trove_health retrieves trove value using get_thrshold_value function which calculates trove value based on yang_prices mapping which stores the price of each yang.
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L509
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1318
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1181

however yang_prices are being updated using execute_task function at seer module periodically, this period is set as update_frequency which would be between 15 seconds to 30 minutes, since the liquidation function doesn't update the price so the price may be at least 15 seconds old, also there is no guarantee that price is updated at last period so can be even older, this old price leads to unfair liquidations. 
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/seer.cairo#L181-L184

since value of yang is not updated it's possible that a wrong percentage of collaterals is freed and transferred to the liquidator, 
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/purger.cairo#L235-L237
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/purger.cairo#L598-L608

this would enable the liquidator to arbitrage liquidations and earn profit in addition to their
Potential Infinite Loop in get_allocation Function
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Description

The allocator contract contains a potential infinite loop in the get_allocation function.

In the get_allocation function, the contract reads the recipients_count using the read() method and stores it in a u32 variable called loop_end. The function then proceeds to iterate over the recipients and percentages arrays using a loop from 0 to loop_end inclusive. However, there is no check to ensure that loop_end is not greater than the maximum value that u32 can hold.

If self.recipients_count.read() returns a very large number, it could potentially lead to an infinite loop, causing the contract to become unresponsive or otherwise fail to function correctly.

## Impact

If the recipients_count is a very large number, borrowing it as a u32 could potentially lead to an infinite loop in the get_allocation function. This could result in a denial of service (DoS) attack, where an attacker intentionally triggers this error condition to disrupt the contract's operation.


## Proof of concept 

To demonstrate the potential infinite loop bug in the get_allocation function, we can create a minimal Rust project that replicates the issue.

Suppose we have the following Rust project:

    use std::error::Error;
    use std::sync::Arc;
    use std::sync::mpsc::channel;
    use std::thread;
    use std::time::Duration;

    struct Contract {
        recipients_count: Arc<std::sync::atomic::AtomicU64>,
    }

    impl Contract {
        fn new() -> Self {
            Contract {
                recipients_count: Arc::new(std::sync::atomic::AtomicU64::new(0)),
            }
       }

        fn get_allocation(&self) -> Result<(), Box<dyn Error>> {
            let loop_end = self.recipients_count.load(std::sync::atomic::Ordering::SeqCst);
            let mut total_percentage = 0;
            for i in 0..=loop_end {
                total_percentage += 1;
            }
            Ok(())
        }
    }

    fn main() -> Result<(), Box<dyn Error>> {
        let contract = Contract::new();
        let recipients_count = Arc::new(std::sync::atomic::AtomicU64::new(u64::MAX));
        let contract_with_large_counter = Contract {
            recipients_count,
        };

        let (tx, rx) = channel::<()>();
        thread::spawn(|| {
            thread::sleep(Duration::from_secs(10));
            tx.send(()).unwrap();
        });

        contract_with_large_counter.get_allocation()?;

        rx.recv()?;

        Ok(())
    }


In this project, we create a Contract struct that contains a counter for the number of recipients. The get_allocation function reads the counter using the load() method and stores it in a loop_end variable. The function then proceeds to iterate over the range 0..=loop_end, which could potentially lead to an infinite loop if loop_end is a very large number, such as u64::MAX.

To demonstrate this issue, we creat
Setting threshold can be executed anytime which can cause unfair liquidation.
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L622-L629
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1357-L1368
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L171-L176
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1262-L1274
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/purger.cairo#L227-L231
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/purger.cairo#L549-L578


# Vulnerability details

## Vulnerability Detail
Setting liquidation threshold per yang (see below) has no restriction to when it should be executed. The only safe time is before opening trove with affected yang. 

Since we know for sure there will be adjustment in the future, in which there are troves currently active and the admin decided to change the threshold of certain yangs (without proper checking), it can cause unfair liquidation to trove owners affected.   

```
File: shrine.cairo
622:         fn set_threshold(                   
623:             ref self: ContractState, 
624:             yang: ContractAddress, 
625:             new_threshold: Ray) { 
626:             self.access_control.assert_has_role(shrine_roles::SET_THRESHOLD);
627: 
628:             self.set_threshold_helper(yang, new_threshold);
629:         }
```
```
File: shrine.cairo
1357:         fn set_threshold_helper(
1358:             ref self: ContractState, 
1359:             yang: ContractAddress, 
1360:             threshold: Ray) 
1361:             {
1362:             assert(threshold.val <= MAX_THRESHOLD, 'SH: Threshold > max');
1363:             self.thresholds.write(self.get_valid_yang_id(yang), threshold);
1364: 
1365:             // Event emission
1366:             self.emit(ThresholdUpdated { yang, threshold });
1367:         }
```

Let's further discuss on how untimely threshold setting could exactly affects the liquidation. 
In the function [liquidate](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/purger.cairo#L222-L257), the first step is to get trove health of the trove we are trying to liquidate. This can be executed by the function below:
```
File: shrine.cairo
1051:         // Returns a tuple of a trove's threshold, LTV based on compounded debt, trove value and compounded debt
1052:         // Returns a Health struct comprising the trove's threshold, LTV based on compounded debt,
1053:         // trove value and compounded debt;
1054:         fn get_trove_health(self: @ContractState, trove_id: u64) -> Health {
1055:             let interval: u64 = now();
1056: 
1057:             // Get threshold and trove collateral value
1058:             let trove_yang_balances: Span
Reentrancy Vulnerability in deposit_helper and withdraw_helper functions
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/main/src/core/abbot.cairo#L240
https://github.com/code-423n4/2024-01-opus/blob/main/src/core/abbot.cairo#L251


# Vulnerability details

## Description


The deposit_helper and withdraw_helper functions in the Abbot contract have a reentrancy vulnerability due to the lack of proper protection when updating the state variables. These functions use the reentrancy_guard.start() and reentrancy_guard.end() methods to prevent reentrancy attacks, but they don't protect the state variables being updated in these functions. This allows an attacker to manipulate the state variables within the protected block, leading to a reentrancy vulnerability.

## Impact 

Allows an attacker to create situations where they can repeatedly enter the deposit or withdraw function, gaining unauthorized advantages on the asset balances within the contract. This can lead to mismanaged token amounts, incorrect asset proportions, or in the worst-case scenario, unbounded minting or draining of tokens.


## Proof of Concept 


Consider the following scenario, where an attacker with address MALICIOUS_USER manipulates the system using the reentrancy vulnerability:

1 .  MALICIOUS_USER calls the deposit function with an AssetBalance containing a legitimate yang_asset address and a specific amount.

2 .  The deposit_helper function is executed with the provided trove_id, user (which matches MALICIOUS_USER), and the yang_asset.

3 .  Before the reentrancy guard can block reentrancy, the MALICIOUS_USER manages to alter the contract's state by calling a malicious function that mimics some actions performed by legitimate functions, e.g. by sending a transaction to the contract to call melt function using the available exposed endpoints, passing the MALICIOUS_USER as the user, and an amount greater than or equal to yang_amt, thereby triggering the reentrancy vulnerability condition.

4 .  After successfully manipulating the contract's state, MALICIOUS_USER can create unintended behavior, such as inflating the token balances, transferring an exceeding amount of tokens, or exploiting the vulnerability for further harmful actions.



## Recommedation

- Ensure that the state variables are updated before executing any external calls.
- Implement mutual exclusion with the use of starknet::external::External when performing actions that might trigger reentrancy


## Tools Used 

Manual Review 


## Assessed type

Reentrancy