블록체인

[ethereum] ERC721을 이용한 NFT 만들기 - 1편

멍개. 2022. 8. 28. 06:53

ERC721 기반의 NFT를 만들어 본 후 가볍게 실습을 진행해보는 시간을 가져보도록 하겠습니다.

· NFT란?

최소한 NFT가 무엇인지부터 살펴보자. NFT는 대체 불가능한 토큰을 의미합니다. 여기서 대체 불가능한 토큰이 의미하는 바가 무엇인지 알아보도록 하겠습니다.

대체 가능한 토큰 예시

앞의 그림처럼 A와 B는 서로 1000원을 가지고 있습니다. 만약, 서로가 천원을 서로 주고받더라도 각각의 1000원은 동일한 가치를 가집니다. 여기서 1000원 대신 특정 물건으로 대체해보겠습니다. 앞의 예시와 다르게 특정 물건은 고유한 유니크 값을 가지고 있습니다. 같은 정보를 가지고 있는 데이터라고 하더라도 생성일이 다르기 때문에 유니크한 값으로 만들 수 있습니다. 이러한 특성을 대체 불가능하다고 합니다.

A가 만든 캐릭터는 누가 가지고 있던간에 A가 만든 캐릭터라는 속성을 가지고 있습니다. 즉, 누가 가지고 있더라도 A가 만들었고 고유한 속성을 그대로 가지고 있습니다. 이때 이들은 데이터로 움직이기 때문에 캐릭터 정보를 다 움직이는 것이 아닌 캐릭터에 할당된 유니크한 값(ID)값으로 주고 받으며 캐릭터 하나하나를 토큰으로 취급합니다.

만약, A가 캐릭터를 10개 소유하고 있다면 10개의 토큰을 가지고 있는겁니다.

ERC20으로 만들어진 토큰의 경우 특정 주소가 소유하고 있는 토큰을 조회하면 지불하여 사용 가능한 토큰 수량을 조회하며, ERC721로 만들어진 NFT의 경우 보유중인 대체 불가능한 무언가를 소유하고 있는 수량입니다.

여기서 무언가는 어떻게 어떤 형태로 만드냐에 따라 결정됩니다.

NFT를 만들기 위해 우리는 만들어져있는 인터페이스를 활용할 수 있습니다.

● NFT 구현

NFT를 구현하기 위해 어떤 인터페이스를 제공하는지 알아보겠습니다.

event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

function balanceOf(address _owner) external view returns (uint256);
function ownerOf(uint256 _tokenId) external view returns (address);
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
function approve(address _approved, uint256 _tokenId) external payable;
function setApprovalForAll(address _operator, bool _approved) external;
function getApproved(uint256 _tokenId) external view returns (address);
function isApprovedForAll(address _owner, address _operator) external view returns (bool);
function supportsInterface(bytes4 interfaceID) external view returns (bool);

우리는 해당 인터페이스를 직접 구현하지 않고 구현된 인터페이스를 기반으로 우리가 원하는 기능을 구현할것입니다.

· interface(인터페이스)

library Address {

    function isContract(address account) internal view returns (bool) {
        uint256 size;
        assembly { size := extcodesize(account) }
        return size > 0;
    }
}

library SafeMath {

    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "SafeMath: addition overflow");

        return c;
    }

    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b <= a, "SafeMath: subtraction overflow");
        uint256 c = a - b;

        return c;
    }

    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        if (a == 0) {
            return 0;
        }

        uint256 c = a * b;
        require(c / a == b, "SafeMath: multiplication overflow");

        return c;
    }

    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b > 0, "SafeMath: division by zero");
        uint256 c = a / b;

        return c;
    }

    function mod(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b != 0, "SafeMath: modulo by zero");
        return a % b;
    }
}

library Counters {
    using SafeMath for uint256;

    struct Counter {
        uint256 _value; // 기본값: 0
    }

    function current(Counter storage counter) internal view returns (uint256) {
        return counter._value;
    }

    function increment(Counter storage counter) internal {
        counter._value += 1;
    }

    function decrement(Counter storage counter) internal {
        counter._value = counter._value.sub(1);
    }
}
interface IERC165 {
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

contract ERC165 is IERC165 {
    bytes4 private constant _INTERFACE_ID_ERC165 = 0x01ffc9a7;

    mapping(bytes4 => bool) private _supportedInterfaces;

    constructor () internal {
        _registerInterface(_INTERFACE_ID_ERC165);
    }

    function supportsInterface(bytes4 interfaceId) external view returns (bool) {
        return _supportedInterfaces[interfaceId];
    }

    function _registerInterface(bytes4 interfaceId) internal {
        require(interfaceId != 0xffffffff, "ERC165: invalid interface id");
        _supportedInterfaces[interfaceId] = true;
    }
}

contract IERC721 is IERC165 {
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

    function balanceOf(address owner) public view returns (uint256 balance);

    function ownerOf(uint256 tokenId) public view returns (address owner);

    function safeTransferFrom(address from, address to, uint256 tokenId) public;

    function transferFrom(address from, address to, uint256 tokenId) public;
    function approve(address to, uint256 tokenId) public;
    function getApproved(uint256 tokenId) public view returns (address operator);

    function setApprovalForAll(address operator, bool _approved) public;
    function isApprovedForAll(address owner, address operator) public view returns (bool);


    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public;
} 

contract IERC721Receiver {
    function onERC721Received(address operator, address from, uint256 tokenId, bytes memory data)
    public returns (bytes4);
}
contract ERC721 is ERC165, IERC721 {
    using SafeMath for uint256;
    using Address for address;
    using Counters for Counters.Counter;

    bytes4 private constant _ERC721_RECEIVED = 0x150b7a02;

    // 토큰 ID에서 소유자로의 매핑
    mapping (uint256 => address) private _tokenOwner;

    // 토큰 ID에서 승인된 주소로의 매핑
    mapping (uint256 => address) private _tokenApprovals;

    // 소유자에서 소유한 토큰 개수로의 매핑
    mapping (address => Counters.Counter) private _ownedTokensCount;

    mapping (address => mapping (address => bool)) private _operatorApprovals;

    bytes4 private constant _INTERFACE_ID_ERC721 = 0x80ac58cd;

    constructor () public {
        _registerInterface(_INTERFACE_ID_ERC721);
    }

    function balanceOf(address owner) public view returns (uint256) {
        require(owner != address(0), "ERC721: balance query for the zero address");

        return _ownedTokensCount[owner].current();
    }

    function ownerOf(uint256 tokenId) public view returns (address) {
        address owner = _tokenOwner[tokenId];
        require(owner != address(0), "ERC721: owner query for nonexistent token");

        return owner;
    }

    function approve(address to, uint256 tokenId) public {
        address owner = ownerOf(tokenId);
        require(to != owner, "ERC721: approval to current owner");

        require(msg.sender == owner || isApprovedForAll(owner, msg.sender),
            "ERC721: approve caller is not owner nor approved for all"
        );

        _tokenApprovals[tokenId] = to;
        emit Approval(owner, to, tokenId);
    }

    function getApproved(uint256 tokenId) public view returns (address) {
        require(_exists(tokenId), "ERC721: approved query for nonexistent token");

        return _tokenApprovals[tokenId];
    }

    function setApprovalForAll(address to, bool approved) public {
        require(to != msg.sender, "ERC721: approve to caller");

        _operatorApprovals[msg.sender][to] = approved;
        emit ApprovalForAll(msg.sender, to, approved);
    }

    function isApprovedForAll(address owner, address operator) public view returns (bool) {
        return _operatorApprovals[owner][operator];
    }

    function transferFrom(address from, address to, uint256 tokenId) public {
        require(_isApprovedOrOwner(msg.sender, tokenId), "ERC721: transfer caller is not owner nor approved");

        _transferFrom(from, to, tokenId);
    }

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

    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public {
        transferFrom(from, to, tokenId);
        require(_checkOnERC721Received(from, to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer");
    }

    function _exists(uint256 tokenId) internal view returns (bool) {
        address owner = _tokenOwner[tokenId];
        return owner != address(0);
    }

    function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) {
        require(_exists(tokenId), "ERC721: operator query for nonexistent token");
        address owner = ownerOf(tokenId);
        return (spender == owner || getApproved(tokenId) == spender || isApprovedForAll(owner, spender));
    }

    function _burn(address owner, uint256 tokenId) internal {
        require(ownerOf(tokenId) == owner, "ERC721: burn of token that is not own");

        _clearApproval(tokenId);

        _ownedTokensCount[owner].decrement();
        _tokenOwner[tokenId] = address(0);

        emit Transfer(owner, address(0), tokenId);
    }

    function _burn(uint256 tokenId) internal {
        _burn(ownerOf(tokenId), tokenId);
    }

    function _transferFrom(address from, address to, uint256 tokenId) internal {
        require(ownerOf(tokenId) == from, "ERC721: transfer of token that is not own");
        require(to != address(0), "ERC721: transfer to the zero address");

        _clearApproval(tokenId);

        _ownedTokensCount[from].decrement();
        _ownedTokensCount[to].increment();

        _tokenOwner[tokenId] = to;

        emit Transfer(from, to, tokenId);
    }

    function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data)
        internal returns (bool)
    {
        if (!to.isContract()) {
            return true;
        }

        bytes4 retval = IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, _data);
        return (retval == _ERC721_RECEIVED);
    }

    function _clearApproval(uint256 tokenId) private {
        if (_tokenApprovals[tokenId] != address(0)) {
            _tokenApprovals[tokenId] = address(0);
        }
    }
}

NFT를 만들기 위해 ERC721을 구현하기 위한 인터페이스가 좀 길지만 전체적인 흐름은 토큰ID를 주고받는 코드입니다.

기본적으로 ERC721의 인터페이스는 특정 토큰이 생성되서 소유권을 부여하는 부분이 없습니다.

mapping (uint256 => address) private _tokenOwner;
mapping (address => Counters.Counter) private _ownedTokensCount;

특정 토큰의 소유 주소를 가지고 있는 정보와, 특정 주소가 몇개의 토큰을 소유하고 있는지에 대한 정보를 가지고 있는 변수입니다.

ERC721 컨트랙트에 다음코드를 추가하여 소유권을 부여하는 _mint를 추가합니다.

contract ERC721 is ERC165, IERC721 {
    // ... 중략 ...

    function _mint(address to, uint256 tokenId) internal {
        require(to != address(0), "ERC721: mint to the zero address");
        require(!_exists(tokenId), "ERC721: token already minted");

        _tokenOwner[tokenId] = to;         // 소유권 부여
        _ownedTokensCount[to].increment(); // 토큰 수량 +1

        emit Transfer(address(0), to, tokenId);
    }
}

여기까지 ERC721 준비는 끝났습니다. 이제 실질적으로 대체 불가능한 무엇인가를 만들어서 tokenId를 만들어 _mint를 호출하여 소유권을 부여해주면 됩니다.

· ERC165 알아가기

ERC165는 해당 스마트 컨트랙트에 특정 인터페이스가 구현되어 있는지 알려줍니다. 이를 인터페이스ID라고 합니다. 인터페이스ID는 methodId들을 XOR 연산한 값 입니다. 이를통해 특정 메서드를 구현했는지 알 수 있습니다.

methodId를 가져오는 방법은 2가지가 있습니다.

contract InterfaceId {

    mapping (address => uint) public balanceOf;
    
    function case2() public pure returns(bytes4, bytes4) {
        return (
            bytes4(keccak256('balanceOf(address)')), // 방법 1. methodId: 0x70a08231
            this.balanceOf.selector                  // 방법 2. methodId: 0x70a08231
        );
    }
}

앞의 코드처럼 methodId를 가져올 수 있습니다. 개인적으론 방법 2를 선호합니다. 방법 1은 오타가 발생하면 버그를 찾기가 너무 난해해지기 때문입니다. 하지만 방법1을 사용할 때가 있습니다. 만약 다음과 같은 함수가 있다고 해봅시다.

function test(uint a) public {}

function test(uint a, uint b) public {}

오버로딩 된 함수(함수명은 동일하지만 함수 인자로 구분)는 .selector를 이용하여 methodId를 가져올 수 없습니다.

ERC165의 모습은 다음과 같습니다.

interface IERC165 {
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

contract ERC165 is IERC165 {
    bytes4 private constant _INTERFACE_ID_ERC165 = 0x01ffc9a7;

    mapping(bytes4 => bool) private _supportedInterfaces;

    constructor ()  {
        _registerInterface(_INTERFACE_ID_ERC165);
    }

    function supportsInterface(bytes4 interfaceId) external view returns (bool) {
        return _supportedInterfaces[interfaceId];
    }

    function _registerInterface(bytes4 interfaceId) internal {
        require(interfaceId != 0xffffffff, "ERC165: invalid interface id");
        _supportedInterfaces[interfaceId] = true;
    }
}

_supportedInterfaces는 methodId(interfaceId)를 키로 가지며, 구현 유무에 따라 true, false를 저장합니다.

    function supportsInterface(bytes4 interfaceId) external view returns (bool) {
        return _supportedInterfaces[interfaceId];
    }

_registerInterface는 methodId가 구현되었다고 저장합니다.

    function _registerInterface(bytes4 interfaceId) internal {
        require(interfaceId != 0xffffffff, "ERC165: invalid interface id");
        _supportedInterfaces[interfaceId] = true;
    }

ERC165를 상속받은 ERC721의 생성자에서 _registerInterface를 호출하여 각 인터페이스가 구현되었다는 사실을 알려줍니다. 실제로는 methodId: true 형태로 저장하고 있는 상태입니다.

// 0x80ac58cd;
bytes4 private constant _INTERFACE_ID_ERC721 = 
      bytes4(keccak256('balanceOf(address)')) ^
      bytes4(keccak256('ownerOf(uint256)')) ^
      bytes4(keccak256('approve(address,uint256)')) ^
      bytes4(keccak256('getApproved(uint256)')) ^
      bytes4(keccak256('setApprovalForAll(address,bool)')) ^
      bytes4(keccak256('isApprovedForAll(address,address)')) ^
      bytes4(keccak256('transferFrom(address,address,uint256)')) ^
      bytes4(keccak256('safeTransferFrom(address,address,uint256)')) ^
      bytes4(keccak256('safeTransferFrom(address,address,uint256,bytes)'));

또는 this.balanceOf.selector를 실행해도 각 메서드에 대한 methodId를 가져올 수 있습니다. this.함수.selector는 해당 함수의 methodId를 가져옵니다.

0x80ac58cd는 ERC721에서 제공하는 모든 함수를 구현했다는 것을 의미합니다.

만약 특정 인터페이스만 구현했다면 ERC721의 생성자(constructor)는 다음과 같이 구현할 수 있습니다.

constructor ()  {
    _registerInterface(bytes4(keccak256('balanceOf(address)')) ^ bytes4(keccak256('ownerOf(uint256)')));
}

인터페이스에서 2개만 구현되었음을 의미합니다. 저는 일반적으로 전체를 xor한것 + 개별적인 methodId도 등록하는 것을 선호합니다.

constructor ()  {
    _registerInterface(bytes4(keccak256('balanceOf(address)')));
    _registerInterface(bytes4(keccak256('ownerOf(uint256)')));
    _registerInterface(bytes4(keccak256('balanceOf(address)')) ^ bytes4(keccak256('ownerOf(uint256)')));
}

이렇게 해야 개별적인 methodId를 supportsInterface()를 호출하여 해당 메서드가 구현되었는지 알 수 있기 때문입니다. 해당 코드는 selector를 이용하여 다음과 같이 수정할 수 있습니다.

constructor ()  {
    _registerInterface(this.balanceOf.selector);
    _registerInterface(this.ownerOf.selector);
    _registerInterface(this.balanceOf.selector ^ this.ownerOf.selector);
}

이렇게 구현하는 이유는 dapp에서 특정 컨트랙트 주소를 받고 해당 컨트랙트 주소가 ERC의 표준을 잘 구현했는지 검사하는 목적으로 사용합니다. 가령 ERC20이라면 transfer 함수가 구현되어 있는지 알 수 있습니다.

물론 실질적인 메서드 구현은 하지 않았지만 해당 methodId를 _registerInterface을 통해 true로 했다면 supportsInterface를 호출하는 dapp은 구현이 되었다고 인식합니다.

interfaceID 간단하게 구하는 방법이 있습니다.

pragma solidity ^0.8.0;

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);

    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

interface IERC721 {
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
    function balanceOf(address owner) external view returns (uint256 balance);
    function ownerOf(uint256 tokenId) external view returns (address owner);
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;
    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;
    function approve(address to, uint256 tokenId) external;
    function getApproved(uint256 tokenId) external view returns (address operator);
    function setApprovalForAll(address operator, bool _approved) external;
    function isApprovedForAll(address owner, address operator) external view returns (bool);
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes calldata data
    ) external;
}


contract ttt  {

    function getInterfaceIDByERC20() public pure returns (bytes4) {
        return type(IERC20).interfaceId;
    }

    function getInterfaceIDByERC721() public pure returns (bytes4) {
        // bytes4 interfaceId = 
        //   this.balanceOf.selector ^
        //   this.ownerOf.selector ^
        //   this.approve.selector ^
        //   this.getApproved.selector ^
        //   this.setApprovalForAll.selector ^
        //   this.isApprovedForAll.selector ^
        //   this.transferFrom.selector ^
        //   this.safeTransferFrom.selector ^
        //   this.safeTransferFrom(address,address,uint256,bytes)')); // 0x80ac58cd
        return type(IERC721).interfaceId; // 0x80ac58cd
    }

}

type 함수에 인터페이스를 전달한후 interfaceId를 통해 interfaceId를 구할 수 있습니다.

· 대체 불가능 품목 구현

우리는 대체 불가능한 품목으로 캐릭터를 만들어 보겠습니다.

contract CharactorByERC721 is ERC721{
    struct Charactor {
        string  name;  // 캐릭터 이름
        uint256 level; // 캐릭터 레벨
    }

    Charactor[] public charactors; // default: [] 
    address public owner;          // 컨트랙트 소유자

    constructor () public {
        owner = msg.sender; 
    }

    modifier isOwner() {
      require(owner == msg.sender);
      _;
    }

    // 캐릭터 생성
    function mint(string memory name, address account) public isOwner {
        uint256 cardId = charactors.length; // 유일한 캐릭터 ID
        charactors.push(Charactor(name, 1));
        _mint(account, cardId); // ERC721 소유권 등록
    }
}

우리는 이름과 레벨 정보를 가진 캐릭터를 생성하여 대체 불가능 토큰화 시키는 코드를 완성했습니다. ERC721를 관리하는 관리자만 캐릭터를 생성할 수 있는 권한을 부여한 코드입니다.

modifier isOwner() {
  require(owner == msg.sender);
  _;
}

해당 코드가 컨트랙트 관리자인지 검사하는 코드입니다.

● 동작 확인

우리는 gnache-cli로 구성한 프라이빗 네트워크에 해당 컨트랙트를 배포한 훈 remix로 테스트를 진행합니다.

· 컨트랙트 배포

 
$ ganache-cli

Ganache CLI v6.12.1 (ganache-core: 2.13.1)

Available Accounts
==================
(0) 0x359b3a8108140E9ef9AA1eE99989BfCb700A7998 (100 ETH)
(1) 0x40758C66149641E955eD0bBbBfE54C728967FC6d (100 ETH)
(2) 0xE5cfe607eD4a37f8F579BDd949693C5a7B470287 (100 ETH)
(3) 0x19E51f231383dF875c01fD24D9C652fD3C84f4d6 (100 ETH)
(4) 0x5b70DD2330DC3e7ABBFEc7421962A58cA2de82c4 (100 ETH)
(5) 0x448543B6d743E276618Daa0dCF948c2507Fb0AC7 (100 ETH)
(6) 0x58EDe0F214690b7eFacd97461f749152255e9C87 (100 ETH)
(7) 0x0bC225A34f03062c677543ccA21397F7F560Bd02 (100 ETH)
(8) 0x1D3278089589ff6103B8aDb807e3332Bf2c6d5d4 (100 ETH)
(9) 0x9BF35fC633e7C3eceF88B3F36082D54AD1559E8B (100 ETH)

Private Keys
==================
(0) 0x64b0ba7f1479b2ae308c16572dffdaaa8f8ebba4a992c5d6892a8d24913c001e
(1) 0x9e3d8c8dc86c2a96ac9b5b58c5f653b5cab1853f0cbf321bc0d99b3cc646e428
(2) 0x408f62d250f1f29b2f196c78583da533f0b7e7c8a097237dbc4dd24a0aee7cbd
(3) 0x88e4e9cd80515c130a59b8e060c7f12fe7f5c94046303b03fe13b48c7df55918
(4) 0x1fbcca603a2643b16e2800e9f7cfefd429a5222bfa8dd9a742d0ef43738f9c11
(5) 0xd0caf806bdac8c56255edc1bd873a93a148383cd826ce701da7092fd6aca552b
(6) 0x6c2442112c56481bb21108eb47b730d5f4c50f8b9aa626a2bad90729a80e08a3
(7) 0x25298a064037bc7f817419058552aa11b4e3a3e1777f8d26b1f5cd444af75e67
(8) 0x6d919bb336e36f515cdbeb895497eb6368a7dbace09054fbb0aeac25c24d752f
(9) 0x170bbe066cd89e1faf6bfb39348a9af9fd927da5ee700ab20521fe0384d86f39

HD Wallet
==================
Mnemonic:      local hope lawn opinion leopard rate volcano pipe opera maximum exit cube
Base HD Path:  m/44'/60'/0'/0/{account_index}

Gas Price
==================
20000000000

Gas Limit
==================
6721975

Call Gas Limit
==================
9007199254740991

Listening on 127.0.0.1:8545

ganache-cli를 이용하여 매우 편안하게 프라이빗 전용 테스트 네트워크 생성이 가능합니다. 해당 네트워크에 우리가 작성한 컨트랙트 코드를 배포해보겠습니다.

Deploy 버튼을 누르면 배포 완료합니다.

우리가 만든 NFT의 기능을 확인할 수 있습니다.

리믹스는 파란색/주황색 버튼으로 조회/트랜잭션 기능을 구분합니다.

▶ 조회

owner 버튼(파란색)으로 해당 컨트랙트의 관리자를 조회할 수 있습니다. 해당 예제에선 컨트랙트 배포자를 관리자로 취급합니다.

balanceOf 버튼(파란색)은 전달한 주소의 보유중인 토큰수량 입니다. 여기선 캐릭터 수량이 됩니다.

charactors 버튼(파란색)은 토큰 아이디를 전달하여 캐릭터 정보를 조회합니다.

ownerOf 버튼(파란색)은 토큰 아이디를 전달하여 소유자 주소를 조회합니다.

getApproved 버튼(파란색)는 특정 토큰 아이디의 전송 권한을 가진 소유자가 아닌 주소를 조회합니다.(approve 버튼으로 설정된 주소입니다.)

▶ transaction

approve 버튼(주황색) 소유권 이외의 전송 권한 설정

mint 버튼(주황색) 생성된 토큰의 소유권 부여

safeTransferFrom, transferFrom 버튼(주황색) 토큰 소유권 이동

transferFrom 코드를 보면 _isApprovedOrOwner() 검사를 통해 호출자가 토큰 아이디의 소유자인지 또는 approve로 등록된 주소인지 검사한 후 진행 유/무를 판단합니다.

· 테스트

다음과 같은 정보로 mint를 호출합니다.

name: mung-charactor
account: 0x359b3a8108140E9ef9AA1eE99989BfCb700A7998

토큰 아이디는 mint를 호출한 순서로 0부터 1씩 증가 합니다.

balanceOf, charactors, ownerOf로 보유중인 토큰수, 정보 조회, 토큰 소유자 조회 결과입니다.

앞에서 만든 토큰을 다음 주소로 전송해보겠습니다.

0x7CA4C0d9d04E2C0F7376f47786C681b0CDbFA64E
from:    소유자 주소 => 0x359b3a8108140E9ef9AA1eE99989BfCb700A7998
to:      수신자 주소 => 0x7CA4C0d9d04E2C0F7376f47786C681b0CDbFA64E
tokenId: 토큰 아이디 => 0

ownerOf와 balanceOf로 해당 토큰 소유권 이동된 걸 확인할 수 있습니다.