链上订单簿中的 19 个安全陷阱(以及如何修复它们)

  • hacken
  • 发布于 2025-11-18 12:46
  • 阅读 13

本文深入探讨了链上订单簿在实际应用中面临的19个常见问题,涵盖了从交易抢跑、订单唯一性到Gas用量、时间逻辑以及预言机利用等多个方面。文章不仅分析了这些问题的根本原因,还提供了相应的缓解措施,旨在帮助智能合约工程师、协议架构师和审计人员构建更安全、可靠的去中心化交易系统。

链上订单簿在纸面上看起来很棒。你可以获得完全的透明性,与 DeFi 的其余部分的可组合性,以及对用户和监管机构来说清晰的故事:“一切都在链上”。

但是,一旦你开始实施它们,事情就会变得很快变得混乱。围绕撮合、部分成交、取消、gas 飙升、清算和预言机更新的极端情况出现在你未计划的地方。一个单一的漏洞可能会冻结交易,将价值泄漏给 MEV 机器人,或者随着时间的推移悄悄地破坏你的账簿。

这篇文章是为那些正在实际构建或审查这些系统的人准备的——智能合约工程师、协议架构师,以及从事现货 DEX、永续合约、RFQ 引擎或混合型应用审计员。我们不会重新解释限价订单如何工作。相反,我们将专注于现实世界中的实现往往会出错的地方。

1. 抢跑交易和交易排序

在无需许可的区块链中,交易在包含在一个区块之前在 mempool 中公开可见。在公共 mempool 中创建、取消和执行订单允许对手监控所有活动并有策略地重新排序他们的交易。缺少承诺-揭示阶段使得攻击者能够在限价订单变得有利可图时立即狙击它们。

类似地,当取消被即时处理且没有摩擦时,交易者可能会观察到一个传入的订单,并在同一区块内取消或重新发布他们自己的订单,从而操纵队列优先级或避免不利的成交。这种成交和取消之间的竞争也可能导致 taker 的交易意外回滚,导致不可预测的用户体验并破坏订单的可靠性。

  • 根本原因: 缺乏承诺阶段和取消的冷却期。
  • 缓解措施:
    • 承诺-揭示方案: 将一个订单分成两个步骤——首先提交订单参数的哈希承诺,然后在第二个交易中揭示细节——隐藏了实际订单,直到它是不可撤销的。这可以防止对价格或数量的狙击。然而,它使交易成本翻倍并增加了延迟。
  • 指定的 Taker 地址: 通过将特定的 taker 地址嵌入到订单数据中(并将其包含在签名中),合约强制只有指定的参与方才能成交订单。这消除了通用的 taker 竞争,但需要 maker 和 taker 之间的链下协调。
  • 批量拍卖/时间加权执行: 订单可以被收集到离散的时间间隔中(例如,1 分钟批次),而不是立即执行每个订单。在每个间隔结束时,所有匹配同时发生,从而减少了该窗口内的时间优势。时间加权平均价格(TWAP)机制可以进一步平滑多个区块上的价格影响。
  • 强制执行冷却期或取消费,以阻止快速的战术性取消。

2. 订单唯一性和签名重放

如果订单哈希或签名不能紧密匹配所有用户预期的参数(包括 nonce/salt、合约和链上下文),则有效的链下签名可能会在多个订单、链或合约升级中被重新提交或重放。如果没有严格执行的唯一性,先前已成交或已取消的订单可以通过重新提交签名来“复活”,从而导致双重成交或未经授权的执行。在多链部署或协议升级中,未能对签名进行域分离会启用跨链和跨合约重放攻击,可能会耗尽多个部署中的流动性。

  • 根本原因: nonce 使用不足和缺乏适当的域分离。
  • 缓解措施:
    • EIP-712 域分离: 构建一个 EIP-712 域,其中包括合约的地址和链 ID。此域被添加到订单结构的哈希之前,确保签名无法在不同的合约或链上重放。域分隔符通常如下所示:
bytes32 DOMAIN_SEPARATOR = keccak256(
    abi.encode(
        keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
        keccak256(bytes("MyDEX")),
        keccak256(bytes("1")),
        block.chainid,
        address(this)
    )
);
  • Nonces 和 Salts: 每个订单应包括一个 `nonce` 或 `salt` 字段:一个唯一的 256 位值,maker 随机选择或递增。订单哈希集成了这个 nonce:
bytes32 orderHash = keccak256(
    abi.encode(
        ORDER_TYPEHASH,
        maker,
        taker,
        tokenGive,
        tokenGet,
        amountGive,
        amountGet,
        expiration,
        nonce
    )
);

3. 部分成交和状态一致性

部分成交需要对“剩余数量”和“可用数量”字段进行细致的管理。未能原子性地更新这些字段,或未能在并发交易期间协调差异,可能导致可用余额与订单意图失去同步。这可能允许 taker 成交超过预期数量,或导致订单卡在“无法成交”的状态。当计算不使用精确的定点运算时,也可能出现错误,导致灰尘级别的成交或舍入误差累积,并最终表现为非同小可的资金损失或无法认领的订单残余。

functionfill(bytes32 hash, uint256 takerFill) external {
Order storage o = orders[hash];
o.executedAmount += takerFill;
IERC20(o.token).transferFrom(o.maker, msg.sender, takerFill);
// Missing: o.remainingAmount = o.totalAmount - o.executedAmount;
}

在上面的示例中,允许部分成交的订单维护两个相关的字段:remainingAmount(订单仍然可以成交多少)和 executedAmount。如果这些字段被单独更新或以错误的顺序更新,竞争条件和不同步可能允许过度成交或留下永远无法完全清除的微小“灰尘”订单,并且在这里,remainingAmount 永远不会更新,导致重复成交超过原始上限。

  • 根本原因: 对相关状态字段的单独更新导致不一致。
  • 缓解措施:
    • 在单个交易中原子性地更新所有相关状态字段。
    • 采用定点运算(例如,FullMath.mulDiv)来处理精确的余数计算。
    • 强制执行最小成交量以防止利用性的灰尘成交。为了防止 1-wei 的“灰尘”成交过早地使订单失效,强制执行相对于原始订单的最小成交量。或者,在最终成交后,如果`remainingTakerAmount`低于阈值,自动将其归零,吸收灰尘。
    • 使用显式的布尔标志 `allowMultipleFills`。如果 `false`,则第一个部分成交触发 `remainingTakerAmount = 0`,有效地取消该订单。如果 `true`,则多个成交会减少剩余数量直到为零。

4. 匹配逻辑和容错

当匹配引擎逻辑或链上成交循环处理一批订单时,单个订单中的任何失败——例如尝试与被列入黑名单或不符合条件的交易对手进行成交,或由于 allowance/approval 问题而失败的转账——都可能导致整个批次回滚。此外,价格和大小计算中的简单整数除法或未检查的下溢可能允许零值成交或破坏订单状态的溢出。如果没有细粒度的错误处理(例如每个订单的 try/catch 或 revert-on-failure),该协议可以被任何人通过将单个有问题订单提交到批次中来轻松进行 DoS。

functionmatchBatch(bytes32[] memory orderHashes) external {
for (uint i = 0; i < orderHashes.length; i++) {
    _match(orderHashes[i]); // reverts on any failure
}
}

在上面的示例中,循环遍历未结订单的批量匹配例程可能会因为任何单个成交失败而完全回滚——由于零 allowance、被列入黑名单的交易对手或转账回滚——导致简单的 DoS。

  • 根本原因: 未检查的转账失败和计算期间的整数下溢/溢出。
  • 缓解措施:
    • 在关键转账周围实施错误捕获逻辑(try/catch),以允许尽管发生孤立的失败也能继续处理。
    • 添加对舍入结果的严格验证,以防止零值或意外交易。
    • 应用原子转账序列。通过先更新状态并采用安全转账功能,token 移动中的任何失败都会导致整个交易回滚,从而回滚状态更改。例如:
    • 状态更新
    • Taker 转账
    • Maker 转账
    • 费用结算
    • 使用交叉乘法进行价格验证,而不是直接除法,以防止舍入误差。例如,对于限价订单,强制执行:
require(
    remainingTakerAmount * order.amountGet >= fillAmount * order.amountGive,
    "Price condition not met"
);

5. 外部调用和重入

订单履行函数通常与 ERC-20 transferFrom 交互,并且还可以支持用于高级结算的Hook(例如,onOrderFill 或 permit 扩展)。如果在这些调用之后执行内部会计处理,或者如果没有重入保护地链接多个外部调用,则恶意合约可以利用递归调用来操纵订单状态、耗尽资金或绕过成交/取消限制。如果协议允许用户提供的合约或任意调用目标,那么重入尤其严重。

functionfill(...) external {
IERC20(token).transferFrom(...) // external call
orders[hash].executedAmount += fillAmt; // state update
}

在上面的示例中,恶意 token 合约可以重新进入交换逻辑并操纵相同的订单或其他订单。也就是说,攻击者的 transferFrom 钩可以再次调用 fill()。

  • 根本原因: 没有保护措施的直接外部调用与内部状态更新混合在一起。
  • 缓解措施:
    • 遵循严格的检查-效果-交互模式,在外部调用之前更新状态。
    • 在关键函数上应用 ReentrancyGuard 修饰符。
    • 实施拉取支付系统,以实现更安全的外部资金转账。

6. Gas 使用和 DoS 保护

即使没有任何一个用户进行垃圾信息攻击,订单簿也可能因 全局操作 而遭受 DoS 攻击,这些全局操作的成本随订单簿的大小或对抗性输入而变化。典型的罪魁祸首包括:对 所有订单 的无限次迭代,链上 排序/堆维护,在单个调用中扫描多个 价格水平,或其 gas 随 n 个匹配项 增长 的批量函数。

随着订单簿的增长,这些 O(n)/O(n²) 路径要么超过区块 gas 限制,要么使关键维护(清理、结算、重新索引)变得负担不起,从而导致协议停滞。

  • 根本原因:
    • 需要线性扫描才能进行例行操作的 全局数组/列表
    • 链上 排序/优先级维护,而不是委托给链下索引器。
    • 尝试在每次交易中进行 过多 成交/级别的批量流程。
    • 维护任务缺乏 分页/检查点 模式。
  • 缓解措施:
    • 数据建模: 将订单存储在以 orderHash 为键的 O(1) 映射中;将价格水平索引保存在链下(索引器)并提交每次交易一小部分匹配项的有界证明。
    • 通过 MAX_LEVELS_PER_TX, MAX_FILLS_PER_TX 限制所有循环 ;永远不要迭代 allOrders。
    • 分页/检查点:
    • 使用光标进行 prune(startKey, limit) 样式的维护。
    • 使用有时限或计数上限的订单簿操作。
    • 分摊责任: 将成本高昂的选择/排序移至链下;链上仅验证提交的小批次的 本地不变性
    • Gas 感知 API: 拒绝会超出安全范围的调用;记录最坏情况下的成本。

7. 基于时间的逻辑和到期

如果到期逻辑只是检查 block.timestamp 是否超过了到期参数,那么它很容易受到极端情况的影响,例如矿工操纵的时间戳或仅在成交时才检查到期时间的过时状态。此外,如果取消或清理逻辑不能可靠地处理过期的订单,这些订单可能会保留在订单簿中,导致不正确的可用余额、在预期到期后意外成交,或者用户无法从过期/取消的订单中收回资金。

  • 根本原因: 不一致的时间戳条件和缺乏全面的时间戳验证。
  • 缓解措施:
    • 清楚地定义并一致地应用到期检查 (block.timestamp < expiration)。
    • 记录到期时间是互斥的。如果需要互包含行为,请使用 `<=` 并注意矿工时间戳漂移可能高达 \~15 秒。
    • 可选择引入较短的宽限期,以缓冲较小的时间戳差异。
    • 记录并验证对时间戳敏感的状态参数的更新。

8. 费用计算和佣金标志

如果在取消或声明订单时(而不是在下订单时固定)计算动态费用等级或折扣标志,则订单创建和结算之间更改的协议参数可能会导致退款计算偏离原始预期。这会导致不一致的会计处理、潜在的过度退款或退款不足,以及用户在取消之前操纵其费用等级以获取经济利益的漏洞。

uint256 fee = isPremium[msg.sender] ? premiumRate : standardRate;
functioncancel(...) external {
// user may have switched tiers here
_refund(order.amount + fee);
}

在上面的示例中,如果在取消时重新评估佣金费率或折扣(而不是在下订单时固定),用户可以在订单中期更改其等级以套取费用退款。

  • 根本原因: 费用标志是在取消时动态检索的,而不是持久化的。
  • 缓解措施:
    • 在订单创建时显式地持久化费用等级信息,并在退款或取消期间使用存储的值。
    • 清楚地记录并强制执行费用计算中的舍入策略。
    • 默认情况下,为防止舍入误差,费用向下舍入。如果需要向上舍入,请对模块集成进行广泛的测试。

9. 波动率控制和熔断机制

旨在防止在极端市场波动期间执行的熔断机制和波动率边界可能会错误地触发,如果检查没有以同时存在买入和卖出流动性为条件。如果在只有订单簿的一侧存在时计算或强制执行边界,DEX 可能会因不完整的上下文而阻止交易很长时间,从而导致市场参与者拒绝服务。

if (potentialPrice > staticRef * (1 + threshold)) {
haltTrading(); // even if no sell orders exist
}

在上面的示例中,基于波动率 bands 的自动 halts, haltTrading, 如果只有一侧(买入或卖出)有深度,则可能会错误地触发,从而阻止任何交易直到手动干预。

  • 根本原因: 在未确保买入和卖出方都存在的情况下进行的波动率检查。
  • 缓解措施:
    • 仅当市场的买卖双方都有活跃订单时才调用波动率检查。
    • 确保独立于熔断机制条件的全面盘前边界验证。

10. 资金费率机制和头寸管理

未平仓头寸的资金费率应与每次头寸变动同步更新。延迟或推迟资金费率计算——例如,在头寸合并、回滚或部分平仓期间——为复杂的用户创造了通过在资金更新之前在账户之间转移头寸或合并头寸来博弈系统的机会,从而避免支付并造成系统性缓冲短缺。缺少每个交易者的缓冲限制可能会使用户能够反复地为自己操纵资金逻辑。

functionmergePositions(...) external {
positions[p1].size += positions[p2].size;
delete positions[p2]; // funding owed on p2 never charged
}

在上面的示例中,头寸在没有立即结算费用的情况下合并,允许用户避免支付累积的资金费用,从而耗尽协议缓冲区。

  • 根本原因: 延迟的资金费率计算和在头寸回滚或合并期间缺少更新。
  • 缓解措施:
    • 在每次头寸更改或合并事件期间实施实时资金费率更新。
    • 设置每个交易者的缓冲限制以限制操纵资金费率。
    • 确保在回滚期间进行准确的状态更新,以保持正确的全局资金计算。

11. 清算和价格操纵

如果基于单个价格快照(即使来自链上预言机)触发清算,攻击者可以暂时操纵价格源,从而以人为的价格强制清算。缺乏时间加权平均价格(TWAP)的使用或缺少多区块清算延迟使得攻击者能够抢先进行清算或从短暂的波动中获利,从而导致普通用户遭受不公平的损失。

  • 根本原因: 基于单实例、可能被操纵的价格检查的清算逻辑。
  • 缓解措施:
    • 使用时间加权平均价格 (TWAP) 或多区块平均进行清算决策。例如:
price = (cumPrice[current] - cumPrice[past]) / (currentBlock - pastBlock);
  • 引入强制性清算延迟以允许市场稳定。
  • 利用公平的排序机制或随机交易排序来缓解抢先交易。

12. 订单数量和资金管理

在成交或取消操作中对数量和 availableQuantity 的不一致更新会导致协议会计中的不平衡,从而可能允许双重支出、用户资金被困或 UI 和实际链上状态之间的差异。当通过单独的代码路径处理头寸合并、部分平仓或费用退款时,这可能会变得特别严重。

matchOrder(...) {
if (filled == order.available) {
delete orders[id]; // quantity cleared, availableQuantity stale
}
}

在上面的示例中,在成交或取消时对 quantity 与 availableQuantity 的不匹配更新会创建剩余 token 或双重支出场景。

  • 根本原因: 导致差异的 availableQuantity 的部分更新。
  • 缓解措施:
    • 在订单履行时同时更新 quantity 和 availableQuantity 字段。
    • 严格验证履行后状态以防止双重支出或资金被卡住。

13. 访问控制和受信任的转发器

订单簿合约通常会公开特权入口点(费用/配置更新、紧急暂停、通过 relayer 创建和匹配、市场上市)。如果任何这些都可以在没有严格的角色检查的情况下被调用,或者如果 元交易转发器 是受信任的但 _msgSender() 没有被一致地使用,攻击者可以 (a) 直接调用管理函数,(b) 通过恶意转发器欺骗发送者,或者 (c) 通过从受损 EOA 调用 grantRole/set_XYZ 来提升权限。在可升级的部署中,未受保护的初始化程序或不安全的代理管理员允许攻击者重新初始化、获取所有权或交换实现。即使有角色,自身的角色管理员(例如,CONFIG_ROLE 管理员是 CONFIG_ROLE)也允许循环权限授予。

  • 根本原因:
    • 缺少最小权限 RBAC 或错误配置的角色管理层次结构。
    • 不完整的 EIP-2771 集成(未在所有地方覆盖 _msgSender(),混合使用 msg.sender 和 _msgSender() → 发送者欺骗)。
    • 单密钥 EOA 作为超级管理员(没有 2 步所有权 / 多重签名 / 时间锁)。
    • 可升级性陷阱:未受保护的初始化程序、代理管理员暴露、存储布局冲突。
  • 缓解措施:
    • 使用 多重签名(对于 CONFIG_ROLE)和 角色撤销 路径。
    • 记录所有特权更改;考虑对高影响参数使用 时间锁

14. 取消策略和反恶意取消

零成本、即时取消让 maker 能够 对 mempool 做出反应 并在成交着陆之前撤回订单。这创造了“JIT 流动性”的假象,增加了 Taker 回滚率,并启用了 队列狙击(以稍微更好的价格取消/重新发布以始终保持在订单簿的顶部)。Maker 还可以通过发布订单然后在同一区块内取消来恶意攻击,从而在从未打算交易的情况下膨胀状态和事件。

functioncancel(bytes32 hash) external {
require(msg.sender == orders[hash].maker, "ONLY_MAKER");
delete orders[hash]; // ❌ zero friction
}
  • 根本原因:
    • 没有 time-in-force 模型(例如,GTC vs IOC vs FOK)或取消之前的 冷却时间
    • 没有 经济摩擦(保证金/罚款)使最后一刻的取消成本高昂。
    • 取消路径 无条件 删除状态(没有速率限制,没有分数/打击系统),因此策略机器人将取消用作一流的信号。
  • 缓解措施:
    • 添加 time-in-force冷却时间;可选择要求 小额保证金 仅在冷却时间后或在成交时才可退还。
    • 保持取消 gas 限制且非迭代。

15. 每个用户的订单限制和反垃圾邮件

单个帐户(或一小部分帐户)可以通过 放置大量微小订单,扰乱索引器并将其他用户的交互推入更高的 gas 区域来降低用户体验。即使使用高效的全局逻辑,来自一个地址的 突发 放置/取消模式也会膨胀日志,触发频繁的重新组织敏感状态更改,并且可以用于恶意攻击等待成交的接受者。

function place(Order memory o) external {
_store(o); // ❌ no per-user or global cap
}
  • 根本原因:
    • 缺少每个用户的未结订单上限和每个间隔(速率)限制。
    • 没有经济摩擦(垃圾邮件保证金 / 放置费)来阻止零意图订单。
    • 没有最短生存时间:用户可以在紧密循环中放置→取消→重新放置。
  • 缓解措施:
    • 每个地址(可选择每个市场)的硬性上限和速率限制:
    • MAX_OPEN_PER_USER, MAX_PLACED_PER_WINDOW, 滑动窗口强制执行。
    • 垃圾邮件保证金或放置费(在_成交_或_到期_时退还,而不是在提前取消时退还)。
    • 在取消资格之前,最短生存时间 (MIN_LIVE_SECONDS) 可阻止放置和撤回:

16. 最小订单量、名义价值和 Tick/Lot

低于 最小基本规模名义价值 的订单会产生未成交的灰尘并扭曲费用(舍入为零)。缺乏 tick/lot 粒度会产生病态的价格/大小对,从而破坏匹配等效性(例如,7 位小数的价格遇到 6 位小数的资产)。多小数 token 使这变得复杂:错误缩放的价格/金额会导致 看起来有效 但无法结算的订单(费用 > 名义价值或永远无法清除的灰尘残余)。

  • 根本原因:
    • 缺少 市场元数据(小数位数、tick 大小、lot 大小、最小名义价值)和放置时的 标准化
    • token(基本/报价)和费用单位之间不一致的 小数缩放
    • 最小成交规模 视为 最小订单规模 的替代品(它们解决不同的问题)。
  • 缓解措施:
    • 将金额标准化为 市场元数据(小数位数),然后在 放置时 强制执行 tick(价格网格)lot(大小网格)最小基本规模最小名义价值
struct Market {
uint8  baseDec; uint8 quoteDec;
uint256 tick; uint256 lot;
uint256 minBase; uint256 minNotional; // normalized to 18 decimals
}
mapping(bytes32 => Market) public markets; // by marketId
function\_validatePlacement(bytes32 mId, uint256 rawAmount, uint256 rawPrice) internalview {
Market memory m = markets[mId];
// normalize to 18 decimals for checks
uint256 amount18 = rawAmount * (10 ** (18 - m.baseDec));
uint256 price18  = rawPrice  * (10 ** (18 - m.quoteDec));
require(amount18 % m.lot  == 0, "LOT");
require(price18  % m.tick == 0, "TICK");
require(amount18 >= m.minBase, "MIN_BASE");
uint256 notional18 = FullMath.mulDiv(amount18, price18, 1e18);
require(notional18 >= m.minNotional, "MIN_NOTIONAL");
}

17. 自成交预防

如果没有自成交预防 (STP),用户可以 交叉对抗他们自己的挂单(同一地址)以伪造交易量、赚取回扣、操纵价格打印或步进内部队列。在元交易上下文中,_msgSender() 与 msg.sender 不匹配会导致 假阴性/假阳性(通过中继器填写时,做市商看起来不同)。在双边匹配中,未能检测到两个订单共享一个 受益所有者(相同的地址或已知的代理)允许系统洗盘交易。

functionfill(bytes32 hash) external {
Order storage o = orders[hash];
// ❌ no maker==taker guard
_transfer(o.maker, msg.sender, ...);
}
  • 根本原因:
    • 单边成交时没有 maker != taker 保护;双边匹配器中没有 STP 策略
    • 不一致的 发送者解析(EIP-2771 未统一应用)。
    • 缺少 每个市场的 STP 配置(阻止 / 取消最旧 / 递减和取消)以强制执行可预测的反洗规则。
  • 缓解措:
    • 对于单边成交,强制执行 maker != taker。
    • 对于双边匹配,实施 STP 策略(阻止 / 取消最旧 / 递减和取消),并具有清晰、确定的规则。
    • 使用 _msgSender() 来确保元交易一致性。

18. 影响订单的预言机/结算不变性

当订单有效性/结算源自外部标头或预言机(例如,PoW 标头、跨链证明、TWAP 累加器)时,错误的数学或过时的数据 会级联到订单簿中:无效的价格、不正确的放置/更新/取消资格或不安全的结算。常见的错误包括 紧凑难度解码 溢出/下溢、字节序错误、滥用 EVM prevrandao 作为“随机性”以及未能强制执行 单调累积工作量新鲜度限制——所有这些都可能让不正确的状态解锁订单操作。

  • 根本原因:
    • 共识/价格数据的错误 256 位算术编码/字节序
    • 没有 故障关闭 行为(合约继续使用过时/无效的预言机运行)。
    • 当上游源更新或失败时,不 重新验证 派生字段(例如,来自预言机价格的名义价值)。
  • 缓解措施:
    • 在无效或过时的预言机/标头状态下 故障关闭
    • 使用溢出安全代码验证共识数学(例如,来自紧凑位的 PoW 目标)。
    • 强制执行 新鲜度窗口单调工作量(对于标头)或 单调累加器(对于 TWAP)。

19. 更新和派生字段重新计算的不变性

updateOrder 通常会改变主要字段(价格、数量、溢价、行使价),而不会 重新计算 相关字段(名义价值、支付、费用)或重新检查不变性。攻击者可以通过 增量更新 将订单从有效状态“转移”到无效状态,从而绕过仅在 place() 强制执行的检查。部分成交与更新相结合会产生 不一致的状态(例如,支付 > 剩余名义价值)或允许 费用/溢价 超过经济价值。

functionupdateOrder(bytes32 h, Update u) external {
Order storage o = orders[h];
o.price  = u.price;  // ❌ derived fields not recomputed
o.amount = u.amount; // ❌ invariants not rechecked
}
  • 根本原因:
    • 多个入口点 复制 验证逻辑,而不是集中可重用的检查。
    • 每次更改时缺少 原子重新计算 派生值。
    • 不变性没有 单一事实来源,导致创建/更新/取消/成交路径之间出现偏差。
  • 缓解措施:
    • 将所有 派生字段重新计算不变性检查 集中在一个内部函数中;从 placeupdate 和任何更改订单字段的路径中调用它。
    • 针对名义价值和剩余数量重新验证 费用/溢价/支付上限

结论

链上订单簿默认情况下不是不安全的——它们只是放大了你在设计和实施中采取的每一个捷径。我们涵盖的大多数“陷阱”不会出现在正常情况下的测试中。它们出现在负载下、动荡的市场期间,或者当一个对抗性搜索者认为你的协议值得他们花费时间时。

使用这 19 种失效模式作为一份动态清单,而不是一次性阅读。将它们转化为不变性、模糊测试用例、监控警报和审查问题,用于每一个影响你的匹配、结算或风险逻辑的新功能。

实际的下一步很简单:选择当前设计中感觉最脆弱的三个部分,并写下你将如何通过代码或测试证明它们_不能_像这里描述的那样失败。完成此操作后,请问你的团队一个问题:

如果我们下周对我们的订单簿进行重大升级,这些风险中的哪些仍然会让我们夜不能寐?

这个答案应该指导你的下一轮审查。

  • 原文链接: hacken.io/insights/order...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
hacken
hacken
江湖只有他的大名,没有他的介绍。