你可以在这里找到本文的所有代码。
在 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
我们可以查看owner
给spender
的授信额度余额
- 通过
transferFrom
,spender
可以从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
只需要在账本中记录下spender
、owner
和授信金额,转账的时候据此验证、调账即可。
- Flow 的代币是分散存储在不同账户中的。要实现
approve
功能,我们需要让spender
能够确实地访问到owner
账户中的“保险箱”,而非在某个账本上调账。
这点差异使得在 Flow 上实现 ERC20 标准中的 approve 机制会更复杂。
Flow 是 Capability-based Access Control,可以理解成“认证不认人”,持有特定“令牌”就能做特定的事。因此我们需要设计一个能让
spender
访问并有限制地使用 owner
”保险箱“的”令牌“。再具体整理下,就有以下需求:owner
可以授权给多个spender
访问自己的资金。
spender
对资金的访问是受限于授信额度的,每个spender
可以有不同的授信额度。
- 授信额度可以超过
owner
现有资金总量。比如 Alice 拥有 40 FUSD,他可以授权 Bob 和 Carl 最多动用 30 FUSD,Dave 最多动用 50 FUSD。
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.Provider
和 FungibleToken.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
提供的字段来获取剩余额度和授权人,需要通过 AllowanceProvider
的 withdraw
方法来转账。但是他们不应该被允许调用 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 的存储大概是这样的情况:
至此我们实现了和 ERC20 代币标准中的 approve 机制类似的功能,而且在总限额之外还可以方便地添加授权过期时间、单笔最大限额等机制。虽然目前看起来没什么应用场景,不过……这不重要hhh。