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
Rank: 64/99
Findings: 2
Award: $83.35
๐ Selected for report: 0
๐ Solo Findings: 0
๐ Selected for report: IllIllI
Also found by: 0x1337, 0x1f8b, 0x4non, 0xDjango, 0xKitsune, 0xNazgul, 0xf15ers, ACai, AlleyCat, Bahurum, BouSalman, CertoraInc, Chom, Dravee, ElKu, FSchmoede, Funen, GimelSec, Hawkeye, JC, JMukesh, Kaiziron, MaratCerby, Metatron, PP1004, Picodes, Ruhum, SmartSek, StErMi, TerrierLover, UVvirus, UnusualTurtle, WatchPug, Waze, _Adam, asutorufos, berndartmueller, blackscale, blockdev, broccolirob, c3phas, catchup, cryptphi, csanuragjain, defsec, delfin454000, dipp, eccentricexit, ellahi, fatherOfBlocks, gzeon, hansfriese, horsefacts, hubble, ilan, joestakey, kebabsec, minhquanym, oyc_109, parashar, pauliax, rotcivegaf, sach1r0, sashik_eth, shenwilly, simon135, sorrynotsorry, sseefried, throttle, unforgiven, xiaoming90
52.0369 USDC - $52.04
There is a inconsistent use of revert strings for the the different contractsโ initialize()
methods. The contract RubiConMarket.sol
is the only one having a revert string at L552:
require(!initialized, "contract is already initialized");
The same revert string should be used for the initialize()
methods in the contracts:
In the contract RubiConMarket.sol
at L93-L110 there is a whole contract commented out. If this is a left-over then delete it to remove any unused code.
There is a inconsistent use of named and unnamed return variables in the code base. A good example is two methods isApprovedStrategist()
and isApprovedPair()
in BathHouse.sol
, which both do some checks and returns a bool
. However, one of them uses a named return value, where the other does not. Since the methods are similar, there should be a consistent use.
The method isApprovedStrategist()
found at L366 has the signature:
function isApprovedStrategist(address wouldBeStrategist) external view returns (bool)
And the method isApprovedPair()
found at L382 has the signature:
function isApprovedPair(address pair) public view returns (bool outcome)
Either make both methods return (bool)
or a named variable (bool outcome)
. This is applicable to many other occurrences in the code base.
๐ Selected for report: IllIllI
Also found by: 0x1f8b, 0x4non, 0xDjango, 0xNazgul, 0xf15ers, 0xkatana, Chom, DavidGialdi, Dravee, ElKu, FSchmoede, Fitraldys, Funen, GimelSec, JC, Kaiziron, MaratCerby, Metatron, MiloTruck, Picodes, Randyyy, RoiEvenHaim, SmartSek, Tomio, UnusualTurtle, WatchPug, Waze, _Adam, antonttc, asutorufos, berndartmueller, blackscale, blockdev, c3phas, catchup, csanuragjain, defsec, delfin454000, ellahi, fatherOfBlocks, gzeon, hansfriese, ilan, joestakey, minhquanym, oyc_109, pauliax, pedroais, reassor, rfa, rotcivegaf, sach1r0, samruna, sashik_eth, simon135, z3s
31.3128 USDC - $31.31
Several of the for-loops in the code can be optimized to cost less gas. One of the examples is the for-loop found in BathPair.sol
at L311-L317:
for (uint256 index = 0; index < array.length; index++) { if (uid == array[index]) { _index = index; assigned = true; return _index; } }
4 modifications can be done to this loop to save gas:
i
to 0, since this is automatically done when declaring a uint
(the same argument as G-01).++i
is cheaper than postincrement i++
, since the latter will make use of a temporary variable to store the returned value in before incrementing it. This can be saved by doing a preincrement, which increments the value and then returns it.i
.With the above suggestions the for-loop above can be rewritten into:
uint256 arrayLength = array.length; for (uint256 index; index < arrayLength;) { if (uid == array[index]) { _index = index; assigned = true; return _index; } unchecked { ++index; } }
If possible keep revert strings at a maximum of 32 bytes. Strings are stored in slots of 32 bytes, and hence the length of the revert string should be at max 32 bytes to fit inside 1 slot. If the string is just 33 bytes it will occupy 2 slots (64 bytes). Keeping the string size at 32 bytes or below will save gas on both deployment and when the revert condition is met.
Examples revert strings longer than 32 bytes:
RubiconMarket.sol L306: "_offer.buy_gem.transferFrom(msg.sender, _offer.owner, spend) failed - check that you can pay the fee" RubiconMarket.sol L574: "Offer can not be cancelled because user is not owner, and market is open, and offer sells required amount of tokens." RubiconRouter.sol L338: "must send as much ETH as max_fill_withFee" RubiconRouter.sol L392: "didnt send enough native ETH for WETH offer" RubiconRouter.sol L506: "must send enough native ETH to pay as weth and account for fee" BathBuddy.sol L96: "Caller is not the Bath Token beneficiary of these rewards"
The above is a non-exhaustive list and more occurrences can be found throughout the code.
It is more expensive to compare a boolean to a constant true/false
than just evaluate the boolean it self, and both options will give the same result. Therefore, do not use == true
or == false
in require
or if
statements.
An example can be found in BathToken.sol
at L228:
modifier onlyPair() { require( IBathHouse(bathHouse).isApprovedPair(msg.sender) == true, "not an approved pair - bathToken" ); _; }
This should instead be:
modifier onlyPair() { require( IBathHouse(bathHouse).isApprovedPair(msg.sender), "not an approved pair - bathToken" ); _; }
This should, with the optimizer set to 200 as in your code, save 6 gas. This is not a lot, but the case explained above was found quite a lot of places, and hence the total gas saving can be significant higher.
The method isApprovedStrategist()
in the contract BathHouse.sol
can be simplified, since it makes use of unneeded boolean constants.
The method can be found at L366-L379:
function isApprovedStrategist(address wouldBeStrategist) external view returns (bool) { if ( approvedStrategists[wouldBeStrategist] == true || !permissionedStrategists ) { return true; } else { return false; } }
Can be simplified into:
function isApprovedStrategist(address wouldBeStrategist) external view returns (bool) { return (approvedStrategists[wouldBeStrategist] == true || !permissionedStrategists) }
This should, with the optimizer set to 200, save 9 gas
per call to the method.
The method isApprovedPair()
in the contract BathHouse.sol
can be simplified, since it makes use of unneeded boolean constants.
The method can be found at L382-L384:
function isApprovedPair(address pair) public view returns (bool outcome) { pair == approvedPairContract ? outcome = true : outcome = false; }
Can be simplified into:
function isApprovedPair(address pair) public view returns (bool outcome) { outcome = (pair == approvedPairContract); }
This should, with the optimizer set to 200, save 24 gas
per call to the method.
The method scrubStrategistTrade()
in the contract BathPair.sol
has the modifier onlyApprovedStrategist()
, however, this is redundant due to the remaining logic in the method.
The method can be found at L566-L575 and looks like this:
function scrubStrategistTrade(uint256 id) public onlyApprovedStrategist(msg.sender) { require( msg.sender == strategistTrades[id].strategist, "you are not the strategist that made this order" ); handleStratOrderAtID(id); }
The require statement makes the modifier onlyApprovedStrategist()
redundant, since the only way to get into the array strategistTrades[]
is through the method placeMarketMakingTrades()
, which also haves the onlyApprovedStrategist()
modifier.
So there is no chance, that a user not being a approved strategist can get through the require statement above. Because of this the execution of the modifier can be left out to save some gas.
However, it is not possible to leave out the require statement instead of the modifier, since the user being a strategist of a trade implies the user is a approved strategist, but not vice versa.