はじめに

初めまして。 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

yaml
- name: asset
  type: function
  stateMutability: view

  inputs: []

  outputs:
    - name: assetTokenAddress
      type: address

Valutで使用される元となるトークンのアドレスを取得する関数。 Valutはトークン量の確認、預入、引き出しの際に元となるトークンを使用します。 Read関数であり、戻り値として「assetTokenAddress」というアドレス型の変数が返されます。 このアドレスは、Valutで使用されているERC20トークンコントラクトのアドレスを指します。

totalAssets

yaml
- name: totalAssets
  type: function
  stateMutability: view

  inputs: []

  outputs:
    - name: totalManagedAssets
      type: uint256

Vaultが「管理」しているassetの総量を取得する関数。 総量には利回りによる複利の成長も含まれます。 また、Vaultによってassetに対して課金される手数料も含まれます。 Read関数であり、出力として「totalManagedAssets」という符号なし整数型(uint256)の変数が返されます。 この値は、Vaultが現在「管理」しているassetの総量を表します。

convertToShares

yaml
- name: convertToShares
  type: function
  stateMutability: view

  inputs:
    - name: assets
      type: uint256

  outputs:
    - name: shares
      type: uint256

引数に渡されたassetに対して、Vaultがどれだけのshareと交換できるか計算する関数。

関数の実装にあたり、以下の条件があります。

  • 計算において手数料などの費用は含まれない。
  • 誰が呼び出しても同じ結果が得られる。
  • 実際の交換時には、スリッページが起きない。
  • 計算結果の小数点は切り捨てられる。
  • 計算は「ユーザーごと」の価格(価格ごとのシェア数)を反映するものではなく、「平均ユーザー」価格(平均的なユーザーが交換時に期待できる価格)を反映する必要があります。

convertToAssets

yaml
- name: convertToAssets
  type: function
  stateMutability: view

  inputs:
    - name: shares
      type: uint256

  outputs:
    - name: assets
      type: uint256

引数に渡されたshareに対して、Vaultがどれだけのassetと交換できるか計算する関数。

関数の実装にあたり、以下の条件があります。

  • 計算において手数料などの費用は含まれない。
  • 誰が呼び出しても同じ結果が得られる。
  • 実際の交換時には、スリッページが起きない。
  • 計算結果の小数点は切り捨てられる。
  • 計算は「ユーザーごと」の価格(価格ごとのシェア数)を反映するものではなく、「平均ユーザー」価格(平均的なユーザーが交換時に期待できる価格)を反映する必要があります。

maxDeposit

yaml
- 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

yaml
- name: previewDeposit
  type: function
  stateMutability: view

  inputs:
    - name: assets
      type: uint256

  outputs:
    - name: shares
      type: uint256

オンチェーンまたはオフチェーンのユーザーが現在のオンチェーンの状況で預入をシミュレートする関数。 関数の実装にあたり、以下の条件があります。

  • 同じトランザクション内でdeposit関数を呼び出した場合と同じか、それ以上のshareを返す。
  • maxDeposit関数の制限などを考慮しない。
  • 常に預入が受け入れられると仮定。
  • 預入手数料も考慮する必要があるが、その他のVaultの制限によりリバートする可能性がある。

deposit

yaml
- 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

yaml
- 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

yaml
- name: previewMint
  type: function
  stateMutability: view

  inputs:
    - name: shares
      type: uint256

  outputs:
    - name: assets
      type: uint256

オンチェーンまたはオフチェーンのユーザーが現在のオンチェーンの状況で発行をシミュレートするための関数。

関数の実装にあたり、以下の条件があります。

  • 同じトランザクション内でmint関数を呼び出した場合と同じか、それ以下のasset量を返す。
  • maxMint関数の制限などを考慮しない。
  • 常に発行が受け入れられると仮定。
  • 預入手数料も考慮する必要があるが、その他のVaultの制限によりリバートする可能性がある。

mint

yaml
- 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

yaml
- name: maxWithdraw
  type: function
  stateMutability: view

  inputs:
    - name: owner
      type: address

  outputs:
    - name: maxAssets
      type: uint256

引数に渡されたオーナー(owner)がVaultの保有量から引き出すことのできる最大のasset量を計算する関数。

関数の実装にあたり、以下の条件があります。

  • 計算は、実際の引き出し制限よりも低い値を返す。
  • 引き出しが完全に無効になっている場合(一時的にでも)は、0を返す。

previewWithdraw

yaml
- name: previewWithdraw
  type: function
  stateMutability: view

  inputs:
    - name: assets
      type: uint256

  outputs:
    - name: shares
      type: uint256

オンチェーンまたはオフチェーンのユーザーが、現在のオンチェーンの状況で引き出しをシミュレートする関数。

関数の実装にあたり、以下の条件があります。

  • 同じトランザクション内でwithdraw関数を呼び出した場合と同じか、それ以下のshare量を返す。
  • maxWithdraw関数の制限などを考慮しない。
  • 常に引き出しが受け入れられると仮定。
  • 引き出し手数料も考慮する必要があるが、その他のVaultの制限によりリバートする可能性がある。

withdraw

yaml
- 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を直接燃やすフロー。
  • 通常、実装によってはVaultに対して事前承認(approve)が必要。

maxRedeem

yaml
- name: maxRedeem
  type: function
  stateMutability: view

  inputs:
    - name: owner
      type: address

  outputs:
    - name: maxShares
      type: uint256

引数に渡されたオーナー(owner)がVaultの保有量から、取り戻すことのできる最大のshare量を計算する関数。 関数の実装にあたり、以下の条件があります。

  • 計算は、実際の取り戻し制限よりも低い値を返す。
  • 取り戻しが完全に無効になっている場合(一時的にでも)は、0を返す。

previewRedeem

yaml
- name: previewRedeem
  type: function
  stateMutability: view

  inputs:
    - name: shares
      type: uint256

  outputs:
    - name: assets
      type: uint256

オンチェーンまたはオフチェーンのユーザーが、現在のオンチェーンの状況で取り戻しをシミュレートする関数。 関数の実装にあたり、以下の条件があります。

  • 同じトランザクション内でredeem関数を呼び出した場合と同じか、それ以下のasset量を返す。
  • maxRedeem関数の制限などを考慮しない。
  • 常に取り戻しが受け入れられると仮定。
  • 引き出し手数料も考慮する必要があるが、その他のVaultの制限によりリバートする可能性がある。

redeem

yaml
- 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を直接取り戻すフロー。
  • 通常、実装によってはVaultに対して事前承認(approve)が必要。

Event

Deposit

yaml
- 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

yaml
- 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に興味がある」、「スマートコントラクトの開発に携わりたい」など、少しでも興味を持っている方はまずはお話ししましょう!