BlockChain

(BlockChain) NFT 토큰 컨트랙트

JJeongHyun 2023. 3. 14. 16:55
반응형

OpenSea 등 NFT 마켓에서 사용하는 컨트랙트

  • NFT 토큰 컨트랙트 구현
import "../node_modules/@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "../node_modules/@openzeppelin/contracts/access/Ownable.sol";
import "../node_modules/@openzeppelin/contracts/utils/Strings.sol";
  1. ERC721 기본 컨트랙트
  2. owner 관련 컨트랙트, _owner 등을 추가
  3. 기존에 int, uint 등을 string화 하려면 byte로 바꿨다가 변경해야 하는데 String 라이브러리에서 제공하는 기능을 사용

 

uint public constant MAX_TOKEN_COUNT = 1000;
uint public mint_price = 1 ether;
string public metadataURI;
  1. NFT 최대 발행량
    • constant : JS에서 const를 의미한다, 바뀌지 않는 변수(상수), solidity에서의 표현
    • 상수의 변수명을 정할 때 전부 대문자로 선언
  2. minting의 가격, 사용자가 NFT를 올릴 때 마다 1 ether씩 받는다
    • 연산으로 양을 표현하게 될 경우 가스비 소모
    • ether, gwei, second, minute, day 등 단위로 사용
    • ether인 경우 solidity에서 10 ** 18으로 인식
  3. NFT의 tokenId 값에 매칭되는 tokenURI의 앞부분, 웹페이지에서의 baseURL과 같은 기능
  struct TokenData {
    uint Rank;
    uint Type;
  }
mapping(uint => TokenData) public TokenDatas;
  • tokenId →TokenData
uint[4][4] public tokenCount;
  • 토큰 데이터에 따른 NFT 토큰 발행량 확인, 4 * 4 이중 배열

 

tokenURL을 생성하는 함수

function tokenURI(uint _tokenId) public view override returns (string memory) {
    string memory Rank = Strings.toString(TokenDatas[_tokenId].Rank);
    string memory Type = Strings.toString(TokenDatas[_tokenId].Type);

    return string(abi.encodePacked(metadataURI, "/", Rank, "/", Type, ".json"));
  }
  • Rank, Type은 uint 타입인데 바로 string으로 형변환이 불가능
    • 이에 라이브러리를 사용해서 uint → bytes → string으로 변환
  • NFT에 대한 데이터를 저장한 URL 주소를 찾아 데이터를 받아올 수 있도록 구현

NFT 생성하는 함수

function mintToken() public payable {
    require(msg.value >= mint_price);
    require(MAX_TOKEN_COUNT > ERC721Enumerable.totalSupply());

    uint tokenId = ERC721Enumerable.totalSupply() + 1;
    TokenData memory random = getRandomTokenData(msg.sender, tokenId);
    TokenDatas[tokenId] = random;
    tokenCount[random.Rank - 1][random.Type - 1] += 1;

    payable(Ownable.owner()).transfer(msg.value);
    _mint(msg.sender, tokenId);
  }
  • CA에게 ether를 지급해서 NFT를 구매한다는 느낌
  • 생성 시 ether를 받고 가격을 확인, 돈을 받고 NFT를 생성
  • NFT 최대 개수 확인, 현재 1000개 이하로만 생성 가능
  • NFT 총 수량을 기준으로 ID 생성, 총발행량의 +1로 tokenId 생성
  • 무작위 Rank, Type을 만듦
  • 생성한 토큰 데이터를 ID와 매칭하여 저장
  • Rank, Type을 기준으로 NFT 수량 정리
  • 받은 Ether 컨트랙트 소유자에게 전달(NFT 토큰 컨트랙트 등록자) 후 NFT 생성

 

TokenData를 랜덤으로 만들어주는 함수

function getRandomTokenData(address _owner, uint _tokenId) private pure returns (TokenData memory) {
    uint randomNum = uint(keccak256(abi.encodePacked(_owner, _tokenId))) & 100;

    TokenData memory data;

    if (randomNum < 5) {
      data.Rank = 4;
      if (randomNum == 1) data.Type = 1;
      else if (randomNum == 2) data.Type = 2;
      else if (randomNum == 3) data.Type = 3;
      else data.Type = 4;
    } else if (randomNum < 13) {
      data.Rank = 3;
      if (randomNum < 7) data.Type = 1;
      else if (randomNum < 9) data.Type = 2;
      else if (randomNum < 11) data.Type = 3;
      else data.Type = 4;
    } else if (randomNum < 37) {
      data.Rank = 2;
      if (randomNum < 19) data.Type = 1;
      else if (randomNum < 25) data.Type = 2;
      else if (randomNum < 31) data.Type = 3;
      else data.Type = 4;
    } else {
      data.Rank = 1;
      if (randomNum < 52) data.Type = 1;
      else if (randomNum < 68) data.Type = 2;
      else if (randomNum < 84) data.Type = 3;
      else data.Type = 4;
    }
    return data;
  }
  •  solidity에서는 random 함수(메서드)가 없다
  • 따라서, 유일한 값인 tokenId를 가져와서 암호화한 후 나머지 연산으로 0 ~ 99까지 랜덤 한 수를 만든다

metadataURI를 수정할 수 있는, 변경하는 함수

function setMetadataURI(string memory _uri) public onlyOwner {
    metadataURI = _uri;
  }
  • 컨트랙트 배포자, 등록자(소유자)만 실행할 수 있도록 하는 접근 제한자를 사용
    • onlyOwner

TokenData의 Rank 조회

function getTokenRank(uint _tokenId) public view returns (uint) {
    return TokenDatas[_tokenId].Rank;
  }

 

TokenData의 Type 조회

function getTokenType(uint _tokenId) public view returns (uint) {
    return TokenDatas[_tokenId].Type;
  }

 

Rank, Type을 구분하여 NFT의 수량을 확인

function getTokenCount() public view returns (uint[4][4] memory) {
    return tokenCount;
  }
  • 배열의 전체를 반환하기 위한 함수
  • getter 함수는 요소 하나만 반환해주고 배열의 전체를 조회하는 것은 불가능하기에 따로 view 함수를 생성

 

전체 코드

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "../node_modules/@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "../node_modules/@openzeppelin/contracts/access/Ownable.sol";
import "../node_modules/@openzeppelin/contracts/utils/Strings.sol";

contract nftToken is ERC721Enumerable, Ownable {
  uint public constant MAX_TOKEN_COUNT = 1000;
  uint public mint_price = 1 ether;
  string public metadataURI;

  struct TokenData {
    uint Rank;
    uint Type;
  }
  mapping(uint => TokenData) public TokenDatas;
  uint[4][4] public tokenCount;

  constructor(
    string memory _name,
    string memory _symbol,
    string memory _metadataURI
  ) ERC721(_name, _symbol) {
    metadataURI = _metadataURI;
  }

  function tokenURI(
    uint _tokenId
  ) public view override returns (string memory) {
    string memory Rank = Strings.toString(TokenDatas[_tokenId].Rank);
    string memory Type = Strings.toString(TokenDatas[_tokenId].Type);

    return string(abi.encodePacked(metadataURI, "/", Rank, "/", Type, ".json"));
  }

  function mintToken() public payable {
    require(msg.value >= mint_price);
    require(MAX_TOKEN_COUNT > ERC721Enumerable.totalSupply());

    uint tokenId = ERC721Enumerable.totalSupply() + 1;
    TokenData memory random = getRandomTokenData(msg.sender, tokenId);
    TokenDatas[tokenId] = random;
    tokenCount[random.Rank - 1][random.Type - 1] += 1;

    payable(Ownable.owner()).transfer(msg.value);
    _mint(msg.sender, tokenId);
  }

  function getRandomTokenData(
    address _owner,
    uint _tokenId
  ) private pure returns (TokenData memory) {
    uint randomNum = uint(keccak256(abi.encodePacked(_owner, _tokenId))) & 100;

    TokenData memory data;

    if (randomNum < 5) {
      data.Rank = 4;
      if (randomNum == 1) data.Type = 1;
      else if (randomNum == 2) data.Type = 2;
      else if (randomNum == 3) data.Type = 3;
      else data.Type = 4;
    } else if (randomNum < 13) {
      data.Rank = 3;
      if (randomNum < 7) data.Type = 1;
      else if (randomNum < 9) data.Type = 2;
      else if (randomNum < 11) data.Type = 3;
      else data.Type = 4;
    } else if (randomNum < 37) {
      data.Rank = 2;
      if (randomNum < 19) data.Type = 1;
      else if (randomNum < 25) data.Type = 2;
      else if (randomNum < 31) data.Type = 3;
      else data.Type = 4;
    } else {
      data.Rank = 1;
      if (randomNum < 52) data.Type = 1;
      else if (randomNum < 68) data.Type = 2;
      else if (randomNum < 84) data.Type = 3;
      else data.Type = 4;
    }
    return data;
  }

  function setMetadataURI(string memory _uri) public onlyOwner {
    metadataURI = _uri;
  }

  function getTokenRank(uint _tokenId) public view returns (uint) {
    return TokenDatas[_tokenId].Rank;
  }

  function getTokenType(uint _tokenId) public view returns (uint) {
    return TokenDatas[_tokenId].Type;
  }

  function getTokenCount() public view returns (uint[4][4] memory) {
    return tokenCount;
  }
}