本文介绍了如何使用OpenZeppelin Defender实现gasless元交易,包括使用Relayer服务代表用户发送交易,避免用户管理私钥等复杂操作。文章详细讲解了ERC-2771、ERC-2612和ERC-3009三种不同的gasless交易标准,并提供了相应的代码示例和操作步骤, 同时也介绍了如何使用 Defender 的 Relayers 和 Actions 实现安全、便捷的元交易。
Defender 提供了一种无缝且安全的方式,使用 Relayer 实现 gasless meta-transactions。这些 Relayer 代表用户处理发送交易,从而消除了用户管理私钥、交易签名、nonce 管理、gas 预估和交易包含的需求。
这个演示应用程序展示了如何不仅使用 ERC-2771 实现 meta-transactions,还探索了其他 gasless transaction 标准:
ERC-2771: 安全原生 Meta Transactions:这个演示应用程序 使用 ERC2771Forwarder 和 ERC2771Context 来实现 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 预估和交易包含。
你可以在此处查看实时演示应用程序。如果用户有可用资金来支付交易费用,它会直接接受注册,否则数据将作为 meta-transaction 发送。
在示例代码中,SimpleRegistry
合约 的功能是获取一个字符串并存储它。该合约的 meta-transaction 实现 通过将签名者与交易发送者分离来实现相同的结果。
在比较代码时,请注意 meta-transaction 使用 _msgSender()
而不是 SimpleRegistry 使用 msg.sender
。通过从 ERC2771Context
和 ERC2771Forwarder
扩展,该合约变得能够进行 meta-transaction。
所有 OpenZeppelin 合约都与 _msgSender() 的使用兼容。 |
两个合约之间的第二个根本变化是 meta-transaction 合约(Registry)需要指定受信任 forwarder 的地址,在本例中,它是 ERC2771Forwarder
合约的地址。
首先,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"
运行 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
文件中。
在 contracts
目录中,你可以找到 SimpleRegistry.sol
和 Registry.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
以编译它以进行部署。
你可以使用 defender-sdk
包中的 Relayer 客户端轻松部署已编译的智能合约,而无需处理私钥。
deploy.js
脚本从本地 .env
文件中提取 Relayer 的凭据,以及 Registry
和 ERC2771Forwarder
合约的 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
文件中。
演示应用程序使用 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。
将 Action webhook 保存到你的 .env
文件中作为 WEBHOOK_URL
,并在 /app .env
文件中作为 REACT_APP_WEBHOOK_URL
。
使用 yarn sign
后跟 yarn invoke
测试 meta-transaction 的功能。
关键构建块已经铺设好,所以接下来就是制作一个利用这些组件的 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);
}
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
。签名分为 v
、r
和 s
组件以进行验证。
它如何使用 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' },
});
创建一个后端服务以与 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();
// ...
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
与 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' },
});
创建一个后端服务以与 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
打开应用程序:http://localhost:3000/
在 Metamask 中更改为 Sepolia 网络
输入要注册的名称并在 Metamask 中签署 meta-transaction
你的姓名将被注册,显示创建 meta-transaction 的地址和姓名。
使用前端亲自查看它的工作原理!比较使用有资金的帐户注册表签名时发生的情况,然后尝试使用 ETH 余额为零的帐户进行注册。
- 原文链接: docs.openzeppelin.com/de...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!