Appearance
L1-3: ETH Streaming(ETHストリーミング支払い)
1. 課題
時間ベースのETHストリーミング支払いコントラクトを作成します。受信者は最後の引き出しからの経過時間に比例してETHをアンロックできます。コアとなる数式:
unlocked = (経過時間 * cap) / unlockTime
2. 背景
従来の支払い方法(月給、プロジェクト単位の支払い)は、継続的なサービス提供のシナリオ(秒単位課金のストリーミング、リアルタイムAPI呼び出しなど)には適していません。ストリーミング支払いにより、受信者は支払い元による手動送金を信頼することなく、いつでもアンロック済みの部分を引き出せます。
3. 解決策
コアデータ構造
solidity
struct Stream {
uint256 cap; // 1サイクルあたりの最大引き出し可能量
uint256 timeOfLastWithdrawal; // 最後に全額引き出した時刻のタイムスタンプ(0 = 新規ストリーム)
}
主要メカニズム
- 新規ストリーム:
timeOfLastWithdrawal = 0→ フルcapが即座に利用可能 - 全額引き出し後:
timeOfLastWithdrawal = block.timestamp→ 時間がゼロから蓄積開始 - 一部引き出し:
timeOfLastWithdrawalを残量に応じて逆方向にシフト
4. 遭遇した落とし穴
4.1 timeOfLastWithdrawalは単純にリセットできない
これは本レッスンで最も重要な落とし穴です。 受信者が一部引き出しを行う場合、timeOfLastWithdrawalを単純にblock.timestampに設定することはできません。
4.2 算術アンダーフロー
timeOfLastWithdrawalのオフセット計算時に、(remaining * unlockTime) / capがblock.timestampより大きくなる可能性があり、アンダーフローが発生します。
4.3 capの上限
アンロック量はcapを超えてはいけません。timeOfLastWithdrawal == 0の場合の特別な処理が必要です。
5. 落とし穴の原因
5.1 詳細な説明
cap = 10 ETH、unlockTime = 100秒と仮定します:
- 新規ストリーム作成(0秒)、10 ETH利用可能
- t=50秒:3 ETHを引き出し(残り7 ETHが利用可能)
- もし
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:状態更新を先に、その後に転送 |