最近、去中心化取引所の開発チュートリアルを執筆中で、https://github.com/WTFAcademy/WTF-Dapp で Uniswap V3 のコード実装を参考にし、多くの知識を学びました。著者は以前に簡単な NFT 契約を開発したことがありますが、今回は初めて Defi の契約を開発する挑戦をしました。これらの小技は、契約開発を学びたい初心者にとって非常に役立つはずです。

契約開発の達人は直接 https://github.com/WTFAcademy/WTF-Dapp に行ってコードを寄付し、Web3 の発展に貢献しましょう。

次に、これらの小技を見てみましょう。一部は奇技淫巧と呼ばれることもあります。

契約をデプロイした契約アドレスは予測可能にする方法があります。

一般的に契約デプロイから得られるのは、見た目がランダムなアドレスです。「 nonce 」に関連しているため、契約アドレスを予測するのは難しいです。しかし、Uniswap では、取引ペアや関連情報から契約のアドレスを推測する必要があります。これは多くの状況で非常に便利です。例えば、取引の権限を判断したり、プールのアドレスを取得したりする場合です。

Uniswap では、契約の作成は「 pool = address(new Uniswap V3 Pool{salt: keccak 256(abi.encode(token 0, token 1, fee))}()); 」のようなコードで行われます。「 salt 」を追加することで CREATE 2 (https://github.com/AmazingAng/WTF-Solidity/blob/main/25_Create2/readme.md) の方法で契約を作成することができます。こうすることで、作成された契約アドレスは予測可能になり、アドレス生成のロジックは「 新アドレス = hash("0x FF", 作成者アドレス, salt, initcode) 」です。

この部分の内容は WTF-DApp コースの https://github.com/WTFAcademy/WTF-Dapp/blob/main/P103_Factory/readme.md この章を参照して詳しく理解できます。

コールバック関数をうまく活用する

Solidity では、契約間で相互に呼び出しが可能です。あるシーンでは A があるメソッドで B を呼び出し、B が呼び出されたメソッドの中で A をコールバックすることがあります。これは特定のシーンで非常に役立ちます。

Uniswap では、「 Uniswap V3 Pool 」契約の「 swap 」メソッドを呼び出して取引を行うと、回呼ばれた「 swapCallback 」が計算された今回の取引に実際に必要な「 Token 」を引き渡します。呼び出し元はコールバックの中で取引に必要なトークンを「 Uniswap V3 Pool 」に送信する必要があり、「 swap 」メソッドを2つの部分に分けて呼び出し元に呼び出させるのではなく、これにより「 swap 」メソッドの安全性を確保し、全体のロジックが完全に実行されることを保証し、煩雑な変数の記録を必要としません。

コードスニペットは以下の通りです:

コースの取引に関する部分を学ぶことで、詳細を理解できます https://github.com/WTFAcademy/WTF-Dapp/blob/main/P106_PoolSwap/readme.md。

例外を使って情報を伝達し、try catch を使って取引の予測を実現する

Uniswap のコードを参考にしていると、彼の https://github.com/Uniswap/v3-periphery/blob/main/contracts/lens/Quoter.sol この契約では、「 Uniswap V3 Pool 」の「 swap 」メソッドを「 try catch 」で包んで実行しています:

これはなぜかというと、私たちは「 swap 」メソッドをシミュレーションして取引に必要なトークンを予測する必要があるからですが、予測するときには実際にトークンの交換が生じないため、エラーが発生します。Uniswap では、取引のコールバック関数内で特殊なエラーをスローし、そのエラーをキャッチして、エラーメッセージから必要な情報を解析しています。

見た目はハックっぽいですが、非常に実用的です。これにより、取引の予測のために swap メソッドを改造する必要がなくなり、ロジックもよりシンプルになります。私たちのコースでは、このロジックを参考にして https://github.com/WTFAcademy/WTF-Dapp/blob/main/demo-contract/contracts/wtfswap/SwapRouter.sol この契約を実装しました。

大数を用いて精度問題を解決する

Uniswap のコードには多くの計算ロジックがあります。例えば、現在の価格と流動性に基づいて交換されるトークンを計算する場合、計算過程で除算操作による精度の損失を回避する必要があります。Uniswap では、計算過程で「 << FixedPoint 96.RESOLUTION 」という操作を頻繁に使用します。これは左に96ビットシフトすることを意味し、「 2 ^ 96 」で掛け算するのと同等です。左にシフトした後に除算演算を行うことで、通常の取引がオーバーフローせず(一般的に「 uint 256 」を用いて計算し、十分に)、精度を保証できます。

コードは以下の通りです(価格と流動性を用いて取引所に必要なトークン数を計算します):

見ると、まず Uniswap では価格は「 2 ^ 96 」で平方根を掛けた形で表現されます(上記コードの「 sqrtRatioAX 96 」と「 sqrtRatioBX 96 」に対応)。次に流動性「 liquidity 」が左に移動して「 numerator 1 」を計算します。以下の計算では、「 2 ^ 96 」が計算過程で約分され、最終結果が得られます。

もちろん、どのようにしても理論的には精度の損失はありますが、この状況は最小単位の損失にすぎず、受け入れ可能です。

さらに詳しい内容は、https://github.com/WTFAcademy/WTF-Dapp/blob/main/P106_PoolSwap/readme.md この記事を学ぶことで理解できます。

Share の方法で収益を計算する

Uniswap では、LP(流動性提供者)の手数料収益を記録する必要があります。明らかに、取引のたびに各 LP にそれぞれの手数料を記録することはできません。そうすれば大量のガスを消費してしまいます。では、どう処理すれば良いのでしょうか?

Uniswap では、「 Position 」に次の構造体が定義されているのが見えます:

その中には「 feeGrowthInside0LastX128 と feeGrowthInside1LastX128 」が含まれており、彼らは各ポジション(Position)が前回手数料を引き出した時に各流動性が受け取るべき手数料を記録しています。

簡単に言えば、私は総手数料と各流動性がどれだけの手数料を配分されるべきかを記録するだけで済みます。これにより、LP が手数料を引き出す際に手元の流動性に基づいてどれだけ引き出せる手数料があるかを計算することができます。まるであなたがある会社の株を保持しているように、株の利益を引き出す際には、その会社の過去の一株当たりの利益と、前回引き出した時の利益を知っていれば良いのです。

以前、(巧妙な契約設計、stETH がどのように日ごとに自動で利息を発生させるか?あなたの ETH が質権の取得に参加して安定した利息を得る)という記事で stETH の利息計算方法を紹介しましたが、これも似たような理屈です。

すべての情報をチェーン上から取得する必要はありません。

チェーン上のストレージは比較的高価なので、すべての情報をチェーン上に載せる必要はなく、またはチェーン上から取得する必要もありません。例えば、Uniswap のフロントエンドウェブサイトが呼び出す多くのインターフェースは従来の Web2 のインターフェースです。

取引プールのリストや取引プールの情報は、普通のデータベースに保存できます。中には定期的にチェーン上から同期する必要があるものもありますが、リアルタイムでチェーンやノードサービスが提供する PRC インターフェースを呼び出して関連データを取得する必要はありません。

もちろん、現在多くのブロックチェーン PRC のプロバイダーは、高度なインターフェースを提供していますので、より迅速かつ安価な方法でいくつかのデータを取得できます。これは類似の理屈です。例えば、ZAN は特定のユーザーの全ての NFT を取得するためのインターフェースを提供しています。これらの情報は明らかにキャッシュを利用して性能と効率を向上させることができます。詳細は https://zan.top/service/advance-api を訪れてください。

もちろん、重要な取引は必ずチェーン上で行われます。

契約の分割を学び、ERC 721のような既存の標準契約を利用することも学びましょう。

プロジェクトには複数の実際にデプロイされた契約が含まれる可能性があります。実際にデプロイされたのが1つの契約のみであっても、コードを継承の方法で契約を複数に分割して維持することができます。

例えば、Uniswap では、https://github.com/Uniswap/v3-periphery/blob/main/contracts/NonfungiblePositionManager.sol 契約が多くの契約を継承していることがわかります。コードは以下の通りです:

また、「 ERC 721 Permit 」契約の実装を見ていると、直接「 @openzeppelin/contracts/token/ERC 721/ERC 721.sol 」契約を使用していることがわかります。これにより、NFT の方法でポジションを管理することが便利になるだけでなく、既存の標準契約を使用して契約の開発効率を向上させることもできます。

私たちのコースで、https://github.com/WTFAcademy/WTF-Dapp/blob/main/P 108 _PositionManager /readme.md を学んで簡単な ERC 721 の契約を開発してポジションを管理することを試みることができます。

まとめ

どれだけ多くの記事を読むよりも、自分で手を動かして開発する方が実際的です。簡易版の去中心化取引所を実装してみることで、Uniswap のコード実装をより深く理解でき、また実際のプロジェクトで体験する知識も学ぶことができます。

WTF-DApp コースは ZAN の開発者コミュニティと WTF Academy 開発者コミュニティの仲間が共同で完成させたオープンソースのコースです。あなたが Web3 や Defi プロジェクトの開発に興味があるなら、私たちの実戦コース https://github.com/WTFAcademy/WTF-Dapp を参考にし、一歩一歩簡易版の取引所を完成させることができるでしょう。必ず役に立つはずです。

この記事は ZAN チーム(X アカウント @zan_team)の Fisher(X アカウント @yudao 1024)によって執筆されました。