Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

hardhat does not support EIP-712 for payloads with signed integers #407

Closed
willmeister opened this issue Apr 24, 2024 · 5 comments · Fixed by #446
Closed

hardhat does not support EIP-712 for payloads with signed integers #407

willmeister opened this issue Apr 24, 2024 · 5 comments · Fixed by #446
Assignees
Milestone

Comments

@willmeister
Copy link

Version of Hardhat

2.22.3

What happened?

Attempting to create an EIP-712 signature for a type that contains a signed integer (e.g. int256), results in an error in one of two ways:

  1. If the value is a positive int256, the resulting signature is not correct (as verified by ethers.verifyTypedData(...))
  2. If the value is a negative int256, the provider errs with ProviderError: Failed to serialize serde JSON object

Note: The same operation succeeds if the type is instead a uint256.

Minimal reproduction steps

Fresh install unit test example

  1. npm install --save-dev hardhat
  2. npx hardhat init (choose to use typescript without viem, default for everything else)
  3. Copy the following test into a new file in test/sigTest.ts:
import { ethers } from 'hardhat'
import { expect } from 'chai'

describe.only('sig example', function () {
  it(`should recover typed signature for uint`, async function () {
    const [signer] = await ethers.getSigners()

    const preimage = {
      domain: {
        name: 'TestName',
        version: '1',
        chainId: 31337n,
        verifyingContract: '0x1111111111111111111111111111111111111111'
      },
      type: {
        Test: [{ name: 'amount', type: 'uint256' }]
      },
      value: {
        amount: 1234n
      }
    }

    const sig = await signer.signTypedData(preimage.domain, preimage.type, preimage.value)
    const signingAddress = ethers.verifyTypedData(preimage.domain, preimage.type, preimage.value, sig)
    expect(signingAddress).to.equal(await signer.getAddress(), 'signing address mismatch')
  })

  for (const abiEncoded of [false, true]) {
    for (const amount of [1234n, -1234n]) {
      it(`should recover typed signature for${abiEncoded ? ' abi-encoded' : ''} int ${amount}`, async function () {
        const [signer] = await ethers.getSigners()

        const amt = abiEncoded ? ethers.AbiCoder.defaultAbiCoder().encode(['int256'], [amount]) : amount

        const preimage = {
          domain: {
            name: 'TestName',
            version: '1',
            chainId: 31337n,
            verifyingContract: '0x1111111111111111111111111111111111111111'
          },
          type: {
            Test: [{ name: 'amount', type: 'int256' }]
          },
          value: {
            amount: amt
          }
        }

        const sig = await signer.signTypedData(preimage.domain, preimage.type, preimage.value)
        const signingAddress = ethers.verifyTypedData(preimage.domain, preimage.type, preimage.value, sig)
        expect(signingAddress).to.equal(await signer.getAddress(), 'address mismatch')
      })
    }
  }
})
  1. npx hardhat test
  2. Observe the uint256 signature succeeding and the int256 signature either throwing or being uncrecoverable:
npx hardhat test


  sig example
    ✔ should recover typed signature for uint (446ms)
    1) should recover typed signature for int 1234
    2) should recover typed signature for int -1234
    3) should recover typed signature for abi-encoded int 1234
    4) should recover typed signature for abi-encoded int -1234


  1 passing (470ms)
  4 failing

  1) sig example
       should recover typed signature for int 1234:

      address mismatch
      + expected - actual

      -0x254b0d7c18729b651194c4e2308284798580B37b
      +0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266

      at Context.<anonymous> (test/sigTest.ts:56:35)

  2) sig example
       should recover typed signature for int -1234:
     ProviderError: Failed to serialize serde JSON object
      at EdrProviderWrapper.request (/path/to/node_modules/hardhat/src/internal/hardhat-network/provider/provider.ts:430:19)
      at async Context.<anonymous> (/path/to/test/sigTest.ts:53:21)

  3) sig example
       should recover typed signature for abi-encoded int 1234:

      address mismatch
      + expected - actual

      -0x254b0d7c18729b651194c4e2308284798580B37b
      +0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266

      at Context.<anonymous> (test/sigTest.ts:56:35)

  4) sig example
       should recover typed signature for abi-encoded int -1234:
     TypeError: value out-of-bounds for int256 (argument="value", value=115792089237316195423570985008687907853269984665640564039457584007913129638702, code=INVALID_ARGUMENT, version=6.12.0)
      at makeError (node_modules/ethers/src.ts/utils/errors.ts:687:21)
      at assert (node_modules/ethers/src.ts/utils/errors.ts:715:25)
      at assertArgument (node_modules/ethers/src.ts/utils/errors.ts:727:5)
      at /path/to/node_modules/ethers/src.ts/hash/typed-data.ts:140:31
      at /path/to/node_modules/ethers/src.ts/hash/typed-data.ts:388:57
      at Array.map (<anonymous>)
      at /path/to/node_modules/ethers/src.ts/hash/typed-data.ts:387:39
      at TypedDataEncoder.encodeData (node_modules/ethers/src.ts/hash/typed-data.ts:413:37)
      at TypedDataEncoder.encode (node_modules/ethers/src.ts/hash/typed-data.ts:427:21)
      at Function.getPayload (node_modules/ethers/src.ts/hash/typed-data.ts:619:17)

Test via JSON RPC pointing at local hardhat node:

  1. Install hardhat 2.22.3
  2. Run npx hardhat node
  3. Run curl {"jsonrpc":"2.0","id":"1","method":"eth_signTypedData_v4","params":["0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",{"types":{"Test":[{"name":"amount","type":"int256"}],"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}]},"domain":{"name":"TestName","version":"1","chainId":"0x7a69","verifyingContract":"0x1111111111111111111111111111111111111111"},"primaryType":"Test","message":{"amount":"-100000000"}}]} http://127.0.0.1:8545/
  4. Note Failed to serialize serde JSON object error
  5. Run the above curl command with {"amount":"100000000"} (remove negative sign)
  6. Note successful response

Note: the node seems to handle an abi-encoded negative int256 ( e.g. 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0a1f00), but ethers.js v6 encodes BigInts to the decimal string, and hardhat-ethers signTypedData(...) does the same, indicating that signed decimal integers should be supported, and as pointed out by the unit test example, this does not work end-to-end using hardhat either.

Search terms

eth_signTypedData_v4 signTypedData EIP-712 int

@willmeister
Copy link
Author

This issue appears to be limited to the hardhat node. If I update the unit test above to construct an ethers.Wallet so that the signing is done within JS rather than via RPC, it works properly.

Updated test:

  for (const rpcSigner of [false, true]) {
    for (const abiEncoded of [false, true]) {
      for (const amount of [1234n, -1234n]) {
        it(`should recover typed signature for${abiEncoded ? ' abi-encoded' : ''} int ${amount} with ${rpcSigner ? 'rpc' : 'wallet'} signer`, async function () {
          const hardhatPk = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
          const [s] = await ethers.getSigners()
          const signer = rpcSigner ? s : new ethers.Wallet(hardhatPk, s.provider)

          const amt = abiEncoded ? ethers.AbiCoder.defaultAbiCoder().encode(['int256'], [amount]) : amount

          const preimage = {
            domain: {
              name: 'TestName',
              version: '1',
              chainId: 31337n,
              verifyingContract: '0x1111111111111111111111111111111111111111'
            },
            type: {
              Test: [{ name: 'amount', type: 'int256' }]
            },
            value: {
              amount: amt
            }
          }

          const sig = await signer.signTypedData(preimage.domain, preimage.type, preimage.value)
          const signingAddress = ethers.verifyTypedData(preimage.domain, preimage.type, preimage.value, sig)
          expect(signingAddress).to.equal(await signer.getAddress(), 'address mismatch')
        })
      }
    }
  }

@fvictorio fvictorio transferred this issue from NomicFoundation/hardhat Apr 25, 2024
@fvictorio fvictorio assigned fvictorio and unassigned kanej and fvictorio Apr 25, 2024
@fvictorio
Copy link
Member

Thanks @willmeister, I'll look into this.

@fvictorio
Copy link
Member

Thanks for the great reproduction steps @willmeister! I can confirm that this wasn't happening with hardhat@2.20.1, the last version of Hardhat before migrating to EDR. The only scenario where the tests also fail in that version is when amount is negative and abiEncoded is true, but that doesn't work with the wallet signer either, so I guess it's expected.

fvictorio added a commit that referenced this issue May 1, 2024
@fvictorio fvictorio linked a pull request May 1, 2024 that will close this issue
@fvictorio
Copy link
Member

The underlying cause seems to be gakonst/ethers-rs#2785

Hopefully that gets fixed and released soon. If not, we should try to find a workaround (perhaps migrating this part of the code to alloy is the solution, and it's something we need to eventually do anyway since ethers-rs is deprecated).

@agostbiro
Copy link
Collaborator

agostbiro commented May 15, 2024

We've released EDR v0.3.8 with a fix for this issue.

To upgrade to the latest EDR version immediately, you can remove your node_modules directory and npm/yarn/pnpm lock file and reinstall your dependencies. Otherwise you can wait for the next Hardhat release which will automatically use the latest EDR version.

You can verify that you’re using the latest version of EDR by running the following command: npm ls @nomicfoundation/edr.

@Wodann Wodann added this to the EDR v0.3.8 milestone May 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
5 participants