이전 포스트에서 공유한 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]; // 권한 삭제
}
}
'Blockchain > Ethereum' 카테고리의 다른 글
[Ethereum] ERC-721 소스 분석(4) - Enumerable (0) | 2022.03.25 |
---|---|
[Ethereum] ERC-721 소스 분석(3) - 메타 데이터 (0) | 2022.03.25 |
[Ethereum] ERC-721 소스 분석(1) - 인터페이스 (0) | 2022.03.23 |
[Ethereum] ERC-721 (0) | 2022.03.23 |
[Ethereum] ERC-20 (0) | 2022.03.23 |
댓글