본문 바로가기
Blockchain/Ethereum

[Ethereum] ERC-721 소스 분석(2) - 토큰

by AustinProd 2022. 3. 24.

이전 포스트에서 공유한 ERC-721 인터페이스에 이어서 해당 인터페이스들을 구현한 ERC-721 토큰 컨트렉트를 공유한다. 이 소스는 깃허브의 ERC-721 오픈 소스를 참고하여 작성하였다.

 

해당 오픈 소스 코드는 ERC-721 규칙을 잘 따르고 있고, 개발자 입장에서 확장성 있는 코드를 추가하기에 적합하다 판단하해 참조 자료로 선택하게되었다.

 

ERC-721 주요 기능

이 컨트렉트 코드에서 구현된 기능은 다음과 같다.

 

  • 주소 및 토큰 유효성 검증
  • 에러 핸들링
  • 토큰 소유 관계 매핑
  • 토큰 권한 관계 매핑
  • 소유자와 오퍼레이터 관계 매핑
  • 토큰 전송 - 일반 전송(transfer)과 Receiver 인터페이스이 포함된 안전 전송(safeTransfer)
  • 권한(Approval) 관리
  • 토큰 발행
  • 토큰 소각
  • 자산 확인

코드에 대한 해석은 모두 주석에 첨부하였다. 보다 상세한 이해가 필요하다고 생각된다면 OpenZeppeline을 참조하면 좋다.

 

ERC-721 구현 코드

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

// Import
import "./ERC721.sol"; // ERC721 인터페이스
import "./ERC721TokenReceiver.sol"; // ERC721TokenReceiver 인터페이스
import "./SupportsInterface.sol"; // SupportsInterface 컨트렉트
import "./AddressUtils.sol"; // AddressUtils 라이브러리

// ERC-721 인터페이스를 구현해만든 NFT 토큰
contract NFToken is ERC721, SupportsInterface {

  /********************************* 라이브러리 *********************************/
  // 주소 검증 라이브러리
  using AddressUtils for address;


  /********************************* 에러 코드 *********************************/
  // 에러 메세지 코드 상수
  string constant ZERO_ADDRESS = "003001";
  string constant NOT_VALID_NFT = "003002";
  string constant NOT_OWNER_OR_OPERATOR = "003003";
  string constant NOT_OWNER_APPROVED_OR_OPERATOR = "003004";
  string constant NOT_ABLE_TO_RECEIVE_NFT = "003005";
  string constant NFT_ALREADY_EXISTS = "003006";
  string constant NOT_OWNER = "003007";
  string constant IS_OWNER = "003008";


  /********************************* Receiver 상수 *********************************/
  // Receiver 인터페이스 식별자(= Magic Value) -> 지갑, 옥션 등 NFT 토큰을 받는 서비스 운영 가능
  bytes4 internal constant MAGIC_ON_ERC721_RECEIVED = 0x150b7a02; // = bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))


  /********************************* Mapping *********************************/
  // [매핑] NFT 토큰 아이디 -> 소유자 주소
  mapping (uint256 => address) internal idToOwner;

  // [매핑] NFT 토큰 아이디 -> 권한이 있는 어카운트 주소
  mapping (uint256 => address) internal idToApproval;

  // [매핑] 소유자 주소 -> 보유 NFT 수량
  mapping (address => uint256) private ownerToNFTokenCount;

  // [이중 매핑] 소유자 주소 -> 오퍼레이터 주소 -> 관리 권한 (Operator = 토큰은 대신 관리/전송 할 수 있는 주소)
  mapping (address => mapping (address => bool)) internal ownerToOperators;


  /********************************* Modifier *********************************/
  /**
   * @dev 토큰 관리(operate) 권한 검사(msg.sender = 소유자 혹은 오퍼레이터)
   * @param _tokenId : NFT 토큰 ID
   */
  modifier canOperate(uint256 _tokenId) {
    address tokenOwner = idToOwner[_tokenId];
    require(
      tokenOwner == msg.sender || ownerToOperators[tokenOwner][msg.sender],
      NOT_OWNER_OR_OPERATOR
    );
    _;
  }

  /*
   * @dev 토큰 전송(transfer) 권한 검사(msg.sender = 소유자 혹은 오퍼레이터)
   * @param _tokenId : NFT 토큰 ID
   */
  modifier canTransfer(uint256 _tokenId) {
    address tokenOwner = idToOwner[_tokenId];
    require(
      tokenOwner == msg.sender
      || idToApproval[_tokenId] == msg.sender
      || ownerToOperators[tokenOwner][msg.sender],
      NOT_OWNER_APPROVED_OR_OPERATOR
    );
    _;
  }

  /**
   * @dev NFT 토큰 ID 유효성 검사
   * @param _tokenId : NFT 토큰 ID
   */
  modifier validNFToken(uint256 _tokenId) {
    require(idToOwner[_tokenId] != address(0), NOT_VALID_NFT);
    _;
  }

  /********************************* 생성자 *********************************/
  // ERC-721 인터페이스 구현 확인
  constructor() {
    supportedInterfaces[0x80ac58cd] = true; // = ERC-721
  }

  /********************************* 함수 *********************************/
  /*
   * @dev 컨트렉트 어카운트(CA)에 NFT 토큰 전송 함수
   *
   * @error
   *  1. msg.sende != 토큰 소유자(Onwer), 오퍼레이터(Operator), 혹은 권한 있는 주소(Approved Address)
   *  2. _from != 토큰 소유자(Onwer)
   *  3. _to == 공백 주소
   *  4. 유효하지 않은 _tokenId
   *  5. 토큰 전송이 완료 후 유효성 검증 실패
   *    -> 토큰 전송이 완료되면, 해당 함수는 토큰을 받는 주소(_to)에 대한 유효성 검증(Code Size > 0)
   *    -> _to 주소의 onERC721Received 반환값 != `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))`
   *
   * @param _from : NFT 소유자 주소
   * @param _to : NFT 전송 주소
   * @param _tokenId : 전송할 토큰 ID
   * @param _data : Call Message로 전송될 추가 데이터 (정해진 형식 X) -> CA에 전송할 때 필요
   */
  function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata _data) external override payable {
    _safeTransferFrom(_from, _to, _tokenId, _data); // private 토큰 전송 함수
  }

  /*
   * @dev NFT 토큰 전송 함수
   *
   * @notice 위 safeTransferFrom와 동일. 4번째 인자값이 없으며, 이 파라미터는 공백("")으로 전달
   *
   * @error
   *   1. msg.sende != 토큰 소유자(Onwer), 오퍼레이터(Operator), 혹은 권한 있는 주소(Approved Address)
   *  2. _from != 토큰 소유자(Onwer)
   *  3. _to == 공백 주소
   *  4. 유효하지 않은 _tokenId
   *
   * @param _from : NFT 소유자 주소
   * @param _to : NFT 전송 주소
   * @param _tokenId : 전송할 토큰 ID
   */
  function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable override {
    _safeTransferFrom(_from, _to, _tokenId, ""); // private 토큰 전송 함수
  }

  /*
   * @dev NFT 토큰 전송 함수
   *
   * @notice 함수를 호출하는 사람은 NFT를 받는 주소(_to)가 유효한지 검증 필요(토큰이 소실될 수 있음)
   *
   * @error
   *  1. msg.sender != 토큰 소유자(Onwer), 오퍼레이터(Operator), 혹은 권한 있는 주소(Approved Address)
   *  2. _from != 토큰 소유자(Onwer)
   *  3. _to == 공백 주소
   *  4. 유효하지 않은 _tokenId
   *
   * @param _from : NFT 소유자 주소
   * @param _to : NFT 전송 주소
   * @param _tokenId : 전송할 토큰 ID
   */
  function transferFrom(address _from, address _to, uint256 _tokenId) external payable override canTransfer(_tokenId) validNFToken(_tokenId) {
    address tokenOwner = idToOwner[_tokenId]; // 토큰 소유자 주소
    require(tokenOwner == _from, NOT_OWNER); // _form == 소유자
    require(_to != address(0), ZERO_ADDRESS); // _to != 공백주소

    _transfer(_to, _tokenId); // private 토큰 전송 함수
  }

  /*
   * @notice 주소값이 없으면(= 0), 권한이 없는 주소로 간주
   *
   * @dev NFT 토큰에 대한 권한 설정 및 재확인 (필요하면 Payable 함수로 변경 가능)
   *
   * @error -> msg.sender != 토큰 소유자(Onwer), 오퍼레이터(Operator)
   *
   * @param _approved : NFT 권한이 주어질 주소
   * @param _tokenId : 권한이 설정될 토큰 ID
   */
  function approve(address _approved, uint256 _tokenId) external payable override canOperate(_tokenId) validNFToken(_tokenId) {
    address tokenOwner = idToOwner[_tokenId]; // 토큰 소유자
    require(_approved != tokenOwner, IS_OWNER); // 권한 설정 주소 != 토큰 소유자

    idToApproval[_tokenId] = _approved; // 권한 매핑

    emit Approval(tokenOwner, _approved, _tokenId); // Event 발송(-> 프론트)
  }

  /*
   * @dev 제 3자(operator)에 대한 권한(approval) 설정
   *
   * @notice msg.sender가 보유한 토큰이 없더라도 설정 가능
   *
   * @param _operator : 오퍼레이터 리스트에 추가할 주소
   * @param _approved : 오퍼레이터 권한(approved) 설정(true/false)
   */
  function setApprovalForAll(address _operator, bool _approved) external override {
    ownerToOperators[msg.sender][_operator] = _approved; // 권한 설정(-> true/false)

    emit ApprovalForAll(msg.sender, _operator, _approved); // Event 발송(-> 프론트)
  }

  /*
   * @dev 사용자 주소가 보유한 토큰 수량 반환
   *
   * @error -> 토큰 보유한 주소 == 0
   *
   * @param _owner : NFT 소유자 주소
   * @return : 주소가 보유한 NFT 수량
   */
  function balanceOf(address _owner) external override view returns (uint256) {
    require(_owner != address(0), ZERO_ADDRESS); // 주소 != 0
    return _getOwnerNFTCount(_owner);
  }

  /*
   * @dev 토큰을 보유 주소 반환
   *
   * @error -> 토큰 보유한 주소 == 0
   *
   * @param _tokenId :  토큰 ID
   * @return _owner : 토큰 보유자 주소
   */
  function ownerOf(uint256 _tokenId) external override view returns (address _owner) {
    _owner = idToOwner[_tokenId];
    require(_owner != address(0), NOT_VALID_NFT);
  }

  /*
   * @dev NFT 토큰의 권한 주소 반환(토큰 ID가 유효해야함)
   *
   * @param _tokenId ID of the NFT to query the approval of.
   * @return Address that _tokenId is approved for.
   */
  function getApproved(uint256 _tokenId) external override view validNFToken(_tokenId) returns (address) {
    return idToApproval[_tokenId];
  }

  /*
   * @dev 토큰 보유자와 매핑된 오퍼레이터 권한 반환
   *
   * @param _owner : 토큰 소유자 주소
   * @param _operator : 오퍼레이터 주소
   * @return 권한 설정 반환(true/false)
   */
  function isApprovedForAll(address _owner, address _operator) external override view returns (bool) {
    return ownerToOperators[_owner][_operator];
  }

  /*
   * @dev [Internal] 실제 토큰 전송 함수(별도 체크 로직 없음)
   *
   * @param _to 토큰 받을 주소
   * @param _tokenId : 전송할 토큰
   */
  function  _transfer(address _to, uint256 _tokenId) internal virtual {
    address from = idToOwner[_tokenId]; // 토큰 소유자 주소
    _clearApproval(_tokenId); // 해당 토큰 권한 초기화(delete)

    _removeNFToken(from, _tokenId); // 토큰 소유자의 자산에서 토큰 보유 기록 삭제
    _addNFToken(_to, _tokenId); // 토큰에 대한 소유권 이전(+ 받는 사람 자산 추가)

    emit Transfer(from, _to, _tokenId); // Event 발송(-> 프론트)
  }

  /*
   * @dev [Internal] NFT 발행
   *
   * @param _to : 발행된 토큰이 매핑될 주소
   * @param _tokenId : msg.sender로 부터 발행된 토큰 ID
   */
  function _mint(address _to, uint256 _tokenId) internal virtual {
    require(_to != address(0), ZERO_ADDRESS); // 발행 받을 주소 유효성 검사(!=0)
    require(idToOwner[_tokenId] == address(0), NFT_ALREADY_EXISTS); // 등록된 토큰 ID인지 중복 검사

    _addNFToken(_to, _tokenId); // 토큰 등록

    emit Transfer(address(0), _to, _tokenId); // Event 발송(-> 프론트)
  }

  /*
   * @dev [Internal] NFT 토큰 소각(소각된 토큰 ID는 다시 발행될 수 있음)
   *
   * @error -> 유효하지 않은 토큰 ID
   *
   * @param _tokenId : 소각할 토큰 ID
   */
  function _burn(uint256 _tokenId) internal virtual validNFToken(_tokenId) {
    address tokenOwner = idToOwner[_tokenId]; // 토큰 소유자 주소
    _clearApproval(_tokenId); // 권한 초기화
    _removeNFToken(tokenOwner, _tokenId); // 토큰 소각

    emit Transfer(tokenOwner, address(0), _tokenId); // Event 발송(-> 프론트)
  }

  /*
   * @dev [Internal] 등록된 토큰 소유 정보 초기화
   *
   * @param _from : 토큰 등록 정보를 삭제할 주소(토큰 전소유자)
   * @param _tokenId : 토큰 ID
   */
  function _removeNFToken(address _from, uint256 _tokenId) internal virtual {
    require(idToOwner[_tokenId] == _from, NOT_OWNER); // 토큰에 등록된 소유자 주소 == 토큰 소유자
    ownerToNFTokenCount[_from] -= 1; // 자산 차감

    delete idToOwner[_tokenId]; // 토큰 삭제
  }

  /*
   * @dev [Internal] 토큰 할당
   * @param _to : 토큰이 새로이 할당될 주소(토큰 현소유자)
   * @param _tokenId : 토큰 ID
   */
  function _addNFToken(address _to, uint256 _tokenId) internal virtual {
    require(idToOwner[_tokenId] == address(0), NFT_ALREADY_EXISTS); // 토큰 소유자가 없을 것

    idToOwner[_tokenId] = _to; // 토큰에 소유자 등록
    ownerToNFTokenCount[_to] += 1; // 자산 추가
  }

  /*
   * @dev [Internal] 특정 주소의 보유 NFT 수량 확인.  This is needed for overriding in enumerable
   * extension to remove double storage (gas optimization) of owner NFT count.
   *
   * @param _owner : NFT 자산을 확인할 주소
   * @return 보유한 NFT 수량
   */
  function _getOwnerNFTCount(address _owner) internal virtual view returns (uint256) {
    return ownerToNFTokenCount[_owner];
  }

  /*
   * @notice 4번째 파라미터로 전달받는 Call-data는 전송할 주소에 대한 분기(
   *  -> 없을 경우("") = EOA에 전송
   *  -> 있을 경우 = CA에 전송
   *
   * @dev [Private] EOA 혹은 CA로 토큰 전송(safeTransferFrom에서 호출)
   *
   * @param _from : 토큰 소유자 주소(전소유자)
   * @param _to : 토큰을 전송받을 주소(현소유자)
   * @param _tokenId : 토큰 ID
   * @param _data : Call Message로 전송될 추가 데이터 (정해진 형식 X) -> CA에 전송할 때 필요, EOA에 전송할 때는 공백("")
   */
  function _safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes memory _data) private canTransfer(_tokenId) validNFToken(_tokenId) {
    address tokenOwner = idToOwner[_tokenId]; // 토큰에 등록된 토큰 소유자(Owner)
    require(tokenOwner == _from, NOT_OWNER); // _from == 토큰 소유자(Owner)
    require(_to != address(0), ZERO_ADDRESS); // _to 주소 != 0

    _transfer(_to, _tokenId); // 토큰 전송

    // ERC-721 Receiver 인터페이스 검증
    if (_to.isContract()) {
      bytes4 retval = ERC721TokenReceiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data);
      require(retval == MAGIC_ON_ERC721_RECEIVED, NOT_ABLE_TO_RECEIVE_NFT);
    }
  }

  /*
   * @dev [Private] 토큰 ID에 매핑된 권한 초기화(삭제)
   *
   * @param _tokenId : 토큰 ID
   */
  function _clearApproval(uint256 _tokenId) private {
    delete idToApproval[_tokenId]; // 권한 삭제
  }
}

 

댓글