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: 11/99
Findings: 9
Award: $1,623.35
🌟 Selected for report: 1
🚀 Solo Findings: 1
🌟 Selected for report: xiaoming90
Also found by: 0xNoah, PP1004, hubble, pauliax, reassor, sashik_eth, shenwilly, sseefried
https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathToken.sol#L80 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathToken.sol#L642
rewardsVestingWallet in BathToken is never initialized thus release will never happen:
/// @notice Address of the OZ Vesting Wallet which acts as means to vest bonusToken incentives to pool HODLers IBathBuddy public rewardsVestingWallet;
When calling distributeBonusTokenRewards this will be skipped:
if (rewardsVestingWallet != IBathBuddy(0)) { rewardsVestingWallet.release( (token), receiver, sharesWithdrawn, initialTotalSupply, feeBPS ); }
Consider either initializing rewardsVestingWallet or restructuring this functionality.
#0 - bghughes
2022-06-03T22:08:31Z
Duplicate of #168
🌟 Selected for report: berndartmueller
Also found by: 0x1f8b, 0xDjango, 0xsomeone, ACai, Bahurum, BouSalman, CertoraInc, Deivitto, Dravee, GimelSec, IllIllI, JMukesh, Kaiziron, PP1004, Ruhum, SmartSek, VAD37, WatchPug, _Adam, aez121, antonttc, blockdev, broccolirob, camden, cccz, cryptphi, defsec, dipp, ellahi, fatherOfBlocks, gzeon, horsefacts, ilan, jayjonah8, joestakey, kenta, kenzo, minhquanym, oyc_109, pauliax, pedroais, peritoflores, sashik_eth, shenwilly, simon135, throttle, xiaoming90, z3s
0.1022 USDC - $0.10
Judge has assessed an item in Issue #439 as Medium risk. The relevant finding follows:
#0 - HickupHH3
2022-06-27T14:12:46Z
The current best practice is to use safe ERC20 library for token interactions (safeApprove and safeTransfer) dup of #316
BathBuddy has an empty receive function but there is no function to get the native asset out, thus it will be stuck in the contract forever. Release function for native token was present in VestingWallet but removed in BathBuddy: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.6/contracts/finance/VestingWallet.sol#L85-L89
Consider removing this function to prevent accidental loss of a native asset.
#0 - bghughes
2022-05-30T22:34:41Z
Duplicate of #78
🌟 Selected for report: IllIllI
Also found by: 0xDjango, Dravee, SmartSek, StErMi, oyc_109, pauliax, rotcivegaf, sashik_eth, shenwilly, xiaoming90
23.3384 USDC - $23.34
Judge has assessed an item in Issue #439 as Medium risk. The relevant finding follows:
#0 - HickupHH3
2022-06-27T14:13:49Z
The array of bonusTokens only grows, elements can't be removed. Consider introducing a reasonable upper limit or remove function, otherwise it may grow so large to that calls to distributeBonusTokenRewards will start reverting due to block gas limitations.
dup of #249
🌟 Selected for report: hansfriese
Also found by: MiloTruck, kenzo, oyc_109, pauliax, unforgiven, xiaoming90
53.3571 USDC - $53.36
Judge has assessed an item in Issue #439 as Medium risk. The relevant finding follows:
#0 - HickupHH3
2022-06-27T14:18:31Z
Lack of re-entrancy protection, e.g. function strategistBootyClaim first transfers tokens and only then updates the state. Tokens may contain transfer hooks that can be used to exploit re-entrancy.
dup of #157
67.7551 USDC - $67.76
Judge has assessed an item in Issue #439 as Medium risk. The relevant finding follows:
#0 - HickupHH3
2022-06-27T14:15:14Z
In tailOff a strategist can choose any _stratUtil target. A malicious target can drain the tokens. Consider having a whitelist, at least temporary, and you can later disable it if everything goes smoothly.
dup of #211
🌟 Selected for report: pauliax
892.4522 USDC - $892.45
https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathPair.sol#L128 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathHouse.sol#L335-L337
RubiconMarketAddress in BathPair is initialized only once:
RubiconMarketAddress = IBathHouse(_bathHouse).getMarket();
but market can change in Bath house:
/// @notice Admin-only function to set a Bath Token's target Rubicon Market function setMarket(address newMarket) external onlyAdmin { RubiconMarketAddress = newMarket; }
Thus it will get out of sync. Also, the comment says that it changes Bath Token's target Rubicon market but actually it updates its own instance variable.
I think RubiconMarketAddress should sync between BathPair and BathHouse.
#0 - bghughes
2022-06-03T21:25:20Z
Not needed in practice
#1 - HickupHH3
2022-06-23T15:43:58Z
Issue stands because there's a non-zero possibility of it happening.
🌟 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
79.9616 USDC - $79.96
using SafeMath for uint16;
Also, uint16 does not make any sense, nor give any improvements here:
uint16 stratReward = IBathHouse(_bathHouse).getBPSToStrats(); function getBPSToStrats() external view returns (uint8);
Consider storing stratReward in uint256 and removing using SafeMath for uint16.
/// @notice A variable time delay after which a strategist must return funds to the Bath Token uint256 public timeDelay;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
/// @notice Admin-only function to set a Bath Token's timeDelay function setBathTokenMarket(address bathToken, address newMarket)
From the whitepaper: "BathTokens can be redeemed for the underlying tokens at any time". However, there exists a reserve ratio, that is initially set to 50%, meaning not all the liquidity is available to redeem at any time. Please keep your users informed and aware of this.
From the whitepaper: FeeBPS "The fee is currently set to zero but could change in the near future"
feeBPS = 3; //Fee set to 3 BPS initially
assembly { chainId := chainid() }
newBathTokenUnderlying.approve(newOne, initialLiquidityNew); IERC20(asset).transfer(msg.sender, booty);
require(uint128(pay_amt) == pay_amt); require(uint128(buy_amt) == buy_amt);
Also, there are many unchecked casts, e.g.:
take(bytes32(offerId), uint128(offers[offerId].pay_amt)); //We take the whole offer take(bytes32(offerId), uint128(baux)); //We take the portion of the offer that we need
Make sure that large values will not be truncated due to explicit casting and utilize safe casts where possible.
/// @notice Array of Bonus ERC-20 tokens that are given as liquidity incentives to pool withdrawers address[] public bonusTokens; /// @notice Admin-only function to add a bonus token to bonusTokens for pool incentives function setBonusToken(address newBonusERC20) external onlyBathHouse { bonusTokens.push(newBonusERC20); } for (uint256 index = 0; index < bonusTokens.length; index++)
Lack of re-entrancy protection, e.g. function strategistBootyClaim first transfers tokens and only then updates the state. Tokens may contain transfer hooks that can be used to exploit re-entrancy. Either make sure you 100% trust the callers (usually strategists) or implement a re-entrancy guard.
In tailOff a strategist can choose any _stratUtil target. A malicious target can drain the tokens. Consider having a whitelist, at least temporary, and you can later disable it if everything goes smoothly.
Contracts use Solidity version that does not protect from overflow / underflow by default, however, there are regular math operations that could be exploited, e.g.:
uint256 askDelta = info.askPayAmt - offer1.pay_amt; uint256 bidDelta = info.bidPayAmt - offer2.pay_amt;
Or:
uint256 releasable = vestedAmount( address(token), uint64(block.timestamp) ) - released(address(token)); _erc20Released[address(token)] += amount;
Consider using SafeMath operations everywhere.
🌟 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
123.1258 USDC - $123.13
uint256 private _released; event EtherReleased(uint256 amount);
name = "Rubicon Bath House";
In openBathTokenSpawnAndSignal address newOne = _createBathToken...
can be replaced with newBathToken = _createBathToken...
Would be a bit cheapier to just directly use scrubStrategistTrade(ids[index]) here:
uint256 _id = ids[index]; scrubStrategistTrade(_id);
bool assigned = false; for (uint256 index = 0; index < array.length; index++) { if (uid == array[index]) { _index = index; assigned = true; return _index; } } require(assigned, "Didnt Find that element in live list, cannot scrub");
Thus it would be cheapier to replace this code snippet with something like this:
for (uint256 index = 0; index < array.length; index++) { if (uid == array[index]) { return index; } } revert("Didnt Find that element in live list, cannot scrub");
require( bathAssetAddress != address(0) && bathQuoteAddress != address(0), "tokenToBathToken error" ); IBathToken(bathAssetAddress).underlyingBalance() IBathToken(bathQuoteAddress).underlyingBalance()
function openBathTokenSpawnAndSignal require( getBathTokenfromAsset(newBathTokenUnderlying) == address(0), "bathToken already exists for that ERC20" );
_createBathToken:
// Check that it isn't already logged in the registry require( tokenToBathToken[_underlyingERC20] == address(0), "bathToken already exists" );
function batchMarketMakingTrades has onlyApprovedStrategist(msg.sender) modifier, but this modifier is also applied every time in placeMarketMakingTrades. Consider extracting common functionality into a separate internal function to avoid useless repeated checks. Similarly, with requote and batchRequoteOffers. Also, scrubStrategistTrade and scrubStrategistTrades.
Results of repeated external calls should be cached, e.g.:
IBathHouse(bathHouse).reserveRatio()
called twice:
require( ( IBathToken(bathAssetAddress).underlyingBalance().mul( IBathHouse(bathHouse).reserveRatio() ) ).div(100) <= IERC20(underlyingAsset).balanceOf(bathAssetAddress), "Failed to meet asset pool reserve ratio" ); require( ( IBathToken(bathQuoteAddress).underlyingBalance().mul( IBathHouse(bathHouse).reserveRatio() ) ).div(100) <= IERC20(underlyingQuote).balanceOf(bathQuoteAddress), "Failed to meet quote pool reserve ratio" );
IBathHouse(_bathHouse).getMarket()
called twice:
require( IBathHouse(_bathHouse).getMarket() != address(0x0000000000000000000000000000000000000000) && IBathHouse(_bathHouse).initialized(), "BathHouse not initialized" ); bathHouse = _bathHouse; RubiconMarketAddress = IBathHouse(_bathHouse).getMarket();
outOffersByStrategist[_asset][_quote][info.strategist]
accessed twice:uint256 target = getIndexFromElement( id, outOffersByStrategist[_asset][_quote][info.strategist] ); uint256[] storage current = outOffersByStrategist[_asset][_quote][ info.strategist ];
uint128(pay_amt)
and uint128(buy_amt)
casted twice:require(uint128(pay_amt) == pay_amt); require(uint128(buy_amt) == buy_amt); emit LogMake( bytes32(id), keccak256(abi.encodePacked(pay_gem, buy_gem)), msg.sender, pay_gem, buy_gem, uint128(pay_amt), uint128(buy_amt), uint64(block.timestamp) );