xanoutas/solidity-security-findings huggingface dataset:
Reentrancy Vulnerability
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

The Abbot contract is vulnerable to reentrancy attacks due to insufficient checks in critical functions such as deposit, withdraw, forge, and melt. By exploiting this vulnerability, an attacker can repeatedly call these functions before previous executions are completed, allowing them to manipulate asset balances and forge more Yin than allowed. This could lead to significant financial losses for users of the contract and disrupt its normal operation.

## Proof of Concept
Attacker creates a trove and deposits assets, deploys a malicious contract executing a reentrancy attack. This contract calls the withdraw function of the Abbot contract in its fallback, triggering the attack. Afterward, the balance of the malicious contract is checked for successful fund drainage.
const Abbot = artifacts.require("Abbot");

contract("Reentrancy Attack", async (accounts) => {
  it("should successfully exploit reentrancy vulnerability", async () => {
    const attacker = accounts[1];
    const abbotInstance = await Abbot.deployed();

    // Create a trove and deposit assets
    const troveId = await abbotInstance.open_trove([assetBalance], forgeAmount, maxForgeFeePct, { from: attacker });

    // Define malicious contract
    const MaliciousContract = artifacts.require("MaliciousContract");
    const maliciousInstance = await MaliciousContract.deployed();

    // Set up the malicious contract to call withdraw function with a reentrant fallback
    await maliciousInstance.setAbbotContract(abbotInstance.address);
    await maliciousInstance.setTroveId(troveId);
    await maliciousInstance.setAttacker(attacker);

    // Trigger the reentrancy attack by calling the fallback function of the malicious contract
    await maliciousInstance.attack();

    // Check the balance after the attack
    const balance = await web3.eth.getBalance(maliciousInstance.address);
    assert.equal(balance, expectedBalanceAfterAttack, "Balance not drained as expected");
  });
});


## Tools Used

   Truffle framework

## Recommended Mitigation Steps
(1) Implement the Checks-Effects-Interactions pattern to ensure critical state changes precede external calls, reducing reentrancy risk. 

(2) Use a Reentrancy Guard to prevent reentrant calls during critical sections, ensuring no function reentry until execution completion. 

(3) Avoid State Changes after External Calls by finalizing contract state changes before interacting with external contracts to prevent reentrancy   vulnerabilities.




## Assessed type

Reentrancy
It is possible to liquidate troves when yangs are suspended
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

Liquidators can liquidate troves even if the collateral yang is suspended, which prevents users from restoring the health of their troves (loss of funds for the borrowers).

## Proof of concept

In lending projects where users have to maintain their positions healthy, and where there is a *"pause"* functionality for the collateral tokens, it is mandatory that:

1. Liquidators can **NOT** liquidate positions where the collateral is paused (otherwise it is a loss of funds for the borrowers)
2. There must be a grace period in which users will be able to deposit more collateral to heal their positions after a collateral token is unsuspended **BEFORE** liquidators can liquidate such positions (otherwise it becomes a who-comes-first war between liquidators and borrowers)

However, this is not implemented in the project. As walking through the code would make this report very dense, I have modified the already written `test_purger.cairo::test_liquidate_suspended_yang` test (which did revert, although it had the labbel `#[ignore]` so it was not shown by default when you do `scarb test`) to the next one:

```rs
    #[test]
    fn test_liquidate_suspended_yang() {
        let (shrine, abbot, _, absorber, purger, yangs, gates) = purger_utils::purger_deploy_with_searcher(
            purger_utils::SEARCHER_YIN.into(), Option::None
        );

        // user 1 opens a trove with ETH and BTC that is close to liquidation
        // `funded_healthy_trove` supplies 2 ETH and 0.5 BTC totalling $9000 in value, so we
        // create $6000 of debt to ensure the trove is closer to liquidation
        let trove_debt: Wad = (6000 * WAD_ONE).into();
        let target_trove: u64 = purger_utils::funded_healthy_trove(abbot, yangs, gates, trove_debt);

        // Suspend BTC
        let btc: ContractAddress = *yangs[1];
        let current_timestamp: u64 = get_block_timestamp();

        start_prank(CheatTarget::One(shrine.contract_address), shrine_utils::admin());
        shrine.suspend_yang(btc);
        stop_prank(CheatTarget::One(shrine.contract_address));

        assert(shrine.is_healthy(target_trove), 'should still be healthy');

        // The trove has $6000 in debt and $9000 in collateral. BTC's value must decrease
        let target_trove_start_health: Health = shrine.get_trove_health(target_trove);

        start_prank(CheatTarget::One(shrine.contract_address), shrine_utils::admin());
        shrine.set_threshold(btc, Ray { val : 1}); // @audit just a handy way to make a trove unhealthy
        stop_prank(CheatTarget::One(shrine.contract_address));
        
        assert(!shrine.is_healthy(target_trove), 'should be unhealthy');

        // Liquidate the trove
        let searcher = purger_utils::searcher();
        start_prank(CheatTarget::One(purger.contract_address), searcher);
        pu
overflow exist at assert_can_enter in sentinel.cairo can cause withdraw too many exceed max_amt
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
This vulnerability could allow an attacker to bypass the intended check on the maximum asset amount, potentially leading to unauthorized access or manipulation of assets within the contract

## Proof of Concept
The vulnerability can be demonstrated by providing input values that cause an integer overflow in the addition operation within the assert_can_enter function. By carefully crafting the input values, an attacker could make the sum of 'current_total' and 'enter_amt' exceed the maximum value representable by the data type used, thus bypassing the intended check on the maximum asset amount

call:
withdraw(https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/abbot.cairo#L203)
---->convert_to_yang(https://github.com/code-423n4/2024-01-opus/blob/4720e9481a4fb20f4ab4140f9cc391a23ede3817/src/core/abbot.cairo#L210)

The potential overflow issue in the assert_can_enter function, particularly when dealing with large yang_amt. To ensure that the addition operation doesn't result in an overflow



## Tools Used
Manual review
## Recommended Mitigation Steps
    assert(max_amt >= enter_amt, "SE: Exceeds max amount allowed");
    assert(current_total <= max_amt - enter_amt, "SE: Exceeds max amount allowed");




## Assessed type

Under/Overflow
User with 0 debt unfairly being assigned debt which can lead to their liquidation
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
User's with zero debt are assigned trove's debt, which was absorbed, this can lead to lower ltv or liquidation of user's trove. Especially is user has suspended yang balance, and he is assigned debt from redistribution, he will be liquidated at once.


## Proof of Concept

## Tools Used
VS Code

## Recommended Mitigation Steps
Instead of redistributing debt among all yang holder. Yang should be distributed only among debt holders by calculating yang_pct_update_per_unit_debt, and debt should be scaled for all borrowers. This will prevet unfair treatment to user with zero debt.


## Assessed type

Context
The initial deposit for adding a new Yang is not returned when the Yang is removed later. This means the depositor loses those funds permanently if the Yang is removed.
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Requiring an initial deposit that is not returned when a Yang is removed can lead to a loss of funds for the Yang provider. This could discourage Yang providers from adding their assets to the system.
The severity of this issue is high because it can lead to financial losses for Yang providers and potentially limit the number of assets available in the system, reducing its overall utility and adoption.


## Proof of Concept
The initial deposit for adding a new Yang is not returned when the Yang is removed later. This means the depositor loses those funds permanently if the Yang is removed.
Here is the relevant code where the initial deposit is required when adding a new Yang:

       fn add_yang(
           // ...
           gate: ContractAddress
       ) {
          // ...

           // Require an initial deposit when adding a yang to prevent first 
       depositor from front-running
           let yang_erc20 = IERC20Dispatcher { contract_address: yang }; 
           let initial_yang_amt: Wad = fixed_point_to_wad(INITIAL_DEPOSIT_AMT, 
       yang_erc20.decimals());
           let initial_deposit_amt: u256 = INITIAL_DEPOSIT_AMT.into();

           let caller: ContractAddress = get_caller_address();
           let success: bool = yang_erc20.transfer_from(caller, 
       gate.contract_address, initial_deposit_amt);
           assert(success, 'SE: Yang transfer failed');

           // ...
       }

There is no code to return this initial deposit when removing the Yang. This means the depositor permanently loses those token funds.


## Tools Used
Manual
## Recommended Mitigation Steps
The contract could be updated to allow withdrawing the initial deposit when removing a Yan


## Assessed type

Other
Loss of collateral for user earned through redistribution
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
In `withdraw_helper` function, which is called inside withdraw function, user's yang deposit amount is before it is updated in `self.charge(trove_id)`, incase of an exception redistribution. What this does is, it updates user's deposit as old value at end of function.
To withdraw = 4;
At start deposit = 10;  
At end of charge = 12; // assuming 2 yangs redistributed to user
At end of function = 6; // 10-4

This makes user loses yang he was assigned in redistribution.


## Proof of Concept

This test was added in test_shrine_redistribution test file
```
    #[test]
    fn test_shrine_one_exceptional_redistribution_wrong_update() {
        let shrine: IShrineDispatcher = shrine_utils::shrine_setup_with_feed(Option::None);
        let mut spy = spy_events(SpyOn::One(shrine.contract_address));

        // Manually set up troves so that the redistributed trove (trove 1) uses all three yangs
        // while the recipient troves (trove 2 and 3) uses only yang 2.
        let yangs: Span<ContractAddress> = shrine_utils::three_yang_addrs_reversed();
        let yang1_addr = *yangs.at(2);
        let yang2_addr = *yangs.at(1);
        let yang3_addr = *yangs.at(0);

        let trove1_owner = common::trove1_owner_addr();
        let redistributed_trove: u64 = common::TROVE_1;

        start_prank(CheatTarget::All, shrine_utils::admin());
        shrine.deposit(yang1_addr, redistributed_trove, shrine_utils::TROVE1_YANG1_DEPOSIT.into());
        shrine.deposit(yang2_addr, redistributed_trove, shrine_utils::TROVE1_YANG2_DEPOSIT.into());
        shrine.deposit(yang3_addr, redistributed_trove, shrine_utils::TROVE1_YANG3_DEPOSIT.into());
        shrine.forge(trove1_owner, redistributed_trove, shrine_utils::TROVE1_FORGE_AMT.into(), 0_u128.into());

        let trove2_owner = common::trove2_owner_addr();
        let recipient_trove1: u64 = common::TROVE_2;
        shrine.deposit(yang2_addr, recipient_trove1, TROVE2_YANG2_DEPOSIT.into());
        shrine.forge(trove2_owner, recipient_trove1, TROVE2_FORGE_AMT.into(), 0_u128.into());

        let trove3_owner = common::trove3_owner_addr();
        let recipient_trove2: u64 = common::TROVE_3;
        shrine.deposit(yang2_addr, recipient_trove2, TROVE3_YANG2_DEPOSIT.into());
        shrine.forge(trove3_owner, recipient_trove2, TROVE3_FORGE_AMT.into(), 0_u128.into());

        let before_recipient_trove1_health: Health = shrine.get_trove_health(recipient_trove1);
        let before_recipient_trove2_health: Health = shrine.get_trove_health(recipient_trove2);

        // Note that since there is only one yang in recipient troves, the check here is a lot simpler because
        // all redistributions will follow the same proportion. See the next test with two recipient yangs for
        // a more detailed calculation when there is more than one recipient yang with different proportions
Neglect of Exceptional Redistribution Amounts in `withdraw_helper` Function
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Issue Description

The `withdraw_helper` function in the `shrine` contract handles withdrawal logic for both the `withdraw` and `seize` functions. It is responsible for updating trove balances, total yang balances, and charging interest for the trove via the `charge` function. However, there is an oversight in the current implementation:

```cairo
fn withdraw_helper(ref self: ContractState, yang: ContractAddress, trove_id: u64, amount: Wad) {
    ...

    let new_trove_balance: Wad = trove_balance - amount;
    let new_total: Wad = self.yang_total.read(yang_id) - amount;

    self.charge(trove_id);

    //@audit will no account for exceptional redistribution added to deposits balance in `charge` call

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

The issue in the code above is that the `withdraw_helper` function then proceeds to update the storage variables `yang_total` and `deposits` using the previously calculated `new_total` and `new_trove_balance` values without accounting for any new yang balance added to the trove after an exceptional redistribution. This results in neglecting any exceptional redistributions added to the `deposits` balance during the `charge` call :

```cairo
fn charge(ref self: ContractState, trove_id: u64) {
    ...

    // If there was any exceptional redistribution, write updated yang amounts to trove
    if updated_trove_yang_balances.is_some() {
        let mut updated_trove_yang_balances = updated_trove_yang_balances.unwrap();
        loop {
            match updated_trove_yang_balances.pop_front() {
                Option::Some(yang_balance) => {
                    //@audit will updated the trove yang balance
                    self.deposits.write((*yang_balance.yang_id, trove_id), *yang_balance.amount);
                },
                Option::None => { break; },
            };
        };
    }

    ...
}
```

Because the trove `deposits` map is changed in `charge` function but `withdraw_helper` uses directly the value `new_trove_balance` which was calculated before the `charge` call, the exceptional redistribution added to `deposits` will be overriden and will be neglected in the trove yang balance.

This oversight could result in financial losses for all protocol users. When users withdraw yang amounts, any exceptional redistributions that should have been added to their trove balances will be neglected and lost.

## Impact

Users are at risk of losing all yang exceptional redistribution amounts due to an error in the `withdraw_helper` function, which causes it to ne
System can be put in recovery mode intentionally to liquidate other users
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
A malicious user can create a new trove and either borrow a huge amount of Yin, or withdraw just enough portion of his collateral to force system into recovery mode, to trigger liquidation for troves whose ltv is near their threshold, thus liquidating them and earning liquidation bonuses, all this in a single transaction. It should never be possible for a user transaction to put system in a recovery mode, otherwise it can be exploited

In similar project like Liquity, it is always ensured at end of borrow or withdraw that system is not put into recovery mode, thereby triggering liquidations.

## Proof of Concept
Here is unit test in shrine

```
    #[test]
    fn test_liquidate_recovery_exploit() {
        let shrine: IShrineDispatcher = shrine_utils::recovery_mode_test_setup(Option::None);

        start_prank(CheatTarget::All, shrine_utils::admin());

        shrine.forge(common::trove1_owner_addr(), common::WHALE_TROVE, 300000000000000000000000_u128.into(), 0_u128.into());

        let trove_health: Health = shrine.get_trove_health(common::TROVE_1);

        assert(trove_health.ltv > trove_health.threshold,'Position not liquidatable');
    }

```

This shows by borrowing a huge amount, whale put system in recovery mode and have trove health below threshold, where it can be liquidated

## Tools Used
VS Code
Scarb

## Recommended Mitigation Steps
Add following lines at end of both borrow and withdraw
```
if(self.get_live()){
  assert(!self.is_recovery_mode(), 'SH: System in recovery mode');
}
```



## Assessed type

Other
pull_redistributed_debt_and_yangs() may get more yang than deserved.
Audit: 2024-01-opus-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Vulnerability details

in `pull_redistributed_debt_and_yangs()`

If `is_exceptional_redistribution=true`, we calculate the deserved `yang` based on the `recipient_yang_balance.amount` at that time.

`yang_increment = recipient_yang_balance.amount * exc_yang_redistribution.unit_yang`

```rust
       fn pull_redistributed_debt_and_yangs(
            self: @ContractState, trove_id: u64, mut trove_yang_balances: Span<YangBalance>, mut trove_debt: Wad
        ) -> (Option<Span<YangBalance>>, Wad) {
            let trove_last_redistribution_id: u32 = self.trove_redistribution_id.read(trove_id);
            let current_redistribution_id: u32 = self.redistributions_count.read();

            // Early termination if no redistributions since trove was last updated
            if current_redistribution_id == trove_last_redistribution_id {
                return (Option::None, trove_debt);
            }

            let mut has_exceptional_redistributions: bool = false;

            // Outer loop over redistribution IDs.
            // We need to iterate over redistribution IDs, because redistributed collateral from exceptional
            // redistributions may in turn receive subsequent redistributions
            let mut tmp_redistribution_id: u32 = trove_last_redistribution_id + 1;

            // 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;
            let loop_end: u32 = current_redistribution_id + 1;
            loop {
                if tmp_redistribution_id == loop_end {
                    break;
                }

                let is_exceptional: bool = self.is_exceptional_redistribution.read(tmp_redistribution_id);
                if is_exceptional {
                    has_exceptional_redistributions = true;
                }

                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) => {
                            let redistribution: YangRedistribution = self
                                .yang_redistributions
                                .read((*original_yang_balance.yang_id, tmp_redistribution_id));
                            // If the trove has deposited a yang, check for ordinary redistribution first.
                            // Note that we cannot skip to the next yang at the end of this `if` block because
                            // we still need to check for exceptional redistribution in case the recipient pool
                            // amount was below `MIN_RECIPIENT_POOL_YANG`.
                            if (*original_yang_balance.amount).is_no