Skip to content

Latest commit

 

History

History
677 lines (487 loc) · 28.7 KB

37_ERC-721.md

File metadata and controls

677 lines (487 loc) · 28.7 KB

ERC-721

NFT를 생성할 수 있는 표준


[TOC]


ERC-721

ERC-721은 EIP-721에서 논의되어 생성된 표준(Standard)로, NFT(Non-Fungible Token)를 생성할 수 있는 표준이다.

"A standard interface for non-fungible tokens, also known as deeds."

(증서로도 알려져 있는, 대체 불가능한 토큰을 위한 표준 인터페이스)

NFT는 각각의 토큰 하나하나가 고유하고 식별할 수 있다. 즉, NFT가 가지는 성질은 고유하되, 서로 다른 두 NFT가 갖는 가치는 다르다.

함수의 기능

ERC-721에는 9개의 표준 함수가 있다.

함수명 속성
balanceOf owner가 소유한 NFT의 개수를 반환
ownerOf 특정 tokenId를 가진 NFT의 소유주 주소를 반환
approve 특정 계정에게 자신이 소유한 NFT 하나를 사용하도록 허용
getApproved 특정 NFT가 다른 계정에서 사용 승인되었는지의 여부 반환
setApprovalForAll 특정 계정에게 자신이 소유한 모든 NFT에 대한 사용을 허용
isApprovedForAll owner가 특정 계정에게 자신의 모든 NFT에 대한 사용을 허용했는지의 여부 반환
transferFrom NFT 소유권 전송
safeTransferFrom 받는 주소가 NFT를 받을 수 있는지 확인 후 NFT 소유권 전송
safeTransferFrom -

이 표준 함수를 포함한 컨트랙트를 통해 민팅된 토큰이 바로 NFT이다. 여기서는 OpenZeppelin에서 제공하는 ERC-721 API에 구현된 코드를 사용한다.

변수

string private _name;
string private _symbol;
mapping(uint256 => address) private _owners;
mapping(address => uint256) private _balances;
mapping(uint256 => address) private _tokenApprovals;
mapping(address => mapping(address => bool)) private _operatorApprovals;
  • _name: 토큰의 이름을 저장한다.

  • _symbol: 토큰의 심볼을 저장한다.

    • 예를 들어, 이름이 Bored Ape Yacht Club인 토큰의 심볼은 BAYC이다.
  • _owners: 각 토큰의 ID와 토큰 소유자의 주소를 매핑한다.

  • _balances: 토큰 소유자의 주소와 소유자가 가지고 있는 토큰의 갯수를 매핑한다.

  • _tokenApprovals: 토큰 ID와 approved된 주소를 매핑한다.

  • _operatorApprovals: 토큰 소유자와 operator 주소의 approval 여부를 저장한다.

    • e.g., 0x1234가 0x5678에게 자신의 모든 토큰을 관리할 수 있는 operator 권한을 준 경우

      _operatorApprovals[0x1234][0x5678];  // true
    • e.g., 0x1234가 0x5678의 operator 권한을 취소한 경우

      _operatorApprovals[0x1234][0x5678];  // false

balanceOf(address owner) -> uint256

balanceOf() 함수는 owner 주소가 가지고 있는 NFT의 개수를 리턴한다.

function balanceOf(address owner) public view virtual override returns (uint256) {
  require(owner != address(0), "ERC721: address zero is not a valid owner); // owner의 주소가 0인 경우
  return _balances[owner];  // _balances 변수에 매핑된 값을 가져온다.
}

ownerOf(uint256 tokenId) -> address

모든 NFT는 발행된 컨트랙트 내에서 고유한 token ID를 가지고 있다. 따라서 컨트랙트 주소와 token ID만 있으면 해당 NFT의 정보에 접근할 수 있다. ownerOf() 함수는 token ID를 통해 토큰 owner의 주소를 반환한다.

function ownerOf(uint256 tokenId) public view virtual override returns (address) {
  address owner = _onwers[tokenId]; // _owners 변수에는 token ID와 owner의 주소가 매핑되어 있다.
}

approve(address to, uint256 tokenId)

approve() 함수는 특정 tokenId를 제3자가 사용할 수 있도록 승인(approve)할 수 있다. approve()를 통해 tokenId 사용을 승인받은 제3자는 oprator라고 부르며, operator는 이 tokenId를 다른 스마트 컨트랙트에 사용하거나 다시 approve할 수도 있다. approve() 함수는 소유권을 승인하는 행위이기 때문에, tokenID의 owner나 operator만 호출할 수 있다.

function approve(address to, uint256 tokenId) public virtual override {
  // 유효성 검사 1. operator(to)의 주소와 토큰의 owner가 동일인인지 확인
  address owner = ERC721.ownerOf(tokenId);
  require(to != owner, "ERC721: approval to current owner);

  // 유효성 검사 2. approve 함수를 호출한 사람과 토큰의 owner가 동일한지 확인
  require(
    _msgSender() == owner || isApprovadForAll(owner, _msgSender()),
    "ERC721: approve caller is not token owner nor approved for all
  );

  _approve(to, tokenId);
}

function _approve(address to, uint256 tokenId) internal virtual {
  _tokenApprovals[tokenId] = to;  // _tokenApprovals 변수에 tokenId와 approve된 operator의 주소를 매핑한다.
  emit Approval(ERC721.ownerOf(tokenId), to, tokenId);
}

getApproved(uint256 tokenId) -> address

getApproved() 함수는 token ID가 누군가에게 승인(approve)된 상태이면, 그 승인된 operator의 주소를 반환한다.

function getApproved(uint256 tokenId) public view virtual override returns (address) {
  return _tokenApprovals[tokenId];
}

setApprovalForAll(address to, bool approved)

setApprovalForAll() 함수는 이 함수를 호출한 msg.sender가 이 컨트랙트에서 가지고 있는 모든 NFT를 특정 operator에게 승인하는 함수이다.

첫 번째 인자인 to는 operator의 주소이며, 두 번째 인자인 approved`는 승인 여부를 나타낸다.

  • approvedtrue인 경우: to 주소에게 모든 NFT의 사용을 승인한다.
  • approvedfalse인 경우: operator인 to 주소로부터 NFT 사용 승인을 철회한다.
function setApprovalForAll(address operator, bool approved) public virtual override {
  _setApprovalForAll(_msgSender(), operator, approved);
}

function _setApprovalForAll(address owner, address operator, bool approved) internal virtual {
  require(owner != operator, "ERC721: approve to caller");  // 컨트랙트를 호출한 msg.sender와 approve를 주려는 operator가 동일한지 확인
  _operatorApprovals[owner][operator] = approved; // _operatorApprovals에 owner와 operator, 그리고 approve 여부가 매핑
  emit ApprovalForAll(owner, operator, approved);
}

isApprovedForAll(address owner, address operator) -> bool

isApprovedForAll() 함수는 첫 번째 인자 owner가 두 번째 인자 operator 주소에게 setApprovalForAll() 함수를 통해 모든 NFT를 승인했는지의 여부를 전달한다.

  • 리턴값이 true인 경우: setApprovalForAll() 함수를 호출하여 owner의 모든 NFT에 대해 operator에게 승인한 상태이다.
  • 리턴값이 false인 경우: setApprovalForAll() 함수를 호출한 적이 없거나, setApprovalForAll()을 호출하여 승인을 철회한 상태이다.
function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
  return _operatorApprovals[owner][operator];
}

transferFrom(address from, address to, uint256 tokenId)

transferFrom()from 주소에서 to 주소로 tokenId를 옮긴다. 따라서 from 주소는 옮기려는 토큰의 owner이거나, 승인 받은 operator여야 한다.

function transferFrom(address from, addess to, uint256 tokenId) public virtual override {
  require(_iApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner nor approved");

  _transfer(from, to, tokenId);
}

// tokenId가 spender의 소유이거나, spender에게 승인되었는지 확인
function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) {
  address owner = ERC721.ownerOf(tokenId);
  return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender);
}

function _transfer(address from, address to, uint256 tokenId) internal virtual {
  // 유효성 검사
  require(ERC721.ownerOf(tokenId) == from, "ERC721: trasfer from incorrect owner");
  require(to != address(0), "ERC721: transfer to the zero address");

  // 이전 owner(=from)가 승인했던 approvals를 모두 삭제
  _approve(address(0), tokenId);

  _balances[from] -= 1; // 이전 owner의 NFT 보유 갯수 갱신
  _balances[to] += 1; // 현재 owner(=to)의 NFT 보유 갯수 갱신
  _owners[tokenId] = to;  // tokenId의 owner를 to로 변경

  emit Transfer(from, to, tokenId);
}

그러나 실제로 NFT를 다른 주소로 옮길 때 transferFrom() 대신 safeTransferFrom() 사용을 권장하고 있다.

safeTransferFrom(address from, address to, uint256 tokenId)

safeTransferFrom()의 특징은 NFT를 받는 주소가 NFT를 받을 수 있는 주소인지 확인한다는 점이다.

ERC165 표준을 사용한다.

function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override {
  safeTransferFrom(from, to, tokenId, "");
}

function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual override {
  require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner nor approved");
  // 결국 인자가 4개인 함수를 호출한다.
  _safeTransfer(from, to, tokenId, data);
}

function _safeTransfer(address from, address to, uint256 tokenId, bytes memory data) internal virtual {
  _transfer(from, to, tokenId);
  require(_checkOnERC721Received(from, to, tokenId, data), "ERC721: transfer to non ERC721Receiver implementer");
}

function _checkOnERC721Received(
  address from,
  address to,
  uint256 tokenId,
  bytes memory data
) private returns (bool) {
  if (to.isContract()) {
    try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data) returns (bytes4 retval) {
      // B 컨트랙트가 `onERC721Received()` 함수를 실행하고나면, A 컨트랙트는 B 컨트랙트의 `onERC721Received()` 가 반환한 Selector가 `IERC721Receiver` 인터페이스 표준에 맞게 구현된 `onERC721Received()` 함수인지 확인한다.
      return retval == IERC721Receiver.onERC721Received.selector;
    } catch (bytes memory reason) {
      if (reason.length == 0) {
        revert("ERC721: transfer to non ERC721Receiver implementer");
      } else {
        assembly {
          revert(add(32, reason), mload(reason))
        }
      }
    } else {
      return true;
    }
  }
}

onERC721Received() 함수 안에는 전달받은 NFT를 다루는 함수를 작성해야 한다.

그리고 NFT를 송신한 A 컨트랙트에게 자신이 onERC721Received() 함수를 가지고 있고, 실행했음을 알려줄 수 있도록 onERC721Received() 함수의 Selector를 반환한다.

ERC-165(Standard Interface Detection)를 통해, 모든 컨트랙트의 함수는 고유한 Selector를 가진다. 함수의 Selector는 쉽게 말해 함수의 아이디로, Selector를 구하는 방식은 2가지가 있다.

  1. 함수의 시그니처(함수명과 파라미터의 타입)를 통해 함수의 시그니처를 keccak256으로 암호화한 후, bytes4로 형변환한다.

    // balanceOf(address) 함수의 Selector를 구하는 경우
    bytes4(keccak256("balanceOf(address)")
  2. 컨트랙트 내부 메서드의 Selector 속성을 통해

    function onERC721Received(address msgSender, address nftContractAddress, uint256 _tokenId, bytes calldata _public virtual override returns (bytes4) {
        // 전달 받은 NFT를 다루는 함수..
        return this.onERC721Received.selector;
    }

ERC721Burnable

openzeppelin의 ERC21 extension으로, NFT를 소각하는 기능을 담고 있다.

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

import "../ERC721.sol";
import "../../../utils/Context.sol";

/**
 * @title ERC721 Burnable Token
 * @dev ERC721 Token that can be burned (destroyed).
 */
abstract contract ERC721Burnable is Context, ERC721 {
    function burn(uint256 tokenId) public virtual {
        //solhint-disable-next-line max-line-length
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner nor approved");
        _burn(tokenId);
    }
}

ERC721 Pausable

openzeppelin의 ERC21 extension으로, NFT transfer을 잠시 멈출 수 있도록 하는 기능을 담고 있다.

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

import "../ERC721.sol";
import "../../../security/Pausable.sol";

/**
 * @dev ERC721 token with pausable token transfers, minting and burning.
 *
 * Useful for scenarios such as preventing trades until the end of an evaluation
 * period, or having an emergency switch for freezing all token transfers in the
 * event of a large bug.
 */
abstract contract ERC721Pausable is ERC721, Pausable {
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual override {
        super._beforeTokenTransfer(from, to, tokenId);

        require(!paused(), "ERC721Pausable: token transfer while paused");
    }
}

<실습> Remix를 이용한 ERC-721 개발

  1. Remix에 접속하여 Feil explorers 왼쪽 상단에서 + 버튼을 누르고 프로젝트를 생성한다.

  2. contracts 폴더 내에 자동으로 생성된 파일을 삭제하고 MyNFT.sol 파일을 생성한다.

  3. MyNFT.sol 파일에 ERC-721 컨트랙트 코드를 입력한다.

    여기서는 직접 표준을 구현하는 대신 Open Zeppelin에서 제공하는 라이브러리를 import하여 사용한다.

    // SPDX-License-Identifier: MIT
     pragma solidity ^0.8.7;
    
     import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
    
     contract MyNFTs is ERC721 {
         constructor() public ERC721("MyNFT", "NFT") {}
     }
  4. 화면 왼쪽 compiler 탭에서 Compile MyNFT.sol 버튼을 클릭해 ERC-721 파일을 컴파일한다.

  5. 왼쪽 탭에서 Deploy & run transactions 탭을 선택하고 MetaMask 지갑이 연결(Ropsten 네트워크)되어 있는지 확인한다.

  6. Contract에서 배포할 컨트랙트(MyNFT)를 선택하고 배포 트랜잭션을 보낸다.

  7. 배포가 완료되면 왼쪽 하단 "Deployed Contracts"에서 컨트랙트 함수를 실행할 수 있다.

그러나 OpenZeppelin에서 제공하는 ERC-721 라이브러리에는 ERC-721 표준 함수 9가지만 구현되어 있다. 실제로 여기서 작성한 컨트랙트로 NFT를 민팅(발행)하기 위해서는 컨트랙트에 새로운 함수를 추가해야 한다.

//Contract based on [https://docs.openzeppelin.com/contracts/3.x/erc721](https://docs.openzeppelin.com/contracts/3.x/erc721)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";

contract MyNFT is ERC721URIStorage, Ownable {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    constructor() public ERC721("MyNFT", "NFT") {}

    function mintNFT(address recipient, string memory tokenURI)
        public onlyOwner
        returns (uint256)
    {
        _tokenIds.increment();

        uint256 newItemId = _tokenIds.current();
        _mint(recipient, newItemId);
        _setTokenURI(newItemId, tokenURI);

        return newItemId;
    }
}
  1. Remix에 코드를 붙여넣고, 위와 같이 몇 가지를 수정한다.
  • solidity version을 사용하는 컴파일러의 버전(^0.8.7)으로 변경한다.
  • 발행할 컨트랙트의 이름을 지정한다. (공식 튜토리얼로부터 복사한 컨트랙트의 이름은 MyNFT이지만, 이 컨텐츠에서 사용한 컨트랙트의 이름은 MyNFTs이다.)

위의 코드에서는 NFT를 민팅하는 함수 mintNFT()가 추가된 것을 확인할 수 있다. 여기서 주목할 점은, ERC-721 컨트랙트가 ERC721URIStorageOwnable 컨트랙트를 상속받고 있다는 것이다.

  • ERC721URIStorage는 토큰 정보를 저장하는 tokenURI를 관리하는 컨트랙트이다.
    • NFT는 FT와 다르게 각 토큰이 고유한 특성을 가지고 있다. 가령, 고유한 이미지 등이 특성이 될 수 있다. tokenURI는 이미지 주소 등 NFT의 특성을 저장하는 JSON 객체이다.
    • mintNFT() 함수에서는 ERC721URIStorage 컨트랙트를 사용해 tokenURI를 관리한다.
  • Ownable은 컨트랙트의 소유권(ownership)을 관리하는 컨트랙트이다.
    • 컨트랙트의 특정 함수는 아무나 실행하는 것이 아니라, 컨트랙트의 소유자만 실행할 수 있어야 하는 경우도 있다. 예를 들어, NFT를 민팅할 때는 아무나 민팅하는 것이 아니라, 컨트랙트를 관리하는 소유자만이 민팅하여 정해진 발행량을 준수하도록 해야 한다.
    • 이런 경우, Ownable 컨트랙트의 onlyOwner라는 modifier를 사용하여, 함수를 실행하기 전 함수 호출 계정이 컨트랙트 소유자의 계정인지 확인한다.
    • mintNFT() 함수에서도 함수 실행 전 onlyOwner modifier를 통해 함수를 호출한 계정이 컨트랙트 소유자 계정인지 확인한다. 이를 통해 mintNFT() 함수를 컨트랙트 소유자만 실행할 수 있도록 한다.
  1. 화면 왼쪽 컴파일러에서 컨트랙트의 컴파일을 진행한다.
  2. 배포 탭으로 이동하여 CONTRACT에서 배포할 컨트랙트를 선택하고 ,Deploy 버튼을 눌러 컨트랙트를 배포한다.
  3. 배포된 컨트랙트를 통해 NFT를 발행할 수 있다. (mintNFT 함수)

<실습> OpenSea에 NFT 발행하기

앞의 실습에서 작성한 MyNFT라는 ERC-721 기반 컨트랙트 코드를 이더리움 Rinkeby 테스트넷에 배포하면, OpenSea 테스트넷에에서 발행한 NFT를 확인할 수 있다.

OpenSea에서는 이더리움 테스트 네트워크 중 하나인 Rinkeby 네트워크를 지원한다.

OpenSea에서 MyNFTs 확인하기

  1. Remix에서 MyNFTs 컨트랙트를 이더리움 Rinkeby 네트워크에 업로드한다.

    컨트랙트 배포 트랜잭션을 보내기 위해 Rinkeby 네트워크에서 faucet으을 받아야 한다.

  2. https://testnets.opensea.io/ 에 접속한다.

  3. 검색창에 컨트랙트 주소를 입력하면, 배포한 NFT 컨트ㅐㄱ트가 뜨는 것을 확인할 수 있다.

  4. NFT를 하나 민팅하고, OpenSea에서 NFT 상세 페이지를 들어간다. 해당 NFT에 아무런 내용이 들어가 있지 않다. 이미지나, 속성 등 일반적으로 알고 있는 NFTT 모습과는 다소 다른 모습이다. 이는 NFT를 발행할 떄 tokenURI를 설정하기 않았기 때문이다.

tokenURI

tokenURI는 NFT에 고유성을 부여하는 정보를 담고 있는 JSON 형태의 엔드포인트이다. tokenURI는 다음과 같은 스키마를 따른다.

{
    "name": "name #1",
    "description": "description",
    "image": "imageUri",
    "attributes": [
        {
            "trait_type1": "value1",
            "trait_type2": "value2",
            ...
        }
    ]
}

객체 형태의 JSON 메타데이터는, name, description, image, attributes 등을 담고 있다. name은 NFT의 이름, description은 NFT에 대한 설명, image는 image URI, attributes는 NFT에 고유성을 부여하는 특성이다.

MyNFTs 컨트랙트에 배포하기

  1. AWS S3 버킷에 tokenURI로 사용할 JSON 파일을 업로드한다.

    AWS는 아마존에서 제공하는 클라우드 서비스로, 아마존의 데이터센터에 있는 서버를 유/무료로 사용할 수 있다.

    {
        "name": "Songzero #1",
        "description": "Deep dive into songzero collection!",
        "image": ...,
        "attributes": [
            {
                "trait_type": "Power",
                "value": "Max"
            }
        ]
    }
  2. 해당 JSON 파일이 AWS S3 버킷에 올라가면, 파일에 접근할 수 있는 uri가 생성된다.

  3. Rinkeby 테스트넷에 배포한 MyNFTS 컨트랙트에서 mintNFT 함수를 실행한다. 이 때, tokenURI 파라미터에 AWS에 업로드한 JSON 파일의 URI를 입력한다.

  4. transact 버튼을 눌려 NFT를 민팅한다. 이 함수의 실행 결과로 나타난 트랜잭션 해시를 복사하여, 이더스캔에서 검색한다. 정상적으로 NFT가 발행된 것을 확인할 수 있다.

  5. OpenSea에 들어가 NFT가 정상적으로 배포된 것을 확인한다. (예시)

<실습> Truffle을 이용한 ERC-721 개발

Local에서 ERC-721 개발하기

로컬에서 NFT를 개발하기 위해, Truffle과 Ganache를 사용한다.

  1. 먼저, 새로운 폴더를 생성하고 Truffle을 이용한 개발 환경을 준비한다.

    # 폴더 생성
    mkdir erc721
    # 폴더 진입
    cd erc721
    # truffle 초기화
    truffle init
    # npm 초기화
    npm init
  2. 개발 환경 준비를 마쳤다면 VS Code를 실행한다. VS Code에서 나타나는 포더와 파일 구조는 다음과 같다.

    ㄴ contracts
        ㄴ Migrations.sol
    ㄴ migrations
        ㄴ 1_initial_migrtion.js
    ㄴ test
        ㄴ .gitkeep
    ㄴ package.json
    ㄴ truffle-config.js
    
  3. truffle-config.js를 열고 solc를 설정한다. solc는 Solidity, Compiler의 버전 등을 설정한다. 이 예시에서 사용하는 Soliditiy의 버전은 "0.8.15"이고, Compiler의 버전은 "london"이다. 주석을 제거하고 다음과 같이 설정을 수정한다.

      compilers: {
        solc: {
          version: "0.8.15",
          settings: { 
           evmVersion: "london"
          }
        }
      },
  4. 다음으로 네트워크 설정을 편집한다. Ganache를 이용해 로컬 환경에서 테스트할 예정이므로, 먼저 Ganache를 등록한다. 다음과 같이 주석을 제거하고, development를 구분하기 좋게 ganache로 변경한다.

      networks: {
        ganache: {
          host: '127.0.0.1',  // Localhost (default: none)
          port: 8545, // Standard Ethereum port (default: none)
          network_id: '*',  // Any network (default: none)
        },
      },
  5. 다음으로 contracts > MyNFTs.sol 파일을 생성하고 코드를 입력한다.

    //Contract based on [https://docs.openzeppelin.com/contracts/3.x/erc721](https://docs.openzeppelin.com/contracts/3.x/erc721)
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.15;
    
    import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
    import "@openzeppelin/contracts/utils/Counters.sol";
    import "@openzeppelin/contracts/access/Ownable.sol";
    import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
    
    contract MyNFTs is ERC721URIStorage, Ownable {
        using Counters for Counters.Counter;
        Counters.Counter private _tokenIds;
    
        constructor() public ERC721("MyNFTs", "MNFT") {}
    
        function mintNFT(string memory tokenURI)
            public onlyOwner
            returns (uint256)
        {
            _tokenIds.increment();
    
            uint256 newItemId = _tokenIds.current();
            _mint(msg.sender, newItemId);
            _setTokenURI(newItemId, tokenURI);
    
            return newItemId;
        }
    }

    위 코드는 기존 Remix에서 작성한 코드에서, CLI 환경에서 보다 편리하기 테스트하기 위해 함수 mintNFT 부분에서 변경되었다.

    • 파라미터 address recipient 삭제

    • 파라미터 address recipient를 사용하는 _mint 함수 실행부에서 ,recipientmsg.sender로 대체

    이렇게 변경하면, owner의 지갑 주소에서만 함수 mintNFT를 실행할 수 있고, 새로운 NFT는 항상 오너의 지갑 주소로 발행된다.

  6. 이 파일에서는 OpenZeppelin 파일을 참조하고 있기 때문에, npm을 통해 openzeppelin을 설치한다.

    npm install @openzeppelin/contracts
  7. 붙여놓은 코드를 정상적으로 배포하기 위해 migration 파일을 설정한다.

    migration/1_initial_migration.js 파일을 열고 다음과 같이 편집한다.

    const Migrations = artifacts.require("Migrations");
    const MyNFTs = artifacts.require("MyNFTs");    // 추가됨
    
    module.exports = function (deployer) {
      deployer.deploy(Migrations);
      deployer.deploy(MyNFTs);    // 추가됨
    };
### Truffle로 ERC-721 배포하기

로컬 환경에서 Ganache를 이용해 컨트랙트를 배포한다.

1. 먼저, 새로운 터미널을 열고 `ganache-cli`를 실행한다.

```bash
# 새로운 터미널에서 ganache-cli 실행
ganache-cli
  1. 기존 터미널에서 Truffle로 배포한다. 이때, truffle-config.js 파일의 network에 설정한 ganache로 실행해야 한다.

    truffle migrate --compile-all --network ganache

    위 명령어에서 --network에 이어 나오는 ganachetruffle-config.js에서 설정한 network이며, 이를 통해 ganache-cli와 연결하고 컨트랙트를 배포한다.

  2. 컨트랙트 배포를 성공했다면, Truffle console을 이용해 컨트랙트를 조작할 수 있다. 다음의 명령어를 터미널 프롬프트에 입력하고 Truffle console에 진입한다.

    truffle console --network ganache
  3. Truffle console에서, 배포한 컨트랙트의 인스턴스를 받아온다. 그리고 배포한 컨트랙트가 작성한 코드와 일치하는지 확인한다.

    instance = await MyNFTs.deployed()
    instance.name()
    # 'MyNFTs'
    instance.symbol()
    # 'MNFT'
    
  4. 배포된 컨트랙트에 대한 확인을 마치면, 작성한 함수가 잘 동작하는지 확인한다. Ganache를 이용해 컨트랙트를 배포할 때 default로 첫번째 지갑의 주소(0번째 index 지갑)에서 컨트랙트를 배포한다.

    instance.mintNFT("https://urclass-images.s3.ap-northeast-2.amazonaws.com/beb/section4/unit4/test.json", { from: accounts[0] })
    # Tx Information
    
  5. 발행된 NFT로부터 tokenURI를 확인한다.

    instance.tokenURI(1)
    # "https://urclass-images.s3.ap-northeast-2.amazonaws.com/beb/section4/unit4/test.json"
    

앞으로 다른 네트워크(ex. 메인넷)에 배포할 때에는 truffle-config.js에 네트워크 정보를 추가하고, Truffle 명령어 중 옵션 --network와 함께 네트워크 정보를 담고 있는 키를 입력하면 된다.

IPFS와 NFT

IPFS(InterPlanetary File System, 분산 파일 시스템)는 모든 컴퓨터를 연결하고자 하는 분산된 P2P 파일 시스템이다. IPFS Web은 기존의 HTTP Web의 문제점을 해결하고 보완한 새로운 Web이다.

IPNS에 파일을 업로드하면, 업로드된 파일의 해시값이 산출되고, 이 해시값은 업로드된 파일의 영구적인 이름으로 사용된다. 이 해시값을 이용해, IPFS를 제공하는 노드의 엔드포인트를 tokenURI로써 블록체인에 저장하여 온체인 NFT를 구현할 수 있다.

제공하려는 NFT의 특징에 따라 IPFS를 선택할 수도, 또는 아마존 s3 버킷과 같은 클라우드 컴퓨팅을 사용할 수도 있다.

Remix에서 IPFS에 배포하기

Remix에서 컨트랙트를 배포할 때는 위 사진처럼 Publish to IPFS 체크박스가 존재한다. 이 체크박스에 체크하는 것으로, 컨트랙트를 IPFS와 함께 배포할 수 있다.

Copyright © 2022 Song_Artish