UniswapV2でトークンの価格取得/スワップ(web3py)

# 概要

Uniswapはその誕生以降、様々なクローンが様々なチェーンで作られています。そのほとんどがUniswapV2に対応しており、適宜アドレスを変えるだけで同じように扱えます。
今回はgas代が安く、Infuraなどの登録なしでRPCが使える、PolygonチェーンのQuickswapを例にトークンの価格取得とスワップの実装を行います。

# DAI/ETHの価格取得
はじめにUniswapのfactoryとrouterのコントラクトインスタンスを取得します。factoryは、DAI-ETHのペアコントラクトのアドレスを取得するために必要です。routerはスワップする際に必要になります。

```python
from web3 import Web3,HTTPProvider
import json, os, time

RPC_URL = "https://polygon-rpc.com" # Polygon Matic RPC address
FACTORY_ADDRESS = "0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32" # QuickSwap UniswapV2Factory
ROUTER_ADDRESS = "0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff" # QuickSwap UniswapV2Router02
DAI_ADDRESS = "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063" # DAI on polygon
WETH_ADDRESS ="0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619"# WETH on polygon

with open("abis/IUniswapV2Pair.json", "r") as f:
    IUniswapV2Pair =json.loads(f.read())
with open("abis/IUniswapV2Factory.json", "r") as f:
    IUniswapV2Factory =json.loads(f.read())
with open("abis/IUniswapV2Router02.json", "r") as f:
    IUniswapV2Router02 =json.loads(f.read())
with open("abis/IERC20.json", "r") as f:
    IERC20 =json.loads(f.read())

w3 = Web3(HTTPProvider(RPC_URL))
factory = w3.eth.contract(address = FACTORY_ADDRESS , abi = IUniswapV2Factory["abi"])
router = w3.eth.contract(address = ROUTER_ADDRESS , abi = IUniswapV2Router02["abi"])
```

`FACTORY_ADDRESS`, `ROUTER_ADDRESS`は[このページ](https://polygonscan.com/accounts/label/quickswap)、DAIやWETHのコントラクトアドレスは[このページ](https://polygonscan.com/tokens)から確認できます。ABIファイルは以下のリンクからダウンロードできます。

https://unpkg.com/browse/@uniswap/v2-periphery@1.1.0-beta.0/build/

次にDAI-ETHペアコントラクトのアドレスを`getPair`メソッドで取得し、そのアドレスを使ってコントラクトインスタンスを作ります。
```python
eth_dai_address = factory.functions.getPair(DAI_ADDRESS , WETH_ADDRESS).call() # eth_daiのコントラクトアドレスを取得
eth_dai = w3.eth.contract(address = eth_dai_address, abi = IUniswapV2Pair["abi"]) # eth_daiのコントラクトインスタンスを取得
print("token0 = ",  eth_dai.functions.token0().call())
print("token1 = ",  eth_dai.functions.token1().call())
```
出力結果
```
token0 =  0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619
token1 =  0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063
```

ペア通貨はそれぞれ`token0`, `token1`と呼ばれます。今回はWETHが`token0`, DAIが`token1`に対応していることがわかります。

次に価格を取得します。
```python
reserves = eth_dai.functions.getReserves().call()
print("token0 reserver = ", reserves[0]*10**(-18))
print("token1 reserver = ", reserves[1]*10**(-18))
print(" block.timestamp = ", reserves[2])
print("token0 price = ", reserves[1]/reserves[0]) # DAI/ETH price in this case
```
出力結果
```
token0 reserver =  3025.605972350233
token1 reserver =  6015799.838675748
block.timestamp =  1653458719
token0 price =  1988.2958632590182
```
reserverとはプールされているトークン量です。ETHもDAIも最小単位に対し小数点が18桁目にあるので、$10^{-18}$を掛けています。`reservers`の第3要素は値を取得したブロックのタイムスタンプになっています。`reserves[1]/reserves[0]`が`token0`1単位あたりの`token1`の量、この場合はDAI/ETHの価格になっています。

# DAI→WETHのスワップ
DAI→WETHのスワップを実装してみます。あらかじめウォレットにいくらかDAIを用意しておいて下さい。

## approve

スワップはUniswapのRouterコントラクトが、ウォレットからDAIを`transferFrom`メソッドで引き抜き、ETHに交換します。そのためスワップに先立って、Routerコントラクトのアドレスに対し`approve`を行う必要があります。

```python
my_address = os.getenv("ADDRESS")
my_private_key = os.getenv("PRIVATE_KEY")
dai = w3.eth.contract(address = DAI_ADDRESS, abi = IERC20["abi"])

dai_amount = Web3.toWei("0.1", "ether") # スワップしたい量

nonce = w3.eth.get_transaction_count(my_address)
tx_approve = dai.functions.approve(ROUTER_ADDRESS, dai_amount).buildTransaction(\
    {'chainId': 137, \
     'type': 2, \
     'gas': 10**6, \
    'maxFeePerGas': w3.toWei('100', 'gwei'),\
    'nonce': nonce, })
signed_tx_approve = w3.eth.account.sign_transaction(tx_approve, private_key = my_private_key)
w3.eth.send_raw_transaction(signed_tx_approve.rawTransaction)
```

## swap
いよいよスワップです。
スワップのメソッドはいくつかありますが、今回は`swapExactTokensForTokens`を使います。
```solidity
function swapExactTokensForTokens(
  uint amountIn,
  uint amountOutMin,
  address[] calldata path,
  address to,
  uint deadline
) external returns (uint[] memory amounts);
```
これは`amountIn`で指定した量のトークン(`path[0]`で指定されたもの）をトークン(`path[path.length - 1]`で指定されたもの）になるべく多く交換し、アドレス`to`に送金するメソッドです。
`path`はスワップの経路を指定します。今回はDAIとWETHのペアプールが存在するので直接スワップすることができて、`path[0]`にDAIのコントラクトアドレス、`path[1]`にETHのコントラクトアドレスを指定すれば良いです。`amountOutMin`は交換後の最低トークン量を指定します。この量を下回るならばrevertされます。`deadline`には取引の期限を指定します。

```python
amountIn = dai_amount
amountOutMin =int(reserves[0]/reserves[1]*amountIn*0.99) # 1%以内のスリッページで実行
path = [DAI_ADDRESS, WETH_ADDRESS] # daiからwethへ両替することを指定
to = my_address
deadline = int(time.time() + 60*10) # 10分以内に実行されなければrevert
nonce = w3.eth.get_transaction_count(my_address)
tx_swap = router.functions.swapExactTokensForTokens(amountIn, amountOutMin, path, to, deadline).buildTransaction(\
    {'chainId': 137, \
     'type': 2, \
     'gas': 10**6, \
    'maxFeePerGas': w3.toWei('100', 'gwei'),\
    'nonce': nonce, })
signed_tx_swap = w3.eth.account.sign_transaction(tx_swap, private_key = my_private_key)
w3.eth.send_raw_transaction(signed_tx_swap.rawTransaction)
```
以上でスワップが完了します。

# 参考文献
https://docs.uniswap.org/protocol/V2/concepts/core-concepts/swaps