curvefi / stableswap-ng

Automatic Market Maker (AMM) for 2 or more pegged assets, written in Vyper.
Other
34 stars 15 forks source link

Permit signature not working for Curve LP token pufETHwstE #54

Closed 9inpachi closed 3 months ago

9inpachi commented 3 months ago

Hi.

Description

I am trying to generate a permit signature for a Curve LP token but the permit I get is not correct and one of our contracts that uses a permit to check if a token is approved reverts transactions.

Example

I have a complete reproducable example which uses two ERC20 tokens namely pufETH and pufETHwstE. Using the exact same code, the permit signature works for pufETH but doesn't work for pufETHwstE. Here's the link to the codesandbox: https://codesandbox.io/p/sandbox/curve-lp-permit-signature-rkth43. Please check config.ts for more information on how to test the example.

The example uses the following code for generating the permit signature using viem.

import { Address, getContract, parseSignature } from "viem";
import { chain, publicClient, selectedToken, walletClient } from "./config";
import { ERC20Permit } from "./contracts/erc20-permit";

export type PermitSignature = {
  r: Address;
  s: Address;
  v?: bigint;
  yParity: number;
  deadline: bigint;
};

export const getPermitSignature = async (
  ownerAddress: Address,
  spenderAddress: Address,
  value: bigint
): Promise<PermitSignature> => {
  const contract = getContract({
    address: selectedToken.chainAddresses[chain.id],
    abi: ERC20Permit,
    client: {
      public: publicClient,
      wallet: walletClient,
    },
  });

  const permitNonces = await contract.read.nonces([ownerAddress]);
  const name = await contract.read.name();
  const domain = <const>{
    name,
    version: selectedToken.permitVersion,
    chainId: chain.id,
    verifyingContract: selectedToken.chainAddresses[chain.id],
  };
  const types = <const>{
    Permit: [
      { name: "owner", type: "address" },
      { name: "spender", type: "address" },
      { name: "value", type: "uint256" },
      { name: "nonce", type: "uint256" },
      { name: "deadline", type: "uint256" },
    ],
  };
  const deadline = BigInt(2 ** 256) - 1n;

  const signature = await walletClient.signTypedData({
    account: ownerAddress,
    domain,
    types,
    primaryType: "Permit",
    message: {
      owner: ownerAddress,
      spender: spenderAddress,
      value,
      nonce: permitNonces,
      deadline,
    },
  });

  return { ...parseSignature(signature), deadline };
};
heswithme commented 3 months ago

Hi @9inpachi

I think the reason is

image

Your signature code is

  const signature = await walletClient.signTypedData({
    account: ownerAddress,
    domain,
    types,
    primaryType: "Permit",
    message: {
      owner: ownerAddress,
      spender: spenderAddress,
      value,
      nonce: permitNonces,
      deadline,
    },
  });

and here you use domain, defined in your code as

  const domain = <const>{
    name,
    version: selectedToken.permitVersion,
    chainId: chain.id,
    verifyingContract: selectedToken.chainAddresses[chain.id]
  };

If you check EIP-712, you can see that one of signed fields of domain separator is salt, and this is missing from your code.

In pufETH this parameter is 0x0000000000000000000000000000000000000000000000000000000000000000 (accessible throught reading eip712Domain method)

However, curve pools initialize salt as previous blockhash at deployment, which is nonzero. Check out field 39 here: https://etherscan.io/token/0xeeda34a377dd0ca676b9511ee1324974fa8d980d#readContract This way you can figure out the salt for curve pools.

The reason your code works for some tokens is that zero salt produces same signature as non-declared salt. You can verify this behavior by placing console.log(depositPermitSignature); in index.ts, and running the code with

  const domain = <const>{
    name,
    version: selectedToken.permitVersion,
    chainId: chain.id,
    verifyingContract: selectedToken.chainAddresses[chain.id],
    salt: 0x0000000000000000000000000000000000000000000000000000000000000000,
  };

Observe produced signatures with this snippet, and with present code - and you will see that signatures are the same.

TLDR: Add Salt.

9inpachi commented 3 months ago

Thanks @heswithme for the detailed response!

It's working now after adding the salt as you said. Thanks a lot!