The Uniswap Universal Router was adapted to work with the Velodrome ecosystem. This router allows a single swap to be routed through both Velodrome Finance pools and Uniswap pools.
To see the commit of the smart contracts that was used in the latest deployment, see branch deployed-commit. To see the addresses of this latest deployment on each network, see folder deploy-addresses.
The Universal Router is a ERC20 and NFT swap router that allows users greater flexibility when performing trades across multiple token types.
Our flexible command style allows us to provide users with:
- Splitting and interleaving of Uniswap trades
- Purchases of NFTs across 8 marketplaces
- Partial fills of trades
- Wrapping and Unwrapping of ETH
- Time-bound, signature controlled token approvals using Permit2
Transactions are encoded using a string of commands, allowing users to have maximum flexibility over what they want to perform. With all of these features available in a single transaction, the possibilities available to users are endless
The Universal Router codebase consists of the UniversalRouter contract, and all of its dependencies. The purpose of the UniversalRouter is to allow users to unify Uniswap ERC20 swaps (on V2 and V3) with NFT purchases across 8 marketplaces, in a single transaction.
UniversalRouter integrates with Permit2, to enable users to have more safety, flexibility, and control over their ERC20 token approvals.
Calls to UniversalRouter.execute, the entrypoint to the contracts, provide 2 main parameters:
bytes commands: A bytes string. Each individual byte represents 1 command that the transaction will execute.bytes[] inputs: An array of bytes strings. Each element in the array is the encoded parameters for a command.
commands[i] is the command that will use inputs[i] as its encoded input parameters.
Through function overloading there is also an optional third parameter for the execute function:
uint256 deadline: The timestamp deadline by which this transaction must be executed. Transactions executed after this specified deadline will revert.
Each command is a bytes1 containing the following 8 bits:
0 1 2 3 4 5 6 7
┌─┬─┬───────────┐
│f│r| command │
└─┴─┴───────────┘
-
fis a single bit flag, that signals whether or not the command should be allowed to revert. Iffisfalse, and the command reverts, then the entire transaction will revert. Iffistrueand the command reverts then the transaction will continue, allowing us to achieve partial fills. If using this flag, be careful to include further commands that will remove any funds that could be left unused in theUniversalRoutercontract. -
ris one bit of reserved space. This will allow us to increase the space used for commands, or add new flags in future. -
commandis a 6 bit unique identifier for the command that should be carried out. The values of these commands can be found within Commands.sol, or can be viewed in the table below.
┌──────┬───────────────────────────────┐
│ 0x00 │ V3_SWAP_EXACT_IN │
├──────┼───────────────────────────────┤
│ 0x01 │ V3_SWAP_EXACT_OUT │
├──────┼───────────────────────────────┤
│ 0x02 │ PERMIT2_TRANSFER_FROM │
├──────┼───────────────────────────────┤
│ 0x03 │ PERMIT2_PERMIT_BATCH │
├──────┼───────────────────────────────┤
│ 0x04 │ SWEEP │
├──────┼───────────────────────────────┤
│ 0x05 │ TRANSFER │
├──────┼───────────────────────────────┤
│ 0x06 │ PAY_PORTION │
├──────┼───────────────────────────────┤
│ 0x07 │ ------- │
├──────┼───────────────────────────────┤
│ 0x08 │ V2_SWAP_EXACT_IN │
├──────┼───────────────────────────────┤
│ 0x09 │ V2_SWAP_EXACT_OUT │
├──────┼───────────────────────────────┤
│ 0x0a │ PERMIT2_PERMIT │
├──────┼───────────────────────────────┤
│ 0x0b │ WRAP_ETH │
├──────┼───────────────────────────────┤
│ 0x0c │ UNWRAP_WETH │
├──────┼───────────────────────────────┤
│ 0x0d │ PERMIT2_TRANSFER_FROM_BATCH │
├──────┼───────────────────────────────┤
│ 0x0e-│ ------- │
│ 0x20 │ │
├──────┼───────────────────────────────┤
│ 0x21 │ EXECUTE_SUB_PLAN │
├──────┼───────────────────────────────┤
│ 0x22-│ ------- │
│ 0x3f │ │
└──────┴───────────────────────────────┘
Note that some of the commands in the middle of the series are unused. These gaps allowed us to create gas-efficiencies when selecting which command to execute.
Each input bytes string is merely the abi encoding of a set of parameters. Depending on the command chosen, the input bytes string will be different. For example:
The inputs for V3_SWAP_EXACT_IN is the encoding of 5 parameters:
addressThe recipient of the output of the tradeuint256The amount of input tokens for the tradeuint256The minimum amount of output tokens the user wantsbytesThe UniswapV3 path you want to trade alongboolA flag for whether the input funds should come from the caller (through Permit2) or whether the funds are already in the UniversalRouterboolA flag for whether the swap should be executed using UniV3 or Slipstream pools
Whereas in contrast CRYPTOPUNKS has just 3 parameters encoded:
uint256The ID of the punk you wish to purchaseaddressThe recipient of the punkuint256The amount of ETH to pay for the punk
Encoding parameters in a bytes string in this way gives us maximum flexiblity to be able to support many commands which require different datatypes in a gas-efficient way.
For a more detailed breakdown of which parameters you should provide for each command take a look at the Dispatcher.dispatch function, or alternatively at the ABI_DEFINITION mapping in planner.ts.
Developer documentation to give a detailed explanation of the inputs for every command will be coming soon!
- Clone the repository with all submodules
Clone the repository with:
git clone --recurse-submodules https://github.com/Uniswap/universal-router.git
- Create
.envfile with a complete fork url.
OPTIMISM_RPC_URL=''
- Run yarn commands to compile and test
yarn install
yarn compile
yarn testIf you run into an issue on yarn compile where it cannot find the dependencies in the lib folder try to clone all the submodules with:
git submodule update --init --recursive
yarn test:gasforge install
forge build
forge test- Install the latest version of
@uniswap/universal-routerpackage. - Add git submodules for contracts that aren't a node package. Make sure there's an empty
.gitmodulesfile. Then run:git submodule add https://github.com/transmissions11/solmate git submodule add https://github.com/Uniswap/permit2
- You should now be able to import contracts from universal-router and compile.
Before you submit your PR, run all of the following and commit the changes:
# make sure all tests pass this will also update gas snapshots
yarn test:all
# lint code
yarn prettier:fixIf you are only concerned with investigating gas diffs, you can run this command to only run gas tests
yarn test:gasFill out parameters in script/deployParameters/Deploy<network>.s.sol
For the supported chain list, the wrapper script performs a simulation first and only broadcasts when --broadcast is passed:
./script/deploy.sh <network>
./script/deploy.sh <network> --broadcastTo run every configured chain:
./script/deployAll.sh
./script/deployAll.sh --broadcastAfter broadcasting, verify all deployed routers against local bytecode and deploy parameters:
./script/verifyDeploy.sh --allforge script --broadcast \
--rpc-url <RPC-URL> \
--private-key <PRIVATE_KEY> \
--sig 'run()' \
script/deployParameters/Deploy<network>.s.sol:Deploy<network>forge script --broadcast \
--rpc-url <RPC-URL> \
--private-key <PRIVATE-KEY> \
--sig 'run()' \
script/deployParameters/Deploy<network>.s.sol:Deploy<network> \
--etherscan-api-key <ETHERSCAN-API-KEY> \
--verifyTron (TVM) requires a different deployment flow because:
forge script --broadcastis not viable on Tron — Tron usesref_block_hash/ref_block_bytesfor replay protection instead of nonces, so JSON-RPC adapters (TronGrid, Chainstack) do not exposeeth_getTransactionCountoreth_feeHistory.- CreateX is not deployed on Tron mainnet, so addresses are not cross-chain deterministic.
- The TVM
CREATE2prefix is0x41instead of EVM's0xff— pool address computation must use the override atcontracts/overrides/tron/Create2.sol.
Always build with FOUNDRY_PROFILE=tron so V3SwapRouter.computePoolAddress resolves to the 0x41-prefix Create2:
FOUNDRY_PROFILE=tron forge buildThe compiled artifacts land in out-tron/. Verify the substitution took effect:
# The bytecode hash must differ from the default profile output
FOUNDRY_PROFILE=tron forge inspect UniversalRouter bytecode | sha256sum
forge inspect UniversalRouter bytecode | sha256sumRun the SunSwap V3 pool address unit tests to confirm the override is working:
FOUNDRY_PROFILE=tron forge test --match-path "test/tron/SunSwapPoolAddress.t.sol" -vPermit2 and UnsupportedProtocol are pre-deployed on Tron mainnet and hardcoded in DeployTron.s.sol:
| Contract | Tron address |
|---|---|
| Permit2 | 0x7CD82628f138187BbCa0b8A075cDcd4D99fc7A52 |
| UnsupportedProtocol | 0x7e1AD7cCDd670b7A44083945bf4c2D7f8b5CF3B8 |
If redeploying from scratch, use deploy-tron.mjs to send a CreateSmartContract transaction directly via the TronGrid REST API.
Because forge cannot broadcast to Tron, deployment is driven by the Node.js script script/deploy-tron.mjs, which reads the compiled bytecode from out-tron/ and submits Tron-native transactions.
The script takes two env vars:
| Var | Description |
|---|---|
TRON_RPC_URL |
A Tron JSON-RPC endpoint. TronGrid (https://api.trongrid.io/jsonrpc), Alchemy (https://tron-mainnet.g.alchemy.com/v2/<key>), or Nile testnet (https://nile.trongrid.io/jsonrpc). |
TRON_PRIVATE_KEY |
Deployer private key, hex with 0x prefix. |
Note: For non-TronGrid/non-localhost URLs (e.g. Alchemy), the script uses that URL for JSON-RPC queries (ethers) but falls back to public TronGrid for TronWeb REST calls (
/wallet/*). Both paths work for deployment.
FOUNDRY_PROFILE=tron forge build # ensure out-tron/ is up to date
TRON_RPC_URL=https://tron-mainnet.g.alchemy.com/v2/<key> \
TRON_PRIVATE_KEY=0x<deployer-key> \
yarn deploy:tronThe script is idempotent — if an address is already recorded in deployment-addresses/tron.json it skips that contract. The deployed UniversalRouter address is written to that file automatically.
To test against Tron's public testnet before spending mainnet TRX, use the Nile testnet and fund your deployer from the faucet at nileex.io:
TRON_RPC_URL=https://nile.trongrid.io/jsonrpc \
TRON_PRIVATE_KEY=0x<deployer-key> \
yarn deploy:tronAfter deployment, confirm the router resolves SunSwap V3 pools correctly by calling quoteExactInput against a known USDT/WTRX pool and comparing to the live pool addresses in test/tron/SunSwapPoolAddress.t.sol.
Fill out parameters in scripts/deployParameters/<network>.json
forge script --broadcast \
--rpc-url <RPC-URL> \
--private-key <PRIVATE_KEY> \
--sig 'runAndDeployPermit2(string)' \
script/deployParameters/DeployUniversalRouter.s.sol:DeployUniversalRouter \
<pathToJSON>