토큰을 어떻게 발행하는가?
-> Network에 배포된 smartcontract에 정의된 Mint함수로 token을 발행한다
NFT발행하는 법?
-> smartcontract 배포 -> smartcontract에 mint함수 call로 token mint
수수료는 언제?
-> smartcontract 배포시1번 -> token을 mainnet에 mint할때마다
수수료는 왜?
-> mainnet에 배포한다는것은 이더리움넷에 chain을 연결하는 것이다. 이 과정에서 gas fee가 발생
토큰에 Metadata?
-> IPFS으로 METADATA저장 (필자는 pinata이용)
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"
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의 주소를 인자로 넘겨준다
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"
구매자는 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))
안녕하세요. 메타버스와 NFT 발행 프로젝트 진행중이며, 스마트컨트랙트 개발중에 있습니다. lazy minting 관련 리서치중 위 게시물을 참고하게 되었는데요. 개발 채용 혹은 자문 관련해서 연락드립니다. 자세한 사항은 메일 부탁드립니다. sg@scale.kr