The token swaps need permits.
You'll approve the permit contract with an amount and that one approval can be utilised across all contracts that support the permit contract.
It needs one per token to grant allowance to the permit contract. Then it needs a separate approval to spend the permits allowance.
Beyond that you can call the exactIn/Out
function that you want.
Universal router is advanced for programmatic interaction, it services lots of pool types and gives users the ability to compose complex swaps but it makes even basic interaction feel less intuitive.
Have done examples for both from eth (no permit), as well as from token with permit, which is the one I will lift for this answer. Will leave both linked below.
Working example:
Removed in favour of a simplified but more specific example using values relevant to the op, found at the end of this post.
The more complete examples can still be reached here.
Permit
Permit is a little involved, there are two ways you can do it. In both cases it is necessary to first perform a classical approval for the permit2 contract to move your tokens, as it is that contracts allowance that will be spent.
This approval needs to be performed in a preceding transaction (unless as part of an atomic call from a custom contract for example).
Following that approval we have two options
- We can explicitly "approve" the router as a spender of the permit2 contracts allowance in another transaction/call.
The benefit of this method is that the process is intuitive and familiar, it can be performed via a block explorer without any special encoding or signing.
- We can sign permit data (A set of encoded values), and send the signature for that permit along with the swap.
The logic to building a swap
We can look at the commands
contract to get the list of steps we want to take, then encode the calldata for each of those steps.
In this example we wrap the native coin and then perform a swap on v3.
commands = '0x0b00'
0b : # uint256 constant WRAP_ETH = 0x0b;
00 : # uint256 constant V3_SWAP_EXACT_IN = 0x00;
We can look at the dispatcher
contract to see what is being decoded on those calls:
if (command == Commands.WRAP_ETH) {
// equivalent: abi.decode(inputs, (address, uint256))
address recipient;
uint256 amountMin;
For wrapping eth we can see it takes a destination address and amount.
The amount will be the msg.value
we send with the transaction, the address should be the router as we want to perform a swap after.
For the swap we need:
# address recipient,
# uint256 amountIn,
# uint256 amountOutMinimum,
# bytes calldata path,
# bool payer
A path
unlike the other values needs "packed encoding", this is the values concatenated without the zero padding.
# (address tokenIn, uint24 fee, address tokenOut)
path = encode_packed(['address','uint24','address'], [WMATIC_ADDRESS, FEE, USDC_ADDRESS])
Can think about payer
as if from eoa (your wallet) or the router:
False == the router
True == msg.sender
The encoding of the resulting calldata looks like this:
wrap_calldata = encode(['address', 'uint256'], [router.address, amount])
v3_calldata = encode(['address', 'uint256', 'uint256', 'bytes', 'bool'], [to, amount, slippage, path, from_eoa])
Finally, we can either encode (not packed) the args together, concatonate with the function sig and use as the calldata in a
raw transaction, or simply pass to the execute
function of a contract instance:
swap = router.functions.execute(commands, [wrap_calldata, v3_calldata], deadline).build_transaction(tx)
Troubleshooting
edit
Issues that came up:
Using too low a gas amount
Using an incorrect fee parameter
- 100,500,3000,10000 are the possible tiers, representing 0.01%,0.05%,0.3%,1% -
- This will effect the pool you trade on and if the path is traversable or not. So worth double checking.
Can use a factory contract to check the pools and pick the one with enough liquidity and the lowest fee.
Double check the addresses used to replace the one's in the example.
I am simulating with this calldata which appears to be working.
'0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000004a817c8000000000000000000000000000000000000000000000000000000000000000002000c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001000000000000000000000000001a0a18ac4becddbd6389559687d1a73d8927e41600000000000000000000000000000000000000000000003635c9adc5dea00000000000000000000000000000000000000000000000000000000000000098968000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002b6894cde390a3f51155ea41ed24a33a4827d3063d000064bb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000093b80018010b759c3cda255f3790df4aff1b0060000000000000000000000000000000000000000000000000000000000989680'
For completeness and confidence:
0x3593564c // call signature execute(bytes,bytes[],uint256)
0000000000000000000000000000000000000000000000000000000000000060 //offset to command bytes
00000000000000000000000000000000000000000000000000000000000000a0 //offset to swap data
00000000000000000000000000000000000000000000000000000004a817c800 //deadline
0000000000000000000000000000000000000000000000000000000000000002 //length
000c000000000000000000000000000000000000000000000000000000000000 //command bytes
0000000000000000000000000000000000000000000000000000000000000002 //elements in array
0000000000000000000000000000000000000000000000000000000000000040 //offset to swap
0000000000000000000000000000000000000000000000000000000000000160 //tuple bytes
0000000000000000000000000000000000000000000000000000000000000100 //length of swap bytes
0000000000000000000000001a0a18ac4becddbd6389559687d1a73d8927e416 //router destination after swap
00000000000000000000000000000000000000000000003635c9adc5dea00000 //amount in
0000000000000000000000000000000000000000000000000000000000989680 //amount out min for swap
00000000000000000000000000000000000000000000000000000000000000a0 //offset to unwrap
0000000000000000000000000000000000000000000000000000000000000001 //source bool
000000000000000000000000000000000000000000000000000000000000002b //path length
6894cde390a3f51155ea41ed24a33a4827d3063d000064bb4cdb9cbd36b01bd1 //path
cbaebf2de08d9173bc095c000000000000000000000000000000000000000000 //path
0000000000000000000000000000000000000000000000000000000000000040 //length of unwrap bytes
000000000000000000000000093b80018010b759c3cda255f3790df4aff1b006 //your address as destination after unwrap
0000000000000000000000000000000000000000000000000000000000989680 //amount out min for weth
Most are offset to start of array data or length of data in array, because the call contains a byte array followed by an array of byte arrays.
For example 0x60 means jump 96 bytes to start of array data, 0x02 means the data is 2 bytes long, which are the command bytes (starting from the left) 0x000c.. etc
You can use both the eth.call(unsigned_tx)
method and tenderly.co (screenshot), without needing to sign anything or risk any money on the test.
Specific example:
from web3 import Web3; w3 = Web3(Web3.HTTPProvider('https://1rpc.io/bnb'))
from os import getenv
from dotenv import load_dotenv
load_dotenv()
eoa = w3.eth.account.from_key(getenv('KEY_FROM_ENV'))
#----------------------
PERMIT_ADDRESS = '0x31c2F6fcFf4F8759b3Bd5Bf0e1084A055615c768'
SIMON_ADDRESS = '0x6894CDe390a3f51155ea41Ed24a33A4827d3063D'
WETH_ADDRESS = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c'
ROUTER_ADDRESS = '0x1A0A18AC4BECDDbd6389559687d1A73d8927E416'
# grab from here:
# - https://github.com/0xMaka/w3py/blob/main/universal_swap_from_token.py
# or double check yours/grab from explorer.
ROUTER_ABI = ''
PERMIT_ABI = ''
ERC20_ABI = ''
router = w3.eth.contract(address=ROUTER_ADDRESS, abi=ROUTER_ABI)
permit = w3.eth.contract(address=PERMIT_ADDRESS, abi=PERMIT_ABI)
token = w3.eth.contract(address=SIMON_ADDRESS, abi=ERC20_ABI )
tx = {
'from': eoa.address,
'value': 0,
'chainId': w3.eth.chain_id,
'gas': 250000,
'gasPrice': w3.eth.gas_price,
'nonce': w3.eth.get_transaction_count(eoa.address)
}
#---------------------------------------------------------------
commands = '0x00'
from eth_abi import encode
from eth_abi.packed import encode_packed
to = eoa.address
amount = 1000 * 10 ** 18
slippage = 10000000
FEE = 100
path = encode_packed(['address','uint24','address'], [SIMON_ADDRESS, FEE, WETH_ADDRESS])
from_eoa = True
v3_calldata = encode(['address', 'uint256', 'uint256', 'bytes', 'bool'], [to, amount, slippage, path, from_eoa])
deadline = 2*10**10
# --------------------------------------------------------------------------------------------------------------------------------------------------------------------
def sign_tx(tx, key):
return w3.eth.account.sign_transaction(tx, private_key=key)
def send_tx(signed_tx):
return w3.eth.send_raw_transaction(signed_tx.rawTransaction)
def main():
# approvals removed
swap = router.functions.execute(commands, [v3_calldata], deadline).build_transaction(tx)
print(swap)
#print('[-] Simulating swap...')
#w3.eth.call(swap)
print('[-] Attempting swap...')
tx_hash = send_tx(sign_tx(swap, eoa.key))
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
print (f'[>] Hash of swap: {tx_hash}\n[>] {receipt}')
if __name__ == '__main__':
main()