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
~$15.9M Lost: Trusted Volumes & More | BlockSec Weekly
Security Insights

~$15.9M Lost: Trusted Volumes & More | BlockSec Weekly

This BlockSec bi-weekly security report covers 11 notable attack incidents identified between April 27 and May 10, 2026, across Sui, Ethereum, BNB Chain, Base, Blast, and Berachain, with total estimated losses of approximately $15.9M. Three incidents are analyzed in detail: the highlighted $1.14M Aftermath Finance exploit on Sui, where a signed/unsigned semantic mismatch in the builder-fee validation allowed an attacker to inject a negative fee that was converted into positive collateral during settlement; the $5.87M Trusted Volumes RFQ authorization mismatch on Ethereum; and the $5.7M Wasabi Protocol infrastructure-to-contract-control compromise across multiple EVM chains.

Newsletter - April 2026
Security Insights

Newsletter - April 2026

In April 2026, the DeFi ecosystem experienced three major security incidents. KelpDAO lost ~$290M due to an insecure 1-of-1 DVN bridge configuration exploited via RPC infrastructure compromise, Drift Protocol suffered ~$285M from a multisig governance takeover leveraging Solana's durable nonce mechanism, and Rhea Finance incurred ~$18.4M following a business logic flaw in its margin-trading module that allowed circular swap path manipulatio

~$7.04M Lost: GiddyDefi, Volo Vault & More | BlockSec Weekly
Security Insights

~$7.04M Lost: GiddyDefi, Volo Vault & More | BlockSec Weekly

This BlockSec weekly security report covers eight attack incidents detected between April 20 and April 26, 2026, across Ethereum, Avalanche, Sui, Base, HyperLiquid, and MegaETH, with total estimated losses of approximately $7.04M. The highlighted incident is the $1.3M GiddyDefi exploit, where the attacker did not break any cryptography or use a flash loan but simply replayed an existing on-chain EIP-712 signature with the unsigned `aggregator` and `fromToken` fields swapped out for a malicious contract, demonstrating how partial signature coverage turns any historical signature into a generic permit. Other incidents include a $3.5M Volo Vault operator key compromise on Sui, a $1.5M Purrlend privileged-role takeover, a $413K SingularityFinance oracle misconfiguration, a $142.7K Scallop cross-pool index injection, a $72.35K Kipseli Router decimal mismatch, a $50.7K REVLoans (Juicebox) accounting pollution, and a $64K Custom Rebalancer arbitrary-call exploit.