用 Cadence 实现 ERC20 的 approve 机制

Created
Mar 14, 2022 07:52 AM
作者
翻译
Tags
notion image
你可以在这里找到本文的所有代码。
 
在 ERC20 代币标准中,定义了这三个方法:
interface IERC20 { // ... function approve(address spender, uint256 value) external returns (bool); function allowance(address owner, address spender) external view returns (uint256); function transferFrom(address from, address to, uint256 value) external returns (bool); }
  • 通过 approve 某个地址 owner 可以授权其他地址 spender 花费自己一定金额的代币
  • 通过 allowance 我们可以查看 ownerspender 的授信额度余额
  • 通过 transferFromspender 可以从 owner 内取用授信额度内的金额
 
这几个方法在 ERC20 的世界中是必须的,尤其在使用各种 DeFi 产品的时候。以 Uniswap 为例,Alice 如果希望把自己的 USDC 换成 AAVE,则流程大概是这样:
💡
Alice 对 Uniswap:小 U 啊,我允许你动我的 USDC,给你 1000 USDC 的额度吧。 Alice 对 Uniswap:小 U 啊,我想要把 1000 USDC 换成 AAVE,给我搞定! Uniswap 对 USDC 合约:我代表 Alice 从她账上转 1000 USDC 到 AAVE-USDC 流动性池的账上! (假设算出来 Alice 能换到 5 个 AAVE) Uniswap 对 AAVE 合约:把这 5 个 AAVE 从我池子账上转到 Alice 账上!
从上面可以看出:
  • ERC20 标准下,所有代币都是由其合约做统一记账的。比如 Uniswap 从 Alice 账户向流动性池转账 USDC 的操作,实际上就是由 USDC 合约做一下账目变更。代币合约可以类比为现实世界中的银行,每个储户的资金只是他们账上的数字。
  • EVM 生态中和合约的交互有点像对自己的助理说话,给他授权,具体的执行由他负责。每笔交易的内容基本上就是对合约中特定方法的调用。执行操作的主要逻辑都在合约里。
在这样的机制下,approve 机制就显得格外重要。没有授权,则接下来的执行无从谈起。
 
在以对资源的处理为特色的 Cadence 世界中,情况就不一样了。以 BloctoSwap 为例,Alice 如果希望把自己的 1000 FUSD 换成 BLT,则:
💡
Alice 从自己的储藏室里面拿出 FUSD 保险箱,从中取出 1000 个 FUSD Alice 把 1000 个 FUSD 送进 BloctoSwap,BloctoSwap 收到后给她送回 2000 个 BLT
Alice 把 2000 个 BLT 放进自己的 BLT 保险箱,再把保险箱放进储藏室里
从上面可以看出:
  • 在 Flow 生态中,代币是由账户直接持有的,而非由代币合约记账表示。相较于 EVM 生态,这种模式是更去中心化的。
  • Flow 生态中和合约的交互更像是从工具箱中取工具,具体事务的执行还需要账户在交易中亲力亲为。Flow 的交易不是单纯的方法调用,还有许多执行逻辑。
 
两相对比,我们可以知道在 Flow 生态对 approve 机制并没有像 EVM 生态那样的刚需,但如果我们就是要用 Cadence 来实现 approve 功能,让别人可以动用自己的部分资金,应该怎么做呢?
 
首先我们回顾👆提到的 Flow 和 EVM 系的区别:
  • EVM 的代币只是合约中记录的数字,由一个中心账本管理。实现 approve 只需要在账本中记录下 spenderowner 和授信金额,转账的时候据此验证、调账即可。
  • Flow 的代币是分散存储在不同账户中的。要实现 approve 功能,我们需要让 spender 能够确实地访问到 owner 账户中的“保险箱”,而非在某个账本上调账。
这点差异使得在 Flow 上实现 ERC20 标准中的 approve 机制会更复杂。
 
Flow 是 Capability-based Access Control,可以理解成“认证不认人”,持有特定“令牌”就能做特定的事。因此我们需要设计一个能让 spender 访问并有限制地使用 owner ”保险箱“的”令牌“。再具体整理下,就有以下需求:
  1. owner 可以授权给多个 spender 访问自己的资金。
  1. spender 对资金的访问是受限于授信额度的,每个 spender 可以有不同的授信额度。
  1. 授信额度可以超过 owner 现有资金总量。比如 Alice 拥有 40 FUSD,他可以授权 Bob 和 Carl 最多动用 30 FUSD,Dave 最多动用 50 FUSD。
  1. owner 可以随时调整 spender 的授信额度,或者取消、恢复授信。
 
在 Flow 中,Capability 就是所谓的“令牌“。假设已经有了一个遵循官方 FT 接口的代币 FUSD,在我们的 /storage/fusdVault 路径中存放着 @FUSD.Vault ,如果我们是这个 Vault 的持有人 Alice,现在想让 Bob 也能够使用它,一个方法就是将其 link 到 /private 中,生成私有的 Capability<&FUSD.Vault>,再将这个 Capability 交给 Bob。这样一来,Bob 就可以通过 Capability 访问到我们的 @FUSD.Vault
这样做的问题在于:Bob 对于 Vault 的访问是无限制的。如果他想,他可以转走整个 Vault 中的资金。这样不符合需求 2。
如果我们把 Vault 对每个 spender 拆分出等同于授信额度的一份,则不满足需求 3。
 
我们重点解决对 Vault 访问无限制的问题。现在我们在 Vault 外再包一层,用于添加额度限制:
pub resource Allowance: AllowanceInfo, AllowanceProvider, AllowanceManager { pub var value: UFix64 priv let vaultCap: Capability<&{FungibleToken.Provider, FungibleToken.Balance}> pub fun getVaultOwner(): Address { return self.vaultCap.address } pub fun setAllowance(value: UFix64) { self.value = value } pub fun withdraw(amount: UFix64): @FungibleToken.Vault { pre { amount <= self.value: "Withdraw amount exceed allowance value" amount <= self.vaultCap.borrow()!.balance: "Withdraw amount exceed vault's balance" } self.value = self.value - amount return <- self.vaultCap.borrow()!.withdraw(amount: amount) } init(value: UFix64, vaultCap: Capability<&{FungibleToken.Provider, FungibleToken.Balance}>) { self.value = value self.vaultCap = vaultCap emit AllowanceCreated(by: self.owner?.address, value: value) } }
Allowance 资源的 vaultCap 字段存储的是访问目标 Vault 的 Capability,我们将其限定为实现 FungibleToken.ProviderFungibleToken.Balance 的任意资源。
另外,Allowance 还实现了 3 个接口:
pub resource interface AllowanceInfo { pub var value: UFix64 pub fun getVaultOwner(): Address } pub resource interface AllowanceProvider { pub fun withdraw(amount: UFix64): @FungibleToken.Vault } pub resource interface AllowanceManager { pub fun setAllowance(value: UFix64) }
  • AllowanceInfo 提供了这个授权的金额和创建人信息。
  • AllowanceProvider 提供了和 Vault 一致的 withdraw 方法,在具体实现中,我们需要比较取款金额和授信额度,保证取款金额在授信额度内。
  • AllowanceManager 提供了修改授信额度的方法。
现在 Alice 可以向 Bob 提供 Allowance 的 Capability 来让他可以动用自己的资金了。
对于 Bob 而言,他需要能够通过 AllowanceInfo 提供的字段来获取剩余额度和授权人,需要通过 AllowanceProviderwithdraw 方法来转账。但是他们不应该被允许调用 setAllowance 方法修改限额,所以提供给 Bob 的应该是 Capability<&{AllowanceProvider, AllowanceInfo}>
 
Bob 可以创建一个 AllowanceCapReceiver 来存放自己接收到的各种授权:
pub resource AllowanceCapReceiver: AllowanceCapReceiverPublic { priv var allowanceCaps: {Address: [Capability<&{AllowanceProvider, AllowanceInfo}>]} pub fun addAllowanceCap(_ cap: Capability<&{AllowanceProvider, AllowanceInfo}>) { let caps: [Capability<&{AllowanceProvider, AllowanceInfo}>] = self.allowanceCaps[cap.address] ?? [] caps.append(cap) self.allowanceCaps[cap.address] = caps } pub fun getAllowanceCapsInfoByApprover(_ approver: Address): [&{AllowanceInfo}] { let infos: [&{AllowanceInfo}] = [] if let caps = self.allowanceCaps[approver] { for cap in caps { if let info = cap.borrow() { infos.append(info) } } } return infos } pub fun getAllowanceCapsByApprover(_ approver: Address): [Capability<&{AllowanceProvider, AllowanceInfo}>] { return self.allowanceCaps[approver] ?? [] } init() { self.allowanceCaps = {} emit AllowanceCapReceiverCreated(by: self.owner?.address) } }
AllowanceReceiver 实现了下面的接口:
pub resource interface AllowanceCapReceiverPublic { pub fun addAllowanceCap(_ allowance: Capability<&{AllowanceProvider, AllowanceInfo}>) pub fun getAllowanceCapsInfoByApprover(_ approver: Address): [&{AllowanceInfo}] }
Bob 只会暴露 AllowanceCapReceiverPublic 中的方法给外部,使得 Alice 可以通过 addAllowanceCap 方法来对 Bob 进行授权。getAllowanceCapsByApprover 则只能由被授权方 Bob 自己调用。
 
Bob 可能收到来自多个其他账户的授权,所以我们需要一个数组来存放 allowances。
 
现在 Alice 和 Bob 要使用 Approver 合约里面的工具进行资金的授权,Alice 是授权人,Bob 是被授权人。那么:
  • Bob 需要创建 AllowanceCapReceiver,并暴露 AllowanceCapReceiverPublic 接口
import FUSD from "../contracts/FUSD.cdc" import FungibleToken from "../contracts/FungibleToken.cdc" import Approver from "../contracts/Approver.cdc" transaction { prepare(signer: AuthAccount) { if signer.borrow<&Approver.AllowanceCapReceiver>(from: Approver.AllowanceCapReceiverStoragePath) != nil { return } signer.save( <- Approver.createAllowanceCapReceiver(), to: Approver.AllowanceCapReceiverStoragePath ) signer.link<&{Approver.AllowanceCapReceiverPublic}>( Approver.AllowanceCapReceiverPubPath, target: Approver.AllowanceCapReceiverStoragePath ) } }
  • Alice 生成 Vault 的私有 Capability,并将之装入 allowance 中,再将 allowance 也放入 /private 内,生成私有的 Capability<&{Approver.AllowanceInfo, Approver.AllowanceProvider}> 。之后 Alice 从 Bob 账户中借出 AllowanceCapReceiver,将上面生成的 Allowance 的 Capability 放进 Receiver 中供 Bob 使用(这个 Capability 可以也可以交给其他人使用)
import FUSD from "../contracts/FUSD.cdc" import FungibleToken from "../contracts/FungibleToken.cdc" import Approver from "../contracts/Approver.cdc" transaction(spender: Address, value: UFix64) { let allowanceCap: Capability<&{Approver.AllowanceProvider, Approver.AllowanceInfo}> prepare(signer: AuthAccount) { signer.link<&{FungibleToken.Provider, FungibleToken.Balance}>(/private/fusdVault, target: /storage/fusdVault) let vaultCap = signer.getCapability<&{FungibleToken.Provider, FungibleToken.Balance}>(/private/fusdVault)! let allowance <- Approver.createAllowance( value: value, vaultCap: vaultCap ) let pathID = "fusdAllowanceFor".concat(spender.toString()) let storagePath = StoragePath(identifier: pathID)! let publicPath = PublicPath(identifier: pathID)! let privatePath = PrivatePath(identifier: pathID)! signer.save(<- allowance, to: storagePath) signer.link<&{Approver.AllowanceInfo}>(publicPath, target: storagePath) signer.link<&{Approver.AllowanceProvider, Approver.AllowanceInfo}>( privatePath, target: storagePath ) self.allowanceCap = signer.getCapability<&{Approver.AllowanceProvider, Approver.AllowanceInfo}>(privatePath) } execute { let receiver = getAccount(spender).getCapability<&{Approver.AllowanceCapReceiverPublic}>( Approver.AllowanceCapReceiverPubPath).borrow() ?? panic("Could not borrow AllowanceCapReceiver capability") receiver.addAllowanceCap(self.allowanceCap) } }
  • Bob 现在可以从 AllowanceCapReceiver 中取出 Alice 给的 Capability,并通过它转走 Alice 账户中的资金了。这并不影响 Alice 自己对资金的自由使用。
import FUSD from "../contracts/FUSD.cdc" import FungibleToken from "../contracts/FungibleToken.cdc" import Approver from "../contracts/Approver.cdc" transaction(from: Address, to: Address, value: UFix64) { let capReceiver: &Approver.AllowanceCapReceiver prepare(signer: AuthAccount) { self.capReceiver = signer.borrow<&Approver.AllowanceCapReceiver>(from: Approver.AllowanceCapReceiverStoragePath) ?? panic("Could not borrow AllowanceCapReceiver reference") } execute { let vaultReceiver = getAccount(to).getCapability<&{FungibleToken.Receiver}>(/public/fusdReceiver).borrow() ?? panic("Could not get Receiver capability") let cap = self.capReceiver.getAllowanceCapsByApprover(from)[0] vaultReceiver.deposit(from: <- cap.borrow()!.withdraw(amount: value)) } }
  • Alice 可以随时修改给 Bob 的额度。
import Approver from "../contracts/Approver.cdc" transaction(spender: Address, value: UFix64) { prepare(signer: AuthAccount) { let pathID = "fusdAllowanceFor".concat(spender.toString()) let storagePath = StoragePath(identifier: pathID)! let allowance = signer.borrow<&Approver.Allowance>(from: storagePath) ?? panic("Could not borrow Allowance reference") allowance.setAllowance(value: value) } }
  • Alice 也可以终止给 Bob 授信。
transaction(spender: Address) { prepare(signer: AuthAccount) { let pathID = "fusdAllowanceFor".concat(spender.toString()) let privatePath = PrivatePath(identifier: pathID)! let publicPath = PublicPath(identifier: pathID)! signer.unlink(privatePath) signer.unlink(publicPath) } }
  • Alice 也可以恢复给 Bob 的授信。
import Approver from "../contracts/Approver.cdc" transaction(spender: Address) { prepare(signer: AuthAccount) { let pathID = "fusdAllowanceFor".concat(spender.toString()) let storagePath = StoragePath(identifier: pathID)! let privatePath = PrivatePath(identifier: pathID)! let publicPath = PublicPath(identifier: pathID)! signer.link<&{Approver.AllowanceProvider, Approver.AllowanceInfo}>(privatePath, target: storagePath) signer.link<&{Approver.AllowanceInfo}>(publicPath, target: storagePath) signer.getCapability<&{Approver.AllowanceProvider, Approver.AllowanceInfo}>(privatePath).borrow() ?? panic("Could not get private {AllowanceProvider, AllowanceInfo} capability") signer.getCapability<&{Approver.AllowanceInfo}>(publicPath).borrow() ?? panic("Could not get public {AllowanceInfo} capability") } }
  • Alice 可以查询自己给某人的授信情况。
import Approver from "../contracts/Approver.cdc" pub struct AllowanceInfo { pub let value: UFix64 pub let vaultOwner: Address init(value: UFix64, vaultOwner: Address) { self.value = value self.vaultOwner = vaultOwner } } pub fun main(approver: Address, spender: Address): AllowanceInfo { let path = PublicPath(identifier: "fusdAllowanceFor".concat(spender.toString()))! let info = getAccount(approver) .getCapability<&{Approver.AllowanceInfo}>(path) .borrow() ?? panic("Could not borrow AllowanceInfo capability") return AllowanceInfo(value: info.value, vaultOwner: info.getVaultOwner()) }
  • Bob 也可以查询目前自己所有的授信情况。
import Approver from "../contracts/Approver.cdc" pub struct AllowanceInfo { pub let value: UFix64 pub let vaultOwner: Address init(value: UFix64, vaultOwner: Address) { self.value = value self.vaultOwner = vaultOwner } } pub fun main(approver: Address, spender: Address): [AllowanceInfo] { let receiver = getAccount(spender) .getCapability<&{Approver.AllowanceCapReceiverPublic}>(Approver.AllowanceCapReceiverPubPath) .borrow() ?? panic("Could not borrow AllowanceCapReceiverPublic capability") let infos: [AllowanceInfo] = [] for i in receiver.getAllowanceCapsInfoByApprover(approver) { infos.append( AllowanceInfo(value: i.value, vaultOwner: i.getVaultOwner()) ) } return infos }
另外,在 Solidity 中,如果 Alice 只给 Bob 授信,则只有 Bob 可以动用 Alice 的资产。在 Cadence 中,Bob 完全可以把 Capability 转移给 Carl,让 Carl 也可以动用 Alice 的资产(当然,额度是 Bob 和 Carl 共享的)。这是由二者访问机制的差异造成的。事实上,如果 Bob 和 Carl 共谋了,则 Bob 将资产转移给 Carl 和 Bob 给 Carl Capability 让他自行取用也没什么区别。
 
Alice 和 Bob 的存储大概是这样的情况:
notion image
 
至此我们实现了和 ERC20 代币标准中的 approve 机制类似的功能,而且在总限额之外还可以方便地添加授权过期时间、单笔最大限额等机制。虽然目前看起来没什么应用场景,不过……这不重要hhh。