如何针对嵌套结构体进行EIP712签名?

本篇文章会给大家介绍如何对嵌套结构体进行签名(EIP712 TypedData Encoding With Nested Array of Structs)。本文案例是结构体中嵌套其他结构体数组,与结构体中嵌套其他结构体处理方式一样。

如果你对EIP712相关概念和知识了解不多,可以先阅读下面这篇文章。

https://mirror.xyz/xyyme.eth/cJX3zqiiUg2dxB1nmbXbDcQ1DSdajHP5iNgBc6wEZz4

在某个项目中,需要将下面代码块中的StrategyRequest 结构体进行EIP712签名,不过在测试中,发现和使用ethers v6 生成的structHash 不一致,最后是如何排查解决的呢?

    enum OptionType {
        LONG_CALL,
        LONG_PUT,
        SHORT_CALL,
        SHORT_PUT
    }

    struct Option {
        uint256 positionId;
        // underlying asset address
        address underlying;
        // option strike price (with 18 decimals)
        uint256 strikePrice;
        int256 premium;
        // option expiry timestamp
        uint256 expiryTime;
        // order size
        uint256 size;
        // option type
        OptionType optionType;
        bool isActive;
    }

    struct Future {
        uint256 positionId;
        // underlying asset address
        address underlying;
        // (with 18 decimals)
        uint256 entryPrice;
        // future expiry timestamp
        uint256 expiryTime;
        // order size
        uint256 size;
        bool isLong;
        bool isActive;
    }

    struct StrategyRequest {
        address admin;
        uint256 timestamp;
        uint256 mergeId;
        CollateralInfo[] collaterals;
        Option[] option;
        Future[] future;
    }

一、首先查看结构体typehash是否正确。由于该结构体是多层嵌套不同结构体,按照EIP712标准,当struct 里面还有 struct 的情况,内部struct 的签名对象类型哈希中的结构体对象名称需要按照字母从a到z拼接;因此StrategyRequest结构体中的内部结构体排序之后为:1:CollateralInfo,2:Future,3:Option;代码如下:

//keccak256("StrategyRequest(address admin,uint256 timestamp,uint256 mergeId,CollateralInfo[] collaterals,Option[] option,Future[] future)CollateralInfo(address collateralToken,uint256 collateralAmount)Future(uint256 positionId,address underlying,uint256 entryPrice,uint256 expiryTime,uint256 size,bool isLong,bool isActive)Option(uint256 positionId,address underlying,uint256 strikePrice,int256 premium,uint256 expiryTime,uint256 size,uint256 optionType,bool isActive)");
bytes32 public constant STRATEGY_REQUEST_TYPE_HASH =
        0xc9571297085db7b4ed482bfb5dc48ceb0aa85911a4dd114186601baab392814c;

二、生成其他结构体的typehash

    //keccak256("CollateralInfo(address collateralToken,uint256 collateralAmount)");
    bytes32 public constant COLLATERAL_INFO_TYPE_HASH =
        0x9ad6f6e9b22cfe4c61d51d75600afaaae0fe0f7dbc24f035d03f68b7f87920fa;

    //keccak256("Future(uint256 positionId,address underlying,uint256 entryPrice,uint256 expiryTime,uint256 size,bool isLong,bool isActive)");
    bytes32 public constant FUTURE_TYPE_HASH = 0xcdff66689589cd15845093f3be135b778815fc2b8dfa35ff5112e645191afe86;

    //keccak256("Option(uint256 positionId,address underlying,uint256 strikePrice,int256 premium,uint256 expiryTime,uint256 size,uint256 optionType,bool isActive)");
    bytes32 public constant OPTION_TYPE_HASH = 0xbc63504838568be333400315a4bfe079d052fe27fe59b4bdac11192ccbca3e47;

三、结构体hash

在一个结构体中里面没有其他结构体情况下,structHash生成采用以下方式:

   struct StrategyRequest {
        address admin;
        uint256 timestamp;
        uint256 mergeId;
    }

    function _structHash(StrategyRequest memory strategy) internal pure returns (bytes32) {
        bytes32 structHash = keccak256(
            abi.encode(
                Constants.STRATEGY_REQUEST_TYPE_HASH,
                strategy.admin,
                strategy.timestamp,
                strategy.mergeId
            )
        );
        return structHash;
    }

在一个结构体中里面有其他结构体情况下,structHash生成采用以下方式:

1、首先把其他结构体的每个字段进行整体abi.encode之后进行keccak256

2、如果涉及内部字段是数组结构体参数,需要先实现第1步,然后将所有的keccak256 hash 进行abi.encode 之后再进行keccak256

3、将keccak256后的变量作为参数放置到structHash 中的abi.encode中

4、最后再进行keccak256之后即可生成structHash

    function _structHash(StrategyTypes.StrategyRequest memory strategy) internal pure returns (bytes32) {
    bytes32 structHash = keccak256(
        abi.encode(
            Constants.STRATEGY_REQUEST_TYPE_HASH,
            strategy.admin,
            strategy.timestamp,
            strategy.mergeId,
            _hashCollateral(strategy.collaterals),
            _hashOption(strategy.option),
            _hashFuture(strategy.future)
        )
    );
    return structHash;
}


    function _hashStrategy(StrategyTypes.StrategyRequest memory strategy) internal view returns (bytes32) {
        bytes32 structHash = _structHash(strategy);
        return _hashTypedDataV4(structHash);
    }

    function _hashCollateral(StrategyTypes.CollateralInfo[] memory collaterals) internal pure returns (bytes32) {
        uint256 len = collaterals.length;
        bytes32[] memory collateralsHashes = new bytes32Unsupported embed;
        for (uint256 i; i < len; ) {
            collateralsHashes[i] = keccak256(abi.encode(Constants.COLLATERAL_INFO_TYPE_HASH, collaterals[i]));
            unchecked {
                ++i;
            }
        }
        bytes32 collateralsHash = keccak256(abi.encodePacked(collateralsHashes));
        return collateralsHash;
    }

    function _hashOption(StrategyTypes.Option[] memory options) internal pure returns (bytes32) {
        uint256 len = options.length;
        bytes32[] memory optionsHashes = new bytes32Unsupported embed;
        for (uint256 i; i < len; ) {
            optionsHashes[i] = keccak256(
                abi.encode(
                    Constants.OPTION_TYPE_HASH,
                    options[i].positionId,
                    options[i].underlying,
                    options[i].strikePrice,
                    options[i].premium,
                    options[i].expiryTime,
                    options[i].size,
                    options[i].optionType,
                    options[i].isActive
                )
            );
            unchecked {
                ++i;
            }
        }
        bytes32 optionsHash = keccak256(abi.encodePacked(optionsHashes));
        return optionsHash;
    }

    function _hashFuture(StrategyTypes.Future[] memory futures) internal pure returns (bytes32) {
        uint256 len = futures.length;
        bytes32[] memory futuresHashes = new bytes32Unsupported embed;
        for (uint256 i; i < len; ) {
            futuresHashes[i] = keccak256(
                abi.encode(
                    Constants.FUTURE_TYPE_HASH,
                    futures[i].positionId,
                    futures[i].underlying,
                    futures[i].entryPrice,
                    futures[i].expiryTime,
                    futures[i].size,
                    futures[i].isLong,
                    futures[i].isActive
                )
            );
            unchecked {
                ++i;
            }
        }
        // 这里使用abi.encodePacked和 abi.encode都可以,因为里面参数为bytes32数组,打包之后效果一样
        bytes32 futuresHash = keccak256(abi.encodePacked(futuresHashes));
        return futuresHash;
    }

四,使用ethers输出日志和链上结果比较

const domain = {
    name: "XXX Protocol",
    version: "1",
    chainId: 10,
    verifyingContract: "0x1f9090aae28b8a3dceadf281b0f12828e676c326",
  };

  const types = {
    StrategyRequest: [
      { name: "admin", type: "address" },
      { name: "timestamp", type: "uint256" },
      { name: "mergeId", type: "uint256" },
      { name: "collaterals", type: "CollateralInfo[]" },
      { name: "option", type: "Option[]" },
      { name: "future", type: "Future[]" },
    ],
    CollateralInfo: [
      { name: "collateralToken", type: "address" },
      { name: "collateralAmount", type: "uint256" },
    ],
    Future: [
      { name: "positionId", type: "uint256" },
      { name: "underlying", type: "address" },
      { name: "entryPrice", type: "uint256" },
      { name: "expiryTime", type: "uint256" },
      { name: "size", type: "uint256" },
      { name: "isLong", type: "bool" },
      { name: "isActive", type: "bool" },
    ],
    Option: [
      { name: "positionId", type: "uint256" },
      { name: "underlying", type: "address" },
      { name: "strikePrice", type: "uint256" },
      { name: "premium", type: "int256" },
      { name: "expiryTime", type: "uint256" },
      { name: "size", type: "uint256" },
      { name: "optionType", type: "uint256" },
      { name: "isActive", type: "bool" },
    ],
  };

  const data = {
    admin: "0x45ee97cD5f227EdC94376A71a094F1D05ad0d151",
    timestamp: 1701092243,
    mergeId: 6,
    collaterals: [
      {
        collateralToken: "0x6495ce3e8808d7f4edf7e6e61e97099473b7a962",
        collateralAmount: "8",
      },
      {
        collateralToken: "0x6495ce3e8808d7f4edf7e6e61e97099473b7a962",
        collateralAmount: "8",
      }
    ],
    future: [
      {
        positionId: 0,
        underlying: "0x650c92218306ef82c9ebdb5297ff2288f05c24a8",
        entryPrice: "19200000000000000000000",
        expiryTime: "1703836800",
        size: "2000000000000000000",
        isLong: true,
        isActive: true,
      },
    ],
    option: [
      {
        positionId: 0,
        underlying: "0x650c92218306ef82c9ebdb5297ff2288f05c24a8",
        strikePrice: "19200000000000000000000",
        premium: "44000000000000000000",
        expiryTime: "1703836800",
        size: "2000000000000000000",
        optionType: 0,
        isActive: true,
      },
    ],
  };

  let strategyManagerAddr = config[network.name].strategyManager;
  const eip712FacetInterface = await ethers.getContractAt("EIP712Facet", strategyManagerAddr);
  console.log("hashDomain:\t", ethers.TypedDataEncoder.hashDomain(domain));

  // let structHash = await eip712FacetInterface.structHash(data);
  // console.log("structHash1:\t", structHash.toString());

  const hashStruct = ethers.TypedDataEncoder.hashStruct("StrategyRequest", types, data);
  console.log("structHash2:\t", hashStruct);

  // let result = await eip712FacetInterface.hashStrategy(data);
  // console.log("typeDataHash1:\t", result.toString());

  const typeDataHash = ethers.TypedDataEncoder.hash(domain, types, data);
  console.log("typeDataHash2:\t", typeDataHash);

参考链接

https://eips.ethereum.org/EIPS/eip-712