xanoutas/solidity-security-findings huggingface dataset:
user funds loss in withdraw() of StRSR because code don't revert when calculated rsrAmount is zero
Audit: 2023-01-reserve-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Function `withdraw()` in StRSR completes an account's unstaking. but when calculated amount of RSR token is 0 code still burn user draftRSR and returns. This would cause users small amount of deposits to get burned and user won't receive any funds. as `withdraw()` is callable by anyone a malicious attacker can call `withdraw()` for each item separately which result in 0 RSR amount and user would lose his funds. As users separate withdrawals can  sum up and calculated rsrAmount would be bigger than 0 for user so contract shouldn't burn the small amounts of user balance for nothing.

## Proof of Concept
This is `withdraw()` code:
```
    function withdraw(address account, uint256 endId) external notPausedOrFrozen {
        // == Refresh ==
        assetRegistry.refresh();

        // == Checks + Effects ==
        require(basketHandler.fullyCollateralized(), "RToken uncollateralized");
        require(basketHandler.status() == CollateralStatus.SOUND, "basket defaulted");

        uint256 firstId = firstRemainingDraft[draftEra][account];
        CumulativeDraft[] storage queue = draftQueues[draftEra][account];
        if (endId == 0 || firstId >= endId) return;

        require(endId <= queue.length, "index out-of-bounds");
        require(queue[endId - 1].availableAt <= block.timestamp, "withdrawal unavailable");

        uint192 oldDrafts = firstId > 0 ? queue[firstId - 1].drafts : 0;
        uint192 draftAmount = queue[endId - 1].drafts - oldDrafts;

        // advance queue past withdrawal
        firstRemainingDraft[draftEra][account] = endId;

        // ==== Compute RSR amount
        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;

        emit UnstakingCompleted(firstId, endId, draftEra, account, rsrAmount);

        // == Interaction ==
        IERC20Upgradeable(address(rsr)).safeTransfer(account, rsrAmount);
    }
```
As you can see function is callable by anyone and code would return when calculated `rsrAmount` is 0 and it would burn user draft items. This would cause users to lose funds if they withdraw small amounts even if those small amounts sums would create a bigger RSR Amount. code would burn those drafts separately and user would receive nothing. imagine this scenario:
1. user1 has has 20 draft token in 4 withdraw item and the ratio between draft to RSR is 5 to 1. (5 draft Token worth 1 RSR token).
2. attacker would call `withdraw(user1, 1)` and it code would calculated rsrAmount as 0 and
Governance manipulation through signatures
Audit: 2023-01-reserve-findings
Severity: HIGH

# Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSRVotes.sol#L118
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSRVotes.sol#L118-L135


# Vulnerability details

## Impact
The impact is critical because it enables anyone to submit as many signatures as they want manipulating the governance. The probe relies that in the delegateBySig function, the nonce it is not checked upon msg.sender, it is checked upon the output for the recover with the ECDSA library. If users re-input the v,r,s, parameters from other users and change the delegate, the ECDSA will not give an error and the function will pass and as there is no check that signer == msg.sender, this random function will pass and you would have managed to vote extra. This can be repeated an "infinite" amount of times. Will show better in the PoC.


        function delegateBySig(
        address delegatee,
        uint256 nonce,
        uint256 expiry,
        uint8 v,
        bytes32 r,
        bytes32 s
       ) public {
        require(block.timestamp <= expiry, "ERC20Votes: signature expired");
        //@audit uncheked that signaer is not address 0
        address signer = ECDSAUpgradeable.recover(
            _hashTypedDataV4(keccak256(abi.encode(_DELEGATE_TYPEHASH, delegatee, nonce, expiry))),
            v,
            r,
            s
         );
        require(nonce == _useNonce(signer), "ERC20Votes: invalid nonce");
        _delegate(signer, delegated);
         }

## Proof of Concept
1 User votes calling delegateBySig  (this step is optional because anyone can call with their own v,r,s values)
2Attacker goes to etherscan and takes those values and adds them as a parameter in the same function but changes the delegatee.
3 Nonce will always be 0 because the hash will be different and once it goes through recover, it gives a different address.
4 Done all checks have been passed, this exploit could be repeated as many times as wanted to manipulate votes.

Proof by image:
Note that both delegates are different and it does not fail meanwhile v,r,s and nonce are correct.
https://imgur.com/a/MFd3TTZ

## Tools Used
Manual

## Recommended Mitigation Steps
There is a lot to improve in the function. But the main thing would be to check whether msg.sender==signer. This check it is currently not done, which can lead to passing arbitrary addresses as I demonstrated.



        require(block.timestamp <= expiry, "ERC20Votes: signature expired");
        //@audit uncheked that signaer is not address 0
        address signer = ECDSAUpgradeable.recover(
            _hashTypedDataV4(keccak256(abi.encode(_DELEGATE_TYPEHASH, delegatee, nonce, expiry))),
            v,
            r,
            s
         );
        require(signer == msg.sender);  //new require that would "solve the issue"
        require(nonce == _useNonce(signer), "ERC20Votes: invalid nonce");
        _d
Compilation Error: Undefined function 'shiftl_toFix' in CTokenFiatCollateral Contract
Audit: 2023-01-reserve-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

```
return shiftl_toFix(rate, shiftLeft);
```
It should be noted that the function "shiftl_toFix" is not defined in the contract and not imported from any library, this means that the compiler will throw an error when trying to deploy the contract and this function can't be called.

The `CTokenFiatCollateral` contract uses a function called "shiftl_toFix" in the "refPerTok()" function to calculate the quantity of whole reference units per whole collateral tokens. 

## Proof of Concept

Attempt to compile the `CTokenFiatCollateral` contract using a Solidity compiler, such as Truffle or Remix.

Observe that the compiler throws an error, indicating that the "shiftl_toFix" function is not defined in the contract and not imported from any library.

Attempt to deploy the contract on the Ethereum network but observe that it fails as the contract is not compiled successfully.

Attempt to call the `refPerTok()` function but observe that it throws an error as it calls the `shiftl_toFix` function which is not defined.

Observe that the contract cannot be used as intended, and users will be unable to interact with it.

```
pragma solidity 0.8.9;

import "./CTokenFiatCollateral.sol";

contract Exploit {
    CTokenFiatCollateral cTokenFiatCollateral;

    constructor() public {
        cTokenFiatCollateral = new CTokenFiatCollateral();
    }

    function refPerTok() public view returns (uint192) {
        return cTokenFiatCollateral.refPerTok();
    }
}
```

As you can see, the contract "Exploit" will fail when trying to call the `refPerTok()` function from `CTokenFiatCollateral` contract because it contains an undefined function `shiftl_toFix`.


## Tools Used

Remix, Manual audit, Vs Code

## Recommended Mitigation Steps

Define the `shiftl_toFix` function within the `CTokenFiatCollateral` contract and provide an implementation for it.

```
function shiftl_toFix(uint256 x, uint8 y) internal pure returns (uint192) {
    return uint192(x) << y;
}
```

Use this newly defined function in the `refPerTok()` function instead of the undefined one.

```
function refPerTok() public view override returns (uint192) {
    uint256 rate = ICToken(address(erc20)).exchangeRateStored();
    int8 shiftLeft = 8 - int8(referenceERC20Decimals) - 18;
    return shiftl_toFix(rate, shiftLeft);
}
```

Re-compile and re-deploy the contract on the Ethereum network after making these changes.
[High - 1] StaticATokenLM depositors are loosing tokens by depositing AToken directly
Audit: 2023-01-reserve-findings
Severity: HIGH

# Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/aave/StaticATokenLM.sol#L100


# Vulnerability details

## Impact

In the [`StaticATokenLM`](https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/plugins/aave/StaticATokenLM.sol#L100) contract, users are allowed to deposit either `AToken` (yield bearing tokens from AAVE), or their underlying tokens.
The amount is converted to shares systematically, even if the depositor deposits shares.
As shares are usually worth more than assets, a depositor who is depositing shares will receive less StaticATokenLM because they are treated as assets.

## Proof of Concept

In the following POC, it is shown that the amount of StaticATokens is not converted when sending ATokens directly, which is the case for the assets.
At the end of the test, the assertion `assertEq(sa.balanceOf(address(this)), 150 * 10**9);` fails. The actual value is `100 * 10**9`.

```solidity
contract StaticTest is Test {
    function testDeposit() public {
        FakeAsset fa = new FakeAsset();
        FakeAToken ft = new FakeAToken();
        ft.setAsset(address(fa));

        AaveLendingPoolMock lp = new AaveLendingPoolMock(address(0));
        // 200%
        lp.setNormalizedIncome(address(fa), 2e18);
        StaticATokenLM sa = new StaticATokenLM(ILendingPool(address(lp)), address(ft), "", "");
        ft.mint(address(this), 100);
        fa.mint(address(this), 100);

        ft.approve(address(sa), type(uint256).max);
        fa.approve(address(sa), type(uint256).max);

        // We deposit in underlying directly, should take 100 and give 50
        // because the rate is 200% right now
        sa.deposit(address(this), 100, 0, true);
        assertEq(sa.balanceOf(address(this)), 50 * 10**9);

        // Depositing in ATokens, should be 1:1
        sa.deposit(address(this), 100, 0, false);
        assertEq(sa.balanceOf(address(this)), 150 * 10**9);
    }
}
```

Note: the full POC is available on this [gist](https://gist.github.com/iFrostizz/00efcc35c5bbecf867965b76f857ef96). It was trimmed for readability.

## Tools Used

Manual inspection

## Recommended Mitigation Steps

Handle the deposit from `AToken` without converting them

```solidity
function _deposit(
    address depositor,
    address recipient,
    uint256 amount,
    uint16 referralCode,
    bool fromUnderlying
) internal returns (uint256) {
    require(recipient != address(0), StaticATokenErrors.INVALID_RECIPIENT);
    _updateRewards();

	uint256 amountToMint;
	
    if (fromUnderlying) {
        ASSET.safeTransferFrom(depositor, address(this), amount);
        LENDING_POOL.deposit(address(ASSET), amount, address(this), referralCode);
	amountToMint = _dynamicToStaticAmount(amount, rate());
    } else {
        ATOKEN.safeTransferFrom(depositor, address(this), amount);
	amountToMint = amount;
    }

    _mint(recipient, amountToMint);

    return amoun
Withdraw someone elses funds
Audit: 2023-01-reserve-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
In the withdraw function:

    function withdraw(address account, uint256 endId) external notPausedOrFrozen {

anyone can input another user address and the funds will be withdrawn to the user. While funds are not stolen, this breaks the entire project because users are not going to be able to use it if someone tracks addresses and withdraws directly for them. Users will not know thinking the funds will still be in the protocol, but far from that, funds were returned to them.


## Proof of Concept

1 Attacker checks etherscan/mempool for any transaction that enables withdrawing for any user in the protocol
2 Attacker calls the withdraw function with the user address
3 Funds are sent back to the first user, disabling him and others from using the protocol correctly

## Tools Used
manual

## Recommended Mitigation Steps
add a require that the msg.sender == to the account address

       function withdraw(address account, uint256 endId) external notPausedOrFrozen {
       require(msg.sedner == account,"wrong user"); // here
melt()  calculates lastPayoutBal wrongly
Audit: 2023-01-reserve-findings
Severity: HIGH

# Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Furnace.sol#L82


# Vulnerability details

## Impact
Detailed description of the impact of this finding.

``melt()``  calculates ``lastPayoutBal`` wrongly since it calls  ``rToken.melt(amount)`` later.

## Proof of Concept
``melt()``  calculates ``lastPayoutBal`` wrongly since it calls  ``rToken.melt(amount)`` later. Therefore, ``lastPayoutBal`` should just record the current balance of rToken of the furnace contract, and be consistent  with the documentation "lastPayoutBal' = rToken.balanceOf'(this) (balance now == at end of pay period)"

## Tools Used
Remix

## Recommended Mitigation Steps
Set ``lastPayoutBal``  the current balance of rToken of the furnace contract. 

```
lastPayoutBal = rToken.balanceOf(address(this));
```
Rounding error in distribute() function 
Audit: 2023-01-reserve-findings
Severity: HIGH

# Lines of code

https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Distributor.sol#L61-L65


# Vulnerability details

## Impact
Detailed description of the impact of this finding.
The ``distribute()`` function might suffer from serious rounding error since it calculates ``tokensPerShare`` that has a division rounding error, and then use ``uint256 transferAmt = tokensPerShare * numberOfShares;`` to calculate the ``transferAmt`` for each destination. In the worst case, ``tokensPerShare = 0`` and as a result, there is no distribution at all!

``
## Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
The main problem of ``distribute()`` is that it calculates ``tokensPerShare`` first, which introduces a rounding error, and then use it to calculate ``transferAmt`` based on the following line:
```
uint256 transferAmt = tokensPerShare * numberOfShares;

```
This is a standard division-followed-by-multiplication rounding error. It should be corrected by multiplication-followed-by-division. 

To see this: suppose we need to distribute 9999 RSR tokens to 10,000 shares to Alice and Bob, each of them has 5000 shares:
1) tokensPerShare = 9999/10000 = 0; 
2)  transferAmt = tokensPerShare * numberOfShares = 0 * 5000 = 0
3) As a result, both Alice and Bob will receive zero RST tokens, although we expect they will each receive 4999.5. 
RSR tokens. 

We need to use the multiplication-followed-by-division to correct this: 
```
uint256 transferAmt =  amount*numberOfShares/totalShares;

```


## Tools Used
Remix

## Recommended Mitigation Steps
Reimplement it as follows using the multiplication-followed-by-division to calculate distribution amount for each destination. 

```
function distribute(
        IERC20 erc20,
        address from,
        uint256 amount
    ) external notPausedOrFrozen {
        require(erc20 == rsr || erc20 == rToken, "RSR or RToken");
        bool isRSR = erc20 == rsr; // if false: isRToken
       
        {
            RevenueTotals memory revTotals = totals();
            uint256 totalShares = isRSR ? revTotals.rsrTotal : revTotals.rTokenTotal;
            require(totalShares > 0, "nothing to distribute");
        }

        // Evenly distribute revenue tokens per distribution share.
        // This rounds "early", and that's deliberate!

        Transfer[] memory transfers = new Transfer[](destinations.length());
        uint256 numTransfers;

        for (uint256 i = 0; i < destinations.length(); ++i) {
            address addrTo = destinations.at(i);

            uint256 numberOfShares = isRSR
                ? distribution[addrTo].rsrDist
                : distribution[addrTo].rTokenDist;
            if (numberOfShares == 0) continue;
            uint256 transferAmt = amount*numberOfShares/totalShares;

            if (addrTo == FURNACE) {
                addrTo = furnace;
RecollateralizationLib: Flaw in basket range calculation leads to early basket compromise and loss to RToken holders
Audit: 2023-01-reserve-findings
Severity: HIGH

# Lines of code

https://github.com/reserve-protocol/protocol/blob/b30ab2068dddf111744b8feed0dd94925e10d947/contracts/p1/mixins/RecollateralizationLib.sol#L201


# Vulnerability details

## Impact
This report deals with how the recollateralization algorithm underestimates the number of baskets it can hold (by calculating unnecessary costs for collateral that does not need to be traded).  

When this number of baskets it thinks it can hold is reached - which is less than what it can actually hold - it compromises its baskets (i.e. reduces the number of baskets needed for full collateralization).  

This causes RToken holders to lose value as the excess assets are not used to only appreciate RToken value but is also processed in part as `rsr` revenue.  

## Proof of Concept
In order to understand this issue it is necessary to understand the recollateralization algorithm.  

Here is how recollateralization works and how the algorithm under-estimates the baskets it can acquire:  

1. `RecollateralizationLib.prepareRecollateralizationTrade` is called to determine the next trade ([https://github.com/reserve-protocol/protocol/blob/b30ab2068dddf111744b8feed0dd94925e10d947/contracts/p1/mixins/RecollateralizationLib.sol#L62-L113](https://github.com/reserve-protocol/protocol/blob/b30ab2068dddf111744b8feed0dd94925e10d947/contracts/p1/mixins/RecollateralizationLib.sol#L62-L113))

2. In this function, the basket range is calculated with the `basketRange` function ([https://github.com/reserve-protocol/protocol/blob/b30ab2068dddf111744b8feed0dd94925e10d947/contracts/p1/mixins/RecollateralizationLib.sol#L88](https://github.com/reserve-protocol/protocol/blob/b30ab2068dddf111744b8feed0dd94925e10d947/contracts/p1/mixins/RecollateralizationLib.sol#L88))

3. The recollateralization algorithm tries to recollateralize the RToken up to the bottom of the basket range ([https://github.com/reserve-protocol/protocol/blob/b30ab2068dddf111744b8feed0dd94925e10d947/contracts/p1/mixins/RecollateralizationLib.sol#L380](https://github.com/reserve-protocol/protocol/blob/b30ab2068dddf111744b8feed0dd94925e10d947/contracts/p1/mixins/RecollateralizationLib.sol#L380)). Therefore it is important that this bottom of the range is correct. As we will see it is underestimated.  

4. The bottom of the range is calculated as `assetsLow.div(basketPriceHigh)` ([https://github.com/reserve-protocol/protocol/blob/b30ab2068dddf111744b8feed0dd94925e10d947/contracts/p1/mixins/RecollateralizationLib.sol#L201](https://github.com/reserve-protocol/protocol/blob/b30ab2068dddf111744b8feed0dd94925e10d947/contracts/p1/mixins/RecollateralizationLib.sol#L201))

5. `assetsLow` is calculated by the `totalAssetValue` function which calculates the sum of all collateral valued with their `low` prices ([https://github.com/reserve-protocol/protocol/blob/b30ab2068dddf111744b8feed0dd94925e10d947/contracts/p1/mixins/RecollateralizationLib.sol#L264](https://github.com/reserve-protocol/protocol/blob/b30ab2068dddf111744b8fee
RSR will stuck + Withdrawal wont work
Audit: 2023-01-reserve-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Withdrawals will not work once `seizeRSR` is called. Also this will cause some RSR to become unusable as shown in POC

## Proof of Concept
1. Lets say 3 unstakes were made for stakeRSR `20,30,40` using `unstake` function by User A, B, C
2. This calls the `pushDraft` function which updates the draftRSR and also put equivalent draft amount in User queues

```
function pushDraft(address account, uint256 rsrAmount)
        internal
        returns (uint256 index, uint64 availableAt)
    {
        draftRSR += rsrAmount;
        
...
CumulativeDraft[] storage queue = draftQueues[draftEra][account];
...
        queue.push(CumulativeDraft(uint176(oldDrafts + draftAmount), availableAt));
    }
```

3. So we will have 3 entries which can be withdrawn after `availableAt` timestamp

```
draftRSR = 20+30+40 = 90

uint256 newTotalDrafts = (draftRate * draftRSR) / FIX_ONE;
        uint256 draftAmount = newTotalDrafts - totalDrafts;

// For simplicity lets say draftRate was 2 then

draftQueues[draftEra][A] = queue(40, availableAt);
draftQueues[draftEra][B] = queue(60, availableAt);
draftQueues[draftEra][C] = queue(80, availableAt);

totalDrafts = 40+60+80 = 180
```

4. Due to unfortunate condition, RSR needs to be seized. 
5. `seizeRSR` function is called which resulted in draftRSR to be reduced by amount 41. Remember seizing does not changes totalDrafts

```
draftRSR = 90 - 41 = 49
totalDrafts= 180
```

6. Now post availableAt timestamp User C tries to withdraw the funds. It tries to withdraw the 80 draft amount which gets success

```
function withdraw(address account, uint256 endId) external notPausedOrFrozen {

...
totalDrafts = newTotalDrafts;
        draftRSR = newDraftRSR;

        emit UnstakingCompleted(firstId, endId, draftEra, account, rsrAmount);

        // == Interaction ==
        IERC20Upgradeable(address(rsr)).safeTransfer(account, rsrAmount);

}
```

7. Lets say withdrawn 80 draft amount corresponded to 39 RSR then new values would be :

```
draftRSR = 49 - 39 = 10
totalDrafts= 180 - 80 = 100
```

8. Now User B tries to withdraw the 60 draft amount which fails since its equivalent 30 draftRSR does not exist in system

```
 uint256 rsrAmount = draftRSR - newDraftRSR;
//  uint256 rsrAmount = 10-30 // underflow
```

9. Same goes with User A since system now does not have 20 draftRSR

10. Thus the 10 draftRSR is now stuck in system and both User A & B wont be able to claim their withdraw

## Recommended Mitigation Steps
The drafts at draftQueues[draftEra] should also change by same proportion once RSR is seized and taken from draftRSR. This will ensure all withdrawals went smoothly