Table of contents
No headings in the article.
你可以在这里找到本文的所有代码。
在 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。