xanoutas/solidity-security-findings huggingface dataset:
Potential loss of precision in reward rate calculation
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/periphery/BathBuddy.sol#L191-L228


# Vulnerability details

## Impact

In the BathBuddy contract rewards are implemented by using a `rewardRate` that represent **the reward rate per second**. When the owner calls the `notifyRewardAmount` function to initiate the reward process, this variable is calculated as the total amount of reward tokens divided by the configured duration of the rewards (lines 196-198 and 206-208):

https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/periphery/BathBuddy.sol#L191-L228

```solidity
191:     function notifyRewardAmount(
192:         uint256 reward,
193:         IERC20 rewardsToken
194:     ) external onlyOwner updateReward(address(0), address(rewardsToken)) {
195:         if (block.timestamp >= periodFinish[address(rewardsToken)]) {
196:             rewardRates[address(rewardsToken)] = reward.div(
197:                 rewardsDuration[address(rewardsToken)]
198:             );
199:         } else {
200:             uint256 remaining = periodFinish[address(rewardsToken)].sub(
201:                 block.timestamp
202:             );
203:             uint256 leftover = remaining.mul(
204:                 rewardRates[address(rewardsToken)]
205:             );
206:             rewardRates[address(rewardsToken)] = reward.add(leftover).div(
207:                 rewardsDuration[address(rewardsToken)]
208:             );
209:         }
210: 
211:         // Ensure the provided reward amount is not more than the balance in the contract.
212:         // This keeps the reward rate in the right range, preventing overflows due to
213:         // very high values of rewardRate in the earned and rewardsPerToken functions;
214:         // Reward + leftover must be less than 2^256 / 10^18 to avoid overflow.
215:         // Note********** ERC20s must be here*************
216:         uint256 balance = rewardsToken.balanceOf(address(this));
217:         require(
218:             rewardRates[address(rewardsToken)] <=
219:                 balance.div(rewardsDuration[address(rewardsToken)]),
220:             "Provided reward too high"
221:         );
222: 
223:         lastUpdateTime[address(rewardsToken)] = block.timestamp;
224:         periodFinish[address(rewardsToken)] = block.timestamp.add(
225:             rewardsDuration[address(rewardsToken)]
226:         );
227:         emit RewardAdded(reward);
228:     }
```

There is a potential loss of precision in this calculation that may eventually cause the `rewardRate` variable to be rounded down, causing the effect of distributing less rewards than available. This could occur if the reward token has a low number of decimals (for example [GUSD](https://etherscan.io/token/0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd) has only 2 decimals), meaning that small values for the `reward` variable are representative quantities of the token. Given this scenario, it is possible that the `reward` varia
BathBuddy contract should implement methods to pause and unpause contract
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/periphery/BathBuddy.sol#L38


# Vulnerability details

## Impact

The BathBuddy contract inherits from OpenZeppelin `Pausable` contract with the intention of adding pausing features to the contract. 

https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/periphery/BathBuddy.sol#L38

```solidity
38: contract BathBuddy is ReentrancyGuard, IBathBuddy, Pausable {
```

The [`Pausable` implementation](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/Pausable.sol) contains all the logic to implement pausing, but doesn't include any external or public functionality to actually trigger the pause or resume, this task is left to the derived contract.

The BathBuddy contract fails to implement these functions, as there is no callable function from the outside that modifies the pause state. The pausing mechanism is intended to be used in the `getReward` function, as this function includes the `whenNotPaused`:

https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/periphery/BathBuddy.sol#L168-L185

```solidity
168:     function getReward(
169:         IERC20 rewardsToken,
170:         address holderRecipient
171:     )
172:         external
173:         override
174:         nonReentrant
175:         whenNotPaused
176:         updateReward(holderRecipient, address(rewardsToken))
177:         onlyBathHouse
178:     {
179:         uint256 reward = tokenRewards[address(rewardsToken)][holderRecipient];
180:         if (reward > 0) {
181:             tokenRewards[address(rewardsToken)][holderRecipient] = 0;
182:             rewardsToken.safeTransfer(holderRecipient, reward);
183:             emit RewardPaid(holderRecipient, reward);
184:         }
185:     }
```

This means that protocol admin won't be able to pause this function if needed as there is no accessible function to enable the mechanism at all.

## Proof of Concept

The BathBuddy contract code doesn't include any external or public function to pause or resume the contract. The OpenZeppelin implementations only contains internal functions to provide support so that the derived contract implements the public interface:

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/Pausable.sol#L82-L104

```solidity
/**
  * @dev Triggers stopped state.
  *
  * Requirements:
  *
  * - The contract must not be paused.
  */
function _pause() internal virtual whenNotPaused {
    _paused = true;
    emit Paused(_msgSender());
}

/**
  * @dev Returns to normal state.
  *
  * Requirements:
  *
  * - The contract must be paused.
  */
function _unpause() internal virtual whenPaused {
    _paused = false;
    emit Unpaused(_msgSender());
}
```

## Recommendation

The BathBuddy contract should implement the functions to expose the pausing mechanism. These functions should only be accessible to the owner of the contract.

```solidity
function pause() ext
USERS COULD RECEIVE LESSER REWARD AFTER PERIODFINISH HAS ELAPSED
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/periphery/BathBuddy.sol#L112-L119


# Vulnerability details

## Impact
Users claiming their rewards one last time when withdrawing their staked cToken from the Compound pool are going to reap a lesser amount if `periodFinish[token]` has elapsed.

## Proof of Concept
`periodFinish[token]`, which is meant to be assigned to `lastTimeRewardApplicable()` and `lastUpdateTime[token]`, will be selected whenever `block.timestamp` is greater than `periodFinish[token]`:

[BathBuddy.sol#L112-L119](https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/periphery/BathBuddy.sol#L112-L119)

```
    function lastTimeRewardApplicable(
        address token
    ) public view returns (uint256) {
        return
            block.timestamp < periodFinish[token]
                ? block.timestamp
                : periodFinish[token];
    }
```
When this happens, `rewardPerToken()` returns a smaller integer because `lastTimeRewardApplicable(token)` - `lastUpdateTime[token]` is equal to zero:

[BathBuddy.sol#L129-L130](https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/periphery/BathBuddy.sol#L129-L130)

```
                lastTimeRewardApplicable(token)
                    .sub(lastUpdateTime[token])
``` 

This will make `earned()` return a smaller integer too:

[BathBuddy.sol#L149-L151](https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/periphery/BathBuddy.sol#L149-L151)

```
                    rewardPerToken(token).sub(
                        userRewardsPerTokenPaid[token][account]
                    )
```

As a result, the user will end up assigned a smaller reward to be claimed via [`getReward()`](https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/periphery/BathBuddy.sol#L179-L182): 

[BathBuddy.sol#L251](https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/periphery/BathBuddy.sol#L251)

```
            tokenRewards[token][account] = earned(account, token);
```

## Recommended Mitigation Steps
It is recommended removing `periodFinish[]` from the reward logic so that the latest `block.timestamp` is always used in the arithmetic operation of `lastTimeRewardApplicable(token) - lastUpdateTime[token]` pertaining to `rewardPerToken()` as well as when assigning earlier to `lastUpdateTime[token]` in [`updateReward()`](https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/periphery/BathBuddy.sol#L249).
Take may fail if someone performs a partial buy
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/RubiconMarket.sol#L328
https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/RubiconMarket.sol#L1194


# Vulnerability details

## Impact
Taking an offer may fail if someone else performs a partial buy before.

## Proof of Concept
The function `SimpleMarket.buy` will return false if `quantity > _offer.pay_amt`:

```js
function buy(
    uint256 id,
    uint256 quantity
) public virtual can_buy(id) synchronized returns (bool) {
    OfferInfo memory _offer = offers[id];
    uint256 spend = mul(quantity, _offer.buy_amt) / _offer.pay_amt;

    require(uint128(spend) == spend, "spend is not an int");
    require(uint128(quantity) == quantity, "quantity is not an int");

    ///@dev For backwards semantic compatibility.
    if (
        quantity == 0 ||
        spend == 0 ||
        quantity > _offer.pay_amt ||
        spend > _offer.buy_amt
    ) {
        return false;
    }
```

This will cause a revert in the calling function `_buys`:

```js
require(super.buy(id, amount));
```

If a user calls `take` with input `maxTakeAmount` (which will end up calling `_buys`) and someone else performs a partial buy before that for the same offer, than the call will revert if `maxTakeAmount > remaining _offer.pay_amt`. The preferrable effect would be that the user receives what is left, as the user has already signified to buy at the offers price and which is also suggested by the parameter name `maxTakeAmount`

## Tools Used
Manual review

## Recommended Mitigation Steps
Adjust the `quantity` to be paid if it is higher than the current `_offer.pay_amt` instead of reverting.
Modifier can_offer is incorrect 
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L596-L600


# Vulnerability details

## Impact
Modifier can_offer is incorrect because it checks if `!isClosed()` is true [598](https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L598). The method `isClosed()` always return false and the condition is passed every time. Reading the comment above the modifier [596](https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L596) I think that the developers wants to stop market when its need and no new offers to be allowed for creating. 

## Proof of Concept
I do not have written PoC but i think the problem is visible. 
[596-600](https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L596-L600)

## Tools Used
Mannual review

## Recommended Mitigation Steps
Instead of calling of `isClosed()` method use `stop()` method which fit more with this condition. 

```
/// @dev After close_time has been reached, no new offers are allowed.
    modifier can_offer() override {
        require(!stop());
        _;
    }
```

Also refactor the stop() method

```
function stop(bool isStopped) external auth {
        stopped = isStopped;
    }
```
`block.number` will not work as expected on Arbitrum blockchain 
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/utilities/poolsUtility/Position.sol#L322-L333
https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/RubiconMarket.sol#L940-L944


# Vulnerability details

## Impact
the `block.number` will not work as it is on optimism when the protocol deployed on Arbitrum blockchain. 

## Proof of Concept
this structure may work as expected on optimism, it is problematic for use on Arbitrum. According to Arbitrum Docs `block.number` returns the most recently synced L1 block number. Once per minute the block number in the Sequencer is synced to the actual L1 block number.
Arbitrum doc: 
https://developer.arbitrum.io/time

## Tools Used
manual review / Arbiturm docs

## Recommended Mitigation Steps
I recommend use of `block.timestamp` rather than block.number or use Arbitrum Multicall contract that has a function to return proper `block.number` to get the correct block.number, as dddDD said:
`Yep, we’re using block.number there rn rather for testing, assuming that we’re getting correct block.number and to not to focus attention on it.
So on OP mainnet, they have a contract to fetch proper L1 block without delays, will use that.
On Arbitrum, the Multicall contract has a func to return proper block.number`
Offers not fully bought up due to not considering fee deduction
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/RubiconMarket.sol#L1053
https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/RubiconMarket.sol#L1093
https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/RubiconMarket.sol#L1313


# Vulnerability details

## Impact

During some function calls offers are meant to be fully bought up if caller provides enough tokens. Due to subtraction of fees this never happens and the offers are reiterated until they are considered dust and discarded.

## Proof of Concept

In `_matcho`, `sellAllAmount` and `buyAllAmount` offers are meant to be fully bought up if the user provides more tokens than the current best offer can give:

```js
//WARDEN-NOTE: in sellAllAmount and buyAllAmount, this will call `buy`
take(bytes32(offerId), uint128(offers[offerId].pay_amt)); //We take the whole offer

//WARDEN-NOTE: in _matcho, if t_buy_amt large enough, offer is meant to be bought fully
buy(best_maker_id, min(m_pay_amt, t_buy_amt));
```
However, in the `RubiconMarket.buy` function the amount of tokens is decreased before passing it to `_buys` which will do the actual matching and accounting:

```js
//in `buy` function:
amount = calcAmountAfterFee(amount);

...

function calcAmountAfterFee(
    uint256 amount
) public view returns (uint256 _amount) {
    require(amount > 0);
    _amount = amount;
    _amount -= mul(amount, feeBPS) / 100_000; 

    if (makerFee() > 0) {
        _amount -= mul(amount, makerFee()) / 100_000;
    }
}
```

This will lead to offers never being fully taken in `_matcho`, `sellAllAmount` and `buyAllAmount`. They will be reiterated over until the remaining amount is considered dust and then discarded, before the next offer can be processed. Besides the dust issue, this is also gas inefficient and will cause `fill_amt` to be accounted incorrectly in `sellAllAmount` and `buyAllAmount`

## Tools Used
Manual Review

## Recommended Mitigation Steps
Increase the input amount value when `take` or `buy` is called for whole offers so that full `offer.pay_amt` is reached after subtracting fees.
No deadline parameter in `sellAllAmount()` and `buyAllAmount()` functions:
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/RubiconMarket.sol#L1028-L1112


# Vulnerability details

## Impact
There is no deadline parameter in these two functions,This missing feature enables pending transactions to be maliciously executed at a later point.

## Proof of Concept
Consider following scnerio:

1. Alice wants to create order of 1000DAI for 1 ETH . She signs the transaction with  minOutputAmount = 0.99 ETH to allow for some slippage.
2.The transaction is submitted to the mempool, however, Alice chose a transaction fee that is too low for miners to be interested in including her transaction in a block. The transaction stays pending in the mempool for extended periods, which could be hours, days, weeks, or even longer.
3. When the average gas fee dropped far enough for Alice's transaction to become interesting again for miners to include it. In the meantime, the price of ETH could have drastically changed. She will still at least get 0.99 ETH due to minOutputAmount, but the DAI value of that output might be significantly lower. She has unknowingly performed a bad trade due to the pending transaction she forgot about.

An even worse way this issue can be maliciously exploited is through MEV:

1. The swap transaction is still pending in the mempool. Average fees are still too high for miners to be interested in it. The price of  has gone up significantly since the transaction was signed (lets say its not dai now and some other token), meaning Alice would receive a lot more ETH when the swap is executed. But that also means that her minOutputAmount value is outdated and would allow for significant slippage.
2. A MEV bot detects the pending transaction. Since the outdated minOutputAmount now allows for high slippage, the bot sandwiches Alice, resulting in significant profit for the bot and significant loss for Alice.


## Tools Used
Manual Review

## Recommended Mitigation Steps
Add deadline param
RubiconMarket.buy() Missing sanity check on feeTo before transfer
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L340


# Vulnerability details

## Impact


The function buy() calls the function transferFrom but there is no sanity check to the address destination.

If feeTo is set to 0x0, some ERC20 implementations (ex USDC) will revert on this transfer, making offers unbuyable.

Empty feeTo could also deny the service in buy method.




## Proof of Concept


https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L339-L342

```solidity

        require(
            _offer.buy_gem.transferFrom(msg.sender, feeTo, fee),
            "Insufficient funds to cover fee"
        );            

```
    
## Tools Used

Manual Review

## Recommended Mitigation Steps

Check if feeTo is set to a non-zero address before transferring fees.
`Position.closePosition` fails when maker fee is big enough.
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/utilities/poolsUtility/Position.sol#L459
https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/utilities/poolsUtility/Position.sol#L210
https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/utilities/poolsUtility/Position.sol#L290


# Vulnerability details

## Impact
`Position.closePosition` fails because `rubiconMarket.getFeeBPS()` does not take maker fee into account.

## Proof of Concept
Five tests fail when I set `makerFee` as below in [`leverage-wrapper.ts`](https://github.com/code-423n4/2023-04-rubicon/blob/main/test/hardhat-tests/leverage-wrapper.ts#L31
).
```ts
  async function deployPoolsUtilityFixture() {
    const [owner, otherAccount] = await ethers.getSigners();

    const RubiconMarketFactory = await ethers.getContractFactory(
      "RubiconMarket"
    );
    const rubiconMarket = await RubiconMarketFactory.deploy();
    await rubiconMarket.initialize(owner.address);

    // ***** Changed part ***** //
    // `makerFee` has decimals 5. Say it's 0.02%.
    await ethers.provider.send(
      "hardhat_setStorageAt",
      [
        rubiconMarket.address,
        ethers.utils.solidityKeccak256(["string"], ["WOB_MAKER_FEE"]),
        ethers.utils.hexlify(ethers.utils.zeroPad(ethers.BigNumber.from("20").toHexString(), 32))
      ]
    );
```

All failing tests revert with the reason **'_repay: balance of quote lt. debt'**
```sh
$ yarn hardhat test

Leverage positions Test
    Pools Utility Test
      Short positions 📉
        ✓ should open short position
        ✓ should revert short with invalid leverage
        ✓ should increase margin for opened short position

        1) should close profitable short position

        2) should close unprofitable short position
      Long positions 📈
        ✓ should open long position
        ✓ should revert long with 1x leverage
        ✓ should increase margin for opened long position

        3) should close opened profitable long position?

        4) should close unprofitable long position?
        ✓ should open long position with 1.25x leverage?

        5) should open two longs and properly close them?

```

## Tools Used
Manual.

## Recommended Mitigation Steps
Replace `_buyAmount` calculation in [`_marketBuy`](https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/utilities/poolsUtility/Position.sol#L460) with [RubiconMarket.getBuyAmountWithFee](https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L1115).

```solidity
    function _marketBuy(
        address _asset,
        address _quote,
        uint256 _maxFill
    ) internal {
        uint256 _buyAmount = rubiconMarket.getBuyAmountWithFee(
            ERC20(_asset),
            ERC20(_quote),
            _maxFill
        );
        // The same
    }
```
RubiconMarket.sol: Unsafe Arithmetic Operations
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L1044


# Vulnerability details

## Impact

Unsafe multiplications using the unitary denominations of either 1 ether (1e18) or 10**9 (1e9), both of which can easily lead to overflows when used as a multiplier for large amounts of assets.



## Proof of Concept


https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L1044

```solidity
           mul(pay_amt, 1 ether) <
```

https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L1084

```solidity
	     mul(buy_amt, 1 ether) <
```
                
https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L1056-L1059

```solidity
                uint256 baux = rmul(
                    mul(pay_amt, 10 ** 9),
                    rdiv(offers[offerId].pay_amt, offers[offerId].buy_amt)
                ) / 10 ** 9;    
```
                
https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L1098-L1101

```solidity
                    rmul(
                        mul(buy_amt, 10 ** 9),
                        rdiv(offers[offerId].buy_amt, offers[offerId].pay_amt)
                    ) / 10 ** 9
```
                    
https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L1141-L1144

```solidity
            rmul(
                mul(pay_amt, 10 ** 9),
                rdiv(offers[offerId].pay_amt, offers[offerId].buy_amt)
            ) / 10 ** 9
```
            
https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L1174-L1177

```solidity       
            rmul(
                mul(buy_amt, 10 ** 9),
                rdiv(offers[offerId].buy_amt, offers[offerId].pay_amt)
            ) / 10 ** 9   
```


## Tools Used

Manual Review

## Recommended Mitigation Steps

Use of the mul operation exposed by the DSMath library already incorporated into the codebase to guarantee all operations are performed safely and cannot overflow.