Issues Getting ERC20Permit Function to Work with Gasless Transactions

Hi team, is there an example of how to properly call the ERC20Permit function on a smart contract using Biconomy? We have a token that implements this method; however, we get an execution reverted: Permit: invalid signature error when hitting one of the require guards in the permit function. This guard expects the owner to be the signer, which is the case.

require(signer == owner, "ERC20Permit: invalid signature");

It appears to break after the contract recovers the address from the v , r , s params passed to the permit function. We also see the same error when attempting to call permit on USDC. If I attempt to recover the signer’s address manually I notice that I get back the same address but the letters that have been capitalized in the signer’s address are all lowercase. I believe that this may be why we’re unable to get past the guard in the permit function. We are using eth_signTypedData_v4 to sign the message.

Please let me know if I can provide any additional information but at this point I’m looking for an example of how to properly use Biconomy to call ERC20Permit on a contract that has implemented it and I should be able to take it from there. Thanks in advance for your help on this.

Hi,
Can you share the details how you’re signing for permit. It would also depend on the type hash in your contract and domain data , message you’re passing.
For reference you can make use of this : mexa-sdk/PermitClient.js at master · bcnmy/mexa-sdk · GitHub
It ships with Biconomy SDK and can be used for DAI / EIP2612 type permits

When signing the request owner should be toChecksumAddress

Thanks. Will refactor my implementation using this example to see if I’m able to get a step further. Will post the details here if that doesn’t work.

1 Like

Here is how I’m making the call to the ERC20 permit function using the PermitClient as suggested above. I’m able to successfully make the call but get the same invalid signature error as I did in my previous approach.

export const callPermitMethod = async (walletProvider, userAddress, contractAddress, contractData) => {
    const domainData = {
        name: 'Chained Metrics',
        version: '1',
        chainId: 137,
        verifyingContract: contractAddress  // 0x690bF64128400f6477B3760d64C1f2C728e8017e
    };

    const deadline = (Math.round(Date.now() / 1000)) + 30;

    const permitClient = new PermitClient(walletProvider, userAddress, contractAddress);

    const permitOptions = {
        domainData,
        spender: contractData.broker_address, // 0x4e25c0123779657477CCD3558114334610B3e335
        value: contractData.value.toString(),
        deadline,
        userAddress,
    };

    await permitClient.eip2612Permit(permitOptions);
};

Here is the original way I was making the call, which yields the same invalid signature error as the PermitClient.

export const callPermitMethod_DEPRECATED = async (contract, walletProvider, userAddress, contractAddress, contractData) => {

    const nonce = await getNonce(userAddress, contract, biconomyEthersProvider);

    const EIP712Domain = [
        { name: 'name', type: 'string' },
        { name: 'version', type: 'string' },
        { name: 'chainId', type: 'uint256' },
        { name: 'verifyingContract', type: 'address' }
    ];

    const domainData = {
        name: 'Chained Metrics',
        version: '1',
        chainId: 137,
        verifyingContract: contractAddress  // 0x690bF64128400f6477B3760d64C1f2C728e8017e
    };

    const Permit = [
        { name: 'owner', type: 'address' },
        { name: 'spender', type: 'address' },
        { name: 'value', type: 'uint256' },
        { name: 'nonce', type: 'uint256' },
        { name: 'deadline', type: 'uint256' }
    ];

    const deadline = (Math.round(Date.now() / 1000)) + 30;
    const message = {
        owner: userAddress,
        spender: contractData.broker_address, // 0x4e25c0123779657477CCD3558114334610B3e335
        value: contractData.value.toString(),
        nonce,
        deadline
    };

    const dataToSign = JSON.stringify({ 
        types: {
            EIP712Domain,
            Permit
        },
        domain: domainData,
        primaryType: 'Permit',
        message
    });

    const signature = await signMessage(userAddress, walletProvider, dataToSign);
    const { r, s, v } = getSignatureParameters(signature);

    await contract.permit(userAddress, message.spender, message.value, deadline, v, r, s);
};

And here is the error returned from the method calls detailed above:

{
  code: -32603
  data:
  code: 3
  data: 
 "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001e45524332305065726d69743a20696e76616c6964207369676e61747572650000"
  message: "execution reverted: ERC20Permit: invalid signature"
  [[Prototype]]: Object
  message: "Internal JSON-RPC error."
  stack: "{\n  \"code\": -32603,\n  \"message\": \"Internal JSON-RPC error..."
}

Any thoughts on steps to take to resolve?

cc: @livingrock

Another bit of information to provide is that I am able to recover the signing address when using recoverTypedSignature_v4 from eth-sig-util and passing the result to Web3.utils.toChecksumAddress. I’m not sure if this is expected but it leads me to believe that the r, s and v values that I’m passing to the contract permit method are incorrect. To generate these values I used getSignatureParameters, which was defined in an example I found in the Biconomy docs.

I also noticed that I did not provide the definition of signMessage and getSignatureParameters in my reply above. They appear below as follows:

// signMessage
export const signMessage = async (userAddress, walletProvider, dataToSign, signatureMethod = 'eth_signTypedData_v4') => {
    const signature = await walletProvider.send(signatureMethod, [userAddress, dataToSign]);

    return signature;
};

// getSignatureParamters -- taken from Biconomy example
export const getSignatureParameters = signature => {
    if (!Web3.utils.isHexStrict(signature)) {
        throw new Error(
            'Given value "'.concat(signature, '" is not a valid hex string.')
        );
    }
    var r = signature.slice(0, 66);
    var s = "0x".concat(signature.slice(66, 130));
    var v = "0x".concat(signature.slice(130, 132));

    v = Web3.utils.hexToNumber(v);
    if (![27, 28].includes(v)) v += 27;
    return {
        r: r,
        s: s,
        v: v
    };
};

Thank you again for your help. Please let me know if I can provide any additional information.

Which wallet are you using for signing?
Can you get your contracts verified on polygonscan so I can take a look?

Sure, will work on this. In the meantime here is the exact code of our deployed contract (0x690bF64128400f6477B3760d64C1f2C728e8017e):

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import {ERC20, ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol";

contract ChainedMetricsToken is ERC20Permit {
    constructor () ERC20("ChainedMetricsToken", "CMETRIC") ERC20Permit("ChainedMetricsToken") public {
        _mint(msg.sender, 3.14 * 10 ** 18 * 10 ** 10);
    }
}

As you can see, we are using OpenZeppelin’s ERC20permit function, which appears to be a draft at the moment. Would you recommend we use another implementation for this or roll this ourselves? We are using MetaMask for signing.

And here are the contents of the contract we are inheriting from for the ERC20Permit functionality.

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/draft-ERC20Permit.sol)

pragma solidity ^0.8.0;

import "./draft-IERC20Permit.sol";
import "../ERC20.sol";
import "../../../utils/cryptography/draft-EIP712.sol";
import "../../../utils/cryptography/ECDSA.sol";
import "../../../utils/Counters.sol";

/**
 * @dev Implementation of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in
 * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612].
 *
 * Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by
 * presenting a message signed by the account. By not relying on `{IERC20-approve}`, the token holder account doesn't
 * need to send a transaction, and thus is not required to hold Ether at all.
 *
 * _Available since v3.4._
 */
abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712 {
    using Counters for Counters.Counter;

    mapping(address => Counters.Counter) private _nonces;

    // solhint-disable-next-line var-name-mixedcase
    bytes32 private immutable _PERMIT_TYPEHASH =
        keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");

    /**
     * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`.
     *
     * It's a good idea to use the same `name` that is defined as the ERC20 token name.
     */
    constructor(string memory name) EIP712(name, "1") {}

    /**
     * @dev See {IERC20Permit-permit}.
     */
    function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) public virtual override {
        require(block.timestamp <= deadline, "ERC20Permit: expired deadline");

        bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline));

        bytes32 hash = _hashTypedDataV4(structHash);

        address signer = ECDSA.recover(hash, v, r, s);
        require(signer == owner, "ERC20Permit: invalid signature");

        _approve(owner, spender, value);
    }

    /**
     * @dev See {IERC20Permit-nonces}.
     */
    function nonces(address owner) public view virtual override returns (uint256) {
        return _nonces[owner].current();
    }

    /**
     * @dev See {IERC20Permit-DOMAIN_SEPARATOR}.
     */
    // solhint-disable-next-line func-name-mixedcase
    function DOMAIN_SEPARATOR() external view override returns (bytes32) {
        return _domainSeparatorV4();
    }

    /**
     * @dev "Consume a nonce": return the current value and increment.
     *
     * _Available since v4.1._
     */
    function _useNonce(address owner) internal virtual returns (uint256 current) {
        Counters.Counter storage nonce = _nonces[owner];
        current = nonce.current();
        nonce.increment();
    }
}

We appear to be blowing up on this line:

require(signer == owner, "ERC20Permit: invalid signature");

However, if I test on the JS side, the final line of this snippet evaluates to true:

const signature = await signMessage(userAddress, walletProvider, dataToSign);

const recovered = await recoverTypedSignature_v4({
    data: JSON.parse(dataToSign),
    sig: signature,
});

Web3.utils.toChecksumAddress(userAddress) === Web3.utils.toChecksumAddress(recovered) // => true

Can we also confirm that the getSignatureParameters function pulled from the Biconomy docs is the correct one to use when generating the r, s, v values to pass to the permit call?

export const getSignatureParameters = signature => {
    if (!Web3.utils.isHexStrict(signature)) {
        throw new Error(
            'Given value "'.concat(signature, '" is not a valid hex string.')
        );
    }
    var r = signature.slice(0, 66);
    var s = "0x".concat(signature.slice(66, 130));
    var v = "0x".concat(signature.slice(130, 132));

    v = Web3.utils.hexToNumber(v);
    if (![27, 28].includes(v)) v += 27;
    return {
        r: r,
        s: s,
        v: v
    };
};

const { r, s, v } = getSignatureParameters(signature);
await contract.permit(message.owner, message.spender, message.value, message.deadline, v, r, s);

Also note that we receive the same error when we use the Biconomy PermitClient.

To add to the above. The odd thing is that we appear to blow up here in the inherited contract . . .

address signer = ECDSA.recover(hash, v, r, s);
require(signer == owner, "ERC20Permit: invalid signature");

When this evaluates to true . . .

const signature = await signMessage(userAddress, walletProvider, dataToSign);

const recovered = await recoverTypedSignature_v4({
    data: JSON.parse(dataToSign),
    sig: signature,
});

Web3.utils.toChecksumAddress(userAddress) === Web3.utils.toChecksumAddress(recovered); // => true

I’m wondering if we are using the correct implementation of getSignatureParameters to generate the r, s, v values that are passed to the permit call.

export const getSignatureParameters = signature => {
    if (!Web3.utils.isHexStrict(signature)) {
        throw new Error(
            'Given value "'.concat(signature, '" is not a valid hex string.')
        );
    }
    var r = signature.slice(0, 66);
    var s = "0x".concat(signature.slice(66, 130));
    var v = "0x".concat(signature.slice(130, 132));

    v = Web3.utils.hexToNumber(v);
    if (![27, 28].includes(v)) v += 27;
    return {
        r: r,
        s: s,
        v: v
    };
};

const { r, s, v } = getSignatureParameters(signature);
await contract.permit(message.owner, message.spender, message.value, message.deadline, v, r, s);

Also note that we receive the same error when we use the Biconomy PermitClient.

Just wanted to post an update that this issue has been resolved. Thanks again to the Biconomy team for your help.

2 Likes

You can refer to USDC implementation EIP2612 permit.

I don’t think splitting to r s v is the issue here

oh, I missed this before replying. How did you manage to resolve the issue?