Using permit function of ERC20 v5

I would like to check if I am correctly performing the permit function call from my front end.

I am going step by step because it is complicated:

First, I capture the signer and the current time of the block:

const signerUser = await getSigner();
        let actualTime  = "";
		actualTime = await getBlockTime()

I capture the nonce, the value, with the current time I generate a future deadline and I add 1 to the current nonce:

 const nonce = await contractMiles.nonces($userAddressComplet);
		 const integerPriceChangeAirportWithMiles = parseInt(priceChangeAirportWithMiles); // 10000000000000000000
		 const deadlineOfSign = parseInt(actualTime) + 36000;
		 const intNonce = parseInt(nonce) + 1;

I take all this data and generate the hash using the ethers library in its version ^6.7.1".

let messageHash = ethers.solidityPackedKeccak256([ "address", "address", "uint256", "uint256", "uint256"], [$userAddressComplet, config.contractPassports, integerPriceChangeAirportWithMiles.toString(), intNonce.toString(), deadlineOfSign.toString()]);

Now we need the r, s, v and use the following functions:

	const rawSig = await signerUser.signMessage(messageHash);

	

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

		signature.r; // 0x96f68bacfedd996d40a3eb4db679314545ce3d24f07b7db007528f8507f1f865
		signature.s; // 0x4dd988fba90bf798173fa6b28222c3afb402ff5bbeac5dcc24bd204ae0778b91
		signature.v // 28

Finally we send all these data to the permit function that I share with you:

  function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) public virtual {
        if (block.timestamp > deadline) {
            revert ERC2612ExpiredSignature(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);
        if (signer != owner) {
            revert ERC2612InvalidSigner(signer, owner);
        }

        _approve(owner, spender, value);
    }

It does not work for me.
I'm worried about passing a bytes32 that I don't enter in my hash, the PERMIT_TYPEHASH.

bytes32 private constant PERMIT_TYPEHASH =
        keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");

I use updatable contracts and do not receive readable error.
I share at least the whole process so that you can check if at least, the steps seem correct.

Any help will be always appreciated.

Hey @Cainuriel, please take a look to ether's signTypedData function:

I see you already have the idea of how this works, is just that you're signing the incorrect message. Hope this points you in the right direction.

First of all, thank you for your quick reply:

I am a bit confused about the use of this signature:

(method) JsonRpcSigner.signTypedData(domain: ethers.TypedDataDomain, types: Record<string, ethers.TypedDataField[]>, _value: Record<string, any>): Promise<string>

What do you mean by parameter domain?

The types are the bytes32 discussed above PERMIT_TYPEHASH ?

finally the value is the structure of the data, but we do it as I am doing it?:

ethers.solidityPackedKeccak256([ "address", "address", "uint256", "uint256", "uint256"], [$userAddressComplet, config.contractPassports, integerPriceChangeAirportWithMiles.toString(), intNonce.toString(), deadlineOfSign.toString()]);

Thanks.

I have managed to sign with the recommended method

	const rawSig = await signerUser.signTypedData({ name: "Approve Miles", verifyingContract: config.contractPassports}, 
			 {"types" : [{name:"owner", type:"address"}, {name: "spender", type: "address"}, {name: "value", type: "string"},{name: "nonce", type: "uint256"}, {name: "deadline", type: "uint256"}  ]}, {owner, spender, value, nonce, deadline});

		console.log(`rawSig`, rawSig);

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

		signature.r; // 0x96f68bacfedd996d40a3eb4db679314545ce3d24f07b7db007528f8507f1f865
		signature.s; // 0x4dd988fba90bf798173fa6b28222c3afb402ff5bbeac5dcc24bd204ae0778b91
		signature.v // 28

But it doesn't work.
We still have unidentified error.

Apart from the fact that the information given to the user is an approval to send an ERC20 Token, it does not seem relevant.

I have followed this tutorial but it keeps failing me at the time of calling the emit function.

I have adapted the tutorial to my needs from the front end and do not use it from hardhat.

The transaction is accepted without problems, but finally, it ends up with error transaction.

Using a static call before making the call to permit I managed to get the error:

ERC2612InvalidSigner(address,address)

The account is the correct one and I don't understand very well the rejection.

This is my code:

 export  async function permit() {

    const contractConfig = await contractConfigProvider();
  
      const priceChangeAirportWithMiles = await contractConfig.priceMilesSwitchAirport();
      console.log(`priceChangeAirportWithMiles`, priceChangeAirportWithMiles);

      const chainId = config.idChain;
      console.log(`chainId`, chainId);

      const tokenOwner = await getSigner();

      const tokenReceiver = config.contractPassports;
      const contractMiles = await contractMilesSigner();
    
      console.log(`tokenOwner.address`, tokenOwner.address);

      // set token value and deadline
      const value = priceChangeAirportWithMiles;
      const now = await getTimestampInSeconds()
      const deadline =  now + 36000;
      console.log(`deadline`, deadline);
      // get the current nonce for the deployer address
      const nonces = await contractMiles.nonces(tokenOwner.address);
      console.log(`nonces`, nonces);
      // set the domain parameters
      const domain = {
        name: await contractMiles.name(),
        version: "1",
        chainId: chainId,
        verifyingContract: config.contractPassports
      };
    
      // set the Permit type parameters
      const types = {
        Permit: [{
            name: "owner",
            type: "address"
          },
          {
            name: "spender",
            type: "address"
          },
          {
            name: "value",
            type: "uint256"
          },
          {
            name: "nonce",
            type: "uint256"
          },
          {
            name: "deadline",
            type: "uint256"
          },
        ],
      };
    
      // set the Permit type values
      const values = {
        owner: tokenOwner.address,
        spender: tokenReceiver,
        value: value,
        nonce: nonces,
        deadline: deadline,
      };
  
     console.log(`values`, values);
      

      const signature = await tokenOwner.signTypedData(domain, types, values);

         const sig = ethers.Signature.from(signature)
 
      try {
        await contractMiles.permit.staticCall(
          tokenOwner.address,
          tokenReceiver,
          value,
          deadline,
          sig.v,
          sig.r,
          sig.s,
          {
            gasPrice: 35000000000,
            gasLimit: 160000 
          }
        );
        const tx = await contractMiles.permit(
        tokenOwner.address,
        tokenReceiver,
        value,
        deadline,
        sig.v,
        sig.r,
        sig.s,
        {
          gasPrice: 35000000000,
          gasLimit: 160000 
        }
      );
    
     const ok =  await tx.wait 

  
     if(ok) {
      const isPermit =await contractMiles.allowance(tokenOwner.address, tokenReceiver)
      console.log(`Check allowance of passports miles: ${isPermit}`);
      return isPermit;
     } else {
        console.log(`fail `, ok );
        return false
     }
     
      } catch (error) {
        console.log(`error`, error);
        return false
      }


  
    }

Hey! I see you sorted it out. I'll try to answer the questions I still se relevant:

Using a static call before making the call to permit I managed to get the error:

ERC2612InvalidSigner(address,address)

Right. This indicates that the derived signer is not corresponding to the expected owner, so the parameters are incorrect at some point.

I suggest you to take a look to our test suite so you can get a working example from our contracts.

const domain = {
name: await contractMiles.name(),
version: "1",
chainId: chainId,
verifyingContract: config.contractPassports
};

At first sight, it seems to me that the issue is in the verifyingContract address. It should be that of the token itself.

This is exactly what it was!

If you allow me now to reflect, I would like to question the use of this function. Since it promises gas savings and I don't see it that way.

this is the permit functión:

function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) public virtual {
        if (block.timestamp > deadline) {
            revert ERC2612ExpiredSignature(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);
        if (signer != owner) {
            revert ERC2612InvalidSigner(signer, owner);
        }

        _approve(owner, spender, value);
    }

And this is the function you want to replace for gas savings:

    function approve(address spender, uint256 value) public virtual returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, value);
        return true;
    }

Both subsequently call the internal function _approve with previous logic of different computation.

Recall that both functions do not eliminate the subsequent need for transfer with transferform.

Therefore, I wonder if it is really worth using this function because it is not so clear to me that we will save so much gas.

In the end, the user will have to sign two transactions, and if we allow the user to increase his approved token disposition, he will only transact the token until the end of his approved balance.

I don't think you guys would do something without thinking about it.
So, something escapes me, do we really save gas by using permit method?

Thanks for your help @ernestognw .

Glad it worked!

The "savings" from using a Permit come from the fact that you can both approve and transfer within the same transaction. For example, a protocol may ask you to sign a message and then execute the following steps:

  1. Ask the user to sign an Permit transaction (this doesn't consume gas)
  2. Use the signature in a custom function that calls permit(...) on the target token
  3. Perform a transfer and consume the approval

In my opinion is more a UX improvement rather than a gas improvement. Note that this approval + transfer is not an issue with Account Abstraction.

What do you mean by this final comment?

Yes, it is certainly an improvement in UX. But this usage has been sold as a way to save gas for customers when in fact it is not true.

Yes, it is certainly an improvement in UX. But this usage has been sold as a way to save gas for customers when in fact it is not true.

It does save gas by avoiding 2 transactions! That itself adds an extra 21000 gas units.

What do you mean by this final comment?

A smart wallet can batch transactions because it's a contract, as opposed to an EOA. This is the case for account abstraction.

They are not one, they are two transactions. One, the validation of the signature equivalent to approve with the permit function. And, the second one, the transfer of the ERC20 token. So I do not see such a gas saving. If you validate a larger amount in the following transactions if they will be one if that approve is checked. But you can also do that by directly calling the approve function by entering an amount greater than the transfer.

They are not one, they are two transactions. One, the validation of the signature equivalent to approve with the permit function. And, the second one, the transfer of the ERC20 token.

You can do permit + transfer on the same transaction from a EOA, but you can't do approve + transfer:

function foo(
  address owner,
  address spender,
  uint256 value,
  uint256 deadline,
  uint8 v,
  bytes32 r,
  bytes32 s
) ... {
  myToken.permit(owner, spender, value, deadline, v, r, s);
  ...
  myToken.transferFrom(owner, spender, value);
  ...
}

But you can also do that by directly calling the approve function by entering an amount greater than the transfer.

On a separate transaction, though... Also I wouldn't recommend approving for a larger amount to any contract at all.

I have never made concatenated transactions as in your example, I always make the calls individually. It's an interesting thing to test.
How would it be using the ethers.js library?

Thanks @ernestognw

Now that I take a better look at your example, it is a foo function that makes two calls in a row. That being signatures would have to be asynchronous. This I have done but it is still two calls to the blockchain and not one as we were discussing.

Two calls is not the same as 2 transactions.

Now I understand. Your example is a function in a contract in solidity. The confusion was that I was calling these two functions from the front end.
So, to me, they are two different calls.

Your example puts both transactions together in a single function in the contract.

All understood we close the thread.

Thanks @ernestognw