Tornado Blast Launcher Pro League
Findings & Analysis Report

2024-07-29

Table of contents

Overview

About C4

Code4rena (C4) is an open organization consisting of security researchers, auditors, developers, and individuals with domain expertise in smart contracts.

A C4 Pro League Audit is an event where elite tier Code4rena contributors, commonly referred to as wardens, reviews, audits and analyzes smart contract logic in exchange for a bounty provided by sponsoring projects.

During the Pro League audit outlined in this document, C4 conducted an analysis of the Tornado Blast smart contract system written in Solidity. The audit took place between June 14 - June 20, 2024.

Wardens

2 Wardens contributed to Tornado:

  1. Koolex
  2. csanuragjain

Final report assembled by bytes032, thebrittfactor & Sentinel

Summary

The C4 Pro League analysis yielded 2 HIGH, 3 MEDIUM and 1 LOW severity vulnerabilities.

Additionally, C4 Pro League analysis included 2 findings rated as SYSTEMIC risks.

Scope

The source code was delivered to Code4rena in a private Git repository.

Severity Criteria

C4 assesses the severity of disclosed vulnerabilities based on three primary risk categories: high, medium, and low/non-critical.

High-level considerations for vulnerabilities span the following key areas when conducting assessments:

  • Malicious Input Handling
  • Escalation of privileges
  • Arithmetic
  • Gas use

For more information regarding the severity criteria referenced throughout the submission review process, please refer to the documentation provided on the C4 website, specifically our section on Severity Categorization.

High Risk Findings (1)

[H-2] Storage collision with slot0 of between BaseIndividualTokenMarket and ProxyWithRegistry

Context:

Description:

There is a storage collision here in slot0 between BaseIndividualTokenMarket and ProxyWithRegistry (which inherits from BlastGasAndYield => Ownable).

Proxy shouldn’t inherit from contracts that allocate slots starting from 0, since the impl contract starts from 0 slot usually.

ProxyWithRegistry Storage layout

{
  "storage": [
    {
      "astId": 8,
      "contract": "src/commons/ProxyWithRegistry.sol:ProxyWithRegistry",
      "label": "_owner",
      "offset": 0,
      "slot": "0", ==> using slot 0
      "type": "t_address"
    }
  ],
  "types": {
    "t_address": {
      "encoding": "inplace",
      "label": "address",
      "numberOfBytes": "20"
    }
  }
}

BaseIndividualTokenMarket Storage layout

{
  "storage": [
    {
      "astId": 69510,
      "contract": "src/tokenLauncher/IndividualTokenMarket.sol:BaseIndividualTokenMarket",
      "label": "version",
      "offset": 0,
      "slot": "0", ==> using slot 0
      "type": "t_string_storage"
    },
    {
      "astId": 69548,
      "contract": "src/tokenLauncher/IndividualTokenMarket.sol:BaseIndividualTokenMarket",
      "label": "maxExpArray",
      "offset": 0,
      "slot": "1",
      "type": "t_array(t_uint256)128_storage"
    }
  ],
  "types": {
    "t_array(t_uint256)128_storage": {
      "encoding": "inplace",
      "label": "uint256[128]",
      "numberOfBytes": "4096",
      "base": "t_uint256"
    },
    "t_string_storage": {
      "encoding": "bytes",
      "label": "string",
      "numberOfBytes": "32"
    },
    "t_uint256": {
      "encoding": "inplace",
      "label": "uint256",
      "numberOfBytes": "32"
    }
  }
}

Luckily the affected storage is _owner (from Ownable which is not used) and version from BancorFormula. However, if the ImplementationRegistry used to set a new Implementation by Tornado, critical impact is likely.

Recommendation:

Remove BlastGasAndYield, since it is not used nor Ownable

Tornado:

Fixed with PR-3

C4 Pro League:

Resolved

Medium Risk Findings (3)

[M-1] Rebasing WETH token on Blast is not taken into account in Tornado

Context:

Description:

WETH token on Blast is rebasing and it is not taken into account in Tornado. This creates different problems in the accounting, especially the market (i.e. IndividualTokenMarket).

To demonstrate a particular impact imagine the following scenario:

  • WETH balance increases (due to generated yield from Blast)
  • The market successfully reaches 3 ether
  • Liquidity moved to Thruster

        WETHTyped().withdraw(MAX_WETH_RESERVE());
[tokenLauncher/IndividualTokenMarket.sol:L167-L168](https://github.com/kairos-loan/launcher-contracts-for-audit/blob/47fd00a07c78f8a06d4c5609de2dbb5f18972009/apps/contracts/src/tokenLauncher/IndividualTokenMarket.sol#L167-L168)

- Notice that, only MAX_WETH_RESERVE is withdrawn which is 3 ether.
- As a result, any generated yield is stuck forever in WETH contract as there is no way to claim it.

Please note that, WETH balance doesn't decrease unlike the usual behaviour of rebasing tokens.



**References**:

> Similar to ETH, WETH and USDB on Blast is also rebasing and follows the same yield mode configurations.

> However, unlike ETH where contracts have Disabled yield by default, WETH and USDB accounts have Automatic yield by default for both EOAs and smart contracts.

https://docs.blast.io/building/guides/weth-yield


WETH contract addr:

https://blastscan.io/address/0x83acb050aa232f97810f32afacde003303465ca5#code

### **Recommendation:** 
To mitigate this, the shortest solution is, on market creation, opt out from WETH yield generation.

### **Tornado:**
Fixed with [PR-2](https://github.com/kairos-loan/launcher-contracts-for-audit/pull/2)


### **C4 Pro League:**
Resolved

---

## [M-2] In case `msg.value + marketWethReserve == MAX_WETH_RESERVE`, `swapExactETHForTokens` function will fail


### **Context:** 
- [Router.sol#L54](https://github.com/kairos-loan/launcher-contracts-for-audit/blob/47fd00a07c78f8a06d4c5609de2dbb5f18972009/apps/contracts/src/tokenLauncher/Router.sol#L54)

### **Description:** 
In case `msg.value + marketWethReserve == MAX_WETH_RESERVE`, the function will revert.
This is because, after buying from Bonding Curve, it tries to buy from Thruster with zero amount.

```renlex
     uint256 ethAmountToSpendOnDex = msg.value - ethAmountToSpendOnBondingCurve;

Thruster doesn’t allow buying with zero (i.e. doesn’t skip it, it cause a revert).

    // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
    function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut)
        internal
        pure
        returns (uint256 amountOut)
    {
        require(amountIn > 0, "ThrusterLibrary: INSUFFICIENT_INPUT_AMOUNT");

https://blastscan.io/address/0x44889b52b71e60de6ed7de82e2939fcc52fb2b4e#code#F10#L59

Note: amountIn here is eth.

Tornado:

Fixed with PR-4

C4 Pro League:

Resolved


[M-3] Users might be enforced to buy the token from Dex through Tornado which goes against the protocol design

Context:

Description:

Two Scenarios (probably one issue)
First

Assume there are contracts/users buying from the market directly and not through the router, the market reach MAX_WETH_RESERVE, and launchTokenOnDex is called directly, therefore, the market gets closed. Because of this, the function buy will revert as it tries to buy from a closed market. As a result, users can not buy through the router.

Second

The market reach MAX_WETH_RESERVE but the market is still open (simply no one called launchTokenOnDex directly), now a user tries to buy through the router, it will buy zero amount from the market, it seems that it won’t revert, then launchTokenOnDex will be called and the TX succeeds. The result, subsequent buying will revert.

Note: market => BondingCurve

=> Update: After discussing with the sponsor, it turns out to be a design choice (see below). However, external observers aren’t aware of the fact that Tornado buys automatically from Dex on behalf of the user (if the msg.value provided will make wethReserve exceeds the max 3 ether), this is a problem because If I trust the router to only buy from Tornado (according to the design choice), it shouldn’t in anyway buy from Dex. Even if the user sends the exact msg.value to reach 3 ether (i.e. the user decides to buy only from BondingCurve), other similar transactions sent nearly during the same time might be processed before, which enforce buying from Dex for those who don’t wish to

Recommendation:

Add external a flag (true/false) to be passed by the user, if they agree to buy from Dex in case the wethReserve reached the maximum.

Note: the sponsor suggested to add an additional function to keep compatibility with UNISwapV2 interface (which I agree).

Tornado:

It is a design choice that buys to the router revert post launch. It separate concerns : as a Dex, tornado does not have the token anymore. External observers correctly see that the token is on a single Dex at a time (if users don’t provide liquidity on more of course)

Fixed with PR-5

C4 Pro League:

Resolved

Low Risk Findings(1)

[L-1] Surplus amount returned from Thruster is locked in the market pointlessly after it is closed

Context:

Description:

On launchTokenOnDex, the liquidity is moved to Thruster DeFi. This is done by calling postCurveDexRouter().addLiquidityETH. However, when adding liquidity, dust amount is returned to the sender. This is done on Thruster side.

	// refund dust eth, if any
	if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);

Thruster:code:F1:L106

In this case, the sender is the market (i.e. IndividualTokenMarket). As a result, the ether native is locked in the contract pointlessly since the market is closed and unusable. While the amount is dust and regardless how much it is, it could be utilized to pay the caller of launchTokenOnDex as an incentive even if it is symbolic, this is in favour in the protocol and easy to implement. This can be done by transferring the native balance of the contract to the msg.sender as a last step.

Recommendation:

Transfer the native balance of the contract to the msg.sender as a last step.

Tornado:

This is a use case that would not occur in practice (cf launcher.txs) and the risk involves dust only, so no steps to change anything are taken here

C4 Pro League:

Decision acknowledged

Additional Recommendations

Systemic Risks

User can compromise funds of other token depositors

The Bonding curve causes the prices per token to go up as the WETH reserves increases. This creates an opportunity for early token depositors and even token creator to steal other depositor’s WETH by selling just before reaching MAX_WETH_RESERVE

Assigning s.wethReserve a value from the balance could possibly cause DoS and/or initial price manipulation

Assigning s.wethReserve a value from the balance is problematic. an adversary can cause DoS for a newly created market make it completely unfunctional. This can be done by front-running the launch TX. Moreover, the market creater can break the assumption of the token initial price set by Tornado.

Inconsistent price between Bonding curve and external DEX at transition time

Once WETH reserves reaches 3 WETH, the token is launched on DEX.

From documentation:

When moving to the external DEX, the token price should continue to evolve continuously, I.e at transition time, the price is the same on the bonding curve and on the external DEX.

It seems that price is not the same on bonding curve and on the external DEX at transition time. As shown in POC, it was beneficial for the user to have sold his token on Bonding curve rather than on DEX. Since Users are selling when price is at its peak in the Bonding curve thus they will derive more profits when compared to selling at DEX

Disclosures

C4 is an open organization governed by participants in the community.

C4 audits incentivize the discovery of exploits, vulnerabilities, and bugs in smart contracts. Security researchers are rewarded at an increasing rate for finding higher-risk issues. Audit submissions are judged by a knowledgeable security researcher and solidity developer and disclosed to sponsoring developers. C4 does not conduct formal verification regarding the provided code but instead provides final verification.

C4 does not provide any guarantee or warranty regarding the security of this project. All smart contract software should be used at the sole risk and responsibility of users.