Solidity中如何为带嵌套结构体的EIP712类型数据定义TYPEHASH?
EIP-712嵌套结构体与动态数组的TYPEHASH定义及签名验证修正
问题根源
你当前的实现未遵循EIP-712对嵌套结构体和动态数组的哈希规则,这是签名恢复地址错误的核心原因:
- 嵌套结构体需要单独定义TYPEHASH,且编码时需传入该结构体实例的哈希值,而非直接传入结构体
- 动态数组不能直接编码,必须先对数组内每个元素的哈希值做拼接后再整体哈希
正确实现步骤
1. 定义所有结构体的独立TYPEHASH
每个结构体(包括嵌套的Fee)都需要单独定义TYPEHASH,字符串格式必须严格匹配结构体字段顺序和类型:
bytes32 public constant FEE_TYPEHASH = keccak256("Fee(address recipient,uint256 value)"); bytes32 public constant REQUEST_TYPEHASH = keccak256("Request(address to,address from,Fee[] fees)");
2. 处理动态数组的哈希计算
对于Fee[]这类动态数组,需遍历每个元素计算哈希,再将所有哈希值拼接后做整体哈希:
function hashFees(Fee[] calldata fees) public pure returns (bytes32) { bytes32[] memory feeHashes = new bytes32[](fees.length); for (uint256 i = 0; i < fees.length; i++) { feeHashes[i] = keccak256(abi.encode(FEE_TYPEHASH, fees[i].recipient, fees[i].value)); } return keccak256(abi.encodePacked(feeHashes)); }
3. 修正上层结构体的编码逻辑
encodeRequest函数需要传入嵌套结构体的哈希值、动态数组的哈希值,而非直接传入结构体或数组实例:
function encodeRequest(Request calldata request) public pure returns (bytes memory) { return abi.encode(REQUEST_TYPEHASH, request.to, request.from, hashFees(request.fees)); }
完整修正后的合约代码
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; contract SignatureChecker is EIP712 { using ECDSA for bytes32; struct Fee { address recipient; uint256 value; } struct Request { address to; address from; Fee[] fees; } bytes32 public constant FEE_TYPEHASH = keccak256("Fee(address recipient,uint256 value)"); bytes32 public constant REQUEST_TYPEHASH = keccak256("Request(address to,address from,Fee[] fees)"); constructor() EIP712("SignatureChecker", "1") {} function verify( Request calldata request, bytes calldata signature, address supposedSigner ) external view returns (bool) { return recoverAddress(request, signature) == supposedSigner; } function recoverAddress( Request calldata request, bytes calldata signature ) public view returns (address) { return _hashTypedDataV4(keccak256(encodeRequest(request))).recover(signature); } function encodeRequest(Request calldata request) public pure returns (bytes memory) { return abi.encode(REQUEST_TYPEHASH, request.to, request.from, hashFees(request.fees)); } function hashFees(Fee[] calldata fees) public pure returns (bytes32) { bytes32[] memory feeHashes = new bytes32[](fees.length); for (uint256 i = 0; i < fees.length; i++) { feeHashes[i] = keccak256(abi.encode(FEE_TYPEHASH, fees[i].recipient, fees[i].value)); } return keccak256(abi.encodePacked(feeHashes)); } }
关键规则说明
- 嵌套结构体:每个嵌套结构体必须单独定义TYPEHASH,编码时先计算该结构体实例的哈希(
keccak256(abi.encode(结构体TYPEHASH, 字段值...))),再将哈希值传入上层结构体编码 - 动态数组:先对数组内每个元素计算哈希(遵循结构体哈希规则),将所有哈希值组成数组后,执行
keccak256(abi.encodePacked(哈希数组))得到最终数组哈希值,再传入上层编码 - TYPEHASH格式:字符串必须严格遵循
结构体名(类型1 字段名1,类型2 字段名2,...)格式,字段顺序、类型必须与合约定义完全一致,不能有多余空格
内容的提问来源于stack exchange,提问作者ethadvocate123
相关产品推荐
相关产品推荐

