Lazy Minting Part1

2 분 소요

Lazy Minting(Part1)


정의

  • 창작자(아티스트)가 만든 NFT를 누군가 NFT를 구매할 때까지 블록체인 네트워크 상에 등록하지 않습니다.
  • Lazy Minting으로 발행된 NFT는 구매자가 NFT를 구매하는 시점에 창작자(아티스트)가 NFT를 발행하는 데 발생하는 가스 비용과 NFT 비용을 동시에 지불합니다.
  • NFT 발행에 드는 가스 비용을 구매자가 지불하면서 NFT는 블록체인 네트워크 상에 등록됩니다.

과정(순서)

  • 창작자(아티스트)는 smart contract를 이용하여 NFT를 발행하고 동시에 판매등록을 합니다.
  • 창작자(아티스트)는 Lazy Minting의 프로세스 승인을 위해 지갑, NFT 세부 정보를 자세히 설명하는 개인 서명을 제공합니다.
  • 구매자는 NFT를 구매하고 창작자(아티스트)가 NFT 발행에 필요한 가스 비용가 NFT의 자체 비용을 동시에 지불하고 NFT를 오프체인에서 온체인으로 등록합니다.

공유(플랫폼 간)

  • 대표적인 Lazy Minting 플랫폼인 Rarible, Opensea에서 Lazy Minting으로 각각 NFT를 발행했을때 서로의 Platform에서는 판매 및 List 할 수 없습니다.
  • Lazy Minting은 서명 및 NFT의 정보를 구매자가 구매하기 전까지는 온체인이 아닌 오프체인(DB)상에 존재하기 때문에 다른 Platform에서는 판매 및 List 할 수 없습니다.

Smart Contract

struct NFTVoucher {
  uint256 tokenId;
  uint256 minPrice;
  string uri;
  bytes signature;
}
  • tokenId : 온체인에 발행될 tokenId
  • uri : 해당 nft의 metadata가 담겨있는 ipfs uri
  • minPrice : 창작자(아티스트)로 부터 nft를 구매하기 위한 최소 금액
  • signature : 창작자(아티스트)의 지갑으로 만들어진서명

  function redeem(address redeemer, NFTVoucher calldata voucher) public payable returns (uint256) {
    address signer = _verify(voucher);
    require(hasRole(MINTER_ROLE, signer), "Signature invalid or unauthorized");

    require(msg.value >= voucher.minPrice, "Insufficient funds to redeem");

    _mint(signer, voucher.tokenId);
    _setTokenURI(voucher.tokenId, voucher.uri);

    _transfer(signer, redeemer, voucher.tokenId);

    address payable tokenOwner = payable(signer);
    tokenOwner.transfer(msg.value);       

    return voucher.tokenId;
  }

  function _hash(NFTVoucher calldata voucher) internal view returns (bytes32) {
    return _hashTypedDataV4(keccak256(abi.encode(
      keccak256("NFTVoucher(uint256 tokenId,uint256 minPrice,string uri)"),
      voucher.tokenId,
      voucher.minPrice,
      keccak256(bytes(voucher.uri))
    )));
  }

  function _verify(NFTVoucher calldata voucher, bytes memory signature) 
  internal view returns (address) {
    bytes32 digest = _hash(voucher);
    return ECDSA.recover(digest, signature);
  }

JavaScript

async createVoucher(tokenId, uri, minPrice) {
  const provider = new ethers.providers.Web3Provider(window.ethereum)
  const signer = provider.getSigner()
  console.log(signer)
  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 signer._signTypedData(domain, types, voucher)
  return {
    ...voucher,
    signature,
  }
},
async _signingDomain() {
  if (this.domain != null) {
  	return this.domain
  }
  const chainId = await web3.eth.getChainId()
  console.log(typeof(chainId))
  this.domain = {
  	name: SIGNING_DOMAIN_NAME,
  	version: SIGNING_DOMAIN_VERSION,
  	chainId: BigNumber.from(chainId),
  	verifyingContract: this.eip712ContractAddress
  }
  return this.domain
}
  • _signTypedData(domain, types, voucher) : ether.js에서 제공하는 함수

창작자(크리에이터)

  • 크리에이터는 nft를 판매 등록하는 시점에 signer로서 EIP-712를 통해 signature를 생성한다

  • signer._signTypedData(domain, types, voucher) 를 통해서 크리에이터의 서명을 return 받는다

  • 이렇게 생성된 byte형식의 signature는 구매자가 구매할때 호출되어 redeem 함수에서 실행된다.

  • 구매가 redeem 되기 위해서 voucher의 signature를 통해서 창작자의 주소를 확인하고 나머지 정보들로 nft를 minting 할 때 사용한다.(tokenId, minPrice, uri)


구매자

  • 구매 시에 smart contract의 redeem 함수가 실행된다.

  • _verify(voucher)를 통해서 창작자(크리에이터)의 address를 알아낸다.

  • _verify의 _hash(voucher)를 이용하여 typedData를 생성하고 ECDSA 알고리즘을 통해 창작자(크리에이터)의 address를 복구한다.


요약

  • EIP-712를 이용하여 창작자(크리에이터)는 nft의 tokenId, uri, minPrice, signature를 이용해서 struct 형식의 voucher를 만든다.
  • minPrice가 기입되었기 때문에 자동적으로 판매 등록이 됩니다.
  • 구매자가 판매 등록 listing 되어 있는 nft를 사게 되면 off-chain 상에 저장되어 있는 해당 nft의 voucher를 불러옵니다.
  • voucher의 signature를 통해서 창작자(크리에이터)의 address를 복구하고 tokenId, uri를 이용해서 nft를 minting합니다.
  • minting이 진행되고 나서 transfer를 통해 창작자(크리에이터)에서 구매자로 nft를 이전합니다.