Rubicon contest - IllIllI's results

An order book protocol for Ethereum, built on L2s.

General Information

Platform: Code4rena

Start Date: 23/05/2022

Pot Size: $50,000 USDC

Total HM: 44

Participants: 99

Period: 5 days

Judge: hickuphh3

Total Solo HM: 11

Id: 129

League: ETH

Rubicon

Findings Distribution

Researcher Performance

Rank: 5/99

Findings: 13

Award: $2,507.33

🌟 Selected for report: 3

πŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: cccz

Also found by: 0x1f8b, IllIllI, kenzo, pedroais

Labels

bug
duplicate
3 (High Risk)
sponsor confirmed

Awards

390.3586 USDC - $390.36

External Links

Lines of code

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L440-L452

Vulnerability details

Impact

Users will lose funds associated with orders they've placed, to other users that cancel the order

Proof of Concept

The RubiconRouter does no validation itself of whether a user should be able to cancel an order, and instead relies on RubiconMarket to do the checks:

File: contracts/RubiconRouter.sol   #1

440       function cancelForETH(uint256 id) external returns (bool outcome) {
441           (uint256 pay_amt, ERC20 pay_gem, , ) = RubiconMarket(
442               RubiconMarketAddress
443           ).getOffer(id);
444           require(
445               address(pay_gem) == wethAddress,
446               "trying to cancel a non WETH order"
447           );
448           // Cancel order and receive WETH here in amount of pay_amt
449           outcome = RubiconMarket(RubiconMarketAddress).cancel(id);
450           WETH9(wethAddress).withdraw(pay_amt);
451           msg.sender.transfer(pay_amt);
452       }

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L440-L452

The RubiconMarket only verifies that the message sender is the owner:

File: contracts/RubiconMarket.sol   #2

215       modifier can_cancel(uint256 id) virtual {
216           require(isActive(id));
217           require(getOwner(id) == msg.sender);
218           _;
219       }

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L215-L219

which will always be the RubiconRouter for orders placed through the RubiconRouter:

File: contracts/RubiconMarket.sol   #3

391       /// @notice Key function to make a new offer. Takes funds from the caller into market escrow.
392       function offer(
393           uint256 pay_amt,
394           ERC20 pay_gem,
395           uint256 buy_amt,
396           ERC20 buy_gem
397       ) public virtual can_offer synchronized returns (uint256 id) {
398           require(uint128(pay_amt) == pay_amt);
399           require(uint128(buy_amt) == buy_amt);
400           require(pay_amt > 0);
401           require(pay_gem != ERC20(0x0));
402           require(buy_amt > 0);
403           require(buy_gem != ERC20(0x0));
404           require(pay_gem != buy_gem);
405   
406           OfferInfo memory info;
407           info.pay_amt = pay_amt;
408           info.pay_gem = pay_gem;
409           info.buy_amt = buy_amt;
410           info.buy_gem = buy_gem;
411           info.owner = msg.sender;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L391-L411

Tools Used

Code inspection

Maintain a mapping from submitters to order IDs, inside RubiconRouter, and validate during cancel

#0 - bghughes

2022-06-04T00:44:17Z

Not yet used in production, good issue!

#1 - HickupHH3

2022-06-18T07:14:01Z

duplicate of #17

Findings Information

🌟 Selected for report: kenzo

Also found by: IllIllI, PP1004, blackscale, hansfriese

Labels

bug
duplicate
3 (High Risk)
sponsor confirmed

Awards

390.3586 USDC - $390.36

External Links

Lines of code

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L282 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L171-L177

Vulnerability details

Impact

Extra funds are stolen from the user, and become locked in the RubiconRouter for others to steal

Proof of Concept

Most of the other functions of the RubiconRouter handle fees by taking the amount being provided, using a percentage as the fee, then adding that to the total. swapEntireBalance() (and also getExpectedSwapFill()) use the user's total balance and subtract the fee:

File: contracts/RubiconRouter.sol   #1

282                   maxAmount.sub(buy_amt_min.mul(expectedMarketFeeBPS).div(10000)), //account for fee

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L282

File: contracts/RubiconRouter.sol   #2

171               uint256 _pay = i == 0
172                   ? pay_amt
173                   : (
174                       currentAmount.sub(
175                           currentAmount.mul(expectedMarketFeeBPS).div(10000)
176                       )
177                   );

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L171-L177

If a user has a balance of $1000 and the fee is 1%, to consume the total funds $990.0990099 should be used as the payment, and $9.90099009 as the fee since those two add up to ~$1,000. The code however uses $10 as the fee and $990 as the fee. When amounts are large, this difference can be significant

Tools Used

Code inspection

Divide buy_amt_min by the sum of one plus the fee decimal percentage, in order to get the correct fee

#0 - HickupHH3

2022-06-16T14:45:02Z

duplicate of #104

Findings Information

🌟 Selected for report: Ruhum

Also found by: IllIllI, berndartmueller, blackscale, eccentricexit, hansfriese

Labels

bug
duplicate
3 (High Risk)

Awards

292.7689 USDC - $292.77

External Links

Lines of code

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L267-L282

Vulnerability details

Impact

Users can either be under-charged or over-charged fees if the decimals of the two tokens don't match. This can also lead to the full input token amount being taken as a fee.

Proof of Concept

swapEntireBalance() charges a fee based on buy_amt_min, which is the number of tokens the user expects to receive. All other functions charge the fee on the input token amount, not the output token amount.

File: contracts/RubiconRouter.sol   #1

267       function swapEntireBalance(
268           uint256 buy_amt_min,
269           address[] calldata route, // First address is what is being payed, Last address is what is being bought
270           uint256 expectedMarketFeeBPS
271       ) external returns (uint256) {
272           //swaps msg.sender entire balance in the trade
273           uint256 maxAmount = ERC20(route[0]).balanceOf(msg.sender);
274           ERC20(route[0]).transferFrom(
275               msg.sender,
276               address(this),
277               maxAmount // Account for expected fee
278           );
279           return
280               _swap(
281                   maxAmount,
282                   maxAmount.sub(buy_amt_min.mul(expectedMarketFeeBPS).div(10000)), //account for fee

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L267-L282

Consider the case where a user wants to use USDC (6 decimals) to buy 1 WETH (18 decimals) with a .02% fee

~6,000 USDC = 1 WETH 6000000000(balanceOf(msg.sender)) = 1000000000000000000(buy_amt_min) 1000000000000000000 * 0.0002 = 200000000000000 Fee: 200,000,000 USDC

It's possible to fiddle with the numbers to make the fee eat up exactly the user's balance whenever the output token has more decimals than the input token

Tools Used

Code inspection

Charge the fee based on maxAmount instead of buy_amt_min

#0 - bghughes

2022-06-04T21:36:01Z

Duplicate of #248

#1 - HickupHH3

2022-06-16T14:48:26Z

it's a closer duplicate of #52.

Findings Information

Awards

8.2687 USDC - $8.27

Labels

bug
duplicate
2 (Med Risk)

External Links

Lines of code

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L451 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L491

Vulnerability details

Impact

The use of payable.transfer() is heavily frowned upon because it can lead to the locking of funds. The transfer() call requires that the recipient has a payable callback, only provides 2300 gas for its operation. This means the following cases can cause the transfer to fail:

  • The contract does not have a payable callback
  • The contract's payable callback spends more than 2300 gas (which is only enough to emit something)
  • The contract is called through a proxy which itself uses up the 2300 gas

If a user falls into one of the above categories, they'll be unable to receive funds from the pool, or cancel Ether orders, and the deposited funds will be locked forever

Proof of Concept

File: contracts/RubiconRouter.sol   #1

451           msg.sender.transfer(pay_amt);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L451

File: contracts/RubiconRouter.sol   #2

491           msg.sender.transfer(withdrawnWETH);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L491

Note that there are other instances of payable.transfer(), but these are the only ones that lock user funds

Tools Used

Code inspection

Use address.call{value:x}() instead

#0 - bghughes

2022-06-07T22:30:22Z

Duplicate of #82

Lines of code

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L194-L206 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L267-L278 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L361-L366 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L412-L419

Vulnerability details

Impact

Users that have provided too many tokens in prior transactions have their excess funds (now latent) stolen, so they can't be returned by an admin

There are already latent funds in the contract, so latent funds are possible: https://optimistic.etherscan.io/address/0x7Af14ADc8Aea70f063c7eA3B2C1AD0D7A59C4bFf

Proof of Concept

Many of the functions in RubiconRouter don't check the return value of transferFrom(). There are tokens that do not revert on failure, and instead return an error status. Without having a require() to check the error status, the code allows to continue on to the RubiconMarket, where the funds are withdrawn from the router, and there the status is properly checked:

File: contracts/RubiconRouter.sol   #1

194       function swap(
195           uint256 pay_amt,
196           uint256 buy_amt_min,
197           address[] calldata route, // First address is what is being payed, Last address is what is being bought
198           uint256 expectedMarketFeeBPS //20
199       ) public returns (uint256) {
200           //**User must approve this contract first**
201           //transfer needed amount here first
202           ERC20(route[0]).transferFrom(
203               msg.sender,
204               address(this),
205               pay_amt.add(pay_amt.mul(expectedMarketFeeBPS).div(10000)) // Account for expected fee
206           );

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L194-L206

By constructing a multi-step transaction, a user can claim latent funds in a transaction of their own by doing the following:

  1. identify tokens that do not revert on failure, and have a balance on the RubiconRouter
  2. calculate the total funds needed for a legitimate transaction involving one of the above tokens
  3. subtract the latent funds from the amount above
  4. transfer exactly that smaller amount to the RubiconRouter
  5. set approvals to zero for the RubiconRouter contract, so it can't pull more funds
  6. execute a swap()

There are a bunch of other functions that have the same issue:

File: contracts/RubiconRouter.sol   #2

267       function swapEntireBalance(
268           uint256 buy_amt_min,
269           address[] calldata route, // First address is what is being payed, Last address is what is being bought
270           uint256 expectedMarketFeeBPS
271       ) external returns (uint256) {
272           //swaps msg.sender entire balance in the trade
273           uint256 maxAmount = ERC20(route[0]).balanceOf(msg.sender);
274           ERC20(route[0]).transferFrom(
275               msg.sender,
276               address(this),
277               maxAmount // Account for expected fee
278           );

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L267-L278

File: contracts/RubiconRouter.sol   #3

361       function buyAllAmountForETH(
362           uint256 buy_amt,
363           ERC20 pay_gem,
364           uint256 max_fill_amount
365       ) external returns (uint256 fill) {
366           ERC20(pay_gem).transferFrom(msg.sender, address(this), max_fill_amount); //transfer pay here

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L361-L366

File: contracts/RubiconRouter.sol   #4

361       function buyAllAmountForETH(
362           uint256 buy_amt,
363           ERC20 pay_gem,
364           uint256 max_fill_amount
365       ) external returns (uint256 fill) {
366           ERC20(pay_gem).transferFrom(msg.sender, address(this), max_fill_amount); //transfer pay here

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L361-L366

File: contracts/RubiconRouter.sol   #5

412       function offerForETH(
413           uint256 pay_amt, //maker (ask) sell how much
414           ERC20 pay_gem, //maker (ask) sell which token
415           uint256 buy_amt, //maker (ask) buy how much
416           // ERC20 nativeETH, //maker (ask) buy which token
417           uint256 pos //position to insert offer, 0 should be used if unknown
418       ) external returns (uint256) {
419           ERC20(pay_gem).transferFrom(msg.sender, address(this), pay_amt);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L412-L419

See this similar issue that was found to be of Medium risk

Tools Used

Code inspection

require() that the transferFrom() functions return success

#0 - bghughes

2022-06-04T21:06:20Z

Duplicate of #316

Findings Information

Labels

bug
duplicate
2 (Med Risk)

Awards

16.2035 USDC - $16.20

External Links

Lines of code

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L454-L462 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L494-L505 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L519-L549

Vulnerability details

Impact

Users send more funds than are needed, and the excess gets locked in the contract

This is not hypothetical - there are already funds from six separate tokens stuck in the contract: https://optimistic.etherscan.io/address/0x7Af14ADc8Aea70f063c7eA3B2C1AD0D7A59C4bFf

Proof of Concept

Some functions unnecessarily allow sending more than the requested amount:

File: contracts/RubiconRouter.sol   #1

454       // Deposit native ETH -> WETH pool
455       function depositWithETH(uint256 amount, address targetPool)
456           external
457           payable
458           returns (uint256 newShares)
459       {
460           IERC20 target = IBathToken(targetPool).underlyingToken();
461           require(target == ERC20(wethAddress), "target pool not weth pool");
462           require(msg.value >= amount, "didnt send enough eth");

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L454-L462

File: contracts/RubiconRouter.sol   #2

494       function swapWithETH(
495           uint256 pay_amt,
496           uint256 buy_amt_min,
497           address[] calldata route, // First address is what is being payed, Last address is what is being bought
498           uint256 expectedMarketFeeBPS
499       ) external payable returns (uint256) {
500           require(route[0] == wethAddress, "Initial value in path not WETH");
501           uint256 amtWithFee = pay_amt.add(
502               pay_amt.mul(expectedMarketFeeBPS).div(10000)
503           );
504           require(
505               msg.value >= amtWithFee,

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L494-L505

And the functions that charge fees don't verify that the fee value is the actual fee value in use, and thus may send extra funds. Here's one example of quite a few:

File: contracts/RubiconRouter.sol   #3

519       function swapForETH(
520           uint256 pay_amt,
521           uint256 buy_amt_min,
522           address[] calldata route, // First address is what is being payed, Last address is what is being bought
523           uint256 expectedMarketFeeBPS
524       ) external payable returns (uint256 fill) {
525           require(
526               route[route.length - 1] == wethAddress,
527               "target of swap is not WETH"
528           );
529           //Transfer tokens here first and account for fee
530           require(
531               ERC20(route[0]).transferFrom(
532                   msg.sender,
533                   address(this),
534                   pay_amt.add(pay_amt.mul(expectedMarketFeeBPS).div(10000))
535               ),
536               "initial ERC20 transfer failed"
537           );
538           fill = _swap(
539               pay_amt,
540               buy_amt_min,
541               route,
542               expectedMarketFeeBPS,
543               address(this)
544           );
545   
546           WETH9(wethAddress).withdraw(fill);
547           // msg.sender.transfer(fill);
548           msg.sender.transfer(fill);
549       }

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L519-L549

Swaps may also not consume all of the input tokens, so those remainders should be refunded

See this similar prior issue that was found to be of Medium risk

Tools Used

Code inspection

Reject transactions where msg.value doesn't equal the amount, and look up the actual fee and use that if it's less than expectedMarketFeeBPS. Also, refund any unused funds for transactions that don't consume the full input tokens.

#0 - KenzoAgada

2022-06-04T15:32:16Z

Duplicate of #15. The warden also briefly mentions the separate issue that swaps with ERC20 tokens might not consume all funds so remainder needs to be refunded - judge will need to decide whether that should be separated to a separate issue.

#1 - bghughes

2022-06-04T22:34:31Z

Duplicate of #15

Findings Information

🌟 Selected for report: xiaoming90

Also found by: GimelSec, IllIllI, MaratCerby, PP1004, WatchPug, berndartmueller, blockdev, ilan

Labels

bug
duplicate
2 (Med Risk)
sponsor disputed

Awards

42.6857 USDC - $42.69

External Links

Lines of code

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L251 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L303 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L320 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L348 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L353-L356 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L357

Vulnerability details

Impact

Users will lose funds if fee-on-transfer tokens are used in a withdrawal step, after having been deposited when there was no fee

Proof of Concept

The Rubicon code does not verify that the balances available match the stored amounts, and uses intermediate contracts in hops during transfers. Each hop may incur a fee, which will change the available balance. For both the RubiconRouter and the BathToken, funds are transferred from the RubiconMarket to those contracts before being withdrawn. If there is a fee, the amount stored will not match the amount available, and the calls will revert, locking the user's funds.

File: contracts/RubiconRouter.sol   #1

251               ERC20(route[route.length - 1]).transfer(to, currentAmount);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L251

File: contracts/RubiconRouter.sol   #2

303           ERC20(buy_gem).transfer(msg.sender, fill);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L303

File: contracts/RubiconRouter.sol   #3

320           ERC20(buy_gem).transfer(msg.sender, fill);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L320

File: contracts/RubiconRouter.sol   #4

348           ERC20(buy_gem).transfer(msg.sender, buy_amt);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L348

File: contracts/rubiconPools/BathToken.sol   #5

353           IERC20(filledAssetToRebalance).transfer(
354               destination,
355               rebalAmt.sub(stratReward)
356           );

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L353-L356

File: contracts/rubiconPools/BathToken.sol   #6

357           IERC20(filledAssetToRebalance).transfer(msg.sender, stratReward);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L357

Tools Used

Code inspection

Measure the balance before and after transfers, and use the difference as the amount, rather than the stated amount

#0 - bghughes

2022-06-03T23:02:14Z

Global risk to any ERC-20 by this logic

#1 - HickupHH3

2022-06-23T15:45:38Z

grouping it with the FoT / deflationary issue: #112

Findings Information

🌟 Selected for report: kenzo

Also found by: 0x1f8b, 0xsomeone, Dravee, IllIllI, MaratCerby, berndartmueller, cryptphi, xiaoming90

Labels

bug
duplicate
2 (Med Risk)
upgraded by judge

Awards

42.6857 USDC - $42.69

External Links

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

#0 - HickupHH3

2022-06-27T13:51:49Z

  1. A second call to the function may revert Some ERC20 tokens, such as Tether, revert() if approve() is called a second time without having called approve(0) first dup of #100

Findings Information

🌟 Selected for report: 0x1f8b

Also found by: CertoraInc, IllIllI, rotcivegaf, unforgiven

Labels

bug
duplicate
2 (Med Risk)
upgraded by judge

Awards

87.8307 USDC - $87.83

External Links

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

#0 - HickupHH3

2022-06-27T13:47:17Z

  1. Incorrect implementation of DOMAIN_SEPARATOR I confirmed with the sponsor that the v1 being appended after the separtor was constructed was an attempt to help with future upgrades. However, the purpose of the DOMAIN_SEPARATOR is to actually ensure that signatures pre-upgrade are invalid post-upgrade. Users shouldn't be able to double spend - once on the old contract, and once on the new one. It looks like this behavior was done in the previous version of Rubicon as well. While no funds are at risk because the separator is used with a nonce that is currently kept at its same storage slot, there's nothing guaranteeing this for future upgrades, so it's best to fix the separator to use the actual token name.

dup of #38

Findings Information

🌟 Selected for report: IllIllI

Also found by: 0xDjango, Dravee, SmartSek, StErMi, oyc_109, pauliax, rotcivegaf, sashik_eth, shenwilly, xiaoming90

Labels

bug
2 (Med Risk)
sponsor acknowledged

Awards

23.3384 USDC - $23.34

External Links

Lines of code

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L216-L229 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L334-L337 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L269-L272

Vulnerability details

Impact

There are multiple functions that the admin can call to brick various parts of the system, leading to user's funds being locked

Even if the owner is benevolent the fact that there is a rug vector available may negatively impact the protocol's reputation. See this example where a similar finding has been flagged as a high-severity issue. I've downgraded these instances to be a medium since it requires cooperation of the admin.

Proof of Concept

Here are some examples: Overwrite state:

File: contracts/rubiconPools/BathHouse.sol   #1

216       /// @notice A migration function that allows the admin to write arbitrarily to tokenToBathToken
217       function adminWriteBathToken(ERC20 overwriteERC20, address newBathToken)
218           external
219           onlyAdmin
220       {
221           tokenToBathToken[address(overwriteERC20)] = newBathToken;
222           emit LogNewBathToken(
223               address(overwriteERC20),
224               newBathToken,
225               address(0),
226               block.timestamp,
227               msg.sender
228           );
229       }

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L216-L229

Steal new funds with new malicious market

File: contracts/rubiconPools/BathHouse.sol   #2

334       /// @notice Admin-only function to set a Bath Token's target Rubicon Market
335       function setMarket(address newMarket) external onlyAdmin {
336           RubiconMarketAddress = newMarket;
337       }

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L334-L337

Add a ton of bonus tokens, making withdrawals break because of unbounded for-loop

File: contracts/rubiconPools/BathToken.sol   #3

269       /// @notice Admin-only function to add a bonus token to bonusTokens for pool incentives
270       function setBonusToken(address newBonusERC20) external onlyBathHouse {
271           bonusTokens.push(newBonusERC20);
272       }

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L269-L272

Tools Used

Code inspection

Validate input arguments and require specific upgrade paths for each contract, not just allowing the admin to set whatever they decide, add limit to number of bonus tokens

#0 - bghughes

2022-06-03T20:49:57Z

Centralization risk is acknowledged. Duplicate of #344

Findings Information

🌟 Selected for report: PP1004

Also found by: IllIllI

Labels

bug
duplicate
2 (Med Risk)

Awards

401.6035 USDC - $401.60

External Links

Lines of code

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L290-L297 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L307-L316

Vulnerability details

Impact

Availability of the protocol is impacted. Also, customers waste gas trying to call functions that don't work

Proof of Concept

The two functions below call balanceOf() on the msg.sender, but the RubiconMarket pulls funds from the caller, which is the RubiconRouter, not msg.sender, so the amount will never be available

File: contracts/RubiconRouter.sol   #1

290       function maxBuyAllAmount(
291           ERC20 buy_gem,
292           ERC20 pay_gem,
293           uint256 max_fill_amount
294       ) external returns (uint256 fill) {
295           //swaps msg.sender's entire balance in the trade
296           uint256 maxAmount = ERC20(buy_gem).balanceOf(msg.sender);
297           fill = RubiconMarket(RubiconMarketAddress).buyAllAmount(

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L290-L297

File: contracts/RubiconRouter.sol   #2

307       function maxSellAllAmount(
308           ERC20 pay_gem,
309           ERC20 buy_gem,
310           uint256 min_fill_amount
311       ) external returns (uint256 fill) {
312           //swaps msg.sender entire balance in the trade
313           uint256 maxAmount = ERC20(buy_gem).balanceOf(msg.sender);
314           fill = RubiconMarket(RubiconMarketAddress).sellAllAmount(
315               pay_gem,
316               maxAmount,

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L307-L316

Tools Used

Code inspection

Transfer funds from the user before calling buyAllAmount() or sellAllAmount()

#0 - bghughes

2022-06-04T21:31:01Z

Duplicate of #282

Summary

Low Risk Issues

IssueInstances
1Unsafe use of transfer()/transferFrom() with IERC207
2Return values of transfer()/transferFrom() not checked7
3Unused/empty receive()/fallback() function3
4Incorrect implementation of DOMAIN_SEPARATOR1
5Return unused fees5
6Migrations should do some validation1
7A second call to the function may revert1
8Front-runable initializer1
9Vulnerable to cross-chain replay attacks due to static DOMAIN_SEPARATOR1
10Contracts should extend interfaces they extend1
11NatSpec incorrect1
12Misleading function name1
13Missing checks for address(0x0) when assigning values to address state variables16

Total: 46 instances over 13 issues

Non-critical Issues

IssueInstances
1Consider addings checks for signature malleability1
2No use of two-phase ownership transfers1
3Return values of approve() not checked3
4Adding a return statement when the function defines a named return variable, is redundant2
5require()/revert() statements should have descriptive reason strings55
6public functions not called by the contract should be declared external instead26
7Non-assembly method available1
82**<n> - 1 should be re-written as type(uint<n>).max5
9type(uint<n>).max should be used instead of uint<n>(-1)1
10constants should be defined rather than using magic numbers43
11Redundant cast1
12Missing event for critical parameter change13
13Use a more recent version of solidity2
14Use a more recent version of solidity3
15Use scientific notation (e.g. 1e18) rather than exponentiation (e.g. 10**18)10
16Inconsistent spacing in comments127
17Variable names that consist of all capital letters should be reserved for const/immutable variables1
18Typos17
19NatSpec is incomplete1
20Event is missing indexed fields33
21Not using the named return variables anywhere in the function is confusing15

Total: 361 instances over 21 issues

Low Risk Issues

1. Unsafe use of transfer()/transferFrom() with IERC20

Some tokens do not implement the ERC20 standard properly but are still accepted by most code that accepts ERC20 tokens. For example Tether (USDT)'s transfer() and transferFrom() functions do not return booleans as the specification requires, and instead have no return value. When these sorts of tokens are cast to IERC20, their function signatures do not match and therefore the calls made, revert. Use OpenZeppelin’s SafeERC20's safeTransfer()/safeTransferFrom() instead

There are 7 instances of this issue:

File: contracts/rubiconPools/BathPair.sol

601:              IERC20(asset).transfer(msg.sender, booty);

615:              IERC20(quote).transfer(msg.sender, booty);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L601

File: contracts/rubiconPools/BathToken.sol

353           IERC20(filledAssetToRebalance).transfer(
354               destination,
355               rebalAmt.sub(stratReward)
356:          );

357:          IERC20(filledAssetToRebalance).transfer(msg.sender, stratReward);

565:          underlyingToken.transferFrom(msg.sender, address(this), assets);

602:              underlyingToken.transfer(feeTo, _fee);

605:          underlyingToken.transfer(receiver, amountWithdrawn);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L353-L356

2. Return values of transfer()/transferFrom() not checked

Not all IERC20 implementations revert() when there's a failure in transfer()/transferFrom(). The function signature has a boolean return value and they indicate errors that way instead. By not checking the return value, operations that should have marked as failed, may potentially go through without actually making a payment

There are 7 instances of this issue:

File: contracts/rubiconPools/BathPair.sol

601:              IERC20(asset).transfer(msg.sender, booty);

615:              IERC20(quote).transfer(msg.sender, booty);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L601

File: contracts/rubiconPools/BathToken.sol

353           IERC20(filledAssetToRebalance).transfer(
354               destination,
355               rebalAmt.sub(stratReward)
356:          );

357:          IERC20(filledAssetToRebalance).transfer(msg.sender, stratReward);

565:          underlyingToken.transferFrom(msg.sender, address(this), assets);

602:              underlyingToken.transfer(feeTo, _fee);

605:          underlyingToken.transfer(receiver, amountWithdrawn);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L353-L356

3. Unused/empty receive()/fallback() function

If the intention is for the Ether to be used, the function should call another function, otherwise it should revert (e.g. require(msg.sender == address(weth)))

There are 3 instances of this issue:

File: contracts/peripheral_contracts/BathBuddy.sol   #1

69:       receive() external payable {}

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L69

File: contracts/RubiconRouter.sol   #2

37:       receive() external payable {}

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L37

File: contracts/RubiconRouter.sol   #3

39:       fallback() external payable {}

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L39

4. Incorrect implementation of DOMAIN_SEPARATOR

I confirmed with the sponsor that the v1 being appended after the separtor was constructed was an attempt to help with future upgrades. However, the purpose of the DOMAIN_SEPARATOR is to actually ensure that signatures pre-upgrade are invalid post-upgrade. Users shouldn't be able to double spend - once on the old contract, and once on the new one. It looks like this behavior was done in the previous version of Rubicon as well. While no funds are at risk because the separator is used with a nonce that is currently kept at its same storage slot, there's nothing guaranteeing this for future upgrades, so it's best to fix the separator to use the actual token name.

There is 1 instance of this issue:

File: contracts/rubiconPools/BathToken.sol   #1

199          DOMAIN_SEPARATOR = keccak256(
200              abi.encode(
201                  keccak256(
202                      "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
203                  ),
204                  keccak256(bytes(name)),
205                  keccak256(bytes("1")),
206                  chainId,
207                  address(this)
208              )
209          );
210:         name = string(abi.encodePacked(_symbol, (" v1")));

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L199-L210

5. Return unused fees

The expectedMarketFeeBPS argument to a bunch of functions is a user projection. The actual fee is a static value set by the admin, not a dynamic one, so the difference between the two values should be returned, as is done for some of the other token transfers in this contract.

There are 5 instances of this issue:

File: contracts/RubiconRouter.sol

194      function swap(
195          uint256 pay_amt,
196          uint256 buy_amt_min,
197          address[] calldata route, // First address is what is being payed, Last address is what is being bought
198          uint256 expectedMarketFeeBPS //20
199:     ) public returns (uint256) {

267      function swapEntireBalance(
268          uint256 buy_amt_min,
269          address[] calldata route, // First address is what is being payed, Last address is what is being bought
270          uint256 expectedMarketFeeBPS
271:     ) external returns (uint256) {

325      function buyAllAmountWithETH(
326          ERC20 buy_gem,
327          uint256 buy_amt,
328          uint256 max_fill_amount,
329          uint256 expectedMarketFeeBPS
330:     ) external payable returns (uint256 fill) {

494      function swapWithETH(
495          uint256 pay_amt,
496          uint256 buy_amt_min,
497          address[] calldata route, // First address is what is being payed, Last address is what is being bought
498          uint256 expectedMarketFeeBPS
499:     ) external payable returns (uint256) {

519      function swapForETH(
520          uint256 pay_amt,
521          uint256 buy_amt_min,
522          address[] calldata route, // First address is what is being payed, Last address is what is being bought
523          uint256 expectedMarketFeeBPS
524:     ) external payable returns (uint256 fill) {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L194-L199

6. Migrations should do some validation

At the very least, a new bath token should have the same underlying to make sure funds can be taken out

There is 1 instance of this issue:

File: contracts/rubiconPools/BathHouse.sol   #1

216      /// @notice A migration function that allows the admin to write arbitrarily to tokenToBathToken
217      function adminWriteBathToken(ERC20 overwriteERC20, address newBathToken)
218          external
219          onlyAdmin
220      {
221          tokenToBathToken[address(overwriteERC20)] = newBathToken;
222          emit LogNewBathToken(
223              address(overwriteERC20),
224              newBathToken,
225              address(0),
226              block.timestamp,
227              msg.sender
228          );
229:     }

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L216-L229

7. A second call to the function may revert

Some ERC20 tokens, such as Tether, revert() if approve() is called a second time without having called approve(0) first

There is 1 instance of this issue:

File: contracts/RubiconRouter.sol   #1

157:         ERC20(toApprove).approve(RubiconMarketAddress, 2**256 - 1);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L157

8. Front-runable initializer

There is nothing preventing another account from calling the initializer before the contract owner. In the best case, the owner is forced to waste gas and re-deploy. In the worst case, the owner does not notice that his/her call reverts, and everyone starts using a contract under the control of an attacker

There is 1 instance of this issue:

File: contracts/RubiconRouter.sol   #1

41:      function startErUp(address _theTrap, address payable _weth) external {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L41

9. Vulnerable to cross-chain replay attacks due to static DOMAIN_SEPARATOR

See this issue from a prior contest for details

There is 1 instance of this issue:

File: contracts/rubiconPools/BathToken.sol   #1

199          DOMAIN_SEPARATOR = keccak256(
200              abi.encode(
201                  keccak256(
202                      "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
203                  ),
204                  keccak256(bytes(name)),
205                  keccak256(bytes("1")),
206                  chainId,
207                  address(this)
208:             )

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L199-L208

10. Contracts should extend interfaces they extend

Extending an interface ensures that all function signatures are correct, and catches mistakes introduced (e.g. through errant keystrokes)

There is 1 instance of this issue:

File: contracts/rubiconPools/BathToken.sol   #1

387:     /// @notice * EIP 4626 *

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L387

11. NatSpec incorrect

The NatSpec does not match what is being done by the function

There is 1 instance of this issue:

File: contracts/rubiconPools/BathHouse.sol   #1

285:     /// @notice Admin-only function to set a Bath Token's timeDelay

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L285

12. Misleading function name

setBonusToken() is actually appending a new bonus token to the previous list of bonus tokens. The function name should reflect the action it's taking, or else reviewers and users will get confused about what the code is actually doing

There is 1 instance of this issue:

File: contracts/rubiconPools/BathToken.sol   #1

269      /// @notice Admin-only function to add a bonus token to bonusTokens for pool incentives
270      function setBonusToken(address newBonusERC20) external onlyBathHouse {
271          bonusTokens.push(newBonusERC20);
272:     }

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L269-L272

13. Missing checks for address(0x0) when assigning values to address state variables

There are 16 instances of this issue:

File: contracts/RubiconMarket.sol

556:          feeTo = _feeTo;

1252:         AqueductAddress = _Aqueduct;

1257:         feeTo = newFeeTo;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L556

File: contracts/rubiconPools/BathHouse.sol

119:          RubiconMarketAddress = market;

121:          newBathTokenImplementation = _newBathTokenImplementation;

122:          proxyManager = _proxyAdmin;

249:          approvedPairContract = newPair;

254:          admin = newAdmin;

260:          newBathTokenImplementation = newImplementation;

336:          RubiconMarketAddress = newMarket;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L119

File: contracts/rubiconPools/BathPair.sol

126:          bathHouse = _bathHouse;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L126

File: contracts/rubiconPools/BathToken.sol

192:          RubiconMarketAddress = market;

246:          RubiconMarketAddress = newRubiconMarket;

251:          bathHouse = newBathHouse;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L192

File: contracts/RubiconRouter.sol

43:           RubiconMarketAddress = _theTrap;

44:           wethAddress = _weth;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L43

Non-critical Issues

1. Consider addings checks for signature malleability

Use OpenZeppelin's ECDSA contract rather than calling ecrecover() directly

There is 1 instance of this issue:

File: contracts/rubiconPools/BathToken.sol   #1

739:         address recoveredAddress = ecrecover(digest, v, r, s);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L739

2. No use of two-phase ownership transfers

Consider adding a two-phase transfer, where the current owner nominates the next owner, and the next owner has to call accept*() to become the new owner. This prevents passing the ownership to an account that is unable to use it.

There is 1 instance of this issue:

File: contracts/rubiconPools/BathHouse.sol   #1

253      function setBathHouseAdmin(address newAdmin) external onlyAdmin {
254          admin = newAdmin;
255:     }

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L253-L255

3. Return values of approve() not checked

Not all IERC20 implementations revert() when there's a failure in approve(). The function signature has a boolean return value and they indicate errors that way instead. By not checking the return value, operations that should have marked as failed, may potentially go through without actually approving anything

There are 3 instances of this issue:

File: contracts/rubiconPools/BathToken.sol   #1

214:          IERC20(address(token)).approve(RubiconMarketAddress, 2**256 - 1);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L214

File: contracts/rubiconPools/BathToken.sol   #2

256:          underlyingToken.approve(RubiconMarketAddress, 2**256 - 1);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L256

File: contracts/RubiconRouter.sol   #3

465:              target.approve(targetPool, amount);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L465

4. Adding a return statement when the function defines a named return variable, is redundant

There are 2 instances of this issue:

File: contracts/rubiconPools/BathPair.sol   #1

315:                  return _index;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L315

File: contracts/RubiconRouter.sol   #2

379:          return fill;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L379

5. require()/revert() statements should have descriptive reason strings

There are 55 instances of this issue:

File: contracts/RubiconMarket.sol

210:          require(isActive(id));

216:          require(isActive(id));

217:          require(getOwner(id) == msg.sender);

226:          require(!locked);

361:          require(_offer.pay_gem.transfer(_offer.owner, _offer.pay_amt));

379:          require(cancel(uint256(id)));

398:          require(uint128(pay_amt) == pay_amt);

399:          require(uint128(buy_amt) == buy_amt);

400:          require(pay_amt > 0);

401:          require(pay_gem != ERC20(0x0));

402:          require(buy_amt > 0);

403:          require(buy_gem != ERC20(0x0));

404:          require(pay_gem != buy_gem);

416:          require(pay_gem.transferFrom(msg.sender, address(this), pay_amt));

432:          require(buy(uint256(id), maxTakeAmount));

453:          require(!isClosed());

459:          require(isActive(id));

460:          require(!isClosed());

466:          require(isActive(id));

467:          require((msg.sender == getOwner(id)) || isClosed());

591:          require(buy(uint256(id), maxTakeAmount));

595:          require(cancel(uint256(id)));

646:          require(_dust[address(pay_gem)] <= pay_amt);

687:                  require(_unsort(id));

689:                  require(_hide(id));

702:          require(!isOfferSorted(id)); //make sure offers[id] is not yet sorted

703:          require(isActive(id)); //make sure offers[id] is active

715           require(
716               !isActive(id) &&
717                   _rank[id].delb != 0 &&
718                   _rank[id].delb < block.number - 10
719:          );

840:              require(offerId != 0); //Fails if there are not more offers

865:          require(fill_amt >= min_fill_amount);

879:              require(offerId != 0);

906:          require(fill_amt <= max_fill_amount);

921:                  require(offerId != 0); //Fails if there are not enough offers to complete

945:                  require(offerId != 0); //Fails if there are not enough offers to complete

960:          require(buyEnabled);

970:          require(super.buy(id, amount));

985:          require(id > 0);

1002:         require(id > 0);

1119:         require(_dust[address(pay_gem)] <= pay_amt);

1131:         require(isActive(id));

1175:         require(_span[pay_gem][buy_gem] > 0);

1177          require(
1178              _rank[id].delb == 0 && //assert id is in the sorted list
1179                  isOfferSorted(id)
1180:         );

1184:             require(_rank[_rank[id].next].prev == id);

1193:             require(_rank[_rank[id].prev].next == id);

1209:         require(!isOfferSorted(id)); //make sure offer id is not in sorted offers list

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L210

File: contracts/rubiconPools/BathHouse.sol

88:           require(msg.sender == admin);

104:          require(!initialized);

110:          require(_reserveRatio <= 100);

111:          require(_reserveRatio > 0);

280:          require(rr <= 100);

281:          require(rr > 0);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L88

File: contracts/rubiconPools/BathPair.sol

118:          require(!initialized);

143:          require(msg.sender == bathHouse);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L118

File: contracts/rubiconPools/BathToken.sol

186:          require(!initialized);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L186

File: contracts/RubiconRouter.sol

42:           require(!started);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L42

6. public functions not called by the contract should be declared external instead

Contracts are allowed to override their parents' functions and change the visibility from external to public.

There are 26 instances of this issue:

File: contracts/RubiconMarket.sol

240       function getOffer(uint256 id)
241           public
242           view
243           returns (
244               uint256,
245               ERC20,
246               uint256,
247:              ERC20

475:      function getTime() public view returns (uint64) {

551:      function initialize(bool _live, address _feeTo) public {

697       function insert(
698           uint256 id, //maker (ask) id
699           uint256 pos //position to insert into
700:      ) public returns (bool) {

798       function getOfferCount(ERC20 sell_gem, ERC20 buy_gem)
799           public
800           view
801:          returns (uint256)

811:      function getFirstUnsortedOffer() public view returns (uint256) {

817:      function getNextUnsortedOffer(uint256 id) public view returns (uint256) {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L240-L247

File: contracts/rubiconPools/BathHouse.sol

359:      function getBPSToStrats() public view returns (uint8) {

382:      function isApprovedPair(address pair) public view returns (bool outcome) {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L359

File: contracts/rubiconPools/BathPair.sol

631       function getOutstandingStrategistTrades(
632           address asset,
633           address quote,
634           address strategist
635:      ) public view returns (uint256[] memory) {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L631-L635

File: contracts/rubiconPools/BathToken.sol

383:      function asset() public view returns (address assetTokenAddress) {

416       function maxDeposit(address receiver)
417           public
418           pure
419:          returns (uint256 maxAssets)

425       function previewDeposit(uint256 assets)
426           public
427           view
428:          returns (uint256 shares)

435:      function deposit(uint256 assets) public returns (uint256 shares) {

441       function deposit(uint256 assets, address receiver)
442           public
443:          returns (uint256 shares)

450:      function maxMint(address receiver) public pure returns (uint256 maxShares) {

464       function mint(uint256 shares, address receiver)
465           public
466:          returns (uint256 assets)

475       function maxWithdraw(address owner)
476           public
477           view
478:          returns (uint256 maxAssets)

505       function withdraw(
506           uint256 assets,
507           address receiver,
508           address owner
509:      ) public returns (uint256 shares) {

525:      function maxRedeem(address owner) public view returns (uint256 maxShares) {

531       function previewRedeem(uint256 shares)
532           public
533           view
534:          returns (uint256 assets)

542       function redeem(
543           uint256 shares,
544           address receiver,
545           address owner
546:      ) public returns (uint256 assets) {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L383

File: contracts/RubiconRouter.sol

55        function getBookFromPair(
56            ERC20 asset,
57            ERC20 quote,
58            uint256 topNOrders
59        )
60            public
61            view
62            returns (
63                uint256[3][] memory,
64                uint256[3][] memory,
65:               uint256

129       function getBestOfferAndInfo(address asset, address quote)
130           public
131           view
132           returns (
133               uint256, //id
134               uint256,
135               ERC20,
136               uint256,
137:              ERC20

161       function getExpectedSwapFill(
162           uint256 pay_amt,
163           uint256 buy_amt_min,
164           address[] calldata route, // First address is what is being payed, Last address is what is being bought
165           uint256 expectedMarketFeeBPS //20
166:      ) public view returns (uint256 fill_amt) {

194       function swap(
195           uint256 pay_amt,
196           uint256 buy_amt_min,
197           address[] calldata route, // First address is what is being payed, Last address is what is being bought
198           uint256 expectedMarketFeeBPS //20
199:      ) public returns (uint256) {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L55-L65

7. Non-assembly method available

assembly{ id := chainid() } => uint256 id = block.chainid;

There is 1 instance of this issue:

File: contracts/rubiconPools/BathToken.sol   #1

197:              chainId := chainid()

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L197

8. 2**<n> - 1 should be re-written as type(uint<n>).max

Earlier versions of solidity can use uint<n>(-1) instead. Expressions not including the - 1 can often be re-written to accomodate the change (e.g. by using a > rather than a >=, which will also save some gas)

There are 5 instances of this issue:

File: contracts/rubiconPools/BathToken.sol

214:          IERC20(address(token)).approve(RubiconMarketAddress, 2**256 - 1);

256:          underlyingToken.approve(RubiconMarketAddress, 2**256 - 1);

421:          maxAssets = 2**256 - 1; // No limit on deposits in current implementation  = Max UINT

451:          maxShares = 2**256 - 1; // No limit on shares that could be created via deposit in current implementation - Max UINT

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L214

File: contracts/RubiconRouter.sol

157:          ERC20(toApprove).approve(RubiconMarketAddress, 2**256 - 1);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L157

9. type(uint<n>).max should be used instead of uint<n>(-1)

There is 1 instance of this issue:

File: contracts/rubiconPools/BathToken.sol   #1

703:          if (allowance[from][msg.sender] != uint256(-1)) {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L703

10. constants should be defined rather than using magic numbers

There are 43 instances of this issue:

File: contracts/RubiconMarket.sol

/// @audit 10000
296:          uint256 fee = mul(spend, feeBPS) / 10000;

/// @audit 0x0
401:          require(pay_gem != ERC20(0x0));

/// @audit 0x0
403:          require(buy_gem != ERC20(0x0));

/// @audit 20
562:          feeBPS = 20;

/// @audit 9
857:                      pay_amt * 10**9,

/// @audit 9
859:                  ) / 10**9;

/// @audit 9
898:                          buy_amt * 10**9,

/// @audit 9
900:                      ) / 10**9

/// @audit 9
927:                  pay_amt * 10**9,

/// @audit 9
929:              ) / 10**9

/// @audit 9
951:                  buy_amt * 10**9,

/// @audit 9
953:              ) / 10**9

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L296

File: contracts/rubiconPools/BathHouse.sol

/// @audit 100
110:          require(_reserveRatio <= 100);

/// @audit 20
116:          bpsToStrategists = 20;

/// @audit 100
280:          require(rr <= 100);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L110

File: contracts/rubiconPools/BathPair.sol

/// @audit 0x0000000000000000000000000000000000000000
122:                  address(0x0000000000000000000000000000000000000000) &&

/// @audit 100
179:              ).div(100) <= IERC20(underlyingAsset).balanceOf(bathAssetAddress),

/// @audit 100
187:              ).div(100) <= IERC20(underlyingQuote).balanceOf(bathQuoteAddress),

/// @audit 10000
556:              amount.sub((stratRewardBPS.mul(amount)).div(10000)),

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L122

File: contracts/rubiconPools/BathToken.sol

/// @audit 256
214:          IERC20(address(token)).approve(RubiconMarketAddress, 2**256 - 1);

/// @audit 3
218:          feeBPS = 3; //Fee set to 3 BPS initially

/// @audit 256
256:          underlyingToken.approve(RubiconMarketAddress, 2**256 - 1);

/// @audit 10000
352:          uint256 stratReward = (stratProportion.mul(rebalAmt)).div(10000);

/// @audit 256
421:          maxAssets = 2**256 - 1; // No limit on deposits in current implementation  = Max UINT

/// @audit 256
451:          maxShares = 2**256 - 1; // No limit on shares that could be created via deposit in current implementation - Max UINT

/// @audit 10000
498:              uint256 _fee = assets.mul(feeBPS).div(10000);

/// @audit 10000
537:          uint256 _fee = r.mul(feeBPS).div(10000);

/// @audit 10000
599:          uint256 _fee = r.mul(feeBPS).div(10000);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L214

File: contracts/peripheral_contracts/BathBuddy.sol

/// @audit 10000
106:              uint256 _fee = amount.mul(poolFee).div(10000);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L106

File: contracts/RubiconRouter.sol

/// @audit 3
63:               uint256[3][] memory,

/// @audit 3
64:               uint256[3][] memory,

/// @audit 3
68:           uint256[3][] memory asks = new uint256[3][](topNOrders);

/// @audit 3
68:           uint256[3][] memory asks = new uint256[3][](topNOrders);

/// @audit 3
69:           uint256[3][] memory bids = new uint256[3][](topNOrders);

/// @audit 3
69:           uint256[3][] memory bids = new uint256[3][](topNOrders);

/// @audit 256
157:          ERC20(toApprove).approve(RubiconMarketAddress, 2**256 - 1);

/// @audit 10000
175:                          currentAmount.mul(expectedMarketFeeBPS).div(10000)

/// @audit 10000
205:              pay_amt.add(pay_amt.mul(expectedMarketFeeBPS).div(10000)) // Account for expected fee

/// @audit 10000
233:                          currentAmount.mul(expectedMarketFeeBPS).div(10000)

/// @audit 10000
282:                  maxAmount.sub(buy_amt_min.mul(expectedMarketFeeBPS).div(10000)), //account for fee

/// @audit 10000
334:              max_fill_amount.mul(expectedMarketFeeBPS).div(10000)

/// @audit 10000
502:              pay_amt.mul(expectedMarketFeeBPS).div(10000)

/// @audit 10000
534:                  pay_amt.add(pay_amt.mul(expectedMarketFeeBPS).div(10000))

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L63

11. Redundant cast

The type of the variable is the same as the type to which the variable is being cast

There is 1 instance of this issue:

File: contracts/RubiconRouter.sol   #1

/// @audit address(wethAddress)
331:          address _weth = address(wethAddress);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L331

12. Missing event for critical parameter change

There are 13 instances of this issue:

File: contracts/RubiconMarket.sol

1231      function setFeeBPS(uint256 _newFeeBPS) external auth returns (bool) {
1232          feeBPS = _newFeeBPS;
1233          return true;
1234:     }

1237      function setAqueductDistributionLive(bool live)
1238          external
1239          auth
1240          returns (bool)
1241      {
1242          AqueductDistributionLive = live;
1243          return true;
1244:     }

1247      function setAqueductAddress(address _Aqueduct)
1248          external
1249          auth
1250          returns (bool)
1251      {
1252          AqueductAddress = _Aqueduct;
1253          return true;
1254:     }

1256      function setFeeTo(address newFeeTo) external auth returns (bool) {
1257          feeTo = newFeeTo;
1258          return true;
1259:     }

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L1231-L1234

File: contracts/rubiconPools/BathHouse.sol

253       function setBathHouseAdmin(address newAdmin) external onlyAdmin {
254           admin = newAdmin;
255:      }

259       function setNewBathTokenImplementation(address newImplementation) external onlyAdmin {
260           newBathTokenImplementation = newImplementation;
261:      }

269       function setPermissionedStrategists(bool _new) external onlyAdmin {
270           permissionedStrategists = _new;
271:      }

274       function setCancelTimeDelay(uint256 value) external onlyAdmin {
275           timeDelay = value;
276:      }

279       function setReserveRatio(uint256 rr) external onlyAdmin {
280           require(rr <= 100);
281           require(rr > 0);
282           reserveRatio = rr;
283:      }

335       function setMarket(address newMarket) external onlyAdmin {
336           RubiconMarketAddress = newMarket;
337:      }

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L253-L255

File: contracts/rubiconPools/BathToken.sol

245       function setMarket(address newRubiconMarket) external onlyBathHouse {
246           RubiconMarketAddress = newRubiconMarket;
247:      }

250       function setBathHouse(address newBathHouse) external onlyBathHouse {
251           bathHouse = newBathHouse;
252:      }

260       function setFeeBPS(uint256 _feeBPS) external onlyBathHouse {
261           feeBPS = _feeBPS;
262:      }

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L245-L247

13. Use a more recent version of solidity

Use a solidity version of at least 0.8.13 to get the ability to use using for with a list of free functions

There are 2 instances of this issue:

File: contracts/rubiconPools/BathPair.sol   #1

8:    pragma solidity =0.7.6;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L8

File: contracts/peripheral_contracts/BathBuddy.sol   #2

2:    pragma solidity >=0.6.0 <0.8.0;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L2

14. Use a more recent version of solidity

Use a solidity version of at least 0.8.4 to get bytes.concat() instead of abi.encodePacked(<bytes>,<bytes>) Use a solidity version of at least 0.8.12 to get string.concat() instead of abi.encodePacked(<str>,<str>)

There are 3 instances of this issue:

File: contracts/RubiconMarket.sol   #1

7:    pragma solidity =0.7.6;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L7

File: contracts/rubiconPools/BathToken.sol   #2

8:    pragma solidity =0.7.6;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L8

File: contracts/RubiconRouter.sol   #3

5:    pragma solidity =0.7.6;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L5

15. Use scientific notation (e.g. 1e18) rather than exponentiation (e.g. 10**18)

There are 10 instances of this issue:

File: contracts/RubiconMarket.sol

73:       uint256 constant WAD = 10**18;

74:       uint256 constant RAY = 10**27;

857:                      pay_amt * 10**9,

859:                  ) / 10**9;

898:                          buy_amt * 10**9,

900:                      ) / 10**9

927:                  pay_amt * 10**9,

929:              ) / 10**9

951:                  buy_amt * 10**9,

953:              ) / 10**9

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L73

16. Inconsistent spacing in comments

Some lines use // x and some use //x. The instances below point out the usages that don't follow the majority, within each file

There are 127 instances of this issue:

File: contracts/RubiconMarket.sol

188:      /// @dev The mapping that makes up the core orderbook of the exchange

193:      /// @dev This parameter is in basis points

196:      /// @dev This parameter provides the address to which fees are sent

208:      /// @notice Modifier that insures an order exists and is properly in the orderbook

214:      /// @notice Modifier that checks the user to make sure they own the offer and its valid before they attempt to cancel it

254:      /// @notice Below are the main public entrypoints

270:      /// @notice Accept a given `quantity` of an offer. Transfers funds from caller/taker to offer maker, and from market to caller/taker.

271:      /// @notice The fee for taker trades is paid in this function.

295:          // Fee logic added on taker trades

349:      /// @notice Allows the caller to cancel the offer if it is their own.

350:      /// @notice This function refunds the offer to the maker.

391:      /// @notice Key function to make a new offer. Takes funds from the caller into market escrow.

440:      // Fee logic

451:      /// @dev After close_time has been reached, no new offers are allowed.

457:      /// @dev After close, no new buys are allowed.

464:      /// @dev After close, anyone can cancel an offer.

529:      /// @dev Below is variable to allow for a proxy-friendly constructor

532:      /// @dev unused deprecated variable for applying a token distribution on top of a trade

534:      /// @dev unused deprecated variable for applying a token distribution of this token on top of a trade

548:      uint256 public dustId; // id of the latest offer marked as dust

550:      /// @dev Proxy-safe initialization of storage

555:          /// @notice The market fee recipient

561:          /// @notice The starting fee on taker trades in basis points

569:      // After close, anyone can cancel an offer

579:      // ---- Public entrypoints ---- //

598:      // Make a new offer. Takes funds from the caller into market escrow.

600:      // If matching is enabled:

601:      //     * creates new offer without putting it in

602:      //       the sorted list.

603:      //     * available to authorized contracts only!

604:      //     * keepers should call insert(id,pos)

605:      //       to put offer in the sorted list.

607:      // If matching is disabled:

608:      //     * calls expiring market's offer().

609:      //     * available to everyone without authorization.

610:      //     * no sorting is done.

626:      // Make a new offer. Takes funds from the caller into market escrow.

677:      // Cancel an offer. Refunds offer maker.

712:      //  Function should be called by keepers.

726:      //    Function is used to avoid "dust offers" that have

727:      //    very small amount of tokens to sell, and it would

728:      //    cost more gas to accept the offer, than the value

729:      //    of tokens received.

754:      //    If matchingEnabled true(default), then inserted offers are matched.

755:      //    Except the ones inserted by contracts, because those end up

756:      //    in the unsorted list of offers, that must be later sorted by

757:      //    keepers using insert().

758:      //    If matchingEnabled is false then RubiconMarket is reverted to ExpiringMarket,

759:      //    and matching is not done, and sorted lists are disabled.

771:      //      the best offer is the lowest one if it's an ask,

772:      //      and highest one if it's a bid offer

782:      //      the worse offer is the higher one if its an ask,

783:      //      a lower one if its a bid offer,

784:      //      and in both cases the newer one if they're equal.

790:      //      the better offer is in the lower priced one if its an ask,

791:      //      the next higher priced one if its a bid offer

792:      //      and in both cases the older one if they're equal.

807:      //      Contracts can't calculate the insertion position of their offer because it is not an O(1) operation.

808:      //      Their offers get put in the unsorted list of offers.

809:      //      Keepers can calculate the insertion position offchain and pass it to the insert() function to insert

810:      //      the unsorted offer into the sorted list. Unsorted offers will not be matched, but can be bought with buy().

816:      //      Can be used to cycle through all the unsorted offers.

842:              // There is a chance that pay_amt is smaller than 1 wei of the other token

855:                  // if lower

881:              // There is a chance that buy_amt is smaller than 1 wei of the other token

957:      // ---- Internal Functions ---- //

972:          // If offer has become dust during buy, we cancel it

992:          // Find the larger-than-id order whose successor is less-than-id.

1004:         // Look for an active order.

1013:             // if we did find a nearby active offer

1014:             // Walk the order book down from there...

1018:                 // Guaranteed to run at least once because of

1019:                 // the prior if statements.

1026:                 // ...or walk it up.

1062:         // there is at least one offer stored for token pair

1068:             // Ugly hack to work around rounding errors. Based on the idea that

1069:             // the furthest the amounts can stray from their "true" values is 1.

1070:             // Ergo the worst case has t_pay_amt and m_pay_amt at +1 away from

1071:             // their "correct" values and m_buy_amt and t_buy_amt at -1.

1072:             // Since (c - 1) * (d - 1) > (a + 1) * (b + 1) is equivalent to

1073:             // c * d > a * b + a + b + c + d, we write...

1085:             // ^ The `rounding` parameter is a compromise borne of a couple days

1086:             // of discussion.

1110:     // Make a new offer without putting it in the sorted list.

1111:     // Takes funds from the caller into market escrow.

1112:     // Keepers should call insert(id,pos) to put offer in the sorted list.

1169:     // Remove offer from the sorted list (does not cancel offer)

1183:             // offers[id] is not the highest offer

1236:     /// @dev unused deprecated function for applying a token distribution on top of a trade

1246:     /// @dev unused deprecated variable for applying a token distribution on top of a trade

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L188

File: contracts/rubiconPools/BathPair.sol

119:          address _bathHouse = msg.sender; //Assume the initializer is BathHouse

224:          order memory offer1 = getOfferInfo(info.askId); //ask

225:          order memory offer2 = getOfferInfo(info.bidId); //bid

494:          uint256 assetRebalAmt, //amount of ASSET in the quote buffer

495:          uint256 quoteRebalAmt, //amount of QUOTE in the asset buffer

540:          uint256 amount, //fill amount to handle

541:          uint256 hurdle, //must clear this on tail off

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L119

File: contracts/rubiconPools/BathToken.sol

193:          bathHouse = msg.sender; //NOTE: assumed admin is creator on BathHouse

217:          feeTo = address(this); //This contract is the fee recipient, rewarding HODLers

218:          feeBPS = 3; //Fee set to 3 BPS initially

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L193

File: contracts/RubiconRouter.sol

72:           //1. Get best offer for each asset

84:           //2. Iterate from that offer down the book until topNOrders

124:          //3. Return those topNOrders for either side of the order book

133:              uint256, //id

165:          uint256 expectedMarketFeeBPS //20

198:          uint256 expectedMarketFeeBPS //20

200:          //**User must approve this contract first**

201:          //transfer needed amount here first

243:                  0 //naively assume no fill_amt here for loop purposes?

272:          //swaps msg.sender entire balance in the trade

282:                  maxAmount.sub(buy_amt_min.mul(expectedMarketFeeBPS).div(10000)), //account for fee

295:          //swaps msg.sender's entire balance in the trade

312:          //swaps msg.sender entire balance in the trade

366:          ERC20(pay_gem).transferFrom(msg.sender, address(this), max_fill_amount); //transfer pay here

384:          uint256 pay_amt, //maker (ask) sell how much

386:          uint256 buy_amt, //maker (ask) buy how much

387:          ERC20 buy_gem, //maker (ask) buy which token

388:          uint256 pos //position to insert offer, 0 should be used if unknown

405:              //return any potential fill amount on the offer

413:          uint256 pay_amt, //maker (ask) sell how much

414:          ERC20 pay_gem, //maker (ask) sell which token

415:          uint256 buy_amt, //maker (ask) buy how much

417:          uint256 pos //position to insert offer, 0 should be used if unknown

431:              //return any potential fill amount on the offer as native ETH

470:          //Send back bathTokens to sender

490:          //Send back withdrawn native eth to sender

529:          //Transfer tokens here first and account for fee

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L72

17. Variable names that consist of all capital letters should be reserved for const/immutable variables

If the variable needs to be different based on which class it comes from, a view/pure function should be used instead (e.g. like this).

There is 1 instance of this issue:

File: contracts/rubiconPools/BathToken.sol   #1

66:       bytes32 public DOMAIN_SEPARATOR;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L66

18. Typos

There are 17 instances of this issue:

File: contracts/RubiconMarket.sol

/// @audit blocknumber
540:          uint256 delb; //the blocknumber where this entry was marked for delete

/// @audit acumulator
851:                  fill_amt = add(fill_amt, offers[offerId].pay_amt); //Add amount bought to acumulator

/// @audit acumulator
860:                  fill_amt = add(fill_amt, baux); //Add amount bought to acumulator

/// @audit acumulator
890:                  fill_amt = add(fill_amt, offers[offerId].buy_amt); //Add amount sold to acumulator

/// @audit acumulator
901:                  ); //Add amount sold to acumulator

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L540

File: contracts/rubiconPools/BathHouse.sol

/// @audit liquity
40:       /// @notice Key, system-wide risk parameter for all liquity Pools

/// @audit gaurentee
132:      /// @notice Please note, creating a Bath Token in this fashion ~does not~ gaurentee markets will be made for the new pair. This function signals the desire to have a new pair supported on Rubicon for strategists to consider market-making for

/// @audit withdrawls
414:          // Note, the option of a fee recipient for pool withdrawls exists. For all pools this is set to the pool itself in production and is visible via ~feeTo~ on any respective contract

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L40

File: contracts/rubiconPools/BathPair.sol

/// @audit StrategitTrade
81:       /// @notice Log a new market-making trade placed by a strategist, resulting in a StrategitTrade

/// @audit acitve
211:      /// @dev The local array of strategist IDs that exists for any given strategist [query via getOutstandingStrategistTrades()] acts as an acitve RAM for outstanding strategist trades

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L81

File: contracts/rubiconPools/BathToken.sol

/// @audit intially
29:       /// @notice The RubiconMarket.sol instance that all pool liquidity is intially directed to as market-making offers

/// @audit constract
220:          // Complete constract instantiation

/// @audit distibute
627:      /// @notice Function to distibute non-underlyingToken Bath Token incentives to pool withdrawers

/// @audit releaseable
641:                  // The BathBuddy pool should accrue ERC-20 rewards just like OZ VestingWallet and simply just release the withdrawer's relative share of releaseable() tokens

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L29

File: contracts/peripheral_contracts/BathBuddy.sol

/// @audit customizable
17:    * The vesting schedule is customizable through the {vestedAmount} function.

/// @audit transfering
110:              // Keep tokens here by not transfering the _fee anywhere, it is accrued to the Bath Token's Bath Buddy

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L17

File: contracts/RubiconRouter.sol

/// @audit wouldbe
187:          // Return the wouldbe resulting swap amount

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L187

19. NatSpec is incomplete

There is 1 instance of this issue:

File: contracts/rubiconPools/BathHouse.sol   #1

/// @audit Missing: '@return'
389       /// @param underlyingERC20 The underlying ERC-20 asset that underlies the newBathTokenAddress
390       /// @param _feeAdmin Recipient of pool withdrawal fees, typically the pool itself
391       function _createBathToken(ERC20 underlyingERC20, address _feeAdmin)
392           internal
393:          returns (address newBathTokenAddress)

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L389-L393

20. Event is missing indexed fields

Each event should use three indexed fields if there are three or more fields

There are 33 instances of this issue:

File: contracts/RubiconMarket.sol

115:      event LogItemUpdate(uint256 id);

116       event LogTrade(
117           uint256 pay_amt,
118           address indexed pay_gem,
119           uint256 buy_amt,
120           address indexed buy_gem
121:      );

168:      event LogInt(string lol, uint256 input);

180:      event OfferDeleted(uint256 id);

485       event LogNote(
486           bytes4 indexed sig,
487           address indexed guy,
488           bytes32 indexed foo,
489           bytes32 indexed bar,
490           uint256 wad,
491           bytes fax
492:      ) anonymous;

512:      event LogBuyEnabled(bool isEnabled);

513:      event LogMinSell(address pay_gem, uint256 min_amount);

514:      event LogMatchingEnabled(bool isEnabled);

515:      event LogUnsortedOffer(uint256 id);

516:      event LogSortedOffer(uint256 id);

517:      event LogInsert(address keeper, uint256 id);

518:      event LogDelete(address keeper, uint256 id);

519:      event LogMatch(uint256 id, uint256 amount);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L115

File: contracts/rubiconPools/BathHouse.sol

65        event LogNewBathToken(
66            address underlyingToken,
67            address bathTokenAddress,
68            address bathTokenFeeAdmin,
69            uint256 timestamp,
70            address bathTokenCreator
71:       );

74        event LogOpenCreationSignal(
75            ERC20 newERC20Underlying,
76            address spawnedBathToken,
77            uint256 initialNewBathTokenDeposit,
78            ERC20 pairedExistingAsset,
79            address pairedExistingBathToken,
80            uint256 pairedBathTokenDeposit,
81            address signaler
82:       );

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L65-L71

File: contracts/rubiconPools/BathPair.sol

82        event LogStrategistTrade(
83            uint256 strategistTradeID,
84            bytes32 askId,
85            bytes32 bidId,
86            address askAsset,
87            address bidAsset,
88            uint256 timestamp,
89            address strategist
90:       );

93        event LogScrubbedStratTrade(
94            uint256 strategistIDScrubbed,
95            uint256 assetFill,
96            address assetAddress,
97            address bathAssetAddress,
98            uint256 quoteFill,
99            address quoteAddress,
100           address bathQuoteAddress
101:      );

104       event LogStrategistRewardClaim(
105           address strategist,
106           address asset,
107           uint256 amountOfReward,
108           uint256 timestamp
109:      );

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L82-L90

File: contracts/rubiconPools/BathToken.sol

85        event Approval(
86            address indexed owner,
87            address indexed spender,
88            uint256 value
89:       );

92:       event Transfer(address indexed from, address indexed to, uint256 value);

95:       event LogInit(uint256 timeOfInit);

98        event LogDeposit(
99            uint256 depositedAmt,
100           IERC20 asset,
101           uint256 sharesReceived,
102           address depositor,
103           uint256 underlyingBalance,
104           uint256 outstandingAmount,
105           uint256 totalSupply
106:      );

109       event LogWithdraw(
110           uint256 amountWithdrawn,
111           IERC20 asset,
112           uint256 sharesWithdrawn,
113           address withdrawer,
114           uint256 fee,
115           address feeTo,
116           uint256 underlyingBalance,
117           uint256 outstandingAmount,
118           uint256 totalSupply
119:      );

122       event LogRebalance(
123           IERC20 pool_asset,
124           address destination,
125           IERC20 transferAsset,
126           uint256 rebalAmt,
127           uint256 stratReward,
128           uint256 underlyingBalance,
129           uint256 outstandingAmount,
130           uint256 totalSupply
131:      );

134       event LogPoolCancel(
135           uint256 orderId,
136           IERC20 pool_asset,
137           uint256 outstandingAmountToCancel,
138           uint256 underlyingBalance,
139           uint256 outstandingAmount,
140           uint256 totalSupply
141:      );

144       event LogPoolOffer(
145           uint256 id,
146           IERC20 pool_asset,
147           uint256 underlyingBalance,
148           uint256 outstandingAmount,
149           uint256 totalSupply
150:      );

153       event LogRemoveFilledTradeAmount(
154           IERC20 pool_asset,
155           uint256 fillAmount,
156           uint256 underlyingBalance,
157           uint256 outstandingAmount,
158           uint256 totalSupply
159:      );

162       event Deposit(
163           address indexed caller,
164           address indexed owner,
165           uint256 assets,
166           uint256 shares
167:      );

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L85-L89

File: contracts/peripheral_contracts/BathBuddy.sol

53:       event EtherReleased(uint256 amount);

54:       event ERC20Released(address indexed token, uint256 amount);

57        event LogClaimBonusToken(
58            address indexed receiver,
59            address indexed callingPool,
60            uint256 amountReceived,
61            uint256 shares,
62            IERC20 bonusToken,
63            uint256 releasableAmountToWholePool
64:       );

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L53

File: contracts/RubiconRouter.sol

25:       event LogNote(string, uint256);

27        event LogSwap(
28            uint256 inputAmount,
29            address inputERC20,
30            uint256 hurdleBuyAmtMin,
31            address targetERC20,
32            bytes32 indexed pair,
33            uint256 realizedFill,
34            address recipient
35:       );

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L25

21. Not using the named return variables anywhere in the function is confusing

Consider changing the variable to be an unnamed one

There are 15 instances of this issue:

File: contracts/RubiconMarket.sol

/// @audit z
57:       function min(uint256 x, uint256 y) internal pure returns (uint256 z) {

/// @audit z
61:       function max(uint256 x, uint256 y) internal pure returns (uint256 z) {

/// @audit z
65:       function imin(int256 x, int256 y) internal pure returns (int256 z) {

/// @audit z
69:       function imax(int256 x, int256 y) internal pure returns (int256 z) {

/// @audit active
232:      function isActive(uint256 id) public view returns (bool active) {

/// @audit owner
236:      function getOwner(uint256 id) public view returns (address owner) {

/// @audit id
382       function make(
383           ERC20 pay_gem,
384           ERC20 buy_gem,
385           uint128 pay_amt,
386           uint128 buy_amt
387:      ) external virtual returns (bytes32 id) {

/// @audit closed
471:      function isClosed() public pure returns (bool closed) {

/// @audit success
678       function cancel(uint256 id)
679           public
680           override
681           can_cancel(id)
682:          returns (bool success)

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L57

File: contracts/rubiconPools/BathToken.sol

/// @audit amountWithdrawn
375       function withdraw(uint256 _shares)
376           external
377:          returns (uint256 amountWithdrawn)

/// @audit totalManagedAssets
388:      function totalAssets() public view returns (uint256 totalManagedAssets) {

/// @audit shares
435:      function deposit(uint256 assets) public returns (uint256 shares) {

/// @audit shares
441       function deposit(uint256 assets, address receiver)
442           public
443:          returns (uint256 shares)

/// @audit maxShares
525:      function maxRedeem(address owner) public view returns (uint256 maxShares) {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L375-L377

File: contracts/RubiconRouter.sol

/// @audit fill_amt
161       function getExpectedSwapFill(
162           uint256 pay_amt,
163           uint256 buy_amt_min,
164           address[] calldata route, // First address is what is being payed, Last address is what is being bought
165           uint256 expectedMarketFeeBPS //20
166:      ) public view returns (uint256 fill_amt) {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L161-L166

Summary

Gas Optimizations

IssueInstances
1Multiple address mappings can be combined into a single mapping of an address to a struct, where appropriate2
2State variables only set in the constructor should be declared immutable3
3State variables can be packed into fewer storage slots3
4Using calldata instead of memory for read-only arguments in external functions saves gas12
5Avoid contract existence checks by using solidity version 0.8.10 or later117
6State variables should be cached in stack variables rather than re-reading them from storage71
7Multiple accesses of a mapping should use a local variable cache59
8internal functions only called once can be inlined to save gas12
9<array>.length should not be looked up in every loop of a for-loop3
10require()/revert() strings longer than 32 bytes cost extra gas26
11keccak256() should only need to be called on a specific string literal once1
12Using bools for storage incurs overhead12
13Use a more recent version of solidity5
14Use a more recent version of solidity1
15Using > 0 costs more gas than != 0 when used on a uint in a require() statement6
16It costs more gas to initialize variables to zero than to let the default of zero be applied13
17internal functions not called by the contract should be removed to save deployment gas7
18++i costs less gas than i++, especially when it's used in for-loops (--i/i-- too)8
19Splitting require() statements that use && saves gas8
20Usage of uints/ints smaller than 32 bytes (256 bits) incurs overhead37
21Using private rather than public for constants, saves gas1
22Don't compare boolean expressions to boolean literals4
23Duplicated require()/revert() checks should be refactored to a modifier or function12
24Division by two should use bit shifting4
25require() or revert() statements that check input arguments should be at the top of the function4
26Empty blocks should be removed or emit something3
27Superfluous event fields8
28Functions guaranteed to revert when called by normal users can be marked payable35

Total: 477 instances over 28 issues

Gas Optimizations

1. Multiple address mappings can be combined into a single mapping of an address to a struct, where appropriate

Saves a storage slot for the mapping. Depending on the circumstances and sizes of types, can avoid a Gsset (20000 gas) per mapping combined. Reads and subsequent writes can also be cheaper when a function requires both values and they both fit in the same storage slot. Finally, if both fields are accessed in the same function, can save ~42 gas per access due to not having to recalculate the key's keccak256 hash (Gkeccak256 - 30 gas) and that calculation's associated stack operations.

There are 2 instances of this issue:

File: contracts/RubiconMarket.sol   #1

543       mapping(address => mapping(address => uint256)) public _best; //id of the highest offer for a token pair
544       mapping(address => mapping(address => uint256)) public _span; //number of offers stored for token pair in sorted orderbook
545:      mapping(address => uint256) public _dust; //minimum sell amount for a token to avoid dust offers

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L543-L545

File: contracts/rubiconPools/BathPair.sol   #2

52        mapping(address => mapping(address => mapping(address => uint256[])))
53            public outOffersByStrategist;
54    
55        /// @notice Tracks the market-kaing fill amounts on a per-asset basis of a strategist
56        /// @dev strategist => erc20asset => fill amount per asset;
57:       mapping(address => mapping(address => uint256)) public strategist2Fills;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L52-L57

2. State variables only set in the constructor should be declared immutable

Avoids a Gsset (20000 gas) in the constructor, and replaces each Gwarmacces (100 gas) with a PUSH32 (3 gas).

There are 3 instances of this issue:

File: contracts/peripheral_contracts/BathBuddy.sol   #1

31:       address public beneficiary;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L31

File: contracts/peripheral_contracts/BathBuddy.sol   #2

32:       uint64 public start;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L32

File: contracts/peripheral_contracts/BathBuddy.sol   #3

33:       uint64 public duration;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L33

3. State variables can be packed into fewer storage slots

If variables occupying the same slot are both written the same function or by the constructor, avoids a separate Gsset (20000 gas). Reads of the variables can also be cheaper

There are 3 instances of this issue:

File: contracts/RubiconMarket.sol   #1

/// @audit Variable ordering with 4 slots instead of the current 5:
/// @audit uint256(32):last_offer_id, mapping(32):offers, uint256(32):feeBPS, address(20):feeTo, bool(1):locked
186:      uint256 public last_offer_id;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L186

File: contracts/rubiconPools/BathHouse.sol   #2

/// @audit Variable ordering with 10 slots instead of the current 11:
/// @audit string(32):name, mapping(32):approvedStrategists, uint256(32):reserveRatio, uint256(32):timeDelay, mapping(32):tokenToBathToken, address(20):admin, bool(1):initialized, bool(1):permissionedStrategists, uint8(1):bpsToStrategists, address(20):proxyManager, address(20):RubiconMarketAddress, address(20):approvedPairContract, address(20):newBathTokenImplementation
20:       string public name;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L20

File: contracts/rubiconPools/BathToken.sol   #3

/// @audit Variable ordering with 17 slots instead of the current 18:
/// @audit string(32):symbol, string(32):name, uint256(32):feeBPS, uint256(32):totalSupply, uint256(32):outstandingAmount, uint256[](32):deprecatedStorageArray, mapping(32):deprecatedMapping, mapping(32):balanceOf, mapping(32):allowance, bytes32(32):DOMAIN_SEPARATOR, mapping(32):nonces, address[](32):bonusTokens, address(20):RubiconMarketAddress, bool(1):initialized, uint8(1):decimals, address(20):bathHouse, address(20):feeTo, user-defined(20):underlyingToken, user-defined(20):rewardsVestingWallet
22:       bool public initialized;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L22

4. Using calldata instead of memory for read-only arguments in external functions saves gas

When a function with a memory array is called externally, the abi.decode() step has to use a for-loop to copy each index of the calldata to the memory index. Each iteration of this for-loop costs at least 60 gas (i.e. 60 * <mem_array>.length). Using calldata directly, obliviates the need for such a loop in the contract code and runtime execution.

If the array is passed to an internal function which passes the array to another internal function where the array is modified and therefore memory is used in the external call, it's still more gass-efficient to use calldata when the external function uses modifiers, since the modifiers may prevent the internal functions from being called. Structs have the same overhead as an array of length one

There are 12 instances of this issue:

File: contracts/rubiconPools/BathPair.sol

413:          address[2] memory tokenPair, // ASSET, Then Quote

414:          uint256[] memory askNumerators, // Quote / Asset

415:          uint256[] memory askDenominators, // Asset / Quote

416:          uint256[] memory bidNumerators, // size in ASSET

417:          uint256[] memory bidDenominators // size in QUOTES

464:          uint256[] memory ids,

465:          address[2] memory tokenPair, // ASSET, Then Quote

466:          uint256[] memory askNumerators, // Quote / Asset

467:          uint256[] memory askDenominators, // Asset / Quote

468:          uint256[] memory bidNumerators, // size in ASSET

469:          uint256[] memory bidDenominators // size in QUOTES

578:      function scrubStrategistTrades(uint256[] memory ids)

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L413

5. Avoid contract existence checks by using solidity version 0.8.10 or later

Prior to 0.8.10 the compiler inserted extra code, including EXTCODESIZE (700 gas), to check for contract existence for external calls. In more recent solidity versions, the compiler will not insert these checks if the external call has a return value

There are 117 instances of this issue:

File: contracts/RubiconMarket.sol

298:              _offer.buy_gem.transferFrom(msg.sender, feeTo, fee),

305:              _offer.buy_gem.transferFrom(msg.sender, _offer.owner, spend),

309:              _offer.pay_gem.transfer(msg.sender, quantity),

361:          require(_offer.pay_gem.transfer(_offer.owner, _offer.pay_amt));

416:          require(pay_gem.transferFrom(msg.sender, address(this), pay_amt));

665               IAqueduct(AqueductAddress).distributeToMakerAndTaker(
666                   getOwner(id),
667                   msg.sender
668:              );

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L298

File: contracts/rubiconPools/BathHouse.sol

157               newBathTokenUnderlying.transferFrom(
158                   msg.sender,
159                   address(this),
160                   initialLiquidityNew
161:              ),

165:          newBathTokenUnderlying.approve(newOne, initialLiquidityNew);

168:          IBathToken(newOne).deposit(initialLiquidityNew, msg.sender);

172               desiredPairedAsset.transferFrom(
173                   msg.sender,
174                   address(this),
175                   initialLiquidityExistingBathToken
176:              ),

180           desiredPairedAsset.approve(
181               pairedPool,
182               initialLiquidityExistingBathToken
183:          );

186           IBathToken(pairedPool).deposit(
187               initialLiquidityExistingBathToken,
188               msg.sender
189:          );

242:              IBathPair(_bathPairAddress).initialized() != true,

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L157-L161

File: contracts/rubiconPools/BathPair.sol

121:              IBathHouse(_bathHouse).getMarket() !=

123:                  IBathHouse(_bathHouse).initialized(),

128:          RubiconMarketAddress = IBathHouse(_bathHouse).getMarket();

149:              IBathHouse(bathHouse).isApprovedStrategist(targetStrategist) ==

168           bathAssetAddress = IBathHouse(bathHouse).tokenToBathToken(
169               underlyingAsset
170:          );

171           bathQuoteAddress = IBathHouse(bathHouse).tokenToBathToken(
172               underlyingQuote
173:          );

176:                  IBathToken(bathAssetAddress).underlyingBalance().mul(

177:                      IBathHouse(bathHouse).reserveRatio()

179:              ).div(100) <= IERC20(underlyingAsset).balanceOf(bathAssetAddress),

184:                  IBathToken(bathQuoteAddress).underlyingBalance().mul(

185:                      IBathHouse(bathHouse).reserveRatio()

187:              ).div(100) <= IERC20(underlyingQuote).balanceOf(bathQuoteAddress),

218           address bathAssetAddress = IBathHouse(bathHouse).tokenToBathToken(
219               _asset
220:          );

221           address bathQuoteAddress = IBathHouse(bathHouse).tokenToBathToken(
222               _quote
223:          );

298:          ) = IRubiconMarket(RubiconMarketAddress).getOffer(id);

367           uint256 newAskID = IBathToken(bathAssetAddress).placeOffer(
368               ask.pay_amt,
369               ask.pay_gem,
370               ask.buy_amt,
371               ask.buy_gem
372:          );

374           uint256 newBidID = IBathToken(bathQuoteAddress).placeOffer(
375               bid.pay_amt,
376               bid.pay_gem,
377               bid.buy_amt,
378               bid.buy_gem
379:          );

500           address _bathAssetAddress = IBathHouse(_bathHouse).tokenToBathToken(
501               _underlyingAsset
502:          );

503           address _bathQuoteAddress = IBathHouse(_bathHouse).tokenToBathToken(
504               _underlyingQuote
505:          );

512:          uint16 stratReward = IBathHouse(_bathHouse).getBPSToStrats();

545:          uint16 stratRewardBPS = IBathHouse(bathHouse).getBPSToStrats();

555           IStrategistUtility(_stratUtil).UNIdump(
556               amount.sub((stratRewardBPS.mul(amount)).div(10000)),
557               tokenToHandle,
558               targetToken,
559               hurdle,
560               _poolFee,
561               targetPool
562:          );

599:                  fillCountA.mul(IERC20(asset).balanceOf(address(this)))

601:              IERC20(asset).transfer(msg.sender, booty);

613:                  fillCountQ.mul(IERC20(quote).balanceOf(address(this)))

615:              IERC20(quote).transfer(msg.sender, booty);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L121

File: contracts/rubiconPools/BathToken.sol

188:              abi.encodePacked(("bath"), token.symbol())

211:          decimals = token.decimals(); // v1 Change - 4626 Adherence

214:          IERC20(address(token)).approve(RubiconMarketAddress, 2**256 - 1);

228:              IBathHouse(bathHouse).isApprovedPair(msg.sender) == true,

256:          underlyingToken.approve(RubiconMarketAddress, 2**256 - 1);

323           uint256 id = IRubiconMarket(RubiconMarketAddress).offer(
324               pay_amt,
325               pay_gem,
326               buy_amt,
327               buy_gem,
328               0,
329               false
330:          );

353           IERC20(filledAssetToRebalance).transfer(
354               destination,
355               rebalAmt.sub(stratReward)
356:          );

357:          IERC20(filledAssetToRebalance).transfer(msg.sender, stratReward);

562:          uint256 _before = underlyingToken.balanceOf(address(this));

565:          underlyingToken.transferFrom(msg.sender, address(this), assets);

566:          uint256 _after = underlyingToken.balanceOf(address(this));

602:              underlyingToken.transfer(feeTo, _fee);

605:          underlyingToken.transfer(receiver, amountWithdrawn);

757:          uint256 _pool = IERC20(underlyingToken).balanceOf(address(this));

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L188

File: contracts/peripheral_contracts/BathBuddy.sol

114:              token.transfer(recipient, amountWithdrawn);

140:                  IERC20(token).balanceOf(address(this)) + released(token),

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L114

File: contracts/RubiconRouter.sol

73            uint256 bestAskID = RubiconMarket(_RubiconMarketAddress).getBestOffer(
74                asset,
75                quote
76:           );

77            uint256 bestBidID = RubiconMarket(_RubiconMarketAddress).getBestOffer(
78                quote,
79                asset
80:           );

95:                   ) = RubiconMarket(_RubiconMarketAddress).getOffer(bestAskID);

101:                  ) = RubiconMarket(_RubiconMarketAddress).getOffer(bestBidID);

106               uint256 nextBestAsk = RubiconMarket(_RubiconMarketAddress)
107:              .getWorseOffer(lastAsk);

108               uint256 nextBestBid = RubiconMarket(_RubiconMarketAddress)
109:              .getWorseOffer(lastBid);

110               (uint256 ask_pay_amt, , uint256 ask_buy_amt, ) = RubiconMarket(
111                   _RubiconMarketAddress
112:              ).getOffer(nextBestAsk);

113               (uint256 bid_pay_amt, , uint256 bid_buy_amt, ) = RubiconMarket(
114                   _RubiconMarketAddress
115:              ).getOffer(nextBestBid);

141           uint256 offer = RubiconMarket(_market).getBestOffer(
142               ERC20(asset),
143               ERC20(quote)
144:          );

150:          ) = RubiconMarket(_market).getOffer(offer);

157:          ERC20(toApprove).approve(RubiconMarketAddress, 2**256 - 1);

178               uint256 wouldBeFillAmount = RubiconMarket(_market).getBuyAmount(
179                   ERC20(output),
180                   ERC20(input),
181                   _pay
182:              );

202           ERC20(route[0]).transferFrom(
203               msg.sender,
204               address(this),
205               pay_amt.add(pay_amt.mul(expectedMarketFeeBPS).div(10000)) // Account for expected fee
206:          );

236:              if (ERC20(input).allowance(address(this), _market) == 0) {

239               uint256 fillAmount = RubiconMarket(_market).sellAllAmount(
240                   ERC20(input),
241                   _pay,
242                   ERC20(output),
243                   0 //naively assume no fill_amt here for loop purposes?
244:              );

251:              ERC20(route[route.length - 1]).transfer(to, currentAmount);

273:          uint256 maxAmount = ERC20(route[0]).balanceOf(msg.sender);

274           ERC20(route[0]).transferFrom(
275               msg.sender,
276               address(this),
277               maxAmount // Account for expected fee
278:          );

296:          uint256 maxAmount = ERC20(buy_gem).balanceOf(msg.sender);

297           fill = RubiconMarket(RubiconMarketAddress).buyAllAmount(
298               buy_gem,
299               maxAmount,
300               pay_gem,
301               max_fill_amount
302:          );

303:          ERC20(buy_gem).transfer(msg.sender, fill);

313:          uint256 maxAmount = ERC20(buy_gem).balanceOf(msg.sender);

314           fill = RubiconMarket(RubiconMarketAddress).sellAllAmount(
315               pay_gem,
316               maxAmount,
317               buy_gem,
318               min_fill_amount
319:          );

320:          ERC20(buy_gem).transfer(msg.sender, fill);

332:          uint256 _before = ERC20(_weth).balanceOf(address(this));

342           fill = RubiconMarket(RubiconMarketAddress).buyAllAmount(
343               buy_gem,
344               buy_amt,
345               ERC20(wethAddress),
346               max_fill_amount
347:          );

348:          ERC20(buy_gem).transfer(msg.sender, buy_amt);

350:          uint256 _after = ERC20(_weth).balanceOf(address(this));

355:              WETH9(wethAddress).withdraw(delta);

356:              msg.sender.transfer(delta);

366:          ERC20(pay_gem).transferFrom(msg.sender, address(this), max_fill_amount); //transfer pay here

367           fill = RubiconMarket(RubiconMarketAddress).buyAllAmount(
368               ERC20(wethAddress),
369               buy_amt,
370               pay_gem,
371               max_fill_amount
372:          );

373:          WETH9(wethAddress).withdraw(buy_amt); // Fill in WETH

374:          msg.sender.transfer(buy_amt); // Return native ETH

377:              ERC20(pay_gem).transfer(msg.sender, max_fill_amount - fill);

394:          uint256 _before = ERC20(buy_gem).balanceOf(address(this));

396           uint256 id = RubiconMarket(RubiconMarketAddress).offer(
397               pay_amt,
398               ERC20(wethAddress),
399               buy_amt,
400               buy_gem,
401               pos
402:          );

403:          uint256 _after = ERC20(buy_gem).balanceOf(address(this));

406:              ERC20(buy_gem).transfer(msg.sender, _after - _before);

419:          ERC20(pay_gem).transferFrom(msg.sender, address(this), pay_amt);

421:          uint256 _before = ERC20(wethAddress).balanceOf(address(this));

422           uint256 id = RubiconMarket(RubiconMarketAddress).offer(
423               pay_amt,
424               pay_gem,
425               buy_amt,
426               ERC20(wethAddress),
427               pos
428:          );

429:          uint256 _after = ERC20(wethAddress).balanceOf(address(this));

433:              WETH9(wethAddress).withdraw(delta);

434:              msg.sender.transfer(delta);

441           (uint256 pay_amt, ERC20 pay_gem, , ) = RubiconMarket(
442               RubiconMarketAddress
443:          ).getOffer(id);

450:          WETH9(wethAddress).withdraw(pay_amt);

451:          msg.sender.transfer(pay_amt);

460:          IERC20 target = IBathToken(targetPool).underlyingToken();

464:          if (target.allowance(address(this), targetPool) == 0) {

465:              target.approve(targetPool, amount);

469:          newShares = IBathToken(targetPool).deposit(amount);

471:          ERC20(targetPool).transfer(msg.sender, newShares);

480:          IERC20 target = IBathToken(targetPool).underlyingToken();

483:              IBathToken(targetPool).balanceOf(msg.sender) >= shares,

486:          IBathToken(targetPool).transferFrom(msg.sender, address(this), shares);

487:          withdrawnWETH = IBathToken(targetPool).withdraw(shares);

488:          WETH9(wethAddress).withdraw(withdrawnWETH);

491:          msg.sender.transfer(withdrawnWETH);

531               ERC20(route[0]).transferFrom(
532                   msg.sender,
533                   address(this),
534                   pay_amt.add(pay_amt.mul(expectedMarketFeeBPS).div(10000))
535:              ),

546:          WETH9(wethAddress).withdraw(fill);

548:          msg.sender.transfer(fill);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L73-L76

6. State variables should be cached in stack variables rather than re-reading them from storage

The instances below point to the second+ access of a state variable within a function. Caching of a state variable replace each Gwarmaccess (100 gas) with a much cheaper stack read. Other less obvious fixes/optimizations include having local memory caches of state variable structs, or having local caches of state variable contracts/addresses.

There are 71 instances of this issue:

File: contracts/RubiconMarket.sol

/// @audit owner
24:           emit LogSetOwner(owner);

/// @audit last_offer_id
437:          return last_offer_id;

/// @audit feeTo
330:              feeTo,

/// @audit buyEnabled
749:          emit LogBuyEnabled(buyEnabled);

/// @audit matchingEnabled
766:          emit LogMatchingEnabled(matchingEnabled);

/// @audit _head
1211:         if (_head == id) {

/// @audit offers[id].pay_gem
262:              offers[id].pay_gem,

/// @audit offers[id].buy_gem
263:              offers[id].buy_gem,

/// @audit _rank[id].delb
718:                  _rank[id].delb < block.number - 10

/// @audit offers[offerId].buy_amt
849:              if (pay_amt >= offers[offerId].buy_amt) {

/// @audit offers[offerId].buy_amt
852:                  pay_amt = sub(pay_amt, offers[offerId].buy_amt); //Decrease amount to sell

/// @audit offers[offerId].buy_amt
858:                      rdiv(offers[offerId].pay_amt, offers[offerId].buy_amt)

/// @audit offers[offerId].pay_amt
851:                  fill_amt = add(fill_amt, offers[offerId].pay_amt); //Add amount bought to acumulator

/// @audit offers[offerId].pay_amt
853:                  take(bytes32(offerId), uint128(offers[offerId].pay_amt)); //We take the whole offer

/// @audit offers[offerId].pay_amt
858:                      rdiv(offers[offerId].pay_amt, offers[offerId].buy_amt)

/// @audit offers[offerId].pay_amt
888:              if (buy_amt >= offers[offerId].pay_amt) {

/// @audit offers[offerId].pay_amt
891:                  buy_amt = sub(buy_amt, offers[offerId].pay_amt); //Decrease amount to buy

/// @audit offers[offerId].pay_amt
892:                  take(bytes32(offerId), uint128(offers[offerId].pay_amt)); //We take the whole offer

/// @audit offers[offerId].pay_amt
899:                          rdiv(offers[offerId].buy_amt, offers[offerId].pay_amt)

/// @audit offers[offerId].buy_amt
890:                  fill_amt = add(fill_amt, offers[offerId].buy_amt); //Add amount sold to acumulator

/// @audit offers[offerId].buy_amt
899:                          rdiv(offers[offerId].buy_amt, offers[offerId].pay_amt)

/// @audit offers[offerId].buy_amt
917:              pay_amt = sub(pay_amt, offers[offerId].buy_amt); //Decrease amount to pay

/// @audit offers[offerId].buy_amt
928:                  rdiv(offers[offerId].pay_amt, offers[offerId].buy_amt)

/// @audit offers[offerId].pay_amt
928:                  rdiv(offers[offerId].pay_amt, offers[offerId].buy_amt)

/// @audit offers[offerId].pay_amt
941:              buy_amt = sub(buy_amt, offers[offerId].pay_amt); //Decrease amount to buy

/// @audit offers[offerId].pay_amt
952:                  rdiv(offers[offerId].buy_amt, offers[offerId].pay_amt)

/// @audit offers[offerId].buy_amt
952:                  rdiv(offers[offerId].buy_amt, offers[offerId].pay_amt)

/// @audit offers[id].pay_amt
975:              offers[id].pay_amt < _dust[address(offers[id].pay_gem)]

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L24

File: contracts/rubiconPools/BathHouse.sol

/// @audit admin
125:          approveStrategist(admin);

/// @audit newBathTokenImplementation
426:              newBathTokenImplementation,

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L125

File: contracts/rubiconPools/BathPair.sol

/// @audit bathHouse
171:          bathQuoteAddress = IBathHouse(bathHouse).tokenToBathToken(

/// @audit bathHouse
177:                      IBathHouse(bathHouse).reserveRatio()

/// @audit bathHouse
185:                      IBathHouse(bathHouse).reserveRatio()

/// @audit bathHouse
221:          address bathQuoteAddress = IBathHouse(bathHouse).tokenToBathToken(

/// @audit last_stratTrade_id
207:          return last_stratTrade_id;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L171

File: contracts/rubiconPools/BathToken.sol

/// @audit RubiconMarketAddress
214:          IERC20(address(token)).approve(RubiconMarketAddress, 2**256 - 1);

/// @audit feeTo
602:              underlyingToken.transfer(feeTo, _fee);

/// @audit feeTo
613:              feeTo,

/// @audit underlyingToken
565:          underlyingToken.transferFrom(msg.sender, address(this), assets);

/// @audit underlyingToken
566:          uint256 _after = underlyingToken.balanceOf(address(this));

/// @audit underlyingToken
577:              underlyingToken,

/// @audit underlyingToken
605:          underlyingToken.transfer(receiver, amountWithdrawn);

/// @audit underlyingToken
609:              underlyingToken,

/// @audit totalSupply
400:              assets.mul(totalSupply)

/// @audit totalSupply
459:          ).div(totalSupply);

/// @audit totalSupply
570:              assets.mul(totalSupply)

/// @audit totalSupply
582:              totalSupply

/// @audit totalSupply
616:              totalSupply

/// @audit outstandingAmount
288:              outstandingAmount,

/// @audit outstandingAmount
300:              outstandingAmount,

/// @audit outstandingAmount
337:              outstandingAmount,

/// @audit bonusTokens
635:              for (uint256 index = 0; index < bonusTokens.length; index++) {

/// @audit bonusTokens
636:                  IERC20 token = IERC20(bonusTokens[index]);

/// @audit rewardsVestingWallet
643:                      rewardsVestingWallet.release(

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L214

File: contracts/peripheral_contracts/BathBuddy.sol

/// @audit start
156:          } else if (timestamp > start + duration) {

/// @audit start
159:              return (totalAllocation * (timestamp - start)) / duration;

/// @audit duration
159:              return (totalAllocation * (timestamp - start)) / duration;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L156

File: contracts/RubiconRouter.sol

/// @audit RubiconMarketAddress
449:          outcome = RubiconMarket(RubiconMarketAddress).cancel(id);

/// @audit wethAddress
340:          WETH9(wethAddress).deposit{value: max_fill_withFee}(); // Pay with native ETH -> WETH

/// @audit wethAddress
345:              ERC20(wethAddress),

/// @audit wethAddress
355:              WETH9(wethAddress).withdraw(delta);

/// @audit wethAddress
373:          WETH9(wethAddress).withdraw(buy_amt); // Fill in WETH

/// @audit wethAddress
398:              ERC20(wethAddress),

/// @audit wethAddress
426:              ERC20(wethAddress),

/// @audit wethAddress
429:          uint256 _after = ERC20(wethAddress).balanceOf(address(this));

/// @audit wethAddress
433:              WETH9(wethAddress).withdraw(delta);

/// @audit wethAddress
450:          WETH9(wethAddress).withdraw(pay_amt);

/// @audit wethAddress
468:          WETH9(wethAddress).deposit{value: amount}();

/// @audit wethAddress
488:          WETH9(wethAddress).withdraw(withdrawnWETH);

/// @audit wethAddress
508:          WETH9(wethAddress).deposit{value: amtWithFee}();

/// @audit wethAddress
546:          WETH9(wethAddress).withdraw(fill);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L449

7. Multiple accesses of a mapping should use a local variable cache

The instances below point to the second+ access of a value inside a mapping, within a function. Caching a mapping's value in a local storage variable when the value is accessed multiple times, saves ~42 gas per access due to not having to recalculate the key's keccak256 hash (Gkeccak256 - 30 gas) and that calculation's associated stack operations.

There are 59 instances of this issue:

File: contracts/RubiconMarket.sol

/// @audit offers[id]
260:              keccak256(abi.encodePacked(offers[id].pay_gem, offers[id].buy_gem)),

/// @audit offers[id]
261:              offers[id].owner,

/// @audit offers[id]
262:              offers[id].pay_gem,

/// @audit offers[id]
263:              offers[id].buy_gem,

/// @audit offers[id]
264:              uint128(offers[id].pay_amt),

/// @audit offers[id]
265:              uint128(offers[id].buy_amt),

/// @audit offers[id]
266:              offers[id].timestamp

/// @audit offers[id]
303:          offers[id].buy_amt = sub(_offer.buy_amt, spend);

/// @audit offers[id]
341:          if (offers[id].pay_amt == 0) {

/// @audit _rank[id]
718:                  _rank[id].delb < block.number - 10

/// @audit _rank[id]
824:              _rank[id].prev != 0 ||

/// @audit offers[id]
825:              _best[address(offers[id].pay_gem)][address(offers[id].buy_gem)] ==

/// @audit offers[offerId]
845:                  wdiv(offers[offerId].buy_amt, offers[offerId].pay_amt)

/// @audit offers[offerId]
849:              if (pay_amt >= offers[offerId].buy_amt) {

/// @audit offers[offerId]
851:                  fill_amt = add(fill_amt, offers[offerId].pay_amt); //Add amount bought to acumulator

/// @audit offers[offerId]
852:                  pay_amt = sub(pay_amt, offers[offerId].buy_amt); //Decrease amount to sell

/// @audit offers[offerId]
853:                  take(bytes32(offerId), uint128(offers[offerId].pay_amt)); //We take the whole offer

/// @audit offers[offerId]
858:                      rdiv(offers[offerId].pay_amt, offers[offerId].buy_amt)

/// @audit offers[offerId]
858:                      rdiv(offers[offerId].pay_amt, offers[offerId].buy_amt)

/// @audit offers[offerId]
884:                  wdiv(offers[offerId].pay_amt, offers[offerId].buy_amt)

/// @audit offers[offerId]
888:              if (buy_amt >= offers[offerId].pay_amt) {

/// @audit offers[offerId]
890:                  fill_amt = add(fill_amt, offers[offerId].buy_amt); //Add amount sold to acumulator

/// @audit offers[offerId]
891:                  buy_amt = sub(buy_amt, offers[offerId].pay_amt); //Decrease amount to buy

/// @audit offers[offerId]
892:                  take(bytes32(offerId), uint128(offers[offerId].pay_amt)); //We take the whole offer

/// @audit offers[offerId]
899:                          rdiv(offers[offerId].buy_amt, offers[offerId].pay_amt)

/// @audit offers[offerId]
899:                          rdiv(offers[offerId].buy_amt, offers[offerId].pay_amt)

/// @audit offers[offerId]
916:              fill_amt = add(fill_amt, offers[offerId].pay_amt); //Add amount to buy accumulator

/// @audit offers[offerId]
917:              pay_amt = sub(pay_amt, offers[offerId].buy_amt); //Decrease amount to pay

/// @audit offers[offerId]
928:                  rdiv(offers[offerId].pay_amt, offers[offerId].buy_amt)

/// @audit offers[offerId]
928:                  rdiv(offers[offerId].pay_amt, offers[offerId].buy_amt)

/// @audit offers[offerId]
940:              fill_amt = add(fill_amt, offers[offerId].buy_amt); //Add amount to pay accumulator

/// @audit offers[offerId]
941:              buy_amt = sub(buy_amt, offers[offerId].pay_amt); //Decrease amount to buy

/// @audit offers[offerId]
952:                  rdiv(offers[offerId].buy_amt, offers[offerId].pay_amt)

/// @audit offers[offerId]
952:                  rdiv(offers[offerId].buy_amt, offers[offerId].pay_amt)

/// @audit offers[id]
975:              offers[id].pay_amt < _dust[address(offers[id].pay_gem)]

/// @audit offers[id]
975:              offers[id].pay_amt < _dust[address(offers[id].pay_gem)]

/// @audit offers[id]
988:          address pay_gem = address(offers[id].pay_gem);

/// @audit _rank[pos]
1022:                     pos = _rank[pos].prev;

/// @audit _rank[pos]
1029:                     pos = _rank[pos].next;

/// @audit offers[high]
1043:             mul(offers[high].buy_amt, offers[low].pay_amt);

/// @audit offers[low]
1043:             mul(offers[high].buy_amt, offers[low].pay_amt);

/// @audit offers[best_maker_id]
1066:             m_pay_amt = offers[best_maker_id].pay_amt;

/// @audit offers[id]
1134:         ERC20 pay_gem = offers[id].pay_gem;

/// @audit offers[pos]
1139:             offers[pos].buy_gem != buy_gem ||

/// @audit _rank[pos]
1149:             _rank[pos].prev = id;

/// @audit _rank[id]
1162:             _rank[id].prev = prev_id;

/// @audit offers[id]
1174:         address pay_gem = address(offers[id].pay_gem);

/// @audit _rank[id]
1184:             require(_rank[_rank[id].next].prev == id);

/// @audit _rank[<etc>]
1185:             _rank[_rank[id].next].prev = _rank[id].prev;

/// @audit _rank[id]
1185:             _rank[_rank[id].next].prev = _rank[id].prev;

/// @audit _rank[id]
1185:             _rank[_rank[id].next].prev = _rank[id].prev;

/// @audit _rank[id]
1188:             _best[pay_gem][buy_gem] = _rank[id].prev;

/// @audit _rank[id]
1191:         if (_rank[id].prev != 0) {

/// @audit _rank[<etc>]
1193:             require(_rank[_rank[id].prev].next == id);

/// @audit _rank[id]
1193:             require(_rank[_rank[id].prev].next == id);

/// @audit _rank[<etc>]
1194:             _rank[_rank[id].prev].next = _rank[id].next;

/// @audit _rank[id]
1194:             _rank[_rank[id].prev].next = _rank[id].next;

/// @audit _rank[id]
1194:             _rank[_rank[id].prev].next = _rank[id].next;

/// @audit _rank[id]
1198:         _rank[id].delb = block.number; //mark _rank[id] for deletion

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L260

8. internal functions only called once can be inlined to save gas

Not inlining costs 20 to 40 gas because of two extra JUMP instructions and additional stack operations needed for function calls.

There are 12 instances of this issue:

File: contracts/RubiconMarket.sol

32:       function isAuthorized(address src) internal view returns (bool) {

435:      function _next_id() internal returns (uint256) {

1001:     function _findpos(uint256 id, uint256 pos) internal view returns (uint256) {

1049      function _matcho(
1050          uint256 t_pay_amt, //taker sell how much
1051          ERC20 t_pay_gem, //taker sell which token
1052          uint256 t_buy_amt, //taker buy how much
1053          ERC20 t_buy_gem, //taker buy which token
1054          uint256 pos, //position id
1055          bool rounding //match "close enough" orders?
1056:     ) internal returns (uint256 id) {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L32

File: contracts/rubiconPools/BathPair.sol

160       function enforceReserveRatio(
161           address underlyingAsset,
162           address underlyingQuote
163       )
164           internal
165           view
166:          returns (address bathAssetAddress, address bathQuoteAddress)

205:      function _next_id() internal returns (uint256) {

213:      function handleStratOrderAtID(uint256 id) internal {

305       function getIndexFromElement(uint256 uid, uint256[] storage array)
306           internal
307           view
308:          returns (uint256 _index)

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L160-L166

File: contracts/rubiconPools/BathToken.sol

629       function distributeBonusTokenRewards(
630           address receiver,
631           uint256 sharesWithdrawn,
632:          uint256 initialTotalSupply

657:      function _mint(address to, uint256 value) internal {

663:      function _burn(address from, uint256 value) internal {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L629-L632

File: contracts/peripheral_contracts/BathBuddy.sol

149       function _vestingSchedule(uint256 totalAllocation, uint64 timestamp)
150           internal
151           view
152:          returns (uint256)

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L149-L152

9. <array>.length should not be looked up in every loop of a for-loop

The overheads outlined below are PER LOOP, excluding the first loop

  • storage arrays incur a Gwarmaccess (100 gas)
  • memory arrays use MLOAD (3 gas)
  • calldata arrays use CALLDATALOAD (3 gas)

Caching the length changes each of these to a DUP<N> (3 gas), and gets rid of the extra DUP<N> needed to store the stack offset

There are 3 instances of this issue:

File: contracts/rubiconPools/BathPair.sol   #1

311:          for (uint256 index = 0; index < array.length; index++) {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L311

File: contracts/rubiconPools/BathPair.sol   #2

582:          for (uint256 index = 0; index < ids.length; index++) {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L582

File: contracts/rubiconPools/BathToken.sol   #3

635:              for (uint256 index = 0; index < bonusTokens.length; index++) {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L635

10. require()/revert() strings longer than 32 bytes cost extra gas

There are 26 instances of this issue:

File: contracts/RubiconMarket.sol

304           require(
305               _offer.buy_gem.transferFrom(msg.sender, _offer.owner, spend),
306               "_offer.buy_gem.transferFrom(msg.sender, _offer.owner, spend) failed - check that you can pay the fee"
307:          );

308           require(
309               _offer.pay_gem.transfer(msg.sender, quantity),
310               "_offer.pay_gem.transfer(msg.sender, quantity) failed"
311:          );

571:          require(isActive(id), "Offer was deleted or taken, or never existed.");

572           require(
573               isClosed() || msg.sender == getOwner(id) || id == dustId,
574               "Offer can not be cancelled because user is not owner, and market is open, and offer sells required amount of tokens."
575:          );

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L304-L307

File: contracts/rubiconPools/BathHouse.sol

143           require(
144               getBathTokenfromAsset(newBathTokenUnderlying) == address(0),
145               "bathToken already exists for that ERC20"
146:          );

147           require(
148               getBathTokenfromAsset(desiredPairedAsset) != address(0),
149               "bathToken does not exist for that desiredPairedAsset"
150:          );

156           require(
157               newBathTokenUnderlying.transferFrom(
158                   msg.sender,
159                   address(this),
160                   initialLiquidityNew
161               ),
162               "Couldn't transferFrom your initial liquidity - make sure to approve BathHouse.sol"
163:          );

171           require(
172               desiredPairedAsset.transferFrom(
173                   msg.sender,
174                   address(this),
175                   initialLiquidityExistingBathToken
176               ),
177               "Couldn't transferFrom your initial liquidity - make sure to approve BathHouse.sol"
178:          );

397           require(
398               _underlyingERC20 != address(0),
399               "Cant create bathToken for zero address"
400:          );

409           require(
410               newBathTokenImplementation != address(0),
411               "no implementation set for bathTokens"
412:          );

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L143-L146

File: contracts/rubiconPools/BathPair.sol

148           require(
149               IBathHouse(bathHouse).isApprovedStrategist(targetStrategist) ==
150                   true,
151               "you are not an approved strategist - bathPair"
152:          );

174           require(
175               (
176                   IBathToken(bathAssetAddress).underlyingBalance().mul(
177                       IBathHouse(bathHouse).reserveRatio()
178                   )
179               ).div(100) <= IERC20(underlyingAsset).balanceOf(bathAssetAddress),
180               "Failed to meet asset pool reserve ratio"
181:          );

182           require(
183               (
184                   IBathToken(bathQuoteAddress).underlyingBalance().mul(
185                       IBathHouse(bathHouse).reserveRatio()
186                   )
187               ).div(100) <= IERC20(underlyingQuote).balanceOf(bathQuoteAddress),
188               "Failed to meet quote pool reserve ratio"
189:          );

318:          require(assigned, "Didnt Find that element in live list, cannot scrub");

570           require(
571               msg.sender == strategistTrades[id].strategist,
572               "you are not the strategist that made this order"
573:          );

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L148-L152

File: contracts/rubiconPools/BathToken.sol

235           require(
236               msg.sender == bathHouse,
237               "caller is not bathHouse - BathToken.sol"
238:          );

470:          require(_shares == shares, "did not mint expected share count");

510           require(
511               owner == msg.sender,
512               "This implementation does not support non-sender owners from withdrawing user shares"
513:          );

516           require(
517               assetsReceived >= assets,
518               "You cannot withdraw the amount of assets you expected"
519:          );

547           require(
548               owner == msg.sender,
549               "This implementation does not support non-sender owners from withdrawing user shares"
550:          );

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L235-L238

File: contracts/peripheral_contracts/BathBuddy.sol

43            require(
44                beneficiaryAddress != address(0),
45                "VestingWallet: beneficiary is zero address"
46:           );

94            require(
95                msg.sender == beneficiary,
96                "Caller is not the Bath Token beneficiary of these rewards"
97:           );

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L43-L46

File: contracts/RubiconRouter.sol

336           require(
337               msg.value >= max_fill_withFee,
338               "must send as much ETH as max_fill_withFee"
339:          );

390           require(
391               msg.value >= pay_amt,
392               "didnt send enough native ETH for WETH offer"
393:          );

444           require(
445               address(pay_gem) == wethAddress,
446               "trying to cancel a non WETH order"
447:          );

504           require(
505               msg.value >= amtWithFee,
506               "must send enough native ETH to pay as weth and account for fee"
507:          );

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L336-L339

11. keccak256() should only need to be called on a specific string literal once

It should be saved to an immutable variable, and the variable used instead. If the hash is being used as a part of a function selector, the cast to bytes4 should also only be done once

There is 1 instance of this issue:

File: contracts/rubiconPools/BathToken.sol   #1

201                   keccak256(
202                       "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
203:                  ),

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L201-L203

12. Using bools for storage incurs overhead

    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/58f635312aa21f947cae5f8578638a85aa2519f5/contracts/security/ReentrancyGuard.sol#L23-L27 Use uint256(1) and uint256(2) for true/false to avoid a Gwarmaccess (100 gas), and to avoid Gsset (20000 gas) when changing from 'false' to 'true', after having been 'true' in the past

There are 12 instances of this issue:

File: contracts/RubiconMarket.sol

191:      bool locked;

449:      bool public stopped;

526:      bool public buyEnabled = true; //buy enabled

527:      bool public matchingEnabled = true; //true: enable matching,

530:      bool public initialized;

533:      bool public AqueductDistributionLive;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L191

File: contracts/rubiconPools/BathHouse.sol

32:       mapping(address => bool) public approvedStrategists;

35:       bool public initialized;

38:       bool public permissionedStrategists;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L32

File: contracts/rubiconPools/BathPair.sol

32:       bool public initialized;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L32

File: contracts/rubiconPools/BathToken.sol

22:       bool public initialized;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L22

File: contracts/RubiconRouter.sol

23:       bool public started;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L23

13. Use a more recent version of solidity

Use a solidity version of at least 0.8.0 to get overflow protection without SafeMath Use a solidity version of at least 0.8.2 to get simple compiler automatic inlining Use a solidity version of at least 0.8.3 to get better struct packing and cheaper multiple storage reads Use a solidity version of at least 0.8.4 to get custom errors, which are cheaper at deployment than revert()/require() strings Use a solidity version of at least 0.8.10 to have external calls skip contract existence checks if the external call has a return value

There are 5 instances of this issue:

File: contracts/RubiconMarket.sol

7:    pragma solidity =0.7.6;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L7

File: contracts/rubiconPools/BathHouse.sol

7:    pragma solidity =0.7.6;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L7

File: contracts/rubiconPools/BathPair.sol

8:    pragma solidity =0.7.6;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L8

File: contracts/rubiconPools/BathToken.sol

8:    pragma solidity =0.7.6;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L8

File: contracts/RubiconRouter.sol

5:    pragma solidity =0.7.6;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L5

14. Use a more recent version of solidity

Use a solidity version of at least 0.8.2 to get simple compiler automatic inlining Use a solidity version of at least 0.8.3 to get better struct packing and cheaper multiple storage reads Use a solidity version of at least 0.8.4 to get custom errors, which are cheaper at deployment than revert()/require() strings Use a solidity version of at least 0.8.10 to have external calls skip contract existence checks if the external call has a return value

There is 1 instance of this issue:

File: contracts/peripheral_contracts/BathBuddy.sol   #1

2:    pragma solidity >=0.6.0 <0.8.0;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L2

15. Using > 0 costs more gas than != 0 when used on a uint in a require() statement

This change saves 6 gas per instance

There are 6 instances of this issue:

File: contracts/RubiconMarket.sol

400:          require(pay_amt > 0);

402:          require(buy_amt > 0);

985:          require(id > 0);

1002:         require(id > 0);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L400

File: contracts/rubiconPools/BathHouse.sol

111:          require(_reserveRatio > 0);

281:          require(rr > 0);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L111

16. It costs more gas to initialize variables to zero than to let the default of zero be applied

There are 13 instances of this issue:

File: contracts/RubiconMarket.sol

990:          uint256 old_top = 0;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L990

File: contracts/rubiconPools/BathPair.sol

311:          for (uint256 index = 0; index < array.length; index++) {

427:          for (uint256 index = 0; index < quantity; index++) {

480:          for (uint256 index = 0; index < quantity; index++) {

582:          for (uint256 index = 0; index < ids.length; index++) {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L311

File: contracts/rubiconPools/BathToken.sol

635:              for (uint256 index = 0; index < bonusTokens.length; index++) {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L635

File: contracts/RubiconRouter.sol

82:           uint256 lastBid = 0;

83:           uint256 lastAsk = 0;

85:           for (uint256 index = 0; index < topNOrders; index++) {

168:          uint256 currentAmount = 0;

169:          for (uint256 i = 0; i < route.length - 1; i++) {

226:          uint256 currentAmount = 0;

227:          for (uint256 i = 0; i < route.length - 1; i++) {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L82

17. internal functions not called by the contract should be removed to save deployment gas

If the functions are required by an interface, the contract should inherit from that interface and use the override keyword

There are 7 instances of this issue:

File: contracts/RubiconMarket.sol

61:       function max(uint256 x, uint256 y) internal pure returns (uint256 z) {

65:       function imin(int256 x, int256 y) internal pure returns (int256 z) {

69:       function imax(int256 x, int256 y) internal pure returns (int256 z) {

76:       function wmul(uint256 x, uint256 y) internal pure returns (uint256 z) {

441:      function getFeeBPS() internal view returns (uint256) {

959:      function _buys(uint256 id, uint256 amount) internal returns (bool) {

1113      function _offeru(
1114          uint256 pay_amt, //maker (ask) sell how much
1115          ERC20 pay_gem, //maker (ask) sell which token
1116          uint256 buy_amt, //maker (ask) buy how much
1117          ERC20 buy_gem //maker (ask) buy which token
1118:     ) internal returns (uint256 id) {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L61

18. ++i costs less gas than i++, especially when it's used in for-loops (--i/i-- too)

Saves 6 gas per loop

There are 8 instances of this issue:

File: contracts/rubiconPools/BathPair.sol

311:          for (uint256 index = 0; index < array.length; index++) {

427:          for (uint256 index = 0; index < quantity; index++) {

480:          for (uint256 index = 0; index < quantity; index++) {

582:          for (uint256 index = 0; index < ids.length; index++) {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L311

File: contracts/rubiconPools/BathToken.sol

635:              for (uint256 index = 0; index < bonusTokens.length; index++) {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L635

File: contracts/RubiconRouter.sol

85:           for (uint256 index = 0; index < topNOrders; index++) {

169:          for (uint256 i = 0; i < route.length - 1; i++) {

227:          for (uint256 i = 0; i < route.length - 1; i++) {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L85

19. Splitting require() statements that use && saves gas

See this issue which describes the fact that there is a larger deployment gas cost, but with enough runtime calls, the change ends up being cheaper

There are 8 instances of this issue:

File: contracts/RubiconMarket.sol

715           require(
716               !isActive(id) &&
717                   _rank[id].delb != 0 &&
718                   _rank[id].delb < block.number - 10
719:          );

1177          require(
1178              _rank[id].delb == 0 && //assert id is in the sorted list
1179                  isOfferSorted(id)
1180:         );

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L715-L719

File: contracts/rubiconPools/BathPair.sol

120           require(
121               IBathHouse(_bathHouse).getMarket() !=
122                   address(0x0000000000000000000000000000000000000000) &&
123                   IBathHouse(_bathHouse).initialized(),
124               "BathHouse not initialized"
125:          );

346           require(
347               bathAssetAddress != address(0) && bathQuoteAddress != address(0),
348               "tokenToBathToken error"
349:          );

419           require(
420               askNumerators.length == askDenominators.length &&
421                   askDenominators.length == bidNumerators.length &&
422                   bidNumerators.length == bidDenominators.length,
423               "not all order lengths match"
424:          );

471           require(
472               askNumerators.length == askDenominators.length &&
473                   askDenominators.length == bidNumerators.length &&
474                   bidNumerators.length == bidDenominators.length &&
475                   ids.length == askNumerators.length,
476               "not all input lengths match"
477:          );

506           require(
507               _bathAssetAddress != address(0) && _bathQuoteAddress != address(0),
508               "tokenToBathToken error"
509:          );

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L120-L125

File: contracts/rubiconPools/BathToken.sol

740           require(
741               recoveredAddress != address(0) && recoveredAddress == owner,
742               "bathToken: INVALID_SIGNATURE"
743:          );

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L740-L743

20. Usage of uints/ints smaller than 32 bytes (256 bits) incurs overhead

When using elements that are smaller than 32 bytes, your contract’s gas usage may be higher. This is because the EVM operates on 32 bytes at a time. Therefore, if the element is smaller than that, the EVM must use more operations in order to reduce the size of the element from 32 bytes to the desired size.

https://docs.soliditylang.org/en/v0.8.11/internals/layout_in_storage.html Use a larger size then downcast where needed

There are 37 instances of this issue:

File: contracts/RubiconMarket.sol

129:          uint128 pay_amt,

130:          uint128 buy_amt,

131:          uint64 timestamp

140:          uint128 pay_amt,

141:          uint128 buy_amt,

142:          uint64 timestamp

152:          uint128 take_amt,

153:          uint128 give_amt,

154:          uint64 timestamp

163:          uint128 pay_amt,

164:          uint128 buy_amt,

165:          uint64 timestamp

177:          uint64 timestamp

205:          uint64 timestamp;

385:          uint128 pay_amt,

386:          uint128 buy_amt

431:      function take(bytes32 id, uint128 maxTakeAmount) external virtual {

475:      function getTime() public view returns (uint64) {

584:          uint128 pay_amt,

585:          uint128 buy_amt

590:      function take(bytes32 id, uint128 maxTakeAmount) public override {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L129

File: contracts/rubiconPools/BathHouse.sol

52:       uint8 public bpsToStrategists;

235:          int128 _shapeCoefNum

359:      function getBPSToStrats() public view returns (uint8) {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L52

File: contracts/rubiconPools/BathPair.sol

35:       int128 internal deprecatedStorageVarKept420Proxy;

115:      function initialize(uint256 _maxOrderSizeBPS, int128 _shapeCoefNum)

512:          uint16 stratReward = IBathHouse(_bathHouse).getBPSToStrats();

542:          uint24 _poolFee

545:          uint16 stratRewardBPS = IBathHouse(bathHouse).getBPSToStrats();

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L35

File: contracts/rubiconPools/BathToken.sol

27:       uint8 public decimals;

718:          uint8 v,

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L27

File: contracts/peripheral_contracts/BathBuddy.sol

32:       uint64 public start;

33:       uint64 public duration;

40:           uint64 startTimestamp,

41:           uint64 durationSeconds

133:      function vestedAmount(address token, uint64 timestamp)

149:      function _vestingSchedule(uint256 totalAllocation, uint64 timestamp)

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L32

21. Using private rather than public for constants, saves gas

If needed, the value can be read from the verified contract source code. Savings are due to the compiler not having to create non-payable getter functions for deployment calldata, and not adding another entry to the method ID table

There is 1 instance of this issue:

File: contracts/rubiconPools/BathToken.sol   #1

70        bytes32 public constant PERMIT_TYPEHASH =
71:           0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L70-L71

22. Don't compare boolean expressions to boolean literals

if (<x> == true) => if (<x>), if (<x> == false) => if (!<x>)

There are 4 instances of this issue:

File: contracts/rubiconPools/BathHouse.sol   #1

242:              IBathPair(_bathPairAddress).initialized() != true,

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L242

File: contracts/rubiconPools/BathHouse.sol   #2

372:              approvedStrategists[wouldBeStrategist] == true ||

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L372

File: contracts/rubiconPools/BathPair.sol   #3

149               IBathHouse(bathHouse).isApprovedStrategist(targetStrategist) ==
150:                  true,

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L149-L150

File: contracts/rubiconPools/BathToken.sol   #4

228:              IBathHouse(bathHouse).isApprovedPair(msg.sender) == true,

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L228

23. Duplicated require()/revert() checks should be refactored to a modifier or function

Saves deployment costs

There are 12 instances of this issue:

File: contracts/RubiconMarket.sol

216:          require(isActive(id));

595:          require(cancel(uint256(id)));

591:          require(buy(uint256(id), maxTakeAmount));

460:          require(!isClosed());

645:          require(!locked, "Reentrancy attempt");

1119:         require(_dust[address(pay_gem)] <= pay_amt);

1209:         require(!isOfferSorted(id)); //make sure offer id is not in sorted offers list

879:              require(offerId != 0);

1002:         require(id > 0);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L216

File: contracts/rubiconPools/BathToken.sol

547           require(
548               owner == msg.sender,
549               "This implementation does not support non-sender owners from withdrawing user shares"
550:          );

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L547-L550

File: contracts/RubiconRouter.sol

247:          require(currentAmount >= buy_amt_min, "didnt clear buy_amt_min");

481:          require(target == ERC20(wethAddress), "target pool not weth pool");

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L247

24. Division by two should use bit shifting

<x> / 2 is the same as <x> >> 1. The DIV opcode costs 5 gas, whereas SHR only costs 3 gas

There are 4 instances of this issue:

File: contracts/RubiconMarket.sol   #1

77:           z = add(mul(x, y), WAD / 2) / WAD;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L77

File: contracts/RubiconMarket.sol   #2

81:           z = add(mul(x, y), RAY / 2) / RAY;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L81

File: contracts/RubiconMarket.sol   #3

85:           z = add(mul(x, WAD), y / 2) / y;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L85

File: contracts/RubiconMarket.sol   #4

89:           z = add(mul(x, RAY), y / 2) / y;

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L89

25. require() or revert() statements that check input arguments should be at the top of the function

Checks that involve constants should come before checks that involve state variables

There are 4 instances of this issue:

File: contracts/RubiconMarket.sol   #1

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

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L283

File: contracts/RubiconMarket.sol   #2

646:          require(_dust[address(pay_gem)] <= pay_amt);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L646

File: contracts/rubiconPools/BathHouse.sol   #3

110:          require(_reserveRatio <= 100);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L110

File: contracts/rubiconPools/BathHouse.sol   #4

111:          require(_reserveRatio > 0);

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L111

26. Empty blocks should be removed or emit something

The code should be refactored such that they no longer exist, or the block should do something useful, such as emitting an event or reverting. If the contract is meant to be extended, the contract should be abstract and the function signatures be added without any default implementation. If the block is an empty if-statement block to avoid doing subsequent checks in the else-if/else conditions, the else-if/else conditions should be nested under the negation of the if-statement, because they involve different classes of checks, which may lead to the introduction of errors when the code is later modified (if(x){}else if(y){...}else{...} => if(!x){if(y){...}else{...}})

There are 3 instances of this issue:

File: contracts/peripheral_contracts/BathBuddy.sol   #1

69:       receive() external payable {}

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L69

File: contracts/RubiconRouter.sol   #2

37:       receive() external payable {}

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L37

File: contracts/RubiconRouter.sol   #3

39:       fallback() external payable {}

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L39

27. Superfluous event fields

block.timestamp and block.number are added to event information by default so adding them manually wastes gas

There are 8 instances of this issue:

File: contracts/RubiconMarket.sol

131:          uint64 timestamp

142:          uint64 timestamp

154:          uint64 timestamp

165:          uint64 timestamp

177:          uint64 timestamp

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L131

File: contracts/rubiconPools/BathHouse.sol

69:           uint256 timestamp,

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L69

File: contracts/rubiconPools/BathPair.sol

88:           uint256 timestamp,

108:          uint256 timestamp

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L88

28. Functions guaranteed to revert when called by normal users can be marked payable

If a function modifier such as onlyOwner is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided. The extra opcodes avoided are CALLVALUE(2),DUP1(3),ISZERO(3),PUSH2(3),JUMPI(10),PUSH1(3),DUP1(3),REVERT(0),JUMPDEST(1),POP(2), which costs an average of about 21 gas per call to the function, in addition to the extra deployment cost

There are 35 instances of this issue:

File: contracts/rubiconPools/BathHouse.sol

208       function createBathToken(ERC20 underlyingERC20, address _feeAdmin)
209           external
210           onlyAdmin
211:          returns (address newBathTokenAddress)

217       function adminWriteBathToken(ERC20 overwriteERC20, address newBathToken)
218           external
219:          onlyAdmin

232       function initBathPair(
233           address _bathPairAddress,
234           uint256 _maxOrderSizeBPS,
235           int128 _shapeCoefNum
236:      ) external onlyAdmin returns (address newPair) {

253:      function setBathHouseAdmin(address newAdmin) external onlyAdmin {

259:      function setNewBathTokenImplementation(address newImplementation) external onlyAdmin {

264:      function approveStrategist(address strategist) public onlyAdmin {

269:      function setPermissionedStrategists(bool _new) external onlyAdmin {

274:      function setCancelTimeDelay(uint256 value) external onlyAdmin {

279:      function setReserveRatio(uint256 rr) external onlyAdmin {

286       function setBathTokenMarket(address bathToken, address newMarket)
287           external
288:          onlyAdmin

294       function setBonusToken(address bathToken, address newBonusToken)
295           external
296:          onlyAdmin

302       function setBathTokenBathHouse(address bathToken, address newAdmin)
303           external
304:          onlyAdmin

310       function setBathTokenFeeBPS(address bathToken, uint256 newBPS)
311           external
312:          onlyAdmin

319       function bathTokenApproveSetMarket(address targetBathToken)
320           external
321:          onlyAdmin

327       function setBathTokenFeeTo(address bathToken, address feeTo)
328           external
329:          onlyAdmin

335:      function setMarket(address newMarket) external onlyAdmin {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L208-L211

File: contracts/rubiconPools/BathPair.sol

324       function placeMarketMakingTrades(
325           address[2] memory tokenPair, // ASSET, Then Quote
326           uint256 askNumerator, // Quote / Asset
327           uint256 askDenominator, // Asset / Quote
328           uint256 bidNumerator, // size in ASSET
329           uint256 bidDenominator // size in QUOTES
330:      ) public onlyApprovedStrategist(msg.sender) returns (uint256 id) {

412       function batchMarketMakingTrades(
413           address[2] memory tokenPair, // ASSET, Then Quote
414           uint256[] memory askNumerators, // Quote / Asset
415           uint256[] memory askDenominators, // Asset / Quote
416           uint256[] memory bidNumerators, // size in ASSET
417           uint256[] memory bidDenominators // size in QUOTES
418:      ) external onlyApprovedStrategist(msg.sender) {

440       function requote(
441           uint256 id,
442           address[2] memory tokenPair, // ASSET, Then Quote
443           uint256 askNumerator, // Quote / Asset
444           uint256 askDenominator, // Asset / Quote
445           uint256 bidNumerator, // size in ASSET
446           uint256 bidDenominator // size in QUOTES
447:      ) public onlyApprovedStrategist(msg.sender) {

463       function batchRequoteOffers(
464           uint256[] memory ids,
465           address[2] memory tokenPair, // ASSET, Then Quote
466           uint256[] memory askNumerators, // Quote / Asset
467           uint256[] memory askDenominators, // Asset / Quote
468           uint256[] memory bidNumerators, // size in ASSET
469           uint256[] memory bidDenominators // size in QUOTES
470:      ) external onlyApprovedStrategist(msg.sender) {

493       function rebalancePair(
494           uint256 assetRebalAmt, //amount of ASSET in the quote buffer
495           uint256 quoteRebalAmt, //amount of QUOTE in the asset buffer
496           address _underlyingAsset,
497           address _underlyingQuote
498:      ) external onlyApprovedStrategist(msg.sender) {

535       function tailOff(
536           address targetPool,
537           address tokenToHandle,
538           address targetToken,
539           address _stratUtil, // delegatecall target
540           uint256 amount, //fill amount to handle
541           uint256 hurdle, //must clear this on tail off
542           uint24 _poolFee
543:      ) external onlyApprovedStrategist(msg.sender) {

566       function scrubStrategistTrade(uint256 id)
567           public
568:          onlyApprovedStrategist(msg.sender)

578       function scrubStrategistTrades(uint256[] memory ids)
579           external
580:          onlyApprovedStrategist(msg.sender)

591       function strategistBootyClaim(address asset, address quote)
592           external
593:          onlyApprovedStrategist(msg.sender)

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L324-L330

File: contracts/rubiconPools/BathToken.sol

245:      function setMarket(address newRubiconMarket) external onlyBathHouse {

250:      function setBathHouse(address newBathHouse) external onlyBathHouse {

255:      function approveMarket() external onlyBathHouse {

260:      function setFeeBPS(uint256 _feeBPS) external onlyBathHouse {

265:      function setFeeTo(address _feeTo) external onlyBathHouse {

270:      function setBonusToken(address newBonusERC20) external onlyBathHouse {

279:      function cancel(uint256 id, uint256 amt) external onlyPair {

294:      function removeFilledTradeAmount(uint256 amt) external onlyPair {

306       function placeOffer(
307           uint256 pay_amt,
308           ERC20 pay_gem,
309           uint256 buy_amt,
310           ERC20 buy_gem
311:      ) external onlyPair returns (uint256) {

346       function rebalance(
347           address destination,
348           address filledAssetToRebalance, /* sister or fill asset */
349           uint256 stratProportion,
350           uint256 rebalAmt
351:      ) external onlyPair {

https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L245

#0 - liveactionllama

2022-05-27T16:44:23Z

Warden created this issue as a placeholder, because their submission was too large for the contest form. They then emailed their md file to our team on 05/27/2022. I've updated this issue with their md file content.

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter