xanoutas/solidity-security-findings huggingface dataset:
Broken logic if users interact with the project directly and not via a smart contract (or AA)
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

Due to Starknet's native account abstraction (AA), if user's ever interact with the project directly and **NOT** through smart contracts, then it will be possible for anyone to impersonate them and manage their trove positions. The possible impacts range from griefing (closing troves soon after other users open them) to theft of funds (the trove owner would be `0` which would be anyone calling the contract directly).

## Proof of Concept

**NOTE** $\Rightarrow$ for reference, see the [docs](https://docs.starknet.io/documentation/architecture_and_concepts/Smart_Contracts/system-calls/#get_caller_address) and [this post](https://mixbytes.io/blog/cairo-contracts-overview#rec509010273)

There has been a big trend between new L2 solutions implementing native account abstraction (AA), as it is the case with Starknet, which makes the distinction at the address level between contracts and non-contracts entities. This matters because this distinction is done too at the syscalls level, where `get_caller_address` returns the address of the caller (Cairo 1.0 transcribes its *"Rusty"*  code to Cairo 0 assembly, eventually getting to this syscall and bubbling up the return value).

The issue is that, per the [specs](https://docs.starknet.io/documentation/architecture_and_concepts/Smart_Contracts/system-calls/#get_caller_address):

    Returns the address of the calling contract, or 0 if the call was not initiated by another contract.

which means it is possible to have transactions initiated by the `0` address by *"anyone"* (from the called contracts POV). For example, let's imagine the next scenario:

1. Bob creates a trove by calling **DIRECTLY** `abbot::open_trove`, which registers `0` as the owner of the current trove id
2. Alice sees this, and calls **DIRECTLY** `abbot::close_trove` after Bob's transaction and closes Bob's trove, transferring his assets from the corresponding `gate`s to `address(0)`
   1. As both calls are done **DIRECTLY**, `get_caller_address` will return `0` for both, meaning from the, in this case, `abbot` contract, the caller is the same, even when Bob and Alices are two different entities

## Recommended Mitigation Steps

This is a little quirk Starknet has, so I would personally prevent any interactions where `get_caller_address` returns `0`, that is, change all occurrences of this pattern:

```rs
    ...

    let caller: ContractAddress = get_caller_address();

    ...
```

to

```rs
    ...

    let caller: ContractAddress = get_caller_address();
    assert(caller != 0, "Invalid caller");

    ...
```

(just `Ctrl+f` with vscode and change them).



## Assessed type

Other
`absorber.set_reward` should calls `absorber.bestow`
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
`absorber.set_reward` can be used to updating a existing reward, but before updating, the function doesn't call `bestow` to claiming the reward in original blesser, in such case, if there's some reward in the original blesser, the reward will lost.


## Proof of Concept
As show in code above, the [absorber.set_reward](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/absorber.cairo#L365-L389) function doesn't call [absorber.bestow](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/absorber.cairo#L914-L975) to claim the existing rewards before update an existing blesser.
```rust
        fn set_reward(ref self: ContractState, asset: ContractAddress, blesser: ContractAddress, is_active: bool) {
            self.access_control.assert_has_role(absorber_roles::SET_REWARD);

            assert(asset.is_non_zero() && blesser.is_non_zero(), 'ABS: Address cannot be 0');

            let reward: Reward = Reward { asset, blesser: IBlesserDispatcher { contract_address: blesser }, is_active };

            // If this reward token hasn't been added yet, add it to the list
            let reward_id: u8 = self.reward_id.read(asset);

            if reward_id == 0 {
                let current_count: u8 = self.rewards_count.read();
                let new_count = current_count + 1;

                self.rewards_count.write(new_count);
                self.reward_id.write(asset, new_count);
                self.rewards.write(new_count, reward);
            } else {
                // Otherwise, update the existing reward
                self.rewards.write(reward_id, reward);<<<--- self.bestow() should be called before this line
            }

            // Emit event
            self.emit(RewardSet { asset, blesser, is_active });
        }
```
## Tools Used
VIM

## Recommended Mitigation Steps
```diff
diff --git a/src/core/absorber.cairo b/src/core/absorber.cairo
index 473275f..8278488 100644
--- a/src/core/absorber.cairo
+++ b/src/core/absorber.cairo
@@ -381,6 +381,7 @@ mod absorber {
                 self.rewards.write(new_count, reward);
             } else {
                 // Otherwise, update the existing reward
+                self.bestow();
                 self.rewards.write(reward_id, reward);
             }

```



## Assessed type

Other
ERC4626 inflat issue mitigation 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/gate.cairo#L191-L223
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/absorber.cairo#L667-L705


# Vulnerability details

## Impact
Both `absorber` and `gate` use the same mitigation for ERC4626 first depositor front-running vulnerability, but current implementation is not sufficient. By abusing the flaw, even though malicious attacker can't benefit from the mitigation, he can cause other normal users lose asset.

## Proof of Concept
Because of `absorber` and `gate` use the same mitigation, I will take `gate` as example.

Suppose the yang's decimals is **18**

When [sentinel.add_yang](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/sentinel.cairo#L174-L214) is called to add yang to shrine, `initial_yang_amt` is passed to [shrine.add_yang](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/sentinel.cairo#L209) as mitigation to the inflat issue.
And [initial_yang_amt](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/sentinel.cairo#L202) is set as [INITIAL_DEPOSIT_AMT](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/sentinel.cairo#L33) which is `const INITIAL_DEPOSIT_AMT: u128 = 1000;`

In [sentinel.cairo#L204-L206](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/sentinel.cairo#L204-L206), `yang_erc20.transfer_from` is called to transfer 1000 wei yang_erc from caller to gate

And then the code flow will fall into [shrine.add_yang](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L565-L618), when the function is called, `initial_yang_amt` is still `1000`

In `shrine.add_yang`, the `yang_total` will be set to `1000` in [shrine.cairo#L591](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L591)

So when the admin(which is the first depositor) calls [sentinel.add_yang](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/sentinel.cairo#L174-L214), he will transfer 1000 wei yang_asset and he will recevie 1000 yang_amt yang.
After that, when the second user calls [abbot.open_trove](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/abbot.cairo#L131-L161), [gate.convert_to_yang_helper](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/gate.cairo#L208-L222) will calculate his `yang_amt` by [gate.cairo#L220](https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/gate.cairo#L220)
```rust
        fn convert_to_yang_helper(self: @ContractState, asset_amt: u12
Lack of validation in allocator. set_allocation function will cause incosistent behaviour.
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-L167


# Vulnerability details

## Impact
According to the allocation module implementation, it is evident that the total percent should be equal to RAY. But this can be violated by incorrectly passing duplicate recipients. This either blocks distribution of equalizer assets or distributing less than intended.

## Proof of Concept
```cairo
    #[test]
    fn test_allocation_not_incorrect() {
        let allocator = equalizer_utils::allocator_deploy(
            equalizer_utils::initial_recipients(), equalizer_utils::initial_percentages(), Option::None
        );
        start_prank(CheatTarget::One(allocator.contract_address), shrine_utils::admin());

        let one: ContractAddress = contract_address_try_from_felt252('new recipient 1').unwrap();
        let two: ContractAddress = contract_address_try_from_felt252('new recipient 2').unwrap();
        let three: ContractAddress = contract_address_try_from_felt252('new recipient 3').unwrap();


        let recipients = array![
            one,
            two,
            three,
            one,
        ].span();

        let percentages = array![
            150000000000000000000000000_u128.into(), // 15% (Ray)
            300000000000000000000000000_u128.into(), // 30% (Ray)
            300000000000000000000000000_u128.into(), // 30% (Ray)
            250000000000000000000000000_u128.into(), // 25% (Ray)
        ].span();

        allocator.set_allocation(recipients, percentages);

        let (recipients_after, percentages_after) = allocator.get_allocation();
        assert(recipients_after.len() == 4, 'wrong len');
        assert(percentages_after.len() == 4, 'wrong len');
        stop_prank(CheatTarget::One(allocator.contract_address));
    }
```
Corresponding Percentages of recipients:
- new recipient 1 => 25%
- new recipient 2 => 30%
- new recipient 3 => 30%
So total is not 1 RAY. 

Also while distribution this will be blocked as the distribution will be as follows:
- new recipient 1 => 25%
- new recipient 2 => 30%
- new recipient 3 => 30%
- new recipient 1 => 25%
Which is more than 100% i.e 1 RAY.


## Tools Used
Manual Review

## Recommended Mitigation Steps
Enforce checks to validate no duplicates provided and the address is 0.

```diff
        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) => {
Malicious user can manipualte baseInterestRate
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1529-L1550
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/gate.cairo#L107-L116
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/seer.cairo#L222-L225
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/seer.cairo#L181-L184


# Vulnerability details

## Summary 
The baseInterest rate is derived from the combined values of deposited yangs and the interest associated with each yang. If the trove exclusively deposits a yang with a lower interest rate, the trove owner can manipulate the base_interest. This manipulation involves obtaining a flash loan for the assets of the yang with the lower interest rate and transferring them to a gate. Since the trove owner is the sole holder at this gate, there is no loss incurred. The owner then updates the price using the seer module (as the value of assets in the gate has increased, though no new yang has been minted, resulting in inflated yang prices). Importantly, the assets are deposited without incurring any interest charges. Consequently, when interest is applied to the trove, it is heavily weighted towards this specific yang with the lower interest rate. After charging interest, the trove owner can simply withdraw a portion of the yangs and repay the flash loan. This strategy is effective when one of the yangs has a lower interest rate than the others and has only one depositor.

The greater the deposited value for a yang, the more pronounced the impact on the base interest, influenced by the interest rate of that specific yang. Therefore, with the increased value of the underlying yang, interest is more heavily weighted towards this specific yang.
## Impact
Manipulation of base interest rate by a malicious user 
## Vulnerability Details 
In the code snippet below, the base interest rate is influenced by both yang values and their respective interest rates. For instance, if we consider two yangs with identical values, where the first has a 5% interest rate and a deposit of 100, and the second has a deposit of 100 with a 10% interest rate, the resulting base interest rate would be 7.5. If we increase the value of the first yang the interest rate is directed towards the 5% rate.
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/shrine.cairo#L1529-L1550

Before every deposit interest is charged, so we need to inflate the yang price of that yang ( this is possible when there is one depositor ) so we transfer some assets to the gate as this doesn't mint new yangs so the ratio of assets is changed.  
https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/gate.cairo#L107-L116

now we can call execute_task public function at seer module to update yang pric
No initial validation for seer update frequency
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
According to the seer implementation `update_frequency` is bounded by limits i.e,

```cairo
assert(
    LOWER_UPDATE_FREQUENCY_BOUND <= new_frequency && new_frequency <= UPPER_UPDATE_FREQUENCY_BOUND,
    'SEER: Frequency out of bounds'
);
```
But there is no validation for `update_frequency` value in constructor, which allows unbounded arbitrary value.

## Proof of Concept
```cairo
#[test]
fn test_update_frequency_constructor() {
        let UPPER_UPDATE_FREQUENCY_BOUND: u64 = consteval_int!(4 * 60 * 60); // 4 hours * 60 minutes * 60 seconds
        let freq: u64 = consteval_int!(24 * 60 * 60);
        let (seer, _, _) = seer_utils::deploy_seer_contract(Option::None, Option::None, Option::None, freq);
        let current_value = seer.get_update_frequency();
        assert(current_value > UPPER_UPDATE_FREQUENCY_BOUND, 'Value did not set');
}
```

## Tools Used
Manual Review

## Recommended Mitigation Steps
```
    #[constructor]
    fn constructor(
        ref self: ContractState,
        admin: ContractAddress,
        shrine: ContractAddress,
        sentinel: ContractAddress,
        update_frequency: u64
    ) {
        ...
-       self.update_frequency.write(update_frequency);
-       self.emit(UpdateFrequencyUpdated { old_frequency: 0, new_frequency: update_frequency });
+       self.set_update_frequency(update_frequency);
    }
```


## Assessed type

Invalid Validation
Flashloan with unsupported tokens doesn't revert
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
According to `eip-3156` specification, the call to `flashLoan` should fail if the token provided is not supported by the flash lender, which is handled in `flashFee` function.But the current implementation did not revert because the fee value is directly injected, instead of calling `flashFee` function.
## Proof of Concept
```
#[test]
    fn test_flash_mint_wrong_token_revert() {
        let (shrine, flashmint, borrower) = flash_mint_utils::flash_borrower_setup();

        let mut calldata: Span<felt252> = flash_mint_utils::build_calldata(true, flash_borrower_contract::VALID_USAGE);
        let yin = shrine_utils::yin(shrine);

        let flash_mint_caller: ContractAddress = common::non_zero_address();
        start_prank(CheatTarget::One(flashmint.contract_address), flash_mint_caller);


        let token_class = Option::Some(declare('erc20_mintable'));
        // Random Token not Shrine's Yin Token.
        let RANDOM_TOKEN: ContractAddress = common::deploy_token('Ranom Token', 'RTN', 18, 0_u256, flash_mint_caller, token_class);

        flashmint.flash_loan(borrower, RANDOM_TOKEN, 0_u256, calldata);

        assert(yin.balance_of(borrower).is_zero(), 'Wrong yin bal');
        stop_prank(CheatTarget::One(flashmint.contract_address));
    }

[PASS] opus::tests::flash_mint::test_flash_mint::test_flash_mint::test_flash_mint_wrong_token_revert, gas: ~81271

```

## Tools Used
Manual Review
## Recommended Mitigation Steps
This would be mitigated by getting fee from `flashFee` function, instead of directly injecting the value
```diff
let borrower_resp: u256 = IFlashBorrowerDispatcher { contract_address: receiver }
-               .on_flash_loan(initiator, token, amount, FLASH_FEE, call_data);
+               .on_flash_loan(initiator, token, amount, self.flash_fee(token, amount), call_data);
```


## Assessed type

Invalid Validation
Insufficient Error Handling in enter function
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

The `enter` function has a potential issue related to the maximum amount of assets allowed for a yang. The function uses the `assert_can_enter` helper function to check if the `enter` operation is valid under the current on-chain conditions. This helper function includes an assertion that checks if the total assets after the `enter` operation would exceed the maximum amount allowed for the yang:

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

```
assert(current_total + enter_amt <= max_amt, 'SE: Exceeds max amount allowed');
```

If the assertion fails, the function will panic and terminate the program, which is not a desirable behavior in a smart contract. This could lead to a denial of service if a user tries to enter an amount that exceeds the maximum limit.

The `exit` function doesn't seem to have any problematic code based on the provided snippet. However, it's important to note that the function assumes that the yang has been added and is live. If these conditions are not met, the function will panic and terminate the program.

### Mitigation 

To mitigate the issue in the `enter` function, you could modify the `assert_can_enter` function to return a `Result` instead of panicking. This way, you can handle the error gracefully and provide a meaningful error message to the user:

```
fn assert_can_enter(self: @ContractState, yang: ContractAddress, gate: IGateDispatcher, enter_amt: u128) -> Result<(), &'static str> {
    if gate.contract_address.is_zero() {
        return Err('SE: Yang not added');
    }
    if !self.yang_is_live.read(yang) {
        return Err('SE: Gate is not live');
    }
    let suspension_status: YangSuspensionStatus = self.shrine.read().get_yang_suspension_status(yang);
    if suspension_status != YangSuspensionStatus::None {
        return Err('SE: Yang suspended');
    }
    let current_total: u128 = gate.get_total_assets();
    let max_amt: u128 = self.yang_asset_max.read(yang);
    if current_total + enter_amt > max_amt {
        return Err('SE: Exceeds max amount allowed');
    }
    Ok(())
}
```

Then, in the `enter` function, you can handle the error returned by `assert_can_enter`:

```
fn enter( ref self: ContractState, yang: ContractAddress, user: ContractAddress, trove_id: u64, asset_amt: u128 ) -> Result<Wad, &'static str> {
    self.access_control.assert_has_role(sentinel_roles::ENTER);
    let gate: IGateDispatcher = self.yang_to_gate.read(yang);
    match self.assert_can_enter(yang, gate, asset_amt) {
        Ok(_) => Ok(gate.enter(user, trove_id, asset_amt)),
        Err(e) => Err(e),
    }
}
```

### Impact

The vulnerable code could lead to unexpected termination of the program, which is not desirable in a smart contract. By modifying the code to handle errors gracefully, you can prevent the program from terminating and provide meaningful error
Insufficient validation of call_data parameter
Audit: 2024-01-opus-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

In the `flash_loan` function of the contract, the `call_data` parameter is passed to the `on_flash_loan` function in the receiver contract. However, the contract does not perform any validation on the `call_data` before passing it along. This lack of validation could potentially lead to vulnerabilities if the `call_data` is not handled correctly by the receiver contract. Malicious actors could exploit this by passing in crafted `call_data` that could, for example, trigger reentrancy attacks or other unintended actions in the receiver contract.

## Recommendatiom
The contract should validate the `call_data` to ensure it conforms to expected parameters and encodes a function call to a known and safe function in the receiver contract. Here is an example of how such validation could be implemented:

```
fn flash_loan(
    ref self: ContractState,
    receiver: ContractAddress,
    token: ContractAddress,
    amount: u256,
    call_data: Span<felt252>
) -> bool {
    // Validate call_data before proceeding
    assert(self.is_valid_call_data(call_data), 'Invalid call data');

    // Existing flash loan logic...
}

fn is_valid_call_data(&self, call_data: Span<felt252>) -> bool {
    // Implement validation logic here
    // For example, check the length and format of call_data
    // and ensure it corresponds to a known function signature
    true // Placeholder for actual validation logic
}
```

The impact of not validating `call_data` could be significant, as it could allow an attacker to execute arbitrary code or exploit the contract in other ways, leading to loss of funds or other security breaches. 


## Assessed type

Invalid Validation