Lazy Minting Part2

2 분 소요

Lazy Minting(Part2)


Signature

Signature : wallet만을 이용해서 특정 EIP712에 해당하는 서명을 도출해냄

  • 온체인(blockchain / on-chain)과 상호작용이 없이 순전히 오프체인(off-chain)을 이용하여 만듬
  • Signature 생성시 domain에 보증할 smart contract 정보(domain), voucher의 실제 값(voucher), voucher의 구조체(types)를 사용
  • 만들어진 서명(signature)은 온체인(on-chain)이 아닌 오프체인(off-chain)인 데이터베이스(database)에 저장함

Domain

Domain : 서명이 유효한 contract 및 제반 사항을 확인함

  • name : smart contract의 EIP712 constructor name
  • version : smart contract의 EIP712 constructor version
  • chainId : wallet이 최근 선택한 network chain의 고유 id
  • vertifyContract : smart contract의 contract address

Wallet address & Contract address -> Signature(off-chain)

wallet을 이용하여 signature 생성

  • 만들어진 서명(signature)은 데이터베이스(database)에 저장함
  • contract address + user wallet address -> Keccak256 Hash -> 332 entry Uint8Array -> Signature

Javascript(off-chain)

import Web3 from 'web3';
import { BigNumber, ethers} from 'ethers'

const SIGNING_DOMAIN_NAME = "EIP712"
const SIGNING_DOMAIN_VERSION = "1"

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.nftContractAddress
  }
  return this.domain
}



Signature -> Wallet address(on-chain)

smart contract를 이용하여 address 도출

  • ECDSA.sol openzeppelin library 사용
  • smart contract(on-chain)을 이용하여 signature로 wallet address 도출
  • signature -> Construct Keccak256 Hash -> toEthSignedMessageHash -> Recover the signer’s wallet address(public key)

_hash(voucher)

  • _hashTypedDataV4를 이용하여 voucher parameter를 EIP712 메세지의 인코딩된 해시를 반환

verify(voucher, signature)

  • ECDSA.recover(_hash(voucer), signature)를 통해서 signer의 wallet address를 도출해냄

redeem(redeemer, voucher)

  • verify 함수와 voucher parameter를 이용하여 signer의 address를 도출함
  • 도출된 signer의 address와 voucher의 nft 정보를 이용해서 mint함
  • mint 후 transfer(redeemer, voucher.tokenId)를 이용하여 nft 소유권을 구매자에게 양도

민팅 & 구매 시 : minting transaction + transfer transaction 두 번 발생(가스비 2번 발생)
레이지민팅 & 구매 : minting, transfer transaction 한 번 발생(가스비 1번 발생)
가스비 : 레이지민팅 « 민팅

const redeemer = (await this.getEthWallet())[0];
const mynftContractAddress = new web3.eth.Contract(nftContract.abi, nftContractAddress);
const signer = '0x0000000000000000000000000000000000000000'
const nonce = await web3.eth.getTransactionCount(
  publicAddress,
  "latest"
); 
const tx = {
  from: redeemer,
  to: nftContractAddress,
  nonce: nonce,
  value: ethers.utils.parseEther(price.toString()).toHexString(),
  data: mynftContractAddress.methods.redeem(redeemer, voucher).encodeABI(),
};

Smart Contract(on-chain)

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.7;
pragma experimental ABIEncoderV2;

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol";

contract nftToken is ERC721URIStorage, EIP712, AccessControl {
  using ECDSA for bytes32;

  bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

  mapping (address => uint256) pendingWithdrawals;

  constructor(address payable minter)
    ERC721("erc721", "my-nft") 
    EIP712("EIP712", "1") {
      _setupRole(MINTER_ROLE, minter);
    }


  struct NFTVoucher {

    uint256 tokenId;
    uint256 minPrice;
    string uri;
    bytes 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);
  }

  function supportsInterface(bytes4 interfaceId) public view virtual override (AccessControl, ERC721) returns (bool) {
    return ERC721.supportsInterface(interfaceId) || AccessControl.supportsInterface(interfaceId);
  }
}