ERC-721 Lazyminting

이상학·2021년 11월 6일
0

Blockchain

목록 보기
2/4

토큰을 어떻게 발행하는가?
-> Network에 배포된 smartcontract에 정의된 Mint함수로 token을 발행한다

NFT발행하는 법?
-> smartcontract 배포 -> smartcontract에 mint함수 call로 token mint

수수료는 언제?
-> smartcontract 배포시1번 -> token을 mainnet에 mint할때마다

수수료는 왜?
-> mainnet에 배포한다는것은 이더리움넷에 chain을 연결하는 것이다. 이 과정에서 gas fee가 발생

토큰에 Metadata?
-> IPFS으로 METADATA저장 (필자는 pinata이용)

lazyminting : 구매자가 토큰을 mint하고 수수료를 납부하게 하는 방식

creater의 nft접근 문턱을 낮추기 위한방식. nft를 off-chain으로 만들어 두고, 구매하려는 사람이 해당 nft를 mint하고 자기자신에게 transfer 한다.

NFTVoucher - unminted NFT

creator가 NFT등록 -> NFTVoucher 생성

구매자가 NFT구매 -> signed NFTVoucher 획득 + smartcontract의 redeem 함수 call -> NFTVoucher정보를 토대로 NFT mint -> 구매자 계좌로 NFT transfer

_verify
_hash

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

import "hardhat/console.sol";
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 PPTOKEN 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("PlayPlzNFT", "PP-NFT") 
    EIP712("LazyNFT-Voucher", "1") {
      _setupRole(MINTER_ROLE, minter);
    }


  struct NFTVoucher {

    uint256 tokenId;
    uint256 minPrice;
    string uri;
    bytes signature;
  }


  // / @notice Redeems an NFTVoucher for an actual NFT, creating it in the process.
  // / @param redeemer The address of the account which will receive the NFT upon success.
  // / @param voucher An NFTVoucher that describes the NFT to be redeemed.
  // / @param signature An EIP712 signature of the voucher, produced by the NFT creator.
  function redeem(address redeemer, NFTVoucher calldata voucher, address signer) public payable returns (uint256) {
    // make sure signature is valid and get the address of the signer
    // 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;
  }

  function withdraw() public {
    require(hasRole(MINTER_ROLE, msg.sender), "Only authorized minters can withdraw");

    // IMPORTANT: casting msg.sender to a payable address is only safe if ALL members of the minter role are payable addresses.
    address payable receiver = payable(msg.sender);

    uint amount = pendingWithdrawals[receiver];
    // zero account before transfer to prevent re-entrancy attack
    pendingWithdrawals[receiver] = 0;
    receiver.transfer(amount);
  }

  function availableToWithdraw() public view returns (uint256) {
    return pendingWithdrawals[msg.sender];
  }

  // / @notice Returns a hash of the given NFTVoucher, prepared using EIP712 typed data hashing rules.
  // / @param voucher An NFTVoucher to hash.
  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))
    )));
  }

  // / @notice Verifies the signature for a given NFTVoucher, returning the address of the signer.
  // / @dev Will revert if the signature is invalid. Does not verify that the signer is authorized to mint NFTs.
  // / @param voucher An NFTVoucher describing an unminted NFT.
  // / @param signature An EIP712 signature of the given voucher.
  function _verify(NFTVoucher calldata voucher, bytes memory signature) internal view returns (address) {
    bytes32 digest = _hash(voucher);
    return digest.toEthSignedMessageHash().recover(signature);
  }

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

//tokenurl(metadata)"https://gateway.pinata.cloud/ipfs/QmahzYGkKLWYu1hPJcXMc2mJwtVibpgQyUfyjqyH26JfBm"

Point

1. migrate시 migration.js에서

constructor(address payable minter)
ERC721("PlayPlzNFT", "PP-NFT")
EIP712("LazyNFT-Voucher", "1") {
_setupRole(MINTER_ROLE, minter);
}

constructor에서 payable minter의 주소를 넣어야하기때문에

  var Playplztoken = artifacts.require("PPTOKEN");
  module.exports = function(deployer) {  deployer.deploy(Playplztoken,"0x63306Ee846B59Ab24910c663a0B3339B1Fe9991E");
  };

deploy시 뒤에 payable minter의 주소를 인자로 넘겨준다

2. NFTVoucher란

struct NFTVoucher {

uint256 tokenId;
uint256 minPrice;
string uri;
bytes signature;

}

contract내에서 발급된 토큰들은 서로를 구별하기 위해 (ERC-721규격 토큰들은) tokenId 이외로 Uri가 필요하다. tokenid는 contract내에서 중복되지 않으면 되고, token에 metadata를 담고있는 uri를 설정해준다. signature란 해당 voucher에 대해 creator가 만든 signature이다. signer instance로서 저장. 마지막으로 token의 가격까지 결정해준다.

ex) tokenId : 1, minprice : 1(eth), uri : https://gateway.pinata.cloud/ipfs/QmahzYGkKLWYu1hPJcXMc2mJwtVibpgQyUfyjqyH26JfBm"

3. redeem funtion

구매자는 NFT를 구매할때 해당 token의 smartcontract속 redeem function을 Call한다.

function redeem(address redeemer, NFTVoucher calldata voucher, address signer) public payable returns (uint256) {

넘겨줘야하는 인자는
redeem할 사람(구매자)의 주소
NFTVoucher(tuple형태로 작성)
address signer(sign한 사람 주소(creator))

profile
기억의 지배장

1개의 댓글

comment-user-thumbnail
2022년 2월 15일

안녕하세요. 메타버스와 NFT 발행 프로젝트 진행중이며, 스마트컨트랙트 개발중에 있습니다. lazy minting 관련 리서치중 위 게시물을 참고하게 되었는데요. 개발 채용 혹은 자문 관련해서 연락드립니다. 자세한 사항은 메일 부탁드립니다. sg@scale.kr

답글 달기