xanoutas/solidity-security-findings huggingface dataset:
`SimpleMarket.calcAmountAfterFee` is not correct
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


## Impact
The implementation of `SimpleMarket.calcAmountAfterFee` is wrong and the result is slightly less than the correct amount.


## Proof of Concept

`calcAmountAfterFee` is for actual buy token amount from total buy token amount after fee deduction. This method is used in the `RubiconMarket` contract, but its implementation is in the parent contract `SimpleMarket`.

The fee is deducted in `SimpleMarket.buy` method. Although it is overridden in `RubiconMarket`, but `RubiconMarket.buy` still calls `SimpleMarket.buy` where the main logic exists. 

In `SimpleMarket.buy`, `spend` is the actual buy amount for current offer.

```solidity
    uint256 spend = mul(quantity, _offer.buy_amt) / _offer.pay_amt;
```

And the fee is based on `spend`. 
```solidity
    uint256 fee = mul(spend, feeBPS) / 100_000;
```

So `total amount = spend * (1 + fee ratio)`, and `calcAmountAfterFee` calculates `spend` from `total amount`, so the correct implementation of `calcAmountAfterFee` is as follows:
`amount / (1 + fee ratio)`, instead of `amount * (1 - fee ratio)`.

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

## Tools Used
Manual Review

## Recommended Mitigation Steps

We can correct as follows:

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

        if (makerFee() > 0) {
 -          _amount -= mul(amount, makerFee()) / 100_000;
 +          _amount -= mul(amount, 10_000) / (makerFee() + 10_000);
        }
    }
```
Note: 100K is used for total basis points here, but it is not correct. So I wrote mitigation using 10K. I will report this another issue in more detail.
Closing a position doesn't unwind the leveraged position
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#L210-L223


# Vulnerability details

## Impact

The Position contract can be used to create a leveraged position using different pieces of the protocol. The `openPosition` function implementation uses a borrow loop to create the leverage, in which it repeatedly executes the following cycle: it deposits the given asset into the Compound fork, borrows the quote asset and exchanges the quote asset back for the original asset using the RubiconMarket.

However, its counterparty function `closePosition` doesn't follow the same pattern. As we can see in the following snippet of code, the function just tries to repay the debt and then redeem the cTokens for the underlying collateral:

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

```solidity
210:     function closePosition(uint256 posId) external onlyOwner {
211:         Position memory pos = positions[posId];
212:         require(pos.isActive, "closePosition: POS ISN'T ACTIVE");
213: 
214:         // load values to memory
215:         address asset = pos.asset;
216:         address quote = pos.quote;
217:         uint256 bathTokenAmount = pos.bathTokenAmount;
218: 
219:         _repay(asset, quote, posId);
220:         _redeem(asset, bathTokenAmount);
221: 
222:         _removePosition(posId);
223:     }
```

Expanding on the `_repay` function we can see it swaps the collateral asset for the quote asset and tries to repay the loaned amount. However, the asset tokens have been deposited as collateral in the Compound fork as part of the creation of the leveraged position.

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

```solidity
279:     /// @notice repay debt + interest
280:     function _repay(address _asset, address _quote, uint256 _posId) internal {
281:         address _bathTokenQuote = bathHouseV2.getBathTokenFromAsset(_quote);
282:         uint256 _amountToRepay = borrowBalanceOfPos(_posId);
283: 
284:         // sell asset for quote
285:         _rubiconSwap(_asset, _quote, _amountToRepay, false);
286:         uint256 _quoteBalance = IERC20(_quote).balanceOf(address(this));
287: 
288:         require(
289:             _amountToRepay <= _quoteBalance,
290:             "_repay: balance of quote lt. debt"
291:         );
292:         uint256 _borrowBalance = borrowBalance(_bathTokenQuote);
293:         if (_amountToRepay > _borrowBalance) {
294:             _amountToRepay = _borrowBalance;
295:         }
296: 
297:         IERC20(_quote).approve(_bathTokenQuote, _amountToRepay);
298:         require(
299:             CErc20Interface(_bathTokenQuote).repayBorrow(_amountToRepay) == 0,
300:             "_repay: ERROR"
301:         );
302:     }
```

The user won't have enough available assets to repay the complete borrowed amount and exit the position. T
Fee inclusivity calculations are inaccurate in RubiconMarket
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

Trading in the RubiconMarket has associated fee costs that are paid by the taker of the offer. These fees include the protocol fee and a new "maker fee" introduced in v2. Fees are pulled from the taker (caller of the function) independently of the trade amount, which means fees are not included in the trade amount. These are implemented in the `buy` function of the base contract `SimpleMarket`:

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

```solidity
337:         /// @dev Fee logic added on taker trades
338:         uint256 fee = mul(spend, feeBPS) / 100_000;
339:         require(
340:             _offer.buy_gem.transferFrom(msg.sender, feeTo, fee),
341:             "Insufficient funds to cover fee"
342:         );
343: 
344:         // taker pay maker 0_0
345:         if (makerFee() > 0) {
346:             uint256 mFee = mul(spend, makerFee()) / 100_000;
347: 
348:             /// @dev Handle the v1 -> v2 migration case where if owner == address(0) we transfer this fee to _offer.recipient
349:             if (_offer.owner == address(0) && getRecipient(id) != address(0)) {
350:                 require(
351:                     _offer.buy_gem.transferFrom(
352:                         msg.sender,
353:                         _offer.recipient,
354:                         mFee
355:                     ),
356:                     "Insufficient funds to cover fee"
357:                 );
358:             } else {
359:                 require(
360:                     _offer.buy_gem.transferFrom(msg.sender, _offer.owner, mFee),
361:                     "Insufficient funds to cover fee"
362:                 );
363:             }
364: 
365:             emit emitFee(
366:                 bytes32(id),
367:                 msg.sender,
368:                 _offer.owner,
369:                 keccak256(abi.encodePacked(_offer.pay_gem, _offer.buy_gem)),
370:                 _offer.buy_gem,
371:                 mFee
372:             );
373:         }
```

One of the new additions in the RubiconMarket v2 is fee inclusivity, a feature that would allow users to operate on the market by including the fee directly in the specified amount. This is present in different places of the contract, but the core implementation can be founded in the `calcAmountAfterFee` function:

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

```solidity
578:     function calcAmountAfterFee(
579:         uint256 amount
580:     ) public view returns (uint256 _amount) {
581:         require(amount > 0);
582:         _amount = amount;
583:         _amount -= mul(amount, feeBPS) / 100_000;
584: 
585:         if (makerFee() > 0) {
586:             _amount -= mul(amount, makerFee()) / 100_000;
587:         }
588:     }
```

As we can see in the previous
`Position._marketBuy` sells more quote amount than it should be
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#L467-L472


# Vulnerability details


## Impact
`Position._marketBuy` sells more quote amount than it should be, so it will revert in most cases.


## Proof of Concept

`Position._marketBuy` tries to get asset tokens from `_maxFill` of quote amount. 

```solidity
        rubiconMarket.buyAllAmount(
            ERC20(_asset),
            _buyAmount,
            ERC20(_quote),
            _maxFill
        );
```
`_maxFill` is an offer's buy token in Rubicon market, so we need additional fee amount because Rubicon market deducts fees from offer's buy token. We only have `_maxFill` of quote tokens, so we should input fee deducted value of `_maxFill` to `RubiconMarket.buyAllAmount`.


## Tools Used
Manual Review

## Recommended Mitigation Steps

We should deduct fees from `_maxFill`. `fee` is correctly deducted when we get _buyAmount before in order to start actual trade.

```solidity
        rubiconMarket.buyAllAmount(
            ERC20(_asset),
            _buyAmount,
            ERC20(_quote),
 -          _maxFill
 +          _maxFill.sub(fee)
        );
```
`Position` uses wrong fees for the Rubicon market
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#L459
https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/utilities/poolsUtility/Position.sol#L480


# Vulnerability details

## Impact
`Position` uses smaller fees for the Rubicon market, and it might block Rubicon swap via Rubicon market.


## Proof of Concept
`Position` get fees from Rubicon markets using `RubiconMarket.getFeeBPS` in `_marketSell` and `_marketBuy`.  
```solidity
        uint256 _feeBPS = rubiconMarket.getFeeBPS();
        uint256 _fee = _minFill.mul(_feeBPS).div(10000);
```
`RubiconMarket.getFeeBPS` only returns `RubiconMarket.feeBPS`, but we also need to consider maker fees, too.

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

So `Position` contract will consider less market fees, and `RubiconMarket.sellAllAmount` will fail when asset amount is less than it should be.


## Tools Used
Manual Review

## Recommended Mitigation Steps
feeBPS should be `RubiconMarket.feeBPS` + `RubiconMarket.makerFee()`
`RubiconMarket` uses 100K as total basis points
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


## Impact
100K is used for total basis points, so this protocol and the offer makers will get less fees than expected.

## Proof of Concept

In `RubiconMarket`, 100K is used as the total basis points.

```solidity
        uint256 fee = mul(spend, feeBPS) / 100_000;
```

`feeBPS` is in basis points and the comment on Line 710 also says about it.

```solidity
        /// @notice The starting fee on taker trades in basis points
        feeBPS = 1;
```

The correct value for basis points is 10K, and `Position` contracts also uses it when interacts with `RubiconMarket`. As a result, this protocol and the offer makers of `RubiconMarket` will get less fees fees.


## Tools Used
Manual Review

## Recommended Mitigation Steps
We should use 10K for total basis points.
`take` doesn't take the whole offer in `RubiconMarket`
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


## Impact
In `RubiconMarket`, `take` doesn't take the whole offer, so `sellAllAmount` and `buyAllAmount` will revert with OOG in most cases, and it will consume too much gas when it fortunately runs.

## Proof of Concept

In `RubiconMarket.sellAllAmount`, `take` starts with the current best offer’s total `pay_amt`.

```solidity
    take(bytes32(offerId), uint128(offers[offerId].pay_amt)); //We take the whole offer
```

`take` simply calls `buy`, and then `_buy` of `RubiconMarket` with fee deducted amount of `pay_amt`.

```solidity
        amount = calcAmountAfterFee(amount);

        function(uint256, uint256) returns (bool) fn = matchingEnabled 
            ? _buys
            : super.buy;

        return fn(id, amount);
```
And then `super(=SimpleMarket).buy` with the fee deducted amount. So it doesn’t take the whole offer.

The `amount` in `_buys` is deducted from `pay_amt`, `_unsort` will not be called, and the best offer will not be updated while it should be updated this time.

```solidity
        if (amount == offers[id].pay_amt) {
            if (isOfferSorted(id)) {
                //offers[id] must be removed from sorted list because all of it is bought
                _unsort(id);
            } else {
                _hide(id);
            }
        }
```
        
‌As a result, `RubiconMarket.sellAllAmount` will call `take` for the same offer with smaller amount again and again.

`RubiconMarket.buyAllAmount` has the same problem.

## Tools Used
Manual Review

## Recommended Mitigation Steps
We can call `_buy` method directly, instead of `take` or `buy` methods.
Low level calls to accounts with no code will succeed in `FeeWrapper`
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/utilities/FeeWrapper.sol#L60-L73
https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/utilities/FeeWrapper.sol#L76-L89


# Vulnerability details

## Impact

The FeeWrapper contract wraps calls to an arbitrary target contract to add support for fees. The implementation executes a low level call to proxy the call to the target contract. This can be seen in the `_rubicall` and `_rubicallPayable` functions:

https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/utilities/FeeWrapper.sol#L60-L73

```solidity
60:     function _rubicall(
61:         CallParams memory _params
62:     ) internal returns (bytes memory) {
63:         // charge fee from feeParams
64:         _chargeFee(_params.feeParams, _params.target);
65: 
66:         (bool _OK, bytes memory _data) = _params.target.call(
67:             bytes.concat(_params.selector, _params.args)
68:         );
69: 
70:         require(_OK, "low-level call to the Rubicon failed");
71: 
72:         return _data;
73:     }
```

https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/utilities/FeeWrapper.sol#L76-L89

```solidity
76:     function _rubicallPayable(
77:         CallParams memory _params
78:     ) internal returns (bytes memory) {
79:         // charge fee from feeParams
80:         uint256 _msgValue = _chargeFeePayable(_params.feeParams);
81: 
82:         (bool _OK, bytes memory _data) = _params.target.call{value: _msgValue}(
83:             bytes.concat(_params.selector, _params.args)
84:         );
85: 
86:         require(_OK, "low-level call to the router failed");
87: 
88:         return _data;
89:     }
```

Low level calls behave differently than function calls in Solidity. Calls at the EVM level to accounts with no code are successful, this is the expected and normal behavior. It is Solidity that adds checks to prevent accidental calls to accounts with no code while compiling code for normal function calls. 

This means that if the target account has no code, then the wrapped call using the FeeWrapper contract will succeed and the operation will be executed successfully. An accidental mistake may go unnoticed and may also cause unexpected loss of funds, as this call may include call value for transferring ETH.

## Proof of Concept

In the following test, we use the FeeWrapper contract to execute a call to an account with no code. The transaction will succeed and the ETH will be transferred to the target account.

Note: the snippet shows only the relevant code for the test. Full test file can be found [here](https://gist.github.com/romeroadrian/f3b7d6f9ab043340de7deb67a9c515e5). 

```solidity
function test_FeeWrapper_CallsToAccountsNoCodeSucceed() public {
    FeeWrapper.CallParams memory callParams;
    callParams.selector = bytes4(0x01020304);
    callParams.args = abi.encode(uint256(42));
    callParams.target = makeAddr("address with no code");
    callParams.feeParam
Rewards for initial period may be lost in `BathBuddy` 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#L191-L228


# Vulnerability details

## Impact

Rewards in the BathBuddy contract are initiated when the owner calls the `notifyRewardAmount`. This function calculates the reward rate per second (lines 196-198 and 207-208) and also records the start of the reward period (line 223):

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:     }
```

The intention here is to calculate how many tokens should be rewarded by unit of time (second) and record the span of time for the reward cycle. However, this has an edge case where rewards are not counted for the initial period of time until there is at least one participant (in this case, a holder of BathTokens). During this initial period of time, the reward rate will still apply but as there isn't any participant, then no one will be able to claim these rewards. See PoC for a detailed example of the issue.

## Proof of Concept

In the following test, we initiate the reward process by calling `notifyRewardAmount`. At the middle of the
Leverage _Limit Calculation Vulnerability Due to Collateral Factor in _borrowLimit Function
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#L526-L600


# Vulnerability details

## Impact
In the _borrowLimit function, a while loop is used to calculate the _limit and _lastBorrow values. If the _collateralFactor is less than 0.667, the loop may fail to break, for some leverage example 3X leverage and preventing the function from completing and causing a denial-of-service (DOS) vulnerability.

## Proof of Concept
The _borrowLimit function, defined in the [contract code](https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/utilities/poolsUtility/Position.sol#L526-L583), uses a while loop to calculate the _limit and _lastBorrow values. However, if the _collateralFactor is less than 0.667, the while loop may never break, leading to a DOS vulnerability.

For example, if we want 3X leverage and assume _assetAmount = 100, the _desiredAmount would be 300. If the asset has a _collateralFactor of 0.65, the while loop will iterate to calculate the _assetAmount as follows:

_assetAmount = 100 + 100*0.65¹ + 100*0.65² + .... + 100*0.65^(n-1)

This is a GP series with a=100 and r=0.65. If we assume that the number of terms in the series is infinite, we can calculate the sum as:

sum = a/(1-r)
Plugging in a=100 and r=0.65, we get:
sum = 100/(1-0.65) = 285.71
This means that the maximum leverage possible for a _collateralFactor of 0.65 is 2.85X, and we cannot create 3X leverage. As a result, the while loop in _borrowLimit will never break, causing the function to fail and creating a DOS vulnerability.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Add leverageMax function for calculating max leverage let take n=10 iteration because infinite iteration not possible. by adding below code you can create dynamic leverage.

```solidity
function leverageMax(uint256 _assetAmount , uint256 _collateralFactor) public pure returns (uint256) {
    uint256 sum = 0;
    uint256 term = _assetAmount ;

    for (uint256 i = 0; i < 10; i++) {
        sum += term;
        term *= _collateralFactor;
    }
    return sum;
}
```
If _collateralFactor=0.65 you get max possible leverage as 2.81X
instead of fixed max 3X leverage you can add leverageMax  function and getting more leverage example if _collateralFactor=0.9 you can create 6X leverage position.

update _leverageCheck function as below
```solidity
-function _leverageCheck(uint256 _leverage, bool _long) internal pure {
+function _leverageCheck(address _bathToken,uint256 _assetAmount,  uint256 _leverage, bool _long) internal {
      uint256 _wad = WAD;
-    uint256 _leverageMax = WAD.mul(3);
+    (, uint256 _collateralFactor, ) = comptroller.markets(_bathToken);
+    uint256 _leverageMax = leverageMax(_assetAmount, _collateralFactor);
    _long // long can't be with 1x leverage
        ? require(
            _leverage > _wad && _leverage <= _leverageMax,