通过 Web 应用程序转发 Gasless 元交易 - OpenZeppelin 文档

本文介绍了如何使用OpenZeppelin Defender实现gasless元交易,包括使用Relayer服务代表用户发送交易,避免用户管理私钥等复杂操作。文章详细讲解了ERC-2771、ERC-2612和ERC-3009三种不同的gasless交易标准,并提供了相应的代码示例和操作步骤, 同时也介绍了如何使用 Defender 的 Relayers 和 Actions 实现安全、便捷的元交易。

使用 Web 应用程序中继 Gasless Meta-Transactions

Defender 提供了一种无缝且安全的方式,使用 Relayer 实现 gasless meta-transactions。这些 Relayer 代表用户处理发送交易,从而消除了用户管理私钥、交易签名、nonce 管理、gas 预估和交易包含的需求。

这个演示应用程序展示了如何不仅使用 ERC-2771 实现 meta-transactions,还探索了其他 gasless transaction 标准:

  • ERC-2771: 安全原生 Meta Transactions:这个演示应用程序 使用 ERC2771ForwarderERC2771Context 来实现 meta-transactions,从而将 msg.sender 与 Relayer 的地址分离。用户需要做的就是使用他们想要从中发出交易的帐户签署消息。使用用户的私钥,为目标合约和所需交易的数据形成签名。此签名是链下发生的,不花费任何 gas。签名传递给 Relayer,以便它可以为用户执行交易(并支付 gas)。

  • ERC-2612: Permit 函数:此标准引入了一种在 ERC-20 代币中启用 gasless 代币批准的方法。用户可以通过签署消息而不是直接支付传统 "approve" 函数的 gas 费用来授予 relayer 服务支出权限。这允许 relayer 代表用户处理代币批准。

  • ERC-3009: Transfer with Authorization:此标准通过链下授权促进 gasless 代币转账。用户签署消息,授权特定的代币转账,然后任何人(包括 relayer 服务)都可以将其提交到区块链。

使用带有 Relayer 的 Defender 可以轻松安全地实现 gasless meta-transaction 中继,这使你可以轻松发送交易,而无需管理私钥、交易签名、nonce 管理、gas 预估和交易包含。

准备工作

  • OpenZeppelin Defender 帐户。你可以在此处注册 Defender。

  • 已安装 GitYarn

1. ERC-2771:安全原生 Meta Transactions

你可以在此处查看实时演示应用程序。如果用户有可用资金来支付交易费用,它会直接接受注册,否则数据将作为 meta-transaction 发送。

在示例代码中,SimpleRegistry 合约 的功能是获取一个字符串并存储它。该合约的 meta-transaction 实现 通过将签名者与交易发送者分离来实现相同的结果。

在比较代码时,请注意 meta-transaction 使用 _msgSender() 而不是 SimpleRegistry 使用 msg.sender。通过从 ERC2771ContextERC2771Forwarder 扩展,该合约变得能够进行 meta-transaction。

所有 OpenZeppelin 合约都与 _msgSender() 的使用兼容。

两个合约之间的第二个根本变化是 meta-transaction 合约(Registry)需要指定受信任 forwarder 的地址,在本例中,它是 ERC2771Forwarder 合约的地址。

1.1 配置项目

首先,fork 该存储库并导航到本指南的目录。在那里,使用 yarn 安装依赖项:

$ git clone https://github.com/openzeppelin/workshops.git
$ cd workshops/25-defender-metatx-api/
$ yarn

在项目根目录中创建一个 .env 文件,并从 API 密钥页面 提供 API 密钥和密钥。私钥将用于本地测试,但 Relayer 用于实际的合约部署。

PRIVATE_KEY="0xabc"
API_KEY="abc"
API_SECRET="abc"

1.2 创建 Relayer

运行 Relayer 创建脚本,它将使用 .env 文件中的 Defender API 参数:

$ yarn create-relay

Relayer 使用 defender-sdk 包创建:

// ...
const client = new Defender(creds);

// 使用 Defender SDK 客户端创建 Relayer。
const requestParams = {
  name: 'MetaTxRelayer',
  network: 'sepolia',
  minBalance: BigInt(1e17).toString(),
};

const relayer = await client.relay.create(requestParams);
// ...

创建后,该脚本将获取 Relayer ID 并创建一个 API 密钥和密钥集以通过它发送交易。Relayer ID 自动存储在 relayer.json 文件中,其 API 参数存储在 .env 文件中。

1.3 使用 Hardhat 编译合约

contracts 目录中,你可以找到 SimpleRegistry.solRegistry.sol 合约。前者合约包含 meta-transaction 功能,你可以在此处看到:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/metatx/ERC2771Context.sol";
import "@openzeppelin/contracts/metatx/ERC2771Forwarder.sol";

contract Registry is ERC2771Context {
  event Registered(address indexed who, string name);

  mapping(address => string) public names;
  mapping(string => address) public owners;

  constructor(ERC2771Forwarder forwarder) // 初始化受信任 forwarder
    ERC2771Context(address(forwarder)) {
  }

  function register(string memory name) external {
    require(owners[name] == address(0), "Name taken");
    address owner = _msgSender(); // 从 msg.sender 更改
    owners[name] = owner;
    names[owner] = name;
    emit Registered(owner, name);
  }
}

运行 npx hardhat compile 以编译它以进行部署。

1.4 使用 Relayer 部署

你可以使用 defender-sdk 包中的 Relayer 客户端轻松部署已编译的智能合约,而无需处理私钥。

deploy.js 脚本从本地 .env 文件中提取 Relayer 的凭据,以及 RegistryERC2771Forwarder 合约的 artifacts,并使用 ethers.js 进行部署。这些合约的相关地址将保存到本地文件 deploy.json 中。

// ...
const creds = {
  relayerApiKey: process.env.RELAYER_API_KEY,
  relayerApiSecret: process.env.RELAYER_API_SECRET,
};
const client = new Defender(creds);

const provider = client.relaySigner.getProvider();
const signer = client.relaySigner.getSigner(provider, { speed: 'fast' });

const forwarderFactory = await ethers.getContractFactory('ERC2771Forwarder', signer)
const forwarder = await forwarderFactory.deploy('ERC2771Forwarder')
  .then((f) => f.deployed())

const registryFactory = await ethers.getContractFactory('Registry', signer)
const registry = await registryFactory.deploy(forwarder.address)
  .then((f) => f.deployed())
// ...

使用 yarn deploy 运行此脚本。

合约部署后,可以安全地删除 Relayer 密钥和密钥;除非需要额外的本地测试,否则不需要它们。合约地址将保存在 deploy.json 文件中。

1.5 通过 API 创建 Action

演示应用程序使用 Action 来提供必要的逻辑,以告诉 Relayer 将交易发送到 Forwarder 合约,并提供签名者的地址。每次从应用程序调用其 webhook 时,都会触发 Action。

由于组件之间的紧密关系,Relayer 凭据可以通过简单地实例化一个新的 provider 和 signer 安全地提供给 Action。

Action 在这里的位置至关重要 — 只有 Action 的 webhook 暴露给前端。Action 的作用是根据分配给它的逻辑执行交易:如果用户有资金,他们支付交易费用。如果不是,则 Relayer 支付交易费用。

重要的是,Relayer 的 API 密钥和密钥与前端隔离。如果 Relayer 密钥暴露,任何人都可以潜在地使用 Relayer 发送他们想要的任何交易。

这是 Action 的代码,可以在 action/index.js 中找到:

const { Defender } = require('@openzeppelin/defender-sdk');
const { ethers } = require('hardhat')

const { ForwarderAbi } = require('../../src/forwarder');
const ForwarderAddress = require('../../deploy.json').ERC2771Forwarder;

async function relay(forwarder, request, signature, whitelist) {
  // 根据白名单决定是否要中继此请求
  const accepts = !whitelist || whitelist.includes(request.to);
  if (!accepts) throw new Error(`Rejected request to ${request.to}`);

  // 在 forwarder 合约上验证请求
  const valid = await forwarder.verify(request, signature);
  if (!valid) throw new Error(`Invalid request`);

  // 通过 relayer 将 meta-tx 发送到 forwarder 合约
  const gasLimit = (parseInt(request.gas) + 50000).toString();
  return await forwarder.execute(request, signature, { gasLimit });
}

async function handler(event) {
  // 解析 webhook 有效负载
  if (!event.request || !event.request.body) throw new Error(`Missing payload`);
  const { request, signature } = event.request.body;
  console.log(`Relaying`, request);

  // 初始化 Relayer provider 和 signer,以及 forwarder 合约
  const creds = { ... event };

  const client = new Defender(creds);

  const provider = client.relaySigner.getProvider();
  const signer = client.relaySigner.getSigner(provider, { speed: 'fast' });
  const forwarder = new ethers.Contract(ForwarderAddress, ForwarderAbi, signer);

  // 中继交易!
  const tx = await relay(forwarder, request, signature);
  console.log(`Sent meta-tx: ${tx.hash}`);
  return { txHash: tx.hash };
}

module.exports = {
  handler,
  relay,
}

请注意,Action 代码必须包含一个 index.js 文件,该文件导出一个 handler 入口点。如果代码依赖于任何外部依赖项(例如导入的 ABI),则必须使用 webpack、rollup 等捆绑 Action。你可以通过 Defender 或使用 defender-sdk 包创建一个 Action。

运行 yarn create-action 以编译代码并通过 SDK 的 action.create() 方法使用捆绑的代码创建 Action:

// ...
const { actionId } = await client.action.create({
  name: "Relay MetaTx",
  encodedZippedCode: await client.action.getEncodedZippedCodeFromFolder('./build/action'),
  relayerId: relayerId,
  trigger: {
    type: 'webhook'
  },
  paused: false
});
// ...

前往 Defender Actions 并复制 Actions 的 webhook,以便你可以测试功能并将应用程序连接到 Action 以中继 meta-transactions。

复制 Webhook

将 Action webhook 保存到你的 .env 文件中作为 WEBHOOK_URL,并在 /app .env 文件中作为 REACT_APP_WEBHOOK_URL

使用 yarn sign 后跟 yarn invoke 测试 meta-transaction 的功能。

1.6 创建 Web 应用程序

关键构建块已经铺设好,所以接下来就是制作一个利用这些组件的 Web 应用程序。

你可以在 register.js 文件中看到这种关系的详细信息。用户的交易请求通过 Action 的 webhook 发送到 Relayer,这会根据应用程序提供的参数执行 Action 的逻辑。请注意,签名者的 nonce 是从交易中递增的。

import { ethers } from 'ethers';
import { createInstance } from './forwarder';
import { signMetaTxRequest } from './signer';

async function sendTx(registry, name) {
  console.log(`Sending register tx to set name=${name}`);
  return registry.register(name);
}

async function sendMetaTx(registry, provider, signer, name) {
  console.log(`Sending register meta-tx to set name=${name}`);
  const url = process.env.REACT_APP_WEBHOOK_URL;
  if (!url) throw new Error(`Missing relayer url`);

  const forwarder = createInstance(provider);
  const from = await signer.getAddress();
  const data = registry.interface.encodeFunctionData('register', [name]);
  const to = registry.address;

  const request = await signMetaTxRequest(signer.provider, forwarder, { to, from, data });

  return fetch(url, {
    method: 'POST',
    body: JSON.stringify(request),
    headers: { 'Content-Type': 'application/json' },
  });
}

export async function registerName(registry, provider, name) {
  if (!name) throw new Error(`Name cannot be empty`);
  if (!window.ethereum) throw new Error(`User wallet not found`);

  await window.ethereum.enable();
  const userProvider = new ethers.BrowserProvider(window.ethereum);
  const userNetwork = await userProvider.getNetwork();
  console.log(userNetwork)
  if (userNetwork.chainId !== 11155111) throw new Error(`Please switch to Sepolia for signing`);

  const signer = userProvider.getSigner();
  const from = await signer.getAddress();
  const balance = await provider.getBalance(from);

  const canSendTx = balance.gt(1e15);
  if (canSendTx) return sendTx(registry.connect(signer), name);
  else return sendMetaTx(registry, provider, signer, name);
}

2. ERC-2612:Permit 函数

EIP-2612 引入了 permit 函数,这是一种在 ERC-20 代币中启用 gasless 交易的工具。通过使用户能够通过签名消息而不是 approve 函数来修改其 allowance 的方法扩展 ERC-20 接口,此标准使用户能够在不直接支付 gas 费用的情况下批准代币。此标准使 relayer 服务能够代表用户执行交易,同时用户只需签署消息。

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

此函数基于已签名的批准修改 owner 的代币的 spender 的 allowance。签名分为 vrs 组件以进行验证。

2.1 EIP-712 签名前端

它如何使用 EIP-712 进行结构化数据签名:EIP-2612 利用 EIP-712 来创建和签名结构化数据。这提供了正在签名的数据的人类可读表示,从而增强了安全性和用户体验。示例代码:

// ...
 const domain = {
    name: name,
    version: '1',
    chainId: chainId,
    verifyingContract: ERC20_ADDRESS,
  };

  const types = {
    Permit: [\
      { name: 'owner', type: 'address' },\
      { name: 'spender', type: 'address' },\
      { name: 'value', type: 'uint256' },\
      { name: 'nonce', type: 'uint256' },\
      { name: 'deadline', type: 'uint256' },\
    ]
  };

  const value = {
    owner: OWNER_ADDRESS,
    spender: SPENDER_ADDRESS,
    value: amount,
    nonce: nonce,
    deadline: deadline,
  };

  const signature = await wallet.signTypedData(domain, types, value);
  const sig = ethers.Signature.from(signature);
  const recoveredAddress = ethers.verifyTypedData(domain, types, value, signature);

  const request = {
    owner: OWNER_ADDRESS,
    spender: SPENDER_ADDRESS,
    amount,
    deadline,
    v: sig.v,
    r: sig.r,
    s: sig.s
  };

  return fetch(`${url}/relayerForwardMessage`, {
    method: 'POST',
    body: JSON.stringify(request),
    headers: { 'Content-Type': 'application/json' },
  });

2.2 Relayer 服务

创建一个后端服务以与 Defender Relayer 交互。该服务最初需要设置 Defender Relayer。配置完成后,它将处理来自前端的传入请求,并将已签名的 EIP-712 消息转发到合约。该服务将利用 Relayer 执行合约的 permit 函数,从而允许 Relayer 支付 gas 费用。该服务将促进最终用户的代币批准,从而可以使用 Relayer 进行后续操作,例如将代币转账到不同的钱包。

import { ethers, defender } from "hardhat";

// ...
const creds = {
  relayerApiKey: process.env.RELAYER_API_KEY,
  relayerApiSecret: process.env.RELAYER_API_SECRET,
};
const client = new Defender(creds);

const provider = client.relaySigner.getProvider();
const signer = client.relaySigner.getSigner(provider, { speed: 'fast' });

const erc20 = await ethers.getContractAt("ERC20Token", CONTRACT_ADDRESS);

// 你现在可以使用这些值来调用 permit 函数
// permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
const tx = await erc20.permit(
  request.owner,
  request.spender,
  request.amount,
  request.deadline,
  request.v,
  request.r,
  request.s
);

await tx.wait();
console.log("Permit executed!");

// 示例后续操作
const transferTx = await erc20.transferFrom(request.owner, to, request.amount);
await transferTx.wait();
// ...

3. ERC-3009:Transfer with Authorization

ERC-3009 引入了一种通过链下授权进行 gasless 代币转账的标准。此标准允许用户签署消息以授权代币转账,然后可以通过 Defender Relayer 服务将其提交到链上。与 EIP-2612 的比较(签名差异): 虽然 EIP-2612 侧重于批准,但 ERC-3009 直接授权转移。主要区别在于:

  • 目的:ERC-3009 授权特定的转移,而 EIP-2612 批准 allowance。

  • 灵活性:ERC-3009 不需要 EIP-712 进行结构化数据签名,从而在消息格式方面提供了更大的灵活性。

  • 时间窗口:ERC-3009 包括 validAfter 和 validBefore 参数,从而可以更精确地控制何时可以执行授权。

函数定义:

function transferWithAuthorization(
    address from,
    address to,
    uint256 value,
    uint256 validAfter,
    uint256 validBefore,
    bytes32 nonce,
    uint8 v,
    bytes32 r,
    bytes32 s
) external

3.1 EIP-712 签名前端

与 ERC-2612 类似,你可以使用 EIP-712 格式在前端以最终用户的身份签署消息。虽然 ERC-3009 为前端消息签名提供了更大的灵活性,但此示例遵循 EIP-712 标准。示例代码:

  //...

  const validAfter = Math.floor(Date.now() / 1000); // 现在
  const validBefore = validAfter + 3600; // validAfter 后 1 小时
  const value = ethers.parseEther("10"); // 要转移的金额
  const nonce = ethers.randomBytes(32);

  const domain = {
    name: name,
    version: '1',
    chainId: chainId,
    verifyingContract: ERC20_ADDRESS,
  };

  const types = {
    TransferWithAuthorization: [\
      { name: 'from', type: 'address' },\
      { name: 'to', type: 'address' },\
      { name: 'value', type: 'uint256' },\
      { name: 'validAfter', type: 'uint256' },\
      { name: 'validBefore', type: 'uint256' },\
      { name: 'nonce', type: 'bytes32' },\
    ]
  };

  const valueToSign = {
    from: FROM_ADDRESS,
    to: TO_ADDRESS,
    value: value,
    validAfter: validAfter,
    validBefore: validBefore,
    nonce: nonce,
  };

  const signature = await wallet.signTypedData(domain, types, valueToSign);
  const sig = ethers.Signature.from(signature);
  const request = {
    from: FROM_ADDRESS,
    to: TO_ADDRESS,
    value,
    validAfter,
    validBefore,
    nonce,
    v: sig.v,
    r: sig.r,
    s: sig.s
  };

  return fetch(`${url}/relayerForwardMessage`, {
    method: 'POST',
    body: JSON.stringify(request),
    headers: { 'Content-Type': 'application/json' },
  });

3.2 Relayer 服务

创建一个后端服务以与 Defender Relayer 交互。该服务最初需要设置 Defender Relayer。配置完成后,它将处理来自前端的传入请求,并将已签名的消息转发到合约。该服务将利用 Relayer 执行合约的 transferWithAuthorization 函数,从而允许 Relayer 支付 gas 费用。该服务将促进最终用户的代币转账。

import { ethers, defender } from "hardhat";

// ...
const creds = {
  relayerApiKey: process.env.RELAYER_API_KEY,
  relayerApiSecret: process.env.RELAYER_API_SECRET,
};
const client = new Defender(creds);

const provider = client.relaySigner.getProvider();
const signer = client.relaySigner.getSigner(provider, { speed: 'fast' });

const erc20 = await ethers.getContractAt("ERC20Token", CONTRACT_ADDRESS);

const tx = await erc20.transferWithAuthorization(
    request.from,
    request.to,
    request.value,
    request.validAfter,
    request.validBefore,
    request.nonce,
    request.v,
    request.r,
    request.s
);

await tx.wait();
console.log("TransferWithAuthorization executed!");
// ...

试用应用程序

安装必要的依赖项并运行该应用程序。

$ cd app
$ yarn
$ yarn start
  1. 打开应用程序:http://localhost:3000/

  2. 在 Metamask 中更改为 Sepolia 网络

  3. 输入要注册的名称并在 Metamask 中签署 meta-transaction

  4. 你的姓名将被注册,显示创建 meta-transaction 的地址和姓名。

使用前端亲自查看它的工作原理!比较使用有资金的帐户注册表签名时发生的情况,然后尝试使用 ETH 余额为零的帐户进行注册。

参考文献

← 添加完整的私有网络

工厂克隆的自动监控 →

  • 原文链接: docs.openzeppelin.com/de...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
OpenZeppelin
OpenZeppelin
江湖只有他的大名,没有他的介绍。