大数据|技术分享|新代币标准的讨论

Stable Cadence 将是 Cadence 编程语言发展的一个重要里程碑,此后,Cadence 将不会再有任何重大的变化。因此,在此里程碑之前,社区会对 Cadence 做出一些修改。具体修改提案请见论坛连接。我们希望看到 Cadence 编程语言(以及使用它的应用)可以有效使用很多年,所以从长远来看,将语言设置为尽可能安全与易于使用是非常重要的。

我们希望看到的最大变化之一是:重构和简化 FT 和 NFT 标准,目的是让它们更简洁、更强大。

当前 Flow 的 FT 和 NFT 标准(以下简称“token标准”)是在 2019 年中期设计的,那时的 Cadence本身仍在设计发展阶段。虽然当前的标准满足基本需求,但仍有很多不足之处(我们认为可以改进)。在之后的三年里,我们学到了很多东西,并希望能做出根本性的改进,以及其他一些较小的提升。

首先,我们将讨论当前 token 标准的缺点,然后讨论核心的修改提案,最后是其他较小的修改提案。

这些改动对于当前 Flow 上所有 FT 和 NFT 来说都是破坏性的,但我们将为所有项目提供清晰且简单的升级和迁移路径,并在迁移过程中提供帮助。为了简化流程,我们的计划是为项目提供一个窗口期,以便在旧版本的 Cadence 淘汰之前升级合约和下游依赖。我们还将提出一项功能,允许开发人员预先上传升级后的合同。这些升级后的合同将在下一个更新 Cadence 版本的 spork 中生效。


当前的token标准
在阅读修改提案之前,请确保您熟悉 Flow 的当前代币标准:

  • Flow Fungible Token Standard:
    https://github.com/onflow/flow-ft
  • Flow NFT Standard:
    https://github.com/onflow/flow-nft

当前的 token 标准使用合约 interface。它们的设计方式要求每个具体合约都提供一个 Vault 或 NFT 类型。这意味着任何需要多个 token 的项目都必须部署多个合约。对于简单的 token 模型,这种复杂性是没有意义的。再者,由于合约类型限制,项目无法为其 NFT 提供自定义名称。
// 举例 // 实现该标准的合约必须定义一个 Vault // 实现了 FungibleToken.Vault 并且不能再新添加 pub contract interface FungibleToken { // 只能定义一个兼容标准的Vault pub resource Vault: Provider, Receiver, Balance { pub var balance: UFix64 pub fun withdraw(amount: UFix64): @Vault { /* … */ } pub fun deposit(from: @Vault) { /* … */ } } }

此外,实现 NFT 标准更复杂,因为每个 NFT 实现者还需要实现自己的 Collection 资源。(示例 NFT 合约的 Collection 实现是 NFT 实现本身的 5 倍!)
// NonFungibleToken举例 pub contract interface NonFungibleToken { // 项目只能定义一种符合标准的token类型 pub resource NFT { pub let id: UInt64 } // 项目还必须实现自定义集合类型, // 对于特定的新 NFT,这不应该是必要的 // 一个集合应该可以是异构的 pub resource Collection: Provider, Receiver, CollectionPublic { pub var ownedNFTs: @{UInt64: NFT} pub fun withdraw(withdrawID: UInt64): @NFT pub fun deposit(token: @NFT) pub fun getIDs(): [UInt64] pub fun borrowNFT(id: UInt64): &NFT }

使用合约 interface 的动机是希望 token 接口中的 Receiver 和 Provider 类型是静态可检查的,例如 FlowToken.Provider 将 FlowToken.Vault 作为其返回类型,而不是 FungibleToken.Vault。由于编程语言类型系统中的 vagaries of “variance”,使用合约接口尤其是类型需求的价值并未被有效实现,现在我们却被其代价所困。

Nested Type Requirements(嵌套类型)是一个相当高级的概念。就像接口的继承需要保持类型一致来确保与接口的字段或方法一样,嵌套类型也需要保证嵌套类型的一致性。这在其他编程语言中并不常见,因此 Cadence 的新手需要学习和理解此功能,才能了解如何实现简单的 Fungible Token。

FT合约需要提供创建空 Vault 的接口方法,因为资源只能在定义它们的合约内创建/构造。因此FT标准要求FT合约提供/实现函数 createEmptyVault。这使得学习者很难推断理解FT的代码,因为创建Vault相关的代码一部分在 Vault 资源(初始化程序)中实现,一部分在合约中实现(createEmptyVault 函数)。这也是为什么当前需要在合约内去定义Vault类型,而不能单独定义的原因。


可能的解决方案
有几种方式可以改进当前的标准。我们提出了一种我们认为可以解决上述问题的提案。但是,我们并不完全致力于具体的提议修改,并且强烈鼓励改进此建议或是替代解决方案。

该提案试图通过将 token 的大部分功能实现封装在资源和或资源接口中来解决上述的一些问题,而不是作为合约中的嵌套类型。这将会简化标准,从而可以为每个合约定义多个 Vault 和 NFT,使用特定名称而不是通用的 “NFT”。

我们还建议在 Vault 内定义事件类型。这也需要更新 Cadence 以允许在资源中定义类型,这将是一个单独的 FLIP。

通过将静态函数作为语言特性引入(如下面建议的标准),创建空 Vault 的函数可以添加到 Vault 资源本身,而不是在合约中定义。这允许将与Vault创建相关的代码保存在同一个地方。Cadence 中添加的静态函数也将会在 FLIP 中。

该提案还包括过去曾提出的一些其他修改,比如:
  • 引入 transfer function
  • 使用函数的默认实现(尤其与 NFT metadata相关)
  • 不要求 NFT ID 字段是特定值或类型
  • 使用可重用的 NFT collection,而不是要求每个项目为特定的 NFT 实现collection。


新Token标准
以下是我们对token标准的修改示例,以实现上述的提议。

■ 新Fungible Token接口
当前的合约接口会被大致替换成:
// A shared contract, deployed in a well-known location. pub contract FungibleToken { pub resource interface Provider { pub fun withdraw(amount: UFix64): @{Vault} { post { result.balance == amount } }pub fun transfer(amount: UFix64, recipient: &AnyResource{Receiver}) }pub resource interface Receiver { pub fun deposit(from: @{Vault}) }pub resource interface Balance { pub fun getBalance(): UFix64 }pub resource interface Vault: Provider, Receiver { // 该提案只提供一个函数来获取Valut的余额, // 因为某些项目可能想要计算余额而不是将其存储为单个字段 pub fun getBalance(): UFix64pub fun withdraw(amount: UFix64): @{Vault} { pre { self.getBalance() >= amount } post { self.getBalance() == before(self.getBalance()) - amount } }pub fun deposit(from: @{Vault}) { post { self.getBalance() == before(self.getBalance()) + before(from.getBalance()) } }pub fun transfer(amount: UFix64, recipient: &AnyResource{Receiver}) {} } }

■ 新 Fungible Token 样例实现
// A specific contract, deployed into a user account import FungibleToken from 0x02 import StandardMetadata from 0x04pub contract TokenExample { pub resource ExampleVault: FungibleToken.Vault {// events 可以定义在合约甚至资源内部 pub event TokensWithdrawn(amount: UFix64, from: Address?) pub event TokensDeposited(amount: UFix64, to: Address?)access(self) var balance: UFix64init(balance: UFix64) { self.balance = balance }// createEmpty()资源创建函数将被放在资源内部 pub static fun createEmpty(): @ExampleVault {} return <-create ExampleVault(balance: 0.0) }pub fun withdraw(amount: UFix64): @ExampleVault { self.balance = self.balance - amount return <-create Vault(balance: amount) }pub fun deposit(from: @ExampleVault) { self.balance = self.balance + from.balance from.balance = 0.0 destroy from }pub fun transfer(amount: UFix64, recipient: &AnyResource{Receiver}) {let tokens <- self.withdraw(amount: amount)recipient.deposit(from: <-tokens)}pub fun getBalance(): UFix64 { return self.balance }}

■ 新NonFungibleToken Interfaces
// A shared contract, deployed in a well-known location. pub contract NonFungibleToken {// 我们将使用一个通用的NFT collection,所以会有标准的paths定义在合约中 pub let collectionStoragePath: StoragePath pub let collectionPublicPath: PublicPathpub event Withdraw(type: Type, id: UFix64, from: Address?) pub event Deposit(type: Type, id: UFix64, to: Address?) pub event Transfer(type: Type, id: UFix64, from: Address?, to: Address)pub resource interface NFT { // We also don’t want to restrict how an NFT defines its ID // They can store it, compute it, or use the UUID pub fun getID(): UInt64// Two functions for the NFT Metadata Standard pub fun getViews() : [Type] pub fun resolveView(_ view:Type): AnyStruct? } // 这不是provider的最终提案。 // 理想情况下,我们可以有一个provider接口只针对特定NFT类型的ID数组有效 // 以便所有者可以保护NFT不被无意访问 pub resource interface Provider { pub fun withdraw(type: Type, withdrawID: UInt64): @{NFT} { post { result.getID() == withdrawID } }pub fun transfer(type: Type, withdrawID: UInt64, recipient: &AnyResource{Receiver})}pub resource interface Receiver { pub fun deposit(token: @{NFT}) }pub resource interface CollectionPublic: Receiver { // A user will likely have a way to restrict which NFTs // they want to receive so they can’t get spammed pub fun deposit(token: @AnyResource{NFT}) pub fun getIDs(): {Type: [UInt64]} pub fun borrowNFT(id: UInt64): &{NFT} }// A full implementation of NFT collection that // *is not* specific to one NFT type // 用户可以只使用collection的一个实例来存储他们所有的NFT // 而不必为每个项目使用一个特定的collection pub resource NFTCollection: CollectionPublic, Receiver, Provider { access(self) let ownedNFTs: {Type: {UInt64: @{NFT}}pub fun deposit(token: @AnyResource{NFT}) { pre { // 可以允许用户指定他们愿意接收的NFT类型 // 这样他们就不会收到垃圾NFT }}pub fun withdraw(type: Type, withdrawID: UInt64): @{NFT} {}pub fun transfer(type: Type, withdrawID: UInt64, recipient: &AnyResource{Receiver}) {}// Also could potentially include batch withdraw, batch deposit, // and batch transfer pub fun getIDs(): {Type: [UInt64] {}pub fun borrowNFT(id: UInt64): &{NFT} { }} }

■ 新Non-Fungible Token 样例实现
// A specific contract, deployed into a user account import NonFungibleToken from 0x03 import StandardMetadata from 0x04pub contract ExampleTokenImplementationpub resource ExampleNFT: NonFungibleToken.NFT { // Could also use uuid as the unique identifier access(self) let name: Stringpub fun getID(): UInt64 { // this could also use a id field if needed return self.uuid }// From the NFT Metadata Standard pub fun getViews(): [Type] { return [StandardMetadata.SimpleName] }// From the NFT Metadata Standard pub fun resolveView(_ view:Type): AnyStruct? { if view == StandardMetadata.SimpleName { return StandardMetadata.SimpleName(name: self.name) } return nil }init(initID: UInt64) { self.id = initID self.name = "Token name" } } }

样例中值得注意的一些点:
  • 项目不必担心添加样板代码来实现 nft collection 资源了。由于新标准不再使用合约接口并使NFT更加通用,因此用户可以将单个 collection 用于所有 NFT。
  • 我们不再为 FungibleToken 或 NonFungibleToken 使用合约级别的接口,因此合约中将不再包含一些标准部分,如 totalSupply 字段和标准events。totalSupply 字段可能由仅指定 totalSupply 字段的简单合约接口处理。替换标准events更加困难,因为我们在 Cadence 中决定要避免接口中的类型要求。为了解决这个问题,我们建议项目在合约中定义自己的事件类型。仍然会有事件名称和参数的标准,但不再由合约接口强制执行。这绝对是一个仍有待商榷的话题。
  • 使用资源接口而不是合约接口的另一个好处是,一个项目可以在同一个合约中定义任意数量的可替代代币和/或不可替代代币,从而为开发人员提供更多的灵活性和效率。
  • 我们为NFT collection包含了标准storage和public path。由于每个用户将使用相同的集合,因此项目不需要有自己的路径。
  • 我们还提供了一种transfer方法,使转移更加直接,这是许多社区成员要求的功能。
  • 这里有一份2021年对现有NFT标准改进的讨论(连接)。上述提案解决了其中许多改进,但不是全部。由于标准重构的性质,其中一些不再相关,而其中一些需要对 Cadence 进行额外的更改所以不在当前升级的范围里。一些超出范围的改进包括:
    • 枚举帐户中的资源
    • 使用泛型
    • 要求在特定函数中发出某个事件
    • NFT nonce
我们意识到这些并不是微不足道的改动,但我们相信通过与社区合作,我们可以为每个人设计一条合约升级路径,最大限度地减少升级的难度。如果这个初始提案得到了良好的反馈,我们将继续定义详细的升级路径和迁移计划。在承诺升级之前,这也需要得到社区的批准。Flow 团队确信,从长远来看,每个人的努力都是值得的,并会产生更清晰、更安全、更可组合的代币标准。
【大数据|技术分享|新代币标准的讨论】

    推荐阅读