0

Hello Stack Overflow Community,

I am working on swapping tokens in a base layer2 network using Uniswap V3 and ethers.js. I have successfully approved the transaction using the approve function, but when I try to execute the swap using exactInputSingle, I encounter a "Warning! Error encountered during contract execution [execution reverted]" error.

Here's a simplified version of my code:

import { getPoolImmutables, getPoolState } from './utils';
const { ethers } = require('ethers');
const { abi: IUniswapV3PoolABI } = require('@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json');
const SwapRouterABI = require('./routerabi.json');
const ERC20ABI = require('./abi.json');

const QUICKNODE_HTTP_ENDPOINT = 'https://sepolia.base.org';
const WALLET_ADDRESS = '';
const WALLET_SECRET = '';

const provider = new ethers.providers.JsonRpcProvider(QUICKNODE_HTTP_ENDPOINT);
const poolAddress = '0x7516D071921973fDFf29C071f42d21C227CF5740'; 
const swapRouterAddress = '0x050E797f3625EC8785265e1d9BDd4799b97528A1';

async function main() {
  const poolContract = new ethers.Contract(poolAddress, IUniswapV3PoolABI, provider);
  const immutables = await getPoolImmutables(poolContract);
  const state = await getPoolState(poolContract);

  const wallet = new ethers.Wallet(WALLET_SECRET);
  const connectedWallet = wallet.connect(provider);

  const swapRouterContract = new ethers.Contract(swapRouterAddress, SwapRouterABI, provider);
  const inputAmount = 0.09;
  const amountIn = ethers.utils.parseUnits(inputAmount.toString(), 18);
  const approvalAmount = (amountIn * 10).toString();
  const tokenContract0 = new ethers.Contract(immutables.token1, ERC20ABI, provider);

  const approvalResponse = await tokenContract0
    .connect(connectedWallet)
    .approve(swapRouterAddress, approvalAmount);

  const params = {
    tokenIn: immutables.token1,
    tokenOut: immutables.token0,
    fee: immutables.fee,
    recipient: WALLET_ADDRESS,
    deadline: Math.floor(Date.now() / 1000) + 60 * 10,
    amountIn: '0x013fbe85edc90000',
    amountOutMinimum: 0,
    sqrtPriceLimitX96: 0,
    nounce: await connectedWallet.getTransactionCount(),
  };

  const transaction = swapRouterContract
    .connect(connectedWallet)
    .exactInputSingle(params, {
      gasLimit: ethers.utils.hexlify(1000000),
      gasPrice: ethers.utils.parseUnits('10', 'gwei'),
    })
    .then((transaction: any) => {
      console.log(transaction);
    });
}

main();

I suspect that I might need to use the execute function, but I'm not sure how to do that or if that's the correct approach.

Any guidance on how to properly execute a swap in a Uniswap V3 pool on a base layer2 network using ethers.js would be greatly appreciated.

Thank you!

6

2 Answers 2

0
+50

I have never used the sdk, but here is a direct example of using Uniswap Universal Router, encoding the calldata manually:

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)

Complete example:

from net import con; w3 = con('POLYGON') # i.e from web3 import Web3; w3 = Web3(Provider(Endpoint))
from os import getenv
from dotenv import load_dotenv
load_dotenv()
KEY = getenv('your_key')
EOA = getenv('your_address')
#----------------------

#---
USDC_ADDRESS = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174'
WMATIC_ADDRESS = '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270'
WETH_ADDRESS= '0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619'
WMATIC_USDC_ADDRESS = '0xcd353F79d9FADe311fC3119B841e1f456b54e858'
USDC_WETH_ADDRESS = '0x34965ba0ac2451A34a0471F04CCa3F990b8dea27'
ROUTER_ADDRESS = '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD'

ROUTER_ABI = '''
[{"inputs":[{"components":[{"internalType":"address","name":"permit2","type":"address"},{"internalType":"address","name":"weth9","type":"address"},{"internalType":"address","name":"seaportV1_5","type":"address"},{"internalType":"address","name":"seaportV1_4","type":"address"},{"internalType":"address","name":"openseaConduit","type":"address"},{"internalType":"address","name":"nftxZap","type":"address"},{"internalType":"address","name":"x2y2","type":"address"},{"internalType":"address","name":"foundation","type":"address"},{"internalType":"address","name":"sudoswap","type":"address"},{"internalType":"address","name":"elementMarket","type":"address"},{"internalType":"address","name":"nft20Zap","type":"address"},{"internalType":"address","name":"cryptopunks","type":"address"},{"internalType":"address","name":"looksRareV2","type":"address"},{"internalType":"address","name":"routerRewardsDistributor","type":"address"},{"internalType":"address","name":"looksRareRewardsDistributor","type":"address"},{"internalType":"address","name":"looksRareToken","type":"address"},{"internalType":"address","name":"v2Factory","type":"address"},{"internalType":"address","name":"v3Factory","type":"address"},{"internalType":"bytes32","name":"pairInitCodeHash","type":"bytes32"},{"internalType":"bytes32","name":"poolInitCodeHash","type":"bytes32"}],"internalType":"struct RouterParameters","name":"params","type":"tuple"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"BalanceTooLow","type":"error"},{"inputs":[],"name":"BuyPunkFailed","type":"error"},{"inputs":[],"name":"ContractLocked","type":"error"},{"inputs":[],"name":"ETHNotAccepted","type":"error"},{"inputs":[{"internalType":"uint256","name":"commandIndex","type":"uint256"},{"internalType":"bytes","name":"message","type":"bytes"}],"name":"ExecutionFailed","type":"error"},{"inputs":[],"name":"FromAddressIsNotOwner","type":"error"},{"inputs":[],"name":"InsufficientETH","type":"error"},{"inputs":[],"name":"InsufficientToken","type":"error"},{"inputs":[],"name":"InvalidBips","type":"error"},{"inputs":[{"internalType":"uint256","name":"commandType","type":"uint256"}],"name":"InvalidCommandType","type":"error"},{"inputs":[],"name":"InvalidOwnerERC1155","type":"error"},{"inputs":[],"name":"InvalidOwnerERC721","type":"error"},{"inputs":[],"name":"InvalidPath","type":"error"},{"inputs":[],"name":"InvalidReserves","type":"error"},{"inputs":[],"name":"InvalidSpender","type":"error"},{"inputs":[],"name":"LengthMismatch","type":"error"},{"inputs":[],"name":"SliceOutOfBounds","type":"error"},{"inputs":[],"name":"TransactionDeadlinePassed","type":"error"},{"inputs":[],"name":"UnableToClaim","type":"error"},{"inputs":[],"name":"UnsafeCast","type":"error"},{"inputs":[],"name":"V2InvalidPath","type":"error"},{"inputs":[],"name":"V2TooLittleReceived","type":"error"},{"inputs":[],"name":"V2TooMuchRequested","type":"error"},{"inputs":[],"name":"V3InvalidAmountOut","type":"error"},{"inputs":[],"name":"V3InvalidCaller","type":"error"},{"inputs":[],"name":"V3InvalidSwap","type":"error"},{"inputs":[],"name":"V3TooLittleReceived","type":"error"},{"inputs":[],"name":"V3TooMuchRequested","type":"error"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"RewardsSent","type":"event"},{"inputs":[{"internalType":"bytes","name":"looksRareClaim","type":"bytes"}],"name":"collectRewards","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"commands","type":"bytes"},{"internalType":"bytes[]","name":"inputs","type":"bytes[]"}],"name":"execute","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes","name":"commands","type":"bytes"},{"internalType":"bytes[]","name":"inputs","type":"bytes[]"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"execute","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256[]","name":"","type":"uint256[]"},{"internalType":"uint256[]","name":"","type":"uint256[]"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC1155BatchReceived","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC1155Received","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC721Received","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"int256","name":"amount0Delta","type":"int256"},{"internalType":"int256","name":"amount1Delta","type":"int256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"uniswapV3SwapCallback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]
'''

router = w3.eth.contract(address=ROUTER_ADDRESS, abi=ROUTER_ABI)

#  https://github.com/Uniswap/universal-router/blob/main/contracts/libraries/Commands.sol
#
#    // Command Types where value<0x08, executed in the first nested-if block
#    uint256 constant V3_SWAP_EXACT_IN = 0x00;
#    uint256 constant V3_SWAP_EXACT_OUT = 0x01;
#    uint256 constant PERMIT2_TRANSFER_FROM = 0x02;
#    uint256 constant PERMIT2_PERMIT_BATCH = 0x03;
#    uint256 constant SWEEP = 0x04;
#    uint256 constant TRANSFER = 0x05;
#    uint256 constant PAY_PORTION = 0x06;
#    // COMMAND_PLACEHOLDER = 0x07;
#
#....
#    // Command Types where 0x08<=value<=0x0f, executed in the second nested-if block
#    uint256 constant V2_SWAP_EXACT_IN = 0x08;
#    uint256 constant V2_SWAP_EXACT_OUT = 0x09;
#    uint256 constant PERMIT2_PERMIT = 0x0a;
#    uint256 constant WRAP_ETH = 0x0b;
#    uint256 constant UNWRAP_WETH = 0x0c;
#
# ----

# we will wrap the native coin, then swap on v3

commands = '0x0b00'

# https://github.com/Uniswap/universal-router/blob/main/contracts/base/Dispatcher.sol
#
#                    if (command == Commands.V3_SWAP_EXACT_IN) {
#                        // equivalent: abi.decode(inputs, (address, uint256, uint256, bytes, bool))
#                        address recipient;
#                        uint256 amountIn;
#                        uint256 amountOutMin;
#                        bool payerIsUser
#....
#
#
# https://github.com/Uniswap/universal-router/blob/228f2d151a5fc99836d72ae00f81db92cdb44bd3/contracts/modules/uniswap/v3/V3SwapRouter.sol
#
#    /// @notice Performs a Uniswap v3 exact input swap
#    /// @param recipient The recipient of the output tokens
#    /// @param amountIn The amount of input tokens for the trade
#    /// @param amountOutMinimum The minimum desired amount of output tokens
#    /// @param path The path of the trade as a bytes string
#    /// @param payer The address that will be paying the input
#    function v3SwapExactInput(
#        address recipient,
#        uint256 amountIn,
#        uint256 amountOutMinimum,
#        bytes calldata path,
#        address payer

from eth_abi import encode
from eth_abi.packed import encode_packed
# some sane inputs (sane doesn't mean safe in this case, always use a good slippage value unless protected some other way)
to = EOA
amount = 1 * 10 ** 17
slippage = 0
FEE = 500
# (address tokenIn, uint24 fee, address tokenOut)
path = encode_packed(['address','uint24','address'], [WMATIC_ADDRESS, FEE, USDC_ADDRESS])
from_eoa = False # the router or user? router after wrapping

wrap_calldata = encode(['address', 'uint256'], [router.address, amount])
v3_calldata = encode(['address', 'uint256', 'uint256', 'bytes', 'bool'], [to, amount, slippage, path, from_eoa])

deadline = 2*10**10

print(commands)
print(wrap_calldata.hex())
print(v3_calldata.hex())
print(deadline)
# --------------------------------------------------------------------------------------------------------------------------------------------------------------------

tx = {
        'from': EOA,
        'value': amount,
        'chainId': 137,
        'gas': 250000,
        'maxFeePerGas': w3.eth.gas_price * 2,
        'maxPriorityFeePerGas': w3.eth.max_priority_fee * 2,
        'nonce': w3.eth.get_transaction_count(EOA)
}

def sign_tx(tx, key):
  sig = w3.eth.account.sign_transaction
  signed_tx = sig(tx, private_key=key)
  return signed_tx

def send_tx(signed_tx):
  w3.eth.send_raw_transaction(signed_tx.rawTransaction)
  tx_hash = w3.to_hex(w3.keccak(signed_tx.rawTransaction))
  return tx_hash

def main():
  swap = router.functions.execute(commands, [wrap_calldata, 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, KEY))
  receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
  print (f'[>] Hash of swap: {tx_hash}\n[>] {receipt}')

if __name__ == '__main__':
  main()

Uni's universal router is similar to sushi's route processor, if not a little simpler, so can be worth looking at the walk through of one of those swaps:

It covers the same considerations in trying to understand the universal router.

Please clarify if and what you are still be having an issue with.

2
0

if you are on Base mainnet im assuming you are using swapRouter02 with is just inherits from a lot of different routers.

The problem is that swapRouter02 struct params is different from the swapRouter.

for example, this is the swapRouter02 struct params

struct ExactInputSingleParams { address tokenIn; address tokenOut; uint24 fee; address recipient; uint256 amountIn; uint256 amountOutMinimum; uint160 sqrtPriceLimitX96; } it doest have a a deadline key value argument

this is the swapRouter input struct params struct ExactInputSingleParams { address tokenIn; address tokenOut; uint24 fee; uint256 deadline; address recipient; uint256 amountIn; uint256 amountOutMinimum; uint160 sqrtPriceLimitX96; }

check the repo for the swapRouter02 here: https://github.com/Uniswap/swap-router-contracts/blob/main/contracts/interfaces/IV3SwapRouter.sol

hope it helps

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