You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用web3.js实现ERC20Permit签名授权:签名有效但allowance仍为0

ERC20Permit签名授权后allowance为0、nonce未更新的问题修复

核心问题:误用.call()调用状态变更函数

permit是修改合约状态的函数(会更新allowance和nonce),你当前用.call()仅做本地模拟调用,不会向链上发送交易,所以合约状态完全没有变更。必须改用.send()发送交易触发链上状态修改:

// 错误写法:仅模拟调用,不修改链上状态
// await contract.methods.permit(account.address, contractAddress, amount, deadline, v, r, s).call();

// 正确写法:发送交易,需用户钱包签名确认
await contract.methods.permit(account.address, contractAddress, amount, deadline, v, r, s)
  .send({ from: account.address });

签名构建中的关键错误

1. nonce格式错误

你将合约返回的nonce转成了十六进制字符串nonce.toString(16),但EIP712签名要求uint256类型使用十进制数字,格式不匹配会导致签名验证失败,进而permit无法生效。直接使用原始数字即可:

// 错误写法
// nonce: nonce.toString(16),    

// 正确写法
nonce: nonce,

2. deadline参数不一致且单位错误

  • 签名构建时的deadline计算为Date.now() + 20 * 60(仅加1200毫秒,约2分钟),而调用permit时用的是Date.now() + 20 * 60 * 1000(20分钟),两者必须完全一致;
  • OpenZeppelin的ERC20Permit合约中,deadline参数是秒级时间戳,而非毫秒,你之前的毫秒时间戳会导致有效期计算错误。

统一修正:

// 计算20分钟有效期的秒级时间戳
const deadline = Math.floor(Date.now() / 1000) + 20 * 60;

3. 动态获取链ID避免硬编码

硬编码chainId: 5可能导致跨链或链ID变更时签名失效,建议动态获取链ID:

const chainId = await web3.eth.getChainId();
const domain = {    
  name: contractName,    
  version: "1",    
  chainId: chainId,  
  verifyingContract: contractAddress
};

完整修正后的代码片段

调用permit部分

contract = // 你的合约实例
contractAddress = "0x..."; // 合约地址
amount = BigNumber(amount); // 已处理小数位的金额
// 统一计算秒级时间戳的20分钟有效期
const deadline = Math.floor(Date.now() / 1000) + 20 * 60;

// 发送交易而非模拟调用
await contract.methods.permit(account.address, contractAddress, amount, deadline, v, r, s)
  .send({ from: account.address });

// 等待交易上链后查询状态
console.log(await contract.methods.allowance(account.address, contractAddress).call());
console.log(await contract.methods.nonces(account.address).call());

签名构建部分

const EIP712Domain = [  
  { name: "name", type: "string" },  
  { name: "version", type: "string" },  
  { name: "chainId", type: "uint256" },  
  { name: "verifyingContract", type: "address" },
];
const contractName = await contract.methods.name().call();
const chainId = await web3.eth.getChainId();
const domain = {    
  name: contractName,    
  version: "1",    
  chainId: chainId,  
  verifyingContract: contractAddress
};
const Permit = [  
  { name: "owner", type: "address" },  
  { name: "spender", type: "address" },
  { name: "value", type: "uint256" }, 
  { name: "nonce", type: "uint256" },    
  { name: "deadline", type: "uint256" },
];
const nonce = await contract.methods.nonces(account.address).call();
const deadline = Math.floor(Date.now() / 1000) + 20 * 60;
const message = {    
  owner: account.address,    
  spender: contractAddress,  
  value: amount.toString(), // BigNumber转字符串确保类型匹配
  nonce: nonce,    
  deadline: deadline,
};
const params = JSON.stringify({    
  types: {      
    EIP712Domain,      
    Permit,
  },    
  domain,    
  primaryType: "Permit",    
  message, 
});
const _signMessage = async (userAddress, web3, dataToSign, signatureMethod = 'eth_signTypedData_v4') => {
  const signature = await web3.currentProvider.send(signatureMethod, [userAddress, dataToSign]);
  return signature;
};

const signature = await _signMessage(account.address, web3, params);

const getSignatureParameters = signature => {
  if (!web3.utils.isHexStrict(signature.result)) {
      throw new Error(`Given value "${signature.result}" is not a valid hex string.`);
  }
  const r = signature.result.slice(0, 66);
  const s = "0x" + signature.result.slice(66, 130);
  let v = parseInt(signature.result.slice(130, 132), 16);
  if (![27, 28].includes(v)) v += 27;
  return { r, s, v };
}

const { r, s, v } = getSignatureParameters(signature);

内容的提问来源于stack exchange,提问作者Pm Rivière

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 08:40:25