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! As of the current stance, the Uniswap community has been actively voting to deploy V3 on several other chains too, and with the license expiry this April- we may expect several other V3 forks to come up soon.
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
1 | $ git clone https://github.com/Uniswap/deploy-v3.git |
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 an 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)
1 | pragma solidity 0.7.6; |
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” -
1 | $ 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
1 | $ 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
1 | $ 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 (^3.1.6)
Update it to the latest with
1 | $ npm remove @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-
1 | networks: { |
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:
1 | v3-core: UniswapV3Factory.sol, |
For each of these verify the contracts by running the following command on the respective repositories:
1 | $ npx hardhat verify --network <network-name> <deployed-contract-address> |
For eg, verifying the UniswapV3Factory on Boba (use your own address)
1 | $ 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
1 | $ 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)
1 | $ npx hardhat verify --network boba-mainnet 0xb6751A274EDAe02A911E3bB23682FAaF380433b7 "0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000" "0x0000000000000000000000000000000000000000000000000000000000455448" |
And verifying the UniswapV3Staker on Boba
1 | 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
1 | $ git clone --recurse-submodules https://github.com/Uniswap/universal-router.git |
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
1 | permit2, |
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:
1 | $ 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
1 | $ 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
1 | $ 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
1 | $ 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:
1 | $ 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
1 | $ 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
1 | $ 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
1 | $ 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
1 | $ git clone https://github.com/Uniswap/permit2.git |
Now set ETHERSCAN_API_KEY
as an env var
And then proceed to verify with
1 | $ 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
1 | $ 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.
Cheers, and Goodbye for now!