はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

今回は、ロック期間を設けたトークン化ボールトによって、資産の出し入れを一時的に制限しながら安全な運用と正確な清算タイミングを両立しようとする仕組みを提案しているERC6229についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。

概要

ERC6229は、既存のERC4626(トークン化された資産管理ボールトの規格)に、一定期間トークンを引き出せない「ロック期間」を組み込むための仕組みを追加する提案です。 ERC4626では、ユーザー(スマートコントラクトやEOAアカウント)が好きなタイミングで預け入れや引き出しを行うことが前提になっています。 しかし、実際の運用では、ボールト内部で特定の運用戦略を実行するために、トークンを一定期間触れないようにしたい状況があります。

ERC4626については以下の記事を参考にしてください。

この拡張では、そのようなロック期間を正式に扱えるようにし、ロック中に発生した「預けたい」、「引き出したい」という操作をそのまま拒否するのではなく、スケジュールとして受け付け、ロック終了後に反映できるようにする仕組みを整えます。

動機

ERC4626は柔軟な資産運用の枠組みを提供していますが、常に自由に入金・出金できるという前提が、逆に戦略的な制約につながることがあります。 例えば、ボールトが特定のDeFi戦略を実行している場合、運用中に突然資産が出金されると戦略が崩れてしまうことがあります。 また、新規の資金流入があると、それが運用中の処理に影響して、思わぬリスクや損失が発生することもあります。

こうした状況では、特定の期間だけ入出金を停止し、内部処理を安全に完了させたいというニーズが生まれます。 ERC6229は、この現実的な要求に応えるためにロック期間を正式に扱えるようにし、同時にロック中に希望された操作を後で実行できるようにすることで、ユーザー体験を損なわず運用の安定性を確保します。

その結果、ボールト運用者は安全に戦略を実行でき、ユーザーはロック期間中でも操作を予約でき、両者のバランスを取りながらシステム全体として透明性と安定性を向上できます。

仕様

必須の標準

ERC6229に準拠するボールトは、以下の規格を必ず実装する必要があります。

規格 説明
ERC4626 資産管理ボールトの基本機能を提供する規格。入金・出金、シェアの計算などを扱う。
ERC20 シェアを表すトークンの基本仕様。ボールトが発行するシェアはこの規格に従う。

この仕様ではロック期間とスケジュールされた入出金の仕組みを追加するため、基本となるERC4626の動作をそのまま維持しつつ、追加要素を重ねていきます。

用語の定義

用語 説明
asset ボールトが受け入れ運用する基軸となるERC20トークン。例えばUSDCやWETHなど。
share ボールトが発行するERC20トークン形式のシェア。ボールト内の資産に対する持分を表す。
locked ボールトの状態のひとつで、入出金が不可能な期間。内部で特定の戦略が実行されている可能性がある。
unlocked 入出金が自由にできる状態。通常のERC4626の動作に相当する。
round ボールトが locked 状態になるたびに進む期間単位。ロック状態が始まると round が 1 増える。

特に「round」は、スケジュールされた入金や出金がどのタイミングで登録されたかを管理するための重要な指標になります。

View関数

isLocked

javascript
// 現在のボールトのロック状態を返す
function isLocked() external view returns (bool isLocked);

は、ボールトが現在ロック状態にあるかどうかを確認する関数。 この関数はボールトの状態を確認するためのもっとも基本的なメソッドです。 返される値が true の場合、ボールトはロックされており入金や出金はできません。 逆に false の場合、ボールトはロックされておらず、通常のERC4626と同様に資産の入金・出金が可能です。 この値は運用側のロック制御ロジックによって変更され、ユーザーはこの関数を通じて現在の運用ステータスを把握できます。

戻り値

  • isLocked
    • 現在のボールトがロックされている場合は true、ロックされていない場合は false を返します。

vaultRound

javascript
// 現在のボールトのラウンド番号を返す
function vaultRound() external view returns (uint256 vaultRound);

は、ボールトが現在どのラウンドにあるかを示す関数。 ラウンド(round)は、ボールトがロック状態に入るたびに1つ増えていく番号です。 必ず0から開始し、新しいロック期間がスタートする時に1増えます。 この番号はロックのたびに進むため、スケジュールされた入金や出金が「どのラウンドで登録されたものか」を識別するために使用されます。 仕様として、運用者が任意に変更したり、ロック以外の理由で変更されることはありません。

戻り値

  • vaultRound
    • 現在のラウンド番号を返します。0から始まり、ロックの開始ごとにインクリメントされます。

Methods

scheduleDeposit

javascript
// ロック期間中の入金予約を登録する
function scheduleDeposit(uint256 assets) external;

は、ボールトがロックされている期間に「後で入金したい」という意図を登録する関数。 この関数は、ボールトがロックされている時だけ呼び出せます。 通常のERC4626ではロックという概念がないため、ロック中に入金しようとすると拒否されます。 しかしERC6229では、ユーザーがこの関数を使って入金予約を登録できます。 このとき、指定された資産 assets は呼び出し元から即座にボールトに転送されますが、シェア(share)はまだ発行されません。 シェアの発行はロックが解除された後、settleDeposits によって行われるためです。 もしロックされていない状態(unlocked)でこの関数を呼ぶと、仕様上必ず失敗します。

引数

  • assets
    • 予約する入金額。
    • 呼び出し時にボールトへ転送されます。

scheduleRedeem

javascript
// ロック期間中のシェアの償還予約を登録する
function scheduleRedeem(uint256 shares) external;

ロック期間中に「後でシェアを償還したい(出金したい)」という意図を登録する関数。 この関数もロックされている間だけ呼び出すことができます。 ユーザーが指定したシェア shares は即時にボールトへ転送されますが、対応する資産の払い戻しは行われません。 資産の払い戻しはロック解除後、settleRedemptions によって行われます。 ロックされていない状態で呼ぶと、必ず失敗する仕様です。 また、シェアが適切に転送できない場合も処理は失敗します。

引数

  • shares
    • 予約としてボールトへ引き渡すシェアの量。
    • これは後の償還処理の対象になります。

settleDeposits

javascript
// ロック解除後に予約された入金を処理し、新しいシェアを発行する
function settleDeposits(address depositor) external returns (uint256 newShares);

ロック期間中に登録された入金予約をロック解除後に実際の入金として処理し、シェアを発行する関数。 この関数はロックが解除されている状態のみ呼び出せます。 depositor がロック中に登録した入金予約を集計し、その時点でのシェア価格に基づいて新しいシェアを発行します。 発行されたシェアは指定された depositor に付与されます。 予約が存在しない場合は仕様に従い必ず失敗します。 これによってロック期間中のユーザーの意図が実際の入金として確定し、シェアが配布されます。

引数

  • depositor
    • 入金予約を持つユーザーのアドレス。

戻り値

  • newShares
    • 入金予約をもとに発行された新しいシェアの量。

settleRedemptions

javascript
// ロック解除後に予約された償還を処理し、資産を払い戻す
function settleRedemptions(address redeemer) external returns (uint256 burnShares, uint256 redeemAssets);

ロック期間中に登録されたシェア償還予約をロック解除後に実際の出金として処理する関数。 ロック解除後のみ呼ぶことができます。 ロック期間中に redeemer が登録していた償還予約を処理し、現在のシェア価格にもとづいて出金額を計算します。 処理では、ユーザーから預かっていたシェアをBurnし、対応する量の資産を redeemer に払い戻します。 もし予約が存在しない場合、この関数は必ず失敗します。 これにより、ロック期間中に行われた「後で出金したい」という意図が正式に実行されます。

引数

  • redeemer
    • 償還予約を持つユーザーのアドレス。

戻り値

  • burnShares
    • Burnされるシェア量。
  • redeemAssets
    • redeemer へ払い戻される資産量。

getScheduledDeposits

javascript
// 指定ユーザーの予約された入金総額を返す
function getScheduledDeposits(address depositor) external view returns (uint256 totalAssets);

ユーザーがロック期間中に登録した入金予約の総額を取得する関数。 この関数はロック状態に関係なく呼び出せます。 また仕様で「決して revert してはならない」とされているため、エラーで停止することはありません。 指定したユーザーのアドレスに紐づく予約された入金額 totalAssets を返します。

引数

  • depositor
    • 入金予約を確認したいユーザーのアドレス。

戻り値

  • totalAssets
    • そのユーザーの予約中の入金額。

getScheduledRedemptions

javascript
// 指定ユーザーの予約された償還シェア総量を返す
function getScheduledRedemptions(address redeemer) external view returns (uint256 totalShares);

getScheduledRedemptions は、ユーザーがロック期間中に登録した償還予約のシェア総量を取得する関数。 getScheduledDeposits と同様に、エラーで停止してはならない仕様です。 ユーザーがロック期間中にどれだけシェアを償還する意図を登録しているかを確認できます。

引数

  • redeemer
    • 償還予約を確認したいユーザーのアドレス。

戻り値

  • totalShares
    • 登録されている償還予約の合計シェア量。

イベント

ScheduleDeposit

javascript
event ScheduleDeposit(
    address indexed sender,
    uint256 assets,
    uint256 round
);

ロック期間中に入金予約が登録された時に発行されるイベント。 ユーザーが scheduleDeposit を呼び出すと発行されます。 入金額と、その予約が行われたラウンド番号が記録されます。

パラメータ

  • sender
    • 予約を行ったユーザーのアドレス。
  • assets
    • 予約された入金額。
  • round
    • 予約が行われたラウンド番号。

ScheduleRedeem

javascript
event ScheduleRedeem(
    address indexed sender,
    uint256 shares,
    uint256 round
);

ロック期間中に償還予約が登録された時に発行されるイベント。 ユーザーが scheduleRedeem を呼び出した際に発行されます。 償還したいシェア量と登録されたラウンドが記録されます。

パラメータ

  • sender
    • 償還予約を行ったユーザーのアドレス。
  • shares
    • 予約されたシェア量。
  • round
    • 予約が行われたラウンド番号。

SettleDeposits

javascript
event SettleDeposits(
    address indexed depositor,
    uint256 newShares,
    uint256 round
);

ロック解除後に入金予約が清算された時に発行されるイベント。 settleDeposits が成功した時に発行されます。 新しく発行されたシェア量と、その処理がどのラウンドの予約を対象にしたかが記録されます。

パラメータ

  • depositor
    • 清算対象のユーザー。
  • newShares
    • 発行されたシェア量。
  • round
    • 清算された予約のラウンド番号。

SettleRedemptions

javascript
event SettleRedemptions(
    address indexed redeemer,
    uint256 burnShares,
    uint256 redeemAssets,
    uint256 round
);

ロック解除後に償還予約が清算された時に発行されるイベント。 settleRedemptions が成功すると発行され、Burnされたシェア量と払い戻された資産量が記録されます。 また、どのラウンドに登録された予約かも合わせて記録されます。

パラメータ

  • redeemer
    • 清算対象のユーザー。
  • burnShares
    • Burnされたシェア量。
  • redeemAssets
    • 払い戻された資産量。
  • round
    • 清算された予約のラウンド番号。

補足

ERC4626拡張は、できるだけシンプルなインターフェースになるように設計されています。

ここでいう「インターフェース」は、ボールトと外部(ユーザーや他のコントラクト)がやり取りするための関数やイベントの最小セット、という意味です。

そのため、ロック期間の開始時刻や終了時刻、ロック中にどのような戦略でトークンを運用しているかといった、実装ごとに大きく異なる部分はあえて仕様に含めていません。 ロック期間の管理ロジックや、ロック中の運用戦略はプロトコルごとに最適解が違うため、ここを標準で縛ってしまうと利用シーンが限定されてしまいます。 ERC6229は「ロックされているかどうか」、「ロック中に入出金をどう扱うか」の共通部分だけを定義し、それ以外は実装者に委ねています。

また、仕様では「出金を予約するための関数(スケジュールされた withdrawal)」を定義していません。 その理由は、ロック期間中はシェア価格が確定していないためです。 シェア価格が決まっていない状態では、シェア何枚を引き換えに基軸トークン(asset)を何枚戻せるかを計算できません。 出金予約で「トークン〇枚を引き出す」と決めてしまうと、その約束をどのレートで実現するのかが定義できず、仕様としては曖昧さを残してしまいます。

ERC6229はこの問題を避けるため、ロック期間中に扱うのは「入金予約(あとでシェアを発行するための asset のロック)」と「償還予約(あとで asset を払い戻すための share のロック)」に限定し、実際のシェア発行や asset の払い戻しはロック解除後の「確定したシェア価格」に基づいて行う、という整理にしています。

互換性

すでに存在するERC4626の挙動との整合性が重要になります。 ERC6229では、もともとERC4626に含まれている以下の4つの関数の扱いについてルールを定めています。

  • deposit
  • mint
  • withdraw
  • redeem

これらの関数は、ボールトのシェア価格に基づいてシェアの発行や焼却を行います。 しかしロック期間中(isLocked が true の間)は、シェア価格が実質的に「未確定」とみなされるため、これらの関数を実行すると、どのレートでシェアを増減させるべきか決められません。

そのため ERC6229では、isLocked が true のときに deposit / mint / withdraw / redeem が呼ばれた場合、必ず revert させるべきとしています。 これにより、ロック中に不正確な価格でシェアを発行したりBurnしたりすることを防ぎます。

既存のERC4626実装にこの拡張を適用する際は、isLocked の状態チェックをこれら4つの関数の最初に追加し、ロック中は予約専用の scheduleDeposit / scheduleRedeem を使わせる、という構成にするのが基本的な方針になります。

セキュリティ

セキュリティ面で特に重要なのは、「スケジュールされた入金・償還がいつ清算されるか」という点です。

ロック期間中にユーザーが scheduleDeposit や scheduleRedeem を呼ぶと、その意図は「予約」としてボールト内に記録されます。 しかし、その後ロックが解除されても、ユーザーがすぐに settleDeposits や settleRedemptions を呼ぶとは限りません。 もしユーザーが何ラウンドも放置したあと、かなり時間が経ってから清算を行った場合、その予約は「本来想定していたラウンドとは異なるシェア価格」で処理される可能性があります。

例えば、「ラウンド1のロック中に入金予約をしたが、清算せずにラウンド2、ラウンド3と進み、ラウンド3のロック解除後に settleDeposits を実行した」というケースでは、本来ラウンド1終了時点のシェア価格で処理されるべき入金が、ラウンド3終了時点のシェア価格で処理されてしまうことになります。 これにより、ユーザーとボールトのどちらかに不公平な損得が生まれるおそれがあります。

ERC6229は、この問題の具体的な解決方法までは標準として規定していません。 なぜなら、どの解決策が適切かはプロトコルの用途やリスク許容度によって大きく異なるからです。 代わりに、いくつかの代表的なアプローチ例を挙げたうえで、どれを採用するかは実装者に任せています。

代表的な対策例は、以下のように整理できます。

アプローチ 内容
ロック開始前に未清算予約を残さない isLocked を true にする前に、すべての入金・償還予約が清算されていることを必須条件にする。未清算がある場合はロック状態に遷移させない。
ロック遷移時に強制清算する isLocked を true に変更するタイミングで、残っている入金・償還予約を自動的に清算してしまう。ユーザーの明示的な settle 呼び出しを待たない。
ラウンドごとの終値シェア価格を記録する 各ラウンドごとに「そのラウンドの終了時のシェア価格」を保存し、予約がどのラウンドで行われたものかを記録する。ユーザーが後から清算した場合でも、予約したラウンドのシェア価格で計算する。

どの方法にも一長一短があります。

  • ロック前に未清算予約を禁止すると、安全性は高い一方で、ロックに入れない状況が発生しやすくなります。
  • ロック遷移時の強制清算は、ユーザーにとって「いつ清算されるか」がやや見えづらくなる可能性があります。
  • ラウンドごとにシェア価格を保存する方式は柔軟ですが、記録するデータ量やロジックが増えるため、ガスコストや実装の複雑さが増します。

ERC6229は、こうした選択肢を提示するにとどめ、どれか1つを強制はしていません。 実装者は自分たちのプロトコルの特性やユーザー体験、求める安全性のレベルに応じて適切な手法を選び、ドキュメントで明示することが求められます。

最後に

今回は「ロック期間を設けたトークン化ボールトによって、資産の出し入れを一時的に制限しながら安全な運用と正確な清算タイミングを両立しようとする仕組みを提案しているERC6229」についてまとめてきました! いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

他の媒体でも情報発信しているのでぜひ他も見ていってください!