xanoutas/solidity-security-findings huggingface dataset:
Too little fees are transferred
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
`calcAmountAfterFees` deducts too little.

## Proof of Concept
In `buy` the input `amount` (amount that the user want) is reduced by fees:

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

The actual fees are charged on the spend amount however:

```js
//in super.buy
uint256 spend = mul(quantity, _offer.buy_amt) / _offer.pay_amt;

...

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

...

uint256 mFee = mul(spend, makerFee()) / 100_000;
```

`quantity` is the `amount` value that was reduced earlier and is used to calculate spend as seen above. The issue is that the fees are calculated based on that reduced amount. An example calculation:

- user wants to get 100.000 (`amount`) tokens of `pay_gem`
- the offer he targets has `pay_amt = 100.000` and `buy_amt = 100.000`, so conversion rate is 1
- `feeBPS = 100` and `makerFee() = 100`, so both effectively 1% fee
- `amount` is now reduced to 98.000 before passing it to `super.buy`
- `spend = 98000*100000/100000`
- `fee = 98000*100/10000 = 980` (ignoring the issue that BPS is not handled correctly and is 100_000 in the code, but thats separate)
- `mfee = 98000*100/10000 = 980`
- so both `fee` and `mFee` are 980. Since the user wants to buy 100.000 `pay_gem` he would need to pay 100.000 `buy_gem` without fees. The fees should be deducted from that full payment amount and should be 1000 both.


## Tools Used

Manual review

## Recommended Mitigation Steps

Dont reduce `amount`, pass it in full. Then calculate spend, deduct fees from it and after that convert it into the `pay_gem` equivalent before updating the offer values.
Collaterals are shared for all positions in `Position` contract
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#L264


# Vulnerability details

## Impact
`Position._borrowLoop` borrows max borrow amount, so opening position might not work as intended.

## Proof of Concept
`Position._borrowLoop` borrows max borrow amount except for the last loop.

```solidity
    _toBorrow = wmul(_maxBorrow(_bathTokenQuote), _toBorrow);
```
But all collaterals are shared in a comptroller so it can use other position's collateral. If the admin provides collaterals for position 1 and position 2 in different collateral tokens, and then opens the first position, it might use second position's collateral instead of reversion due to lack of collateral. 

And the collateral for increasing margin can also be used for opening positions. 


## Tools Used
Manual Review

## Recommended Mitigation Steps
We should specify some percentage for the first buy to mitigate this borrowing problem.
an attacker can prevent users to migrate their tokens via v2Migrator.sol
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
an Attacker may prevent users to migrate their token by sending some token to the `v2Migrator.sol` contract directly, we will explain how this could be happen in POC below.  

## Proof of Concept
the attacker can prevent user to call migrate function this way : 

1-Alice(an attacker) have some token from v1 and want to migrate it to v2 by calling `migrate` function,everything work as well until now. 

2- then Alice(attacker) decide to send some tokens directly to the `v2Migrator.sol` address and these tokens will stuck in the contract forever(because this contract did not have any function to handle this case. 


3-bob(user) want to change his v1 tokens to v2 by calling `migrate` function but this function will revert because this check will not pass :
```solidity 

 require(
            IERC20(bathTokenV2).balanceOf(address(this)) == 0,
            "migrate: BATH TOKENS V2 STUCK IN THE CONTRACT"
        );
```
https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/V2Migrator.sol#L64

4-this will cause the transaction to revert because the `v2Migrator.sol` balance are not 0 and won't be zero because Alice send some token and make this token stuck in the contract forever

## Tools Used

manual review 

## Recommended Mitigation Steps
I recommend add a functionality that handle this case by setting a logic that send back the token that the attacker send to the `v2Migrator.sol` contract directly or burn it when some attacker send tokens directly to the contract or transfer it to the owner of the protocol address
[M-04] Frontrunning in the take function can lead to an attacker stealing a users' position in the buy function
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/RubiconMarket.sol#L314
https://github.com/RubiconDeFi/rubi-protocol-v2/blob/34db36c71b170c8f13941ab26eb7ada1288ec82f/contracts/RubiconMarket.sol#L564


# Vulnerability details

## Impact
Initial user who wants to enter the Rubicon market at a specific time for a specific asset value will lose out
to a frontrunner who will be able to enter the market at a better price than the initial user.

## Proof of Concept
The victim user calls the ```buy``` function in ```RubiconMarket``` with a specific quantity amount to purchase an asset. The ```buy``` function is executed, and the transaction is pending in the mempool, waiting to be included in a block.

The attacker monitors the mempool and observes the pending buy transaction from the victim user, which contains the specific quantity amount the victim user intends to purchase. The attacker quickly submits a transaction with a higher gas price that calls the ```take``` function with a higher quantity amount, front-running the victim user's buy transaction and bypassing the ```synchronized``` modifier. The attacker's ```take``` transaction is included in the next block, ahead of the victim user's ```buy``` transaction, due to the higher gas price. 

The ```take``` function is executed, and the attacker acquires the asset, possibly at a lower price using the higher quantity amount, taking advantage of the victim user's ```buy``` transaction that hasn't been included in a block yet. The victim user's buy transaction is eventually included in a block, but the asset has already been acquired by the attacker at a lower price.


## Tools Used
Manual Review

## Recommended Mitigation Steps
Add the synchronized modifier to the take function, just like in the buy function.
set critical parameters like feeTo in constrctor
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

There is a risk of loss of fee in certain conditions. This can occur when a user calls the `buy()` function immediately after the contract is deployed.

## Proof of Concept

The `feeTo` parameter is a critical input that determines where fees are sent. If this parameter is not set correctly, fees may be lost. In the current implementation, the feeTo parameter is not set in the constructor, which means that if a user calls the `buy()` function before setting the `feeTo` parameter, the protocol may lose fee.

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

## Tools Used

Manual Review

## Recommended Mitigation Steps

We recommend setting critical input parameters like `feeTo` in the constructor itself at the deployment stage. This will ensure that the parameter is set correctly and avoid any risk of lost fees.
Reward states are not updated during when `BathBuddy` is paused
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/periphery/BathBuddy.sol#L175


# Vulnerability details


## Impact
Reward states are not updated during when `BathBuddy` is paused, so reward amount will not be correct after unpausing.

## Proof of Concept
In `BathBuddy`, only `getReward` and `notifyRewardAmount` can change reward states, and reward states should be updated during paused states for correct reward amounts.

```solidity
    function getReward(
        IERC20 rewardsToken,
        address holderRecipient
    )
        external
        override
        nonReentrant //@audit-info 
        whenNotPaused
```

When `BathBuddy` is paused, it is okay for users not getting rewards. But `getReward` is blocked, so the only way to update is to call `notifyRewardAmount`. `notifyRewardAmount` needs admin previlage, so users can't call it, and it also updates `periodFinish`, too. 

So it is recommended that anyone is able to update rewards during paused states without getting rewards when `BathBuddy` is paused.


## Tools Used
Manual Review

## Recommended Mitigation Steps
It is recommended that anyone is able to update rewards during paused states without getting rewards when `BathBuddy` is paused.
`RubiconMarket._matcho` doesn't consider market fees
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


## Impact
`RubiconMarket._matcho` doesn't consider market fees, so making an offer might end with reversion.


## Proof of Concept

`RubiconMarket._matcho` is called when an offer maker tries to make an offer. `_matcho` finds offers that have better ratio before making an offer and fulfills it because it makes profit. When `_matcho` tries to fulfill the existing offer, it doesn't consider the fee. 

Let us assume that there exists an offer large enough for simplicity, and the ratio is the same for current offer and the old offer.

This scenario will pass the ratio check in line 1300, and will call `buy(best_maker_id, t_buy_amt)`, because `m_pay_amt` is far greater than `t_buy_amt` in our assumption. 

```solidity
            if (
                mul(m_buy_amt, t_buy_amt) >
                mul(t_pay_amt, m_pay_amt) +
                    (
                        rounding
                            ? m_buy_amt + t_buy_amt + t_pay_amt + m_pay_amt
                            : 0
                    )
            ) {
                break;
            }
            // ^ The `rounding` parameter is a compromise borne of a couple days
            // of discussion.
            buy(best_maker_id, min(m_pay_amt, t_buy_amt));
```

We need `t_pay_amt` + fee for successful `buy`, but we only want to spend `t_pay_amt`, and it might cause revert.


## Tools Used
Manual Review

## Recommended Mitigation Steps
We should deduct fees from `t_buy_amt`.
```solidity
 -  buy(best_maker_id, min(m_pay_amt, t_buy_amt));
 +  buy(best_maker_id, min(m_pay_amt, sub(t_buy_amt, fee)));
```
Fee calculation is wrong in `Position._marketBuy`
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


# Vulnerability details

## Impact
The fee calculation of `Position._marketBuy` is not correct.

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

Rubicon market will deduct some fees from this `_maxFill`. 

In Rubicon market implementation, the following equation will hold.

`_maxFill - fee` * (1 + `fee ratio`) = `_maxFill`

`fee` = `_maxFill` * (1 - 1 / (1 + `fee ratio`)) = `_maxFill` * `fee ratio` / (1 + `fee ratio`)

The fee calculation of `Position._marketBuy` is not correct.


## Tools Used
Manual Review

## Recommended Mitigation Steps
We can fix as follows:
```solidity
 -  uint256 _fee = _maxFill.mul(rubiconMarket.getFeeBPS()).div(10000);
 +  uint256 _fee = _maxFill.mul(feeBPS).div(10000 + feeBPS);
```
`Position._marketSell` might revert due to lack of asset tokens
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#L475-L511


# Vulnerability details


## Impact
`Position._marketSell` doesn't make sure that there is enough asset tokens to sell, so it might revert.



## Proof of Concept

`Position._marketSell` sells asset tokens to get quote tokens to repay debts. And it gets asset amounts needed using `RubiconMarket.getPayAmount`. And we should spend `_payAmount + fees` to get `_minFill` amount of quote tokens.

If `_assetBalance < _payAmount`, it is okay because we get additional asset tokens from `msg.sender`.

```solidity
        if (_assetBalance < _payAmount) {
            IERC20(_asset).transferFrom(
                msg.sender,
                address(this),
                _payAmount.sub(_assetBalance).add(_fee)
            );
        }
```

But when `_payAmount <= _assetBalance < _payAmount + fee`, there is no additional payment, so _assetBalance will be less than `_payAmount + fee`, which is the asset tokens we should spend. So `RubiconMarket.sellAllAmount` will revert and so will `Position._marketSell`.


## Tools Used
Manual Review

## Recommended Mitigation Steps

We should get additional payment when `_payAmount <= _assetBalance < _payAmount + fee`, too.

```solidity
 -      if (_assetBalance < _payAmount) {
 +      if (_assetBalance < _payAmount.add(_fee)) {
            IERC20(_asset).transferFrom(
                msg.sender,
                address(this),
 -              _payAmount.sub(_assetBalance).add(_fee)
 +              _payAmount.add(_fee).sub(_assetBalance)
            );
        }
```
`Position._marketSell` gets wrong selling amount
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#L482-L486


# Vulnerability details

## Impact
`Position._marketSell` gets wrong selling amount. We only need to get `_minFill`, but this method tries to fill `_minFill + fee`, and it can cause revert for valid trade.

## Proof of Concept

`Position._marketSell` sells asset tokens to get quote tokens to repay debts. And it gets asset amounts needed using `RubiconMarket.getPayAmount`.

```solidity
        uint256 _payAmount = rubiconMarket.getPayAmount(
            ERC20(_asset),
            ERC20(_quote),
            _minFill.add(_fee)
        );
```

_minFill is in quote token, and it's in pay token of an offer in Rubicon market. In Rubicon market, fee is deducted from buy token of an offer, so we shouldn't add fees to `_minFill`.


## Tools Used
Manual Review

## Recommended Mitigation Steps

We should use `_minFill` without adding fees.

```solidity
        uint256 _payAmount = rubiconMarket.getPayAmount(
            ERC20(_asset),
            ERC20(_quote),
 -          _minFill.add(_fee)
 +          _minFill
        );
```
`RubiconMarket.buy` has wrong 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#L861


# Vulnerability details


## Impact
`RubiconMarket.buy` has wrong fee deduction in its implementation. It is wrong and inconvenient to use.


## Proof of Concept

`RubiconMarket.buy` deducts fee from input `amount`.

```solidity
    amount = calcAmountAfterFee(amount);
```

`amount` is in current offer's pay token, but in the implementation of `SimpleMarket.buy`, fee is taken using buy token. So it is not correct to deduct fees from pay token amount, and we can remove the fee deduction.

`RubiconMarket._matcho` also tries to fulfill some amounts without fee deduction, so it is correct not to deduct fees in `buy` method.

## Tools Used
Manual Review

## Recommended Mitigation Steps
We can just remove the fee deduction.

```solidity
 -      amount = calcAmountAfterFee(amount);
```
DOS attack possibliity on `SimpleMarket.buy` with full amount
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


## Impact
An adversary can front-run and cause DOS for `SimpleMarket.buy` with full amount. But `Position` contract is safe from this attack, so this will not be a high severity. 

## Proof of Concept

`SimpleMarket.buy` fulfills some parts of an offer, and it surely can fulfill the whole offer. An adversary can front-run and block this buy operation with full amount.

In `SimpleMarket.buy`, when Alice wants to fulfill `quantity` = `offer.pay_amt`, an adversary Bob will front-run and fulfill some dust amount. After that, `offer.pay_amt` will be a little smaller, and Alice's `buy` will fail in the following validation.

```solidity
        if (
            quantity == 0 ||
            spend == 0 ||
            quantity > _offer.pay_amt ||
            spend > _offer.buy_amt
        ) {
            return false;
        }
```

Although `SimpleMarket.buy` is overridden by `RubiconMarket.buy`, `RubiconMarket.buy` calls `SimpleMarket.buy`, so this attack path is still possible. `buyAllAmount` and `sellAllAmount` are safe from this attack, so `Position` contract is safe. But others can interact with `RubiconMarket` using `buy` method, so I think medium severity is appropriate.


## Tools Used
Manual Review

## Recommended Mitigation Steps
We can accept quantity which is more than current offer's `pay_amt` in `SimpleMarket.buy`, and return excess pay token amount.
`BathBuddy` doesn't have pause mechanism
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/511636d889742296a54392875a35e4c0c4727bb7/contracts/periphery/BathBuddy.sol#L175


# Vulnerability details

## Impact
`BathBuddy` doesn't have pause mechanism, so `BathBuddy.getReward` will always work. 

## Proof of Concept
`BathBuddy.getReward` has `whenNotPaused` modifier.

```solidity
    function getReward(
        IERC20 rewardsToken,
        address holderRecipient
    )
        external
        override
        nonReentrant
        whenNotPaused
```
So it should work only when it is not paused. But `BathBuddy` doesn't have pause mechanism.
`BathBuddy` inherits openZeppelin's `Pausable` contract, so it can use paused status using `whenNotPaused`, but it doesn't set `_paused` status using `_pause` method. As a result, `BathBuddy.getReward` will work always without any restriction.

## Tools Used
Manual Review

## Recommended Mitigation Steps
We should add proper pausing mechanism to `BathBuddy` contract.
Swaps in Position contract fail to consider potential maker fees of RubiconMarket v2
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#L438-L511


# Vulnerability details

## Impact

The Position contract uses the RubiconMarket to swap assets to create leveraged positions. This implementation is present in the `_rubiconSwap` function, which delegates the functionality to either `_marketBuy` or `_marketSell` functions, depending on whether the operation is a buy or a sell.

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

```solidity
438:     /// @notice execute market buy on RubiconMarket
439:     /// @param _asset - input token
440:     /// @param _quote - output token
441:     /// @param _fillLimit - maximum amount of _quote to be sold
442:     /// @param _buy - true = market buy; false = market sell
443:     function _rubiconSwap(
444:         address _asset,
445:         address _quote,
446:         uint256 _fillLimit,
447:         bool _buy
448:     ) internal {
449:         _buy
450:             ? _marketBuy(_asset, _quote, _fillLimit)
451:             : _marketSell(_asset, _quote, _fillLimit);
452:     }
453: 
454:     function _marketBuy(
455:         address _asset,
456:         address _quote,
457:         uint256 _maxFill
458:     ) internal {
459:         uint256 _fee = _maxFill.mul(rubiconMarket.getFeeBPS()).div(10000);
460:         uint256 _buyAmount = rubiconMarket.getBuyAmount(
461:             ERC20(_asset),
462:             ERC20(_quote),
463:             _maxFill.sub(_fee)
464:         );
465:         IERC20(_quote).approve(address(rubiconMarket), _maxFill);
466: 
467:         rubiconMarket.buyAllAmount(
468:             ERC20(_asset),
469:             _buyAmount,
470:             ERC20(_quote),
471:             _maxFill
472:         );
473:     }
474: 
475:     function _marketSell(
476:         address _asset,
477:         address _quote,
478:         uint256 _minFill
479:     ) internal {
480:         uint256 _feeBPS = rubiconMarket.getFeeBPS();
481:         uint256 _fee = _minFill.mul(_feeBPS).div(10000);
482:         uint256 _payAmount = rubiconMarket.getPayAmount(
483:             ERC20(_asset),
484:             ERC20(_quote),
485:             _minFill.add(_fee)
486:         );
487:         uint256 _assetBalance = IERC20(_asset).balanceOf(address(this));
488: 
489:         /// @dev recalculate fee in _asset form
490:         _fee = _payAmount.mul(_feeBPS).div(10000);
491: 
492:         if (_assetBalance < _payAmount) {
493:             IERC20(_asset).transferFrom(
494:                 msg.sender,
495:                 address(this),
496:                 _payAmount.sub(_assetBalance).add(_fee)
497:             );
498:         }
499: 
500:         IERC20(_asset).approve(
501:             address(rubiconMarket),
502:             IERC20(_asset).balanceOf(address(this))
503:         );
504: 
505:         rubiconMarket.sellAllAmount(
506:             ERC20(_asset),
507: