Skip to content
On this page

L2-20: Atomic Swaps(原子交换 / HTLC)

1. 问题

如何让两个互不信任的参与方在两条不同链上交换资产,而不需要第三方托管?——即"原子交换"(Atomic Swap)。

具体来说:Alice 想用自己的 10 ETH 换取 Bob 的等值 BTC,但双方都不信任对方会先转账。如何在双方都安全的前提下完成这次跨链交换?

2. 原因

原子交换是跨链 DeFi 的基础原语,无需信任任何桥接或中间人即可完成跨链资产互换。HTLC (Hash Time-Locked Contract) 是比特币闪电网络和以太坊间交换的核心技术。

其安全性基于两个密码学原理:

  1. Hash Lock(哈希锁):只有知道秘密原像 s 的人才能解锁资金(hash(s) == hashLock
  2. 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)

  1. Alice 在以太坊上调用 lock(bobAddress, hashLock, timelock) 锁定 ETH
  2. Bob 看到锁定后,在比特币链上创建对应的 HTLC(相同 hashLock,更紧的 timelock)
  3. Alice 揭示 secret 在比特币链上取走 BTC
  4. 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. 技术要点

要点说明
HTLCHash Time-Locked Contract — 跨链原子交换标准
hashLockkeccak256(abi.encodePacked(secret)) — 验证原像
timelockUnix timestamp,超过后发送方可退款
状态机None → Locked → Withdrawn/Refunded
CEIChecks-Effects-Interactions — 防止重入
swapId 设计包含 block.timestamp 防碰撞
适用场景ETH↔BTC、ETH↔ERC20、闪电网络路由

Built with AiAda