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
相关产品推荐
相关产品推荐

