xanoutas/solidity-security-findings huggingface dataset:
Exchange rate calculation exploit via Stake/Unstake
Audit: 2024-07-reserve-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-reserve/blob/main/contracts/p1/StRSR.sol#L304


# Vulnerability details

## Impact

I found a bug in the [StRSR.sol](https://github.com/code-423n4/2024-07-reserve/blob/main/contracts/p1/StRSR.sol)
currently it allows for an attacker to exploit the stake/unstake mechanism to unfairly extract more value from the system than they should be entitled to. This enables a vector to a loss of funds for good intending stakers, and destabilises the staking mechanism.

## Proof of Concept

So the bug is `unstake` and `withdraw`:

Please paste this test into [ZZStRSR.test.ts](https://github.com/code-423n4/2024-07-reserve/blob/main/test/ZZStRSR.test.ts) to run to prove bug: 

```typescript
  describe('StRSRP1 Critical Vulnerability - Stake/Unstake Manipulation', () => {
    let owner: SignerWithAddress
    let addr1: SignerWithAddress
    let addr2: SignerWithAddress
    let attacker: SignerWithAddress
  
    let rsr: ERC20Mock
    let stRSR: TestIStRSR
    let main: TestIMain
    let backingManager: TestIBackingManager
  
    beforeEach(async () => {
      [owner, addr1, addr2, attacker] = await ethers.getSigners()
      ;({ rsr, stRSR, main, backingManager } = await loadFixture(defaultFixture))
    })
  
    it('should not allow exploitation through stake/unstake manipulation', async () => {
      // Initial setup
      const initialStake = fp('1000000') // 1 million RSR
      await rsr.connect(owner).mint(addr1.address, initialStake)
      await rsr.connect(addr1).approve(stRSR.address, initialStake)
      await stRSR.connect(addr1).stake(initialStake)
  
      // Attacker's initial stake
      const attackerStake = fp('1000') // 1000 RSR
      await rsr.connect(owner).mint(attacker.address, attackerStake.mul(2)) // Give extra for gas
      await rsr.connect(attacker).approve(stRSR.address, attackerStake)
      await stRSR.connect(attacker).stake(attackerStake)
  
      // Record initial states
      const initialExchangeRate = await stRSR.exchangeRate()
      console.log('Initial exchange rate:', ethers.utils.formatUnits(initialExchangeRate, 18))
  
      // Attacker starts unstaking process
      await stRSR.connect(attacker).unstake(attackerStake)
  
      // Simulate some rewards coming in
      const rewardAmount = fp('10000') // 10,000 RSR as reward
      await rsr.connect(owner).mint(stRSR.address, rewardAmount)
  
      // Fast forward time, but not enough to complete unstaking
      const unstakingDelay = await stRSR.unstakingDelay()
      await ethers.provider.send('evm_increaseTime', [ethers.BigNumber.from(unstakingDelay).div(2).toNumber()])
      await ethers.provider.send('evm_mine', [])
  
      // Trigger reward distribution
      await stRSR.payoutRewards()
  
      // Record states after reward
      const midExchangeRate = await stRSR.exchangeRate()
      console.log('Exchange rate after reward:', ethers.utils.formatUnits(midExchangeRate, 18))
  
      // Attacker tries to game the sys
Incorrect Balance Update in `FurnaceP1::melt()` Will Lead to Reduced RToken Melting Over Time
Audit: 2024-07-reserve-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/Furnace.sol#L65-L79


# Vulnerability details


## Description

The `FurnaceP1` contract implements a mechanism to continuously melt RTokens based on a predefined ratio and elapsed time. The [`melt()` function](https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/Furnace.sol#L65-L79) is responsible for calculating and executing this melting process. However, there is a critical issue in the implementation where the `lastPayoutBal` is updated incorrectly, leading to a compounding error that results in less RToken being melted over time than intended.

The [`melt()` function](https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/Furnace.sol#L65-L79) calculates the amount of RTokens to be melted based on the `lastPayoutBal`, the elapsed time since the last payout, and the melting ratio. The function then updates the `lastPayoutBal` and calls `rToken.melt()` to burn the calculated amount of tokens. The issue arises from the order of these operations:

```solidity
function melt() public {
    // ... (calculation of amount to melt)

    lastPayout += numPeriods;
    lastPayoutBal = rToken.balanceOf(address(this)) - amount; // Incorrect update
    if (amount != 0) rToken.melt(amount);
}
```

The `lastPayoutBal` is updated before the actual melting occurs, which means it doesn't accurately reflect the balance after melting. This leads to a compounding error where each subsequent call to `melt()` uses an inflated balance as its starting point, resulting in progressively less RToken being melted than intended.

## Impact

The incorrect balance update in the `melt()` function leads to a systematic undermelting of RTokens over time. This deviation from the intended melting rate can have significant implications for the protocol's tokenomics and overall economic model. As the error compounds with each `melt()` call, the discrepancy between the intended and actual melting rates will grow larger over time.

This could lead to an oversupply of RTokens in circulation compared to what the protocol design intended, potentially affecting the token's value and the overall stability of the ecosystem.

## Proof of Concept

To illustrate the impact of this bug, consider the following scenario:

1. Initial RToken balance in the Furnace: 1,000,000 tokens
2. Melting ratio: 1% per period
3. Assume `melt()` is called once per period

Period 1:
- Expected melt amount: 10,000 tokens (1% of 1,000,000)
- Actual melt amount: 10,000 tokens
- `lastPayoutBal` is set to 990,000 (1,000,000 - 10,000) before melting

Period 2:
- Expected melt amount: 9,900 tokens (1% of 990,000)
- Actual melt amount: 9,900 tokens
- `lastPayoutBal` is set to 980,100 (990,000 - 9,900) before melting

Period 3:
- Expected melt amount: 9,801 tokens (1% of 980,100)
- Actual melt amount:
Users will be unable to redeem tokens when unregistered assets exist in old baskets (`RTokenP1::redeemCustom`)
Audit: 2024-07-reserve-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/RToken.sol#L253-L345


# Vulnerability details

## Description

The `RTokenP1` contract implements a [`redeemCustom()` function ](https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/RToken.sol#L253-L345) that allows users to redeem their RTokens for a custom combination of historical baskets. This function relies on the `basketHandler.quoteCustomRedemption()` method to determine the amounts of each asset to be redeemed. However, there is a critical issue in the implementation that can cause the function to revert unexpectedly.

The root cause of the problem lies in the assumption made by `quoteCustomRedemption()` that the size of the `erc20sAll` array should match `assetRegistry.size()`. This assumption breaks down when an asset that was previously part of a basket becomes unregistered but still exists in an old basket.

The [`redeemCustom()` function](https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/RToken.sol#L253-L345) follows these main steps:
1. Refreshes the asset registry
2. Performs various checks and calculations
3. Calls `basketHandler.quoteCustomRedemption()`
4. Processes the redemption based on the quoted amounts

The issue occurs in step 3, where `quoteCustomRedemption()` creates the `erc20sAll` array with a length equal to `assetRegistry.size()`. If an old basket contains more assets than the current `assetRegistry.size()`, the function will attempt to access an index that does not exist, causing an out-of-bounds error and reverting the transaction.

This bug effectively prevents users from redeeming their tokens using the `redeemCustom()` function when unregistered assets exist in old baskets, which is a critical functionality of the protocol.

## Impact

The impact of this issue is severe as it can lead to a denial of service for users attempting to redeem their tokens through the `redeemCustom()` function. This can result in:

1. Loss of trust in the protocol, as users may be unable to access their funds when needed.
2. Potential financial losses for users who are unable to redeem their tokens at crucial times.
3. A possible depeg situation where the RToken's value diverges from its intended value due to the inability to redeem.

## Proof of Concept

Consider the following scenario:

1. The RToken is initially deployed with a basket containing 0.9 USDC, 0.05 USDT, and 0.05 DAI.
2. A governance proposal is passed to change the basket composition to 0.9 DAI and 0.1 USDC, and to unregister USDT.
3. Trading is paused before the rebalancing can be executed, resulting in the backing manager still holding assets according to the old basket composition.
4. A user attempts to redeem their RTokens using `redeemCustom()`, specifying the old basket composition.
5. The `basketHandler.quoteCustomRedemption()` functio
Hyperinflation of RSR tokens during withdrawal
Audit: 2024-07-reserve-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-reserve/blob/main/contracts/p1/StRSR.sol#L304


# Vulnerability details

## Where is bug

Located in [withdraw()](https://github.com/code-423n4/2024-07-reserve/blob/main/contracts/p1/StRSR.sol#L304) of [StRSRP.sol](https://github.com/code-423n4/2024-07-reserve/blob/main/contracts/p1/StRSR.sol) 

## Impact
The flow of the issue is as follows: 
- Firstly: Users stake `RSR` tokens into the contract.
- Secondly: Rewards are added to the contract, but not properly accounted for in the exchange rate.
- Issue begins here: When users withdraw their stakes, they receive significantly more `RSR` than they initially staked or earned through rewards.

Impact of this is an issue that enables hyperinflation of `RSR` tokens. Users can withdraw more `RSR` than they should be entitled to, aggressive attack vectors may even enable draining the contract of all funds/creating tokens out of thin air. This undermines the intended staking system.

## Proof of Concept
Focusing here in the `withdraw` function, is where the bug occurs:

```solidity
function withdraw(address account, uint256 endId) external {
    // safe here

    uint256 newTotalDrafts = totalDrafts - draftAmount;
    // newDraftRSR: {qRSR} = {qDrafts} * D18 / D18{qDrafts/qRSR}
    uint256 newDraftRSR = (newTotalDrafts * FIX_ONE_256 + (draftRate - 1)) / draftRate;
    uint256 rsrAmount = draftRSR - newDraftRSR;

    if (rsrAmount == 0) return;

    // ==== Transfer RSR from the draft pool
    totalDrafts = newTotalDrafts;
    draftRSR = newDraftRSR;

    // == Interactions ==
    leakyRefresh(rsrAmount);
    IERC20Upgradeable(address(rsr)).safeTransfer(account, rsrAmount); // BUG: Incorrect rsrAmount calculation
    emit UnstakingCompleted(firstId, endId, draftEra, account, rsrAmount);

    // == Checks ==
    require(basketHandler.isReady() && basketHandler.fullyCollateralized(), "RToken readying");
}
```

So this issue occurs in the calculation of `rsrAmount`. It does not correctly account for the actual `RSR` value of the drafts being withdrawn, opening up the path to an inflated withdrawal amount.

I wrote this test to show the issue, please past into [ZZStRSR.test.ts](https://github.com/code-423n4/2024-07-reserve/blob/main/test/ZZStRSR.test.ts) to run:

```typescript
  it('should handle multiple users, stakes, and withdrawals correctly', async function () {
    // Setup
    const stakeAmount = bn('1000e18')
    await rsr.connect(owner).mint(addr1.address, stakeAmount.mul(2))
    await rsr.connect(owner).mint(addr2.address, stakeAmount)
  
    // Addr1 and Addr2 stake
    await rsr.connect(addr1).approve(stRSR.address, stakeAmount.mul(2))
    await rsr.connect(addr2).approve(stRSR.address, stakeAmount)
    await stRSR.connect(addr1).stake(stakeAmount)
    await stRSR.connect(addr2).stake(stakeAmount)
  
    console.log('Initial stakes:', stakeAmount.toString())
  
    // Simulate rewards
    const rewardAmount = stakeAmount.div(2)
    await rsr.co
Unexpected loss of funds due to missing of the return value check
Audit: 2024-07-reserve-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-reserve/blob/main/contracts/p1/RToken.sol#L105-L155
https://github.com/code-423n4/2024-07-reserve/blob/main/contracts/p1/RToken.sol#L183-L225
https://github.com/code-423n4/2024-07-reserve/blob/main/contracts/p1/RToken.sol#L253-L344


# Vulnerability details

## Impact
Unexpected loss of funds by the protocol or user may occur due to a missing of the the return value check of the `quote()` function of the `BasketHandler` contract in the `issueTo()` and the `redeemTo()` function of the `RToken` contract.
- Malicious users can issue RTokens for free by using `issueTo()` function.
- Users can lose RTokens by `redeemTo()` function.

## Proof of Concept
The `RToken.sol#redeemCustom()` function performs a check to prevent loss of funds if `BasketHandler.sol#quoteCustomRedemption()` returns an empty redeemtion.
```solidity
    function redeemCustom(
        ...
    ) external notFrozen {
        SNIP...

        // === Get basket redemption amounts ===

286:    (address[] memory erc20s, uint256[] memory amounts) = basketHandler.quoteCustomRedemption(
            basketNonces,
            portions,
            baskets
        );

        SNIP...

        // === Save initial recipient balances ===

        uint256[] memory pastBals = new uint256[](expectedERC20sOut.length);
        for (uint256 i = 0; i < expectedERC20sOut.length; ++i) {
            pastBals[i] = IERC20(expectedERC20sOut[i]).balanceOf(recipient);
            // we haven't verified this ERC20 is registered but this is always a staticcall
        }

        // === Interactions ===

        // Distribute tokens; revert if empty redemption
        {
320:        bool allZero = true;
            for (uint256 i = 0; i < erc20s.length; ++i) {
                if (amounts[i] == 0) continue; // unregistered ERC20s will have 0 amount
323:            if (allZero) allZero = false;

                // Send withdrawal
                // slither-disable-next-line arbitrary-send-erc20
                IERC20Upgradeable(erc20s[i]).safeTransferFrom(
                    address(backingManager),
                    recipient,
                    amounts[i]
                );
            }
333:        if (allZero) revert("empty redemption");
        }

        // === Post-checks ===

        // Check post-balances
        for (uint256 i = 0; i < expectedERC20sOut.length; ++i) {
            uint256 bal = IERC20(expectedERC20sOut[i]).balanceOf(recipient);
            // we haven't verified this ERC20 is registered but this is always a staticcall
            require(bal - pastBals[i] >= minAmounts[i], "redemption below minimum");
        }
    }
```
As you can see, if the returned token array returns an empty array, `allZero` in #333 will remain `true`, so the transaction will be reverted and the user's RToken will not be burned.

This means that users will keep their RTokens and will not suffer any losses.

However, in the `redeemTo()` and `issueTo()` functions
The Loss of Fund of the protocol due to Incorrect Boundary Check of Loop
Audit: 2024-07-reserve-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-reserve/blob/main/contracts/p1/BasketHandler.sol#L521-L600


# Vulnerability details

## Impact
Protocol funds can be lost due to incorrect bounds checking on the loop variable in the `BasketHandler.sol#quoteCustomRedemption()` function.


## Proof of Concept
The `BasketHandler.sol#quoteCustomRedemption()` function returns the redemption value of `amount` BUs for a linear combination of historical baskets.
```solidity
    function quoteCustomRedemption(
        uint48[] memory basketNonces,
        uint192[] memory portions,
        uint192 amount
    ) external view returns (address[] memory erc20s, uint256[] memory quantities) {
        SNIP...

532:    uint256 len; // length of return arrays

        // Calculate the linear combination basket
        for (uint48 i = 0; i < basketNonces.length; ++i) {
            SNIP...

            // Add-in refAmts contribution from historical basket
            Basket storage b = basketHistory[basketNonces[i]];
            for (uint256 j = 0; j < b.erc20s.length; ++j) {
                // untestable:
                //     previous baskets erc20s do not contain the zero address
                if (address(b.erc20s[j]) == address(0)) continue;

                // Search through erc20sAll
553:            uint256 erc20Index = type(uint256).max;
554:            for (uint256 k = 0; k < len; ++k) {
555:                if (b.erc20s[j] == erc20sAll[k]) {
556:                    erc20Index = k;
557:                    break;
558:                }
559:            }

                // Add new ERC20 entry if not found
                uint192 amt = portions[i].mul(b.refAmts[b.erc20s[j]], FLOOR);
563:            if (erc20Index == type(uint256).max) {
                    // New entry found

                    try assetRegistry.toAsset(b.erc20s[j]) returns (IAsset asset) {
                        if (!asset.isCollateral()) continue; // skip token if not collateral

                        erc20sAll[len] = b.erc20s[j];
                        collsAll[len] = ICollateral(address(asset));

                        // {ref} = {1} * {ref}
                        refAmtsAll[len] = amt;
574:                    ++len;
                    } catch (bytes memory errData) {
                        // untested:
                        //     OOG pattern tested in other contracts, cost to test here is high
                        // see: docs/solidity-style.md#Catching-Empty-Data
                        if (errData.length == 0) revert(); // solhint-disable-line reason-string
                        // skip token if no longer registered or other non-gas issue
                    }
                } else {
                    // {ref} = {1} * {ref}
                    refAmtsAll[erc20Index] += amt;
                }
            }
        }

589:    erc20s = new address[](len);
590:    quantities = new uint256[](len);

        // Calculate quantities
593:    for (uint256 i = 0; i < le
Potential Manipulation of Draft RSR During Mass Unstaking Events
Audit: 2024-07-reserve-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/StRSR.sol#L614-L623
https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/StRSR.sol#L490-L499
https://github.com/code-423n4/2024-07-reserve/blob/3f133997e186465f4904553b0f8e86ecb7bbacbf/contracts/p1/StRSR.sol#L693-L695


# Vulnerability details

## Impact
When `stakeRate` is no longer secure, `stRSR` can initiate a new era and draft era through the `resetStakes()` function. However, this function only checks the `stakeRate`, leaving an opportunity for attackers to exploit `draftRSR`.   

Considering that RSR exists as an over-collateralized asset for rToken, in the event of rToken price de-pegging, RSR stakers are likely to unstake in large quantities. Due to the `unstakingDelay`, a significant amount of `stakeRSR` would be treated as `draftRSR`. Moreover, when these users unstake, their `stRSR` is burned, resulting in a loss of voting rights.  

This situation can lead to a scenario where a small amount of `stRSR` with voting rights can influence a large amount of `draftRSR`. Furthermore, when `stakeRSR` is low, it becomes easy to manipulate the `stakeRate`. Attackers can exploit the vulnerability in `resetStake()`, which only checks `stakeRate`, to forcefully execute `resetStakes()` and subsequently `beginDraftEra()`. This action would cause a large amount of `draftRSR` to be considered as distributable `rewardsRSR` in the new Era, preventing stakers from withdrawing their `draftRSR`.  

The potential impact of this vulnerability is severe:  
1. Loss of funds: Stakers may permanently lose access to their unstaked RSR.  
2. Governance manipulation: A small group of stakers could disproportionately influence the protocol's decisions.  
3. Economic imbalance: The misallocation of draftRSR as rewardsRSR could significantly disrupt the protocol's economic model.  


## Proof of Concept
Assume the stRSR contract initially has 1M DAI staked. Due to market volatility or discovery of a protocol bug, a large amount of funds are unstaked, leaving only 0.01 DAI staked in the protocol:  

1. From the system's perspective, stakeRSR is 0.01, corresponding to 0.01 stRSR (assuming stakeRate is still FIX_ONE), while draftRSR is approximately 1M. Since draftRSR has already burned stRSR, this portion of funds has no voting rights.  

2. An attacker donates 110,000 USDC to the stRSR contract, which will be treated as rewardsRSR.  

3. Assuming rewardRatio is MAX_REWARD_RATIO (0.01), after approximately 1006 seconds (16 minutes 48 seconds), about 11,000 USDC will be `_payoutRewards()`, reducing the stakeRate to < 1e12.  

4. At this point, the attacker can stake some RSR to gain voting rights and force the `resetStakes()` proposal to pass.  

5. Due to beginDraftEra(), any Drafts that haven't been withdrawn in time will be treated as rewardsRSR in the new Era.  

6. Users will never be able to wi