카테고리 없음

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

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

지난 시간에 우리는 NFT의 기본 개념과 ERC721을 이용하여 구현해보았습니다.

2022.08.28 - [블록체인] - [ethereum] ERC721을 이용한 NFT 만들기 - 1편

 

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

ERC721 기반의 NFT를 만들어 본 후 가볍게 실습을 진행해보는 시간을 가져보도록 하겠습니다. ​ · NFT란? 최소한 NFT가 무엇인지부터 살펴보자. NFT는 대체 불가능한 토큰을 의미합니다. 여기서 대체

meongae.tistory.com

1편에서 작성한 코드를 추상화하여 좀 더 클린한 형태로 발전시켜 보겠습니다.

우리가 구현한 실제 비즈니스 로직이 포함된 코드는 다음과 같습니다.

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 컨트랙트 인터페이스로 제공하지 않는 _mint()를 추가하였는데 이를 확장하고 우리가 개발에 집중할 수 있는 형태로 제공해보겠습니다.

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();
    emit Transfer(address(0), to, tokenId);
}

자 먼저 _mint에서 접근하는 녀석들을 알아보아야 합니다. _tokenOwner, _ownedTokensCount, _exists를 접근하고 있습니다.

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

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

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

이를 상속받은 컨트랙트에서 접근할 수 있도로 접근자를 수정합니다. 접근자는 private에서 interval로 바꿔줍니다.

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

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

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

· 수정된 ERC721 컨트랙트 코드

contract ERC721 is ERC165, IERC721 {
    using SafeMath for uint256;
    using Address for address;
    using Counters for Counters.Counter;

    bytes4 private constant _ERC721_RECEIVED = 0x150b7a02;

    mapping (uint256 => address) internal _tokenOwner;

    mapping (uint256 => address) internal _tokenApprovals;

    mapping (address => Counters.Counter) internal _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 {
        //solhint-disable-next-line max-line-length
        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);
        }
    }
}

https://github.com/Exchange-M/NFT-ERC721/blob/master/ERC721.sol

 

이제 우리는 깃허브에 배포한 파일을 가져와서 ERC721의 _mint를 구현한 후 비즈니스 로직에 집중할 수 있습니다. 기능 여하에 따라 코드를 개선한 후 올려도 됩니다.

· 개선코드 적용

pragma solidity ^0.5.0;

import 'https://github.com/Exchange-M/NFT-ERC721/blob/master/ERC721.sol';

contract ERC721Impl is ERC721 {
    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();

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

contract CharactorByERC721 is ERC721Impl{
    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 컨트랙트에 있는 메서드들 중에 _burn 또한 ERC721의 기본 인터페이스가 아니므로 상속받은 컨트랙트에서 구성할 수 있습니다.

정상적으로 배포된 모습을 확인할 수 있습니다.