はじめに
初めまして。 CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です! スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
代表的なゲームはクリプトスペルズというブロックチェーンゲームです。
今回はBunzzの新機能『DeCipher』を使用して、UniswapV3の「SwapRouter02」のコントラクトを見てみようと思います。
『DeCipher』はAIを使用してコントラクトのドキュメントを自動生成してくれるサービスです。
詳しい使い方に関しては以下の記事を参考にしてください!
この記事はあくまで個人が「DeCipher」を使用してみての記事になります。 間違った部分などもあるかと思いますが、その際はコメントなどで教えてください。
今回使用する『DeCipher』のリンクは以下になります。
Etherscanのリンクは以下になります。
概要
SwapRouter02コントラクトは、Uniswap分散型取引プロトコルの重要なコンポーネントです。 このコントラクトは、Uniswapプラットフォーム上でトークンを交換するためのルーターとして機能します。
SwapRouter02コントラクトの主な目的は、効率的で安全なトークンの交換をユーザー間で行うことです。 このコントラクトは、トレーダーが最小のスリッページで、最良の利用可能価格でトークンスワップを実行するためのシンプルで使いやすいインターフェースを提供します。
SwapRouter02コントラクトは、Uniswapという分散型取引プラットフォームにおいて、トークン同士の交換をスムーズかつ安全に行うための仕組みです。 このコントラクトは、ユーザーがトークンを交換する際に発生するスリッページ(価格のずれ)を最小限に抑え、最も良い取引価格でトークンをスワップできるようにサポートします。
例えば、あなたがトークンAを持っていて、それをトークンBに交換したいと考えた場合、SwapRouter02コントラクトはこの交換を円滑に実現します。 このコントラクトとやり取りすることで、トークンAからトークンBへの最適な取引経路を計算し、取引を実行することができます。
このコントラクトは、ユーザーにとって使いやすい方法で取引を行うことを重視しています。 つまり、複雑な計算や手続きを気にすることなく、シンプルな手順でトークンスワップを行うことができるようになっています。
また、SwapRouter02コントラクトはセキュリティ面でも配慮されています。 不正な操作や価格操作を防ぐための仕組みが組み込まれており、ユーザーが安全に取引を行えるようになっています。
SwapRouter02コントラクトは、以下の重要な役割を担っています。
トークンのスワップ
このコントラクトは、ユーザーがUniswapの流動性プールと連携して、トークンを別のトークンと交換できるようにします。 最適なスワップ経路を計算し、ユーザーの代わりに取引を実行します。
流動性の提供
このコントラクトは、ユーザーがトークンを預け入れることでUniswapプールに流動性を提供できるようにします。 これにより、トークンのスワップに十分な流動性が確保されます。
SwapRouter02コントラクトは、ユーザーがUniswapの流動性プールに参加して、トークンの流動性を提供する仕組みを提供します。 この流動性プールは、トークンを交換する際に必要なトークン量を提供するためのものであり、トークンスワップが円滑に行われるために重要な要素です。
具体的な例を考えてみましょう。 あなたがトークンAとトークンBの流動性プールにトークンを預け入れると仮定します。 これにより、そのプール内でトークンAとトークンBの供給量が増え、そのプールの総トークン量が増加します。
トークンの供給量が増えることで、ユーザーがトークンAをトークンBに交換しようとする際に、より多くのトークンが利用可能となります。 これにより、スワップ時の価格変動が少なくなり、スリッページ(価格のずれ)が低減されます。 結果として、トークンスワップを行うユーザーにとって、より良い取引価格でスワップが可能となります。
要するに、流動性プールへのトークンの提供は、トークンスワップの効率性を高めるための重要な手段です。ユーザーがトークンを流動性プールに預けることで、プラットフォーム全体の流動性が向上し、ユーザー同士のトークン交換が円滑かつ有利な条件で行えるようになるのです。
価格の計算
このコントラクトは、Uniswapプラットフォーム上のトークンの正確で最新の価格情報を提供します。 これにより、トレードを実行する際にユーザーが価格情報を元に判断ができます。
SwapRouter02コントラクトは、Uniswapプラットフォーム上で取引されているトークンの価格情報を提供する役割を果たしています。 これにより、ユーザーはトークンをスワップする際に、正確で最新の価格情報を手に入れて取引を行うことができます。
例えば、あるトークンAをトークンBに交換する場合を考えてみましょう。 ユーザーはSwapRouter02コントラクトを介してトークンの価格情報を取得し、トークンAとトークンBの現在の交換レートを確認できます。 これにより、ユーザーは実際の取引価格を把握し、どれくらいのトークンBがトークンAと交換されるかを理解することができます。
価格情報が正確で最新であることは、ユーザーがトークンスワップを行う際に重要です。 正確な価格情報が提供されることで、ユーザーは自分のトレード戦略を立てることができ、最良のタイミングで取引を実行できます。 また、価格の急激な変動や不正な操作などにも対応できます。
SwapRouter02コントラクトが提供する価格情報は、ユーザーが合理的な判断を下し、トークンスワップを行う際の信頼性と安全性を高める役割を果たしています。
セキュリティと効率性
SwapRouter02コントラクトは、トークンのスワップのセキュリティと効率性を確保するよう設計されています。 フロントランニングなどの操作を防ぐためのさまざまなメカニズムを組み込み、スリッページを最小化し流動性を最大化するために取引の経路を最適化します。
SwapRouter02コントラクトは、トークンスワップの際にセキュリティと効率性を確保するために様々な対策が講じられています。
まず、セキュリティ面では、フロントランニングや価格操作などの不正な操作を防ぐための仕組みが組み込まれています。 フロントランニングは、他の人よりも先に取引を行って利益を得ようとする行為を指します。SwapRouter02コントラクトは、このような不正な行為を防ぐために、トランザクションの順序を管理したり、適切な手数料を支払うことで優先度を設定するなどの方法を採用しています。
効率性に関しては、スリッページ(価格のずれ)を最小限に抑え、流動性を最大化するために取引の経路を最適化することが重要です。 SwapRouter02コントラクトは、複数の流動性プールやトークン経路の中から最適なものを選択し、最小のスリッページでトークンスワップが行えるようにします。 これにより、ユーザーが最良の取引価格でトークンを交換できる確率が高まります。
要するに、SwapRouter02コントラクトはセキュリティと効率性を両立させるために、不正行為を防ぐメカニズムを備え、トークンスワップの際に最適な経路を選択してスリッページを最小限に抑える役割を果たしています。 これにより、ユーザーは安全かつ効率的なトークンスワップを実現できるのです。
以上のように、SwapRouter02コントラクトはUniswapプラットフォーム上でスムーズで安全なトークンスワップを可能にする重要な役割を果たしています。 ユーザーに信頼性のある効率的な方法を提供し、同時に分散型取引エコシステムの誠実さと安定性を確保しています。
使い方
SwapRouter02コントラクトは、Uniswapの分散型取引所においてトークンのスワップを行うためのスマートコントラクトです。 このコントラクトはISwapRouter02インターフェースの実装であり、トークンのスワップに関するさまざまな機能を提供します。
コントラクトの目的
SwapRouter02コントラクトの目的は、ユーザーがUniswapの分散型取引所上でトークンをスワップできるようにすることです。 Uniswapの流動性プールを利用して、トークンスワップを便利かつ効率的に実行する手段を提供します。
手順
Uniswap上でトークンをスワップするためには、以下の手順が必要です。
- SwapRouter02コントラクトをデプロイします。
- コントラクトに対してトークンの使用権限を許可します。
exactInputまたはexactOutput関数を呼び出して、トークンスワップを実行します。
これにより、ユーザーは簡単にトークンスワップを行うことができ、Uniswapの流動性プールを活用してスムーズなトークンの交換が可能となります。
関数
SwapRouter02コントラクトは、以下のトークンスワップに関する関数を提供しています。
exactInput
入力量を指定してトークンスワップを実行します。 最小の出力量も指定します。 指定した入力量に対して、それに対応する最小の出力量を保証してトークンスワップを実行します。
exactOutput
出力量を指定してトークンスワップを実行します。 最大の入力量も指定します。 指定した出力量を達成するために必要な最大の入力量を計算し、それを使用してトークンスワップを行います。
refundETH
コントラクトに送信された余剰のETHを返金します。 トークンスワップの際に不要なETHが送信された場合に、それを返金するための関数です。
WETH9
WETH9トークンコントラクトのアドレスを返します。 この関数を使用して、WETH9(Wrapped Ether) トークンのアドレスを取得することができます。
使い方
正確な入力量でのスワップの実行。
- SwapRouter02コントラクトに、指定した入力トークン量を使用する許可を与えます。
exactInput関数を呼び出し、以下の引数を指定します。amountIn- 正確な入力トークン量。
amountOutMin- 最小の出力トークン量。
- スワップ時の最低限の出力量を保証します。
path- トークンスワップのためのトークンアドレスの配列。
- 指定したトークン同士のスワップ経路を示します。
to- 出力トークンを受け取るアドレス。
deadline- スワップが実行される期限。
正確な出力量でのスワップの実行。
- SwapRouter02コントラクトに、最大の入力トークン量を使用する許可を与えます。
- exactOutput関数を呼び出し、以下の引数を指定します。
amountOut- 正確な出力トークン量。
amountInMax- 最大の入力トークン量。
- スワップ時に使用される最大の入力量を示します。
path- トークンスワップのためのトークンアドレスの配列。
- 指定したトークン同士のスワップ経路を示します。
to- 出力トークンを受け取るアドレス。
deadline- スワップが実行される期限。
イベント
Swap
トークンスワップが実行されたことを示すイベント。
sender- スワップを開始したアドレス。
amount0In- トークン0の入力量。
amount1In- トークン1の入力量。
amount0Out- トークン0の出力量。
amount1Out- トークン1の出力量。
to- 出力トークンを受け取ったアドレス。
RefundETH
余剰のETHが返金されたことを示すイベント。
amount- 返金されたETHの量。
to- 返金を受け取ったアドレス。
関連ERC/EIP
- EIP-2612: ERC-2612 Permit
- EIP-3009: ERC-3009: Transfer With Authorization
- EIP-3156: Flashbots - MEV-protected mempool transactions
- EIP-3664: ChainID opcode
- EIP-7121: EIP-712 Typed Structured Data Gas Cost Reduction
- EIP-7122: EIP-712 Typed Structured Data Revocation
- EIP-7123: EIP-712 Typed Structured Data Validation
- EIP-7124: EIP-712 Typed Structured Data Validation Revocation
- EIP-7125: EIP-712 Typed Structured Data Validation Revocation Validation
- EIP-7126: EIP-712 Typed Structured Data Validation Revocation Validation Revocation
- EIP-7127: EIP-712 Typed Structured Data Validation Revocation Validation Revocation Validation
パラメータ
_factoryV2
V2ファクトリーコントラクトのアドレスです。
- V2ファクトリーコントラクトは、Uniswap V2のプールを作成および管理する役割を果たします。
- トークンの流動性プールを作成し、トークンのスワップを可能にします。
_factoryV3
V3ファクトリーコントラクトのアドレスです。
- V3ファクトリーコントラクトは、Uniswap V3のプールを作成および管理するためのコントラクトです。
- V3ではより精密な価格帯を設定し、トークンの効率的なスワップを実現します。
_positionManager
ポジションマネージャーコントラクトのアドレスです。
- ポジションマネージャーコントラクトは、Uniswap V3の流動性プール内のポジションを管理する役割を果たします。
- ユーザーはこのコントラクトを介してトークンの流動性を提供および管理することができます。
_WETH9
WETH9コントラクトのアドレスです。
- WETH9(Wrapped Ether) は、イーサリアムのネイティブトークンETHをERC20トークン化したバージョンです。
- ETHをトークンとして取り扱うことができるため、分散型取引所などでトークン同士のスワップを実行する際に便利に使用されます。
Uniswap V2およびUniswap V3のファクトリーコントラクトは、異なるバージョンのUniswapプロトコルをサポートするために存在します。 これらのファクトリーコントラクトは、それぞれのバージョンで特有の機能やプールを作成および管理するために使用されます。
使い分けに関しては、Uniswap V2ファクトリーコントラクトとUniswap V3ファクトリーコントラクトの違いとそれぞれの特徴を考慮して判断されます。
Uniswap V2ファクトリーコントラクト
バージョンの安定性 Uniswap V2は比較的古いバージョンで、多くのユーザーに利用されています。 既に確立されたトークンプールが存在しており、その流動性を利用することができます。
一般的な使用 一般的なトークンスワップや流動性の提供には、Uniswap V2が広く使用されています。 特にV2でのトークンプールが既に存在し、多くのユーザーが利用している場合、引き続きV2を使用することがあります。
Uniswap V3ファクトリーコントラクト
価格帯の最適化 Uniswap V3は価格帯の細かな設定が可能であり、より精密な価格帯を設定することができます。 これにより、ユーザーは特定の価格帯でのトークンスワップをより効率的に行うことができます。
集中的な流動性 Uniswap V3では、トークンの流動性プールを特定の価格帯に集中的に提供することが可能です。 これにより、トークンの特定の価格帯でのスワップが効率的に行えるようになります。
高度な機能 Uniswap V3では、様々なアクティビティを管理するための高度な機能が追加されています。 これにより、より細かいトークンスワップの制御や最適化が可能です。
使い分けの際には、トークンスワップの目的や要件に合わせて適切なバージョンを選択することが重要です。
コントラクト
SwapRouter02
SwapRouter02
contract SwapRouter02 is ISwapRouter02, V2SwapRouter, V3SwapRouter, ApproveAndCall, MulticallExtended, SelfPermit {
constructor(
address _factoryV2,
address factoryV3,
address _positionManager,
address _WETH9
) ImmutableState(_factoryV2, _positionManager) PeripheryImmutableState(factoryV3, _WETH9) {}
}ISwapRouter02
ISwapRouter02インターフェースを実装しています。 これにより、SwapRouter02コントラクトはISwapRouter02のメソッドを含むインターフェースの機能を提供します。
V2SwapRouter
Uniswap V2のスワップルーター機能を提供するコントラクトです。 V2SwapRouterによって、Uniswap V2のバージョン固有のスワップ関連機能が実装されています。
V3SwapRouter
Uniswap V3のスワップルーター機能を提供するコントラクトです。 V3SwapRouterによって、Uniswap V3のバージョン固有のスワップ関連機能が実装されています。
ApproveAndCall
ユーザーがコントラクトに対して承認と同時にコールを実行できる機能を提供します。 これにより、スワップ操作がより効率的に行えます。
ApproveAndCallは、ユーザーがトークンの承認と同時に特定の関数を実行できる仕組みを提供する機能です。 これにより、ユーザーがトークンの承認とトークン操作(スワップなど)を1つのトランザクションで実行できます。
通常、ユーザーがトークンを使用して何らかの操作(たとえばトークンのスワップ)を行う際には、以下の手順が必要です。
- トークンを操作するコントラクトに対して、ユーザーはトークンの承認を行います。 これにより、コントラクトが特定の量のトークンを使用できるようになります。
- 承認が行われたら、ユーザーは実際にトークンの操作(スワップなど)を行うために、別の関数を呼び出します。
ApproveAndCallは、これらの手順を1つのトランザクションで行います。 具体的には、ユーザーはトークンの承認と操作を同時に行うための関数を呼び出します。 この関数は以下のような流れで動作します:
- ユーザーがApproveAndCall関数を呼び出し、トークンの承認量や目的の操作に関する情報を提供します。
- コントラクトはユーザーのトークン承認を確認し、指定された操作を実行します。
これにより、ユーザーは複数のトランザクションを待つ必要なく、1つのトランザクションでトークンの承認と操作をまとめて行えるため、効率的にスワップなどの操作を行うことができます。
MulticallExtended
複数の関数コールを1つのトランザクションで実行できる機能を提供します。 複数の操作を1度のトランザクションでまとめて行えるため、ガスの節約に寄与します。
SelfPermit
イーサリアムのEIP-2612に基づいて、コントラクト自体に対してトークンの承認を行う機能を提供します。
SelfPermitは、イーサリアムのEIP-2612という提案に基づいて実装された機能で、コントラクト自体に対してトークンの承認を行うための機能を提供します。
通常、ユーザーがトークンを操作するコントラクトに対してトークンの承認を行う場合、ユーザーがトークンの所有者であることを確認するために、個別の手順が必要です。 しかし、EIP-2612はこのプロセスをより簡潔にし、トークン所有者の許可なしでコントラクトが自己に対してトークンの承認を行うための仕組みを提供するものです。 これにより、コントラクトが特定のトークンを直接操作するために必要な承認を、トークン所有者の介入なしで行うことができます。
具体的には、SelfPermitを使用すると、コントラクト自体が次のような手順を実行します。
- コントラクトが自己に対してトークンの承認を要求するためのデータを作成します。
- トークン所有者がそのデータに署名し、承認の意思を示します。
- コントラクトは、署名されたデータを使ってトークンの承認を自己に対して行います。
これにより、トークン所有者は一度の承認でコントラクトが特定のトークンを操作できるようにし、またセキュリティを向上させることができます。
SelfPermitは、コントラクトが自己に対してトークンの承認を行うための効率的でセキュアな方法を提供し、トークン操作のプロセスをスムーズにします。
constructor
コントラクトのコンストラクタ関数です。 Uniswap V2およびV3のファクトリーコントラクトのアドレス、ポジションマネージャーコントラクトのアドレス、WETH9トークンコントラクトのアドレスを受け取ります。 これらのアドレスはコントラクトの初期化時に設定され、スワップ操作などに使用されます。
V2SwapRouter
以下の記事に詳しくまとめています。
V3SwapRouter
以下の記事に詳しくまとめています。
ApproveAndCall
トークンの承認と同時に特定の関数を実行できる機能を提供する抽象コントラクト。 ユーザーがトークンの承認と関数の呼び出しを同時に行うための仕組みを提供します。 これにより、トークンの承認と操作を1つのトランザクションで結びつけることができ、操作の効率が向上します。
tryApprove
tryApprove
function tryApprove(address token, uint256 amount) private returns (bool) {
(bool success, bytes memory data) =
token.call(abi.encodeWithSelector(IERC20.approve.selector, positionManager, amount));
return success && (data.length == 0 || abi.decode(data, (bool)));
}概要
指定したトークンに対してPositionManagerへの承認を実行する関数。
引数
token- 承認するトークンのアドレス。
amount- 承認するトークンの量。
戻り値
bool- 承認が成功したかどうか。
getApprovalType
getApprovalType
function getApprovalType(address token, uint256 amount) external override returns (ApprovalType) {
// check existing approval
if (IERC20(token).allowance(address(this), positionManager) >= amount) return ApprovalType.NOT_REQUIRED;
// try type(uint256).max / type(uint256).max - 1
if (tryApprove(token, type(uint256).max)) return ApprovalType.MAX;
if (tryApprove(token, type(uint256).max - 1)) return ApprovalType.MAX_MINUS_ONE;
// set approval to 0 (must succeed)
require(tryApprove(token, 0));
// try type(uint256).max / type(uint256).max - 1
if (tryApprove(token, type(uint256).max)) return ApprovalType.ZERO_THEN_MAX;
if (tryApprove(token, type(uint256).max - 1)) return ApprovalType.ZERO_THEN_MAX_MINUS_ONE;
revert();
}概要 トークンの承認タイプを取得する関数。
詳細 特定のトークンに対して承認が必要かどうか、どのタイプの承認が必要かを判断します。 複数の承認方法を試みて、適切な承認タイプを返します。
引数
token- 承認対象のトークンのアドレス。
amount- 承認するトークンの量。
戻り値
ApprovalType- 承認タイプ(
NOT_REQUIRED、MAX、MAX_MINUS_ONE、ZERO_THEN_MAX、ZERO_THEN_MAX_MINUS_ONE)。
- 承認タイプ(
NOT_REQUIRED
既に十分な承認が行われているため、新たな承認は必要ないことを示す承認タイプです。
MAX
トークンの最大量を承認することを示す承認タイプです。
MAX_MINUS_ONE
トークンの最大量から1を引いた量を承認することを示す承認タイプです。 トークンの最大量をわずかに下回る量を承認することができます。
ZERO_THEN_MAX
最初にトークンの承認を0に設定し、その後最大量を承認することを示す承認タイプです。
一時的にトークンの使用を制限し、その後必要な操作に対して最大量を承認することができます。
ZERO_THEN_MAX_MINUS_ONE
最初にトークンの承認を0に設定し、その後最大量から1を引いた量を承認することを示す承認タイプです。
一時的にトークンの使用を制限し、その後必要な操作に対して最大量をわずかに下回る量を承認することができます。
approveMax
approveMax
function approveMax(address token) external payable override {
require(tryApprove(token, type(uint256).max));
}概要 最大のトークン承認を行う関数。
引数
token- 承認対象のトークンのアドレス。
approveMaxMinusOne
approveMaxMinusOne
function approveMaxMinusOne(address token) external payable override {
require(tryApprove(token, type(uint256).max - 1));
}概要 最大値から1引いたトークン量を承認する関数。
引数
token- 承認対象のトークンのアドレス。
approveZeroThenMax
approveZeroThenMax
function approveZeroThenMax(address token) external payable override {
require(tryApprove(token, 0));
require(tryApprove(token, type(uint256).max));
}概要
最初にトークンの承認を0に設定し、その後最大のトークン承認を行う関数。
引数
token- 承認対象のトークンのアドレス。
approveZeroThenMaxMinusOne
approveZeroThenMaxMinusOne
function approveZeroThenMaxMinusOne(address token) external payable override {
require(tryApprove(token, 0));
require(tryApprove(token, type(uint256).max - 1));
}概要
最初にトークンの承認を0に設定し、その後最大値から1引いた量のトークン承認を行う関数。
引数
token- 承認対象のトークンのアドレス。
callPositionManager
callPositionManager
function callPositionManager(bytes memory data) public payable override returns (bytes memory result) {
bool success;
(success, result) = positionManager.call(data);
if (!success) {
// Next 5 lines from https://ethereum.stackexchange.com/a/83577
if (result.length < 68) revert();
assembly {
result := add(result, 0x04)
}
revert(abi.decode(result, (string)));
}
}概要
PositionManagerコントラクトの特定の関数を呼び出す関数。
詳細
PositionManagerコントラクトの特定の関数を呼び出し、その結果を返します。
関数呼び出しに失敗した場合、エラーメッセージが表示されます。
引数
data- 呼び出す関数と引数のデータ。
戻り値
bytes memory- 関数の実行結果。
balanceOf
balanceOf
function balanceOf(address token) private view returns (uint256) {
return IERC20(token).balanceOf(address(this));
}概要 指定したトークンの残高を取得する関数。
引数
token- 残高を取得するトークンのアドレス。
戻り値
uint256- トークンの残高。
mint
mint
function mint(MintParams calldata params) external payable override returns (bytes memory result) {
return
callPositionManager(
abi.encodeWithSelector(
INonfungiblePositionManager.mint.selector,
INonfungiblePositionManager.MintParams({
token0: params.token0,
token1: params.token1,
fee: params.fee,
tickLower: params.tickLower,
tickUpper: params.tickUpper,
amount0Desired: balanceOf(params.token0),
amount1Desired: balanceOf(params.token1),
amount0Min: params.amount0Min,
amount1Min: params.amount1Min,
recipient: params.recipient,
deadline: type(uint256).max // deadline should be checked via multicall
})
)
);
}概要 Uniswap V3のポジションを新たに作成する関数。
詳細 この関数は、指定されたパラメータに基づいてUniswap V3のポジションを新たに作成します。 トークン0とトークン1の情報、手数料、トレーディングペアの価格範囲(ティック範囲)、希望のトークン0量とトークン1量、最小のトークン0量とトークン1量、受取人のアドレスなどが指定されます。トークンの承認は、前述の方法で行われます。
引数
params- 作成するポジションのパラメータが含まれる構造体。
戻り値
result- ポジション作成の結果を示すデータ。
increaseLiquidity
increaseLiquidity
function increaseLiquidity(IncreaseLiquidityParams calldata params)
external
payable
override
returns (bytes memory result)
{
return
callPositionManager(
abi.encodeWithSelector(
INonfungiblePositionManager.increaseLiquidity.selector,
INonfungiblePositionManager.IncreaseLiquidityParams({
tokenId: params.tokenId,
amount0Desired: balanceOf(params.token0),
amount1Desired: balanceOf(params.token1),
amount0Min: params.amount0Min,
amount1Min: params.amount1Min,
deadline: type(uint256).max // deadline should be checked via multicall
})
)
);
}概要 既存のUniswap V3のポジションに対して流動性を追加する関数。
詳細 指定されたパラメータに基づいて既存のUniswap V3のポジションに流動性を追加します。 ポジションのトークン0とトークン1の情報、追加するトークン0量とトークン1量、最小のトークン0量とトークン1量が指定されます。トークンの承認は、前述の方法で行われます。
引数
params- 流動性を追加するポジションのパラメータが含まれる構造体。
戻り値
result- 流動性追加の結果を示すデータ。
MulticallExtended
multicall
multicall
function multicall(uint256 deadline, bytes[] calldata data)
external
payable
override
checkDeadline(deadline)
returns (bytes[] memory)
{
return multicall(data);
}概要 複数のコールをまとめて実行する関数。
詳細 この関数は、指定されたデータの配列に含まれる複数のコールをまとめて実行します。 デッドライン(締め切り時刻)や前のブロックハッシュの確認も行われます。 実行結果は、それぞれのコールに対応する結果の配列として返されます。
引数
deadline- 実行のデッドライン(締め切り時刻)。
data- 実行するコールのデータの配列。
戻り値
bytes[] memory- 各コールの実行結果を示すデータの配列。
multicall (previousBlockhash version)
multicall
function multicall(bytes32 previousBlockhash, bytes[] calldata data)
external
payable
override
checkPreviousBlockhash(previousBlockhash)
returns (bytes[] memory)
{
return multicall(data);
}概要 複数のコールをまとめて実行する関数。
詳細 この関数は、指定されたデータの配列に含まれる複数のコールをまとめて実行します。 この時、前のブロックハッシュの確認が行われます。 実行結果は、それぞれのコールに対応する結果の配列として返されます。
引数
previousBlockhash- 実行の前のブロックハッシュ。
data- 実行するコールのデータの配列。
戻り値
bytes[] memory- 各コールの実行結果を示すデータの配列。
2つのmulticall関数の違いをわかりやすく説明します。
multicall
概要 指定されたデータの配列に含まれる複数のコールをまとめて実行する関数。
適用条件 デッドライン(締め切り時刻)が指定されている。
詳細 指定されたデータの配列に含まれる複数のコールをまとめて実行します。 デッドラインのチェックが行われ、締め切り時刻を過ぎていないことが確認されます。 実行結果は、それぞれのコールに対応する結果の配列として返されます。
multicall (previousBlockhash version)
概要 指定されたデータの配列に含まれる複数のコールをまとめて実行する関数。
適用条件 前のブロックハッシュが指定されてい。
詳細 指定されたデータの配列に含まれる複数のコールをまとめて実行します。 前のブロックハッシュのチェックが行われ、実行時の前のブロックハッシュと指定された前のブロックハッシュが一致することが確認されます。 実行結果は、それぞれのコールに対応する結果の配列として返されます。
2つの関数の違いは、実行の制約条件に関連しています。 1つ目の関数はデッドラインを、2つ目の関数は前のブロックハッシュを確認することで、安全な実行を保証します。
ImmutableState
factoryV2
factoryV2
address public immutable override factoryV2;概要 Uniswap V2のファクトリーコントラクトのアドレスを格納する変数。
詳細 Uniswap V2のファクトリーコントラクトのアドレスを不変で公開可能な形で変数に格納しています。 Uniswap V2のファクトリーコントラクトは、トークンの取引を管理するための基盤となるコントラクトです。
positionManager
positionManager
address public immutable override positionManager;概要 ポジションマネージャーコントラクトのアドレスを格納する変数。
詳細 この変数はポジションマネージャーコントラクトのアドレスを不変で公開可能な形で変数に格納しています。 ポジションマネージャーコントラクトは、Uniswap V3のポジションを管理するためのコントラクトです。
constructor
constructor
constructor(address _factoryV2, address _positionManager) {
factoryV2 = _factoryV2;
positionManager = _positionManager;
}概要 コントラクトのコンストラクタは初期化時に実行される関数で、変数の初期値を設定します。
詳細
このコンストラクタはfactoryV2とpositionManager変数を初期化します。
コントラクトをデプロイする際に提供されたアドレスがそれぞれの変数に設定されます。
引数:
_factoryV2- Uniswap V2のファクトリーコントラクトのアドレス。
_positionManager- ポジションマネージャーコントラクトのアドレス。
PeripheryPaymentsWithFeeExtended
unwrapWETH9WithFee
unwrapWETH9WithFee
function unwrapWETH9WithFee(
uint256 amountMinimum,
uint256 feeBips,
address feeRecipient
) external payable override {
unwrapWETH9WithFee(amountMinimum, msg.sender, feeBips, feeRecipient);
}概要 WETH9トークンを手数料付きでアンラップする関数。
詳細 指定された最小量のWETH9トークンをアンラップします。 手数料が差し引かれた後、残りのトークンが指定されたアドレスに送信されます。
引数:
amountMinimum- 最小アンラップ量として要求されるWETH9トークンの量。
feeBips- 手数料として支払われる基準ポイントの量。
feeRecipient- 手数料の受取先となるアドレス。
戻り値: なし
sweepTokenWithFee
sweepTokenWithFee
function sweepTokenWithFee(
address token,
uint256 amountMinimum,
uint256 feeBips,
address feeRecipient
) external payable override {
sweepTokenWithFee(token, amountMinimum, msg.sender, feeBips, feeRecipient);
}概要 指定されたトークンを手数料付きでスウィープする関数。
詳細 指定されたトークンをアンラップして手数料が差し引かれた後、残りのトークンが指定されたアドレスに送信されます。
引数:
token: スウィープされる対象のトークンのアドレス。amountMinimum: 最小スウィープ量として要求されるトークンの量。feeBips: 手数料として支払われる基準ポイントの量。feeRecipient: 手数料の受取先となるアドレス。
戻り値: なし
PeripheryPaymentsWithFeeExtendedコントラクトは、手数料を含むトークンのアンラップ(ETHに変換する操作)やトークンのスウィープ(指定されたアドレスにトークンを送る操作)を拡張した機能を提供するものです。 これを具体的な例を用いて説明します。
例えば、あるDeFi向けのプラットフォームがあり、ユーザーがそのプラットフォーム上でトークンのアンラップやスウィープを行いたい場合を考えてみましょう。 このプラットフォームでは、ユーザーがトークンをETHにアンラップしたり、特定のトークンを指定のアドレスに送る操作を行う際に手数料が発生することがあります。
この場合、PeripheryPaymentsWithFeeExtendedコントラクトが使用される可能性があります。 ユーザーはこのコントラクトを介して、手数料を含むトークンのアンラップやスウィープを行うことができます。以下に具体的な使用例を示します。
トークンのアンラップ(Unwrap)
ユーザーがプラットフォーム上でトークンをETHにアンラップしたい場合、次のように呼び出すことができます。
contractInstance.unwrapWETH9WithFee(amountMinimum, feeBips, feeRecipient);ここで、amountMinimumはアンラップする最小量、feeBipsは手数料の割合、feeRecipientは手数料を受け取るアドレスです。
トークンのスウィープ(Sweep)
ユーザーが特定のトークンを指定のアドレスに送る操作を行いたい場合、次のように呼び出すことができます。
contractInstance.sweepTokenWithFee(token, amountMinimum, feeBips, feeRecipient);ここで、tokenは送信するトークンのアドレス、amountMinimumは送信する最小量、feeBipsは手数料の割合、feeRecipientは手数料を受け取るアドレスです。
PeripheryPaymentsWithFeeExtendedコントラクトユーザーが手数料を考慮してトークンのアンラップやスウィープを行う場面で利用され、手数料の支払いや受取り先の指定を簡便に行うためのものです。
PeripheryPaymentsExtended
unwrapWETH9
unwrapWETH9
function unwrapWETH9(uint256 amountMinimum) external payable override {
unwrapWETH9(amountMinimum, msg.sender);
}概要 ETHに変換されたWETH(Wrapped Ether) トークンをアンラップし、元のETHに戻す関数。
詳細 ユーザーがWETHトークンをアンラップし、最低限必要な量のETHを受け取るために使用されます。 アンラップするWETHの最小量を指定する必要があります。
引数:
amountMinimum- アンラップする最小のWETH量。
wrapETH
wrapETH
function wrapETH(uint256 value) external payable override {
IWETH9(WETH9).deposit{value: value}();
}概要 ETHをWETH(Wrapped Ether) トークンに変換する関数。
引数:
value- 変換するETHの量。
sweepToken
sweepToken
function sweepToken(address token, uint256 amountMinimum) external payable override {
sweepToken(token, amountMinimum, msg.sender);
}概要 指定されたトークンを指定のアドレスに送る関数。
詳細 ユーザーが指定されたトークンを指定されたアドレスに送るために使用されます。 送信するトークンのアドレスと送信する最小量を指定する必要があります。
引数:
token- 送信するトークンのアドレス。
amountMinimum- 送信する最小のトークン量。
pull
pull
function pull(address token, uint256 value) external payable override {
TransferHelper.safeTransferFrom(token, msg.sender, address(this), value);
}概要 他のアドレスからトークンを受け取る関数。
詳細 他のアドレスからトークンを受け取るために使用されます。 受け取るトークンのアドレスと受け取る量を指定する必要があります。
引数:
token- 受け取るトークンのアドレス。
value- 受け取るトークンの量。
OracleSlippage
getBlockStartingAndCurrentTick
getBlockStartingAndCurrentTick
function getBlockStartingAndCurrentTick(IUniswapV3Pool pool)
internal
view
returns (int24 blockStartingTick, int24 currentTick)
{
uint16 observationIndex;
uint16 observationCardinality;
(, currentTick, observationIndex, observationCardinality, , , ) = pool.slot0();
// 2 observations are needed to reliably calculate the block starting tick
require(observationCardinality > 1, 'NEO');
// If the latest observation occurred in the past, then no tick-changing trades have happened in this block
// therefore the tick in `slot0` is the same as at the beginning of the current block.
// We don't need to check if this observation is initialized - it is guaranteed to be.
(uint32 observationTimestamp, int56 tickCumulative, , ) = pool.observations(observationIndex);
if (observationTimestamp != uint32(_blockTimestamp())) {
blockStartingTick = currentTick;
} else {
uint256 prevIndex = (uint256(observationIndex) + observationCardinality - 1) % observationCardinality;
(uint32 prevObservationTimestamp, int56 prevTickCumulative, , bool prevInitialized) =
pool.observations(prevIndex);
require(prevInitialized, 'ONI');
uint32 delta = observationTimestamp - prevObservationTimestamp;
blockStartingTick = int24((tickCumulative - prevTickCumulative) / delta);
}
}概要 現在のブロックの開始時点と現在の時点のティック(価格の変動幅)を返す関数。
詳細 指定されたUniswap V3プールにおける現在のティックと開始時点のティックを取得します。 これにより、ブロック内での価格変動を把握し、スリッページを評価するのに役立ちます。
引数:
pool- 操作対象のUniswap V3プールのアドレス。
戻り値:
blockStartingTick- ブロックの開始時点のティック(価格の変動幅)。
currentTick: 現在のティック(価格の変動幅)。
getPoolAddress
getPoolAddress
function getPoolAddress(
address tokenA,
address tokenB,
uint24 fee
) internal view virtual returns (IUniswapV3Pool pool) {
pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, PoolAddress.getPoolKey(tokenA, tokenB, fee)));
}概要 プールのアドレスを取得する関数。
詳細 トークンA、トークンB、および手数料率を指定してプールのアドレスを取得します。
引数:
tokenA- トークンAのアドレス。
tokenB- トークンBのアドレス。
fee- プールの手数料率。
戻り値:
pool- 取得されたプールのインターフェース。
getSyntheticTicks
getSyntheticTicks
function getSyntheticTicks(bytes memory path, uint32 secondsAgo)
internal
view
returns (int256 syntheticAverageTick, int256 syntheticCurrentTick)
{
bool lowerTicksAreWorse;
uint256 numPools = path.numPools();
address previousTokenIn;
for (uint256 i = 0; i < numPools; i++) {
// this assumes the path is sorted in swap order
(address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool();
IUniswapV3Pool pool = getPoolAddress(tokenIn, tokenOut, fee);
// get the average and current ticks for the current pool
int256 averageTick;
int256 currentTick;
if (secondsAgo == 0) {
// we optimize for the secondsAgo == 0 case, i.e. since the beginning of the block
(averageTick, currentTick) = getBlockStartingAndCurrentTick(pool);
} else {
(averageTick, ) = OracleLibrary.consult(address(pool), secondsAgo);
(, currentTick, , , , , ) = IUniswapV3Pool(pool).slot0();
}
if (i == numPools - 1) {
// if we're here, this is the last pool in the path, meaning tokenOut represents the
// destination token. so, if tokenIn < tokenOut, then tokenIn is token0 of the last pool,
// meaning the current running ticks are going to represent tokenOut/tokenIn prices.
// so, the lower these prices get, the worse of a price the swap will get
lowerTicksAreWorse = tokenIn < tokenOut;
} else {
// if we're here, we need to iterate over the next pool in the path
path = path.skipToken();
previousTokenIn = tokenIn;
}
// accumulate the ticks derived from the current pool into the running synthetic ticks,
// ensuring that intermediate tokens "cancel out"
bool add = (i == 0) || (previousTokenIn < tokenIn ? tokenIn < tokenOut : tokenOut < tokenIn);
if (add) {
syntheticAverageTick += averageTick;
syntheticCurrentTick += currentTick;
} else {
syntheticAverageTick -= averageTick;
syntheticCurrentTick -= currentTick;
}
}
// flip the sign of the ticks if necessary, to ensure that the lower ticks are always worse
if (!lowerTicksAreWorse) {
syntheticAverageTick *= -1;
syntheticCurrentTick *= -1;
}
}概要 指定されたパスと時間から、合成的な時加重平均ティックを取得する関数。
詳細 指定されたパス(トークン間の経路)と時間から、指定時間内の合成的な時加重平均ティックを取得します。 また、現在のティックも取得します。 この合成ティックは、スリッページの評価に使用されます。
引数:
path- トークン間の経路を示すバイト列。
secondsAgo- 遡る時間(秒単位)。
戻り値:
syntheticAverageTick- 合成的な時加重平均ティック。
syntheticCurrentTick- 現在のティック。
合成的な時加重平均ティック
例えば、以下のようなトークン間の経路(パス)が考えられるとします。
トークンA → トークンB → トークンCこのパスは、トークンAをトークンBにスワップし、その後トークンBをトークンCにスワップする経路を表しています。 このとき、各スワップにおけるティックの平均値と現在のティックを求めることで、合成的な時加重平均ティックを計算できます。
例えば、トークンA → トークンBのスワップでの平均ティックが100、現在のティックが105であり、トークンB → トークンCのスワップでの平均ティックが110、現在のティックが115とします。
この場合、各スワップの重みを考慮して合成的な時加重平均ティックを計算します。
トークンA → トークンBのスワップがトークンB → トークンCのスワップよりも重要である場合、合成的な時加重平均ティックは(100 + 110) / 2 = 105となります。
逆に、トークンB → トークンCのスワップがより重要である場合、合成的な時加重平均ティックは (105 + 115) / 2 = 110となります。
このようにして、複数のスワップパスにおけるティックの影響を考慮しながら、合成的な時加重平均ティックを計算することができます。 合成的な時加重平均ティックは、スワップにおける価格変動を総合的に評価するために使用され、スリッページの予測や取引の最適化に役立ちます。
toInt24
toInt24
function toInt24(int256 y) private pure returns (int24 z) {
require((z = int24(y)) == y);
}概要
int256をint24にキャストする関数。
詳細
int256をint24にキャストします。
オーバーフローやアンダーフローが発生した場合、リバート(エラーを発生)します。
引数:
y- キャストされる
int256の値。
- キャストされる
戻り値:
z- キャストされた
int24の値。
- キャストされた
toInt24関数は、int256型の値をint24型にキャストする際に使用されます。
int24型は-2^23から2^23-1までの整数値を表現するための型であり、一方でint256型はより広い範囲の整数値を表現できる型です。
キャストを行う際には、値がint24型の範囲内に収まるかどうかを確認する必要があります。
具体例を挙げて、toInt24関数の使用場面を説明します。
例えば、以下のような状況を考えてみましょう。
function calculateTotal(uint256[] memory values) external pure returns (int24) {
int256 total = 0;
for (uint256 i = 0; i < values.length; i++) {
total += int256(values[i]);
}
return toInt24(total);
}この関数は、valuesという配列に含まれる整数値の合計を計算し、その合計値をint24型で返すものです。
例えば、valuesが[100, 200, -150]という配列の場合、合計は150になります。
しかし、この合計値はint256型の範囲内ですが、int24型にキャストするためには範囲が-2^23から2^23-1までに収まっている必要があります。
そのため、キャストを行う前にtoInt24関数を使用して確認します。
もし合計値がこの範囲外になる場合、関数はリバートしてエラーを発生させます。
このように、toInt24関数は整数値のキャストを行う際に、値が指定の範囲内に収まることを確認するために使用されます。
これにより、範囲外の値が不正な結果をもたらすことを防ぐことができます。
getSyntheticTicks
getSyntheticTicks
function getSyntheticTicks(
bytes[] memory paths,
uint128[] memory amounts,
uint32 secondsAgo
) internal view returns (int256 averageSyntheticAverageTick, int256 averageSyntheticCurrentTick) {
require(paths.length == amounts.length);
OracleLibrary.WeightedTickData[] memory weightedSyntheticAverageTicks =
new OracleLibrary.WeightedTickData[](paths.length);
OracleLibrary.WeightedTickData[] memory weightedSyntheticCurrentTicks =
new OracleLibrary.WeightedTickData[](paths.length);
for (uint256 i = 0; i < paths.length; i++) {
(int256 syntheticAverageTick, int256 syntheticCurrentTick) = getSyntheticTicks(paths[i], secondsAgo);
weightedSyntheticAverageTicks[i].tick = toInt24(syntheticAverageTick);
weightedSyntheticCurrentTicks[i].tick = toInt24(syntheticCurrentTick);
weightedSyntheticAverageTicks[i].weight = amounts[i];
weightedSyntheticCurrentTicks[i].weight = amounts[i];
}
averageSyntheticAverageTick = OracleLibrary.getWeightedArithmeticMeanTick(weightedSyntheticAverageTicks);
averageSyntheticCurrentTick = OracleLibrary.getWeightedArithmeticMeanTick(weightedSyntheticCurrentTicks);
}概要 複数のパスにおける合成的な時加重平均ティックを取得し、それらのティックの加重平均を計算する関数。
詳細 複数のパスと指定された時間内でのティックの合成的な時加重平均を取得し、それらのティックの加重平均を計算します。 異なる経路でのスリッページを評価する際に使用されます。 すべてのパスは同じトークンで始まり終わる必要があります。
引数:
paths- 複数のトークン間の経路を示すバイト列の配列。
amounts- 各経路に対する入力量(量の配列)。
secondsAgo- 遡る時間(秒単位)。
戻り値:
averageSyntheticAverageTick- 加重平均された合成的な時加重平均ティック。
averageSyntheticCurrentTick- 加重平均された現在のティック。
checkOracleSlippage
checkOracleSlippage
function checkOracleSlippage(
bytes memory path,
uint24 maximumTickDivergence,
uint32 secondsAgo
) external view override {
(int256 syntheticAverageTick, int256 syntheticCurrentTick) = getSyntheticTicks(path, secondsAgo);
require(syntheticAverageTick - syntheticCurrentTick < maximumTickDivergence, 'TD');
}概要 スリッページを評価し、指定されたティックの最大乖離を超えるかどうかを確認する関数。
詳細 指定されたパスと指定された時間内でのティックの最大乖離を評価し、最大乖離を超える場合にエラーを発生させます。 スリッページを制御するために使用されます。
引数:
path- トークン間の経路を示すバイト列。
maximumTickDivergence- 最大ティック乖離。
離値。
secondsAgo- 遡る時間(秒単位)。
checkOracleSlippage関数は、指定したパスにおけるオラクルのティックの変動が、指定された最大ティックの偏差以下であるかどうかを確認するために使用されます。
ここでは、具体的な例を通じてその動作を説明します。
例えば、以下のような関数があるとします。
function tradeWithSlippage(bytes memory path, uint24 maximumTickDivergence, uint32 secondsAgo) external view {
OracleSlippage oracle = OracleSlippage(addressOfOracle); // 仮のアドレスです
// オラクルスリッページをチェック
oracle.checkOracleSlippage(path, maximumTickDivergence, secondsAgo);
// ここにトレードのロジックを記述する
// ...
}この関数は、指定したパス(トークンの経路)、最大ティックの偏差、および過去の時間からの秒数を受け取ります。
関数内で、OracleSlippageコントラクトのインスタンスを作成し、そのインスタンスのcheckOracleSlippage関数を呼び出しています。
この関数の目的は、指定したパスにおけるオラクルのティックの変動が、指定された最大ティックの偏差以下であることを確認することです。 これにより、トレードを実行する前に、ティックの変動が許容範囲内であるかどうかを確認することができます。
具体的な例を考えてみましょう。
pathが[TokenA, TokenB]とし、maximumTickDivergenceが100、secondsAgoが3600(1時間前のデータを使用)の場合を考えます。
この関数は、1時間前のデータを使用して指定したパスのティック変動を確認し、最大ティックの偏差が100以下であるかどうかをチェックします。
もし指定された偏差を超えてティックが変動している場合、関数はエラーをスローしてトレードを中止します。
このように、checkOracleSlippage関数は、トレードの前にオラクルのティックの変動を確認する重要な手段です。
ティックの変動が大きすぎる場合、トレードの実行が予想外の結果をもたらす可能性があるため、この関数を使用してリスクを管理することができます。
checkOracleSlippage
checkOracleSlippage
function checkOracleSlippage(
bytes[] memory paths,
uint128[] memory amounts,
uint24 maximumTickDivergence,
uint32 secondsAgo
) external view override {
(int256 averageSyntheticAverageTick, int256 averageSyntheticCurrentTick) =
getSyntheticTicks(paths, amounts, secondsAgo);
require(averageSyntheticAverageTick - averageSyntheticCurrentTick < maximumTickDivergence, 'TD');
}概要 複数のパスにおけるスリッページを評価し、指定されたティックの最大乖離を超えるかどうかを確認する関数。
詳細 複数のパスと指定された時間内でのティックの最大乖離を評価し、最大乖離を超える場合にエラーを発生させます。 異なる経路でのスリッページを評価する際に使用されます。
引数:
paths- 複数のトークン間の経路を示すバイト列の配列。
amounts- 各経路に対する入力量(量の配列)。
maximumTickDivergence- 最大ティック乖離値。
secondsAgo- 遡る時間(秒単位)。
PeripheryValidationExtended
checkPreviousBlockhash
checkPreviousBlockhash
modifier checkPreviousBlockhash(bytes32 previousBlockhash) {
require(blockhash(block.number - 1) == previousBlockhash, 'Blockhash');
_;
}checkPreviousBlockhashは、関数が実行される前に特定の条件をチェックし、条件が満たされていれば関数の実行を続行するという制御構造を提供します。
具体例を交えて説明します。
abstract contract PeripheryValidationExtended is PeripheryValidation {
modifier checkPreviousBlockhash(bytes32 previousBlockhash) {
require(blockhash(block.number - 1) == previousBlockhash, 'Blockhash');
_; // チェックを通過したら関数本体を実行
}
}この checkPreviousBlockhash修飾子は、関数が呼び出される前に前のブロックのハッシュが指定されたハッシュと一致するかどうかを確認します。
もし一致しない場合、関数の呼び出しは失敗し、エラーメッセージ'Blockhash' と共に失敗の理由を示します。
例を挙げてみましょう。
contract MyContract is PeripheryValidationExtended {
bytes32 private previousBlockhash;
function setPreviousBlockhash(bytes32 _blockhash) external {
previousBlockhash = _blockhash;
}
function performAction() external checkPreviousBlockhash(previousBlockhash) {
// ブロックハッシュのチェックをパスした場合にのみ実行される
// ...
}
}上記のMyContractコントラクトでは、performAction関数が checkPreviousBlockhash(previousBlockhash)修飾子を使用しています。
このmodifierは、指定されたpreviousBlockhashと前のブロックのハッシュが一致するかを確認し、一致していれば関数を実行します。
一致しない場合、関数は実行されずにエラーが発生します。
これにより、前のブロックのハッシュが特定の値と一致することが必要な場面で、関数の実行条件を確認することができるようになります。
イベント
特になし。
コード
インターフェース
SwapRouter02/contracts/interfaces
ベースコントラクト
SwapRouter02/contracts/base
ライブラリ
SwapRouter02/contracts/libraries
uniswap core
SwapRouter02/@uniswap
openzeppelin contract
- SwapRouter02/@openzeppelin`
最後に
今回の記事では、Bunzzの新機能『DeCipher』を使用して、UniswapV3の「SwapRouter02」のコントラクトを見てきました。 いかがだったでしょうか? 今後も特定のNFTやコントラクトをピックアップしてまとめて行きたいと思います。
普段はブログやQiitaでブロックチェーンやAIに関する記事を挙げているので、よければ見ていってください!




