1

Question:

I've been able to successfully write scripts for swapping tokens on Uniswap V2, but I'm facing challenges when trying to swap tokens on Uniswap/PancakeSwap V3. Specifically, I am trying to understand the Universal Router and the Permit2 contract. The documentation suggests that I need to encode the execute function and also approve the contract using Permit2.

Here are my questions:

Do I need to use Permit2 for every swap (token/ETH)?
Which function is the best to use for swapping tokens?
The options available are:
    V3_SWAP_EXACT_IN
    V3_SWAP_EXACT_OUT
    PERMIT2_TRANSFER_FROM
    PERMIT2_PERMIT_BATCH
    SWEEP
    TRANSFER
    PAY_PORTION
    V2_SWAP_EXACT_IN

I'm particularly interested in understanding the V3_SWAP_EXACT_IN and V2_SWAP_EXACT_IN functions. I found the following information on V3_SWAP_EXACT_IN:

address: The recipient of the output of the trade.
uint256: The amount of input tokens for the trade.
uint256: The minimum amount of output tokens the user wants.
bytes: The UniswapV3 encoded path to trade along.
bool: A flag indicating whether the input tokens should come from the msg.sender (via Permit2) or whether the funds are already in the Universal Router.

However, when I try to implement this in my script, I can't seem to successfully swap a token for ETH. Below is the script I'm working with:

from web3 import Web3
from uniswap_universal_router_decoder import RouterCodec, FunctionRecipient
import time

# Configuration
private_key = ""
rpc_endpoint = "https://bsc-dataseed.binance.org/"
universal_router_address = Web3.to_checksum_address('0x1A0A18AC4BECDDbd6389559687d1A73d8927E416')  # Replace with the actual address

# Initialize Web3
w3 = Web3(Web3.HTTPProvider(rpc_endpoint))
account = w3.eth.account.from_key(private_key)

# Token and Router Addresses
amount_in = 10 * 10**18  # 10 tokens (adjust as needed)
min_amount_out = 10000 * 10**18  # Minimum amount out (adjust as needed)
deadline = int(time.time()) + 600  # 10 minutes from now
path = [
    Web3.to_checksum_address('0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c'),  # WBNB
    Web3.to_checksum_address('0xbb6878497F7733fbeD72156f60c85cdEE868dBC9')  # Token address
]

# Initialize RouterCodec
codec = RouterCodec()

# Encode the transaction data
encoded_data = codec.encode.chain().v2_swap_exact_in(
    FunctionRecipient.SENDER,
    amount_in,
    min_amount_out,
    path
).build(deadline)

# Prepare the transaction
transaction = {
    'to': universal_router_address,
    'value': 0,
    'gas': 2000000,  # Adjust as needed
    'gasPrice': w3.to_wei('5', 'gwei'),  # Adjust as needed
    'nonce': w3.eth.get_transaction_count(account.address),
    'data': encoded_data
}

# Sign the transaction
signed_transaction = w3.eth.account.sign_transaction(transaction, private_key)

# Send the transaction
tx_hash = w3.eth.send_raw_transaction(signed_transaction.rawTransaction)

print(f'Transaction hash: {tx_hash.hex()}')

# Wait for the transaction receipt
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
print(f'Transaction receipt: {receipt}')

I laso followed this tutorial

https://hackernoon.com/how-to-buy-a-token-on-the-uniswap-universal-router-with-python

1 Answer 1

0

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'

enter image description here


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()
1
  • Comments have been moved to chat; please do not continue the discussion here. Before posting a comment below this one, please review the purposes of comments. Comments that do not request clarification or suggest improvements usually belong as an answer, on Ethereum Meta, or in Ethereum Chat. Comments continuing discussion may be removed.
    – eth
    Commented Sep 12 at 15:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.