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: 8/99
Findings: 11
Award: $2,066.05
🌟 Selected for report: 5
🚀 Solo Findings: 1
🌟 Selected for report: Ruhum
Also found by: IllIllI, berndartmueller, blackscale, eccentricexit, hansfriese
https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L267_L287
The swapEntireBalance()
function allows the user to pass a buy_amt_min
value which is the minimum number of tokens they should receive from the swap. But, the function doesn't pass the value to the underlying swap()
function. Thus, the user's min value will be ignored. Since that will result in unexpected outcomes where user funds might be lost, I rate this issue as HIGH.
swapEntireBalance():
function swapEntireBalance( uint256 buy_amt_min, address[] calldata route, // First address is what is being payed, Last address is what is being bought uint256 expectedMarketFeeBPS ) external returns (uint256) { //swaps msg.sender entire balance in the trade uint256 maxAmount = ERC20(route[0]).balanceOf(msg.sender); ERC20(route[0]).transferFrom( msg.sender, address(this), maxAmount // Account for expected fee ); return _swap( maxAmount, maxAmount.sub(buy_amt_min.mul(expectedMarketFeeBPS).div(10000)), //account for fee route, expectedMarketFeeBPS, msg.sender ); }
The second parameter of the _swap()
call should be the min out value. Instead maxAmount.sub(buy_amt_min.mul(expectedMarketFeeBPS).div(10000))
is used.
Example:
amount = 100 buy_amt_min = 99 expectedMarketFeeBPS = 500 // 5% actual buy_amy_min = 100 - (99 * (500 / 10000)) = 95.05
So instead of using 99
the function uses 95.05
which could result in the user receiving fewer tokens than they expected.
none
pass buy_amt_min
directly to _swap()
#0 - bghughes
2022-06-04T21:34:40Z
Duplicate of #104
#1 - HickupHH3
2022-06-16T10:44:57Z
Not a duplicate. This has to do with applying a fee on buy_amt_min
instead of passing the actual value directly. Lower slippage tolerance means potential loss of funds, hence the high severity.
8.2687 USDC - $8.27
https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L356 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
When transferring ETH, use call()
instead of transfer()
.
The transfer()
function only allows the recipient to use 2300 gas. If the recipient uses more than that, transfers will fail. In the future gas costs might change increasing the likelihood of that happening.
Keep in mind that call()
introduces the risk of reentrancy. But, as long as the router follows the checks effects interactions pattern it should be fine. It's not supposed to hold any tokens anyway.
See the linked code snippets above.
none
Replace transfer()
calls with call()
. Keep in mind to check whether the call was successful by validating the return value:
(bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed.")
🌟 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/RubiconMarket.sol#L298 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathToken.sol#L353
Many ERC20 tokens don't implement the specification correctly. Some of them revert and some just return false when there's an issue. Some of them don't return anything. It's the wild west. When your contract is expected to work with a wide range of tokens you should use OpenZeppelin's SafeERC20 library. Otherwise, you might run into an issue where some tokens don't work with your protocol or transfers fail without the contract even knowing. The latter can result in lost funds so I rate it as high. Especially considering ERC20 trades is the main feature of this protocol.
A good example is the USDT token. When a transfer succeeds, it doesn't return true
: https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7#code#L126
So if you have the following check in your contract, it will always fail with the USDT contract:
require(pay_gem.transferFrom(msg.sender, address(this), pay_amt));
Another one is the 0x token which returns false when a transfer wasn't successful. So if you have instances where you don't check the return value, you might expect funds to be transferred although it failed: https://etherscan.io/address/0xe41d2489571d322189246dafa5ebde1f4699f498#code#L71
IERC20(filledAssetToRebalance).transfer( destination, rebalAmt.sub(stratReward) );
none
Add the OpenZepplin SafeERC20 library and replace all the transfer()
and transferFrom()
calls in your contracts with safeTransfer()
and safeTransferFrom()
#0 - bghughes
2022-06-04T01:20:14Z
Duplicate of #316
🌟 Selected for report: cccz
Also found by: AlleyCat, GimelSec, IllIllI, Ruhum, berndartmueller, csanuragjain, dipp, fatherOfBlocks, gzeon, horsefacts, pedroais, shenwilly
16.2035 USDC - $16.20
https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L337 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L391 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#L505
When using the RubiconRouter with ETH, the contract allows the user to transfer more ETH than is actually used within the transaction. The remaining ETH will simply be locked up in the Router contract causing the user to lose funds.
While this would be a mistake by the user, it's pretty easy to stop this from happening. So I think this is a valid issue.
In all the above linked code snippets, the contract verifies that msg.value >= x
. Subsequent calls within the function will use x
instead of msg.value
. The user looses exactly x - msg.value
ETH.
For example, the offerWithETH()
function: https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L391
function offerWithETH( uint256 pay_amt, //maker (ask) sell how much // ERC20 nativeETH, //maker (ask) sell which token uint256 buy_amt, //maker (ask) buy how much ERC20 buy_gem, //maker (ask) buy which token uint256 pos //position to insert offer, 0 should be used if unknown ) external payable returns (uint256) { require( msg.value >= pay_amt, "didnt send enough native ETH for WETH offer" ); uint256 _before = ERC20(buy_gem).balanceOf(address(this)); WETH9(wethAddress).deposit{value: pay_amt}(); uint256 id = RubiconMarket(RubiconMarketAddress).offer( pay_amt, ERC20(wethAddress), buy_amt, buy_gem, pos ); uint256 _after = ERC20(buy_gem).balanceOf(address(this)); if (_after > _before) { //return any potential fill amount on the offer ERC20(buy_gem).transfer(msg.sender, _after - _before); } return id; }
The user specifies the amount of tokens they want to offer in the pay_amt
param. The function then checks whether msg.value
is bigger or equal to pay_amt
. After that, pay_amt
of tokens are deposited into the WETH contract. Meaning, pay_amt - msg.value
tokens are left unused in the Router contract.
none
Verify that msg.value == x
so that the user isn't allowed to pass more than necessary. That way you prevent the user from loosing any funds.
#0 - KenzoAgada
2022-06-06T05:05:05Z
Duplicate of #15.
240.9621 USDC - $240.96
The BathBuddy contract is able to receive ETH. But, there's no way of ever retrieving that ETH from the contract. The funds will be locked up.
Currently, there seems to be no logic in the protocol where ETH is sent to the contract. But, it might happen in the future. So I'd say it's a MED issue.
receive()
function: https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/peripheral_contracts/BathBuddy.sol#L69
none
Remove the receive()
function if the contract isn't supposed to handle ETH. Otherwise, add the necessary logic to release the ETH it gets.
https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathToken.sol#L271 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/peripheral_contracts/BathBuddy.sol#L87-L128
Bonus tokens are distributed with each withdrawal. The admin can add the same token multiple times which breaks the distribution. Users who withdraw early receive more than the rest.
setBonusToken()
used to add bonus tokens: https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathToken.sol#L271
distributeBonusTokenRewards()
is called when a user withdraws their funds: https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathToken.sol#L629
BathBuddy.release()
handling the distribution: https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/peripheral_contracts/BathBuddy.sol#L87-L128
So if the same token is included twice in the bonusTokens
array, the tokens are distributed twice for each withdrawal:
// we ignore fees here bonusTokensReleasable: 30 userWithdrawal: 10 totalSupply: 30 1. release: // see formula: https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/peripheral_contracts/BathBuddy.sol#L103-L105 30 * 10 / 30 = 10 2. release within the same withdrawal since the token is included twice: 20 * 10 / 30 = ~6.7 meaning, the user who withdrew receives ~16.7 of the 30 available bonus tokens
none
Prohibit the same token to be added multiple times to the bonusTokens
array in https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathToken.sol#L271
#0 - bghughes
2022-06-03T19:39:17Z
Duplicate of #450
🌟 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#L1232 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathToken.sol#L261 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathToken.sol#L498-L499
The owner can set an arbitrary fee. If they set it to a value above 100%, withdrawing BathTokens won't be possible anymore because of an underflow. In RubiconMarket the owner can also have an arbitrary fee although that won't result in a DOS. But, the user might pay an absurdly high fee.
There should be checks that only allow fees up to a specific value, e.g. 10%.
For the DOS:
BathToken.setFeeBPS()
: https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathToken.sol#L261fee > r
: https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathToken.sol#L604none
Add a limit to the constructors where a fee is set and to all the configuration functions for fees.
#0 - bghughes
2022-06-03T22:16:52Z
#133 #344
#1 - HickupHH3
2022-06-18T04:21:37Z
Making this the primary issue about not capping fees
🌟 Selected for report: Ruhum
892.4522 USDC - $892.45
https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathPair.sol#L324
The strategist is able to use user funds to trade on the RubiconMarket. They can abuse this to transfer user funds to themselves.
A strategist having access to user funds seems to be a deliberate design choice. But, I believe it's important to note how dangerous that is.
none
There's no easy way to fix this since it's a big part of the protocol. You'd have to overhaul the whole thing.
You could minimize the dmg by limiting the amount of funds a strategist has access to
#0 - bghughes
2022-06-03T21:31:07Z
Duplicate of #344
#1 - HickupHH3
2022-06-23T15:41:03Z
Unique strategist rug-pull vector
https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathHouse.sol#L265 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathPair.sol#L149
Through the BathHouse contract, an admin can allow people to use the BathPair contract to make trades with user funds. But, there's no way of removing people as strategists.
If a strategist turns rogue and abuses the power to make "bad" trades it will result in the loss of user funds. The admin has no way of stopping that.
Granting the strategist permission: https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathHouse.sol#L265
Used within the following modifier to restrict access to BathPair functions: https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathPair.sol#L149
none
Update the function so that a boolean
can be passed as param to be able to both approve and kick out strategists.
#0 - bghughes
2022-06-03T21:13:09Z
Duplicate of #118
🌟 Selected for report: WatchPug
Also found by: Chom, Dravee, Hawkeye, MaratCerby, Ruhum, csanuragjain, fatherOfBlocks, minhquanym
42.6857 USDC - $42.69
https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L447 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L452-L462 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L471
The ExpiringMarket is supposed to only allow buying and offering tokens at specific times. But the function that's supposed to handle that isn't properly implemented. Tokens can be bought and offered at all times.
The RubiconMarket contract inherits the ExpiringMarket so it's part of the core functionality. An important feature like that not working properly is a MED issue IMO.
Here's the short specification of the ExpiringMarket stating the purpose of the close time: https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L447
Here's the modifier for the buy and offer function checking that the market isn't closed yet: https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L452-L462
But, the isClosed()
function always returns false
: https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L471
So the modifier's check will always pass allowing users to buy and offer at all times.
none
Implement the isClosed()
function properly.
#0 - bghughes
2022-06-04T01:24:42Z
#77
#1 - HickupHH3
2022-06-23T14:56:16Z
Closer duplicate of #148 than #77.
🌟 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.0453 USDC - $52.05
stopped
state variable is unusedThe contract has a public state variable called stopped
. It's used nowhere and has no deprecation notice. Consider removing it or if it's part of a planned feature, implement it properly.
Relevant code:
BathHouse.timeDelay
state variable is unusedThe contract has a public state varialbe called timeDelay
. It's used nowhere. But, it's suppoed to be "A variable time delay after which a strategist must return funds to the Bath Token"
There's a function that's supposed to set the timeDelay
of the BathToken but neither does the function do that nor does the BathToken contract support such a value: https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathHouse.sol#L286
Consider removing it or if it's part of a planned feature, implement it properly.
Whenever you make changes to a contract's config it should emit an event. Especially when it's critical values like fees.
Here's a list of all the instances I found: