xanoutas/solidity-security-findings huggingface dataset:
`stake()` when `pausedOrFrozen` will dilute other users rewards
Audit: 2023-01-reserve-findings
Severity: MEDIUM

# Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSR.sol#L212-L237


# Vulnerability details

## Impact

There is no requirement of non pause/frozen mode to `stake()`. The difference for these modes is the rewards payout. During pause/frozen mode, rewards will not be paid out before the stake is recorded. As a result, new stakes in pause/frozen mode will dilute the current stakers shares in StRSR, and lose rewards in the future.



## Proof of Concept

Even in pause/frozen mode, anyone can still call `stake()`. But in pause/frozen mode, accumulated rewards will not be paid out.
```solidity
File: contracts/p1/StRSR.sol
212:     function stake(uint256 rsrAmount) external {
213:         require(rsrAmount > 0, "Cannot stake zero");
214: 
215:         if (!main.pausedOrFrozen()) _payoutRewards();
```

If `_payoutRewards()` is called, `stakeRSR` and `stakeRate` will be updated. `stakeRSR` will increase while `stakeRate` will decrease.
```solidity
496:     function _payoutRewards() internal {

513:             stakeRSR += payout;

524:         stakeRate = (stakeRSR == 0 || totalStakes == 0)
525:             ? FIX_ONE
526:             : uint192((totalStakes * FIX_ONE_256 + (stakeRSR - 1)) / stakeRSR);
```

Then in `stake()`, `stakeRSR` and `stakeRate` will be used to calculate the `stakeAmount` to mint.
```solidity
212:     function stake(uint256 rsrAmount) external {

222:         uint256 newStakeRSR = stakeRSR + rsrAmount;
223:         // newTotalStakes: {qStRSR} = D18{qStRSR/qRSR} * {qRSR} / D18
224:         uint256 newTotalStakes = (stakeRate * newStakeRSR) / FIX_ONE;
225:         uint256 stakeAmount = newTotalStakes - totalStakes;
226: 
227:         // Update staked
228:         address account = _msgSender();
229:         stakeRSR += rsrAmount;
230:         _mint(account, stakeAmount);
```

If in pause/frozen mode, new rsr is staked, existing stakers StRSR will be diluted due to the missing rewards. 

Assuming, `stakeRSR` and `totalStakes` are both 100 (`stakeRate` is 1), if `_payoutRewards()`, `stakeRSR` will increase to 125, `stakeRate` will become 0.8.
Now some stake will try to stake rsr of 100.
For non pause/frozen mode, the new `stakeAmout` to mint is 80. Means this new staker will get 80 / (80 + 100) = 4 / 9 = 44% of future rewards.
However, in pause/frozen mode, no rewards will be paid out, and `stakeRSR` will remain 100, `stakeRate` keeps 1. The new staker will get 100 StRSR to mint, and 100 / (100 + 100) = 1 / 2 = 50% of future rewards.

In the above example, the difference of 44% and 50% of future rewards shares comes from the missed accumulated rewards. 

This staking and rewards mechanism will make the existing stakers lose rewards in the future. And malicious users can try to stake large amount on purpose if the system goes into pause/frozen mode.


## Tools Used
Manual analysis.

## Recommended Mitigation Steps

Disallow `stake()` in pausedOrFrozen mode, just l
`Asset.lotPrice()` doesn't use the most recent price in case of oracle timeout
Audit: 2023-01-reserve-findings
Severity: MEDIUM

# Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/assets/Asset.sol#L144-L145


# Vulnerability details

## Impact

`Asset.lotPrice()` has a fallback mechanism in case that `tryPrice()` fails - it uses the last saved price and multiplies its value by `lotMultiplier` (a variable that decreases as the time since the last saved price increase) and returns the results.
However, the `tryPrice()` might fail due to oracle timeout, in that case the last saved price might be older than the oracle's price.

This can cause the backing manager to misestimate the value of the asset, trade it at a lower price, or do an unnecessary haircut.

## Proof of Concept
In the PoC below:
* Oracle price is set at day 0
* The asset is refreshed (e.g. somebody issued/vested/redeemed)
* After 5 days the oracle gets an update
* 25 hours later the `lotPrice()` is calculated based on the oracle price from day 0 even though a price from day 5 is available from the oracle
* Oracle gets another update
* 25 hours later the `lotPrice()` goes down to zero since it considers the price from day 0 (which is more than a week ago) to be the last saved price, even though a price from a day ago is available from the oracle

```diff
diff --git a/test/fixtures.ts b/test/fixtures.ts
index 5299a5f6..75ca8010 100644
--- a/test/fixtures.ts
+++ b/test/fixtures.ts
@@ -69,7 +69,7 @@ export const SLOW = !!useEnv('SLOW')
 
 export const PRICE_TIMEOUT = bn('604800') // 1 week
 
-export const ORACLE_TIMEOUT = bn('281474976710655').div(2) // type(uint48).max / 2
+export const ORACLE_TIMEOUT = bn('86400') // one day
 
 export const ORACLE_ERROR = fp('0.01') // 1% oracle error
 
diff --git a/test/plugins/Asset.test.ts b/test/plugins/Asset.test.ts
index d49c53f3..7f2f721e 100644
--- a/test/plugins/Asset.test.ts
+++ b/test/plugins/Asset.test.ts
@@ -233,6 +233,45 @@ describe('Assets contracts #fast', () => {
       )
     })
 
+    it('PoC lot price doesn\'t use most recent price', async () => {
+      // Update values in Oracles to 0
+
+      await setOraclePrice(rsrAsset.address, bn('1.1e8'))
+
+      await rsrAsset.refresh();
+      let [lotLow, lotHigh] = await rsrAsset.lotPrice();
+      let descripion = "day 0";
+      console.log({descripion, lotLow, lotHigh});
+      let hour = 60*60;
+      let day = hour*24;
+      await advanceTime(day * 5);
+
+      await setOraclePrice(rsrAsset.address, bn('2e8'));
+      // await rsrAsset.refresh();
+
+      [lotLow, lotHigh] = await rsrAsset.lotPrice();
+      descripion = 'after 5 days (right after update)';
+      console.log({descripion,lotLow, lotHigh});
+
+      await advanceTime(day + hour);
+
+      // Fallback prices should be zero
+
+      [lotLow, lotHigh] = await rsrAsset.lotPrice();
+      descripion = 'after 6+ days';
+      console.log({descripion, lotLow, lotHigh});
+
+      await setOraclePrice(rsrAsset.address, bn('2e8'));
+
+      await advanceTime(day + hour);
+
+
Withdrawals will stuck
Audit: 2023-01-reserve-findings
Severity: MEDIUM

# Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSR.sol#L397


# Vulnerability details

## Impact
If a new era gets started for stakeRSR and draftRSR still point to old era then user will be at risk of losing their future holdings

## Proof of Concept
1. seizeRSR is called with amount 150 where stakeRSR was 50 and draftRSR was 80. The era was 1 currently for both stake and draft
```
function seizeRSR(uint256 rsrAmount) external notPausedOrFrozen {
...
stakeRSR -= stakeRSRToTake;
..
if (stakeRSR == 0 || stakeRate > MAX_STAKE_RATE) {
            seizedRSR += stakeRSR;
            beginEra();
        }
...
draftRSR -= draftRSRToTake;
if (draftRSR > 0) {
            // Downcast is safe: totalDrafts is 1e38 at most so expression maximum value is 1e56
            draftRate = uint192((FIX_ONE_256 * totalDrafts + (draftRSR - 1)) / draftRSR);
        }

...
```

2. stakeRSR portion comes to be 50 which means remaining stakeRSR will be 0 (50-50). This means a new staking era will get started

```
if (stakeRSR == 0 || stakeRate > MAX_STAKE_RATE) {
            seizedRSR += stakeRSR;
            beginEra();
        }
```

3. This causes staking era to become 2

```
function beginEra() internal virtual {
        stakeRSR = 0;
        totalStakes = 0;
        stakeRate = FIX_ONE;
        era++;

        emit AllBalancesReset(era);
    }
```

4. Now draftRSR is still > 0 so only draftRate gets updated. The draft Era still remains 1

5. User stakes and unstakes in this new era. Staking is done in era 2

6. Unstaking calls the pushDraft which creates User draft on draftEra which is still 1

```
function pushDraft(address account, uint256 rsrAmount)
        internal
        returns (uint256 index, uint64 availableAt)
    {
...
CumulativeDraft[] storage queue = draftQueues[draftEra][account];
       ...

        queue.push(CumulativeDraft(uint176(oldDrafts + draftAmount), availableAt));
...
}
```

7. Lets say due to unfortunate condition again seizeRSR need to be called. This time draftRate > MAX_DRAFT_RATE which means draft era increases and becomes 2

```
if (draftRSR == 0 || draftRate > MAX_DRAFT_RATE) {
            seizedRSR += draftRSR;
            beginDraftEra();
        }
```

8. This becomes a problem since all unstaking done till now in era 2 were pointing in draft era 1. Once draft era gets updated to 2, all those unstaking are lost

## Recommended Mitigation Steps
Era should be same for staking and draft. So if User is unstaking at era 1 then withdrawal draft should always be era 1 and not some previous era
Unsafe downcasting in `issue(...)` can be exploited to cause permanent DoS
Audit: 2023-01-reserve-findings
Severity: MEDIUM

# Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L230-L243


# Vulnerability details

## Unsafe downcasting in `issue(...)` can be exploited to cause permanent DoS

#### Important note!
I first found this bug in `issue(...)` at first, but unsafe downcasting appears in many other areas of the codebase, and seem to also be exploitable but no PoC is provided due to time constraints. Either way, using some form of safe casting library to **replace all occurences** of unsafe downcasting will prevent all the issues. I also do not list the individual instances of unsafe downcasting as all occurences should be replaced with safe cast.

### Details
The `amtRToken` is a user supplied parameter in the `issue(uint256 amtRToken)` function
```sol
uint192 amtBaskets = uint192(
	totalSupply() > 0 ? mulDiv256(basketsNeeded, amtRToken, totalSupply()) : amtRToken
);
```
The calculated amount is unsafely downcasted into `uint192`.

This means that if the resulting calculation is a multiple of $2^{192}$, `amtBaskets = 0`

The code proceeds to the following line, where `erc20s` and `deposits` arrays will be empty since we are asking for a quote for 0. (see `quote(...)` in `BasketHandler.sol` where amounts are multiplied by zero)
```sol
(address[] memory erc20s, uint256[] memory deposits) = basketHandler.quote(
            amtBaskets,
            CEIL
        );
```
This means an attacker can call `issue(...)` with a very high `amtRToken` amount that is a multiple of $2^{192}$, without depositing any amount of collateral.

The DoS issues arises because `whenFinished(uint256 amtRToken)` is dependent on `amtRToken`. With such a high value, `allVestAt` will be set so far in the future that it causes a permanent DoS. i.e. Issuances will never vest.
```sol
uint192 vestingEnd = whenFinished(amtRToken); // D18{block number}
```

### Proof of Concept
This PoC demonstrates that an attacker can call `issue(...)` without collateral tokens to modify `allVestAt` variable to an extreme value, such that all further issuances cannot be vested for all users.

Do note that the PoC is done with `totalSupply() == 0` case, so we supply `amtRToken` as a multiple of $2^{192}$. Even if there is an existing `totalSupply()`, we just need to calculate a value for `amtRToken >= 2^192` such that $\frac{\text{basketsNeeded} \times \text{amtRToken}}{totalSupply()} = 0$. This attack does not require `totalSupply()` be zero.

```sol
uint192 amtBaskets = uint192(
	totalSupply() > 0 ? mulDiv256(basketsNeeded, amtRToken, totalSupply()) : amtRToken
);
```

The `amount`, `baskets` and `quantities` values are also messed up, but it would not matter anyways...

Under 'Issuance and Slow Minting' tests in `RToken.test.ts`:
```sol
it('Audit: DoS by downcasting', async function () {
      const issueAmount: BigNumber = BigNumber.from(2n ** 192n)

      // Set basket
      await basketHandler.connect(owner).setPrimeBasket([token0.a
steal RSR stakers funds by reentrancy attack in issue() function's token transfers
Audit: 2023-01-reserve-findings
Severity: MEDIUM

# Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L186-L280
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/BackingManager.sol#L105-L150
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/BasketHandler.sol#L271-L275


# Vulnerability details

## Impact
Function `issue()` loops through basket tokens and transfers them to BackingManager and mint RToken for user if issuance fit in one block, and transfer required tokens to protocol from user. because protocol is permission less and supports all ERC20 tokens it's possible for tokens to be ERC777 compliant or any other protocol tokens which have hook function during transfer, so transfer of tokens can cause external call to holder address. during the basket erc20 token list transfer's external call which triggers hook call, attacker can call `backingManager.manageTokens()` and because `basketsNeeded` updated but tokens balance in BackingManager is not yet updated code would detect that RTokens are not fully collateralized wrongly and would seize RSR stakers fund to become fully collateralized. This would cause RSR stakers to lose funds in favor of the RToken holders. attacker can drain RSR stakers funds totally and then withdraw his inflated RTokens.
This is reading reentrancy attack across contracts.

## Proof of Concept
This is part of `issue()` code where creates RTokens in the same block:
```
.......
.......
        // ==== If the issuance can fit in this block, and nothing is blocking it, then
        // just do a "quick issuance" of iss instead of putting the issuance in the queue:
        // effects and actions if we go this way are the combined actions to create and vest iss:
        //   basketsNeeded += iss.amtBaskets
        //   mint(recipient, iss.amtRToken)
        //   for each token index i, erc20s[i].transferFrom(issuer, backingManager, iss.deposits[i])
        if (
            // D18{blocks} <= D18{1} * {blocks}
            vestingEnd <= FIX_ONE_256 * block.number &&
            queue.left == queue.right &&
            status == CollateralStatus.SOUND
        ) {
            // Fixlib optimization:
            // D18{BU} = D18{BU} + D18{BU}; uint192(+) is the same as Fix.plus
            uint192 newBasketsNeeded = basketsNeeded + amtBaskets;
            emit BasketsNeededChanged(basketsNeeded, newBasketsNeeded);
            basketsNeeded = newBasketsNeeded;

            // Note: We don't need to update the prev queue entry because queue.left = queue.right
            emit Issuance(recipient, amtRToken, amtBaskets);

            // == Interactions then return: transfer tokens ==
            // Complete issuance
            _mint(recipient, amtRToken);

            for (uint256 i = 0; i < erc20s.length; ++i) {
                IERC20Upgradeable(erc20s[i]).safeTransferFrom(
                    issuer
User wont be able to unstake post RSR seize
Audit: 2023-01-reserve-findings
Severity: MEDIUM

# Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSR.sol#L387


# Vulnerability details

## Impact
One RSR is seized the stakeRSR reduces but this has no impact on User minted holding. Post seizing whoever unstake first will be able to secure there full funds and rest would not be able to unstake

## Proof of Concept
1. User A, B, C has made staking and their stakeRSR is 20,30,40. Lets say totalStakes is 1:1 with stakeRSR for simplicity

```
stakeRSR = 20+30+40 = 90
totalStakes = 90
```

2. Due to unfortunate conditions, seize RSR need to be called taking away 40 RSR from stakeRSR. This does not change totalStakes

```
function seizeRSR(uint256 rsrAmount) external notPausedOrFrozen {
...
stakeRSR -= stakeRSRToTake;
// stakeRSR = 90-40 = 50
totalStakes = 90
```

3. User C tries to recover his stake by unstaking his 40 staked RSR. This gets success making

```
stakeRSR = 50 - 40 =10
totalStakes = 90 - 40 =50
```

4. This means User A cannot unstake as shown below. Same goes for User B

```
function unstake(uint256 stakeAmount) external notPausedOrFrozen {

_burn(account, stakeAmount);
// making totalStakes = 50 - 20 = 30

uint256 rsrAmount = stakeRSR - newStakeRSR;
// uint256 rsrAmount = 10 - 30; // underflow
}
```

## Recommended Mitigation Steps
Although this is temporary since once a new user adds new stake, old user can use the extended value of stakeRSR to unstake their funds. But at same time now this new user has to wait for someone else to come and put staking fund

It is necessary to reduce the amount of minted token for staker by same proportion as being seized during seizeRSR. So if 50% is seized then 50% of minted value of user should be taken away
Vest doesn't update to current price, penalizing early depositors of Lending Protocol Tokens
Audit: 2023-01-reserve-findings
Severity: MEDIUM

# Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L378


# Vulnerability details

Per the shown [CToken](https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/assets/CTokenFiatCollateral.sol#L44) and [AToken](https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/assets/ATokenFiatCollateral.sol#L45)'s `refPerTok` can change

e.g. for CToken
```solidity
    function refPerTok() public view override returns (uint192) {
        uint256 rate = ICToken(address(erc20)).exchangeRateStored();
        int8 shiftLeft = 8 - int8(referenceERC20Decimals) - 18;
        return shiftl_toFix(rate, shiftLeft);
    }
```
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/assets/CTokenFiatCollateral.sol#L44

and 
```solidity
    function refPerTok() public view override returns (uint192) {
        uint256 rateInRAYs = IStaticAToken(address(erc20)).rate(); // {ray ref/tok}
        return shiftl_toFix(rateInRAYs, -27);
    }
```

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/assets/ATokenFiatCollateral.sol#L45


For these tokens, [`vest`](https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L378) will not update it's quote based on the current exchangeRate.

Since the system is coded to have a never decreasing refPerTok, this will cause a loss, of the delta of the `refPerTok` change to the early depositors.

### Proof of Concept

- Depositor A `issue`s at X rate (due to refPerTok which is computed at `quantity` in `quote`)
- Price is locked in and we vest over T time

- refPerTok changes by Y% (system is adaptive to it)
- New quote would cost: (100% - Y%) * X
- Later depositors all use this cheaper quote

- Initial Depositor took a worse deal and they are still vesting, they will receive R rTokens
- Later depositors are getting the surplus from them, R + Y rTokens


### Code Explanation
The reason for this discrepancy stands in the fact that:

[`issue`](https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L237) will use `quote`
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L237-L240

```solidity
    (address[] memory erc20s, uint256[] memory deposits) = basketHandler.quote(
        amtBaskets,
        CEIL
    );
```

[`quote`](https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/BasketHandler.sol#L407-L425) which will use [`quantity`]

```solidity
    quantities[i] = quantity(basket.erc20s[i]).mul(amount, rounding).shiftl_toUint(
        int8(IERC20Metadata(address(basket.erc20s[i])).decimals()),
        rounding
    );
`
`beginEra` may break transfers if triggered by `stakeRate > MAX_STAKE_RATE` 
Audit: 2023-01-reserve-findings
Severity: MEDIUM

# Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSR.sol#L574


# Vulnerability details

## Impact
If an era is zeroed then transfers will fail as the checkpoints will also be zeroed.
That's because of the assumption that balances will be reset when `beginEra` is called.

Per the logic in [`seizeRSR`](https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSR.sol#L374), [`beginEra`](https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSR.sol#L574) can be called under two conditions:
[`(stakeRSR == 0 || stakeRate > MAX_STAKE_RATE)`](https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSR.sol#L395)

The same logic applies to [`if (draftRSR == 0 || draftRate > MAX_DRAFT_RATE) {`](https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSR.sol#L4119)

`stakeRSR` being 0 is logical and means that totalSupply is zero, hence this scenario will not break invariants.

However `stakeRate > MAX_STAKE_RATE` may be called when totalSupply is non-zero, causing holders to be unable to transfer their tokens.

## POC
When `beginEra` is called because `stakeRate > MAX_STAKE_RATE`, and balances are non-zero, then some people will lose their tokens and will be unable to transfer them.

Due to this, the following will revert:
```
    function _moveVotingPower(
        address src,
        address dst,
        uint256 amount
    ) private {
        if (src != dst && amount > 0) {
            if (src != address(0)) {
                (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(
                    _checkpoints[era][src],
                    _subtract,
                    amount
                );
```
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSRVotes.sol#L166

Because the balance of the caller is non-zero but their checkpoint is, this will prevent them from being able to transfer tokens


We can quickly find a possible setup that would cause this scenario via the following brute-force script
```python
from random import random
from math import exp

FIX_ONE_256 = 1e18
MAX_STAKE_RATE = 1e9 * FIX_ONE_256
DRAFT_RATE = FIX_ONE_256

def main():

  """   
    Condition
        FIX_ONE_256 = 1e18
        MAX = 1e9 * FIX_ONE_256
        draftRate = (FIX_ONE_256 * totalDrafts + (draftRSR - 1)) / draftRSR;

      
    Intuition for Brute Force
        Make divisor small, so numerator stays big

  """  
  ## Count runs for convenience / Debugging
  run = 0

  ## Random number
  total_drafts = random() * DRAFT_RATE

  ## Start from same (typical scenario)
  draft_rsr = total_drafts
  while True:

    draftRate = (total_drafts * FIX_ONE_256 + (draft_rsr - 1)) / draft_rsr

    print("draftRate", draftRate)
    run += 1

    ## Dec