1inch.exchangeでトークンスワップをやってみる — EthereumDev Tutorial —
米大統領選後は世界同時株高、暗号通貨高という驚きの様相を呈していますが、DeFiも例外ではありません。TVL (Total Value Locked) は引き続き上昇しており、Etherやガバナンストークンの価格も牽引されています。DeFiの文脈で主力はイールドファーミングやAMM (Automated Market Maker) のようなサービスですが、トークンスワップの需要はそれに連動しているように見えます。以下の画像はDune AnalyticsによるDEXのデータですが、メトリクスを見るとUniswap、Curve Financeを中心として盛んにトークンスワップが行われています。[1]

今回はEthereumDevに掲載されている、1inch.exchangeを対象としたトークンスワップのコードを試してみました。EthereumDevの中の人には許可を得ていますのでコードベースもそのまま本家のものを使用させて頂きます。元ネタは以下の2つの記事です。[2] [3]
知らない方のために1inch.exchangeを簡単に説明しておきます。このサービスは一般的にDEX aggregatorと呼ばれます。サービスがUniswapやKyberなどの複数のDEXの中から最適なレートと低いスリッページを発見してユーザは1inch.exchange上でトークンスワップを実行することができます。以下は1 ETHからDAIへのトークンスワップに対するルーティング表示ですが、Exhchanges以下に各DEXとその交換レートが表示されています。

1inch.exhcnageは最適なレートを発見する上でDEXを跨いだトレードを設定し、ユーザはシングルトランザクションで効率的にトークン交換が可能となっています。結果的にUniswapやKyberの単一のDEXでユーザ自身が発見するよりも良いレートで交換できることがメリットです。このようなレートの発見はアービトラージ(裁定取引)といった歪みを発見することに役立つと考えられます。以下はETHからDAIへのスワップですが、この交換に対して複数のトークンルーティング、複数のDEX(UniswapV2, Curve, Mooniswap)を経由する最適な経路が提示されています。

前置きはこれぐらいにして具体的なコードへと移ります。最初のステップでは1inch.exchangeからレートを取得します。次のステップで実際にトークンスワップを行うことにします。この2ステップでは1inch.exhcnageに実装されている以下の関数を使用していきます。テストするためにメインネットをganache-cliでローカルのチェーンにフォークして動作を確認します。[4]
- getExpectedReturn()
- swap()
レートの取得
1inch.exchangeはgithubに使用方法が丁寧に記述されています。ドキュメントは英語ですが平易に書かれており理解に易しいです。以下の図は非常に分かりやすくまとまっていると思いました。[5]

IOneSplit.solで1inch.exchangeのインターフェースが定義されているのでgetExpectedReturn()およびswap()のインターフェースを確認してみます。まずレートを取得したいのでgetExpectedReturn()を見てみましょう。
fromTokenおよびdestTokenはトレード元とトレード先のトークンアドレスを渡します。amountはトークン量です。それに対してreturnAmountでスワップされた後に受け取るトークン量が返されます。distributionはuint256型の配列で各DEXに対するスワップの比率が格納されています。
distributionはスワップに対して使用する各DEXの比率だと述べました。より正確には各DEXというより各コントラクトのスワップに対する比率となり、例えばETHからDAIへの交換の44%はUniswap V1で56%はUniswap V2で行う、といった良いレートで交換が成立するコントラクトの使用比率を意味します。OneSplitBase.solのDEXES_COUNTがそのDEX数の定数だと思いますが現在は34にセットされています。
distributionについてはgithubでも言及されています。getExpectedReturn()で得られるdistribution配列はそのままトークン交換のswap()で使用できます。

レート取得のコードを見てみましょう。EthereumDevのリポジトリのpart1ディレクトリにレート取得のコードがまとまっています。
$ git clone git@github.com:jdourlens/ethereumdevio-dex-tutorial.git && cd ethereumdevio-dex-tutorial/part1
$ npm install
実際にメインネットでのレートを取得するためには index.js
でメインネットへの接続ポイントをプロバイダとして指定する必要があります。ここではInfuraを使用しました(15行目です)。
const web3 = new Web3('https://mainnet.infura.io/v3/<endpoint identifier>');
デフォルトで1 ETHをDAIへスワップする設定になっています。そのまま実行してみましょう。このスワップは1 ETHが464.5 DAIほどに交換され、Uniswap V1で44%、Uniswap V2で56%の比率であることが示されています。何度か実行するとその都度結果が異なることが確認できます。
$ node index.js

distributionを配列のまま表示するには以下のようなコードを記述します。配列の要素数が22のためDEXES_COUNTとは一致していませんが、この部分は対象トークンでスワップができるDEXだけに絞られているのかもしれません。調査が必要です。
console.log(`distribution: ${JSON.stringify(result.distribution)}`);
distribution: ["44","0","0","0","0","0","0","0","0","0","0","0","0","56","0","0","0","0","0","0","0","0"]
トークンスワップ
1inch.exchageプロトコルを使用したレートの取得ができました。次にgetExpectedReturn()で取得したdistributionを使用してトークンのスワップをやってみます。EthereumDevではメインネットをganache-cliでローカルのチェーンにフォークしてテストしています。このテスト手法を今回始めて知ったのでここではこの方法でテストしてみました。
EthereumDevのリポジトリのpart2ディレクトリにスワップのコードがまとまっています。既にリポジトリを取得している場合にはpart2のディレクトリへ移動して npm install
を実行してください。
$ cd ../part2/
$ npm install
余談です。ganache-cliを使用してフォークする場合は同時に使用アカウントのアンロックを行っているのでトランザクションへのサイン処理を明示的に記述する必要がありません。メインネットでERC20トークンのapproveや1inch.exchangeのswapのトランザクションを投げたい場合はtruffle-hdwallet-provider-privkeyのライブラリが使ってトランザクションを処理できそうです。[6]
$ npm install truffle-hdwallet-provider-privkey
準備ができたらganache-cliでメインネットをローカルのチェーンへフォークします。 -i
はネットワークIDのオプションで1がメインネットです。ここでは任意の値を与えます。 --unlock
で使用したいEOAをアンロックできます。 -l
はガスリミットです。この内容についてもEthereumDevでは記事がありました。[7]
$ ganache-cli -f https://mainnet.infura.io/v3/<your end point> -d -i 66 --unlock 0x78bc49be7bae5e0eec08780c86f0e8278b8b035b -l 8000000

デフォルトで1000 DAIをETHへスワップする設定になっています。そのまま実行してみました。 getQuote(fromToken, toToken, amountWithDecimals)
という関数がメインで、内部で2つの処理を行っています。まずは approveToken
です。DAIのERC20トークンのコントラクトの approve
を呼んで1inch.exchangeのコントラクトがユーザのトークンを操作できる量を設定します。実際は既存のallowanceを確認した上でトークン量が不足している場合にのみ approveToken
を呼ぶなどの工夫が必要になります。
次のステップです。 onesplitContract.methods.swap(fromToken, toToken, amountWithDecimals, quote.returnAmount, quote.distribution, 0)
でトークンスワップを実行します。 quote.returnAmount
にはレートの取得で判明したトークン交換量が、 quote.distribution
は各DEXでのトークンスワップの比率が配列で格納されています。これらの変数はgetExpectedReturn()で取得した値です。
$ node index.jsERC20 token approved to 0xC586BeF4a0992C495Cf22e1aeEE4E446CECDee0E
Transaction 0xf444f9661ecfd6544e371cded82e4ffd5819be304908431c4e4179b87f9a8245 was mined.
Finan balances:
Change in ETH balance 2.19
Change in DAI balance -1000.00
EthereumDevではこのコードの実行時にエラーが発生する可能性について触れています。 revert OneSplit: actual return amount is less than minReturn
が発生した場合です。これはgetExpectedReturn()で取得したレートや最適化されたルートが変更されたときに発生する可能性があるようです。トレード量に応じ、 minReturn
のパラメータを低くしスリッページを許容することで対応することができる、と説明されています。

まとめ
- 1inch.exchangeはDEX aggregatorの1つで、サポートされる複数のDEXの中から最適なレートと低いスリッページを発見する
- プラットフォーム上でユーザは効率的に最適なトークンスワップをシングルトランザクションで実現できる
- 1inch.exchangeコントラクト上のgetExpectedReturn()およびswap()でトークンスワップのレート取得およびトークンスワップを行える
- ganache-cliでメインネットをローカルのチェーンにフォークすることでメインネットと同じコントラクトアドレスやステートを利用できる
References
- [1] Dune Analytics
- [2] Trading and Arbitrage on Ethereum DEX: Get the rates (part 1)
- [3] Swap tokens with 1inch Exchange in JavaScript: DEX and Arbitrage (part 2)
- [4] Testing your smart contract with existing protocols: Ganache fork
- [5] 1inch.exhcnage howtouseit scheme
- [6] truffle-hdwallet-provider-privkey
- [7] Testing your smart contract with existing protocols: Ganache fork