Thanos Development Story — 5

Predeploy Contracts: UniswapV3

Aaron Lee
Tokamak Network
6 min readAug 27, 2024

--

As part of our ongoing advancements in Layer 2 solutions, the new Thanos network is being developed with cutting-edge rollup technology to deliver enhanced services. In this development story, we will discuss the predeployment of UniswapV3 contracts, following the predeployment of the USDC Bridge contracts previously covered.

Key Advantages of Predeployment:

  • Improved Network Stability: By deploying critical contracts in advance, we minimize potential issues during initial network operations.
  • Immediate Service Provision: Essential features are swiftly and reliably implemented before the mainnet launch, allowing users to access services immediately upon deployment without additional steps, providing enhanced convenience.

Background Information:

Unlike the previous USDC Bridged, UniswapV3 involves multiple repositories importing various contracts. Instead of adding all contracts to the repository, we built the contracts locally using Hardhat and added only the artifacts to the repository. Uniswap V3 Artifacts.

Both op-bindings and op-chain-ops packages are used in UniswapV3. Op-bindings facilitate contract deployment in a Go environment, dynamically setting bytecode and storage slots, while op-chain-ops configures the network state, generating Layer 1 and Layer 2 genesis blocks, deploying contracts, calculating implementation addresses, and creating immutable values and storage for the initial genesis state.

1. Predeployed Smart Contracts

Deploying UniswapV3 contracts on Layer 2 solutions, such as Optimism, offers significant advantages. For instance, UniswapV3 deployed on Optimism confirms transactions instantly, reduces costs by up to 10 times compared to L1 Ethereum, and improves scalability by supporting up to 0.6 transactions per second. These benefits also apply to the Thanos Network, enhancing user experience and leveraging high scalability and low-cost structure to activate DeFi services.

UniswapV3 is a binary smart contract system composed of several libraries, and we will examine the 11 primary contracts predeployed in the L2 network from a library-centric perspective.

1. Core_[link]

The Core library ensures the safety of assets for all Uniswap users. It consists of a single factory, pool deployer, and multiple pools generated by the factory, defining the pool creation logic and asset interaction.

  • UniswapV3Factory.sol: Defines the logic for creating pools, comprising two tokens and fees. Multiple pools can exist for the same asset pair, distinguished by fees.

2. Periphery_[link]

The Periphery library supports domain-specific interactions with the Core contracts.

  • SwapRouter.sol: Executes stateless swaps in UniswapV3, with functions like exactInputSingle and exactInput for token exchanges, and exactOutputSingle and exactOutput for specific token exchanges. The uniswapV3SwapCallback function handles token payments post-swap.
  • NonfungiblePositionManager.sol: Manages transactions related to creating, adjusting, or terminating positions.
  • NonfungibleTokenPositionDescriptor.sol: The only proxy contract in UniswapV3, generates JSON metadata URI for specific token IDs, returning ERC721-compliant metadata URI using the tokenURI function.
  • TickLens.sol: Queries populated ticks in a specific bitmap index within a Uniswap V3 pool.
  • QuoterV2.sol: Provides expected amounts for a given swap without executing it.
  • NFTDescriptor.sol: Generates metadata and images dynamically for NFTs, including token URI creation, string conversion, and SVG image generation.
  • UniswapInterfaceMulticall.sol: Provides multicall functionality for the Uniswap interface, executing multiple smart contract calls in one transaction and returning results and gas used.

3. UniversalRouter_[link]

Supports ETH, ERC20, and NFT swaps across eight marketplaces, offering flexible and customized transactions by integrating multiple protocols.

  • UniversalRouter.sol: Executes efficient and secure ERC20 and NFT swap commands, validating transaction deadlines and processing results through repeated commands across various media.
  • UnsupportedProtocol.sol: A dummy contract that always reverts, preventing attempts to call unsupported protocols on specific chains.

4. Permit2_[link]

A library used in UniversalRouter to allow users to avoid direct token approvals each time.

  • Permit2.sol: Allows transactions through a one-time signature with SignatureTransfer and sets specific token usage rights for a defined period with AllowanceTransfer. This flexibility and security enhance token transaction management, eliminating repetitive approvals through the Permit2 contract.

2. Development Process

The predeployment process for UniswapV3 contracts follows the same steps as those used in the previous USDC Bridge deployment. A brief summary of these steps is outlined below:

  1. addresses.go: This file pre-defines and initializes the addresses of each contract, and determines whether a proxy pattern will be used for the contracts.
  2. config.go: This file initializes each contract’s constructor and storage through NewL2ImmutableConfig and NewL2StorageConfig. The NewL2ImmutableConfig file defines the constructor, while NewL2StorageConfig sets the addresses and values necessary for storage initialization. In a standard deployment, constructor values or storage information (such as addresses or permissions for interacting with other contracts) would be automatically set according to the contract's logic. However, in the predeployment process, this setup is done manually.
  3. immutables.go: This file inputs immutable settings for contracts that include immutable variables. The PredeploysImmutableConfig structure is defined for this purpose, and the l2ImmutableDeployer function ensures that each array contains the appropriate types before deploying the contract.
  4. setter.go: This file manages the admin settings for specific proxy contracts. For Uniswap, only the NonfungibleTokenPositionDescriptor is a proxy contract, so the configurations for this contract need to be set accordingly.
  5. layer_two.go: This file defines functions related to constructing the genesis block for Layer 2, ultimately setting the initial state of the Layer 2 network and storing the necessary contracts and addresses.

3. Development Differences

The main difference between predeploying USDC Bridge and UniswapV3 lies in the build tools used. USDC Bridge used Forge, while UniswapV3 used Hardhat. Each tool has its pros and cons, necessitating different workflows.

First, when we clone each library and build it into hardhat, we need to add a storage layout so that we can check that it fits properly later. To do this, we added the storageLayout option to hardhat.config.ts so that it can be generated with the contract when it is built.

When building each library with Hardhat, adding the storageLayout option to the hardhat.config.ts file ensures proper verification. By including storageLayout in the outputSelection property, storage layouts are generated alongside contract builds. Unlike Foundry, which creates storageLayout within artifact files, Hardhat generates it within buildinfo files, requiring careful consideration.

Here is the storageLayout for the UniswapV3Factory contract, illustrating variable roles and storage slots:

  • parameters: Stores the Parameters struct at slot 0.
  • owner: Stores the contract owner’s address at slot 3.
  • feeAmountTickSpacing: Maps fee amounts and tick spacing at slot 4.
  • getPool: Maps pool addresses based on token pairs and fee amounts at slot 5.

Accurately defining storage slots is crucial to avoid initialization errors or storage allocation issues. The storageLayout ensures precise variable allocation during predeploy, facilitating error detection and correction.

Binding files must also specify storageLayout positions for each contract, ensuring accurate interactions and verification throughout the predeploy process. The following is an example of a template for generating binding files:

The processHardhatArtifacts function generates binding files for smart contracts, enabling Solidity contracts to be used in Go programs. Key logic includes:

for _, contractName := range contractNames {
artifactPath := filepath.Join(f.HardhatArtifacts, contractName+".json")
log.Printf("Processing artifact at path: %s\n", artifactPath)
log.Printf("generating code for : %s", contractName)
  • Loop through contractNames to process each contract.
  • Set the path to the JSON artifact file for each smart contract in the artifactPath variable. f.HardhatArtifacts is the directory path where the artifact files are located.
art, err := hh.GetArtifact(contractName)
if err != nil {
log.Fatalf("error reading artifact %s: %v\n", contractName, err)
}

storage, err := hh.GetStorageLayout(contractName)
if err != nil {
log.Fatalf("error reading storage layout %s: %v\n", contractName, err)
}
  • Fetching Artifacts: Call hh.GetArtifact(contractName) to retrieve the artifact of the smart contract from Hardhat. This artifact includes the contract's ABI and bytecode information.
  • Retrieving Storage Layout: Call hh.GetStorageLayout(contractName) to obtain the storage layout information of the smart contract from Hardhat.
ser, err := json.Marshal(storage)
if err != nil {
log.Fatalf("error marshaling storage: %v\n", err)
}
serStr := strings.Replace(string(ser), "\"", "\\\"", -1)
  • Serializing Storage Layout: Call json.Marshal(storage) to serialize the storage layout object into a JSON byte array format.
  • Error Handling: If an error occurs during the serialization process, the program terminates and logs the error message.
  • String Conversion: Convert the serialized byte array into a string and replace all double quotes (") with escaped double quotes (\"). This ensures the JSON string is correctly processed according to the template requirements.
// Prepare data for template execution.
d := data{
Name: contractName,
StorageLayout: serStr,
DeployedBin: art.DeployedBytecode.(string),
Package: f.Package,
}

Initializing Data Structure: Initialize the data structure containing the necessary information for template execution.

  • Name: The current smart contract’s name.
  • StorageLayout: The serialized storage layout string.
  • DeployedBin: The deployed bytecode of the smart contract.
  • Package: The package name of the generated Go file.

By retrieving each smart contract’s artifact and storage layout, and subsequently generating the corresponding binding files in Go, the following outcome can be achieved.

This document outlines the advantages of utilizing UniswapV3-related contracts on Layer 2 solutions, the reasons for predeployment, and the steps involved in the predeployment process. The predeployment process involves clearly defining the composition and functionality of each contract and deploying them in accordance with the Layer 2 network requirements. This process includes configuring the storage layout and generating binding files for the contracts, thereby clearly defining the initial state.

--

--