xanoutas/solidity-security-findings huggingface dataset:
withdraw_helper() pull yang will lost
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Vulnerability details

when user call `abbot.withdraw()` -> `shrine.withdraw()` ->`shrine.withdraw_helper()`

The main logic is `withdraw_helper()`
```rust
        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);

            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 });
        }
```
Execution steps:
1. Calculate `let new_trove_balance = trove_balance - amount;`
2. Execute `self.charge(trove_id);`
3. `self.deposits.write((yang_id, trove_id), new_trove_balance);`

However, `self.charge()` may execute `pull_redistributed_debt_and_yangs()` -> `self.deposits.write()`, retrieving `redistributed yang`.
These retrieved `redistributed yang` will be overwritten by the temporary variable `new_trove_balance` in the third step.
This part of the `redistributed yang` will be lost.

## Impact
redistributed yang will lost

## Recommended Mitigation
charge first
```diff
        fn withdraw_helper(ref self: ContractState, yang: ContractAddress, trove_id: u64, amount: Wad) {
            let yang_id: u32 = self.get_valid_yang_id(yang);
+            self.charge(trove_id);
            // 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);

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


## Assessed type

Error
redistribute_helper()  some troves may not updated
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Vulnerability details

in `redistribute_helper()`

Two important arrays (updated_trove_yang_balances[], new_yang_totals[]) are calculated , and then updated based on these two arrays.

1. `updated_trove_yang_balances[]` is used to update the remaining quantity of trove.
2. `new_yang_totals[]` is used to update the total remaining quantity of `yang`.

For example,
Assume there are ETH, BTC and trove[1]
trove[1].ETH = 0
trove[1].BTC = 200
total_yang_ETH = 1000
total_yang_BTC = 2000

Execute redistribute(trove[1]), redistribute eth = 0, redistribute btc = 20

After calculation:
updated_trove_yang_balances = [0,180]
new_yang_totals = [1000,1980]

Then update the corresponding values based on these two arrays. 
**An important point is that these two arrays should be of the same length and correspond one-to-one.**

The main code is as follows:
```rust
        fn redistribute_helper(
            ref self: ContractState,
            redistribution_id: u32,
            trove_id: u64,
            debt_to_redistribute: Wad,
            pct_value_to_redistribute: Ray,
            current_interval: u64
        ) {
...
            loop {
                match trove_yang_balances_copy.pop_front() {
                    Option::Some(yang_balance) => {
                        let trove_yang_amt: Wad = (*yang_balance).amount;
                        let yang_id_to_redistribute = (*yang_balance).yang_id;
                        // Skip over this yang if it has not been deposited in the trove
                        if trove_yang_amt.is_zero() {
@>                           updated_trove_yang_balances.append(*yang_balance);
@>                           continue;
                        }
...

                    if is_ordinary_redistribution {
...
                            new_yang_totals
                                .append(
                                    YangBalance {
                                        yang_id: yang_id_to_redistribute,
                                        amount: redistributed_yang_total_supply - yang_amt_to_redistribute - yang_offset
                                    }
                                );
                      }else {
....
                             new_yang_totals
                                  .append(
                                      YangBalance {
                                          yang_id: yang_id_to_redistribute,
                                          amount: redistributed_yang_total_supply - yang_error
                                      }
                                  );
                        }
....        }//end of loop
            let mut new_yang_totals: Span<YangBalance> = new_yang_totals.span();
            let mut updated_trove_yang_balances: Span<YangBalance> = updated_trove_yang_balances.span();
@>          l
convert_to_yang_helper() loss precision
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Vulnerability details
in `gate.cairo`
When the user calls `deposit()`, it calculates the corresponding shares through `convert_to_yang_helper()`. 
The code is as follows:
```cairo
        fn convert_to_yang_helper(self: @ContractState, asset_amt: u128) -> Wad {
            let asset: IERC20Dispatcher = self.asset.read();
            let total_yang: Wad = self.get_total_yang_helper(asset.contract_address);

            if total_yang.is_zero() {
                let decimals: u8 = asset.decimals();
                // Otherwise, scale `asset_amt` up by the difference to match `Wad`
                // precision of yang. If asset is of `Wad` precision, then the same
                // value is returned
                fixed_point_to_wad(asset_amt, decimals)
            } else {
@>              (asset_amt.into() * total_yang) / get_total_assets_helper(asset).into()
            }
        }
```
The calculation formula is: `(asset_amt.into() * total_yang) / get_total_assets_helper(asset).into()`

The actual calculation of converting Wad to pure numbers is: `(asset_amt * total_yang / 1e18) * 1e18 / total_assets`

The above formula `(asset_amt * total_yang / 1e18)` will lose precision, especially when the asset's decimals are less than 18.

Assume btc as an example, decimals = 8
after `add_yang(btc)` INITIAL_DEPOSIT_AMT = 1000 so: 
total_assets = 1000
total_yang   = 1000e10 = 1e13

If the user deposits 0.0009e8 BTC, according to the formula
= (asset_amt * total_yang / 1e18) 
=  0.0009e8 * 1e13 /1e18 = 0.9e5 * 1e13 /1e18 =  0

With BTC's price at 40,000 USD, 0.0009e8  =  36 USD

The user will lose 36 USD

We should cancel dividing by 1e18 and then multiplying by 1e18, and calculate directly
shares = `asset_amt.into() * total_yang.into() / total_assets.into()`
shares = 0.0009e8 * 1e13 / 1000 = 0.0009e18 = 900000000000000


> Note: In order to successfully deposit should be > 0.0009e8 such as 0.0019e8, which is simplified and convenient to explain.
## Impact

Due to the premature division by `1e18`, precision is lost, and the user loses a portion of their funds.

## Proof of Concept
add to test_abbot.cairo
```cairo
    #[test]
    fn test_wad() {
        let INITIAL_DEPOSIT_AMT: u128 = 1000;
        let decimals:u8 = 8;
        let asset_amt:u128 = 90_000;
        let total_yang:Wad = fixed_point_to_wad(INITIAL_DEPOSIT_AMT, decimals);
        let total_assets:Wad = INITIAL_DEPOSIT_AMT.into();
        let result:Wad = asset_amt.into() * total_yang / total_assets;
        assert(result.into() == 0,' no zero');
        let result2_u:u256 = asset_amt.into() * total_yang.into() / total_assets.into();
        let result2:Wad = Wad { val:result2_u.try_into().expect('u128')};
        assert(result2.into() == 900000000000000,' result2 no zero');
    }
```

```console
$ scarb test -vvv test_wad 

Running 1 test(s
Lack of slippage control in abbot::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-L200


# Vulnerability details

## Impact
Lack of slippage control in abbot::deposit

## Proof of Concept
- Alice wants to deposit some asset to get some yang.
- Before Alice's transaction, Bob donates some assets to related gate to improve yang's price.
- Alice will get less yang than expected.

## Tools Used
Manual

## Recommended Mitigation Steps
Suggest to add some slippage control.


## Assessed type

Error
Some assets remains unclaimable in caretaker module 
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Summary 
The initial provision in the absorber module involves minting 10,000 shares for the system. However, once the caretaker shuts down the shrine, absorption becomes impossible and these assets can't be used, and users must reclaim their collateral by burning yin. The assets available for reclamation are proportional to the total shrine debt. For instance, if there is a debt of 1,000, users can reclaim collateral worth 1,000 USD by burning yin, where each yin represents 1 USD of these assets. It's important to note that the 10,000 shares at the absorber represent a portion of yin linked to the total shrine debt and cannot be utilized for reclamation. As a result, a portion of the assets becomes trapped in the caretaker module and remains unreclaimable.
## Proof of Concept 
Imagine a situation where the total shrine debt is 50,000, and the overall shrine value is 100,000. A user decides to mint 50,000 yin by adding collateral valued at 100,000 to their trove, then providing 50000 yin to absorber. In return, the user receives 40,000 shares equivalent to 40,000 yin in absorber (10,000 shares worth 10,000 yin being minted for the system). Following this transaction, the total shrine value increases to 200,000 USD, with a debt of 100,000 USD.
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/absorber.cairo#L667-L689

After a period, 20% of the absorber pool has been absorbed, causing the value of each share to decrease to 0.8 yin. At this juncture, the administrator opts to shut down the shrine using the caretaker module. The caretaker calculates the backing percentage, which, in this instance, is determined as 50% (assuming no interactions with the pool). Consequently, users would have access to assets valued at 100,000 USD for reclamation.
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/caretaker.cairo#L215-L220

Given that 10,000 shares are valued at 8,000 yin, these yins cannot be utilized for reclamation. Consequently, assets corresponding to this 8,000 yin, equivalent to 8,000 USD, become trapped in the caretaker module and are unavailable for withdrawal. 
## Impact
Some assets get stuck in the caretaker after shutting down the shrine 
direct loss of assets 
## Tools Used
Manual Review
## Recommended Mitigation Steps
before shut down the shrine, burn system shares at absorber 





## Assessed type

Other
`shrine.withdraw_helper` updates trove's deposit incorrectly
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
`shrine.withdraw_helper` updates trove's deposit incorrectly, which might cause trove lose some asset


## Proof of Concept
In [shrine.withdraw_helper](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1379-L1397), the function will first read trove's [deposit - withdraw amount](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1382-L1387), and finally updates the [trove's deposit](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1391-L1392)
```rust
1378         // Withdraw a specified amount of a Yang from a Trove
1379         fn withdraw_helper(ref self: ContractState, yang: ContractAddress, trove_id: u64, amount: Wad) {
1380             let yang_id: u32 = self.get_valid_yang_id(yang);
1381
1382             // Fails if amount > amount of yang deposited in the given trove
1383             let trove_balance: Wad = self.deposits.read((yang_id, trove_id)); <<<--- Here read user's deposit
1384             assert(trove_balance >= amount, 'SH: Insufficient yang balance');
1385
1386             let new_trove_balance: Wad = trove_balance - amount;
1387             let new_total: Wad = self.yang_total.read(yang_id) - amount;
1388
1389             self.charge(trove_id);
1390
1391             self.yang_total.write(yang_id, new_total);
1392             self.deposits.write((yang_id, trove_id), new_trove_balance); <<<--- Here write user's deposits
1393
1394             // Emit events
1395             self.emit(YangTotalUpdated { yang, total: new_total });
1396             self.emit(DepositUpdated { yang, trove_id, amount: new_trove_balance });
1397         }
```

The issue is that between `self.deposits.read` and `self.deposits.write`, `self.charge(trove_id);` is [called](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1389)
And in [shrine.charge](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1400-L1458), the trove's deposit might be updated in [shrine.cairo#L1426](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1426), in such case, the trove's deposit will be incorrect
```rust
1400         fn charge(ref self: ContractState, trove_id: u64) {
    ...
1419 
1420             // If there was any exceptional redistribution, write updated yang amounts to trove
1421             if updated_trove_yang_balances.is_some() {
1422                 let mut updated_trove_yang_balances = updated_trove_yang_balances.unwrap();
1423                 loop {
1424                     match updated_trove_yang_balances.pop_front() {
1425
The melt() function in the Abbot contract allows anyone to repay a user's debt. 
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Allowing anyone to call the melt() function on a trove can be dangerous. The major impact is that it allows an unauthorized party to repay the debt (by destroying Yin) of a user's trove, potentially without the user's consent or knowledge.
The severity of this issue is high because it can lead to the following consequences:
1. Unintended debt repayment: An attacker could target a specific user and repay their debt, potentially disrupting their financial strategy or causing them to lose collateral they had locked in the trove.
2. Denial of service (DoS): An attacker could repeatedly call melt() on a user's trove, effectively preventing the user from managing their own debt and collateral.
3.Financial loss: If an attacker times their melt() calls correctly, they could cause the user to lose their collateral by repaying debt at unfavorable times or in unfavorable market conditions.


## Proof of Concept
The melt() function in the Abbot contract allows anyone to repay a user's debt. Here is a more detailed explanation:
The melt() function is defined as:

       fn melt(ref self: ContractState, trove_id: u64, amount: Wad) {
         // note that caller does not need to be the trove's owner to melt
         self.shrine.read().melt(get_caller_address(), trove_id, amount); 
       }

The key thing to note is that there is no check that the caller is the trove owner. get_caller_address() simply returns whoever called the function.
This means anyone can call melt() and repay debt in any trove by passing in the trove_id.



## Tools Used
Manual
## Recommended Mitigation Steps
melt() should check that `get_caller_address() == self.trove_owner.read(troveId)`, ensuring only a trove's owner can repay its debt


## Assessed type

Other
User can withdraw assets from a trove without fully repaying the debt when closing the trove.
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
It allows a malicious user to withdraw assets from their trove without fully repaying their debt. This could potentially lead to significant losses for the system if not properly addressed.

The severity is high as it could result in a loss of funds. If not fixed, it could be exploited to drain assets from the system.


## Proof of Concept
It is possible for a user to withdraw Yangs from a trove without fully repaying the debt when closing a trove.
In the close_trove function, there is a call to melt to repay the debt:

       shrine.melt(user, trove_id, BoundedWad::max());

However, there is no check after this to ensure the debt is actually 0 before allowing Yang withdrawals.

A malicious user could:
1. Open a trove and take on some debt
2. Call close_trove
3. The melt call repays some of the debt, but not all of it
4. The user can now withdraw all Yangs, while still owing debt

This is a vulnerability because the user has withdrawn assets while still owing debt to the system.


## Tools Used
Manual
## Recommended Mitigation Steps
Add a check that `debt = 0` before allowing Yang withdrawals


## Assessed type

Other
Incorrect handling of inner loop could lead to DoS
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

The issue arises from the lack of a clear exit condition in the inner loop that iterates over `original_yang_balances_copy`. The loop continues as long as `original_yang_balances_copy.pop_front()` returns `Some(original_yang_balance)`. If this condition is met indefinitely, the loop will continue without end, leading to an infinite loop. This could potentially cause the smart contract to hang or consume excessive resources.

### Vulnerable Code

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

```
let mut original_yang_balances_copy = trove_yang_balances;
// Inner loop over all yangs
loop {
    match original_yang_balances_copy.pop_front() {
        Option::Some(original_yang_balance) => {
            // Code continues...
        }
        // No explicit exit condition shown
    }
}
```

### Impact

- DoS: An infinite loop could consume excessive computational resources, potentially leading to a denial of service.

### Mitigation

An explicit exit condition should be added to the inner loop. This could be achieved by checking the length of `original_yang_balances_copy` at the start of each iteration and breaking the loop if the length is zero. Alternatively, the loop could be refactored to a for-loop that iterates over the elements of `original_yang_balances_copy` directly, which would automatically terminate once all elements have been processed.


## Assessed type

DoS
Off By One Error could lead to out of bounds access
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

The issue stems from the handling of array indices with an offset that does not account for the zero-based indexing of arrays. The code indicates that the `yang_id_to_array_idx_offset` is used to adjust yang IDs when indexing into the `trove_yang_balances` array. This approach assumes that yang IDs start from 1, which could misalign with the zero-based indexing of arrays.

### Code

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

```
// Offset to be applied to the yang ID when indexing into the `trove_yang_balances` array
let yang_id_to_array_idx_offset: u32 = 1;
```

And its application in the code:

```
let mut updated_trove_yang_balances = updated_trove_yang_balances.unwrap();
```

### Impact

- Incorrect indexing could lead to operations being performed on the wrong elements, potentially corrupting the data stored in the smart contract.
- Accessing an array out of bounds could cause the contract to revert, leading to a denial of service for functionalities relying on this part of the code.

### Mitigation

Before accessing an array with an adjusted index, implement bounds checking to ensure that the index falls within the valid range of the array. This can prevent attempts to access elements outside the array bounds.


## Assessed type

DoS
Insufficient validation in allocate function
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

The `allocate` function retrieves allocation data from the allocator contract and proceeds to distribute yin to the specified recipients according to their respective percentages. The contract currently performs a check to ensure that the yin balance is not zero before proceeding with the allocation. However, there are no checks to verify that:

- The sum of the allocation percentages equals 100% (or one Ray, which is the unit of measure in this context).
- The recipient addresses provided are valid contracts capable of receiving the yin.

The lack of these validations could lead to scenarios where the allocation does not distribute the yin as intended, or to addresses that cannot properly handle it, potentially leading to loss of funds or other unintended consequences.

## Code Snippet

```
fn allocate(ref self: ContractState) {
    let shrine: IShrineDispatcher = self.shrine.read();
    let balance: Wad = shrine.get_yin(get_contract_address());

    if balance.is_zero() {
        return;
    }

    let allocator: IAllocatorDispatcher = self.allocator.read();
    let (recipients, percentages) = allocator.get_allocation();

    // ... (distribution logic) ...
}
```

## Impact

- Incorrect distribution of yin to recipients, potentially leading to unfair or unintended allocations.
- Loss of yin if sent to invalid addresses that cannot handle the received funds.
- Exploitation by attackers if they can influence the allocation data to redirect yin to their own addresses.

## Mitigation

- Implement additional input validation checks within the `allocate` function to ensure that the sum of the allocation percentages equals one Ray.
- Validate that all recipient addresses are valid contracts that can receive and handle the yin.
- Consider implementing a mechanism to allow for the recovery of yin sent to invalid addresses, if possible.


## Assessed type

Invalid Validation