はじめに
初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
代表的なゲームはクリプトスペルズというブロックチェーンゲームです。
今回は、あるトークンを保有していることで報酬を受け取れる統一された実装を提案しているERC4626についてまとめていきます!
以下にまとめられているものをChatGPTも使用しながら、翻訳・要約・補足してまとめていきます。
概要
この規格により、ERC20トークンを元にVault
(保管庫)とをトークン化する標準APIの実装が可能となります。
Vault
とは、仮想通貨やDeFiにおいて、資産を保管・管理し、特定の機能やサービスを提供する仕組みやプラットフォームのことです。
以下のような目的で使用されます。
- 資産の保管と管理。
- 貸出市場
- 収益を生み出す
- トークン化された資産の取引
この規格は、トークン化されたVault
に対して基本的な機能を提供し、トークンの預入と引き出し、残高の読み取りを行うことができます。
動機
トークン化されたVault
には標準化された実装がなく、さまざまな実装が存在します。
これには、貸出市場、アグリゲーター、イールドトークンなどが含まれます。
貸出市場(Lending Market)とは、借り手と貸し手が資金を提供し、それに対して利息を得る取引が行われる市場のことです。
借り手が資金を必要とする場合や、資産を預けて利息を得たい個人やプロトコルが貸し手として参加することができます。
これにより、資金を活用することができる一方、貸し手は預けた資産に対して利息収入を得ることができる仕組みになっています。
アグリゲーターとは、複数の異なるサービスや情報を一箇所に集約し、統合して提供するプラットフォームやサービスのことです。
異なる取引所の価格情報をまとめて表示するなど、異なるプロトコルやサービスを一元化して利用者に提供します。
イールドトークンとは、保有者に対して所有期間中に自動的に利息を生み出す仮想通貨トークンのことです。
通常のトークンや仮想通貨は、保有者に対して利息を支払うことはありません。
しかし、イールドトークンの場合保有者がトークンを保持しているだけで、定期的な間隔で利息がトークンの残高に追加される仕組みになっています。
ユーザーが特定のトークンを預けると、その預入量に応じた利息を定期的に受け取ることができます。
これにより、多くの標準に適合する必要があるプロトコルのアグリデーターやプラグイン層での統合が困難になり、各プロトコルが独自のアダプタを実装する必要が生じ、エラーの発生につながることで開発リソースが浪費されることになります。
トークン化されたVault
のための標準は、利回りを生むVault
に対する統合を簡単にし、より一貫性のある堅牢な実装パターンを作り出します。
仕様
すべてのERC4626のトークン化されたVault
は、ERC20を実装し、シェアを表現するために使用します。Vaultが非譲渡可能である場合、transferまたはtransferFromの呼び出しでリバート(失敗)する場合があります。EIP-20の操作であるbalanceOf、transfer、totalSupplyなどは、Vaultの「シェア」に対して動作し、Vaultの基礎となる保有物の一部を所有する権利を表します。
すべてのEIP-4626トークン化されたVaultは、EIP-20のオプションのメタデータ拡張を実装する必要があります。nameとsymbol関数は、基礎となるトークンの名前とシンボルをある程度反映するべきです。
EIP-4626トークン化されたVaultは、さまざまな統合でシェアを承認するユーザーエクスペリエンスを向上させるために、EIP-2612を実装することができます。
用語の定義:
asset
Vault
が管理するトークン。
BitcoinやEthereumなどを指します。
対応するERC20規格のコントラクトで定義されます。
share
Vault
をもとに発行されるトークン。
ユーザーが預け入れたasset
と同じ量のトークンを発行します。
Vault
の所有権を示し、mint(発行)/預入/引き出し/償還(redeem)に使用できます。
通常、アセットの量に応じて増減することがあり、Vault
が運用によって利益を生み出すとその利益がshare
保有者に分配されることがあります。
fee
ユーザーがVault
に対して支払う資産、またはshare
の量などの手数料を指します。
手数料は、預入、利回り(yield)、管理資産(AUM)、引き出し、またはVault
によって指定される他の何かのために存在します。
例えば、ユーザーがVault
に資産を預ける際に手数料を支払う必要がある場合や、Vault
から資産を引き出す際に手数料を支払う必要がある場合があります。
slippage
取引を行った際に、実際の取引価格と予想していた価格との間に生じる差異のことです。
特に、大きな取引量を行う場合や市場の流動性が低い場合に影響が出やすい現象です。
例えば、あるトークンを購入するために市場で注文を出したとします。
予想していた価格は1トークンあたり100ドルでしたが、実際に取引が成立した時点での価格は105ドルになっていたとします。
この場合、5ドルの価格差が生じ、この5ドルがスリッページとなります。
関数
asset
- name: asset
type: function
stateMutability: view
inputs: []
outputs:
- name: assetTokenAddress
type: address
Valut
で使用される元となるトークンのアドレスを取得する関数。
Valut
はトークン量の確認、預入、引き出しの際に元となるトークンを使用します。
Read関数であり、戻り値として「assetTokenAddress
」というアドレス型の変数が返されます。
このアドレスは、Valut
で使用されているERC20トークンコントラクトのアドレスを指します。
totalAssets
- name: totalAssets
type: function
stateMutability: view
inputs: []
outputs:
- name: totalManagedAssets
type: uint256
Vault
が「管理」しているasset
の総量を取得する関数。
総量には利回りによる複利の成長も含まれます。
また、Vault
によってasset
に対して課金される手数料も含まれます。
Read関数であり、出力として「totalManagedAssets
」という符号なし整数型(uint256)の変数が返されます。
この値は、Vault
が現在「管理」しているasset
の総量を表します。
convertToShares
- name: convertToShares
type: function
stateMutability: view
inputs:
- name: assets
type: uint256
outputs:
- name: shares
type: uint256
引数に渡されたasset
に対して、Vault
がどれだけのshare
と交換できるか計算する関数。
関数の実装にあたり、以下の条件があります。
- 計算において手数料などの費用は含まれない。
- 誰が呼び出しても同じ結果が得られる。
- 実際の交換時には、スリッページが起きない。
- 計算結果の小数点は切り捨てられる。
- 計算は「ユーザーごと」の価格(価格ごとのシェア数)を反映するものではなく、「平均ユーザー」価格(平均的なユーザーが交換時に期待できる価格)を反映する必要があります。
convertToAssets
- name: convertToAssets
type: function
stateMutability: view
inputs:
- name: shares
type: uint256
outputs:
- name: assets
type: uint256
引数に渡されたshare
に対して、Vault
がどれだけのasset
と交換できるか計算する関数。
関数の実装にあたり、以下の条件があります。
- 計算において手数料などの費用は含まれない。
- 誰が呼び出しても同じ結果が得られる。
- 実際の交換時には、スリッページが起きない。
- 計算結果の小数点は切り捨てられる。
- 計算は「ユーザーごと」の価格(価格ごとのシェア数)を反映するものではなく、「平均ユーザー」価格(平均的なユーザーが交換時に期待できる価格)を反映する必要があります。
maxDeposit
- name: maxDeposit
type: function
stateMutability: view
inputs:
- name: receiver
type: address
outputs:
- name: maxAssets
type: uint256
引数で指定された受取アドレス(receiver
)がVault
に預けることのできる、最大のasset
量を計算する関数。
関数の実装にあたり、以下の条件があります。
- 計算は、受取アドレスが無限の
asset
を持っていると仮定して行われ、実際の預入制限よりも低い値を返す。 - 預入が完全に無効になっている場合(一時的にでも)は、
0
を返す。 - 無制限の預入が許可されている場合は、
2 ** 256 - 1
を返す。
previewDeposit
- name: previewDeposit
type: function
stateMutability: view
inputs:
- name: assets
type: uint256
outputs:
- name: shares
type: uint256
オンチェーンまたはオフチェーンのユーザーが現在のオンチェーンの状況で預入をシミュレートする関数。
関数の実装にあたり、以下の条件があります。
- 同じトランザクション内で
deposit
関数を呼び出した場合と同じか、それ以上のshare
を返す。 -
maxDeposit
関数の制限などを考慮しない。 - 常に預入が受け入れられると仮定。
- 預入手数料も考慮する必要があるが、その他の
Vault
の制限によりリバートする可能性がある。
deposit
- name: deposit
type: function
stateMutability: nonpayable
inputs:
- name: assets
type: uint256
- name: receiver
type: address
outputs:
- name: shares
type: uint256
指定された受取アドレス(receiver
)に対して、引数で指定した量のasset
に対応するVault
のshare
を発行する関数。
関数の実装にあたり、以下の条件があります。
-
ERC20の
approve
/transferFrom
をサポートし、受け取ったasset
トークンを預けるフローをサポートする。 - すべての
asset
を預けられない場合(預入制限に達した、スリッページ、ユーザーが十分なトークンを承認していないなど)、revert
する。 - 実装によっては
Vault
のasset
に対して事前承認(approve
)が必要。
maxMint
- name: maxMint
type: function
stateMutability: view
inputs:
- name: receiver
type: address
outputs:
- name: maxShares
type: uint256
引数で渡された受取アドレス(receiver
)をもとに、Vault
から新たに発行できる最大のshare
量を計算する関数。
関数の実装にあたり、以下の条件があります。
- 計算は、受取アドレスが無限の
asset
を持っていると仮定して行われ、実際の預入制限よりも低い値を返す。 - 預入が完全に無効になっている場合(一時的にでも)は、
0
を返す。 - 無制限の預入が許可されている場合は、
2 ** 256 - 1
を返す。
previewMint
- name: previewMint
type: function
stateMutability: view
inputs:
- name: shares
type: uint256
outputs:
- name: assets
type: uint256
オンチェーンまたはオフチェーンのユーザーが現在のオンチェーンの状況で発行をシミュレートするための関数。
関数の実装にあたり、以下の条件があります。
- 同じトランザクション内で
mint
関数を呼び出した場合と同じか、それ以下のasset
量を返す。 -
maxMint
関数の制限などを考慮しない。 - 常に発行が受け入れられると仮定。
- 預入手数料も考慮する必要があるが、その他の
Vault
の制限によりリバートする可能性がある。
mint
- name: mint
type: function
stateMutability: nonpayable
inputs:
- name: shares
type: uint256
- name: receiver
type: address
outputs:
- name: assets
type: uint256
引数で渡された受取アドレス(receiver
)に対して、指定した量のshare
を新たに発行する関数。
関数の実装にあたり、以下の条件があります。
-
ERC20の
approve
/transferFrom
をサポートし、受け取ったasset
トークンを預けるフローをサポートする。 - すべての
asset
を預けられない場合(預入制限に達した、スリッページ、ユーザーが十分なトークンを承認していないなど)、revert
する。 - 実装によっては
Vault
のasset
に対して事前承認(approve
)が必要。
maxWithdraw
- name: maxWithdraw
type: function
stateMutability: view
inputs:
- name: owner
type: address
outputs:
- name: maxAssets
type: uint256
引数に渡されたオーナー(owner
)がVault
の保有量から引き出すことのできる最大のasset
量を計算する関数。
関数の実装にあたり、以下の条件があります。
- 計算は、実際の引き出し制限よりも低い値を返す。
- 引き出しが完全に無効になっている場合(一時的にでも)は、
0
を返す。
previewWithdraw
- name: previewWithdraw
type: function
stateMutability: view
inputs:
- name: assets
type: uint256
outputs:
- name: shares
type: uint256
オンチェーンまたはオフチェーンのユーザーが、現在のオンチェーンの状況で引き出しをシミュレートする関数。
関数の実装にあたり、以下の条件があります。
- 同じトランザクション内で
withdraw
関数を呼び出した場合と同じか、それ以下のshare
量を返す。 -
maxWithdraw
関数の制限などを考慮しない。 - 常に引き出しが受け入れられると仮定。
- 引き出し手数料も考慮する必要があるが、その他の
Vault
の制限によりリバートする可能性がある。
withdraw
- name: withdraw
type: function
stateMutability: nonpayable
inputs:
- name: assets
type: uint256
- name: receiver
type: address
- name: owner
type: address
outputs:
- name: shares
type: uint256
引数に渡されたオーナー(owner
)からshare
をバーン(破棄)し、指定した量のaseet
トークンを受取アドレス(receiver
)に送信する関数。
関数の実装にあたり、以下の条件があります。
-
Withdraw
イベントを発行する。 - 2つの引き出しフローをサポート。
- 1つはオーナーが
msg.sender
である場合にshare
を直接燃やすフロー。 - もう1つは、
msg.sender
がowner
のshare
に対してERC20承認を持つ場合にshare
を直接燃やすフロー。
- 1つはオーナーが
- 通常、実装によっては
Vault
に対して事前承認(approve
)が必要。
maxRedeem
- name: maxRedeem
type: function
stateMutability: view
inputs:
- name: owner
type: address
outputs:
- name: maxShares
type: uint256
引数に渡されたオーナー(owner
)がVault
の保有量から、取り戻すことのできる最大のshare
量を計算する関数。
関数の実装にあたり、以下の条件があります。
- 計算は、実際の取り戻し制限よりも低い値を返す。
- 取り戻しが完全に無効になっている場合(一時的にでも)は、
0
を返す。
previewRedeem
- name: previewRedeem
type: function
stateMutability: view
inputs:
- name: shares
type: uint256
outputs:
- name: assets
type: uint256
オンチェーンまたはオフチェーンのユーザーが、現在のオンチェーンの状況で取り戻しをシミュレートする関数。
関数の実装にあたり、以下の条件があります。
- 同じトランザクション内で
redeem
関数を呼び出した場合と同じか、それ以下のasset
量を返す。 -
maxRedeem
関数の制限などを考慮しない。 - 常に取り戻しが受け入れられると仮定。
- 引き出し手数料も考慮する必要があるが、その他の
Vault
の制限によりリバートする可能性がある。
redeem
- name: redeem
type: function
stateMutability: nonpayable
inputs:
- name: shares
type: uint256
- name: receiver
type: address
- name: owner
type: address
outputs:
- name: assets
type: uint256
引数に渡されたオーナー(owner
)からshare
を取り戻し、指定した量のasset
トークンを受取人(receiver
)に送信する関数。
関数の実装にあたり、以下の条件があります。
-
Withdraw
イベントを発行する。 - 2つの引き出しフローをサポート。
- 1つはオーナーが
msg.sender
である場合にshare
を直接取り戻すフロー。 - もう1つは、
msg.sender
がowner
のshare
に対してERC20承認を持つ場合にshare
を直接取り戻すフロー。
- 1つはオーナーが
- 通常、実装によっては
Vault
に対して事前承認(approve
)が必要。
Event
Deposit
- name: Deposit
type: event
inputs:
- name: sender
indexed: true
type: address
- name: owner
indexed: true
type: address
- name: assets
indexed: false
type: uint256
- name: shares
indexed: false
type: uint256
トークンがmint
関数やdeposit
関数を通じてVault
に預けられたときに発行されるイベント。
また、預けられたaseet
がshare
に交換され、それらのshare
がowner
に転送されたときに発行される。
-
sender
- 預けたユーザー(送信者)のアドレス。
-
owner
-
Vault
のオーナーのアドレス。
-
-
assets
- 預けられた
asset
の量。
- 預けられた
-
shares
- 交換された
share
の量。
- 交換された
Withdraw
- name: Withdraw
type: event
inputs:
- name: sender
indexed: true
type: address
- name: receiver
indexed: true
type: address
- name: owner
indexed: true
type: address
- name: assets
indexed: false
type: uint256
- name: shares
indexed: false
type: uint256
redeem
関数やwithdraw
関数を通じて、Vault
からshare
が引き出されたときに発行されるイベント。
また、引き出されたshare
はowner
が所有しているため、そのshare
がasset
に交換され受取アドレスに転送されたときに発行される。
-
sender
- 引き出したユーザー(送信者)のアドレス。
-
receiver
-
asset
を受け取ったユーザー(受取人)のアドレス。
-
-
owner
-
share
のowner
のアドレス。
-
-
assets
- 引き出された
asset
の量。
- 引き出された
-
shares
- 交換された
share
の量。
- 交換された
補足
インテグレーター向け最適化
Vault
インターフェースは、インテグレーター(システムやアプリケーションを統合する役割)を最適化するように設計されています。
つまり、Vault
を効果的に活用するための機能は完備している一方で、詳細な仕組みなどは意図的に指定されていません。
これにより、Vault
はオンチェーン上ではブラックボックスとして扱われ、使用前にオフチェーンで検討されることが期待されています。
ERC20の適用
ERC20とは、Ethereumトークンの標準的なインターフェース仕様を指すもので、トークンの承認や残高の計算などの実装詳細がVault
のshare
の計算に直接反映されることから、ERC20が適用されています。
これにより、Vault
はERC20を利用するすべてのユースケースと互換性があります。
mint関数の追加
mint
関数は対称性と機能の完全性のために含まれています。
多くの現行のshare
べースのVault
のユースケースでは、ユーザーが特定の数のshare
(mint
)を最適化するのではなく、特定の量のasset
トークン(deposit
)を最適化する傾向があります。
しかし、将来的には独自の有用なshare
表現を持つVault
戦略が考えられるため、mint
関数が追加されています。
convertTo関数の利用
convertTo
関数は、手数料などの操作に関連する詳細を考慮せずにおおよその見積もりを提供します。
これは、share
やasset
の平均値を必要とするフロントエンドやアプリケーションに役立ちますが、スリッページやその他の手数料を含む正確な値を必要とする場合には、それに対応するプレビュー関数が用意されています。
これらの関数は、deposit
やwithdraw
の制限を考慮しないようにするため、簡単に組み合わせることができるように、max
関数も提供されています。
互換性`
ERC4626は、**ERC20*標準と完全に互換性があり、他の標準との既知の互換性の問題はありません。
ERC4626を使用しないVault
のプロダクション実装の場合、ラッパーアダプターを開発して使用することができます。
参考実装
Solmate EIP-4626やVyper EIP-4626などで参考実装が提供されています。
これらは標準の最小限の実装であり、開発者が独自のロジックを簡単に挿入できるフックが用意されています。
セキュリティ考慮事項:
完全に許可された使用事例では、インターフェースに準拠しているが仕様に準拠していない悪意のある実装に引っかかる可能性があります。
Vault
のユーザーが預けれた資産を失う可能性のある実装をレビューすることをすべてのインテグレーターにお勧めします。
EOAアカウントのアクセスを直接サポートする場合、正確な出力量が得られない場合にトランザクションをリバートする他の手段がないため、スリッページ損失や予期しないdeposit
やwithdrawLimit
に対応するための関数呼び出しを追加することを検討する必要があります。
引用
Joey Santoro (@joeysantoro), t11s (@transmissions11), Jet Jadeja (@JetJadeja), Alberto Cuesta Cañada (@alcueca), Señor Doggo (@fubuloubu), "ERC-4626: Tokenized Vaults," Ethereum Improvement Proposals, no. 4626, December 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4626.
最後に
今回は「あるトークンを保有していることで報酬を受け取れる統一された実装を提案しているERC4626」についてまとめてきました!
いかがだったでしょうか?
実装については今後追記していきます。
質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!
採用強化中!
CryptoGamesでは一緒に働く仲間を大募集中です。
この記事で書いた自分の経験からもわかるように、裁量権を持って働くことができて一気に成長できる環境です。
「ブロックチェーンやWeb3、NFTに興味がある」、「スマートコントラクトの開発に携わりたい」など、少しでも興味を持っている方はまずはお話ししましょう!