ERC2612 and Ethereum dApp usability

ERC2612 and Ethereum dApp usability

Roaming the spaces of Twitter/X, I saw the service smolrefuel.com, (your), which solves the problem of obtaining a gas token on Ether networks, if you do not have it, but it is withdrawn, for example, to a wallet from an exchange, a stablecoin. An interesting case! I thought.

I started to understand how it works. If at a high level, then the algorithm is as follows:

  1. User with wallet W selects a token T (For example, USDC) for which he wants to buy a gas (native) token.

  2. Signs off-chain (no gas required) some “document” A. The document is the following power of attorney (further on, I will use this term, as it seems to me that it successfully reflects the essence of the signed message):

    1. What smart contract S(mart contract) it is allowed to withdraw from the wallet W(allet) amount V(alue) in tokens T(oken) until a certain date D(deadline). Resolution is a common approve mechanism in ERC20.

    2. That is, in fact, it is an analogue of a power of attorney, which allows the authorized person to do something on behalf of the principal.

    3. As S of course, the smart contract of the service acts.

  3. Authorization A is transferred to the backend, where with its help the required contract S permission to withdraw a certain amount is granted, further S withdraws this amount from the user’s wallet, changes the DEX to the native token, takes his commission, transfers the native token to the wallet W. And everyone is satisfied.

ERC2612

If you dig into the details, it works on the ERC2612 standard. You can read the original, but the basis is the permit function implemented in the token T.

function permit(
  address owner, 
  address spender, 
  uint value, 
  uint deadline, 
  uint8 v, bytes32 r, bytes32 s)

It says “allow the smart contract spender spend value tokens from the address owner“, here is the proxy. The trio (v, r, s) represents the same proxy AND from owner, and this is essentially the output of the secp256k1 algorithm. Without going too much into the math, secp256k1 has a nice feature – it’s easy to check that the proxy is actually signed by an address owner. For example, as it is done in the implementation from OpenZeppelin. permission also checks that the deadline of the issued power of attorney has not expired.

To prevent replay attacks, so that the proxy generated on one of the tokens or another network cannot be reused, a unique key is used when creating it domain, in which, according to EIP-712, the token name, contract version, network id, contract address, and salt, if necessary, are encoded. Domain is unique to the token and is a constant value for it. A power of attorney is also used for signing nonce at the level of the token contract, that is, each subsequent proxy will be different from the previous one.

I will write down in pseudocode what the signing of the power of attorney looks like. The sequence of calls and what data is used is important:

// то самое значение domain для уникальности доверенности между
// разными токенами и сетями
domain = {
  name: "Token Name",
  version: "1",
  chainId: "137",
  verifyingContract: "0xAaAaa...."
}

// параметры для permit
values = {
  owner: W,         // адрес кошелька W
  spender: S,       // кому разрешим потратить
  value: V,         // сколько, например 100000000
  nonce: getNonce(T, W) // nonce (счетчик, увеличивающийся на единицу),
  deadline: D       // час с текущего момента
}

A = signTypedData(domain, values) // получили доверенность A

Now A with values is passed to call permission in the token contract as follows (also pseudocode):

v, r, s = splitSignature(A) // получить компоненты доверенности v, r, s
T.permit(owner: W, spender: S, value: V, deadline: D, v, r, s)

Importantly, any contract can make a call. permission does not check which account is making the call; it is only important who signed the power of attorney – therefore, for the person who signed the power of attorney, this operation will be without payment for gas (gasless), because Another wallet will send the transaction to the blockchain, which does the backend in the case of smolrefuel. Now let’s look at the pseudocode permission (Example from OpenZeppelin implementation):

function permit(address owner, address spender, uint value, uint deadline, 
  uint8 v, bytes32 r, bytes32 s) {
  
    if (deadline > NOW())
      throw error("дедлайн просрочен")

    // восстановили "подписанта" доверенности
    address signer = recover(DOMAIN_SEPARATOR, v, r, s)
  
    // тот кто подписал не соответствует заданному owner
    if (signer != owner)
      throw error("неверная подпись")

    // все хорошо, дать аппрув spender на списание value с owner
    approve(owner, spender, value)
  }

DOMAIN_SEPARATOR is a token-bound constant derived from domain.

Prototype

To practically test how the ERC2612 works, I built a prototype. It consists of two components:

Frontend application (react folder) in which the user, using Metamask or another wallet, signs an off-chain power of attorney to withdraw 0.1 USDC on the Polygon network from the current contract address (src/constants.ts, spenderAddress – Install it yourself). App.tsx main code, function sendPermit.

...
// выше получаем данные токена
const values: ValuesDto = {
  owner: eoaAddress!,
  spender: constants.spenderAddress,
  value: ethers.parseUnits("0.1", decimals),
  nonce: nonce,
  deadline: Math.floor(Date.now() / 1000) + 3600,  // 1 hour from now on.
};

const domain: DomainDto = {
  chainId: network!.chainId,
  name: name,
  verifyingContract: constants.tokenAddress,
  version: version,
};

const signature = await signer.current!.signTypedData(domain, constants.permitTypes, values);
const payload: PermitDto = {
  signature: signature,
  values: values,
  domain: domain,
};

// отправляем на бек
const resp = await fetch("http://localhost:9001/permit", { method: "POST", headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });

...

Backend application (console folder), accepts credentials and parameters from the frontend and calls permission. For it to work, you need to fill in 2 important parameters in the .env (see .env.example) file – RPC URL (taken from services such as Alchemy, Infura, etc., there are many of them), and PK – the private key of the address from which it will be sent transaction (do not store private keys in production – this approach is for simplicity) – it must have a gas token on it. The main code in index.ts, the POST handler:

...
// inp.signature – и есть доверенность с фронтенда
const splitted = ethers.Signature.from(inp.signature);

const tokenContract = new ethers.Contract(
  constants.tokenAddress,
  USDC_abi,
  wallet
);

// вызвать permit
const tx = await tokenContract.permit(
  inp.values.owner,
  constants.spenderAddress,
  inp.values.value,
  inp.values.deadline,
  splitted.v,
  splitted.r,
  splitted.s,
  {
    gasLimit: 170000,
  }
);

By default, the prototype works with Polygon USDC, but in constants.ts you can specify any tokenAddress.

Implementation of the standard

As we can see, in general, ERC2612-type standards make life easier for users of cryptowallets, especially beginners. It made me wonder how common this standard is among tokens. To do this, I analyzed the top 10 tokens on the Ethereum, Polygon, Optimism networks. The most interesting things became clear (link to the analysis file).

green marked tokens that correctly implement the ERC2612 and EIP712 standards. For them, it is easiest to issue a power of attorney

. Yellow – Tokens implementing ERC2612 (i.e. functionpermission ), but do not support EIP712. This is a difficult case, becausesignTypedData

implemented in ethers.js, to create a proxy, exactly implements EIP712. Moreover, in the browser, when Metamask signs typed data, then eth_signTypedData_v4 accepts an objectdomain , and calculates the domain separator inside itself, which does not match the domain separator of the token. You can partially solve the problem, but only if you have the private key (eg on the backend). Drag values DOMAIN_SEPARATOR from the contract and use it insignTypedData

, for this you need to modify ethers.js (made a PR; we’ll see whether to include or not). It is hard to tell what kind of cases these could be, because it is impossible to create a correct proxy on the UI (via Metamask). It was not included in the prototype for simplicity, you can practice on your own if you take the modified version of ethers. red noted one token (DAI on Mainnet) that has a functionpermission , but not with ERC2612 (sic!). It allows two options for creating a power of attorney for approval: (1) for the maximum possible amount or (2) to withdraw it altogether. Why this was done is a mystery. permission takes a parameterallowed: bool

. Ugh… And that’s not all. To sign a power of attorney, you need to constructdomain and somewhere to take the field values ​​for it. All of them are trivial exceptversion . Marked with an asteriskcontracts has no public method

version() Therefore, I had to search for the specific version that is used in the domain of this token – I wrote it down after the asterisk. I didn’t have to go through it for a long time, it’s either 1 or 2, but if the developer of the token decides to change the version, your application will stop working… I will note that

version(),

as far as I know, it is not included in the standards, and its implementation is at the discretion of the developer. For example, Optimism DAI has this method, but OP does not, which causes some inconvenience. EIP5267 was even invented to solve this problem, but it is supported by stETH(**) from the whole list, see table

Tools

Separately, I wanted to share a cool tool. tenderly is an excellent debugger. For Ethereum tokens during testing I used a cool transaction simulation tool to avoid paying for expensive gas.

If the transaction did not go through, tenderly has a debugger that allows you to run the transaction step by step. Conclusions The study left mixed feelings. On the one hand, there are standards for improving usability in Ethereum networks. On the other hand, the analysis showed that out of 30 considered tokens on popular networks, only 8 can issue proxies for approval, of which separate processors can be written for three (two for versionand one for non-standard

permission). I can’t call myself a visionary, but apparently this is very little for an adopter. Also, I’d bet a significant increase in development time when adding tokens to your dApp in cases like this if you’re working with something less common than ERC20.

PR minute

. I run the TG-channel Web3 developer. I write small notes (not often) about blockchain/crypto tasks that I solve. Glad to see among the subscribers!Also always happy to chat with the founders/developers of crypto projects.

Related posts