xanoutas/solidity-security-findings huggingface dataset:
malicious user can create orders with `address(0)` that brick the market
Audit: 2023-04-rubicon-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L511-L525
https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L824-L835


# Vulnerability details

## Description
Added in the new version of `RubiconMarket` was the ability to create offers providing `owner` and `recipient`. Which will be the receivers of fees and sales of the offers.

There is however no check that these are not `address(0)`. Since most tokens revert on transfers to `address(0)` a user can create malicious small offers that will revert when they are filled.

This will brick a lot of functions like matching of orders and buy/sellAll. These orders can be created just above `_dust` to minimize funds needed.

## Impact
A user can cause reverts in market functions bricking the market.

## Proof of Concept
PoC test in `Market.t.sol`:
```solidity
pragma solidity ^0.8.0;

import "../../contracts/periphery/TokenWithFaucet.sol";
import "../../contracts/RubiconMarket.sol";

import "forge-std/Test.sol";

/// @notice proxy isn't used here
contract MarketTest is Test {
  //========================CONSTANTS========================
  address public owner;
  address FEE_TO = 0x0000000000000000000000000000000000000FEE;
  // core contracts
  RubiconMarket market;

  // test tokens
  TokenWithFaucet TEST;
  TokenWithFaucet TUSDC;

  address alice = 0x0000000000000000000000000000000000000123;
  address bob = 0x0000000000000000000000000000000000000124;
  address eve = 0x0000000000000000000000000000000000000125;

  // deployRubiconProtocolFixture()
  function setUp() public {
    owner = msg.sender;

    // deploy new Market instance and init
    market = new RubiconMarket();
    market.initialize(FEE_TO);
    market.setFeeBPS(10);

    // deploy test tokens
    TEST = new TokenWithFaucet(address(this), "Test", "TEST", 18);
    TUSDC = new TokenWithFaucet(address(this), "Test Stablecoin", "TUSDC", 6);
    vm.label(address(TEST),"TEST");
    vm.label(address(TUSDC),"TUSDC");

    TUSDC.mint(alice,10e6);
    TEST.mint(bob,10e18);

    TUSDC.mint(eve,1e6);
    TEST.mint(eve,1e18);

    vm.prank(alice);
    TUSDC.approve(address(market),type(uint256).max);

    vm.prank(bob);
    TEST.approve(address(market),type(uint256).max);

    vm.startPrank(eve);
    TUSDC.approve(address(market),type(uint256).max);
    TEST.approve(address(market),type(uint256).max);
    vm.stopPrank();
  }

  function test_createOrdersWithAddress0() public {
    vm.prank(alice);
    market.offer(1e6, TUSDC, 2e18, TEST); // fx 2
    
    vm.prank(bob);
    market.offer(1e18, TEST, 2e6, TUSDC); // fx 0.5

    // malicious user places orders inside the trade gap
    vm.startPrank(eve);
    market.offer(100,TUSDC,150e12,TEST,0,true,address(0),address(0)); // fx 1.5 
    market.offer(100e12,TEST,150,TUSDC,0,true,address(0),address(0)); // fx 0.66
    vm.stopPrank();

    // new order matched against malicious offers will revert
    vm.expectRevert("ERC20
Positions lose out on BathBuddy rewards
Audit: 2023-04-rubicon-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/BathHouseV2.sol#L115-L128


# Vulnerability details

## Description
BathBuddy rewards are handed out over time to users taking part in the pools (compound cTokens).

Since Positions have no way of redeeming these rewards users holding positions lose out on BathBuddy rewards.

The `COMP` (or similar) token rewards are possible to redeem since a user can redeem on behalf of another user, then use `Position::withdraw` to fetch them.

## Impact
Holders of positions lose out on BathBuddy rewards for their `bathToken` (`cToken`) holdings

## Proof of Concept
PoC in `Position.t.sol`:
```solidity
pragma solidity ^0.8.0;

import "../../contracts/compound-v2-fork/WhitePaperInterestRateModel.sol";
import "../../contracts/compound-v2-fork/CErc20Delegate.sol";
import "../../contracts/compound-v2-fork/CErc20.sol";
import "../../contracts/compound-v2-fork/Comptroller.sol";
import "../../contracts/compound-v2-fork/CToken.sol";
import "../../contracts/periphery/TokenWithFaucet.sol";
import "../../contracts/periphery/DummyPriceOracle.sol";
import "../../contracts/periphery/BathBuddy.sol";
import "../../contracts/RubiconMarket.sol";
import "../../contracts/BathHouseV2.sol";
import "../../contracts/utilities/poolsUtility/Position.sol";
import "../../contracts/utilities/poolsUtility/PoolsUtility.sol";

import "forge-std/Test.sol";

contract PositionTest is Test {
  //========================CONSTANTS========================
  address public owner;
  address FEE_TO = 0x0000000000000000000000000000000000000FEE;
  // core contracts
  RubiconMarket market;
  Comptroller comptroller;
  BathHouseV2 bathHouse;

  DummyPriceOracle oracle;

  // test tokens
  TokenWithFaucet TEST;
  TokenWithFaucet TUSDC;
  TokenWithFaucet COMP;
  TokenWithFaucet BUDDY;

  CErc20 cTEST;
  CErc20 cTUSDC;

  BathBuddy buddyToTEST;
  BathBuddy buddyToTUSDC;

  address alice = 0x0000000000000000000000000000000000000123;
  address bob = 0x0000000000000000000000000000000000000124;
  address eve = 0x0000000000000000000000000000000000000125;

  function setUp() public {
    owner = msg.sender;
    // deploy Comptroller instance
    comptroller = new Comptroller();

    // deploy new Market instance and init
    market = new RubiconMarket();
    market.initialize(FEE_TO);
    market.setFeeBPS(10);

    // deploy test tokens
    TEST = new TokenWithFaucet(address(this), "Test", "TEST", 18);
    TUSDC = new TokenWithFaucet(address(this), "Test Stablecoin", "TUSDC", 6);
    vm.label(address(TEST),"TEST");
    vm.label(address(TUSDC),"TUSDC");

    // baseRate = 0.3, multiplierPerYear = 0.02
    WhitePaperInterestRateModel irModel = new WhitePaperInterestRateModel(3e17, 2e16);
    CErc20Delegate bathTokenImplementation = new CErc20Delegate();

    bathHouse = new BathHouseV2();
    bathHouse.initialize(address(comptroller),address(this));
    bathHouse.createBathToken(address(TEST), irModel, 1e18, address(bathTokenImplement
lowering interest rate will cause `closePosition` to fail
Audit: 2023-04-rubicon-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/utilities/poolsUtility/Position.sol#L322-L333
https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/utilities/poolsUtility/Position.sol#L385-L388


# Vulnerability details

## Description
A Position keeps track of its debt by guessing using the current interest rate:
https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/utilities/poolsUtility/Position.sol#L322-L333
```solidity
File: utilities/poolsUtility/Position.sol

322:    function _calculateDebt(
323:        address _bathToken,
324:        uint256 _startBlock,
325:        uint256 _borrowedAmount
326:    ) internal view returns (uint256 _debt) {
327:        uint256 _blockDelta = block.number - _startBlock;
328:
329:        uint256 _interest = (
330:            (_borrowedAmount).mul(borrowRate(_bathToken).mul(_blockDelta))
331:        ).div(10 ** 18);
332:        _debt = _borrowedAmount.add(_interest);
333:    }
```

The issue is that the borrow rate can be changed. Specifically if it is lowered it will cause the calculation to underestimate the debt and the position will not be able to close.

## Impact
Lowering the interest rate will cause position open against that pool to revert when calling `closePosition`. The user must manually repay the loan and then transfer the bathTokens (cTokens) out of the position (via `withdraw`) and redeem themselves. This will leave the position in the Position contract open. Also possibly causing the trader to lose out on the trade opportunity that caused them to want to close the position.

## Proof of Concept
PoC test in `Position.t.sol`:
```solidity
pragma solidity ^0.8.0;

import "../../contracts/compound-v2-fork/WhitePaperInterestRateModel.sol";
import "../../contracts/compound-v2-fork/CErc20Delegate.sol";
import "../../contracts/compound-v2-fork/CErc20.sol";
import "../../contracts/compound-v2-fork/Comptroller.sol";
import "../../contracts/compound-v2-fork/CToken.sol";
import "../../contracts/periphery/TokenWithFaucet.sol";
import "../../contracts/periphery/DummyPriceOracle.sol";
import "../../contracts/RubiconMarket.sol";
import "../../contracts/BathHouseV2.sol";
import "../../contracts/utilities/poolsUtility/Position.sol";
import "../../contracts/utilities/poolsUtility/PoolsUtility.sol";

import "forge-std/Test.sol";

contract PositionTest is Test {
  //========================CONSTANTS========================
  address public owner;
  address FEE_TO = 0x0000000000000000000000000000000000000FEE;
  // core contracts
  RubiconMarket market;
  Comptroller comptroller;
  BathHouseV2 bathHouse;

  DummyPriceOracle oracle;

  // test tokens
  TokenWithFaucet TEST;
  TokenWithFaucet TUSDC;

  CErc20 cTEST;
  CErc20 cTUSDC;

  address alice = 0x0000000000000000000000000000000000000123;
  address bob = 0x0000000000000000000000000000000000000124;

  function setUp() public {
    owner = msg.sender;
    // deploy Comptroller instance
    comptroller = new C
`_borrowLimit` is not updated, leading to inaccurate collateral positions.
Audit: 2023-04-rubicon-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/utilities/poolsUtility/Position.sol#L225-L240
https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/utilities/poolsUtility/Position.sol#L125-L206


# Vulnerability details

## Impact
After `increaseMargin` user's `_borrowLimit` isn't updated, resulting in unutilized collateral and limiting their borrowing capacity.

## Proof of Concept
After increasing the margin using the `increaseMargin` function, the borrowing limit for the position is not updated, and users are unable to borrow more tokens even if they have provided more collateral. This issue can be demonstrated by using the `openPosition` functions.
https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/utilities/poolsUtility/Position.sol#L125-L206

## Tools Used
VS code

## Recommended Mitigation Steps
we recommend updating the user's borrowing limit after increasing the margin in the `increaseMargin` function.
Some positions will get liquidated immediately
Audit: 2023-04-rubicon-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Description
When opening a position the user makes a deposit and takes a loan against this on the Rubicon compound fork. This loan is taken using max liquidity:
https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/utilities/poolsUtility/Position.sol#L306-L319
```solidity
File: utilities/poolsUtility/Position.sol

306:    function _maxBorrow(
307:        address _bathToken
308:    ) internal view returns (uint256 _max) {
309:        (uint256 _err, uint256 _liq, uint256 _shortfall) = comptroller
310:            .getAccountLiquidity(address(this));
311:
312:        require(_err == 0, "_maxBorrow: ERROR");
313:        require(_liq > 0, "_maxBorrow: LIQUIDITY == 0");
314:        require(_shortfall == 0, "_maxBorrow: SHORTFALL != 0");
315:
316:        uint256 _price = oracle.getUnderlyingPrice(CToken(_bathToken));
317:        _max = (_liq.mul(10 ** 18)).div(_price);
318:        require(_max > 0, "_maxBorrow: can't borrow 0");
319:    }
```

The danger here is that the interest rate for a loan needs to be higher than the interest for the deposit of the collateral. Hence the block after the loan is taken it will be under water.

## Impact
Positions opened will in the block after they are created become under water and possible to liquidate.

This only impacts a certain set of leverages (shorts 1x, longs 1.7x and so on) where you loan up to your collateral max, hence medium.

A user will have to know about this behavior in Position and in the same tx (to be safe) increase their margin to not be vulnerable to liquidation.

## Proof of Concept
PoC test, `PositionTest.t.sol`:
```
pragma solidity ^0.8.0;

import "../../contracts/compound-v2-fork/WhitePaperInterestRateModel.sol";
import "../../contracts/compound-v2-fork/CErc20Delegate.sol";
import "../../contracts/compound-v2-fork/CErc20.sol";
import "../../contracts/compound-v2-fork/Comptroller.sol";
import "../../contracts/compound-v2-fork/CToken.sol";
import "../../contracts/periphery/TokenWithFaucet.sol";
import "../../contracts/periphery/DummyPriceOracle.sol";
import "../../contracts/RubiconMarket.sol";
import "../../contracts/BathHouseV2.sol";
import "../../contracts/utilities/poolsUtility/Position.sol";
import "../../contracts/utilities/poolsUtility/PoolsUtility.sol";

import "forge-std/Test.sol";

contract PositionTest is Test {
  //========================CONSTANTS========================
  address public owner;
  address FEE_TO = 0x0000000000000000000000000000000000000FEE;
  // core contracts
  RubiconMarket market;
  Comptroller comptroller;
  BathHouseV2 bathHouse;

  DummyPriceOracle oracle;

  // test tokens
  TokenWithFaucet TEST;
  TokenWithFaucet TUSDC;

  CErc20 cTEST;
  CErc20 cTUSDC;

  address alice = 0x0000000000000000000000000000000000000123;
  address bob = 0x0000000000000000000000000000000000000124;

  function setUp
No access control on `del_rank()` function resulting on deletion of any arbitrary id
Audit: 2023-04-rubicon-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details



## Impact
in [del_rank](https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L937-L947) function it can be called by anyone resulting in deleting of rank of any arbitary
## Proof of Concept

```solidity
function del_rank(uint256 id) external returns (bool) {//@audit anyone can call
require(!locked);
require(
!isActive(id) &&
_rank[id].delb != 0 &&
_rank[id].delb < block.number - 10
);
delete _rank[id];
emit LogDelete(msg.sender, id);
return true;
}
```

## Tools Used
Manual review

## Recommended Mitigation Steps

use access control on who can delete id
Upgraded Q -> 2 from #1373 [1686280329858]
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

Judge has assessed an item in Issue #1373 as 2 risk. The relevant finding follows:

 BathBuddy.getReward() is using the whenNotPaused modifier inherited from OpenZeppelin, but for BathBuddy.sol and BathHouseV2.sol there's no functionality to actually pause it. Consider adding the setPause function for BathBuddy.sol and BathHouseV2.sol.
Upgraded Q -> 2 from #1366 [1686279317781]
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

Judge has assessed an item in Issue #1366 as 2 risk. The relevant finding follows:

 When the user withdraws tokens from bathTokenV1 he pays a fee. The withdraw function then returns the (deposit amount - fee) which is then set as a variable amountWithdrawn in migrate(). Then the new bathTokenV2 are minted to the contract, but the amount that is minted is the amountWithdrawn so the user gets less than he deposits.
Upgraded Q -> 2 from #1275 [1686220504617]
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

Judge has assessed an item in Issue #1275 as 2 risk. The relevant finding follows:

 The value returned by sellAllAmount is calcAmountAfterFee(fill_amt). I.e. it subtract the fees on the total fill amount. However, the fees have been paid in multiple steps if multiple offers have been filled. The applied fees could be lower than calcAmountAfterFee(fill_amt) due to rounding taking place in the match with each individual offer filled.
Upgraded Q -> 2 from #931 [1686211434752]
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

Judge has assessed an item in Issue #931 as 2 risk. The relevant finding follows:

 In the FeeWrapper._rubicallPayable() function, Eth is sent to the _params.target address by calling the low level call function. But the contract existence of _params.target is never checked. If there is no contract at the _params.target the low level call function will return true and the sent Eth will be lost.

Hence it is recommended to check the contract existence of the _params.target address.
Upgraded Q -> 2 from #759 [1686206823979]
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

Judge has assessed an item in Issue #759 as 2 risk. The relevant finding follows:

 Low-level calls call/delegatecall/staticcall return true even if the account called is non-existent (per EVM design). Account existence must be checked before the call.
Upgraded Q -> 2 from #637 [1686198031766]
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

Judge has assessed an item in Issue #637 as 2 risk. The relevant finding follows:

 If _offer.owner == address(0) and getRecipient(d) == address(0), then we will send funds to the 0 address.
We should check to make sure that we are not registering the 0 address as owner, in addition, it is public so any user may lose funds this way.
Checking for boolean in transfer and transferFrom makes the protocol useless for some Non-ERC20 tokens
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Tokens such as [`USDT`](https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7) doesn't return bool.
```

function transfer(address _to, uint _value) public onlyPayloadSize(2 * 32) {
        uint fee = (_value.mul(basisPointsRate)).div(10000);
        if (fee > maximumFee) {
            fee = maximumFee;
        }
        uint sendAmount = _value.sub(fee);
        balances[msg.sender] = balances[msg.sender].sub(_value);
        balances[_to] = balances[_to].add(sendAmount);
        if (fee > 0) {
            balances[owner] = balances[owner].add(fee);
            Transfer(msg.sender, owner, fee);
        }
        Transfer(msg.sender, _to, sendAmount);
    }
```
Putting `transfer` and `trasnferFrom` operations in require needs those functions to return bool.

So, protocol won't be able to work with such kind of tokens.
## Proof of Concept

## Tools Used
Manual Review
## Recommended Mitigation Steps
Consider transferring of tokens outside of require
Use openzeppelin's safeTransferLib
Hardcoding 10**18 when fetching the price from oracle in `Position._maxBorrow()`
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#L316-L317


# Vulnerability details

## Impact 

The `bathToken` decimals may very depending for each underlying. The current code is using a DummyOracle for testing, but the protocol will switch to chainlink (according to discord contest chat messages) before mainnet. 

Chainlink will return different decimals depending on the token. The [recommended](https://ethereum.stackexchange.com/questions/92508/do-all-chainlink-feeds-return-prices-with-8-decimals-of-precision#comment161345_92513) approach when using chainlink is to fetch the `decimals()` when using the fetched price. 

Impact can be increasing the value returned by `_maxBorrow()` (used by `buyAllAmountWithLeverage()`) for tokens with less than 18 decimals and decreasing the value for tokens with more than 18 decimals.

## Proof of Concept

Harcoding 10**18 for the max borrow calculation.

```
uint256 _price = oracle.getUnderlyingPrice(CToken(_bathToken));
_max = (_liq.mul(10 ** 18)).div(_price);
```

## Recommended Mitigation Steps

In `Position._maxBorrow()`, fetch the decimals of the `_bathToken` being used.
Double counting the fees in `RubiconMarket.buy()` and `SimpleMarket.buy()`
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L861
https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L583-L587
https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L319
https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L338-L346


# Vulnerability details

## Impact

The function `RubiconMarket.buy()` will decrease the fees from the input `amount` by calling the function `RubiconMarket.calcAmountAfterFee()`. However, during the actual fee transfers in `SimpleMarket.buy()`, the fees will be deducted again using the same formulas. Double counting the fees can result in accounting error and loss of funds for users and extra profit for the fee receivers. 

## Proof of Concept

`RubiconMarket.buy()` will decrease the amount by decreasing the passed amount by the `(amount * feeBPS) / 100_000` and `(amount * makerFee()) / 100_000` for the maker.

```
_amount -= mul(amount, feeBPS) / 100_000;

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

```

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

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

Afterwards, the fee will be deducted again using the same formulas in `SimpleMarket.buy()`, this time using the `(previously decreased amount * _offer.buy_amt)/ _offer.pay_amy`, which is represented as the `spend`.

```
uint256 fee = mul(spend, feeBPS) / 100_000;
require(
    _offer.buy_gem.transferFrom(msg.sender, feeTo, fee),
    "Insufficient funds to cover fee"
);

// taker pay maker 0_0
if (makerFee() > 0) {
    uint256 mFee = mul(spend, makerFee()) / 100_000;
...
```

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

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

## Tools Used

Manual review.

## Recommended Mitigation Steps

Compute the fees only once and add comments for describing all the necessary fee deductions.
Checking the returned value for erc20 tokens will fail for widely used tokens such as USDT
Audit: 2023-04-rubicon-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L339-L342
https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L350-L357
https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L359-L362
https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L374-L377
https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L379-L382
https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L459-L461
https://github.com/code-423n4/2023-04-rubicon/blob/main/contracts/RubiconMarket.sol#L538


# Vulnerability details

## Impact

The `RubiconMarket.sol` functions such as `buy()` and `cancel()` won't work for some non-standard erc20 tokens (USDT, BNB) that don't return a boolean on transfers. Usage of these tokens will no be possible in Rubicon v2.

One practical problem can be creating orders where `pay_gem` is a standard token and `buy_gem` is not, because in this case orders would be created successfully but they would not be able to be bought.

## Proof of Concept

1. An order is created for WETH (pay_gem) and USDT (buy_gem) through `offer()` 
2. Calling `buy()` will always fail for this order.

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

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

## Recommended Mitigation Steps

Use OpenZeppelin SafeERC20 in `RubiconMarket.sol` when performing ERC20 transfers.