Rubicon contest - Ruhum'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: 8/99

Findings: 11

Award: $2,066.05

🌟 Selected for report: 5

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: Ruhum

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

Labels

bug
3 (High Risk)

Awards

292.7689 USDC - $292.77

External Links

Lines of code

https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L267_L287

Vulnerability details

Impact

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.

Proof of Concept

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.

Tools Used

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.

Findings Information

Awards

8.2687 USDC - $8.27

Labels

bug
2 (Med Risk)
sponsor confirmed

External Links

Lines of code

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

Vulnerability details

Impact

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.

Proof of Concept

See the linked code snippets above.

Tools Used

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.")

Lines of code

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

Vulnerability details

Impact

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.

Proof of Concept

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)
        );

Tools Used

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

Findings Information

Labels

bug
duplicate
2 (Med Risk)
sponsor confirmed

Awards

16.2035 USDC - $16.20

External Links

Lines of code

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

Vulnerability details

Impact

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.

Proof of Concept

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.

Tools Used

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.

Findings Information

🌟 Selected for report: Ruhum

Also found by: 0x1f8b, pauliax

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

240.9621 USDC - $240.96

External Links

Lines of code

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

Vulnerability details

Impact

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.

Proof of Concept

receive() function: https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/peripheral_contracts/BathBuddy.sol#L69

Tools Used

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.

Findings Information

🌟 Selected for report: hubble

Also found by: Ruhum

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/main/contracts/rubiconPools/BathToken.sol#L271 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/peripheral_contracts/BathBuddy.sol#L87-L128

Vulnerability details

Impact

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.

Proof of Concept

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

Tools Used

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

Awards

1.8534 USDC - $1.85

Labels

bug
2 (Med Risk)
sponsor acknowledged

External Links

Lines of code

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

Vulnerability details

Impact

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%.

Proof of Concept

For the DOS:

  1. Owner sets fees to 101% using BathToken.setFeeBPS(): https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathToken.sol#L261
  2. User tries to withdraw which will revert here because fee > r: https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathToken.sol#L604

Tools Used

none

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

Findings Information

🌟 Selected for report: Ruhum

Labels

bug
2 (Med Risk)
sponsor acknowledged

Awards

892.4522 USDC - $892.45

External Links

Lines of code

https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathPair.sol#L324

Vulnerability details

Impact

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.

Proof of Concept

  1. Strategist opens up an offer through placeMarketMakingTrades() where a token is sold for very cheap
  2. Strategist accepts the offer within the same transaction using their private wallet

Tools Used

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

Findings Information

🌟 Selected for report: kenzo

Also found by: 0x1f8b, Ruhum, dirk_y, shenwilly

Labels

bug
duplicate
2 (Med Risk)

Awards

117.1076 USDC - $117.11

External Links

Lines of code

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

Vulnerability details

Impact

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.

Proof of Concept

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

Tools Used

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

Findings Information

🌟 Selected for report: WatchPug

Also found by: Chom, Dravee, Hawkeye, MaratCerby, Ruhum, csanuragjain, fatherOfBlocks, minhquanym

Labels

bug
duplicate
2 (Med Risk)
sponsor acknowledged

Awards

42.6857 USDC - $42.69

External Links

Lines of code

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

Vulnerability details

Impact

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.

Proof of Concept

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.

Tools Used

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.

Report

Low

L-01: ExpiringMarket stopped state variable is unused

The 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:

L-02: BathHouse.timeDelay state variable is unused

The 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.

Non-Critical

N-01: emit events when making changes to a contract's configuration

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:

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