如何集成 Permit2
由 Sara Reynolds 和 Zach Pomerantz 撰写
一个安全的兑换体验必须在安全性和速度之间取得平衡。代币授权就是这种权衡的一个很好的例子。自我托管允许用户指定谁有权限从他们的钱包中转移资金,但发送代币授权交易需要时间并消耗 gas。为了改进这个过程并减少兑换所需的点击次数,我们在2022发布了 Permit2 智能合约。
Permit2 允许代币授权在不同的合约之间共享和管理,而不是为每个新的智能合约签署代币授权,从而创建一个更统一、更具成本效益和更安全的用户体验。Permit2 已经在改进 Uniswap 的产品用户体验,但它是为任何人设计的。
本指南将向你展示如何使用。在其中,我们将:
AllowanceTransfer
函数Permit2 是一个代币授权合约,通过引入基于签名的授权和转移来迭代现有的代币授权机制,适用于任何 ERC20 代币,无论是否支持 EIP-2612。
对于每个代币,用户必须提交一次传统的授权,将 Permit2 设置为授权的支出者。与 Permit2 集成的合约可以“借用”用户在规范 Permit2 合约上授权的任何代币的支出者状态。任何集成的合约都可以通过签署的链下 Permit2 消息暂时成为支出者。这有效地“共享”了一次性的传统代币授权,并跳过了每个支持 Permit2 的协议的单独授权交易——直到 Permit2 消息过期。这是一个主要的用户体验升级,具有额外的安全保障,如过期授权和锁定功能 。
Permit2 的设计为我们提供了一些有用的功能。完整的功能列表可以在我们的自述文件中找到,但值得注意的是:
permit
方法的代币。Permit2 从网络效应中受益匪浅。随着更多团队与 Permit2 集成,我们将开始看到网络效应。那么我们该如何实现呢?
对于应用程序开发者来说,Permit2 是一个非常有用的工具,可以创建一个更简单、更便宜和更安全的用户体验,用户会对此表示赞赏。在本指南中,我们将编写一个与 Permit2 集成的合约,以利用这种新的元交易授权系统。
AllowanceTransfer
合约使用时间限制的到期与其他支出者共享和管理授权。它在功能上类似于传统的代币授权,处理额度、转移和授权到期。如果你的应用程序需要频繁访问和转移代币,这种授权方式最为有效。通过额度转移方式的授权,为每个允许的支出者设置和保存授权金额和到期时间。这允许支出者在达到支出限额或授权到期之前根据需要转移代币。
SignatureTransfer
合约引入了一次性代币转移的许可。支出者被允许转移代币,但仅限于签名被使用的同一交易中。要再次转移资金,你需要再次请求此签名。签名转移方法非常适合那些不经常需要用户代币并希望确保对授权有更严格限制的应用程序。持久的限制和到期时间不会被存储,也不会留下“悬挂的授权”。SignatureTransfer 还允许你通过 permitWitnessTransferFrom 进行更高级的 Permit2 集成,我们计划在后续博客文章中详细介绍。
根据你的应用程序所需的授权类型,你的合约需要调用以下函数之一——尽管这些并不是 Permit2 中唯一可调用的函数:
在这个例子中,我们将编写一个合约以在 Permit2 上使用额度转移风格的授权。我们假设用户已经设置了代币授权以允许 Permit2 作为支出者。
由于我们想要的permit
入口点位于AllowanceTransfer
合约中,我们将首先创建我们的Demo
合约以使用 Permit2 中公开的IAllowanceTransfer
接口。在你的构造函数中,你可以传递 Permit2 地址并将其保存为不可变值。Permit2 部署在主网、以太坊、Optimism、Arbitrum、Polygon 和 Celo 的相同地址 。
import {IAllowanceTransfer} from "Permit2/src/interfaces/IAllowanceTransfer.sol";
contract Demo {
IAllowanceTransfer public immutable permit2;
constructor(address _permit2) {
permit2 = IAllowanceTransfer(_permit2);
}
}
现在你已经使用规范的 Permit2 合约进行了初始化,你的合约可以通过调用permit
来设置授权。这个permit
函数有几个参数。
function permit(address owner, IAllowanceTransfer.PermitSingle memory permitSingle, bytes calldata signature) external
要调用 permit
,我们需要提供所有者的地址、他们的签名以及 PermitSingle
信息。PermitSingle 是一个结构体,包含用户签署的 permit
信息。在 Permit2 允许我们的演示合约成为支出者之前,它必须通过用户的签名和 PermitSingle
数据来验证用户。
为了调用 permit
,我们可以创建一个 permitThroughPermit2
函数来传递这些参数。稍后,我们将看到应用程序前端如何请求和构建 PermitSingle
。
function permitThroughPermit2(PermitSingle calldata permitSingle, bytes calldata signature) public {
permit2.permit(msg.sender, permitSingle, signature);
}
通过传递 msg.sender
作为 owner
变量,我们要求函数的调用者也是签署 PermitSingle
信息的人。这减少了我们为 permitThroughPermit2
收集的一个参数。
在我们允许合约使用 Permit2 授权转移用户的代币之前,我们需要检查我们的演示合约是否被设置为支出者。这会检查用户是否调用了此函数。如果是这样,那么我们知道我们在合约上有支出权限。我们可以添加一个自定义错误 InvalidSpender
,并编写一个检查,强制通过 permitThroughtPermit2
传递的签名构建我们的演示合约地址作为支出者。
我们的函数现在看起来像这样:
function permitThroughPermit2(PermitSingle calldata permitSingle, bytes calldata signature) public {
if (permitSingle.spender != address(this)) revert InvalidSpender();
permit2.permit(msg.sender, permitSingle, signature);
}
Permit2 使用一种持久映射的设计。该设计存储用户设置的代币授权信息,如支出者、金额和到期时间。因为这些信息是存储的,所以此授权将持续到 permitSingle.permitDetails.expiration
。在该时间戳之前,我们的演示合约可以转移资金,直到达到允许的额度。让我们编写另一个函数,通过调用 Permit2 中的 transferFrom
函数 将资金发送到此合约。
function transferToMe(address token, uint160 amount) public {
permit2.transferFrom(msg.sender, address(this), amount, token);
// ...执行一些酷炫的操作...
}
通过传入 msg.sender
,我们确保只有被授权的用户可以触发此函数,将所需代币的某个金额从发送者转移到演示合约。如果这只是我们传入的一个参数,那么任何人都可以触发函数从用户那里提取资金。调用此函数后,我们的演示合约现在拥有可以兑换、存入借贷协议或进行其他操作的代币。
你可能已经注意到,permit
和 transferFrom
是我们合约中的两个独立入口点。如果保持这样,意味着用户必须发起两笔交易才能在与 Permit2 集成的任何合约中获取他们的代币。我们可以将 permit
和 transferFrom
调用合并为一个入口点:
function permitAndTransferToMe(IAllowanceTransfer.PermitSingle calldata permitSingle, bytes calldata signature, uint160 amount) public {
if (permitSingle.spender != address(this)) revert InvalidSpender();
permit2.permit(msg.sender, permitSingle, signature);
permit2.transferFrom(msg.sender, address(this), amount, permitSingle.details.token);
//...执行更酷的操作...
}
如果我们的演示合约通过 permit
被授权的时间超过单笔交易,我们不需要每次调用 transferFrom
和提取代币时都传入签名。
我们使用 AllowanceTransfer
作为我们的入口点。SignatureAllowance
让我们可以做同样的事情,但授权不会超过单笔交易。我们可以使用 permitTransferFrom
和 permitWitnessTransferFrom
函数来确保更严格的限制,但代价是更高的交易成本。
要在前端集成 Permit2,我们将使用 Permit2 SDK。我们将调用我们的演示合约,使用 permitAndTransferToMe
。
首先,为了获取 PermitSingle
的签名,你需要下一个有效的 nonce,可以使用 SDK 获取。
import { AllowanceProvider, PERMIT2_ADDRESS } from '@uniswap/Permit2-sdk'
const allowanceProvider = new AllowanceProvider(ethersProvider, PERMIT2_ADDRESS)
const { amount: permitAmount, expiration, nonce } = allowanceProvider.getAllowanceData(user, token, ROUTER_ADDRESS);
// 你还可以在此处检查金额/到期时间,看看你是否已经被授权 -
// 你可能不需要生成新的签名。
一旦有了这些,你可以构建 PermitSingle
对象,我们需要将其作为第一个参数传递。该对象由 SDK 定义为一个类型化接口,以确保它与合约的结构匹配。要构建它,我们需要一些东西:
permit
不再有效的截止日期。在 Uniswap Labs,我们使用 30 天。这些是我们的演示合约发送到 Permit2 合约的相同输入变量。
import { MaxAllowanceTransferAmount, PermitSingle } from '@uniswap/Permit2-sdk'
const PERMIT_EXPIRATION = ms`30d`
const PERMIT_SIG_EXPIRATION = ms`30m`
/**
* 将到期时间(以毫秒为单位)转换为适合 EVM 的截止时间(以秒为单位)。
* Permit2 将到期时间表示为截止时间,但 JavaScript 通常使用毫秒,
* 因此提供了此便捷函数。
*/
function toDeadline(expiration: number): number {
return Math.floor((Date.now() + expiration) / 1000)
}
const permitSingle: PermitSingle = {
details: {
token: tokenAddress,
amount: MaxAllowanceTransferAmount,
// 你可以设置自己的截止日期 - 我们使用 30 天。
expiration: toDeadline(/* 30 days= */ 1000 * 60 * 60 * 24 * 30),
nonce,
},
spender: spenderAddress,
// 你可以设置自己的截止日期 - 我们使用 30 分钟。
sigDeadline: toDeadline(/* 30 minutes= */ 1000 * 60 * 60 * 30),
}
现在你有了 PermitSingle
对象,你可以将其传递给 SDK 以获取用户签名的类型化数据 ,使用 AllowanceTransfer.getPermitData
。这需要 PermitSingle
、链上的 Permit2 合约地址和链 ID,并返回可以直接传递给像 ethers
这样的库以请求用户签名的类型化数据。
import { AllowanceTransfer, PERMIT2_ADDRESS } from '@uniswap/Permit2-sdk'
const { domain, types, values } = AllowanceTransfer.getPermitData(permitSingle, PERMIT2_ADDRESS, chainId)
// 我们使用 ethers 签名者来签署这些数据:
const signature = await provider.getSigner().signTypedData(domain, types, value)
// 这假设使用的是 ethers@6。我们的 web 应用使用的是 ethers@5,我们遇到了一些跨钱包兼容性问题,所以我们实际上使用一个辅助方法来签名:https://github.com/Uniswap/conedison/blob/6fdf4baf13a799ca6d37f6d3222f9194e1750007/src/provider/signing.ts#L32。
你应该从 ethers
得到一个 signature
,你可以将其传递给其他使用它的方法。有关另一个示例,请查看我们如何传递给 Universal Router。
最后,我们将使用 ethers
实例化我们的合约,并使用它调用 permitAndTransferToMe
。
import { Contract } from 'ethers'
// 我们只需要提供我们将使用的 ABI。
const demoAbi = [
'function permitAndTransferToMe(PermitSingle calldata permitSingle, bytes calldata signature, uint160 amount)',
]
const demoContract = new Contract(demoContractAddress, demoAbi, provider.getSigner())
await demoContract.permitAndTransferToMe(permitSingle, signature, amount)
你已经到达终点了!虽然一开始可能看起来很吓人,但我们希望这篇博客能让它变得更容易。记得查看 Permit2 代码、我们的 SDK 和 文档 了解更多信息。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!