嘿,我是雅各布-塔克。
在这篇文章中,我将带领大家了解我是如何在Cadence中实现BoredApeYachtClub NFT智能合约的,Cadence是Flow区块链 和NBA Top Shot 和Ticketmaster 等流行平台使用的智能合约语言。
在Solidity和Cadence中定义NFT的架构差异 Solidity是BAYC智能合约 所使用的智能合约语言。下面我将强调这两种语言之间的关键区别。
储存 在Solidity中,智能合约作为一个中央账本,存储资产本身。Solidity中的NFT合约看起来像这样:
正如你所看到的,NFT被存储为一个简单的映射,从`nft id`=>`owner address`。NFT本身不是一个资产,甚至不是一个对象,相反,它只是一个位于合同中的数字。
这种方法的问题是它非常脆弱。这是因为Solidity在处理变量方面的行为与任何通用编程语言一样。对于NFTs没有特殊的类型;相反,它们被表示为整数,这使开发者有责任确保不会出错。
由于Cadence的 "面向资源 "模型,NFT被表示为实际的对象,可以移动。资产是不允许 丢失的;如果开发者忘记将资源存储在某处或销毁它,Cadence将在编译时失败,不允许你运行该代码。
此外,Cadence为用户提供对其资产的真正所有权。Cadence让用户在自己的账户内存储NFT集合(和所有其他数据),而不是智能合约中存储数据的映射。这样一来,用户就可以完全控制他们的资产。Cadence中的NFT合约看起来是这样的:
访问控制 我们将遇到的另一个关键区别是我们如何将某些功能限制给某些人。
例如,我们如何确保薄荷函数只能由所有者调用?在Solidity中,你可以使用OpenZeppelin的Ownable 合约和函数修改器的组合来做到这一点,像这样:
然而,Cadence通过使用资源、访问修改器和能力,有内置的访问控制。我们可以定义一个名为Owner的资源(对象),并将其存储在所有者的账户存储中,允许他们借用该资源的引用,并在任何时候翻转销售状态,并消除对OpenZeppelin等第三方库的需要。它看起来像这样:
从Solidity到Cadence重写BAYC 在继续阅读之前,如果你想直接跳到代码,请看这个GitHub repo,其中包含合同和相关的Cadence事务/脚本。
实施NFT标准 重要的是要明白,BAYC合约 是一个NFT合约,因此在Solidity中实现了NFT标准(或 "ERC-721")合约。其工作方式是BAYC合约从ERC-721合约中继承了大量的默认行为,如造币、存储等。它还在上面添加了一些它自己的、BAYC特有的功能。
考虑到这一点,重写合约的第一步是在Cadence中实现NFT-标准 (或 "NonFungibleToken")合约接口的默认实现。而不是像在Solidity中那样简单地继承ERC-721合约,你必须在你的新合约中明确定义所有 "NonFungibleToken "所需的代码。
看看这个实现了 "NonFungibleToken "接口的最低限度的合同:
import NonFungibleToken from "./utility/NonFungibleToken.cdc"
pub contract BoredApeYachtClub: NonFungibleToken {
pub var totalSupply: UInt64
// events
pub event ContractInitialized()
pub event Withdraw(id: UInt64, from: Address?)
pub event Deposit(id: UInt64, to: Address?)
// paths
pub let CollectionStoragePath: StoragePath
pub let CollectionPublicPath: PublicPath
pub resource NFT: NonFungibleToken.INFT {
pub let id: UInt64
init () {
self.id = self.uuid
BoredApeYachtClub.totalSupply = BoredApeYachtClub.totalSupply + 1
}
}
pub resource interface CollectionPublic {
pub fun borrowApeNFT(id: UInt64): &NFT?
}
pub resource Collection: NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, CollectionPublic {
pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}
pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT {
let token <- self.ownedNFTs.remove(key: withdrawID)!
emit Withdraw(id: token.id, from: self.owner?.address)
return <-token
}
pub fun deposit(token: @NonFungibleToken.NFT) {
let token <- token as! @NFT
let id: UInt64 = token.id
self.ownedNFTs[id] <-! token
emit Deposit(id: id, to: self.owner?.address)
}
pub fun getIDs(): [UInt64] {
return self.ownedNFTs.keys
}
pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT {
return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)!
}
pub fun borrowApeNFT(id: UInt64): &NFT? {
let token = &self.ownedNFTs[id] as auth &NonFungibleToken.NFT?
return token as! &NFT?
}
init () {
self.ownedNFTs <- {}
}
destroy () {
destroy self.ownedNFTs
}
}
pub fun createEmptyCollection(): @Collection {
return <- create Collection()
}
init () {
self.totalSupply = 0
self.CollectionStoragePath = /storage/BAYCCollection
self.CollectionPublicPath = /public/BAYCCollection
}
}
这个合同的主要特点是什么?嗯,最重要的是,你可以看到我们有一个叫做NFT的 "资源 "。在Cadence中,资源代表我们可以移动和存储的对象。在这种情况下,它代表了一种名为NFT的资产,我们将在用户账户中铸币和存储。
接下来我们有一个名为 "集合 "的资源。这代表一个对象,它将在其中存储NFT资产。与其在用户的账户中存储单个的NFT,我们不如存储一个包含NFT本身的整个集合。这将使我们更容易发现所有的BAYC NFTs,因为它们将生活在一个地方。
收藏资源本身有几个功能:存款和取款(这是不言自明的),getIDs(读取某人拥有的NFT的ID),以及borrowApeNFT(发现一个NFT的基本元数据)。
你在这个合同中看到的 "路径",即CollectionStoragePath和CollectionPublicPath,描述了一旦我们存储了Collection资源,你将能在账户中找到它的位置。
其他一切,如totalSupply和少数事件,都必须包括在内,以遵守NonFungibleToken接口。这一行定义在NFT资源的init函数中,将确保每次创建NFT时totalSupply都会被递增:
BoredApeYachtClub .totalSupply =BoredApeYachtClub .totalSupply + 1
要了解更多关于在Cadence中实施正式的NFT合同,请查看 Emerald学院的初级Cadence 课程,该课程正是这样做的。
造币功能 现在我们已经涵盖了实施NFT标准,让我们加入合同的其余部分。
下一个最重要的步骤是重写造币。这就是Solidity合约中的造币功能:
function mintApe(uint numberOfTokens) public payable {
require (saleIsActive, "Sale must be active to mint Ape" );
require (numberOfTokens <= maxApePurchase, "Can only mint 20 tokens at a time" );
require (totalSupply().add(numberOfTokens) <= MAX_APES, "Purchase would exceed max supply of Apes" );
require (apePrice.mul(numberOfTokens) <= msg.value, "Ether value sent is not correct" );
for (uint i = 0; i < numberOfTokens; i++) {
uint mintIndex = totalSupply();
if (totalSupply() < MAX_APES) {
_safeMint(msg.sender, mintIndex);
}
}
}
铸币功能的第一步是确保一些检查是真的:
检查以确保销售是活跃的 不允许用户铸造超过允许的最大数额的硬币 确保总供应量不会超过允许的最大猿数 确保传递进来的以太数量等于被购买的猿猴的(价格*数量)。 然后,它运行一个循环,用发件人的地址和一个代表每个NFT的增量id调用mint函数。
为了在Cadence中实现这一点,我们需要定义一些变量来运行我们的检查,然后定义一个薄荷函数:
// custom variables
pub let apePrice: UFix64
pub let maxApePurchase: UInt64
pub var maxApes: UInt64
pub var saleIsActive: Bool
pub fun mintApe(
numberOfTokens: UInt64,
payment: @FlowToken.Vault,
recipientVault: &Collection{NonFungibleToken.Receiver}
) {
pre {
BoredApeYachtClub.saleIsActive: "Sale must be active to mint Ape"
numberOfTokens <= BoredApeYachtClub.maxApePurchase: "Can only mint 20 tokens at a time"
BoredApeYachtClub.totalSupply + numberOfTokens <= BoredApeYachtClub.maxApes: "Purchase would exceed max supply of Apes"
BoredApeYachtClub.apePrice * UFix64(numberOfTokens) == payment.balance: "$FLOW value sent is not correct"
}
var i: UInt64 = 0
while i < numberOfTokens {
recipientVault.deposit(token: <- create NFT())
i = i + 1
}
// deposit the payment to the contract owner
let ownerVault = BoredApeYachtClub.account.getCapability(/public/flowTokenReceiver)
.borrow<&FlowToken.Vault{FungibleToken.Receiver}>()
?? panic("Could not get the Flow Token Vault from the Owner of this contract.")
ownerVault.deposit(from: <- payment)
}
init (maxNftSupply: UInt64) {
self.apePrice = 0.08 // $FLOW
self.maxApePurchase = 20
self.maxApes = maxNftSupply
self.saleIsActive = false
}
这里有几个区别。首先,我们使用一种叫做 "预设条件 "的东西,在函数开始之前就断言条件为真。这是Cadence的一个原生功能,以确保正确的行为。
接下来,我们运行一个循环,使用传入的对买方的BAYC集合的引用,并将猿猴存入那里。因此,我们不是简单地将NFT的id映射到所有者的地址,而是传入一个对买方NFT集合的引用,并真正地将资产移入该集合。
另一个关键区别是,在Solidity中,以太币(或付款)被传递到合同中。在Cadence中,我们将付款作为另一种资源传入,并将该付款转移到所有者的金库中。
注意我们必须在init函数中给这些变量默认值。我把它们设置为与Solidity中的默认变量完全相同:
uint256 public constant apePrice = 80000000000000000;//0.08 ETH
uint public constant maxApePurchase = 20;
uint256 public MAX_APES;//传递到构造函数中
bool public saleIsActive = false;
元数据 Solidity和Cadence之间最大的区别是我们如何处理元数据。在Solidity中,元数据被存储在链外,如IPFS,一个去中心化的存储网络。然后,DApps使用一个 "uri "来获取基于其id的不同NFTs元数据。 在Cadence中,除了图像或其他重度元数据,大多数元数据都存储在链上。这对于可组合性和其他事情来说是非常好的,比如直接在我们的合同中验证元数据,这在Solidity中是做不到的。唯一的问题是在造币时隐藏我们的元数据。如果区块链是公开的,而元数据在链上,我们如何防止用户在铸币时访问NFTs的数据?
好吧,事实证明答案很简单:在用户铸币后上传元数据!这正是Solidity中的做法,在用户铸币后改变 "URI"!这正是Solidity中的做法,即在用户铸币后改变 "URI"。
要做到这一点,我们必须定义两件事:
一个业主资源,将允许业主上传元数据和翻转销售状态。这个资源将被存储在业主的账户中,以访问这些功能。 一个模板结构,所有者可以在他们喜欢的时候使用 fulfillMetadata 函数来上传元数据。我们将使用NFT的序列(从0开始的增量ID)来映射到模板数据:
pub struct Template {
// let's assume this is a `cid` for IPFS
pub let image: String
pub let attributes: {String : String }
init (image: String , attributes: {String : String }) {
self .image = image
self .attributes = attributes
}
}
// maps serial -> Template
access (self ) let templates: {UInt64 : Template}
pub resource Owner {
pub fun flipSaleState() {
BoredApeYachtClub.saleIsActive = !BoredApeYachtClub.saleIsActive
}
pub fun fulfillMetadata(templateId: UInt64 , image: String , attributes: {String : String }) {
BoredApeYachtClub.templates[templateId] = Template(image: image, attributes: attributes)
}
}
然后我们创建一个所有者资源,并在合同的初始功能期间将其存储在合同部署者的账户中:
self .account .save (<- create Owner (), to: /storage/BAYCOwner )
这将允许所有者在任何时候从他们的账户中借用所有者资源,履行元数据或翻转销售状态。同样,我们不需要任何像OpenZeppelin这样的第三方库来管理对我们所有者功能的访问控制。
总结 今天讨论的NFT标准是非常不同的。Solidity通过将一个整数映射到一个地址来表示所有权,而Cadence采取了一种更直观的方法,即通过将不可拆卸的对象存入用户存储和控制的集合中,采用面向资源的模型。
此外,Cadence的安全机制通过其内置的访问控制和强类型化的语法,使得开发人员很难搞砸,这在处理像Bored Apes这样有价值的资产时至关重要。
我强烈鼓励你尝试Cadence,甚至把你自己喜欢的一些Solidity合约映射到Cadence中。如果你有兴趣从0开始学习这门语言,请查看Emerald学院的Cadence初级 课程。
要访问最终的智能合约,请到这里 。