Skip to content
On this page

L1-3: ETH Streaming(ETHストリーミング支払い)

1. 課題

時間ベースのETHストリーミング支払いコントラクトを作成します。受信者は最後の引き出しからの経過時間に比例してETHをアンロックできます。コアとなる数式:

unlocked = (経過時間 * cap) / unlockTime

2. 背景

従来の支払い方法(月給、プロジェクト単位の支払い)は、継続的なサービス提供のシナリオ(秒単位課金のストリーミング、リアルタイムAPI呼び出しなど)には適していません。ストリーミング支払いにより、受信者は支払い元による手動送金を信頼することなく、いつでもアンロック済みの部分を引き出せます。

3. 解決策

コアデータ構造

solidity
struct Stream {
    uint256 cap;                    // 1サイクルあたりの最大引き出し可能量
    uint256 timeOfLastWithdrawal;   // 最後に全額引き出した時刻のタイムスタンプ(0 = 新規ストリーム)
}

主要メカニズム

  1. 新規ストリーム: timeOfLastWithdrawal = 0 → フルcapが即座に利用可能
  2. 全額引き出し後: timeOfLastWithdrawal = block.timestamp → 時間がゼロから蓄積開始
  3. 一部引き出し: timeOfLastWithdrawalを残量に応じて逆方向にシフト

4. 遭遇した落とし穴

4.1 timeOfLastWithdrawalは単純にリセットできない

これは本レッスンで最も重要な落とし穴です。 受信者が一部引き出しを行う場合、timeOfLastWithdrawalを単純にblock.timestampに設定することはできません。

4.2 算術アンダーフロー

timeOfLastWithdrawalのオフセット計算時に、(remaining * unlockTime) / capblock.timestampより大きくなる可能性があり、アンダーフローが発生します。

4.3 capの上限

アンロック量はcapを超えてはいけません。timeOfLastWithdrawal == 0の場合の特別な処理が必要です。

5. 落とし穴の原因

5.1 詳細な説明

cap = 10 ETH、unlockTime = 100秒と仮定します:

  1. 新規ストリーム作成(0秒)、10 ETH利用可能
  2. t=50秒:3 ETHを引き出し(残り7 ETHが利用可能)
  3. もしtimeOfLastWithdrawal = 50を設定すると、t=80秒での経過 = 30秒、アンロック = 3 ETH — しかし残り7 ETHは失われてしまいます!

正しい方法:

shift = (remainingUnlocked * unlockTime) / cap
     = (7 * 100) / 10 = 70秒
timeOfLastWithdrawal = block.timestamp - shift
                     = 50 - 70 = 0  // 「フルcapがまだ利用可能」と同等

t=80秒:経過 = 80-0 = 80秒、アンロック = 8 ETH。ただし残り7 + 新規アンロック8 > cap(10)なので、実際に利用可能 = min(8, 10) = 8 ETH...

実際にはさらに分析が必要です。shiftが0未満になる場合は「フルcapが即時利用可能」を意味し、timeOfLastWithdrawalを0に設定します。

6. 落とし穴の解決方法

6.1

solidity
uint256 remaining = unlocked - amount;
if (remaining == 0) {
    stream.timeOfLastWithdrawal = block.timestamp;
} else {
    uint256 shift = (remaining * unlockTime) / stream.cap;
    if (shift >= block.timestamp) {
        stream.timeOfLastWithdrawal = 0; // フルcapが利用可能と同等
    } else {
        stream.timeOfLastWithdrawal = block.timestamp - shift;
    }
}

6.2

block.timestamp - shiftを計算する前にshift >= block.timestampをチェックします。

6.3

_getUnlockedで、timeOfLastWithdrawal == 0の場合はcapを直接返し、通常はmin(cap, (elapsed * cap) / unlockTime)を返します。

7. 技術的ポイント

ポイント説明
ストリーミング支払いの数学モデル線形アンロック:(elapsed * cap) / unlockTime
仮想タイムスタンプtimeOfLastWithdrawalをオフセットして残り利用枠を保持
アクセス制御OwnableパターンでaddStreamを制限
receive()誰でもコントラクトに入金可能
CEIパターンChecks-Effects-Interactions:状態更新を先に、その後に転送

Built with AiAda