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: 14/99
Findings: 9
Award: $1,207.28
๐ Selected for report: 1
๐ Solo Findings: 1
8.2687 USDC - $8.27
https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L35 6 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L374 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L434 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L451 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L491 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L548
Transfer ETH by using transfer()
may cause this transaction to fail.
In EIP-1884:
In many cases, a recipient of ether
from a CALL
will want to issue a LOG
. The LOG
operation costs 375
plus 375
per topic. If the LOG
also wants to do an SLOAD
, this change may make some such transfers fail.
RubiconRouter.sol:356: msg.sender.transfer(delta); RubiconRouter.sol:374: msg.sender.transfer(buy_amt); // Return native ETH RubiconRouter.sol:434: msg.sender.transfer(delta); RubiconRouter.sol:451: msg.sender.transfer(pay_amt); RubiconRouter.sol:491: msg.sender.transfer(withdrawnWETH); RubiconRouter.sol:548: msg.sender.transfer(fill);
None
Use call{value: amount}()
instead of transfer
:
(bool success, ) = payable(msg.sender).call{value: amount}(""); require(success, "Address: unable to send value, recipient may have reverted");
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol#L60
#0 - bghughes
2022-06-04T21:27:11Z
I don't know enough to know if it's a good issue or not. Seems plausible but IDK the severity
#1 - HickupHH3
2022-06-21T04:18:16Z
Duplicate of #82
๐ 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
https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L251 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L303 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L320 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L348 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L377 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L406 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L471
Some ERC20 returns false rather than reverting when transfer fails. This will cause the transaction to execute successfully but users donโt successfully get the token.
In RubiconRouter.sol, users may lose tokens if transferring tokens to users without check return value:
RubiconRouter.sol:251: ERC20(route[route.length - 1]).transfer(to, currentAmount); RubiconRouter.sol:303: ERC20(buy_gem).transfer(msg.sender, fill); RubiconRouter.sol:320: ERC20(buy_gem).transfer(msg.sender, fill); RubiconRouter.sol:348: ERC20(buy_gem).transfer(msg.sender, buy_amt); RubiconRouter.sol:377: ERC20(pay_gem).transfer(msg.sender, max_fill_amount - fill); RubiconRouter.sol:406: ERC20(buy_gem).transfer(msg.sender, _after - _before); RubiconRouter.sol:471: ERC20(targetPool).transfer(msg.sender, newShares);
Other transfer
should check:
RubiconRouter.sol:202: ERC20(route[0]).transferFrom( RubiconRouter.sol:274: ERC20(route[0]).transferFrom( RubiconRouter.sol:366: ERC20(pay_gem).transferFrom(msg.sender, address(this), max_fill_amount); //transfer pay here RubiconRouter.sol:419: ERC20(pay_gem).transferFrom(msg.sender, address(this), pay_amt); RubiconRouter.sol:486: IBathToken(targetPool).transferFrom(msg.sender, address(this), shares); peripheral_contracts/BathBuddy.sol:114: token.transfer(recipient, amountWithdrawn); rubiconPools/BathPair.sol:601: IERC20(asset).transfer(msg.sender, booty); rubiconPools/BathPair.sol:615: IERC20(quote).transfer(msg.sender, booty); rubiconPools/BathToken.sol:353: IERC20(filledAssetToRebalance).transfer( rubiconPools/BathToken.sol:602: underlyingToken.transfer(feeTo, _fee); rubiconPools/BathToken.sol:605: underlyingToken.transfer(receiver, amountWithdrawn);
None
Use SafeERC20: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol
using SafeERC20 for IERC20;
#0 - bghughes
2022-06-04T21:36:45Z
Duplicate of #316
๐ Selected for report: cccz
Also found by: AlleyCat, GimelSec, IllIllI, Ruhum, berndartmueller, csanuragjain, dipp, fatherOfBlocks, gzeon, horsefacts, pedroais, shenwilly
https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L462 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L504-L507
If a user sends more ETH than the user has to, the contract just accepts it. The user will lose more ETH accidentally.
https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L462
require(msg.value >= amount, "didnt send enough eth");
https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L504-L507
require( msg.value >= amtWithFee, "must send enough native ETH to pay as weth and account for fee" );
None
Use ==
rather than >=
, or refund the remaining ETH.
#0 - bghughes
2022-06-04T22:36:00Z
Duplicate of #15
๐ Selected for report: xiaoming90
Also found by: GimelSec, IllIllI, MaratCerby, PP1004, WatchPug, berndartmueller, blockdev, ilan
https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L392
Some ERC20 tokens have a fee on each transfer
. The protocol doesnโt handle the fee when transferring this kind of ERC20 tokens, leading to the inconsistent amount of token actually received in the contract. This will cause users to fail to execute functions due to not enough tokens in the protocol, or the protocol will lose more tokens to compensate users for the execution.
In the offer
function, the amount of the token is recorded in pay_amt
. However, if the ERC20(pay_gem)
collects a fee on every transfer. The actual amount of token received is not equal to pay_amt
. info.pay_amt = pay_amt;
will record more amount of token than it received.
392 function offer( ... 407 info.pay_amt = pay_amt; ... 416 require(pay_gem.transferFrom(msg.sender, address(this), pay_amt));
None
Use balanceAfter - balanceBefore
rather than using amount
directly:
function retrieveTokens(address sender, uint256 amount) public { uint256 balanceBefore = deflationaryToken.balanceOf(address(this)); deflationaryToken.transferFrom(sender, address(this), amount); uint256 balanceAfter = deflationaryToken.balanceOf(address(this)); amount = balanceAfter.sub(balanceBefore); }
#0 - bghughes
2022-06-04T01:15:30Z
Duplicate of #112
๐ Selected for report: PP1004
Also found by: GimelSec, camden, unforgiven
162.6494 USDC - $162.65
It will be broken if users try to call openBathTokenSpawnAndSignal
on rubiconPools/BathHouse.sol.
Users can call openBathTokenSpawnAndSignal
with parameters newBathTokenUnderlying
and initialLiquidityNew
:
require( newBathTokenUnderlying.transferFrom( msg.sender, address(this), initialLiquidityNew ), "Couldn't transferFrom your initial liquidity - make sure to approve BathHouse.sol" ); newBathTokenUnderlying.approve(newOne, initialLiquidityNew); // Deposit assets and send Bath Token shares to msg.sender IBathToken(newOne).deposit(initialLiquidityNew, msg.sender);
In deposit
of BathToken.sol, it will call underlyingToken transfer:
underlyingToken.transferFrom(msg.sender, address(this), assets);
If the underlyingToken is a fee-on-transfer token, it will revert because assets are not enough.
None
Use balanceAfter - balanceBefore
rather than using amount
directly:
function retrieveTokens(address sender, uint256 amount) public { uint256 balanceBefore = deflationaryToken.balanceOf(address(this)); deflationaryToken.transferFrom(sender, address(this), amount); uint256 balanceAfter = deflationaryToken.balanceOf(address(this)); amount = balanceAfter.sub(balanceBefore); }
#0 - bghughes
2022-06-03T20:45:02Z
True but we likely won't support any fee-on-transfer tokens for BathTokens. Are there any reputable examples at market?
#1 - HickupHH3
2022-06-23T15:03:12Z
USDT is potentially a FoT, but I'm 99% sure they will never turn it on because it'll break a lot more protocols. Digital gold tokens like DGX charge demurrage fees. Reflection tokens were a thing on BSC some time ago...
Duplicate of #126
๐ Selected for report: Ruhum
Also found by: 0x1f8b, 0x4non, 0xDjango, Dravee, GimelSec, StErMi, berndartmueller, blackscale, catchup, cccz, csanuragjain, defsec, eccentricexit, ellahi, horsefacts, hubble, joestakey, kenzo, pedroais, peritoflores, reassor, rotcivegaf, sashik_eth, shenwilly, sseefried, throttle, xiaoming90
1.8534 USDC - $1.85
https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L1231-L1234
The feeBPS
is unlimited in RubiconMarket.sol
, leading to maliciously moving large taxes from this contract. Because the fee is taken separately, the user may lose all tokens owned by the user.
There is no cap on feeBPS
https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L1231-L1234
function setFeeBPS(uint256 _newFeeBPS) external auth returns (bool) { feeBPS = _newFeeBPS; return true; }
When collecting protocol fee, unlimited feeBPS
could lead to maliciously obtaining large amounts of token from users
https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L295-L300
// Fee logic added on taker trades uint256 fee = mul(spend, feeBPS) / 10000; require( _offer.buy_gem.transferFrom(msg.sender, feeTo, fee), "Insufficient funds to cover fee" );
If Alice has a large amount of token, and try to buy an offer from the protocol. A malicious admin can front-run the transaction, set a large feeBPS
to take all tokens owned by alice.
None
There should be a reasonable cap on feeBPS
. (e.g., 20%)
#0 - bghughes
2022-06-04T01:10:16Z
Duplicate of #125
#1 - HickupHH3
2022-06-18T04:35:18Z
Duplicate of #21
๐ Selected for report: GimelSec
892.4522 USDC - $892.45
https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L664-L669
A malicious admin can set the deprecated variables AqueductDistributionLive
and AqueductAddress
to deny specific users to buy.
https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L664-L669
if (AqueductDistributionLive) { IAqueduct(AqueductAddress).distributeToMakerAndTaker( getOwner(id), msg.sender ); }
Admin can set the deprecated variables AqueductDistributionLive
, AqueductAddress
to check specific users and revert transactions to deny them.
None
Delete deprecated code, but reserve variable declaration for slots if using upgradable contracts.
#0 - bghughes
2022-06-04T20:23:34Z
Acknowledged centralization risk #344 #314 #133
#1 - HickupHH3
2022-06-18T04:28:57Z
Less so about centralization risk, more about deprecated functions. Not sure why they're kept. Makes sense to remove them (except for the storage variables of course)
A bit of a stretch, but warden's hypothetical scenario checks out. Hence, leaving it as is.
๐ 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.2368 USDC - $52.24
In document:
Importantly, BathHouse has an admin that is the EOA administrator of the entire protocol in v1.
Itโs better to use multisig wallet or DAO as admin.
https://github.com/code-423n4/2022-05-rubicon#bathhousesol
None
Use a multisig wallet such as Gnosis Safe.
๐ 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
30.8307 USDC - $30.83
++i
rather than i++
In for
loops, using ++i
rather than i++
to save gas.
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++) { 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++) { rubiconPools/BathToken.sol 635: for (uint256 index = 0; index < bonusTokens.length; index++) {
Use ++i
rather than i++
to save gas.