LazyMinting是如何实现的?

业务场景

我们lazyMinting使用场景主要是针对nft的竞拍业务来做的。 creator可以在上架nft时,不需要真正的mint nft,而是等有合适的bidder来购买后,才去mint nft。 这样可以减少nft mint的成本,从而鼓励creator来创建更多的nft;

LazyMinting的实现思路:

核心思路是通过EIP-712的TypeDataSign来验证签名:

用户在链下用privateKey进行签名,链上通过合约验证确认签名消息的合法性以及业务逻辑的合理性。 一但验证通过,立刻执行合约里的方法,mint nft,transfer给真正的买家。

我们项目里具体实现方案是什么?

  1. 定义了auction合约, 记录 竞拍信息ActionData、NFT 购买者凭证信息Voucher
solidity 复制代码
{
  // 竞拍信息
	struct AuctionDetails {
      address seller; // 卖家地址
      string uri; // nft信息
      uint256 startPrice;
      uint256 duration;
      // 最终竞拍人
      address bidder;
      uint256 bidPrice;
      bool isActive;
      bool isEnd;
      uint256 tokenId;
  }
  // 购买者的凭证
	struct NFTVoucher {
        string auctionId;
        uint256 bidPrice;
        bytes signature;
  }
  
  // 记录action信息
	 mapping(string => AuctionDetails))  public auctionIdToAuction;

  // 记录voucher
	mapping(string => NFTVoucher)) public auctionIdToVoucher;
}
  1. 当creator上架一个nft action的时候,传入auctionId,minPrice、duration这些信息,保存在map里;注意此时bidder/tokenId这些信息都是不存在的;

  2. 当用户出价购买时,组装数据,通过_signTypedData方法,调用钱包签名,生成一个Voucher,并且记录到链下;

    ts 复制代码
      /**
       * Creates a new NFTVoucher object and signs it using this LazyMinter's signing key.
    
       * @returns {NFTVoucher}
       */
      async createVoucher(auctionId, bidPrice) {
        const voucher = { auctionId, bidPrice };
        const domain = await this._signingDomain();
    
        const types = {
          NFTVoucher: [
            { name: "auctionId", type: "string" },
            { name: "bidPrice", type: "uint256" },
          ],
        };
        const signature = await this.signer._signTypedData(domain, types, voucher);
    
        // const recoveredAddress = ethers.utils.verifyTypedData(domain, types, voucher, signature);
        // const expectedSignerAddress = this.signer.address;
        // assert(recoveredAddress === expectedSignerAddress); 单纯验证下代码逻辑
        // console.log("recoveredAddress = " + recoveredAddress);
    
        return {
          ...voucher,
          signature,
        };
      }
  3. 当竞拍结束,或者creator提前结束时,选择一个出价高的bidder作为获胜者。 构造一个Winner凭证: WinnerVoucher

    ts 复制代码
    {
      auctionId: "auction123",
      bidder: "0x中标者地址",
      bidPrice: 1000000000000000000,
      nftAddress: "0xNFT合约地址",
      signature: "卖家的签名"
    }
  4. 中标的bidder去链上进行redeem,提交两个voucher: 自身出价的voucher,卖家选择的voucher。

  5. 合约会验证的两个voucher的合法性,检查数据一致等等

    1. address和Voucher里签名的用户是否是同一个,并且验证调用redeem的msg.value > Voucher.bidderPrice
    solidity 复制代码
    // 新增结构体
    struct WinnerVoucher {
        string auctionId;
        address bidder;
        uint256 bidPrice;
        bytes signature;
    }
    
    // 计算哈希
    function _hashWinner(WinnerVoucher calldata voucher)
        internal
        view
        returns (bytes32)
    {
        bytes32 WINNER_VOUCHER_TYPE_HASH = keccak256(
            "WinnerVoucher(string auctionId,address bidder,uint256 bidPrice)"
        );
        bytes32 structHash = keccak256(
            abi.encode(
                WINNER_VOUCHER_TYPE_HASH,
                keccak256(bytes(voucher.auctionId)),
                voucher.bidder,
                voucher.bidPrice
            )
        );
        return _hashTypedDataV4(structHash);
    }
    
    // 新增验证函数
    function _verifyWinner(WinnerVoucher calldata voucher)
        internal
        view
        returns (address)
    {
        bytes32 digest = _hashWinner(voucher);
        return ECDSA.recover(digest, voucher.signature);
    }
    
    // 修改后的 redeem 函数
    function redeemWithWinnerVoucher(
        address _nftAddress,
        string memory _auctionId,
        NFTVoucher calldata nftVoucher,
        WinnerVoucher calldata winnerVoucher
    ) public payable returns (uint256) {
        // 1. 验证 NFT voucher 是中标者签的
        address nftSigner = _verify(nftVoucher);
        require(msg.sender == nftSigner, "Invalid NFT signature");
        
        // 2. 验证 winner voucher 是卖家签的
        AuctionDetails storage auction = auctionIdToAuction[_auctionId];
        address winnerSigner = _verifyWinner(winnerVoucher);
        require(winnerSigner == auction.seller, "Invalid seller signature");
        
        // 3. 验证 winner voucher 指定的 bidder 就是调用者
        require(msg.sender == winnerVoucher.bidder, "NOT the winner");
        
        // 4. 验证出价金额
        require(msg.value >= winnerVoucher.bidPrice, "Insufficient funds");
        
        // 5. mint 并转移 NFT
        uint256 tokenId = nftContract.mint(_auctionId, auction.uri);
        nftContract.safeTransferFrom(address(this), msg.sender, tokenId);
        
        // 5. 将付款转账给卖家,这里假设是用 native token付款的
        (bool success, ) = auction.seller.call{value: winnerVoucher.bidPrice}("");
        require(success);
        
        // 7. 更新拍卖状态
        auction.bidder = winnerVoucher.bidder;
        auction.bidPrice = winnerVoucher.bidPrice;
        auction.isEnd = true;
        auction.tokenId = tokenId;
        
        return tokenId;
    }

repo: github.com/6gunner/sca...

相关推荐
Rockbean10 小时前
3分钟Solidity: 11.11 抢先交易Front Running
web3·智能合约·solidity
DICOM医学影像1 天前
3. go语言从零实现以太坊客户端 - 查询合约中账户余额
golang·区块链·智能合约·solidity·以太坊·web3.0
Rockbean1 天前
3分钟Solidity: 11.10 蜜罐
web3·智能合约·solidity
DICOM医学影像2 天前
1. go语言从零实现以太坊客户端-JSON-RPC
golang·区块链·solidity·以太坊·web3.0·json-rpc·erc20
Rockbean5 天前
3分钟Solidity: 11.1 重入攻击
web3·智能合约·solidity
Rockbean5 天前
3分钟Solidity: 10.6 时间锁定
web3·智能合约·solidity
Rockbean8 天前
3分钟Solidity: 9.8 单向支付通道
web3·智能合约·solidity
DICOM医学影像9 天前
16. web3.js结合Metemask调用合约方法
区块链·智能合约·solidity·以太坊·metamask·web3.0
Rockbean12 天前
3分钟Solidity: 9.3 通过 Create2 预计算合约地址
web3·智能合约·solidity