Flow Community Rewards are here. Earn points for engaging in the ecosystem, spend points on prizes. Learn more.
开发者生态
2023年6月8日
特邀文章:在Cadence中实施Bored Ape Yacht Club智能合约
雅各布-塔克
特邀文章:在Cadence中实施Bored Ape Yacht Club智能合约

嘿,我是雅各布-塔克。 

在这篇文章中,我将带领大家了解我是如何在Cadence中实现BoredApeYachtClubNFT智能合约的,Cadence是Flow区块链NBA Top ShotTicketmaster等流行平台使用的智能合约语言。

在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);
        }
    }
}

铸币功能的第一步是确保一些检查是真的:

  1. 检查以确保销售是活跃的
  2. 不允许用户铸造超过允许的最大数额的硬币
  3. 确保总供应量不会超过允许的最大猿数
  4. 确保传递进来的以太数量等于被购买的猿猴的(价格*数量)。

然后,它运行一个循环,用发件人的地址和一个代表每个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中的默认变量完全相同:


uint256public constant apePrice = 80000000000000000;//0.08 ETH
uintpublic constant maxApePurchase = 20;
uint256public MAX_APES;//传递到构造函数中
boolpublic saleIsActive = false;

元数据

Solidity和Cadence之间最大的区别是我们如何处理元数据。在Solidity中,元数据被存储在链外,如IPFS,一个去中心化的存储网络。然后,DApps使用一个 "uri "来获取基于其id的不同NFTs元数据。

在Cadence中,除了图像或其他重度元数据,大多数元数据都存储在链上。这对于可组合性和其他事情来说是非常好的,比如直接在我们的合同中验证元数据,这在Solidity中是做不到的。唯一的问题是在造币时隐藏我们的元数据。如果区块链是公开的,而元数据在链上,我们如何防止用户在铸币时访问NFTs的数据?

好吧,事实证明答案很简单:在用户铸币后上传元数据!这正是Solidity中的做法,在用户铸币后改变 "URI"!这正是Solidity中的做法,即在用户铸币后改变 "URI"。

要做到这一点,我们必须定义两件事:

  1. 一个业主资源,将允许业主上传元数据和翻转销售状态。这个资源将被存储在业主的账户中,以访问这些功能。
  2. 一个模板结构,所有者可以在他们喜欢的时候使用 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初级课程。

要访问最终的智能合约,请到这里