Back to Blog

安全智能合约开发 (2) — NFT (市场) 中的数字签名使用指南

August 12, 2022
7 min read

1. 引言

数字签名 用于确保真实性和完整性。正如本文所述,“一个有效的数字签名,在满足先决条件的情况下,能让接收者非常有信心地认为消息是由已知发件人创建的(真实性),并且消息在传输过程中未被篡改(完整性)。”

数字签名已广泛应用于智能合约,例如在白名单铸造和订单簿 NFT 市场中。这是因为它可以节省交易成本(链下签名,链上验证)。然而,开发者的滥用也给 NFT 市场带来了风险。在这篇博文中,我们将讨论数字签名在 NFT 生态系统中的滥用。

2. 应用

数字签名已被广泛用于 NFT 合约中的白名单铸造(只有拥有有效签名的用户才能铸造 NFT)和 NFT 市场中的订单验证(只有具有预期签名的订单才能执行)。数据的签名在链下进行,以节省 Gas。接下来,我们将阐述这两种使用场景。

2.1. 白名单铸造

“NFT 铸造”是在区块链上创建 NFT 的过程。大多数 NFT 项目希望推广他们的产品;他们倾向于通过白名单铸造(也称为预售等)来激励用户。赢得名额的人可以以较低的价格(甚至免费)铸造代币。数字签名用于区分白名单铸造者和公开(普通)铸造者。下面是一个白名单铸造实现示例。

function mint_approved(
        vData memory info,
        uint256 number_of_items_requested,
        uint16 _batchNumber
    ) external {
        ...
        require(verify(info), "Unauthorised access secret");
        ...
    }
    function verify(vData memory info) public view returns (bool) {
        require(info.from != address(0), "INVALID_SIGNER");
        bytes memory cat =
            abi.encode(
                info.from,
                info.start,
                info.end,
                info.eth_price,
                info.dust_price,
                info.max_mint,
                info.mint_free
            );
        bytes32 hash = keccak256(cat);
        require(info.signature.length == 65, "Invalid signature length");
        bytes32 sigR;
        bytes32 sigS;
        uint8 sigV;
        bytes memory signature = info.signature;        assembly {
            sigR := mload(add(signature, 0x20))
            sigS := mload(add(signature, 0x40))
            sigV := byte(0, mload(add(signature, 0x60)))
        }        bytes32 data =
            keccak256(
                abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)
            );
        address recovered = ecrecover(data, sigV, sigR, sigS);
        return signer == recovered;
    }

此代码片段来自 Association NFT(存在漏洞 — 请勿复制此代码)。mint_approved() 函数旨在实现白名单铸造:项目所有者对铸造消息(info 变量)进行签名,并将消息发送给允许的铸造者(可以铸造 NFT)。然后,铸造者可以使用签名的变量调用 approved_mint。合约随后会验证消息是否由项目签名(signer == recovered)。如果是,则调用函数的人被允许铸造 NFT(这不安全,因为没有验证调用函数的人是否是白名单中的实际人员)。

2.2. 订单验证

订单验证是数字签名在 NFT 生态系统中的另一个应用。NFT 市场在 NFT 生态系统中发挥着至关重要的作用,因为它们提供了 NFT 的交易功能。由于每个 NFT 代币都是非同质化的,自动化做市商(AMM)交易策略很难在 NFT 市场中使用。因此,大多数 NFT 市场,如 OpenSea、LooksRare 和 X2Y2,都采用订单簿交易模型。

订单簿交易很简单。有一个做市商,即想要以特定价格出售资产的人,和一个接手者,即想要以卖家价格购买资产的人。在这种情况下,订单匹配。在订单簿 NFT 市场中,流程是相同的。唯一的区别在于订单提供过程:NFT 市场使用数字签名进行订单验证。图 1 描述了其中一个订单簿市场(OpenSea)的整个交易流程示例。

**图 1. OpenSea 交易流程**
图 1. OpenSea 交易流程

具体来说,卖家对卖单进行签名并将其存储在 OpenSea 的服务器上。买家可以从 OpenSea 的服务器检索签名的卖单信息,然后使用签名的卖单作为参数调用 NFT 市场合约。市场合约将验证订单,以确保卖家对卖单进行了签名(因为买家发起交易)—— 以防止买家在未经卖家同意的情况下购买资产。

3. 安全事件

霍顿原则 是加密系统的格言,可以表述为“验证所要表达的内容,而非所说出的内容”,或“言行一致,签名即言”。它要求对行为进行完全和精确的签名。如果签名不完整或不准确,结果将是灾难性的。

3.1. Association NFT

回顾第 2.1 节中的 NBA NFT 合约。verify 函数进行了标准的签名验证,但缺少一个关键组件。签名验证仅确保消息是由项目签名的。但是,没有强制执行提供签名的个人与签名消息中的白名单铸造者一致。因此,任何人都可以使用相同的签名通过验证并铸造 NFT。

3.2. OpenSea

另一个安全问题与 OpenSea 相关。2022 年初,研究人员披露了 OpenSea 市场合约(版本:wyvern 2.2)的潜在漏洞,该合约实现了 NFT 交易的核心功能。

在 Wyvern 协议中,用户在链下编写挂单(卖单)或出价(买单),并在链上验证出价的签名。Wyvern 出价包含许多参数,这些参数被聚合到一个字节字符串中以计算出价的摘要。然后,合约将验证摘要的签名。参数聚合方法只是将参数打包到一个字节字符串中,方法如下。

index = ArrayUtils.unsafeWriteAddress(index, order.target);
index = ArrayUtils.unsafeWriteUint8(index, uint8(order.howToCall));
index = ArrayUtils.unsafeWriteBytes(index, order.calldata);
index = ArrayUtils.unsafeWriteBytes(index, order.replacementPattern);
index = ArrayUtils.unsafeWriteAddress(index, order.staticTarget);
index = ArrayUtils.unsafeWriteBytes(index, order.staticExtradata);
index = ArrayUtils.unsafeWriteAddress(index, order.paymentToken);

例如,如果参数由 2 个组件组成:(address,bytes),参数为 (0x9a534628b4062e123ce7ee2222ec20b86e16ca8f,"0xc098"),则聚合后的字节为 0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fc098,即 address + bytes。看起来简单明了,对吧?

现在,考虑一个更复杂的例子,参数结构为 (address,bytes,bytes)

参数 1 是 _(0x9a534628b4062e123ce7ee2222ec20b86e16ca8f, "0xab", "0xcdef")_

参数 2 是 _(0x9a534628b4062e123ce7ee2222ec20b86e16ca8f, "0xabcd", "0xef")_

聚合后的字节为:

参数 1: _0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fabcdef_

参数 2: _0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fabcdef_

哇!两个不同的参数产生了相同的聚合结果,这意味着它们的摘要相同,导致一个签名可以验证两个不同的参数。

这是因为参数中包含许多可变长度的组件。攻击者可以截断变量的一部分,并将截断的部分附加到其之前或之后的组件。不幸的是,Wyvern 合约有许多可变长度的参数,如下所示。

......
    address target;
    /* HowToCall. */
    AuthenticatedProxy.HowToCall howToCall;
    /* Calldata. */
    bytes calldata;
    /* Calldata replacement pattern, or an empty byte array for no replacement. */
    bytes replacementPattern;
    /* Static call target, zero-address for no static call. */
    address staticTarget;
    /* Static call extra data. */
    bytes staticExtradata;
    ......

该漏洞的影响是,攻击者可以(如果可能)控制受害者的账户执行某些恶意行为。漏洞的详细分析请见此处

本节提到的两个安全事件都违反了霍顿原则。具体来说,NBA 合约未在签名消息中包含铸造者(或未检查签名消息中的信息与实际调用者的一致性),而 Wyvern 合约对无结构参数进行签名,导致行为的含义可以被修改,而参数的表示(说法)则保持不变。

4. 建议

遵循霍顿原则,签名你所要表达的内容,而非所说出的内容。签名应包含全面准确的必要信息。

  • 将所有需要验证的信息放入签名中。检查签名消息中的数据与运行时值的一致性(例如,签名消息中的预期用户和实际用户)。
  • 要签名的消息需要确定性地编码,例如,不存在结构不同但编码结果相同的消息。

阅读本系列的另一篇文章

Sign up for the latest updates
The Decentralization Dilemma: Cascading Risk and Emergency Power in the KelpDAO Crisis
Security Insights

The Decentralization Dilemma: Cascading Risk and Emergency Power in the KelpDAO Crisis

This BlockSec deep-dive analyzes the KelpDAO $290M rsETH cross-chain bridge exploit (April 18, 2026), attributed to the Lazarus Group, tracing a causal chain across three layers: how a single-point DVN dependency enabled the attack, how DeFi composability cascaded the damage through Aave V3 lending markets to freeze WETH liquidity exceeding $6.7B across Ethereum, Arbitrum, Base, Mantle, and Linea, and how the crisis forced decentralized governance to exercise centralized emergency powers. The article examines three parameters that shaped the cascade's severity (LTV, pool depth, and cross-chain deployment count) and provides an exclusive technical breakdown of Arbitrum Security Council's forced state transition, an atomic contract upgrade that moved 30,766 ETH without the holder's signature.

Weekly Web3 Security Incident Roundup | Apr 13 – Apr 19, 2026
Security Insights

Weekly Web3 Security Incident Roundup | Apr 13 – Apr 19, 2026

This BlockSec weekly security report covers four attack incidents detected between April 13 and April 19, 2026, across multiple chains such as Ethereum, Unichain, Arbitrum, and NEAR, with total estimated losses of approximately $310M. The highlighted incident is the $290M KelpDAO rsETH bridge exploit, where an attacker poisoned the RPC infrastructure of the sole LayerZero DVN to fabricate a cross-chain message, triggering a cascading WETH freeze across five chains and an Arbitrum Security Council forced state transition that raises questions about the actual trust boundaries of decentralized systems. Other incidents include a $242K MMR proof forgery on Hyperbridge, a $1.5M signed integer abuse on Dango, and an $18.4M circular swap path exploit on Rhea Finance's Burrowland protocol.

Weekly Web3 Security Incident Roundup | Apr 6 – Apr 12, 2026
Security Insights

Weekly Web3 Security Incident Roundup | Apr 6 – Apr 12, 2026

This BlockSec weekly security report covers four DeFi attack incidents detected between April 6 and April 12, 2026, across Linea, BNB Chain, Arbitrum, Optimism, Avalanche, and Base, with total estimated losses of approximately $928.6K. Notable incidents include a $517K approval-related exploit where a user mistakenly approved a permissionless SquidMulticall contract enabling arbitrary external calls, a $193K business logic flaw in the HB token's reward-settlement logic that allowed direct AMM reserve manipulation, a $165.6K exploit in Denaria's perpetual DEX caused by a rounding asymmetry compounded with an unsafe cast, and a $53K access control issue in XBITVault caused by an initialization-dependent check that failed open. The report provides detailed vulnerability analysis and attack transaction breakdowns for each incident.