Appearance
L1-3: ETH Streaming(ETH 流支付)
1. 问题
创建基于时间的 ETH 流支付合约。接收者根据距上次提款的时间长短,按比例解锁 ETH。核心公式:
已解锁 = (经过时间 * cap) / unlockTime
2. 原因
传统支付方式(月薪、按项目支付)不适合持续服务场景(如按秒计费的流媒体、实时 API 调用)。流支付允许接收方随时提取已解锁部分,无需信任支付方手动转账。
3. 方案
核心数据结构
solidity
struct Stream {
uint256 cap; // 单次最大可提取量
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:先更新状态再转账 |