はじめに
1inch Fusion+ クロスチェーンスワップ1は、チェーン間で資産を交換するアトミックスワップ2に、オークションを足したような形式のスワッププロトコルです。ユーザーはチェーンの手数料を支払わずにスワップを実行できます。
この記事では、ユーザーがなぜ完全ガスレスでスワップを実行できるのかについても、Fusion+の元になっているFusionで利用されている オンチェーン指値(limit-order) プロトコルを踏まえて説明します。
いずれも1inchがコントラクトをデプロイしているEVM系チェーンでのみ利用できます。
基本となる設計コンセプト
- 裏でbotを動かすユーザー(リゾルバ)がリスク(後述)を負って競争を行い、リスク込みの最適な価格で約定させる
- エンドユーザーに負担をかけない
- ガス代を支払うのは注文を約定させるリゾルバのみに限定させる
- Permit2規格に対応したコントラクトを使うことで、ユーザーはオフチェーン署名を実行するだけで注文の実行が可能になる。
登場人物
- ユーザー
- オフチェーン注文を出すエンドユーザー
- リレイヤー
- ユーザーが作成したオフチェーン注文をリゾルバに渡すネットワーク
- リゾルバ
- ユーザーからリレイヤーを介して受け取ったオフチェーン注文を使ってオンチェーントランザクションを作り、注文を実行するユーザー。1inchのサービスに許可されたリゾルバだけが、リレイヤーのネットワークに接続できる。
オンチェーン指値(limit-order)プロトコルの仕様
オンチェーン指値(limit-order)プロトコルは、Fusionのシングルチェーン上におけるスワッププロトコルで用いられている仕組みです。おおまかに言ってオークション形式で、裏でbotを動かすリゾルバが競争することで、最適な価格での決済ができるような仕組みになっています。
オークションはダッチオークション3形式で行われ、時間とともにユーザーの注文をオンチェーン実行できる金額が下がっていき、ユーザーが指定した最低値まで下がるか、リゾルバのうちの誰かがオークションを落札すると、終了します。
指値で扱うトークンは任意のERC20トークンですが、ERC20トークン側でPermit2という規格に対応しておくと、オンチェーン上トランザクションを実行するときに、トークンの所有者以外に対してトークンの移動許可を行うことができます。これは通常のERC20規格のapproveとは異なり、オンチェーンで許可トランザクションを発行する必要がありません。
flowchart TD
user["ユーザー"]
relayer["リレイヤー"]
resolver["リゾルバ"]
subgraph ブロックチェーン
subgraph src-contract["スワップ元コントラクト"]
user-src-address["ユーザーのスワップ元アドレス"]
resolver-dst-address["リゾルバのスワップ先アドレス"]
end
subgraph dst-contract["スワップ先コントラクト"]
resolver-src-address["リゾルバのスワップ元アドレス"]
user-dst-address["ユーザーのスワップ先アドレス"]
end
tx["オンチェーントランザクション"]
end
user -->|1. オフチェーントランザクションを送信| relayer
relayer -->|2. オフチェーントランザクションをリレー| resolver
resolver -->|3. 注文を実行するか判定| resolver
resolver -->|4. オフチェーントランザクションを組み立てて注文を実行| tx
tx --> src-contract & dst-contract
user-src-address -->|ユーザーの署名で資金移動| resolver-dst-address
resolver-src-address -->|リゾルバの署名で資金移動| user-dst-address
この指値プロトコルは、同じチェーン内部でのスワップのみサポートしています。
ブリッジを使わないクロスチェーンスワップの仕様
上記の指値プロトコルを改良して、クロスチェーンでの交換(アトミックスワップ2)を行えるようにしたプロトコルが Fusion+です。逆にFusion+では、同じチェーンの中での交換は仕様上不可能になっています。
以前のブログ4でも紹介しましたが、クロスチェーンブリッジにはとにかくセキュリティ上の問題が起きやすく、可能であれば触らないに越したことはないです。Fusion+のクロスチェーンスワップについては、裏でbotを動かすリゾルバ(resolver)がいることが前提になっているので、クロスチェーンブリッジのプールと直接コントラクトがつながっていなくても、動作することになっています。
リゾルバはクロスチェーンブリッジを使用するなどして、どこからかスワップ先のトークンを引っ張ってくる可能性がありますが、そのリスクはFusion+のプロトコルではなく、リゾルバが負うことになります。
リゾルバは、スワップが完了するまでトークンを預け入れておくエスクローコントラクトを「取引が発生する度に」スワップ元チェーンとスワップ先チェーンの両方で作成します。このとき、エスクローコントラクトにはユーザーが保有するシークレット値のハッシュ値を入れておきます。エスクローコントラクトからは規定のブロック数(finality lock period)が経過すると、シークレットの原像を使って出金ができるようになります。エスクローコントラクトから出金を行うのは、エスクローコントラクトをデプロイしたのと同じリゾルバです。リゾルバは、出金のためにシークレットが必要なので、ユーザーがシークレットを公開するのを待ちます。
スワップ元チェーンとスワップ先チェーンの両方で正しくコントラクトが作成されたことを検証したユーザーは、シークレットを公開します。すると、そのシークレットを使って、リゾルバはエスクローコントラクトから自分のトークンを出金し、また、ユーザー用のトークンも出金します。ユーザー用のトークンが出金されない場合、リゾルバのコラテラルが一部没収され、最終的にユーザーのトークンは一定時間後に返金されます。
この仕組みはHTLC(ハッシュタイムロックコントラクト)と呼ばれ、元々はBitcoin上でアトミックスワップを行うために利用されていたものです。UTXO型のBitcoinと違い、アカウント型で実装していますが、コントラクトの作成・入金とそこからの出金というパターンは踏襲されています。
flowchart TD
user["ユーザー"]
relayer["リレイヤー"]
resolver["リゾルバ"]
subgraph src-chain["スワップ元のブロックチェーン"]
src-factory["スワップ元の<br>ファクトリコントラクト"]
src-escrow["スワップ元の<br>エスクロー<br>コントラクト"]
subgraph src-contract["スワップ元トークンコントラクト"]
user-src-address["ユーザーの<br>スワップ元<br>アドレス"]
src-escrow-balance["エスクロー<br>管理の残高"]
resolver-dst-address["リゾルバの<br>スワップ先<br>アドレス"]
end
src-tx["スワップ元の<br>出金tx"]
end
subgraph dst-chain["スワップ先のブロックチェーン"]
dst-factory["スワップ先の<br>ファクトリコントラクト"]
dst-escrow["スワップ先の<br>エスクロー<br>コントラクト"]
subgraph dst-contract["スワップ先トークンコントラクト"]
resolver-src-address["リゾルバの<br>スワップ元<br>アドレス"]
dst-escrow-balance["エスクロー<br>管理の残高"]
user-dst-address["ユーザーの<br>スワップ先<br>アドレス"]
end
dst-tx["スワップ先の<br>出金tx"]
end
user -->|1. オフチェーントランザクションを送信| relayer
relayer -->|2. オフチェーントランザクションをリレー| resolver
resolver -->|3. 注文を実行するか判定| resolver
resolver -->|4. エスクロー<br>コントラクト<br>を作成| src-factory & dst-factory
user -->|5. エスクローを確認後<br>シークレットを公開| relayer
relayer -->|6. シークレットをリレー| resolver
resolver -->|7. シークレット<br>組み立てて引き出しを実行| src-tx & dst-tx
src-tx -->|7. 引き出し実行| src-escrow
dst-tx -->|7. 引き出し実行| dst-escrow
user-src-address -->|4. ユーザーの<br>署名で資金移動| src-escrow-balance
src-escrow-balance -->|7. 引き出し| resolver-dst-address
src-escrow -->|4. 管理権を取得| src-escrow-balance
resolver-src-address -->|4. リゾルバの<br>署名で資金移動| dst-escrow-balance
dst-escrow-balance -->|7. 引き出し| user-dst-address
dst-escrow -->|4. 管理権を取得| dst-escrow-balance
src-factory -->|4. エスクロー<br>コントラクト<br>の生成| src-escrow
dst-factory -->|4. エスクロー<br>コントラクト<br>の生成| dst-escrow
リゾルバは、自分のスワップ先トークンだけを引き出して、ユーザーのスワップ元トークンを放置することが可能です。その場合、リゾルバのコラテラルから一定額が没収されます。さらに、注文を取得したリゾルバ以外の他のリゾルバが、そのトークンをユーザーに償還するトランザクションを発行できるようになります。もし、他のリゾルバも一切ユーザーに対して返金するトランザクションを発行しない場合、ユーザー自身を含め誰でもユーザーに返金できるようになります。
以下画像はコントラクトのREADME5より。経過時間によって、どのプレイヤーがユーザー向け、リゾルバ向けに資金のアンロックができるかが決まっています。
部分約定(partial fill)の仕様
例えば、25%, 50%, 75%, 100% のそれぞれの段階で約定を行えるような注文を作成し、複数のリゾルバが協力して少しずつ注文を約定することができるような仕組みです。流動性プールがないスワッププロトコルである以上、一度に巨額の資金をスワップするのは、リゾルバ側の資金力に依存してしまい難しくなります。
ユーザーはそれぞれの段階に対応したシークレットを用意し、マークル木でユーザーのシークレットハッシュをまとめたものを使用します。ユーザーは全てのシークレットを後に公開します。部分約定の段階に対応したシークレットに応じたコントラクトが複数作成されることになります。
flowchart TD
root
node1["internal node"]
node2["internal node"]
node3["hash(secret for 25%)"]
node4["hash(secret for 50%)"]
node5["hash(secret for 75%)"]
node6["hash(secret for 100%)"]
root --- node1
root --- node2
node1 --- node3
node1 --- node4
node2 --- node5
node2 --- node6
上記の図で言うと、25%までの決済を行うリゾルバは新しく生成したエスクローコントラクトに対して hash(secret for 25%)
の値を登録しておきます。次に注文を取りたいリゾルバは、hash(secret for 25%)
の値はすでに前のリゾルバに取られてしまっているので、hash(secret for 50%)
, hash(secret for 75%)
, hash(secret for 100%)
の値を選ぶことができます。ここでは hash(secret for 75%)
の値を取って、75-25=50%の注文を決済することに決め、2人目もエスクローコントラクトを作成したものとします。最後に、3人目のリゾルバがhash(secret for 100%)
を選んでエスクローコントラクトを作成します。
全ての注文が決済されるために必要なコントラクトが出揃ったら、ユーザーは全てのシークレットを公開し、それぞれのコントラクトをデプロイしたリゾルバが、それぞれのシークレット値を使って、スワップ前のトークンを受け取ります。
MEV保護の仕組み
リゾルバはユーザーのトランザクションを直接実行するのではなく、一度エスクローコントラクトを作成して、その中にトークンの管理権を移動させてから引き出しを実行します。そのため、フロントランニングなどのMEVを使った攻撃を防げます。
なお、リゾルバが現物トークンを確保する過程において他の「MEVに対して脆弱なプロトコル」を使えば、間接的に被害を受ける可能性はありますが、ユーザーは直接影響を受けません。
問題点・注意点
- リゾルバ/リレイヤーへの参加は1inchの許可が必要であり完全競争ではない
- 注文毎のリゾルバのトランザクション数が多く、手数料負担が大きい
- テストネットに対応したリレイヤーのapiが公開されていないので、開発試験を行う場合も最初から実際の資産を使ってメインネットで行う必要がある。もし開発中のシステムにバグがあった場合、リゾルバに延々と負担を強いるような注文を出してしまう可能性がある。その場合、攻撃を意図していなくてもリゾルバからその後の注文を拒否される可能性がある。
- リゾルバはリスクを負うことになり、そのリスクは価格に転嫁される
- リゾルバは以下のような状況に陥る可能性がありますが、そのリスクを考慮した上でユーザー注文を約定させる必要があります。
リゾルバが不利な状況の具体例
- 悪意のあるユーザーMがPolygonチェーン(手数料の安いチェーン)上に、USDCトークンを100万円分持っているとする。
- そのユーザーMがEthereumメインチェーン(手数料の高いチェーン)上で最低50万円分のUSDCにスワップするための注文を1inch api経由作成し、リレイヤー経由でリゾルバのネットワークに送信する。注文は細かく部分約定(partial fill)の設定がされているため、複数のリゾルバが協調して約定できる。
- 複数の脆弱なリゾルバは、上記の注文を見て、50万円分の利益が出ると判断する。それらのリゾルバ(金額が多ければ複数になることも考えられる)は、約定を行うために、まず両方のチェーン上(Polygon, Ethereumメインチェーン)で、エスクローコントラクトを作成する必要がある。Ethereumメインチェーン上で、多額のガス代を支払ってコントラクトを作成する。
- ユーザーMは本来であれば、コントラクトが作成された時点で、シークレット値を公開する必要がある。しかし、ユーザーMは、完全にノーコストでシークレット値を公開しないことを選択する権利を持っている。ユーザーはエスクローコントラクトで定められた制限時間が経過するまでシークレット値を公開しない。
上記のような状況だと、エスクローはメインチェーン上で誰も使わない可能性があるコントラクトを作成するリスクを判断した上で、注文を受けるかどうか判定する必要があります。
難しいのは、上記のような極端な例でなくても、ノーコストで悪意のある行動ができてしまうユーザーと、通常の収益になるユーザーの判定を各リゾルバが行わないといけないという点です。
なおユーザーMは失うものはありませんが、特に得るものはありません(もちろん、ブロック生成時の手数料を得るようなプレイヤーでない限りは)。
おまけ
1inchの印象として、複数のAMMなどの情報を統合して、最適な経路を見つけてトークンスワップを実行するようなイメージがあります。この機能は Classic Swapと呼ばれるAPIで提供されていますが、Fusion(シングルチェーンスワップ), Fusion+(クロスチェーンスワップ)では使われていないということに注意してください。Fusion+でトークンスワップを行うと、中間で複数のAMMが実行されることはありません。裏側でリゾルバがそのようなことをやっているかもしれませんが、そこは完全にブラックボックスになっています。
参考文献
-
swap overview https://portal.1inch.dev/documentation/apis/swap/introduction ↩
-
Atomic swap https://en.bitcoin.it/wiki/Atomic_swap ↩ ↩
-
Dutch auction https://en.wikipedia.org/wiki/Dutch_auction ↩
-
ハッカーはどこを突いた? EVMクロスチェーンブリッジの攻撃手法とは https://tech.hashport.io/5106/ ↩
-
cross-chain-swap https://github.com/1inch/cross-chain-swap?tab=readme-ov-file ↩