Lazy minting

本文是一篇译文,原文在此
在区块链主网络上铸造 NFT 通常需要消耗一些资金,在链上写入数据,你需要为算力和存储消耗支付费用。这对于 NFT 创建者而言,实际上是一个阻力,特别是对那些不想投入太多资金的创作者,他们甚至不知道这些 NFT 是否可以被销售出去。

在技术层面,实际上可以实现推迟 NFT 的铸造花销,直到第一次交易完成。铸造和 NFT 转移操作在同一个交易中完成,因此,NFT。取而代之的,创建 NFT 的初始记录(铸币),算在买家的 gas 费用里。

在购买时铸造的方式,就是 lazy minting。这种方式被类似 OpenSea 这样的交易所采用。

本指引将向你展示在一些 OpenZeppelin 库的帮助下,在以太坊上进行 lazy minting 的过程。

通过该指引,我们将创建一个示例项目,仓库在此。如果你想深入了解,可以 clone 仓库按照你的想法编辑。

1
2
3
git clone https://github.com/ipfs-shipyard/nft-school-examples
cd nft-school-examples/lazy-minting
code . # or whichever editor you prefer

1. 工作原理

lazy minting 的基本前提,就是取代直接通过调用合约函数去创建 NFT 的方式,NFT 铸造者通过账户私钥对数据进行签名。

签名的数据,扮演着凭证或者票据的角色,据此可以兑换 NFT。凭证中,包含了可以铸造 NFT 的所有信息,当然,它也可以包含一些不记录在区块链上的可选数据,比如我们稍后将谈论的价格。签名证明了 NFT 创建者授权创建凭证中指定的 NFT。

当买家希望购买 NFT 时,他们可以调用 redeem 函数,以兑换一个经过签名的凭证。如果签名是合法的,并且属于 NFT 铸造的授权账户,新的 token 将会被基于凭证创建,并且转移给买家。

在以下的示例中,我们使用 Solidity struct 代表一个凭证:

1
2
3
4
5
6
struct NFTVoucher {
uint256 tokenId;
uint256 minPrice;
string uri;
bytes signature;
}

凭证包含的两个信息将会被记录在区块链上:tokenId 以及 uriminPrice 将不会被记录,但是它会被用于 redeem 函数中,以允许创建者设置购买价格。如果 minPrice 大于零,买家在调用 redeem 函数时,至少应支付这么多的 Ether 给卖家。

signature 即 NFT 创建者的签名信息,我们将在下一部分描述。

提示
在凭证中设置价格通常不是必须的,但是通常你可能也需要设置一些条件,不然,任何人都可以通过凭证获取你的 NFT,而只需要支付 gas 费用。
举例来说,如果你正在空投一些 NFT 到指定的已知账户,你的凭证中就需要包含 address recipient 而不是 minPrice,并且,在 redeem 函数中检查确保 msg.sender == voucher.recipient


2. 创建一个签名的凭证

使用签名进行授权的方式保证交易的唯一性实际上是困难的,因为一些狡猾的第三方可能会将某个上下文中的签名用于别处。比如,他们可能会将一个在 Ropsten 测试网络中的 NFT 签名用于主网络中。除非,签名信息中包含饿了这些环境信息,这种 “重放攻击” 在平台上是非常常见且很难防范的。

为了解决这类问题,并提供更好的信息签名体验,以太坊社区开发了EIP-712 协议,这是一种签名标准。在 EIP-712 协议中,签名被限制在一个指定的合约运行网络中。它们也包含类型信息,因此,在类似于 MetaMask 这样的工具中,可以向用户展现关于被签名数据的更多信息,而不仅仅是一串十六进制字符。

在我们的示例中,将使用 JavaScript 类 LazyMinter 构造符合 EIP-712 的凭证。由于签名被限定在一个指定的合约实例中,你还需要提供被部署的合约的地址,并使用 ethers.js 中的 Singer 获取 NFT 创作者的私钥:

1
const lazyminter = new LazyMinter({ myDeployedContract.address, signerForMinterAccount })

完整的 LazyMinter 在此

下面,createVoucher 方法将创建一个签名的 NFT 凭证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async createVoucher(tokenId, uri, minPrice = 0) {
const voucher = { tokenId, uri, minPrice }
const domain = await this._signingDomain()
const types = {
NFTVoucher: [
{name: "tokenId", type: "uint256"},
{name: "minPrice", type: "uint256"},
{name: "uri", type: "string"},
]
}
const signature = await this.signer._signTypedData(domain, types, voucher)
return {
...voucher,
signature,
}
}

首先,我们准备了一个未签名的 voucher 对象,以及一个等待签名的域名信息。types 对象包含了 NFTVoucher 中每个字段的类型信息。
为了创建签名,我们调用了 Signer 对象的 _signTypedData 方法,并传递了域名,定义的类型以及未签名的凭证数据。
最后,我们返回整个完整的凭证对象。

注意
_signTypedData 方法在 ethers.js 的新版本中可能被重命名为了 sighTypedData。可以在 ethers doc 中获取更多信息。


3. 链上凭证兑换

为了使 lazy minting 工作,我们需要在同一个交易中同时实现 NFT 的铸造和转移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function redeem(address redeemer, NFTVoucher calldata voucher) public payable returns (uint256) {
// make sure signature is valid and get the address of the signer
address signer = _verify(voucher);

// make sure that the signer is authorized to mint NFTs
require(hasRole(MINTER_ROLE, signer), "Signature invalid or unauthorized");

// make sure that the redeemer is paying enough to cover the buyer's cost
require(msg.value >= voucher.minPrice, "Insufficient funds to redeem");

// first assign the token to the signer, to establish provenance on-chain
_mint(signer, voucher.tokenId);
_setTokenURI(voucher.tokenId, voucher.uri);

// transfer the token to the redeemer
_transfer(signer, redeemer, voucher.tokenId);

// record payment to signer's withdrawal balance
pendingWithdrawals[signer] += msg.value;

return voucher.tokenId;
}

首先,调用 _verify 函数,该函数将通过返回签名的账户信息,当然,如果签名不合法,则交易失败。

一旦拥有了签名者的地址,就可以通过 AccessControl 中的 hasRole 函数检测他们是否被授权创建 NFT。

接下来,还需要确保买家所支付的金额应可以覆盖 minPrice。如果满足要求,则基于凭证创建一个新的 token,并将其转移给 redmeemer 账户。

最后,在 pendingWithdrawls 中记录要支付给 NFT 创建者的 ether。

EIP-712合约源码 中,可以获取更多关于签名验证过程的信息。


4. 结论

Lazy minting 是一种强力的技术,可以避免在发布 NFT 被收取“前置”费用。

就实现而言,虽然我们在这里展现了其核心技术,实际上在产品环境下,还需要更多细节。比如,你可能需要 NFT 铸造者自己发布一个签名的凭证,也可能你需要后端系统持续对所有未铸造的 NFT 进行跟踪,直到它被兑换。

© 2024 YueGS