You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Wagmi signTypedDataAsync签名与Solidity ECDSA.recover验证不匹配排查

问题描述

我通过Wagmi的signTypedDataAsync函数获取订单签名,前端代码如下:

takerOrder.signature = await signTypedDataAsync({
    types: {
        Order: [
            {name: 'salt', type: 'uint256'},
            {name: 'maker', type: 'address'},
            {name: 'signer', type: 'address'},
            {name: 'taker', type: 'address'},
            {name: 'tokenId', type: 'uint256'},
            {name: 'makerAmount', type: 'uint256'},
            {name: 'takerAmount', type: 'uint256'},
            {name: 'expiration', type: 'uint256'},
            {name: 'nonce', type: 'uint256'},
            {name: 'feeRateBps', type: 'uint256'},
            {name: 'side', type: 'uint8'},
            {name: 'signatureType', type: 'uint8'}
        ]
    },
    primaryType: 'Order',
    message: takerOrder
});

尝试在Solidity中验证该签名但失败,相关合约代码如下:

OrderStruct.sol

// SPDX-License-Identifier: MIT
pragma solidity <0.9.0;

bytes32 constant ORDER_TYPEHASH = keccak256(
    "Order(uint256 salt,address maker,address signer,address taker,uint256 tokenId,uint256 makerAmount,uint256 takerAmount,uint256 expiration,uint256 nonce,uint256 feeRateBps,uint8 side,uint8 signatureType)"
);

struct Order {
    /// @notice Unique salt to ensure entropy
    uint256 salt;
    /// @notice Maker of the order, i.e the source of funds for the order
    address maker;
    /// @notice Signer of the order
    address signer;
    /// @notice Address of the order taker. The zero address is used to indicate a public order
    address taker;
    /// @notice Token Id of the CTF ERC1155 asset to be bought or sold
    /// If BUY, this is the tokenId of the asset to be bought, i.e the makerAssetId
    /// If SELL, this is the tokenId of the asset to be sold, i.e the takerAssetId
    uint256 tokenId;
    /// @notice Maker amount, i.e the maximum amount of tokens to be sold
    uint256 makerAmount;
    /// @notice Taker amount, i.e the minimum amount of tokens to be received
    uint256 takerAmount;
    /// @notice Timestamp after which the order is expired
    uint256 expiration;
    /// @notice Nonce used for onchain cancellations
    uint256 nonce;
    /// @notice Fee rate, in basis points, charged to the order maker, charged on proceeds
    uint256 feeRateBps;
    /// @notice The side of the order: BUY or SELL
    Side side;
    /// @notice Signature type used by the Order: EOA, POLY_PROXY or POLY_GNOSIS_SAFE
    SignatureType signatureType;
    /// @notice The order signature
    bytes signature;
}

enum SignatureType
// 0: ECDSA EIP712 signatures signed by EOAs
{
    EOA,
    // 1: EIP712 signatures signed by EOAs that own Polymarket Proxy wallets
    POLY_PROXY,
    // 2: EIP712 signatures signed by EOAs that own Polymarket Gnosis safes
    POLY_GNOSIS_SAFE
}

enum Side
// 0: buy
{
    BUY,
    // 1: sell
    SELL
}

enum MatchType
// 0: buy vs sell
{
    COMPLEMENTARY,
    // 1: both buys
    MINT,
    // 2: both sells
    MERGE
}

struct OrderStatus {
    bool isFilledOrCancelled;
    uint256 remaining;
}

Hashing.sol

// SPDX-License-Identifier: MIT
pragma solidity <0.9.0;

import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol";

import { IHashing } from "../interfaces/IHashing.sol";

import { Order, ORDER_TYPEHASH } from "../libraries/OrderStructs.sol";

abstract contract Hashing is EIP712, IHashing {
    bytes32 public immutable domainSeparator;

    constructor(string memory name, string memory version) EIP712(name, version) {
        domainSeparator = _domainSeparatorV4();
    }

    /// @notice Computes the hash for an order
    /// @param order - The order to be hashed
    function hashOrder(Order memory order) public view override returns (bytes32) {
        return _hashTypedDataV4(
            keccak256(
                abi.encode(
                    ORDER_TYPEHASH,
                    order.salt,
                    order.maker,
                    order.signer,
                    order.taker,
                    order.tokenId,
                    order.makerAmount,
                    order.takerAmount,
                    order.expiration,
                    order.nonce,
                    order.feeRateBps,
                    order.side,
                    order.signatureType
                )
            )
        );
    }
}

Verifier.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

/// @title Signatures
/// @notice Maintains logic that defines the various signature types and validates them
contract Verifier {
    /// @notice Verifies an ECDSA signature
    /// @dev Reverts if the signature length is invalid or the recovered signer is the zero address
    /// @param signer      - Address of the signer
    /// @param structHash  - The hash of the struct being verified
    /// @param signature   - The signature to be verified
    function verifyECDSASignature(address signer, bytes32 structHash, bytes memory signature)
        external
        pure
        returns (bool)
    {
        return ECDSA.recover(structHash, signature) == signer;
    }

}

调用hashOrder获取订单哈希值后,将其作为structHash传入verifyECDSASignature,同时传入订单的signer和signature,但验证返回false,ECDSA.recover得到的地址与订单签名者不一致,需要实现Wagmi signTypedDataAsync与Solidity ECDSA.recover的正确对接。


解决方案

1. 补充EIP-712域信息(前端关键修复)

Wagmi的signTypedDataAsync必须指定完整的domain参数,该参数要与合约中EIP712初始化的域信息完全匹配,否则签名的哈希会和合约计算的不一致。

修复后的前端代码:

takerOrder.signature = await signTypedDataAsync({
  domain: {
    name: "你的合约名称", // 必须和Hashing合约构造函数传入的name一致
    version: "1", // 必须和Hashing合约构造函数传入的version一致
    chainId: 1, // 对应当前网络的链ID,如主网1、Sepolia测试网11155111等
    verifyingContract: "0xYourContractAddress" // 部署后的Hashing合约地址
  },
  types: {
    Order: [
      {name: 'salt', type: 'uint256'},
      {name: 'maker', type: 'address'},
      {name: 'signer', type: 'address'},
      {name: 'taker', type: 'address'},
      {name: 'tokenId', type: 'uint256'},
      {name: 'makerAmount', type: 'uint256'},
      {name: 'takerAmount', type: 'uint256'},
      {name: 'expiration', type: 'uint256'},
      {name: 'nonce', type: 'uint256'},
      {name: 'feeRateBps', type: 'uint256'},
      {name: 'side', type: 'uint8'},
      {name: 'signatureType', type: 'uint8'}
    ]
  },
  primaryType: 'Order',
  message: takerOrder
});

2. 排除签名字段的哈希计算(合约关键修复)

合约中的Order结构体包含signature字段,但前端签名时的message(takerOrder)不包含该字段(因为签名是生成后才附加的)。如果调用hashOrder时传入的Order包含非空的signature,会导致哈希计算错误。

修改合约,新增一个不包含signature的哈希计算函数:

// 在Hashing.sol中添加
function hashOrderForVerification(Order memory order) public view returns (bytes32) {
    return _hashTypedDataV4(
        keccak256(
            abi.encode(
                ORDER_TYPEHASH,
                order.salt,
                order.maker,
                order.signer,
                order.taker,
                order.tokenId,
                order.makerAmount,
                order.takerAmount,
                order.expiration,
                order.nonce,
                order.feeRateBps,
                order.side,
                order.signatureType
            )
        )
    );
}

验证时调用这个函数获取哈希,而不是原来的hashOrder。

3. 确保字段完全匹配

  • 检查前端message中的所有字段值,尤其是side和signatureType,要和合约枚举的数值对应(比如Side.BUY对应0,Side.SELL对应1)。
  • 确保所有数值类型(如uint256)的取值在前端和合约中一致,没有出现精度丢失或类型转换错误。

4. 验证签名格式

Wagmi返回的签名是标准的65字节r+s+v格式,在传入合约前可以在前端打印signature.length确认长度为65,避免截断或格式错误。


内容的提问来源于stack exchange,提问作者jantektsan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 21:09:59