わざわざUniswap開いてSwapする時代は終わりや!!
はじめに
「このNFT買うのに0.1ETH必要だけど今持ち合わせてないな、、、」
「Uniswapで交換してくるか、、、」
このような経験みなさんはありますでしょうか。
正直めちゃくちゃ手間がかかるわけではありませんが、ここの部分省略できるとUX上がりそうだなと思ったわけです。
ということで購入前に自動でSwapするライブラリを作ってみました!
JPYCのハッカソンということもあって今回はETHで買うものをJPYCで買えるようにJPYC→ETHにSwapする形にします。
使用技術
Uniswap v3 SDK
Uniswapの価格の表示をしたり、流動性の追加や削除を行うことができるSDK。
また、コントラクトのABIも備わっているので簡単にUniswapのコントラクトを呼び出すことができます。
infura
ネットワーク構築用に使用しています。今回はRinkebyネットワークで利用しています。
https://infura.io/
TypeScript
言わずもがなのTypescriptです。最近勉強中です。
ethers.js
web3ProviderやBigNumberで利用しています。web3.jsはほとんど触ったことがないので基本的にはethers.jsを利用しています。
実装
今回のライブラリは基本的に金額表示とSwapの機能の2つを実装しました。
金額表示は1JPYCがどれくらいのETHなのかを取得します。
初期値の設定→金額表示→Swapの順で解説していきます!
初期値
コインの情報
最初はコインの情報を定義します。
今回はJPYCとETHの情報を登録します。今後はバラエティを増やしていきたいと思います。
const name0 = 'JPY Coin'
const symbol0 = 'JPYC'
const decimals0 = 18
const address0 = '0x564e849C68350248B441e1BC592aC8b4e07ef1E9'
const name1 = 'ETH'
const symbol1 = 'ETH'
const decimals1 = 18
const address1 = '0xc778417E063141139Fce010982780140Aa0cD5Ab'
Uniswapのコントラクトの取得
Uniswapのコントラクトをethers.js
を利用して取得してきています。
また、providerはJsonRpcProvider
経由で取得してきているので引数に先ほどinfuraで登録したURLを代入しておきます。
また、コントラクトアドレスは0x1F98431c8aD98523631AE4a59f267346ea31F98
です。
dconst provider = new ethers.providers.JsonRpcProvider(INFURA_URL_TESTNET)
const uniswapContract = new ethers.Contract(
'0x1F98431c8aD98523631AE4a59f267346ea31F984',
UniswapV3Factory_ABI,
provider,
)
プールアドレスの取得
プールとはSwapするために暗号資産をペアで保存しておく場所です。
プールにはそれぞれアドレスが振られていますのでそちらを取得していきます。
const poolAddress = uniswapContract.getPool(
address0,
address1,
10000,
)
1、2個目の引数にはそれぞれのトークンのアドレスを代入します。
3つ目の引数にはプール作成時の手数料を代入します。今回は手数料が1%なので10000を代入しています。
プールのコントラクトの取得
プールアドレスを取得したので次にプールのコントラクトアドレスを取得します。
import { abi as IUniswapV3PoolABI } from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json'
const poolContract = new ethers.Contract(
poolAddress,
IUniswapV3PoolABI,
provider
)
2つ目の引数のABIはUniswapのSDKに入っているのでそこから簡単に参照できます。
ERC20のABIを用意
Swapするトークンのコントラクトを取得するのでABIを用意しておきます。
abi.json
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "spender",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "decimals",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "subtractedValue",
"type": "uint256"
}
],
"name": "decreaseAllowance",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "addedValue",
"type": "uint256"
}
],
"name": "increaseAllowance",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
]
金額表示
ここまで基本的な定数を定義したのでここから実際に金額を取得していきます。
トークン属性の取得
プールコントラクトを経由してプールやトークンの情報を取得します。
const [token0address, token1address, fee, liquidity, slot] =
await Promise.all([
this.poolContract.token0(),
this.poolContract.token1(),
this.poolContract.fee(),
this.poolContract.liquidity(),
this.poolContract.slot0(),
]);
トークンインスタンスの作成
トークン情報が入ったインスタンスを生成します。次のプールのインスタンスを作る際に使用します。
const TokenA = new Token(this.chainId, address0, decimals0, symbol0, name0);
const TokenB = new Token(this.chainId, address1, decimals1, symbol1, name1);
プールインスタンスの作成
プールのインスタンスを作成します。このインスタンスによってプール固有の価格情報を取得することができます。
const pool = new Pool(
TokenA,
TokenB,
fee,
slot[0].toString(),
liquidity.toString(),
slot[1]
);
金額の取得
1JPYCがどれくらいのETHなのかを取得します。
return Number(pool.token0Price.toSignificant(10))
Swap
次はSwapの実装について解説します。
Swapを行う場合、SDK経由で実装するのではなく、コントラクトにアクセスして実行する必要があるので事前にコントラクトを紹介します。
Swapを行うコントラクト
Swapを行うコントラクトはswapExactInputSingle
と swapExactOutputSingle
の2つあります。
この2つのコントラクトの違いは入金額と出金額のどちらかを固定させるかです。
swapExactInputSingle
は入力側の額を固定して出金額をできるだけ大きくします。
一方でswapExactOutputSingle
は出金額を固定してできるだけ入金額を抑えます。
今回はNFTの購入等の出金額があらかじめ決まっている価格のためにSwapを行うので、後者のswapExactOutputSingle
を利用します。
詳しくはこちらのページを参考にしてください。
swapExactInputSingle
ISwapRouter.ExactInputSingleParams memory params =
ISwapRouter.ExactInputSingleParams({
tokenIn: DAI,
tokenOut: WETH9,
fee: poolFee,
recipient: msg.sender,
deadline: block.timestamp,
amountIn: amountIn,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
});
// The call to `exactInputSingle` executes the swap.
amountOut = swapRouter.exactInputSingle(params);
swapExactOutputSingle
ISwapRouter.ExactOutputSingleParams memory params =
ISwapRouter.ExactOutputSingleParams({
tokenIn: DAI,
tokenOut: WETH9,
fee: poolFee,
recipient: msg.sender,
deadline: block.timestamp,
amountOut: amountOut,
amountInMaximum: amountInMaximum,
sqrtPriceLimitX96: 0
});
// Executes the swap returning the amountIn needed to spend to receive the desired amountOut.
amountIn = swapRouter.exactOutputSingle(params);
上記がコントラクトのコードです。最終的にexactOutputSingle
にparams
を渡せるようにします。
swapRouterのコントラクトを取得
swapRouterは先ほどのメソッドを実行するためのコントラクトです。
const immutables = await getPoolImmutables(poolContract)
const swapRouterAddress = '0xE592427A0AEce92De3Edee1F18E0157C05861564'
const swapRouterContract = new ethers.Contract(
swapRouterAddress,
SwapRouterABI,
provider
)
getPoolImmutables
は先ほどの「トークン属性の取得」をメソッド化したものです。
swapRouterAddress
はrinkebyの場合は上記のアドレスを利用します。
また、SwapRouterABI
はSDKから取得可能です。
amountの設定
const inputAmount = price * 1.01
const amountOut = ethers.utils.parseUnits(
inputAmount.toString(),
decimals0
)
const eth = await this.showPrice(inputAmount)
const amountInMaximum = (Math.floor(eth * 1.1)).toString() + "000000000000000000"
price
はETHの金額で、ethers.utils.parseUnits
でBigNumber
型に整形します。こちらで出金額の設定を行っています。
また、最大に入金できる金額も設定しています。今は1.1倍にしていますが、この感覚が正しいかよくわかっていません。
approve
Swapの承認を行います。
const tokenContract0 = new ethers.Contract(
address0,
ERC20ABI,
provider
)
const approvalResponse = await tokenContract0.connect(web3Provider.getSigner()).approve(
swapRouterAddress,
amountInMaximum,
)
Swapの承認を行うために事前に対象のトークンの承認をしておく必要があるので、対象トークン(JPYC)のコントラクトでapproveを得ます。
上記の画像を見ると承認された金額が記されていますが、これがamountInMaximum
になっています。
paramの設定
const params = {
tokenIn: immutables.token0,
tokenOut: immutables.token1,
fee: immutables.fee,
recipient: web3Provider.getSigner().getAddress(),
deadline: Math.floor(Date.now() / 1000) + (60 * 10),
amountOut: amountOut,
amountInMaximum: BigNumber.from(amountInMaximum),
sqrtPriceLimitX96: 0,
}
params
に必要な変数を代入します。tokenIn
が入金するtokenでtokenOut
は出金するトークンです。
また、recipient
はユーザのウォレットアドレスを代入します。
amountInMaximum
はstring
なのでBigNumber
型にパースしておきます。
Swapの実行
最後にコントラクトでSwapを実行します。
const transaction = await swapRouterContract.connect(web3Provider.getSigner()).exactOutputSingle(
params,
{
gasLimit: ethers.utils.hexlify(2000000)
}
)
実行結果のトランザクションは以下のようになります。
課題
- テストネットだとプールがほとんどないので取引量の大きいSwapは現時点では不可能になっている
- 対応しているコイン、チェーンが非常に限定的
- ダイアログのコンポーネントごとライブラリ化したほうが使用しやすい
- 価格が固定値で出てしまう
- ガス代等のSwapに係る情報を充実させないと実用的じゃない
- テスト書く()
終わりに
時間が1日ほどしかなくて駆け足で実装したのでこれからもう少し修正していこうと思います。
今後Uniswap自体に対応しているコインやチェーンを対応していきたいと思います!
QuickSwapやPancakeSwap等のuniswapからフォークしているサービスにも対応できるといいなと現時点では考えています。
最後にライブラリ周りのリンクを貼っておきます。
もし、「こうしたらいい」というようなフィードバックがあればどんどん教えてください🙏
Discussion