Deploying Uniswap V3: a developers guide to navigating the complexities

Souradeep Das | April 5, 2023

The Unicorn is coming to Boba! After much thoughtful contemplation, the diligent unicorn has finally decided to reach for newer pastures. All of this, of-course, happened with the passing of proposal #18018, the community has now decided to launch Uniswap V3 to the Boba Network L2 on Ethereum!

The primary step to Deploying Uniswap V3 (or for that matter a clone) is to deploy the smart contracts that make up this amazing V3 protocol, and that’s exactly what we will be covering in this article.

Before we begin, the Uniswap team deserves a huge shout out for assimilating all the associated content in an organized unified location.
V3 new chain repo
V3 deploy repo

These repositories serve their purpose very well and we(and you) will be using these throughout the article. However, despite their best efforts (and at no fault of theirs), the deployment process might include several intricacies, primarily associated with carrying out the steps on a chain other than Ethereum. While it is not possible, and probably unfair to ask to list out all these intricacies or extra details, this article stands to serve the exact purpose and complement the repos by filling in as many information gaps to make your deployment experience a breeze!

As an added note: the uniswap repos are a work in progress. Look out for the github “commits” mentioned to get the most consistent results from this article

Accio with the unicorn core (or) how to deploy the core v3 contracts?

Get the “v3-deploy repo” and clone it into your local system
Commit: b7aac0f

$ git clone https://github.com/Uniswap/deploy-v3.git
$ cd deploy-v3
$ yarn && yarn build

This repo collects almost all of the contracts that make up the v3 protocol, and additionally provides scripts to deploy them easily, all in a single command. But wait, however appealing that command might be to run now, lets first make sure we have everything necessary and in place.

The command would require the following arguments:-

Private key — an account that has funds to deploy the contracts, this account will not have any privileges on the contract

Json prc — the json rpc url of the network where you will deploy

Weth9 address — address of the WETH9 contract on the specific chain where you are deploying

Native currency label — the native token symbol (ETH)

Owner address — ??

Confirmations — the no of confirmations to wait before each transaction (if you are using a L2 with instant soft finality, select: “0”)

(there are some other optional arguments — state, v2coreFactory and gasPrice, which you don’t have to worry about unless you know what you are doing)

What about the Owner address?

The owner address that you will use here, will specifically control the following on your deployed contracts-
a) UniswapV3Factory contracts owner privileges that allow control over pool protocol fees
b) The ownership of the proxy contract for NonfungibleTokenPositionDescriptor

The address you select here will depend on what you want to do for the deployments

Do you want to centrally control the parameters for your deployment? Choose an address that you deem to be the admin

Do you have a DAO that should control the parameters? Chose the DAO address to be the owner (for most cases of compound like DAOs, this is the Timelock contract)

Do you want the Uniswap DAO (or a DAO on a different layer) to control the parameters? In case you are deploying Uniswap to a new chain, as was the case for Boba — here’s what you need to do.

Ownership across layers

In order for the Uniswap DAO on Ethereum to assume ownership of your contracts that are on a different chain, you will have to use some form of messaging system. In the case of several L2s and rollups, there exists a default message passing system that enables taking data from L1>L2.

In case of Boba/Optimism or the likes this is the crossDomainMessenger which enables message passing, while in the core it is the enqueue() feature that allows users to trigger transaction on L2 by enqueueing transactions on L1

Caution: The following contract works only for protocols that use the Optimism messaging system, furthermore comments in green signify commands for Boba Network

For this we use a contract on L2 that forwards messages from an L1 address, in other words — any L2 contract that has the following contract set as an owner, (or another privilege) will ultimately be owned by the l1Owner address on the ‘root’ chain (here, ethereum)

pragma solidity 0.7.6;
interface Messenger {
    function sendMessage(address _target, bytes memory _message, uint32 _gasLimit) external;
    function relayMessage(
        address _target,
        address _sender,
        bytes memory _message,
        uint256 _messageNonce
    ) external;
    function xDomainMessageSender() external view returns (address);
}
// L2 Contract which receives messages from a specific L1 address and transparently
// forwards them to the destination.
//
// Any other L2 contract which uses this contract's address as a privileged position,
// can be considered to be owned by the `l1Owner`
contract CrossChainAccount {
    Messenger messenger;
    address l1Owner;
    constructor(Messenger _messenger, address _l1Owner) {
        messenger = _messenger;
        l1Owner = _l1Owner;
    }
    // `forward` `calls` the `target` with `data`,
    // can only be called by the `messenger`
    // can only be called if `tx.l1MessageSender == l1Owner`
    function forward(address target, bytes memory data) external {
        // 1. The call MUST come from the L1 Messenger
        require(msg.sender == address(messenger), "Sender is not the messenger");
        // 2. The L1 Messenger's caller MUST be the L1 Owner
        require(messenger.xDomainMessageSender() == l1Owner, "L1Sender is not the L1Owner");
        // 3. Make the external call
        (bool success, bytes memory res) = target.call(data);
        require(success, string(abi.encode("XChain call failed:", res)));
    }
}

The contract takes two parameters –
_messenger = the messenger on L2 (L2CrossDomainMessenger for Boba)
_l1Owner = the address on L1 which actually holds ownership of any privilege the contract above has (Uniswap Timelock contract on L1 for Boba)

For Boba, this is the contract that is deployed and has been given the ownership of the core uniswap contracts

Now that we have figured out what the Owner address should be, let’s continue with the deployment!

Thanks to the script just run the following with your own arguments on your local clone of “v3-deploy” –

$ yarn start -pk <enter-private-key> -j <your-rpc-endpoint> -w9 <weth-address> -ncl <native token label> -o <owner-address-we-decided-upon> -c <no-of-block-confirmations>

For eg, the command to deploy Uniswap-v3 on Boba

$ yarn start -pk <redacted> -j https://mainnet.boba.network -w9 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 -ncl ETH -o 0x53163235746CeB81Da32293bb0932e1A599256B4 -c 0

You will see the contracts being deployed (and some ownership being transferred). And, when all the steps are done — you would also notice a state.json file which holds the deployed addresses.

If the deployment fails in between, rerunning the command will use the state.json file and resume the deployment process from where you stopped.

Known issue — If during a deployment run, the process fails after step 11 i.e “UniswapV3Factory ownership set” — a subsequent run may fail — because adding a new fee tier (step 2) from the script expects the owner of UniswapV3Factory to be the deployer. In this case — try modifying the script to ignore step 2, or start a fresh deployment by deleting state.json

Verifying the contracts on Etherscan

In order to verify these contracts, we will have to revert back to the repositories in which these contracts actually live.

Just open a new window, and clone all of these repositories

$ git clone https://github.com/Uniswap/v3-core && git clone https://github.com/Uniswap/v3-periphery.git && git clone https://github.com/Uniswap/v3-staker.git && git clone https://github.com/Uniswap/swap-router-contracts.git

We will use the @nomiclabs/hardhat-etherscan plugin to verify which all these repos already have set up, so you won’t have to do a thing as long as hardhat-etherscan supports the network on which you are deploying

What if hardhat-etherscan doesn’t natively support the network?

The plugin fortunately allows adding custom networks for verification. (if it does not hardhat-etherscan is probably on an older version, update it to (³.1.6)
Update it to the latest with

$ npm remove @nomiclabs/hardhat-etherscan
$ npm install --save-dev @nomiclabs/hardhat-etherscan

To enable your custom chain with the hardhat-etherscan plugin, add the network to the list, and a custom chain field to etherscan like the following: (and do this for all the repos we have cloned)

For eg, on Boba the config should look like-

networks: {
   hardhat: {
     allowUnlimitedContractSize: false,
   },
   mainnet: {
     url: `https://mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`,
   },
   ...
   ...
   'boba-mainnet': {
     url: 'https://mainnet.boba.network',
   },
 },
 etherscan: {
   // Your API key for Etherscan
   // Obtain one at https://etherscan.io/
   apiKey: {
     'boba-mainnet': '<enter-etherscan-api-key>',
   },
   customChains: [
     {
       network: 'boba-mainnet',
       chainId: 288,
       urls: {
         apiURL: 'https://api.bobascan.com/api',
         browserURL: 'https://bobascan.com',
       },
     },
   ]
 },
 ...
 ...

Now that hardhat-etherscan is ready, in case it wasn’t, let’s use it to verify the contracts we just deployed.

All the contracts are distributed among the four repos, and a key is given below:

v3-core: UniswapV3Factory.sol,
v3-periphery: UniswapInterfaceMulticall.sol, TickLens.sol, NFTDescriptor.sol, NonfungibleTokenPositionDescriptor.sol*, NonfungiblePositionManager*, V3Migrator*
v3-staker: UniswapV3Staker.sol*,
swap-router-contracts: QuoterV2.sol*, SwapRouter02.sol*

For each of these verify the contracts by running the following command on the respective repositories:

$ npx hardhat verify --network <network-name> <deployed-contract-address>

For eg, verifying the UniswapV3Factory on Boba (use your own address)

$ npx hardhat verify --network boba-mainnet 0xFFCd7Aed9C627E82A765c3247d562239507f6f1B

For some of these contracts (indicated with a * ) you would also need to provide the constructor arguments for verification

$ npx hardhat verify --network <network-name> <deployed-contract-address> <constructor-arg-1> <constructor-arg-2> …

For eg, verifying the NonfungibleTokenPositionDescriptor on Boba (use your own address)

$ npx hardhat verify --network boba-mainnet 0xb6751A274EDAe02A911E3bB23682FAaF380433b7 "0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000" "0x0000000000000000000000000000000000000000000000000000000000455448"

And verifying the UniswapV3Staker on Boba

npx hardhat verify --network boba-mainnet 0x6a6c1198f85B084822138DFd3fA9686e4029c091 "0xFFCd7Aed9C627E82A765c3247d562239507f6f1B" "0x0bfc9aC7E52f38EAA6dC8d10942478f695C6Cf71" "2592000" "1892160000"

That’s it! You should now have a set of uniswap v3 contracts deployed, verified and ready to take on all those transactions. Give yourself a pat in the back, take a deep breath, think happy thoughts, and let’s slay that final challenge!

Deploying the Universal Router

The Universal Router is something that you might or might not need, depending on your use case for the contracts. The Universal Router is a fairly new addition to Uniswap, which allows optimizing trades across multiple token types, and the “interfaces” have been moving forwards to utilizing it!

This is where the Universal Router lives: https://github.com/Uniswap/universal-router
Commit: f0e15fe

Clone the repo along with its submodules and build it

WARNING: make sure you have commit f0e15fe or later (latest). The older version with json parameters support, includes an error where constructor arguments are offset by 2, and will result in an incorrect deployment

$ git clone --recurse-submodules https://github.com/Uniswap/universal-router.git
$ cd universal-router
$ yarn && yarn compile

Install Forge if you do not have it installed, follow the installation docs here: https://github.com/foundry-rs/foundry

Now that we have cloned the repo locally, like we have practiced — let’s take a moment and make sure we have everything we need in order to proceed

This deployment process includes the deployment of three contracts, namely-
i) UniversalRouter
ii) UnsupportedProtocol
iii) Permit2

And, these are the parameters the Universal Router works with and we will have to specify

permit2,
            weth9,
            seaport,
            seaportV1_4,
            openseaConduit,
            nftxZap,
            x2y2,
            foundation,
            sudoswap,
            elementMarket,
            nft20Zap,
            cryptopunks,
            looksRare,
            routerRewardsDistributor,
            looksRareRewardsDistributor,
            looksRareToken,
            v2Factory,
            v3Factory,
            pairInitCodeHash,
            poolInitCodeHash,
            paymentRecipient,
            paymentAmountBips,
	unsupported

The curious case of Permit2 and Deterministic Deployments

Of the three contracts, Permit2 is deployed using CREATE2 (deterministic deployment) to have the same address across chains.

If your chain does not enforce strict replay protection you could safely skip this section!

The most common Deterministic Deployment contract is https://github.com/Arachnid/deterministic-deployment-proxy, and this is also what forge uses when you will run the script. It uses a novel method called — Nick’s method, which works with a pre-signed deployment tx (without chainId) to deploy the Deterministic Deployment Proxy to the same address “0x4e59b44847b379578588920ca78fbf26c0b4956c” on all chains. (and this proxy further allows for deterministic deployments through CREATE2)

However, some EVM chains have strict replay protection enabled (EIP-155), in other words they do not allow submitting transaction without a chainId, (which is what the deployment of the Deterministic Deployment will demand), as is the case for Boba Network.

Now, depending on the level where replay protection is enabled on the chain, there may still be ways to make the pre-signed tx work, and there’s and if it’s possible there’s a good chance that you will find the contract already deployed on the address “0x4e59b44847b379578588920ca78fbf26c0b4956c” for the chain.

(For ex, Avax has replay protection on the json-rpc level, so spinning up a node without it enabled, allows to deploy the proxy)

If we are still without the deterministic deployment proxy, the next best option probably is to use an alternative.
Gnosis Safes are some other contracts which require Deterministic Deployment, and demand consistency across all chains. As a result, the Gnosis team were quick to identify, and maintain a deterministic deployment factory (for EIP155 enabled chains) that, if deployed and used on most chains, would produce similar deterministic contract deployments. https://github.com/safe-global/safe-singleton-factory

Find if a safe singleton factory exists on the chain, as an alternative for the deterministic deployment of Permit2

To deploy the Permit2 using the deterministic deployment proxy, you will need to send a tx to the deployment proxy, with data = encoded(salt, depl_code)

Here’s a script that can enable you to submit such a tx
https://gist.github.com/souradeep-das/a30fd91296741234d11fabc5b06b9a72

Alright, let’s return back to the parameters we took a look at before:

permit2: set it to address(0) if you have the deterministic deployment proxy at “0x4e59b44847b379578588920ca78fbf26c0b4956c” and want the script to deploy it for you (or) if you do not — refer to the previous section and deploy the Permit2 using the alt proxy

weth9: set to the WETH9 address on the chain

v3Factory: set to the UniswapV3Factory that was deployed as a part of the core v3 deployment

poolInitCodeHash: set to “0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54” according to the value on your PoolAddress.sol contract (from the core deployments)

pairInitCodeHash: set to BYTES32_ZERO if UniswapV2 is not supported

unsupported: set to address(0) if you want this to be deployed along with the script

All the remaining parameters need to be set to address(0) or UNSUPPORTED_PROTOCOL, or 0 values

Create a new file under script/deployParameters — DeployNetwork.s.sol and add in the appropriate parameters. Use DeployGoerli.s.sol as reference (you will have to replace the parameters and the contract name)

Assuming you have now put the required parameters in a file called DeployNetwork.s.sol, the command to deploy is:

$ forge script --broadcast --rpc-url <rpc-url> --private-key <your-private-key> --sig 'run()' script/deployParameters/DeployNetwork.s.sol:DeployNetwork

If your chain is among the supported networks for forge verification, you could also verify the contracts with the deployment by adding the following three flags

$ forge script --broadcast --rpc-url <rpc-url> --private-key <your-private-key> --sig 'run()' script/deployParameters/DeployNetwork.s.sol:DeployNetwork --chain-id <chain-id-network> --etherscan-api-key <etherscan-api-key> --verify

Fixing common errors

Error:
Failed to get EIP-1559 fees

This means the chain you are trying to deploy doesn’t support EIP-1559, in which case deploy with the following flag added

$ forge script --broadcast --rpc-url <rpc-url> --private-key <your-private-key> --sig 'run()' script/deployParameters/DeployNetwork.s.sol:DeployNetwork --chain-id <chain-id-network> --etherscan-api-key <etherscan-api-key> --verify --legacy

Error:
Nonce already used

$ forge script --broadcast --rpc-url <rpc-url> --private-key <your-private-key> --sig 'run()' script/deployParameters/DeployNetwork.s.sol:DeployNetwork --chain-id <chain-id-network> --etherscan-api-key <etherscan-api-key> --verify --legacy --slow

Once your contracts are deployed, make sure the arguments used were what you intended to use, and the protocols unsupported are set to the deployed unsupportedProtocol contract address. Refer to the generate file — latest.json under broadcast/ for the deployment info.

Verifying the contracts for other non-supported chains

To verify the UnsupportedProtocol and Universal Router contracts, make sure you are on the universal-router repo, and run the following:

$ forge verify-contract <unsupported-protocol-address> contracts/deploy/UnsupportedProtocol.sol:UnsupportedProtocol --verifier-url <etherscan-api-url> --compiler-version "v0.8.17+commit.8df45f5f" --optimizer-runs 1000000 --chain-id <chain-id> --watch

And

$ forge verify-contract <universal-router-address> contracts/UniversalRouter.sol:UniversalRouter --constructor-args <encoded-constructor-args> --verifier-url <etherscan-api-url> --compiler-version "v0.8.17+commit.8df45f5f" --optimizer-runs 1000000 --chain-id <chain-id> --watch

One way to obtain the encoded constructor args is to use cast from foundry

$ cast abi.encode "constructor(address,address,address,address,address,address,address,address,address,address,address,address,address,address,address,address,address,address,bytes32,bytes32,address,uint256)" <arg-1> <arg-2> ….. <arg-22>

For eg, on Boba

$ forge verify-contract 0x020A39620A5af7Ff456B2523C35fc8B897E9071a contracts/deploy/UnsupportedProtocol.sol:UnsupportedProtocol --verifier-url https://api.bobascan.com/api --compiler-version "v0.8.17+commit.8df45f5f" --optimizer-runs 1000000 --chain-id 288 --watch

To verify permit2, grab the permit2 repo here: https://github.com/Uniswap/permit2

$ git clone https://github.com/Uniswap/permit2.git
$ cd permit2
$ forge install

Now set ETHERSCAN_API_KEY as an env var
And then proceed to verify with

$ forge verify-contract <permit2-contract-address> src/Permit2.sol:Permit2 --verifier-url <etherscan-api-url> --compiler-version "v0.8.17+commit.8df45f5f" --optimizer-runs 1000000 --chain-id <chain-id-of-network> --watch

For eg, on Boba

$ forge verify-contract 0xF80c91442D3EF66632958C0d395667075FC82fB0 src/Permit2.sol:Permit2 --verifier-url https://api.bobascan.com/api --compiler-version "v0.8.17+commit.8df45f5f" --optimizer-runs 1000000 --chain-id 288 --watch

Congratulations! With all of that done, you now have a deployed set of Uniswap V3 contracts at your disposal, plus a universal router to go with it, in case you need one.

One more thing? No!
You may now proceed to sip on that well deserved Boba you’ve been longing for!
Goodbye for now 😉

LATEST