pythonを用いてSyncSwapでSwap(ETH⇔USDC)するコード

SyncSwapで使われているRouterを使ったSwapをpythonから実行することがやっとできました。
ただ、結果としてできた、という感じで理解についてはまだまだですので、コードを使われる際はGOXにご注意ください(使うとしても本当に少額で!少し設定を触ると簡単にGOXします!)

参考URLは下記です。


ETH→USDCの方向でSwapするコード@SyncSwap

0.001ETHをUSDCにスワップするコードです。
今回はLineaネットワークを使いました。
説明は後述します。

#linea_eth_usdc_syncswap.py

import json
from web3 import Web3
from datetime import datetime
from eth_abi import encode

lineaRpc = "https://linea-mainnet.infura.io/v3/e209d4f3c05e4356aa94c31f79d28689"
myWallet = 'my_wallet_address'
privateKey = 'my_private_key'
pool = '0x5Ec5b1E9b1Bd5198343ABB6E55Fb695d2F7Bb308'
WETH = '0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f'
USDC = '0x176211869cA2b568f2A7D4EE941E073a821EE1ff'

gas = 200000

web3 = Web3(Web3.HTTPProvider(lineaRpc))

amountIn = web3.to_wei(0.001, 'ether')

with open('syncswap_linea_wethusdcPool_ABI.json') as f:
   poolABI = json.load(f)
poolContract = web3.eth.contract(address=pool, abi=poolABI)

amountOut = poolContract.functions.getAmountOut(WETH, amountIn, myWallet).call()

print("amountOut[USDC] = ")
print(amountOut)

encoded_data = encode(["address", "address", "uint8"], [WETH, myWallet, 1])
zero_address = "0x0000000000000000000000000000000000000000"
swap_steps = [
    {
        'pool': pool,
        'data': encoded_data,
        'callback': zero_address,
        'callbackData': '0x',
        'useVault': True
    }
]

nativeETH = zero_address
paths = [
    {
        'steps': swap_steps,
        'tokenIn': nativeETH,
        'amountIn': amountIn
    }
]

router = '0xC2a1947d2336b2AF74d5813dC9cA6E0c3b3E8a1E'
with open('router_linea_ABI.json') as f:
   routerABI = json.load(f)
routerContract = web3.eth.contract(address=router, abi=routerABI)

now = int(datetime.now().timestamp())
deadline = now + 60*3

tx = routerContract.functions.swap(paths, 0, deadline).build_transaction(
    {
        "from": myWallet,
        "nonce": web3.eth.get_transaction_count(myWallet),
        "value": amountIn,
        "gasPrice": web3.eth.gas_price,
        "gas": gas
    }
)

signed_tx = web3.eth.account.sign_transaction(tx, privateKey)
txHash = web3.eth.send_raw_transaction(signed_tx.raw_transaction)

print('Send transaction')
print(web3.to_hex(txHash))
result = web3.eth.wait_for_transaction_receipt(txHash)
status = result['status']
if status == 1:
   print('Transaction Succeeded')
else:
   print('Transaction Failed')

また、実行したときの結果とトランザクションを載せておきます。

https://lineascan.build/tx/0xde83621b662363ed7991b444bc54a1fbbfb79e2be74de933cf62b5f111733620

説明(まずは必要情報を集める)

やはりまずはSyncSwapのWebページから一般的な方法でスワップしてみます。

ブロックエクスプローラで確認してみます。

https://lineascan.build/tx/0xba0d0872e9a2969443f8880256fa19220769689f52e9f6abf92f001a0a207cdb

まずはRouterのコントラクトアドレスが分かります。

また、Routerコントラクトのswap関数が実行されたことが分かります。

また、WETH-USDC Poolのコントラクトアドレスも必要なので探します。
が、恥ずかしながらトランザクションを追う過程では見つけられませんでした。なので、Docsから確認します。

Classic Pool Factoryのコントラクトアドレスが0x37BAc764494c8db4e54BDE72f6965beA9fa0AC2d
だと分かりましたので、これをブロックエクスプローラで検索します。

Contract→Read ContractにgetPool関数が見つかるので、ここにWETHとUSDCのコントラクトアドレスを入力してQueryします。
すると、WETH-USDC Poolのコントラクトアドレスが
0x5Ec5b1E9b1Bd5198343ABB6E55Fb695d2F7Bb308
だと分かります。

これで必要な情報はで出そろったのでトランザクションを組み立てていきます。

説明(トランザクションの組み立て)

ここがかなり難しかったです。

参考に書きました「Router|API Documentation」を見ると、インターフェースが下記のように定義されています。

// The Router contract has Multicall and SelfPermit enabled.

struct TokenInput {
    address token;
    uint amount;
}

struct SwapStep {
    address pool; // The pool of the step.
    bytes data; // The data to execute swap with the pool.
    address callback;
    bytes callbackData;
}

struct SwapPath {
    SwapStep[] steps; // Steps of the path.
    address tokenIn; // The input token of the path.
    uint amountIn; // The input token amount of the path.
}

struct SplitPermitParams {
    address token;
    uint approveAmount;
    uint deadline;
    uint8 v;
    bytes32 r;
    bytes32 s;
}

struct ArrayPermitParams {
    uint approveAmount;
    uint deadline;
    bytes signature;
}

// Returns the vault address.
function vault() external view returns (address);

// Returns the wETH address.
function wETH() external view returns (address);

// Adds some liquidity (supports unbalanced mint).
// Alternatively, use `addLiquidity2` with the same params to register the position,
// to make sure it can be indexed by the interface.
function addLiquidity(
    address pool,
    TokenInput[] calldata inputs,
    bytes calldata data,
    uint minLiquidity,
    address callback,
    bytes calldata callbackData
) external payable returns (uint liquidity)

// Adds some liquidity with permit (supports unbalanced mint).
// Alternatively, use `addLiquidityWithPermit` with the same params to register the position,
// to make sure it can be indexed by the interface.
function addLiquidityWithPermit(
    address pool,
    TokenInput[] calldata inputs,
    bytes calldata data,
    uint minLiquidity,
    address callback,
    bytes calldata callbackData,
    SplitPermitParams[] memory permits
) external payable returns (uint liquidity);

// Burns some liquidity (balanced).
function burnLiquidity(
    address pool,
    uint liquidity,
    bytes calldata data,
    uint[] calldata minAmounts,
    address callback,
    bytes calldata callbackData
) external returns (IPool.TokenAmount[] memory amounts);

// Burns some liquidity with permit (balanced).
function burnLiquidityWithPermit(
    address pool,
    uint liquidity,
    bytes calldata data,
    uint[] calldata minAmounts,
    address callback,
    bytes calldata callbackData,
    ArrayPermitParams memory permit
) external returns (IPool.TokenAmount[] memory amounts);

// Burns some liquidity (single).
function burnLiquiditySingle(
    address pool,
    uint liquidity,
    bytes memory data,
    uint minAmount,
    address callback,
    bytes memory callbackData
) external returns (uint amountOut);
    
// Burns some liquidity with permit (single).
function burnLiquiditySingleWithPermit(
    address pool,
    uint liquidity,
    bytes memory data,
    uint minAmount,
    address callback,
    bytes memory callbackData,
    ArrayPermitParams calldata permit
) external returns (uint amountOut);

// Performs a swap.
function swap(
    SwapPath[] memory paths,
    uint amountOutMin,
    uint deadline
) external payable returns (uint amountOut);

function swapWithPermit(
    SwapPath[] memory paths,
    uint amountOutMin,
    uint deadline,
    SplitPermitParams calldata permit
) external payable returns (uint amountOut);

/// @notice Wrapper function to allow pool deployment to be batched.
function createPool(address factory, bytes calldata data) external payable returns (address);

これを解読してRouterのswap関数を組み立てていきます。

Routerコントラクトのswap関数をブロックエクスプローラで確認します。

また、先ほど実行したトランザクションを見ると

Routerのswap関数は、
・tuple[] paths
・uint256 amountOutMin
・uint256 deadline
を引数として取ることが分かります。

amountOutMinとdeadlineは分かります(過去のnoteでも出てきました)。

今回の課題はtuple[] pathsで、これはRouterコントラクトのインターフェースでは下記に当たります(SwapPath[] memory pathsです)。

function swap(
    SwapPath[] memory paths,
    uint amountOutMin,
    uint deadline
) external payable returns (uint amountOut);

となると、今度はSwapPath[] memory pathsとは何か?となるのでこれをインターフェースから探します。

struct SwapPath {
    SwapStep[] steps; // Steps of the path.
    address tokenIn; // The input token of the path.
    uint amountIn; // The input token amount of the path.
}

この中で、tokenIn(入力するトークン)、amountIn(いくら入れるか)は分かります。SwapStep[] stepsが分かりません。なので、これもインターフェースから探します。

struct SwapStep {
    address pool; // The pool of the step.
    bytes data; // The data to execute swap with the pool.
    address callback;
    bytes callbackData;
}

poolは先ほど調べたWETH-USDC Poolのアドレスだと分かりますが、他が謎です。例として、ETHをDAIにSwapするJavaScriptコードがあるので、これを参考にすると、data, callback, callbackDataに何を入力すれば良いかが分かります。
dataは(入力するトークンのアドレス、実行する人のアドレス:スワップ後のトークンを誰に送るか、withdrawmode)をABIエンコードしたもの
callbackdataはゼロアドレス("0x0000000000000000000000000000000000000000")
callbackDataは'0x'

ついでにPoolコントラクトにあるswap関数も見てみると、Routerコントラクトのswap関数の中に、Poolコントラクトのswap関数で使用するdataがぶちこんであるんだな、というイメージが持てると思います(合っているかは置いておいて)。

これを0.1ETHをUSDCにスワップするという条件のもと組み立て切るとコードが完成します。

細かいポイント

withdrawmodeは
1: native ETHでやり取りするときに使います
2: wraped ETHのままでやり取りするときに使います
0: は何を意味しているのか分かりません(勉強不足です)

今回は0.001ETHをUSDCにスワップしましたのでwithdrawmodeは1でやりました。ただ、2でも同じ動きをしましたのでETH→USDC方向の場合にはどちらでも良いものと思われます。
ただ、0でやるとETHがどこかへ行ってしまいました(vaultに幽閉された?GOX?)。なので1か2でやる必要があります。
ちなみに、WETHからのスワップにも挑戦しましたが上手くいきませんでした。できた方いたら教えてください。

下記部分のtokenInがnativeETHの場合はゼロアドレスです。

nativeETH = zero_address
paths = [
    {
        'steps': swap_steps,
        'tokenIn': nativeETH,
        'amountIn': amountIn
    }
]

今回はETH(nativeETH)→USDCなので。
ここをWETHにすればWETH→USDCができるのかと思ったのですが上手くいきませんでした。

ここに書いてあるように、nativeETHを使う場合でも、ABIエンコードに使うのはWETHのアドレスです。

あとは、下記の部分。Docsから根拠を見つけることができなかったのですが、useVaultを設定しないとトランザクションが通りません。また、Falseに設定するとPoolに資金が残ってしまい、自身のwalletに返ってきません。
これはChatGPTを用いてエラー処理している過程で分かりました。

swap_steps = [
    {
        'pool': pool,
        'data': encoded_data,
        'callback': zero_address,
        'callbackData': '0x',
        'useVault': True
    }
]

あとは、いつも通り、
・syncswap_linea_wethusdcPool_ABI.json
・router_linea_ABI.json
にPoolコントラクトとRouterコントラクトのABIを貼り付けるぐらいです。

USDC→ETHの方向でSwapするコード@SyncSwap

今度は、2.5USDC→ETHにスワップするコードを示します。
これはETH→USDCにスワップするコードをアレンジしただけです。
※2.5USDCにしているのは、現在の約0.001ETHに相当するからです(逆、をイメージしただけです)。

#linea_usdc_eth_syncswap.py

import json
from web3 import Web3
from datetime import datetime
from eth_abi import encode

lineaRpc = "https://linea-mainnet.infura.io/v3/e209d4f3c05e4356aa94c31f79d28689"
myWallet = 'my_wallet_address'
privateKey = 'my_private_key'
pool = '0x5Ec5b1E9b1Bd5198343ABB6E55Fb695d2F7Bb308'
WETH = '0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f'
USDC = '0x176211869cA2b568f2A7D4EE941E073a821EE1ff'

gas = 200000

web3 = Web3(Web3.HTTPProvider(lineaRpc))

amountIn = 2500000 #USDCのdecimalsは6

with open('syncswap_linea_wethusdcPool_ABI.json') as f:
   poolABI = json.load(f)
poolContract = web3.eth.contract(address=pool, abi=poolABI)

amountOut = poolContract.functions.getAmountOut(USDC, amountIn, myWallet).call()

print("amountOut[wei] = ")
print(amountOut)

encoded_data = encode(["address", "address", "uint8"], [USDC, myWallet, 1])
zero_address = "0x0000000000000000000000000000000000000000"
swap_steps = [
    {
        'pool': pool,
        'data': encoded_data,
        'callback': zero_address,
        'callbackData': '0x',
        'useVault': True
    }
]

paths = [
    {
        'steps': swap_steps,
        'tokenIn': USDC,
        'amountIn': amountIn
    }
]

router = '0xC2a1947d2336b2AF74d5813dC9cA6E0c3b3E8a1E'
with open('router_linea_ABI.json') as f:
   routerABI = json.load(f)
routerContract = web3.eth.contract(address=router, abi=routerABI)

now = int(datetime.now().timestamp())
deadline = now + 60*3

tx = routerContract.functions.swap(paths, 0, deadline).build_transaction(
    {
        "from": myWallet,
        "nonce": web3.eth.get_transaction_count(myWallet),
        "value": 0,
        "gasPrice": web3.eth.gas_price,
        "gas": gas
    }
)

signed_tx = web3.eth.account.sign_transaction(tx, privateKey)
txHash = web3.eth.send_raw_transaction(signed_tx.raw_transaction)

print('Send transaction')
print(web3.to_hex(txHash))
result = web3.eth.wait_for_transaction_receipt(txHash)
status = result['status']
if status == 1:
   print('Transaction Succeeded')
else:
   print('Transaction Failed')
   

実行結果とトランザクションを載せておきます。

https://lineascan.build/tx/0x46797b8c95aab7a4692caf8406b203ac685885c473ea953648b386ca0f1cef40

ETH→USDCコードからの変更点

amountInはUSDC基準に変更しました。

amountIn = 2500000 #USDCのdecimalsは6

返ってくるトークン量の算出もUSDCを入力した場合に置き換えました。

amountOut = poolContract.functions.getAmountOut(USDC, amountIn, myWallet).call()

ABIエンコードするデータも変わります。

encoded_data = encode(["address", "address", "uint8"], [USDC, myWallet, 1])

入力がUSDCなので以下も変更です。

paths = [
    {
        'steps': swap_steps,
        'tokenIn': USDC,
        'amountIn': amountIn
    }
]

ガス代はETHで支払いますが、スワップの対象としてETHを入力する必要は無いので下記のvalueは0にします。

tx = routerContract.functions.swap(paths, 0, deadline).build_transaction(
    {
        "from": myWallet,
        "nonce": web3.eth.get_transaction_count(myWallet),
        "value": 0,
        "gasPrice": web3.eth.gas_price,
        "gas": gas
    }
)

以上です。

ちなみに、withdrawmodeを2としてみましたが、失敗(Reverted)しました。いやWETHどうやって使うんだよ。

まとめ

pythonプログラミングでSyncSwapのRouterを使ったSwapをしてみました(ETH→USDCとUSDC→ETH)。
USDC→ETHの際に、トランザクション構築でvalue: 0のパターンが試せているので、例えばETH以外のトークンとUSDCのスワップなども、定数を置き換えることで可能になると考えている(まだやってはいない)。気が向いたときにこれも試してみたい。


この記事が気に入ったらサポートをしてみませんか?