0

Grateful in advance for any help on this issue.

I'm creating an EIP712 signature with TypedData that includes a nested array of structs, and it is returning the wrong signer when I do the encoding in my solidity smart contract.
If I do the encoding in ethers.js and send it as bytes32 to the contract for consumption in the signature recovery, the correct signer is returned.
Can't figure out what I am doing wrong.

This is the TypedData that includes the nested array of structs Predicate[] . Predicate[] is the type for items in the LoanTermsWithItems TypeData below.

    types: {
        LoanTermsWithItems: [
            { name: "durationSecs", type: "uint32" },
            { name: "deadline", type: "uint32" },
            { name: "proratedInterestRate", type: "uint160" },
            { name: "principal", type: "uint256" },
            { name: "collateralAddress", type: "address" },
            { name: "items", type: "Predicate[]" },
            { name: "payableCurrency", type: "address" },
            { name: "affiliateCode", type: "bytes32" },
            { name: "nonce", type: "uint160" },
            { name: "side", type: "uint8" },
        ],
        Predicate: [
            { name: "data", type: "bytes" },
            { name: "verifier", type: "address" },
        ],
    },
    primaryType: "LoanTermsWithItems" as const,
};

These are the typehashes in the contract:

    bytes32 private constant _ITEMS_TYPEHASH =
        keccak256(
            "LoanTermsWithItems(uint32 durationSecs,uint32 deadline,uint160 proratedInterestRate,uint256 principal,address collateralAddress,Predicate[] items,address payableCurrency,bytes32 affiliateCode,uint160 nonce,uint8 side)Predicate(bytes data,address verifier)"
        );

    
    bytes32 public constant _PREDICATE_TYPEHASH =
        keccak256(
            "Predicate(bytes data,address verifier)"
        );

To use this data for signature recovery in the smart contract, I am encoding each item in the Predicate array separately with its typehash, then concacting all the individual hashes into a single itemsHash with abi.encodePacked() as shown here:

function _encodeData(LoanLibrary.Predicate[] memory predicates) public view returns (bytes32 itemsHash) {
       bytes32[] memory itemHashes = new bytes32[](predicates.length);

        for(uint i = 0; i < predicates.length; ++i){
            itemHashes[i] = keccak256(
                abi.encode(
                    _PREDICATE_TYPEHASH,
                    predicates[i].data,
                    predicates[i].verifier
                )
            );
        }

        // concatenate all predicate hashes
        itemsHash = keccak256(abi.encodePacked(itemHashes));

        return itemsHash;
    }

This is the returned itemsHash 0xe1b3a8e6e495f169e12db9e070738e20e0e4e574dfa556608d77a1ee576ddab3 and I am getting the wrong recovered signer.

If I encode the array of structs in ethers to send as bytes32 to the contract this is the generated itemsHash 0x9adcc5563cba61c299700498f96e5ed258343c4058f7035c9d3862f72a271a9d and the correct signer is recovered.

This is the function I was using in hardhat ethers to do the encoding:

export const encodePredicates = (predicates: ItemsPredicate[]): string => {
    const types = ["(bytes,address)[]"];
    const values = predicates.map(p => [p.data, p.verifier]);

    const coded = ethers.utils.defaultAbiCoder.encode(types, [values]);
    return ethers.utils.keccak256(coded);
};

This is the recovery function in my smart contract

function recoverSignature(
        LoanLibrary.LoanTerms calldata loanTerms,
        Signature calldata sig,
        uint160 nonce,
        Side side,
        bytes32 itemsHash
    ) public view override returns (bytes32 sighash, address signer) {
        bytes32 loanHash = keccak256(
            abi.encode(
                _ITEMS_TYPEHASH,
                loanTerms.durationSecs,
                loanTerms.deadline,
                loanTerms.proratedInterestRate,
                loanTerms.principal,
                loanTerms.collateralAddress,
                itemsHash,
                loanTerms.payableCurrency,
                loanTerms.affiliateCode,
                nonce,
                uint8(side)
            )
        );

        sighash = _hashTypedDataV4(loanHash);
        signer = ECDSA.recover(sighash, sig.v, sig.r, sig.s);
    }

Any help on what I am doing wrong in my solidity encoding would be hugely appreciated.


Additional Info added 6/19:

I printed the data for encoding and the corresponding resulting encoded data created in both ethers and solidity. The result is completely different.

IN ETHERS: Values to encode:

 [
    '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fb2d50040ab4a647dbe14e0edec1a9a2f02c3870000000000000000000000000fb2d50040ab4a647dbe14e0edec1a9a2f02c38700000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000',
    '0x97F030e7b07DF08049e4aD29fAAf6b99450a2efE'
  ]

Code for encoding:

const coded = 
ethers.utils.defaultAbiCoder.encode(types, [values])

Result:

0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000097f030e7b07df08049e4ad29faaf6b99450a2efe00000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fb2d50040ab4a647dbe14e0edec1a9a2f02c3870000000000000000000000000fb2d50040ab4a647dbe14e0edec1a9a2f02c38700000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000

IN SOLIDITY: Values to encode:

0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fb2d50040ab4a647dbe14e0edec1a9a2f02c3870000000000000000000000000fb2d50040ab4a647dbe14e0edec1a9a2f02c38700000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000

Code for encoding:

itemHashes[i] = keccak256(
                abi.encode(
                    _PREDICATE_TYPEHASH,
                    predicates[i].data,
                    predicates[i].verifier
                )
            );

itemHashes[i] Result:

0x02a1aaf52c27c74d616ab46413963a8964fdc91fe8cfe6d82d07e0e666faed28
3
  • hard to tell without the full signature pattern. If you validate with Ethers but not in the contract, Check the orders, chainId, name, version, (verifying contract address as ethers don't care about it) Jun 18, 2023 at 23:32
  • Hi @MadeInDreams thank you for commenting. I checked the signature pattern, it is identical in the ethers version and solidity version. Something is off in the encoding itself. Please see the additional info i just added to the post above, showing the results of the encoding being completely different in ethers vs. solidity, even when starting off with the same data.
    – em_mutable
    Jun 19, 2023 at 12:00
  • Figured out the issue. In the EIP it says that The dynamic values bytes and string are encoded as a keccak256 hash of their contents. I was not hashing them before passing them into _encodeData. I refactored to keccak256(predicates[i].data) in itemHashes[i] and the code is now working.
    – em_mutable
    Jun 19, 2023 at 13:45

1 Answer 1

0

You need to ensure that the EIP domain is set correctly, but the solidity would look similar to this:

using ECDSA for bytes32;

string private constant _ITEMS_TYPE = "LoanTermsWithItems(uint32 durationSecs,uint32 deadline,uint160 proratedInterestRate,uint256 principal,address collateralAddress,Predicate[] items,address payableCurrency,bytes32 affiliateCode,uint160 nonce,uint8 side)"
string private constant _PREDICATE_TYPE = "Predicate(bytes data,address verifier)"
bytes32 private constant _ITEMS_TYPEHASH = keccak256(abi.encodePacked(_ITEMS_TYPE, _PREDICATE_TYPE));
bytes32 private constant _PREDICATE_TYPEHASH = keccak256(abi.encodePacked(_PREDICATE_TYPE));

function recoverSignature(
    uint8 _v,
    bytes32 _r,
    bytes32 _s,
    LoanLibrary.LoanTerms calldata loanTerms,
    uint160 nonce,
    Side side,
    Predicate[] calldata items
) public view override returns (bytes32 sighash, address signer) {

  bytes32[] memory itemsHashes = new bytes32[](items.length);
  for (uint i = 0; i < items.length; i++) {
    itemsHashes[i] = keccak256(abi.encode(_PREDICATE_TYPEHASH, items[i].data, items[i].verifier));
  }

  bytes32 loanHash = keccak256(
    abi.encode(
      _ITEMS_TYPEHASH,
      loanTerms.durationSecs,
      loanTerms.deadline,
      loanTerms.proratedInterestRate,
      loanTerms.principal,
      loanTerms.collateralAddress,
      keccak256(abi.encodePacked(itemsHashes)),
      loanTerms.payableCurrency,
      loanTerms.affiliateCode,
      nonce,
      side
    )
  );

  sighash = _hashTypedDataV4(loanHash);
  signer = sighash.recover(_v, _r, _s);
}

With ethers version 6:

const chainId = (await ethers.provider.getNetwork()).chainId;
const verifyingContract = await loanContract.getAddress();

const request = {
  domain: {
    name: 'LoanTermsWithItems',
    version: '1',
    chainId: chainId,
    verifyingContract,
  },
  types: {
    LoanTermsWithItems: [
      { name: 'durationSecs', type: 'uint32' },
      { name: 'deadline', type: 'uint32' },
      { name: 'proratedInterestRate', type: 'uint160' },
      { name: 'principal', type: 'uint256' },
      { name: 'collateralAddress', type: 'address' },
      { name: 'items', type: 'Predicate[]' },
      { name: 'payableCurrency', type: 'address' },
      { name: 'affiliateCode', type: 'bytes32' },
      { name: 'nonce', type: 'uint160' },
      { name: 'side', type: 'uint8' },
    ],
    Predicate: [
      { name: 'data', type: 'bytes' },
      { name: 'verifier', type: 'address' },
    ],
  },
  message: {
    // the values
  }
};

const signature = await owner.signTypedData(
  request.domain,
  request.types,
  request.message,
);

const sigBreakdown = ethers.Signature.from(signature);

// make the request with sigBreakdown.v, sigBreakdown.r, sigBreakdown.s

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.