Giter VIP home page Giter VIP logo

Comments (7)

frangio avatar frangio commented on July 18, 2024

What do you mean "to save gas"?

from contracts-wizard.

SKYBITDev3 avatar SKYBITDev3 commented on July 18, 2024

In the process of a token holder changing allowance, approve was the usual way to do it, but it requires a transaction, which costs gas. permit also allows changing allowance, but only requires a signature, which costs no gas, so it's a better way. New tokens should have permit as a more cost-effective way to approve allowance than approve.

Dapps these days that work with tokens check whether permit exists in the token, e.g. by looking for selector d505accf. Only if permit doesn't exist in a particular token then they fall back to calling approve.

from contracts-wizard.

frangio avatar frangio commented on July 18, 2024

"Costs no gas" is not really accurate, but I see that the Wizard tooltip is claiming something similar. It should be cheaper than using approve because it's one less transaction, but there is a cost associated with cashing in the permit. It's overall cheaper to use infinite approvals, but it's arguably less secure.

I would definitely recommend including ERC20Permit in a token, but for UX reasons. I agree with making it a default in Wizard.

from contracts-wizard.

SKYBITDev3 avatar SKYBITDev3 commented on July 18, 2024

"Costs no gas" is not really accurate

Why do you say it's not really accurate? I've used signatures as a user of platforms like OpenSea and Rarible, as well as developed dapps that check for permit in the token and calls it instead of calling approve. On the UI instead of a transaction screen, some info about the signature request is shown (particularly the amount) and a "sign" button. After pressing it the allowance approval is done (without any transfer of funds), then a screen appears for the transfer transaction (which of course costs gas).

from contracts-wizard.

frangio avatar frangio commented on July 18, 2024

Generating the signature doesn't cost anything, but the transfer function will be slightly more expensive when it includes a permit, since it has to do additional work to validate it. Overall it will probably be cheaper than having a separate approve transaction.

from contracts-wizard.

SKYBITDev3 avatar SKYBITDev3 commented on July 18, 2024

The important point to note is that anyone can call permit, and it's the caller that pays the gas fee. It doesn't matter who calls permit, as long as a valid signature from the owner was obtained and presented when required, it just proceeds.

If the token owner calls permit, it costs him gas.
If the spender calls permit, it costs her gas, so then the approval process is free for the owner, which makes it much better for user experience.

Here's some of my code in which spender calls permit:

const { ethers, network } = require(`hardhat`)

async function main() {
  const [ownerWallet, spenderWallet] = await ethers.getSigners()
  console.log(`Using network: ${network.name} (${network.config.chainId}), RPC url: ${network.config.url}`)

  const tokenContractName = `TESTERC20`
  const contractAddress = `0xc45Eb76AB6A29B64FD17A0F49e214a1f0a20A59D` // localhost

  const contractOfOwner = await ethers.getContractAt(tokenContractName, contractAddress, ownerWallet)
  const contractOfSpender = await ethers.getContractAt(tokenContractName, contractAddress, spenderWallet)

  let txRec

  const totalSupply = ethers.formatUnits(await contractOfOwner.totalSupply())

  console.log(`Initially:`)
  console.log(`owner has ${ethers.formatUnits(await contractOfOwner.balanceOf(ownerWallet.address))} of ${totalSupply} tokens`)
  console.log(`spender has ${ethers.formatUnits(await contractOfOwner.balanceOf(spenderWallet.address))} of ${totalSupply} tokens`)

  console.log(`owner has allowed spender to spend ${ethers.formatUnits(await contractOfOwner.allowance(ownerWallet.address, spenderWallet.address))} tokens`)

  console.log(`owner has ${ethers.formatUnits(await ethers.provider.getBalance(ownerWallet.address), `ether`)} of native currency`)
  console.log(`spender has ${ethers.formatUnits(await ethers.provider.getBalance(spenderWallet.address), `ether`)} of native currency`)

  const amountAllowedToSpend = ethers.parseUnits(`2`, `ether`)

  if (tokenHasPermit(await contractOfOwner.getDeployedCode())) {
    const deadline = Math.floor(Date.now() / 1000 + 3600)
    const splitSigAsArray = await getPermitSignature(contractOfOwner, ownerWallet, spenderWallet, amountAllowedToSpend, deadline) // ownerWallet does the signing in here to permit spender to spend his tokens
    console.log(`Owner has given his signature`)
    console.log(`Spender is now calling token's permit function with owner's signature so that she can spend his tokens...`)
    txRec = await contractOfSpender.permit( // any account can call the permit function, as long as there's a signature (values in splitSigAsArray) that proves that owner has permitted his tokens to be spent by spender. Gas is paid by permit function caller, so approval process is free for owner if he didn't call permit.
      ownerWallet.address,
      spenderWallet.address,
      amountAllowedToSpend,
      deadline,
      ...splitSigAsArray
    )
  } else {
    console.log(`Owner is calling token's approve function...`)
    txRec = await contractOfOwner.approve(spenderWallet.address, amountAllowedToSpend) // only owner can call approve to let spender spend his tokens. contractOfSpender.approve doesn't work.
  }
  await txRec.wait()

  console.log(`After permit / approve:`)
  console.log(`owner has allowed spender to spend ${ethers.formatUnits(await contractOfOwner.allowance(ownerWallet.address, spenderWallet.address))} tokens`)

  console.log(`owner has ${ethers.formatUnits(await ethers.provider.getBalance(ownerWallet.address), `ether`)} of native currency`)
  console.log(`spender has ${ethers.formatUnits(await ethers.provider.getBalance(spenderWallet.address), `ether`)} of native currency`)


  txRec = await contractOfSpender.transferFrom(ownerWallet.address, spenderWallet.address, amountAllowedToSpend) // spender calls transferFrom to transfer tokens from owner
  await txRec.wait()
  console.log(`After transferFrom:`)

  console.log(`spender took ${ethers.formatUnits(amountAllowedToSpend)} tokens from owner`)

  console.log(`owner has ${ethers.formatUnits(await contractOfOwner.balanceOf(ownerWallet.address))} of ${totalSupply} tokens`)
  console.log(`spender has ${ethers.formatUnits(await contractOfOwner.balanceOf(spenderWallet.address))} of ${totalSupply} tokens`)

}


const getPermitSignature = async (tokenContract, ownerWallet, spenderWallet, value, deadline) => { // owner allows spender to spend value wei tokens before deadline
  const [name, nonce] = await Promise.all([
    tokenContract.name(),
    tokenContract.nonces(ownerWallet.address),
  ])

  const domain = {
    name,
    version: `1`,
    chainId: network.config.chainId,
    verifyingContract: tokenContract.target
  }
  // console.log(`domain: ${JSON.stringify(domain)}`)

  const permitTypes = {
    Permit: [{
      name: `owner`,
      type: `address`
    },
    {
      name: `spender`,
      type: `address`
    },
    {
      name: `value`,
      type: `uint256`
    },
    {
      name: `nonce`,
      type: `uint256`
    },
    {
      name: `deadline`,
      type: `uint256`
    },
    ],
  }
  const permitValues = {
    owner: ownerWallet.address,
    spender: spenderWallet.address,
    value,
    nonce,
    deadline,
  }
  // console.log(`permitValues: ${JSON.stringify(permitValues)}`)

  const signature = await ownerWallet.signTypedData(domain, permitTypes, permitValues) // owner signs the data
  const { v, r, s } = ethers.Signature.from(signature)

  // const recoveredAddress = ethers.verifyTypedData(
  //   domain,
  //   permitTypes,
  //   permitValues,
  //   { v, r, s }
  // )
  // console.log(`recoveredAddress: ${recoveredAddress}. Verification ${recoveredAddress === ownerWallet.address ? "passed" : "failed"}.`)

  return [v, r, s]
}

const tokenHasPermit = bytecode => bytecode.includes(`d505accf`) && bytecode.includes(`7ecebe00`) // permit & nonces selectors


main().catch(error => {
  console.error(error)
  process.exitCode = 1
})

Here's the output, showing no change in owner's native currency balance:

Using network: localhost (31337), RPC url: http://127.0.0.1:8545
Initially:
owner has 100.0 of 1000.0 tokens
spender has 0.0 of 1000.0 tokens
owner has allowed spender to spend 0.0 tokens
owner has 9999.9614911964478728 of native currency
spender has 10000.0 of native currency
Owner has given his signature
Spender is now calling token's permit function with owner's signature so that she can spend his tokens...
After permit / approve:
owner has allowed spender to spend 2.0 tokens
owner has 9999.9614911964478728 of native currency
spender has 9999.999880784353054822 of native currency
After transferFrom:
spender took 2.0 tokens from owner
owner has 98.0 of 1000.0 tokens
spender has 2.0 of 1000.0 tokens
Done in 8.48s.

from contracts-wizard.

SKYBITDev3 avatar SKYBITDev3 commented on July 18, 2024

I hope this will be done in v5.0, at https://docs.openzeppelin.com/contracts/5.x-rc/wizard and https://wizard.openzeppelin.com.

from contracts-wizard.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.