Appearance
L2-20: Atomic Swaps(原子交换 / HTLC)
1. 问题
如何让两个互不信任的参与方在两条不同链上交换资产,而不需要第三方托管?——即"原子交换"(Atomic Swap)。
具体来说:Alice 想用自己的 10 ETH 换取 Bob 的等值 BTC,但双方都不信任对方会先转账。如何在双方都安全的前提下完成这次跨链交换?
2. 原因
原子交换是跨链 DeFi 的基础原语,无需信任任何桥接或中间人即可完成跨链资产互换。HTLC (Hash Time-Locked Contract) 是比特币闪电网络和以太坊间交换的核心技术。
其安全性基于两个密码学原理:
- Hash Lock(哈希锁):只有知道秘密原像
s的人才能解锁资金(hash(s) == hashLock) - Time Lock(时间锁):超时后发送方可以取回资金
组合起来就是:接收方必须在一定时间内揭示秘密,否则发送方可以退款。
3. 方案
架构设计
部署 AtomicSwaps 合约
|
├── lock(receiver, hashLock, timelock) → Alice 锁定 ETH
| └── 生成 swapId = keccak256(sender, receiver, amount, hashLock, timelock, timestamp)
|
├── withdraw(swapId, secret) → Bob 揭示秘密取走 ETH
| └── 验证: keccak256(secret) == hashLock
|
└── refund(swapId) → Alice 超时后取回 ETH
└── 验证: block.timestamp > timelock && msg.sender == sender
交换流程(Alice ETH ↔ Bob BTC)
- Alice 在以太坊上调用
lock(bobAddress, hashLock, timelock)锁定 ETH - Bob 看到锁定后,在比特币链上创建对应的 HTLC(相同 hashLock,更紧的 timelock)
- Alice 揭示
secret在比特币链上取走 BTC - Bob 看到 BTC 链上揭示的
secret,用以太坊上的secret调用withdraw(swapId, secret)取走 ETH
关键: Alice 必须先揭示 secret(在 BTC 链上),Bob 看到后才能在 ETH 链上揭示——Bob 的 BTC→ETH 是单向信息流(Bob 不会主动放弃自己的 BTC)。
状态机
None → Locked → Withdrawn (Bob reveals secret)
→ Refunded (Alice refunds after timelock)
核心代码
solidity
function withdraw(bytes32 swapId, bytes32 secret) external {
Swap storage swap = swaps[swapId];
require(swap.state == SwapState.Locked, "Not locked");
require(swap.receiver == msg.sender, "Not receiver");
require(keccak256(abi.encodePacked(secret)) == swap.hashLock, "Wrong secret");
swap.state = SwapState.Withdrawn;
(bool ok, ) = swap.receiver.call{value: swap.amount}("");
require(ok, "Transfer failed");
}
function refund(bytes32 swapId) external {
Swap storage swap = swaps[swapId];
require(swap.state == SwapState.Locked, "Not locked");
require(swap.sender == msg.sender, "Not sender");
require(block.timestamp > swap.timelock, "Timelock not expired");
swap.state = SwapState.Refunded;
(bool ok, ) = swap.sender.call{value: swap.amount}("");
require(ok, "Transfer failed");
}
4. 遭遇的陷阱
4.1 swapId 的唯一性
swapId = keccak256(sender, receiver, amount, hashLock, timelock, block.timestamp) 包含 timestamp 确保唯一性。但同一区块内两笔参数完全相同的交易会产生冲突——理论上可能但实践中极罕见。
4.2 跨链时序攻击
Alice 和 Bob 的 timelock 设置不合理可能导致:Alice 的 ETH 超时退款,但 BTC 侧的 secret 已被揭示——Alice 同时拿到了 BTC 和已退款的 ETH。
4.3 重放攻击防护
withdraw 和 refund 都检查 SwapState.Locked,执行后立即更新状态为 Withdrawn/Refunded。这是经典的 Checks-Effects-Interactions 模式——防止重入和安全的状态转换。
4.4 secret 的信息泄露
如果在 withdraw 交易的 calldata 中公开了 secret,mev 搜索者可以抢跑复制你的 secret 去解锁另一个链上的 HTLC。在实际跨链交换中,secret 泄露的时序至关重要。
5. 陷阱的原因
5.1
block.timestamp 虽然在同一区块内是常数,但 swapId 包含所有 6 个参数,如果 sender/receiver/amount/hashLock/timelock 都相同且在同一区块内发生,swapId 会冲突。典型解决方案:加 nonce 或直接用递增计数器。
5.2
正确的 timelock 设置规则:
- 发起方(Alice)的 timelock 应长于接收方(Bob)的
- Bob 在 BTC 上的 timelock 应该比 Alice 在 ETH 上的 timelock 短(例如:24h vs 48h)
- 这样 Alice 要么在 24h 内揭示 secret(然后 Bob 在 48h 内揭示),要么放弃(双方各自退款)
5.3
solidity
// ✅ CEI: 先更新状态
swap.state = SwapState.Withdrawn;
// 再执行外部调用
(bool ok, ) = swap.receiver.call{value: swap.amount}("");
如果 call 在状态更新之前,攻击者可以重入 withdraw 函数再次取款。
6. 如何解决陷阱
- swapId 唯一性:在生产环境中使用
swapCount++作为 swapId 而非 hash 计算 - Timelock 梯度:发起方的 timelock > 接收方的 timelock,至少 2x 差距
- CEI 模式:状态更新始终在外部调用之前
- Secret 保护:使用 Flashbots 捆绑或私有交易池中继 withdraw 交易
7. 技术要点
| 要点 | 说明 |
|---|---|
| HTLC | Hash Time-Locked Contract — 跨链原子交换标准 |
| hashLock | keccak256(abi.encodePacked(secret)) — 验证原像 |
| timelock | Unix timestamp,超过后发送方可退款 |
| 状态机 | None → Locked → Withdrawn/Refunded |
| CEI | Checks-Effects-Interactions — 防止重入 |
| swapId 设计 | 包含 block.timestamp 防碰撞 |
| 适用场景 | ETH↔BTC、ETH↔ERC20、闪电网络路由 |