xanoutas/solidity-security-findings huggingface dataset:
Insufficient validation in release function
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

The `release` function does not include explicit validation for the `trove_id` parameter to ensure it is within expected bounds or conditions. Here is the relevant part of the code:

```
fn release(ref self: ContractState, trove_id: u64) -> Span<AssetBalance> {
    // ... existing code ...
    let trove_owner: ContractAddress = self
        .abbot
        .read()
        .get_trove_owner(trove_id)
        .expect('CA: Owner should not be zero');
    // ... existing code ...
}
```

### Mitigation

The contract should include checks to ensure that the `trove_id` provided to the `release` function is valid. Here is an example of how such validation could be implemented:

```
fn release(ref self: ContractState, trove_id: u64) -> Span<AssetBalance> {
    assert(trove_id > 0, "CA: Invalid trove_id"); // Ensure trove_id is greater than 0
    // ... existing code ...
    let trove_owner: ContractAddress = self
        .abbot
        .read()
        .get_trove_owner(trove_id)
        .expect('CA: Owner should not be zero');
    // ... existing code ...
}
```
The contract now ensures that only valid troves can have their assets released, which helps to maintain the integrity of the asset management process and protect against unauthorized actions.

### Impact

The impact of not validating the `trove_id` is that the function could be called with an invalid or unintended identifier, which could lead to unauthorized release of assets or other unexpected behavior. This could potentially result in loss of assets or manipulation of the contract state.




## Assessed type

Invalid Validation
Insufficient validation in normalize function
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

The `normalize` function is designed to allow users to help the contract address budget deficits by burning their yin. The function performs a check to ensure that the budget is indeed negative before proceeding with the burn. However, the function does not validate the `yin_amt` against the caller's balance, which could lead to a situation where a user is able to burn an amount of yin that exceeds their actual balance.

## Code Snippet

```
fn normalize(ref self: ContractState, yin_amt: Wad) -> Wad {
    let shrine: IShrineDispatcher = self.shrine.read();
    let budget: SignedWad = shrine.get_budget();
    if budget.is_negative() {
        let wipe_amt: Wad = min(yin_amt, budget.val.into());
        let caller: ContractAddress = get_caller_address();
        shrine.eject(caller, wipe_amt);
        shrine.adjust_budget(wipe_amt.into());

        // ... (event emission logic) ...
    } else {
        WadZeroable::zero()
    }
}
```

## Impact

Users burning more yin than they own, which could disrupt the intended economic mechanisms of the contract.

## Mitigation

Introduce a balance check within the `normalize` function to ensure that the `yin_amt` specified by the user does not exceed their current balance.



## Assessed type

Invalid Validation
Insufficient validation in constructor
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

The constructor does not perform explicit validation to ensure that the addresses passed as parameters are not zero addresses. Here's the relevant code snippet for the constructor:

```
#[constructor]
fn constructor(
    ref self: ContractState,
    admin: ContractAddress,
    shrine: ContractAddress,
    abbot: ContractAddress,
    sentinel: ContractAddress,
    equalizer: ContractAddress
) {
    // Constructor implementation
}
```

### Mitigation

Add explicit validation to ensure that the addresses are not zero addresses. Here's an example of how the validation can be performed within the constructor:

```
#[constructor]
fn constructor(
    ref self: ContractState,
    admin: ContractAddress,
    shrine: ContractAddress,
    abbot: ContractAddress,
    sentinel: ContractAddress,
    equalizer: ContractAddress
) {
    assert(admin != Default::default(), "Admin address cannot be zero");
    assert(shrine != Default::default(), "Shrine address cannot be zero");
    assert(abbot != Default::default(), "Abbot address cannot be zero");
    assert(sentinel != Default::default(), "Sentinel address cannot be zero");
    assert(equalizer != Default::default(), "Equalizer address cannot be zero");

    // Constructor implementation
}
```

### Impact

The impact of not validating the addresses in the constructor is that the contract may be deployed with zero addresses, which can lead to unexpected behavior and potential security vulnerabilities. For example, a zero address for the `admin` parameter could result in a situation where the contract is not properly initialized with an admin address, compromising the access control mechanism.



## Assessed type

Invalid Validation
Insufficient validation in set_allocation_helper function
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

The `set_allocation_helper` function in the provided StarkNet contract code is responsible for updating the allocation of recipients and their respective percentage shares. While it includes several important checks, there are areas where input validation could be improved to enhance the contract's security and robustness.


The function checks for:
- Non-zero length of recipients.
- Matching lengths of recipients and percentages arrays.
- The total percentage equals one `RAY_ONE`.

However, it lacks validation for:
- Non-zero recipient addresses.
- Positive percentage values for each recipient.

### Impact

1. Without checking for non-zero recipient addresses, there's a risk of assigning allocations to an invalid address, which could lead to loss of funds or tokens that cannot be recovered.

2. Not ensuring that each percentage is greater than zero could result in allocations that are meaningless, as a recipient with a 0% share would not receive anything. This could lead to confusion or unintended distribution of allocations.

### Mitigation 

```
fn set_allocation_helper(ref self: ContractState, recipients: Span<ContractAddress>, percentages: Span<Ray>) {
    let recipients_len: u32 = recipients.len();
    assert(recipients_len.is_non_zero(), 'AL: No recipients');
    assert(recipients_len == percentages.len(), 'AL: Array lengths mismatch');

    let mut total_percentage: Ray = RayZeroable::zero();
    let mut idx: u32 = LOOP_START;

    let mut recipients_copy = recipients;
    let mut percentages_copy = percentages;
    loop {
        match recipients_copy.pop_front() {
            Option::Some(recipient) => {
                // Ensure recipient address is not zero
                assert(*recipient != ContractAddress::zero(), 'AL: Zero address recipient');

                let percentage: Ray = *(percentages_copy.pop_front().unwrap());
                // Ensure percentage is greater than zero
                assert(percentage > RayZeroable::zero(), 'AL: Zero percentage');

                self.recipients.write(idx, *recipient);
                self.percentages.write(*recipient, percentage);

                total_percentage += percentage;

                idx += 1;
            },
            Option::None => { break; }
        };
    };

    assert(total_percentage == RAY_ONE.into(), 'AL: sum(percentages) != RAY_ONE');

    self.recipients_count.write(recipients_len);

    self.emit(AllocationUpdated { recipients, percentages });
}
```


## Assessed type

Invalid Validation
If just one of the tokens accepted as yang underlying tokens becomes non-transferable, recovery of all assets on system shutdown is going to fail
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

Some of ECR20 tokens accepted to the system may have some issues, which can cause their operators to pause them or otherwise make them not transferable. Or they can issued by malicious entities with the aim to rugpull them in the future by pausing all transfers. If this happens, not only all operations on troves containing these assets will fail, but the shutting down the system and reclaiming all assets will fail, even if there are no troves with the affected asset deposited.

The admin of the Opus instance is not required to be malicious or foolish enough to add any random ERC20 tokens. Major tokens such as USDC are upgradeable and are under control of a single entity. Even if the `pause()` function is not currently present, it can be added on an upgrade. If the issuer or USDC decides to migrate to a completely new USDCv2 contract and pause all transfers of the previous one, this problem will appear.

Such a major change is likely to call for decommissioning and redeployment of the affected Opus shrine, however, the envisioned smooth shutdown & recovery of the assets will be impossible due to failing transfers.

Potential scenario:
1. A shrine operates with safe, immutable ERC20 tokens (such as WETH) as the underlying collateral tokens, with some amount of yin forged and outstanding.
2. Users convince the governance to add a new upgradeable token BADERC20 as the basis for a new yang in the system.
3. Admin adds BADERC20 and transfers INITIAL_DEPOSIT_AMT to the gate contract as required.
4. The entity controlling BADERC20 upgrades its contract to a pausable version, and pauses all transfers of BADERC20.
5. Time passes, and the admin of the Opus instance wants to shut it down for whatever reason.

Impact:
1. `caretaker.shut()` cannot be called anyone as long as there are some outstanding yin tokens, caretaker will attempt to transfer a portion from each yang to itself via `sentinel.exit()`, which will fail.
2. If the `caretaker.shut()` already has been called and the shrine is killed, users trying to recover their assets will still fail, because `reclaim()` will attempt to transfer the assets including BADERC20 from the caretaker to the user.

Since the `INITIAL_DEPOSIT_AMT` of the asset can never be removed from the system, once a yang is added, the system forever remains vulnerable to the pausing of any of the assets, even if the asset is not locked in any troves.

Note that if just a user's address is blacklisted by the BADERC20 token, there will be no problem with (2), because the user can always transfer their yin to another, fresh address, and reclaim from that.


## Tools Used

Manual review.

## Recommended Mitigation Steps

Allow to specify an optional list of yangs to recover as a parameter to `kill` and `reclaim` calls. If the li
Incorrect Liquidation Thresholds Triggering False Positives
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Bug Description Risks when depositing yin

- When yin price per share gets too low, epochs can increment. This could slash yin deposits.

- In new epochs with low yin, shares can be concentrated to a single depositor, risking loss. 

- No checks are shown if absorber contract yin balance suddenly drops, which could indicate a hack to drain funds.

The potential attack vectors are:

- Front-running liquidations via price manipulation 

- Forcing epoch transitions by manipulating yin valuation

- Exploiting low liquidity after resets to steal yin

## Proof of Concept
The Purger's reliance on current spot prices from oracles to determine collateral ratios for liquidation creates an attack vector where an adversary could manipulate reported spot prices to intentionally trigger liquidations. Specifically, the `preview_liquidate` and `get_liquidation_penalty_internal` functions in Purger use the current `ltv` (loan-to-value) ratio for a trove, retrieved via the `get_trove_health` call to Shrine. 

The Shrine's `get_trove_health` function calculates the trove's collateral value based on current spot prices of the collateral assets by calling `get_threshold_and_value`.

Code where spot prices are fetched: [src/core/shrine.cairo#L1306-L1324](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1306-L1324)

```cairo
fn get_threshold_and_value(
    self: @ContractState, mut yang_balances: Span<YangBalance>, interval: u64
) -> (Ray, Wad) {
    let mut weighted_threshold_sum: Ray = RayZeroable::zero();
    let mut total_value: Wad = WadZeroable::zero();


    loop {
        match yang_balances.pop_front() {
            Option::Some(yang_balance) => {
                // Update cumulative values only if the yang balance is greater than 0
                if (*yang_balance.amount).is_non_zero() {
                    let yang_threshold: Ray = self.get_yang_threshold_helper(*yang_balance.yang_id);
                    let (price, _, _) = self.get_recent_price_from(*yang_balance.yang_id, interval);


                    let yang_deposited_value = *yang_balance.amount * price;
                    total_value += yang_deposited_value;
                    weighted_threshold_sum += wadray::wmul_rw(yang_threshold, yang_deposited_value);
                }
            },
```

By manipulating the spot price of assets within that window, an attacker could artificially reduce the ltv ratio. This enables liquidations that should not occur if prices reflected true market value.

The main trigger flow would be:

1. Attacker temporarily manipulates spot price for a collateral asset down significantly 
2. This reduces the current collateral value for troves with that asset
3. The manipulated price feeds into Purger's `get_trove_health` call from Shrine
4. Th
Insufficient validation in timelock calculations
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

The code calculates the timelock for a request based on the current timestamp and a multiplier if a previous request has not cooled down:

```
let current_timestamp: u64 = get_block_timestamp();
let mut timelock: u64 = REQUEST_BASE_TIMELOCK;
if request.timestamp + REQUEST_COOLDOWN > current_timestamp {
    timelock = request.timelock * REQUEST_TIMELOCK_MULTIPLIER;
}
let capped_timelock: u64 = min(timelock, REQUEST_MAX_TIMELOCK);
```

The code also determines the current epoch based on the amount of `yin` provided by the caller:

```
let (new_provision_shares, issued_shares) = self.convert_to_shares(amount, false);
...
let current_epoch: u32 = self.current_epoch.read();
```



The code correctly calculates the timelock for a request and determines the current epoch. However, it does not explicitly validate that these calculations do not result in unexpected values. For example, without validation, the calculated timelock could exceed the maximum allowed timelock due to an excessively large `REQUEST_TIMELOCK_MULTIPLIER` or `REQUEST_BASE_TIMELOCK`, leading to unexpected delays in request processing. Similarly, the current epoch could be incorrectly determined if the `convert_to_shares` function returns an unexpected value.

### Impact 

Unexpected values in time-related calculations can have various negative impacts on the contract's functionality and security, including:

- Excessive timelocks could delay request processing, affecting the contract's usability and efficiency.
- Incorrect epoch determination could disrupt the contract's economic mechanisms, affecting rewards distribution, asset absorption, and more.

### Mitigation 

The contract should include validation checks to ensure that the calculated timelock and the determined epoch are within logical and expected bounds. Here's an example of how the code could be modified to include such checks:

```
let current_timestamp: u64 = get_block_timestamp();
let mut timelock: u64 = REQUEST_BASE_TIMELOCK;
if request.timestamp + REQUEST_COOLDOWN > current_timestamp {
    timelock = request.timelock * REQUEST_TIMELOCK_MULTIPLIER;
    assert(timelock <= REQUEST_MAX_TIMELOCK, 'ABS: Timelock exceeds maximum allowed value');
}
let capped_timelock: u64 = min(timelock, REQUEST_MAX_TIMELOCK);
...
let (new_provision_shares, issued_shares) = self.convert_to_shares(amount, false);
assert(new_provision_shares >= 0 && issued_shares >= 0, 'ABS: Invalid shares calculation');
let current_epoch: u32 = self.current_epoch.read();
assert(current_epoch >= FIRST_EPOCH, 'ABS: Invalid epoch determination');
```

In this modified code, assertions are added to validate the calculated timelock and the determined epoch, ensuring that they do not exceed the maximum allowed values or fall below the minimum expected values.



## Assessed type

Invalid Validation
Incorrect Liquidation Thresholds Triggering False Positives
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
> - Liquidations are handled by the Purger module, not directly the Absorber.
> - The Purger uses current collateral ratios based on oracle prices for liquidation state.
> - Front-running liquidations may be possible by manipulating spot prices. 
> - No price averaging mechanisms are shown to make manipulation harder.

The core impact of this issue is that solvent, non-liquidatable troves can be incorrectly liquidated due to inaccuracies in the liquidation threshold formula. This means users can have their collateral seized and liquidated even when their collateral ratios should still keep them solvent under normal circumstances.

To reiterate why in more detail:

The liquidation threshold calculation relies on targeting a maximum loan-to-value (LTV) ratio via a formula that assumes a linear relationship between a trove's debt and collateral value. However, at higher debt and collateral values, the WadRay fixed point types start to lose precision due to integer rounding. This makes the actual relationship non-linear.

The non-linear relationship between collateral and debt at scale breaks the assumption in the formula. The target LTV ratio becomes unreliable and inflated beyond the true maximum solvent LTV defined by the intended collateral ratio requirements.

When this happens, the threshold calculation allows liquidations past the point that should be permitted based on the stated collateral ratio requirements for the system. Solvent troves start getting flagged as undercollateralized and liquidated since the target threshold is now invalid.

The end impact is that users can have their collateral involuntarily liquidated well before they have actually breached the collateral ratios they should have margin for based on the protocol's risk parameters. This amounts to incorrect and invalid liquidations that can cause loss of funds for trove owners.

The key thing for developers is to realize the threshold formula needs to account for potential non-linear relationships between collateral and debt. Using simpler math constructs like rational numbers instead of fixed point may help. But additionally having liquidation buffer margins that are larger would make the system more robust to these formula limitations.

## Proof of Concept
Potential to manipulate liquidations by exploiting the price feeds into the Purger module. The root cause lies in the Purger's reliance on current spot prices from oracles to determine collateral ratios for liquidation. This creates an attack vector where an adversary could manipulate reported spot prices to intentionally trigger liquidations.

Specifically, the `preview_liquidate` and `get_liquidation_penalty_internal` functions in Purger use the current `ltv` (loan-to-value) ratio for a trove, retrieved via the `get_trove_health` call to Shrine.
If the sequencer gets paused, asset price changes during the inactivity period may lead to mass liquidations once the sequencer is unpaused
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

Depending on the order of oracle updates after the sequencer gets unpaused, fresh prices can enter the system before the users have a chance to react.

This will cause user losses at the expense of liquidation bots, as well as create congestion of transactions in the system, making user experience worse, and reducing  their trust to product.


## Tools Used

Manual review.

## Recommended Mitigation Steps

Add `pause()` functionality to the purger or seer, to give the user's an optional grace period after a sequencer downtime.



## Assessed type

MEV
Overly Strict Liquidation Conditions in Purger Causing False Positives
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/roles.cairo#L195-L197
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/roles.cairo#L185-L187


# Vulnerability details

## Impact
**Least Privilege Principles** The roles mostly follow principles of least privilege by granting restricted subsets of permissions: [src/core/roles.cairo#L195-L197](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/roles.cairo#L195-L197)

[src/core/roles.cairo#L185-L187](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/roles.cairo#L185-L187)

```cairo
fn sentinel() -> u128 {
  ADD_YANG + UPDATE_YANG_SUSPENSION 
}

fn purger() -> u128 {
  MELT + REDISTRIBUTE + SEIZE
}
```

This limits blast radius if a contract with that role is compromised.

**Exceptions**  

A few exceptions granting unnecessary privileges:

- `sentinel_roles.purger()` has `EXIT` permission to withdraw assets from Gates - unnecessary capability

- `shrine_roles.all_roles()` combines total privileges only needed for testing

- No restriction on admin roles updating permissions  

## Tools Used
Vscode

## Recommended Mitigation Steps
🔥 Remove `EXIT` for Purger in Sentinel  

🔥 Restrict `all_roles()` to test environments

🔒 Add timelock for admin role changes


## Assessed type

Access Control