Profile
Back to NewsBack
GitHub Trending 31 min
Reader Mode
0xProject/0x-settler: 0x settlement contracts using Permit2

0xProject/0x-settler: 0x settlement contracts using Permit2

12 hours ago

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.

  • 0x0000000000001fF3684f28c67538d4D072C22734 on chains supporting the Cancun
hardfork (Ethereum mainnet, Ethereum Sepolia testnet, Polygon, Base, Optimism, Arbitrum, Bnb, World Chain, Fantom Sonic, Ink, Avalanche, Unichain, Berachain, Scroll, HyperEvm, Plasma, Monad mainnet, Abstract, Linea, Tempo, RobinHood, Arc)
  • 0x0000000000005E88410CcDFaDe4a5EfaE4b49562 on chains supporting the Shanghai
hardfork (Mantle)

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)
For the above reasons, there are settlement paths in Settler which allow for custody of the sell token or the buy token. You will see the usage of 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
balances of both tokens this is a fair comparison.
  • The Taker does not have a balance of the token being bought, except in the
USDC/USDT tests, where the Taker starts with 1,000 USDT.
  • Fee Recipient has a non-zero balance of the fee tokens.
  • Nonces for Permit2 and Rfq orders (0x V4) are initialized.
  • setUp is used as much as possible with limited setup performed in the
test. Warmup trades are avoided completely as to not warm up storage access.

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 spender specified 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)
as Permit2 transfer can only occur once. a 0xV4 equivalent would be 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

potential batch flow CURRENTLY UNUSED

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)

Chat with me