보다 더 생생한 기록

[Solidity] Lazy Minting 코드 분석 본문

블록체인

[Solidity] Lazy Minting 코드 분석

viviviviviid 2022. 11. 16. 20:55

https://github.com/yusefnapora/lazy-minting/blob/master/lazy-mint-example/contracts/LazyNFT.sol

 

GitHub - yusefnapora/lazy-minting: A work-in-progress example of minting NFTs at the point of sale

A work-in-progress example of minting NFTs at the point of sale - GitHub - yusefnapora/lazy-minting: A work-in-progress example of minting NFTs at the point of sale

github.com

 

https://www.youtube.com/watch?v=vYwYe-Gv_XI

 

 

//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 LazyNFT 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("LazyNFT", "LAZ") 
    EIP712("LazyNFT-Voucher", "1") {
      _setupRole(MINTER_ROLE, minter);
    }

  /// @notice 아직 블록체인에 기록되지 않은 발행되지 않은 NFT를 나타냅니다. 서명된 바우처는 교환 기능을 사용하여 실제 NFT로 교환할 수 있습니다.
  struct NFTVoucher {
    /// @notice 교환할 토큰의 ID입니다. 고유해야 합니다. 이 ID를 가진 다른 토큰이 이미 있는 경우 교환 기능이 되돌아갑니다.
    uint256 tokenId;

    /// @notice NFT 생성자가 이 NFT의 초기 판매에 대해 기꺼이 수락하는 최소 가격(wei 단위)입니다.
    uint256 minPrice;

    /// @notice 이 토큰과 연결할 메타데이터 URI입니다.
    string uri;
  }


  /// @notice 실제 NFT에 대해 NFTVoucher를 사용하여 프로세스에서 생성합니다.
  /// @param redeemer 성공 시 NFT를 받을 계정의 주소입니다.
  /// @param voucher 교환될 NFT를 설명하는 NFT 바우처입니다.
  /// @param signature NFT 작성자가 생성한 바우처의 EIP712 서명입니다.
  function redeem(address redeemer, NFTVoucher calldata voucher, bytes memory signature) public payable returns (uint256) { // @@@ lazyminting 을 하는 단계 (이전까지는 onchain에 NFT가 올라간게아님)
    // 서명이 유효한지 확인하고 서명자의 주소를 가져옵니다.
    address signer = _verify(voucher, signature);

    // 서명자가 NFT를 생성할 권한이 있는지 확인합니다.
    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);

    // 서명자의 인출 잔액에 대한 지불 기록
    pendingWithdrawals[signer] += msg.value;    // @@@ 아직은 서버계정의 지갑으로 인출된게 아니고, 컨트랙트 내에 있음

    return voucher.tokenId;
  }

  function withdraw() public {  // @@@ 한번에 모았다가 서버계정으로 전송 (가스비 아끼기위해)
    require(hasRole(MINTER_ROLE, msg.sender), "Only authorized minters can withdraw");
    
     // 중요: msg.sender를 지불 가능한 주소로 캐스팅하는 것은 minter 역할의 모든 구성원이 지불 가능한 주소인 경우에만 안전합니다.
    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 EIP712 유형 데이터 해싱 규칙을 사용하여 준비된 주어진 NFTVoucher의 해시를 반환합니다.
  /// @param voucher 해시할 NFTVoucher입니다.
  function _hash(NFTVoucher calldata voucher) internal view returns (bytes32) {     // lazyminting 과정중, 맨 처음 아티스트가 오프체인에 NFT를 올리기 위해, 관련된 파라미터를 집어넣고 해싱하는 과정
    return _hashTypedDataV4(keccak256(abi.encode(
      keccak256("NFTVoucher(uint256 tokenId,uint256 minPrice,string uri)"),
      voucher.tokenId,
      voucher.minPrice,
      keccak256(bytes(voucher.uri))
    )));
  }

  /// @notice 주어진 NFTVoucher에 대한 서명을 확인하여 서명자의 주소를 반환합니다.
  /// @dev 서명이 유효하지 않으면 되돌립니다. 서명자가 NFT를 발행할 권한이 있는지 확인하지 않습니다.
  /// @param voucher 발행되지 않은 NFT를 설명하는 NFTVoucher입니다.
  /// @param signature 주어진 바우처의 EIP712 서명입니다
  function _verify(NFTVoucher calldata voucher, bytes memory signature) internal view returns (address) {
    bytes32 digest = _hash(voucher);    // @@@ 해싱하고 리턴된 byte32 형태의 내용들이 들어감
    return digest.toEthSignedMessageHash().recover(signature);  
    // @@@ digest를 sign된 메세지 해쉬로 만들고
    // @@@ recover(hash, signature) -> address 이므로
    // @@@ 리턴되는 값은, 메세지에 서명한 주소를 반환 
    // @@@ 이때 recover의 param인 signature은 비교하는 대상임 (데이터가 서로 동일한지) 


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