0x Settler
Settlement contracts utilising Permit2 to perform swaps without any passive allowances to the contract.
How do I find the most recent deployment?
The 0x Settler deployer/registry contract is deployed to
0x00000000000004533Fe15556B1E086BB1A72cEae across all chains (unless somebody
screwed up the vanity address and didn't update this document). The
deployer/registry is an ERC1967 UUPS upgradeable contract that implements an
ERC721-compatible NFT. To find the address of the most recent Settler
deployment, call function ownerOf(uint256 tokenId) external view returns (address)
with the tokenId set to the number of the feature that you wish to query. For
taker-submitted flows, the feature number is probably 2 unless something major
changed and nobody updated this document. For gasless/metatransaction flows, the
feature number is probably 3. For intents, the feature number is probably 4. For
bridge settler, the feature number is probably 5. A reverting response indicates
that Settler is paused and you should not interact. Do not hardcode any Settler
address in your integration. _ALWAYS_ query the deployer/registry for the address
of the most recent Settler contract before building or signing a transaction,
metatransaction, or order.
0x API dwell time
There is some lag between the deployment of a new instance of 0x Settler and
when 0x API begins generating calldata targeting that instance. This allows 0x
to perform extensive end-to-end testing to ensure zero downtime for
integrators. During this "dwell" period, a strict comparison between the
.transaction.to
field of the API response and the result of querying
IERC721(0x00000000000004533Fe15556B1E086BB1A72cEae).ownerOf(...) will
fail. For this reason, there is a fallback. If ownerOf does not revert, but
the return value isn't the expected value, _YOU SHOULD ALSO_ query the
selector function prev(uint128) external view returns (address) with the same
argument. If the response from this function call does not revert and the result
is the expected address, then the 0x API is in the dwell time and you may
proceed as normal.
Example Solidity code for checking whether Settler is genuine
interface IERC721Tiny {
function ownerOf(uint256 tokenId) external view returns (address);
}
interface IDeployerTiny is IERC721Tiny {
function prev(uint128 featureId) external view returns (address);
}
IDeployerTiny constant ZERO_EX_DEPLOYER =
IDeployerTiny(0x00000000000004533Fe15556B1E086BB1A72cEae);
error CounterfeitSettler(address);
function requireGenuineSettler(uint128 featureId, address allegedSettler)
internal
view
{
// Any revert in ownerOf or prev will be bubbled. Any error in
// ABIDecoding the result will result in a revert without a reason string.
if (ZERO_EX_DEPLOYER.ownerOf(featureId) != allegedSettler
&& ZERO_EX_DEPLOYER.prev(featureId) != allegedSettler) {
revert CounterfeitSettler(allegedSettler);
}
}
While the above code is the _strongly recommended_ approach, it is comparatively gas-expensive. A more gas-optimized approach is demonstrated below, but it does not cover the case where Settler has been paused due to a bug.
function computeGenuineSettler(uint128 featureId, uint64 deployNonce)
internal
view
returns (address)
{
address zeroExDeployer = 0x00000000000004533Fe15556B1E086BB1A72cEae;
bytes32 salt = bytes32(
uint256(featureId) << 128 | uint256(block.chainid) << 64
| uint256(deployNonce)
);
// for London hardfork chains, substitute
// 0x1774bbdc4a308eaf5967722c7a4708ea7a3097859cb8768a10611448c29981c3
bytes32 shimInitHash =
0x3bf3f97f0be1e2c00023033eefeb4fc062ac552ff36778b17060d90b6764902f;
address shim =
address(
uint160(
uint256(
keccak256(
abi.encodePacked(
bytes1(0xff),
zeroExDeployer,
salt,
shimInitHash
)
)
)
)
);
address settler =
address(
uint160(
uint256(
keccak256(
abi.encodePacked(bytes2(0xd694), shim, bytes1(0x01))
)
)
)
);
return settler;
}
AllowanceHolder addresses
AllowanceHolder is deployed to the following addresses depending on the most advanced EVM hardfork supported on the chain. You can hardcode this address in your integration.
0x0000000000001fF3684f28c67538d4D072C22734on chains supporting the Cancun
0x0000000000005E88410CcDFaDe4a5EfaE4b49562on chains supporting the Shanghai
ERC2771 forwarding MultiCall address
The ERC2771 forwarding MultiCall is deployed to
0x00000000000000CF9E3c5A26621af382fA17f24f across all chains. You can hardcode
this address in your integration. I have no idea why you would want to do that,
but I guess it's a thing that you can do. The ERC2771 forwarding MultiCall is
exclusively used by 0x's solvers for the SettlerIntent flavor of 0x Settler.
CrossChainReceiverFactory address
CrossChainReceiverFactory is deployed to
0x00000000000000304861c3aDfb80dd5ebeC96325 across all chains. You can hardcode
this address in your integration. This contract is used to deploy counterfactual
(submarine) addresses to faciliate swapping and other actions on foreign chains
after bridging.
Permit2 address
Permit2 is deployed to 0x000000000022D473030F116dDEE9F6B43aC78BA3 across all
chains. You can hardcode this address in your integration.
Examples
TypeScript (viem)
Click to see TypeScript example of getting Settler addresses
import { createPublicClient, http, parseAbi } from 'viem';
(async function main() {
const client = createPublicClient({
transport: http(process.env.RPC_URL),
});
const deployer = "0x00000000000004533Fe15556B1E086BB1A72cEae";
const tokenDescriptions = {
2: "taker submitted",
3: "metatransaction",
4: "intents",
5: "bridge",
};
const deployerAbi = parseAbi([
"function prev(uint128) external view returns (address)",
"function ownerOf(uint256) external view returns (address)",
"function next(uint128) external view returns (address)",
]);
const functionDescriptions = {
"prev": "previous",
"ownerOf": "current",
"next": "next",
};
const blockNumber = await client.getBlockNumber();
for (let tokenId in tokenDescriptions) {
for (let functionName in functionDescriptions) {
let addr = await client.readContract({
address: deployer,
abi: deployerAbi,
functionName,
args: [tokenId],
blockNumber,
});
console.log(functionDescriptions[functionName] + " " + tokenDescriptions[tokenId] + " settler address " + addr);
}
}
// output:
// previous taker submitted settler address 0xDf31A70a21A1931e02033dBBa7DEaCe6c45cfd0f
// current taker submitted settler address 0xD07C4F45bc9B389d286CB094F97BB6Ba45f4Bb30
// next taker submitted settler address 0x207e1074858A7e78f17002075739eD2745dbaEce
// previous metatransaction settler address 0x8e3F00919aD73B4C525BE50E86718d86386AF944
// current metatransaction settler address 0x42b17b6B6b93383d52F21Dc61546974E014241bf
// next metatransaction settler address 0x1e1Ed00F1048C99240bB56defC20de44A0A005Cb
// previous intents settler address 0x8331f9ACcE69b02C281F40a00706f758665ccE77
// current intents settler address 0xF3D5719F7E3b49A4c3797d56BD3FED6D3B80F0F9
// next intents settler address 0x25233dDD68dAc9ac40E1E8D80c58d36b524032b2
// previous bridge settler address 0x37C15fed8F78C252d272c47FaF9564d0C5350D29
// current bridge settler address 0x2aaEECD66779f1B517abAD78611115a99E0558c2
// next bridge settler address 0x3DECc6d88C2F0fa5cFa3B4f0AcEb3FE60Afc2B30
})();
JavaScript (Ethers.js)
Click to see JavaScript example of getting Settler addresses
Note that this example uses version 5 of Ethers.js. The current version of
Ethers.js is 6, which is not compatible with this snippet.
"use strict";
const {ethers} = require("ethers");
(async function main() {
const provider = new ethers.providers.JsonRpcProvider(process.env.RPC_URL);
const deployerAddress = "0x00000000000004533Fe15556B1E086BB1A72cEae";
const tokenDescriptions = {
2: "taker submitted",
3: "metatransaction",
4: "intents",
5: "bridge",
};
const deployerAbi = [
"function prev(uint128) external view returns (address)",
"function ownerOf(uint256) external view returns (address)",
"function next(uint128) external view returns (address)",
];
const functionDescriptions = {
"prev": "previous",
"ownerOf": "current",
"next": "next",
};
const deployer = new ethers.Contract(deployerAddress, deployerAbi, provider);
for (let tokenId in tokenDescriptions) {
for (let functionName in functionDescriptions) {
let addr = await deployerfunctionName;
console.log(functionDescriptions[functionName] + " " + tokenDescriptions[tokenId] + " settler address " + addr);
}
}
// output:
// previous taker submitted settler address 0xDf31A70a21A1931e02033dBBa7DEaCe6c45cfd0f
// current taker submitted settler address 0xD07C4F45bc9B389d286CB094F97BB6Ba45f4Bb30
// next taker submitted settler address 0x207e1074858A7e78f17002075739eD2745dbaEce
// previous metatransaction settler address 0x8e3F00919aD73B4C525BE50E86718d86386AF944
// current metatransaction settler address 0x42b17b6B6b93383d52F21Dc61546974E014241bf
// next metatransaction settler address 0x1e1Ed00F1048C99240bB56defC20de44A0A005Cb
// previous intents settler address 0x8331f9ACcE69b02C281F40a00706f758665ccE77
// current intents settler address 0xF3D5719F7E3b49A4c3797d56BD3FED6D3B80F0F9
// next intents settler address 0x25233dDD68dAc9ac40E1E8D80c58d36b524032b2
// previous bridge settler address 0x37C15fed8F78C252d272c47FaF9564d0C5350D29
// current bridge settler address 0x2aaEECD66779f1B517abAD78611115a99E0558c2
// next bridge settler address 0x3DECc6d88C2F0fa5cFa3B4f0AcEb3FE60Afc2B30
})();
Rust (Alloy)
Cargo.toml
[package]
name = "scratch"
version = "0.1.0"
edition = "2021"
[dependencies]
alloy = { git = "https://github.com/alloy-rs/alloy", rev = "e22d9be", features = [
"contract",
"network",
"providers",
"provider-http",
"rpc-client",
"rpc-types-eth",
"rpc-types-trace",
] }
eyre = "0.6.12"
tokio = { version = "1.37.0", features = ["rt-multi-thread", "macros"] }
Click to see Rust example of getting Settler addresses
use alloy::{
network::TransactionBuilder,
primitives::{address, Address, Bytes, U256},
providers::{Provider, ProviderBuilder},
rpc::types::eth::{BlockId, TransactionRequest},
sol,
sol_types::SolCall,
};
use eyre::Result;
use std::collections::HashMap;
use std::env;
const DEPLOYER_ADDRESS: Address = address!("00000000000004533Fe15556B1E086BB1A72cEae");
sol! {
function prev(uint128 featureId) external view returns (address pastTokenOwner);
function ownerOf(uint256 tokenId) external view returns (address tokenOwner);
function next(uint128 featureId) external view returns (address futureTokenOwner);
}
#[tokio::main]
async fn main() -> Result<()> {
let provider = ProviderBuilder::new().on_http(env::var("RPC_URL")?.parse()?);
let block_id = BlockId::number(provider.get_block_number().await?);
let token_ids = vec![2, 3, 4];
let token_descriptions = HashMap::from([
(2, "taker submitted"),
(3, "metatransaction"),
(4, "intents"),
(5, "bridge")
]);
for token_id in token_ids.iter() {
{
let tx = TransactionRequest::default()
.with_to(DEPLOYER_ADDRESS)
.with_input(Bytes::from(
prevCall {
featureId: *token_id,
}
.abi_encode(),
));
let past_owner =
prevCall::abi_decode_returns(&provider.call(&tx).block(block_id).await?, false)?
.pastTokenOwner;
println!(
"previous {0:} settler address {1:}",
token_descriptions[token_id], past_owner
);
}
{
let tx = TransactionRequest::default()
.with_to(DEPLOYER_ADDRESS)
.with_input(Bytes::from(
ownerOfCall {
tokenId: U256::from(*token_id),
}
.abi_encode(),
));
let token_owner =
ownerOfCall::abi_decode_returns(&provider.call(&tx).block(block_id).await?, false)?
.tokenOwner;
println!(
"current {0:} settler address {1:}",
token_descriptions[token_id], token_owner
);
}
{
let tx = TransactionRequest::default()
.with_to(DEPLOYER_ADDRESS)
.with_input(Bytes::from(
nextCall {
featureId: *token_id,
}
.abi_encode(),
));
let future_owner =
nextCall::abi_decode_returns(&provider.call(&tx).block(block_id).await?, false)?
.futureTokenOwner;
println!(
"next {0:} settler address {1:}",
token_descriptions[token_id], future_owner
);
}
}
// output:
// previous taker submitted settler address 0xDf31A70a21A1931e02033dBBa7DEaCe6c45cfd0f
// current taker submitted settler address 0xD07C4F45bc9B389d286CB094F97BB6Ba45f4Bb30
// next taker submitted settler address 0x207e1074858A7e78f17002075739eD2745dbaEce
// previous metatransaction settler address 0x8e3F00919aD73B4C525BE50E86718d86386AF944
// current metatransaction settler address 0x42b17b6B6b93383d52F21Dc61546974E014241bf
// next metatransaction settler address 0x1e1Ed00F1048C99240bB56defC20de44A0A005Cb
// previous intents settler address 0x8331f9ACcE69b02C281F40a00706f758665ccE77
// current intents settler address 0xF3D5719F7E3b49A4c3797d56BD3FED6D3B80F0F9
// next intents settler address 0x25233dDD68dAc9ac40E1E8D80c58d36b524032b2
// previous bridge settler address 0x37C15fed8F78C252d272c47FaF9564d0C5350D29
// current bridge settler address 0x2aaEECD66779f1B517abAD78611115a99E0558c2
// next bridge settler address 0x3DECc6d88C2F0fa5cFa3B4f0AcEb3FE60Afc2B30
Ok(())
}
Python (web3.py)
Click to see Python example of getting Settler addresses
import os, web3
w3 = web3.Web3(web3.Web3.HTTPProvider(os.getenv("RPC_URL")))
deployer_address = "0x00000000000004533Fe15556B1E086BB1A72cEae"
token_descriptions = {
2: "taker submitted",
3: "metatransaction",
4: "intents",
5: "bridge",
}
deployer_abi = [
{
"constant": True,
"inputs": [{"name": "featureId", "type": "uint128"}],
"name": "prev",
"outputs": [{"name": "pastTokenOwner", "type": "address"}],
"payable": False,
"type": "function",
},
{
"constant": True,
"inputs": [{"name": "tokenId", "type": "uint256"}],
"name": "ownerOf",
"outputs": [{"name": "tokenOwner", "type": "address"}],
"payable": False,
"type": "function",
},
{
"constant": True,
"inputs": [{"name": "featureId", "type": "uint128"}],
"name": "next",
"outputs": [{"name": "futureTokenOwner", "type": "address"}],
"payable": False,
"type": "function",
},
]
function_descriptions = {
"prev": "previous",
"ownerOf": "current",
"next": "next",
}
deployer = w3.eth.contract(address=deployer_address, abi=deployer_abi)
for token_id, token_description in token_descriptions.items():
for function_name, function_description in function_descriptions.items():
settler_address = getattr(deployer.functions, function_name)(token_id).call()
print(
function_description,
token_description,
"settler address",
settler_address,
)
output:
previous taker submitted settler address 0xDf31A70a21A1931e02033dBBa7DEaCe6c45cfd0f
current taker submitted settler address 0xD07C4F45bc9B389d286CB094F97BB6Ba45f4Bb30
next taker submitted settler address 0x207e1074858A7e78f17002075739eD2745dbaEce
previous metatransaction settler address 0x8e3F00919aD73B4C525BE50E86718d86386AF944
current metatransaction settler address 0x42b17b6B6b93383d52F21Dc61546974E014241bf
next metatransaction settler address 0x1e1Ed00F1048C99240bB56defC20de44A0A005Cb
previous intents settler address 0x8331f9ACcE69b02C281F40a00706f758665ccE77
current intents settler address 0xF3D5719F7E3b49A4c3797d56BD3FED6D3B80F0F9
next intents settler address 0x25233dDD68dAc9ac40E1E8D80c58d36b524032b2
previous bridge settler address 0x37C15fed8F78C252d272c47FaF9564d0C5350D29
current bridge settler address 0x2aaEECD66779f1B517abAD78611115a99E0558c2
next bridge settler address 0x3DECc6d88C2F0fa5cFa3B4f0AcEb3FE60Afc2B30
Bash (Foundry cast)
Click to see Bash (cast) example of getting Settler addresses
#!/usr/bin/env bash
set -Eeufo pipefail -o posix
if ! hash cast &>/dev/null ; then
echo 'foundry is not installed' >&2
exit 1
fi
declare -r deployer='0x00000000000004533Fe15556B1E086BB1A72cEae'
declare -A token_descriptions
token_descriptions[2]='taker submitted'
token_descriptions[3]='metatransaction'
token_descriptions[4]='intents'
token_descriptions[5]='bridge'
declare -r -A token_descriptions
declare -r -a function_signatures=('prev(uint128)(address)' 'ownerOf(uint256)(address)' 'next(uint128)(address)')
declare -A function_descriptions
function_descriptions["${function_signatures[0]%%(*}"]='previous'
function_descriptions["${function_signatures[1]%%(*}"]='current'
function_descriptions["${function_signatures[2]%%(*}"]='next'
declare -r -A function_descriptions
declare -i token_id
for token_id in "${!token_descriptions[@]}" ; do
declare function_signature
for function_signature in "${function_signatures[@]}" ; do
declare addr
addr="$(cast call --rpc-url "$RPC_URL" "$deployer" "$function_signature" "$token_id")"
function_signature="${function_signature%%(*}"
echo "${function_descriptions["$function_signature"]}"' '"${token_descriptions[$token_id]}"' settler address '"$addr" >&2
done
done
output:
previous bridge settler address 0x37C15fed8F78C252d272c47FaF9564d0C5350D29
current bridge settler address 0x2aaEECD66779f1B517abAD78611115a99E0558c2
next bridge settler address 0x3DECc6d88C2F0fa5cFa3B4f0AcEb3FE60Afc2B30
previous intents settler address 0x8331f9ACcE69b02C281F40a00706f758665ccE77
current intents settler address 0xF3D5719F7E3b49A4c3797d56BD3FED6D3B80F0F9
next intents settler address 0x25233dDD68dAc9ac40E1E8D80c58d36b524032b2
previous metatransaction settler address 0x8e3F00919aD73B4C525BE50E86718d86386AF944
current metatransaction settler address 0x42b17b6B6b93383d52F21Dc61546974E014241bf
next metatransaction settler address 0x1e1Ed00F1048C99240bB56defC20de44A0A005Cb
previous taker submitted settler address 0xDf31A70a21A1931e02033dBBa7DEaCe6c45cfd0f
current taker submitted settler address 0xD07C4F45bc9B389d286CB094F97BB6Ba45f4Bb30
next taker submitted settler address 0x207e1074858A7e78f17002075739eD2745dbaEce
Checking out the commit of a Settler
Settler emits the following event when it is deployed:
event GitCommit(bytes20 indexed);
By retrieving the argument of this event, you get the git commit from which the
Settler was built. For convenience, the script ./sh/checkout_settler_commit.sh
will pull the latest Settler
address, read the deployment event, and checkout the git commit.
Bug Bounty Program
0x hosts a bug bounty on Immunefi at the address https://immunefi.com/bug-bounty/0x .
If you have found a vulnerability in our project, it must be submitted through Immunefi's platform. Immunefi will handle bug bounty communications.
See the bounty page at Immunefi for more details on accepted vulnerabilities, payout amounts, and rules of participation.
Users who violate the rules of participation will not receive bug bounty payouts and may be temporarily suspended or banned from the bug bounty program.
Custody
Custody, not like the delicious custardy, is when the token(s) being traded are temporarily owned by the Settler contract. This sometimes implies an additional, non-optimal transfer. There are multiple reasons that Settler takes custody of the token, here are a few:
- In the middle of a Multihop trade (except AMMs like UniswapV2 and VelodromeV2)
- To split tokens among multiple liquidity sources (Multiplex)
- To distribute positive slippage from an AMM
- To pay fees to a fee recipient in the buy token from an AMM
- Trading against an inefficient AMM that only supports
transferFrom(msg.sender)(e.g Curve)
custody
to represent this. Sell token or Buy token or both custody is represented by
custody.
Gas usage
Gas cost snapshots are stored under ./forge-snapshots. The scope is minimized
by using forge-gas-snapshot.
There is an initial cost for Permit2 when the token has not been previously used. This adds some non-negligble cost as the storage is changed from a 0 for the first time. For this reason we compare warm (where the nonce is non-0) and cold.
Note: The following is more akin to gasLimit than it is gasUsed, this is due
to the difficulty in calculating pinpoint costs (and rebates) in Foundry
tests. Real world usage will be slightly lower, but it serves as a useful
comparison.
[//]: # "BEGIN TABLES"
| VIP | DEX | Pair | Gas | % | | ------------------- | ---------- | --------- | ------ | ------ | | 0x V4 VIP | Uniswap V3 | USDC/WETH | 121309 | 0.00% | | 0x V4 Multiplex | Uniswap V3 | USDC/WETH | 135211 | 11.46% | | Settler VIP (warm) | Uniswap V3 | USDC/WETH | 129975 | 7.14% | | AllowanceHolder VIP | Uniswap V3 | USDC/WETH | 122363 | 0.87% | | UniswapRouter V3 | Uniswap V3 | USDC/WETH | 117706 | -2.97% | | | | | | | | 0x V4 VIP | Uniswap V3 | DAI/WETH | 111980 | 0.00% | | 0x V4 Multiplex | Uniswap V3 | DAI/WETH | 125853 | 12.39% | | Settler VIP (warm) | Uniswap V3 | DAI/WETH | 120646 | 7.74% | | AllowanceHolder VIP | Uniswap V3 | DAI/WETH | 113034 | 0.94% | | UniswapRouter V3 | Uniswap V3 | DAI/WETH | 108377 | -3.22% | | | | | | | | 0x V4 VIP | Uniswap V3 | USDT/WETH | 123571 | 0.00% | | 0x V4 Multiplex | Uniswap V3 | USDT/WETH | 137473 | 11.25% | | Settler VIP (warm) | Uniswap V3 | USDT/WETH | 132171 | 6.96% | | AllowanceHolder VIP | Uniswap V3 | USDT/WETH | 124559 | 0.80% | | UniswapRouter V3 | Uniswap V3 | USDT/WETH | 119840 | -3.02% | | | | | | | | Settler VIP (warm) | Uniswap V3 | WETH/USDC | 130406 | NaN% | | UniswapRouter V3 | Uniswap V3 | WETH/USDC | 118192 | NaN% | | | | | | |
| Custody | DEX | Pair | Gas | % | | -------------------- | ---------- | --------- | ------ | ------- | | 0x V4 TransformERC20 | Uniswap V3 | USDC/WETH | 239864 | 0.00% | | Settler | Uniswap V3 | USDC/WETH | 158908 | -33.75% | | AllowanceHolder | Uniswap V3 | USDC/WETH | 151300 | -36.92% | | | | | | | | 0x V4 TransformERC20 | Uniswap V3 | DAI/WETH | 221467 | 0.00% | | Settler | Uniswap V3 | DAI/WETH | 146864 | -33.69% | | AllowanceHolder | Uniswap V3 | DAI/WETH | 139256 | -37.12% | | | | | | | | 0x V4 TransformERC20 | Uniswap V3 | USDT/WETH | 237179 | 0.00% | | Settler | Uniswap V3 | USDT/WETH | 162245 | -31.59% | | AllowanceHolder | Uniswap V3 | USDT/WETH | 154637 | -34.80% | | | | | | | | Settler | Uniswap V3 | WETH/USDC | 156338 | NaN% | | | | | | |
| MetaTransactions | DEX | Pair | Gas | % | | ---------------- | ---------- | --------- | ------ | ------- | | 0x V4 Multiplex | Uniswap V3 | USDC/WETH | 204706 | 0.00% | | Settler | Uniswap V3 | USDC/WETH | 163974 | -19.90% | | | | | | | | 0x V4 Multiplex | Uniswap V3 | DAI/WETH | 195377 | 0.00% | | Settler | Uniswap V3 | DAI/WETH | 151930 | -22.24% | | | | | | | | 0x V4 Multiplex | Uniswap V3 | USDT/WETH | 206968 | 0.00% | | Settler | Uniswap V3 | USDT/WETH | 167311 | -19.16% | | | | | | | | | | | | |
| RFQ | DEX | Pair | Gas | % | | --------------- | ------- | --------- | ------ | ------- | | 0x V4 | 0x V4 | USDC/WETH | 111684 | 0.00% | | Settler | Settler | USDC/WETH | 114418 | 2.45% | | Settler | 0x V4 | USDC/WETH | 198232 | 77.49% | | AllowanceHolder | Settler | USDC/WETH | 106575 | -4.57% | | | | | | | | 0x V4 | 0x V4 | DAI/WETH | 78553 | 0.00% | | Settler | Settler | DAI/WETH | 94944 | 20.87% | | Settler | 0x V4 | DAI/WETH | 173162 | 120.44% | | AllowanceHolder | Settler | DAI/WETH | 87101 | 10.88% | | | | | | | | 0x V4 | 0x V4 | USDT/WETH | 89665 | 0.00% | | Settler | Settler | USDT/WETH | 106056 | 18.28% | | Settler | 0x V4 | USDT/WETH | 188494 | 110.22% | | AllowanceHolder | Settler | USDT/WETH | 98213 | 9.53% | | | | | | | | Settler | 0x V4 | WETH/USDC | 191787 | NaN% | | | | | | |
| UniversalRouter | DEX | Pair | Gas | % | | --------------- | --------- | --------- | ------ | ------- | | UniversalRouter | UniswapV2 | USDC/WETH | 181544 | 0.00% | | Settler | UniswapV2 | USDC/WETH | 172644 | -4.90% | | UniversalRouter | UniswapV3 | USDC/WETH | 177838 | -2.04% | | Settler | UniswapV3 | USDC/WETH | 174118 | -4.09% | | UniversalRouter | UniswapV4 | USDC/WETH | 142787 | -21.35% | | Settler | UniswapV4 | USDC/WETH | 135362 | -25.44% | | | | | | | | UniversalRouter | UniswapV2 | DAI/WETH | 164039 | 0.00% | | Settler | UniswapV2 | DAI/WETH | 155139 | -5.43% | | UniversalRouter | UniswapV3 | DAI/WETH | 168508 | 2.72% | | Settler | UniswapV3 | DAI/WETH | 164789 | 0.46% | | UniversalRouter | UniswapV4 | DAI/WETH | 125454 | -23.52% | | Settler | UniswapV4 | DAI/WETH | 118029 | -28.05% | | | | | | | | UniversalRouter | UniswapV2 | USDT/WETH | 176035 | 0.00% | | Settler | UniswapV2 | USDT/WETH | 167109 | -5.07% | | UniversalRouter | UniswapV3 | USDT/WETH | 180131 | 2.33% | | Settler | UniswapV3 | USDT/WETH | 176314 | 0.16% | | UniversalRouter | UniswapV4 | USDT/WETH | 137184 | -22.07% | | Settler | UniswapV4 | USDT/WETH | 129759 | -26.29% | | | | | | | | UniversalRouter | UniswapV2 | WETH/USDC | 145094 | 0.00% | | Settler | UniswapV2 | WETH/USDC | 139063 | -4.16% | | UniversalRouter | UniswapV3 | WETH/USDC | 147774 | 1.85% | | Settler | UniswapV3 | WETH/USDC | 147938 | 1.96% | | UniversalRouter | UniswapV4 | WETH/USDC | 109943 | -24.23% | | Settler | UniswapV4 | WETH/USDC | 107136 | -26.16% | | | | | | |
| Curve | DEX | Pair | Gas | % | | ----------------- | --------------------- | --------- | ------ | ------- | | Settler | CurveV2 Tricrypto VIP | USDC/WETH | 231504 | NaN% | | | | | | | | | | | | | | 0x V4 | Curve | USDT/WETH | 400482 | 0.00% | | Settler | Curve | USDT/WETH | 367024 | -8.35% | | Settler | CurveV2 Tricrypto VIP | USDT/WETH | 243871 | -39.11% | | Curve | Curve | USDT/WETH | 287171 | -28.29% | | Curve Swap Router | Curve | USDT/WETH | 359811 | -10.16% | | | | | | | | | | | | |
| DODO V1 | DEX | Pair | Gas | % | | ------- | ------- | --------- | ------ | ----- | | Settler | DODO V1 | USDC/WETH | 293194 | 0.00% | | | | | | | | | | | | | | | | | | | | | | | | |
| Buy token fee | DEX | Pair | Gas | % | | ----------------- | ---------- | --------- | ------ | ----- | | Settler - custody | Uniswap V3 | USDC/WETH | 167661 | 0.00% | | | | | | | | Settler - custody | Uniswap V3 | DAI/WETH | 158303 | 0.00% | | | | | | | | Settler - custody | Uniswap V3 | USDT/WETH | 169857 | 0.00% | | | | | | | | Settler - custody | Uniswap V3 | WETH/USDC | 174047 | 0.00% | | | | | | |
| Sell token fee | DEX | Pair | Gas | % | | -------------- | ---------- | --------- | ------ | ------- | | Settler | Uniswap V3 | USDC/WETH | 173032 | 0.00% | | | | | | | | Settler | Uniswap V3 | DAI/WETH | 158273 | 0.00% | | | | | | | | Settler | Uniswap V3 | USDT/WETH | 175430 | 0.00% | | Settler | Curve | USDT/WETH | 378164 | 115.56% | | | | | | | | Settler | Uniswap V3 | WETH/USDC | 167467 | 0.00% | | | | | | |
| AllowanceHolder | DEX | Pair | Gas | % | | ------------------------------------ | -------------- | --------- | ------ | ------- | | execute | Uniswap V3 VIP | USDC/WETH | 122363 | 0.00% | | Settler - external move then execute | Uniswap V3 | USDC/WETH | 135730 | 10.92% | | execute | RFQ | USDC/WETH | 106575 | -12.90% | | | | | | | | execute | Uniswap V3 VIP | DAI/WETH | 113034 | 0.00% | | Settler - external move then execute | Uniswap V3 | DAI/WETH | 129239 | 14.34% | | execute | RFQ | DAI/WETH | 87101 | -22.94% | | | | | | | | execute | Uniswap V3 VIP | USDT/WETH | 124559 | 0.00% | | Settler - external move then execute | Uniswap V3 | USDT/WETH | 144935 | 16.36% | | execute | RFQ | USDT/WETH | 98213 | -21.15% | | | | | | | | Settler - external move then execute | Uniswap V3 | WETH/USDC | 138894 | NaN% | | | | | | |
| AllowanceHolder sell token fees | DEX | Pair | Gas | % | | ------------------------------- | --- | --------- | ------ | ------ | | no fee | RFQ | USDC/WETH | 106575 | 0.00% | | proportional fee | RFQ | USDC/WETH | 146971 | 37.90% | | fixed fee | RFQ | USDC/WETH | 122824 | 15.25% | | | | | | | | no fee | RFQ | DAI/WETH | 87101 | 0.00% | | proportional fee | RFQ | DAI/WETH | 125510 | 44.10% | | fixed fee | RFQ | DAI/WETH | 99176 | 13.86% | | | | | | | | no fee | RFQ | USDT/WETH | 98213 | 0.00% | | proportional fee | RFQ | USDT/WETH | 142254 | 44.84% | | fixed fee | RFQ | USDT/WETH | 111400 | 13.43% | | | | | | | | | | | | |
[//]: # "END TABLES"
Settler vs X
Settler vs 0xV4
The Settler contracts must perform additional work over 0xV4, namely, invalidate
the state of the Permit2 signed message, this is essentially an additional
SSTORE that must always be performed. Permit2 also does an ecrecover and
(in the metatransaction case) a cold EXTCODESIZE. On the other side, Settler
does not need to perform the same Feature implementation lookup that 0xV4
requires as a proxy. Settler's implicit reentrancy guard uses transient
storage.
With the Curve VIP, 0xV4 has to use a LiquidityProviderSandbox as calling untrusted/arbitrary code is a risk in the protocol. Settler can be more lax with the calls that it makes to other contracts because it does not hold TVL or allowances. Settler does not have an equivalent of the liquidity sandbox, making calls directly to Curve-like contracts.
Settler vs Curve
The Curve pool does not allow for a recipient to be specified, nor does it
allow for tokens to be transfer'd directly into the pool prior to calling the
pool contract. Due to these limitations there is overhead from the transfer
out of the Settler contract to the user. This same limitation applies to the
Curve Swap Router.
Actions
See ISettlerActions for a list of actions and their parameters. The list of actions, their names, the type and number of arguments, and the availability by chain is _**NOT STABLE**_. Do not rely on ABI encoding/decoding of actions directly.
UniswapV3
This settlement path is optimized by performing the Permit2 in the
uniswapV3SwapCallback function performing a permit2TransferFrom and avoiding
an additional transfer. This is further benefitted from tokens being sent to a
pool with an already initialized balance, rathan than to Settler as a temporary
intermediary.
The action UNISWAPV3_VIP exposes this behaviour and it should not be used with
any other action that interacts directly with Permit2 (e.g
TRANSFER_FROM). This is a recommendation; under extraordinary circumstances it
is only possible to achieve the required behavior with multiple Permit2
interactions. Except in the case of metatransaction Settlers, it is possible to
do multiple Permit2 interactions in the same Settler call.
Risk
Since Settler has no outstanding allowances, and no usage of
IERC20.transferFrom, overall risk of user funds loss is greatly reduced.
Permit2 allowances (with short dated expiration) still has some risk. Namely,
Alice permit2 being intercepted and a malicious transaction from Mallory,
which spends Alice's funds, transferring it to Mallory.
To protect funds we must validate the actions being performed originate from the
Permit2 signer. This is simple in the case where msg.sender is the signer of
the Permit2 message. To support metatransactions we utilise the witness
functionality of Permit2 to ensure the actions are intentional from Alice as
msg.sender is a different address.
Gas Comparisons
Day by day it gets harder to get a fair real world gas comparison. With rebates and token balances initialized or not, and the difficulty of setting up the world, touching storage, then performing the test.
To make gas comparisons fair we will use the following methodology:
- Market Makers have balances of both tokens. Since AMM Pools have non-zero
- The Taker does not have a balance of the token being bought, except in the
- Fee Recipient has a non-zero balance of the fee tokens.
- Nonces for Permit2 and Rfq orders (0x V4) are initialized.
setUpis used as much as possible with limited setup performed in the
Technical Reference
Permit2 Based Flows
We utilise Permit2 transfers with an SignatureTransfer. Allowing users to
sign a coupon allowing our contracts to move their tokens. Permit2 uses
PermitTransferFrom struct for transfers.
Permit2 provides the following guarantees:
- Funds can only be transferred from the user who signed the Permit2 coupon
- Funds can only be transferred by the
spenderspecified in the Permit2 coupon - Settler may only transfer an amount up to the amount specified in the Permit2 coupon
- Settler may only transfer a token specified in the Permit2 coupon
- Coupons expire after a certain time specified as
deadline - Coupons can only be used once
struct TokenPermissions {
// ERC20 token address
address token;
// the maximum amount that can be spent
uint256 amount;
}
struct PermitTransferFrom {
TokenPermissions permitted;
// a unique value for every token owner's signature to prevent signature replays
uint256 nonce;
// deadline on the permit signature
uint256 deadline;
}
With this it is simple to transfer the user assets to a specific destination, as well as take fixed fees. The biggest restriction is that we must consume this permit entirely once. We cannot perform the permit transfer at different times consuming different amounts.
The user signs a Permit2 coupon, giving Settler the ability to spend a specific amount of their funds for a time duration. The EIP712 type the user signs is as follows:
PermitTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline)
TokenPermissions(address token,uint256 amount)
This signed coupon is then provided in the calldata to the Settler.execute
function.
Due to this design, the user is prompted for an action two times when performing
a trade. Once to sign the Permit2 coupon, and once to call the Settler.execute
function. This is a tradeoff we are willing to make to avoid passive allowances.
In a metatransaction flow, the user is prompted only once.
Token Transfer Flow
sequenceDiagram
autonumber
User->>Settler: execute
rect rgba(133, 81, 231, 0.5)
Settler->>Permit2: permitTransfer
Permit2->>USDC: transferFrom(User, Settler)
USDC-->>Settler: transfer
end
Settler->>UniswapV3: swap
UniswapV3->>WETH: transfer(Settler)
WETH-->>Settler: transfer
UniswapV3->>Settler: uniswapV3Callback
Settler->>USDC: transfer(UniswapV3)
USDC-->>UniswapV3: transfer
Settler->>WETH: balanceOf(Settler)
Settler->>WETH: transfer(User)
WETH-->>User: transfer
The above example shows the simplest form of settlement in Settler. We abuse some of the sequence diagram notation to get the point across. Token transfers are represented by dashes (-->). Normal contract calls are represented by solid lines. Highlighted in purple is the Permit2 interaction.
For the sake of brevity, following diagrams will have a simplified representation to showcase the internal flow. This is what we are actually interested in describing. The initial user interaction (e.g their call to Settler) and the final transfer is omitted unless it is relevant to highlight in the flow. Function calls to the DEX may only be representative of the flow, not the accurate function name.
Below is the simplified version of the above flow.
sequenceDiagram
autonumber
rect rgba(133, 81, 231, 0.5)
USDC-->>Settler: permitTransfer
end
Settler->>UniswapV3: swap
WETH-->>Settler: transfer
UniswapV3->>Settler: uniswapV3Callback
USDC-->>UniswapV3: transfer
BASIC Flow
This is the most basic flow and a flow that a number of dexes
support. Essentially it is the "call function on DEX, DEX takes tokens from us,
DEX gives us tokens". It has inefficiencies as transferFrom is more gas
expensive than transfer and we are required to check/set allowances to the
DEX. Typically this DEX also does not support a recipient field, introducing
yet another needless transfer in simple swaps.
sequenceDiagram
autonumber
rect rgba(133, 81, 231, 0.5)
USDC-->>Settler: permitTransfer
end
Settler->>DEX: swap
USDC-->>DEX: transfer
WETH-->>Settler: transfer
WETH-->>User: transfer
VIPs
Settler has a number of specialised fill flows and will add more overtime as we add support for more dexes.
UniswapV3
sequenceDiagram
autonumber
Settler->>UniswapV3: swap
WETH-->>User: transfer
UniswapV3->>Settler: uniswapV3Callback
rect rgba(133, 81, 231, 0.5)
USDC-->>UniswapV3: permitTransfer
end
In this flow we avoid extraneous transfers with two optimisations. Firstly, we
utilise the recipient field of UniswapV3, providing the User as the recipient
and avoiding an extra transfer. Secondly during the uniswapV3Callback we
execute the Permit2 transfer, paying the UniswapV3 pool instead of the Settler
contract, avoiding an extra transfer.
This allows us to achieve no custody during this flow and is an extremely gas efficient way to fill a single UniswapV3 pool, or single chain of UniswapV3 pools.
Note this has the following limitations:
- Single UniswapV3 pool or single chain of pools (e.g ETH->DAI->USDC)
- Cannot support a split between pools (e.g ETH->USDC 5bps and ETH->USDC 1bps)
sellTokenForTokenToUniswapV3 as opposed to
MultiPlex[sellTokenForEthToUniswapV3,sellTokenForEthToUniswapV3].
RFQ
sequenceDiagram
autonumber
rect rgba(133, 81, 231, 0.5)
WETH-->>User: permitWitnessTransferFrom
end
rect rgba(133, 81, 231, 0.5)
USDC-->>Market Maker: permitTransfer
end
For RFQ we utilize 2 Permit2 transfers, one for the Market Maker->User and
another for User->Market Maker. This allows us to achieve no custody
during this flow and is an extremely gas efficient way to fill RFQ orders. We
simply validate the RFQ order (e.g Taker/msg.sender).
Note the permitWitnessTransferFrom, we utilise the witness functionality of
Permit2 which allows arbitrary data to be attached to the Permit2 coupon. This
arbitrary data is the actual RFQ order itself, containing the taker/msg.sender
and maker/taker amount and token fields.
A Market maker signs a slightly different Permit2 coupon than a User which contains additional fields. The EIP712 type the Market Maker signs is as follows:
PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,Consideration consideration)
Consideration(address token,uint256 amount,address counterparty,bool partialFillAllowed)
TokenPermissions(address token,uint256 amount)"
With values as follows
{
permitted: {
token: makerToken,
amount: makerAmount
},
spender: settlerAddress,
nonce: unOrderedNonce,
deadline: deadlineUnixTimestamp,
consideration: {
token: takerToken,
amount: takerAmount,
counterParty: taker,
partialFillAllowed: partialFillAllowed
}
}
We use the Permit2 guarantees of a Permit2 coupon to ensure the following:
- RFQ Order cannot be filled more than once
- RFQ Orders expire
- RFQ Orders are signed by the Market Maker
Fees in Basic Flow
In the most Basic flow, Settler has taken custody, usually in both assets. So a fee can be paid out be Settler.
Sell token fee
sequenceDiagram
autonumber
rect rgba(133, 81, 231, 0.5)
USDC-->>Settler: permitTransfer
end
opt sell token fee
Settler-->>Fee Recipient: transfer
end
Settler->>DEX: swap
USDC-->>DEX: transfer
WETH-->>Settler: transfer
WETH-->>User: transfer
While it is possible to utilise Permit2 to pay out the Sell token fee using a batch permit, we do not use that feature in Settler due to the substantial gas overhead in the single-transfer case.
potential batch flow CURRENTLY UNUSED
sequenceDiagram
autonumber
rect rgba(133, 81, 231, 0.5)
USDC-->>Settler: permitTransfer
opt sell token fee
USDC-->>Fee Recipient: transfer
end
end
Settler->>DEX: swap
USDC-->>DEX: transfer
WETH-->>Settler: transfer
WETH-->>User: transfer
Buy token fee
sequenceDiagram
autonumber
rect rgba(133, 81, 231, 0.5)
USDC-->>Settler: permitTransfer
end
Settler->>DEX: swap
USDC-->>DEX: transfer
WETH-->>Settler: transfer
opt buy token fee
WETH-->>Fee Recipient: transfer
end
WETH-->>User: transfer
Fees via Permit2
It is possible to collect fees via Permit2, which is typically in the token that the Permit2 is offloading (e.g the sell token for that counterparty). To perform this we use the Permit2 batch functionality where the second item in the batch is the fee.
RFQ fees via Permit2
sequenceDiagram
autonumber
rect rgba(133, 81, 231, 0.5)
Settler->>Permit2: permitWitnessTransfer
WETH-->>User: transfer
opt buy token fee
WETH-->>Fee Recipient: transfer
end
end
rect rgba(133, 81, 231, 0.5)
Settler->>Permit2: permitTransfer
USDC-->>Market Maker: transfer
opt sell token fee
USDC-->>Fee Recipient: transfer
end
end
Using the Batch functionality we can do one or more transfers from either the User or the Market Maker. Allowing us to take either a buy token fee or a sell token fee, or both, during RFQ order settlement.
This allows us to achieve no custody during this flow and is an extremely gas efficient way to fill RFQ orders with fees.
Uniswap VIP sell token fees via Permit2
sequenceDiagram
autonumber
Settler->>UniswapV3: swap
WETH-->>User: transfer
UniswapV3->>Settler: uniswapV3Callback
rect rgba(133, 81, 231, 0.5)
USDC-->>UniswapV3: permitTransfer
opt sell token fee
USDC-->>Fee Recipient: transfer
end
end
It is possible to collect sell token fees via Permit2 with the UniswapV3 VIP as well, using the Permit2 batch functionality. This flow is similar to the RFQ fees.
This allows us to achieve no custody during this flow and is an extremely gas efficient way to fill UniswapV3 with sell token fees.
Uniswap buy token fees via Permit2
sequenceDiagram
autonumber
Settler->>UniswapV3: swap
WETH-->>Settler: transfer
UniswapV3->>Settler: uniswapV3Callback
rect rgba(133, 81, 231, 0.5)
USDC-->>UniswapV3: permitTransfer
end
opt buy token fee
WETH-->>Fee Recipient: transfer
end
WETH-->>User: transfer
Since UniswapV3 only supports a single recipient, to collect buy token fees,
Settler must take custody of the buy token. These additional transfe
... (README truncated for length)