Chapter 3. ERC-721 토큰표준 함수

Running to Blockchain·2022년 7월 7일
0

개요

이전 포스팅들에서는 이더리움의 EIP중 Standard Track EIP에 해당하는 ERC에 해당하는 세가지 ERC-20, ERC-721, ERC-1155에 대한 기본 개념을 정리해보고, ERC-20 토큰표준 함수들에 대해 살펴보았다.

Chapter 3에서는 이더리움에서 NFT를 발행할 때 사용되는 표준인 ERC-721 토큰표준에서 제공하는 표준함수들에 대해 살펴보도록 하자.



Chapter 3

ERC-721

ERC-721 표준 콘트랙트는 사용자에게 API를 제공하여, 자신만의 대체 불가능한 토큰(NFT)을 주조하고 메타 데이터(이미지, 설명 등)를 표현할 수 있게 한다.

EIP 공식문서에 따르면, ERC-721와 ERC-20는 요구하는 피라미터부터 차이가 있다. ERC-20은 토큰의 개수(amount)를 주로 파라미터로 다룬다면, ERC-721은 토큰의 소유권(토큰ID, 토큰 소유자) 을 주요 파라미터로 다룬다.

예를들어, transfer함수를 비교해보면, ERC-20의 경우 송신주소(from)에서 수신주소(to)로 지정한 수량(amount)만큼의 토큰을 보내게 되지만, ERC-721의 경우 "권한"과 "tokenId"를 송신주소(from)에서 수신주소(to)로 변경하는 처리를 한다.

또한, ERC-721과 ERC-20을 구분하기 위해서는 ERC-165 자체 검사를 수행하여야 한다.

ERC-165 란,
스마트 컨트랙트가 구현하는 인터페이스를 게시하고 있는지 감지하는 표준 방법이다.

이제 ERC-721에서 사용되는 표준함수들에 대해서 좀 더 자세히 알아보도록 하자.


1. ERC-721 표준 Events

해당 Events들은 아래에서 다룰 "ERC-721 표준 Functions"가 호출될 때 함수에서 호출하는 Event이며, 이 Event들을 통해 실제 블록체인에 데이터를 기록한다고 보면 된다.

1-1. event Transfer

event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId)

1-2. event Approval

event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId)

1-3. event ApprovalForAll

event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved)

2. ERC-721 표준 Functions

2-1. balanceOf

파라미터로 넘어온 주소가 보유하고있는 NFT 토큰의 개수를 조회해주는 함수이다.
_owner가 스타유닛 "드론", 스타유닛 "저글링", 스타유닛 "히드라"를 보유하고 있을 때, owner.balanceOf는 3을 리턴한다.
참고로, 각각의 스타유닛은 대체 불가능한 토큰(NFT)이기 때문에 "드론" 2마리, "저글링" 5마리 이런식으로 보유할 수 없다.

function balanceOf(address _owner) external view virtual override returns (uint256) {
	require(_owner != address(0), "ERC721: balance query for the zero address");
    return _balances[_owner];
}

2-2. ownerOf

파라미터로 넘어온 token id에 해당하는 토큰을 소유하고 있는 주소를 반환한다.
이 함수를 호출할 때 NFT 토큰의 ID를 변수로 넘겨준다.
ownerOf 함수는 token의 id를 파라미터로 받고, 토큰을 소유하고 있는 address를 리턴한다.
예를들어, 스타유닛 "드론"의 token id와 함께 함수를 호출하면 "드론"을 보유하는 address "0x...." 이런식으로 값을 리턴한다.
스타유닛 "드론"은 유일하기 때문에, "드론"을 보유하고 있는 address는 오직 하나밖에 없다.

function ownerOf(uint256 _tokenId) external view virtual override returns (address) {
	address owner = _owners[_tokenId];
    require(owner != address(0), "ERC721: owner query for nonexistent token");
    return owner;
}

2-3. transferFrom

NFT토큰 소유자를 _from에서 _to로 변경한다.

function transferFrom(address _from, address _to, uint256 _tokenId) external payable {
	address addr_owner = ownerOf(_tokenId);
    
    // 인자로 받는 _from이 토큰의 소유 계정과 일치하지 않으면 예외 발생.
    require(addr_owner == _from, "_from is NOT the owner of the token");
    // 인자로 받는 _to가 0(null)이라면 예외 발생.
    require(_to != address(0), "Transfer _to address 0x0");
    
    // 해당 토큰의 allowance address 여부 저장
    address addr_allowed = allowance[_tokenId];
    // 토큰의 본 소유계정이 메소드를 호출한 사람에게 소유권을 이전할 수 있도록 승인을 했는지 여부 저장
    bool isOp = operators[addr_owner][msg.sender];
    
    //msg.sender가 토큰의 소유계정이거나, 토큰의 allowance에 있는 계정이거나, 중개인 계정 true인 경우가 아니라면(세개 모두다 해당 안된다면) 예외 발생.
    require(addr_owner == msg.sender || addr_allowed == msg.sender || isOp, "msg.sender does not transferable token");
    
    // transfer : change the owner of the token
    // 토큰의 주인을 _to 계정으로 변경
    tokenOwners[_tokenId] = _to
    // safematch를 사용해서 balance 감소
    balances[_from] = balances[_from].sub(1);
    // safematch를 사용해서 balance 증가
    balances[_to] = balances[_to].add(1);
    
    // reset approved address
    // ERC-721 표준에 의하면, 이전의 allowance를 갖고있던 계정을 리셋해줘야 한다.
    if (allowance[_tokenId] != address(0)) {
    	// null로..
        delete allowance[_tokenId];
    }
    
    // 이벤트 발생
    emit Transfer(_from, _to, _tokenId);
}

2-4. safeTransferFrom

기능상으로는 transferFrom 함수와 동일하지만, 매개변수로 넘어온 _to 주소가 contract계정인지 체크하는 로직이 추가된다.

두 종류의 safeTransferFrom 함수가 존재한다.
하나는 bytes타입의 인자를 더 받는다.

function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes _data) external payable {
	transferFrom(_from, _to, _tokenId);
    
    // check if _to is ContractAddress
    
    // 토큰을 수신하는 계정(주소)가 Contract라면
    if (_to.isContract()) {
    	bytes4 result = ERC721TokenReceiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data);
        
        // erc165 selector 구하여 일치하지 않으면 예외 발생
        require(
        	result == bytes4(keccak256("onERC721Received(address, address, uint256, bytes)")), "receipt of token is NOT completed"
        )
    }

}

후자의 경우, 실제 유저의 주소(EOA)가 아닌 SmartContract 주소에 보낼 때 사용한다. 해당 스마트컨트랙트가 ERC-721토큰을 받을 수 있는 컨트랙트인지 확인을 하는 것이다.
그래서 transferFrom을 사용할 때, 실제 유저의 주소로 보내는 것은 상관없지만, 스마트컨트랙트 주소에 ERC-721토큰을 보낼 때, 그 스마트컨트랙트가 ERC-721토큰을 받을 수 없는 컨트랙트라면 전송한 토큰이 소실될 수 있음을 유의해야한다.

2-5. approve

ERC-721의 approve함수는 ERC-20의 approve 함수와 동일하다.

ERC-20은 토큰을 전송하는 것 뿐만 아니라, 승인받은 제 3자가 토큰을 전송하는 기능에 대한 표준을 제공한다. 토큰을 대신 전송하는 사람을 "operator"라고 하며, 토큰을 보유하고 있는 사람이 tokenId와 operator의 address를 입력하면 operator에게 해당 토큰에 대한 거래를 허용하게 되는 것이다.

function approve(address _to, uint256 _tokenId) external virtual override {
	address owner = ERC721.ownerOf(_tokenId);
    require(_to != owner, "ERC721: approval to current owner");
    require(_msgSender() == owner || isApprovedForAll(owner, _msgSender()), "ERC721: approve caller is not owner nor approved for all");
    
    _approve(_to, _tokenId);
}

2-6. getApproved

파라미터로 tokenId를 넘겨주면, 그 토큰에 해당하는 operator를 반환해준다.

function getApproved(uint256 _tokenId) external view virtual override returns (address) {
	require(_exists(_tokenId), "ERC721: approved query for nonexistent token");
    
    return _tokenApprovals[_tokenId];
}

2-7. setApprovalForAll

위의 approve함수를 한꺼번에 처리해주는 함수라고 생각하면 된다.
setApprovalForAll 함수는 NFT토큰 소유자가 함수호출시 파라미터로 넘겨주는 주소에게 본인의 모든 NFT토큰에 대한 전송 권한을 부여 또는 해제하는 함수이다.
즉, setApprovalForAll을 호출한 NFT토큰 owner는 자신이 보유한 모든 NFT토큰에 대해 operator에게 전송권한을 부여한다. _approved 변수에 true를 입력하면 모든 토큰에 대한 전송권한을 갖도록, false를 입력하면 모든 토큰에 대한 전송권한을 취소하도록 한다.

function setApprovalForAll(address _operator, bool _approved) external virtual override {
	require(_operator != _msgSender(), "ERC721: approve to caller");
    
    _operatorApprovals[_msgSender()][_operator] = _approved;
    
    // 이벤트 호출
    emit ApprovalForAll(_msgSender(), _operator, _approved);
}

2-8. isApprovedForAll

isApprovedForAll함수는 setApprovalForAll의 권한이 있는지 bool의 형태로 리턴해주는 함수이다.
함수 호출시 파라미터로는 토큰 owner의 address와, operator의 address를 입력한다.

function isApprovedForAll(address owner, address operator) external view virtual override returns (bool) {
	return _operatorApprovals[owner][operator]
}

3. 메타데이터 관리 함수

3-1. name

function name() external view returns (string _name);

3-2. symbol

function symbol() external view returns (string _symbol);

3-3. tokenURI

function tokenURI(uint256 _tokenId) external view returns (string);

4. Wallet Interface

ERC-721 토큰을 받으려는 스마트컨트랙트의 주소는 아래의 인터페이스들을 필수적으로 구현해야한다.
다시말해서 지갑, 옥션, 중매 관련 앱은 반드시 아래의 인터페이스들을 갖고있어야만 한다.

/// @dev Note: the ERC-165 identifier for this interface is 0x150b7a02.
interface ERC721TokenReceiver {
    /**
     * @notice NFT 수신 처리
     * @dev ERC721 스마트 컨트랙트는 `safeTransfer` 후 수신자가 구현한
     * 이 함수를 호출합니다. 이 함수는 반드시 함수 선택자를 반환해야 하며,
     * 그렇지 않을 경우 호출자는 트랜잭션을 번복할 것입니다. 반환될 선택자는
     * `this.onERC721Received.selector`로 얻을 수 있습니다. 이 함수는
     * 전송을 번복하거나 거절하기 위해 예외를 발생시킬 수도 있습니다.
     * 참고: ERC721 컨트랙트 주소는 항상 메시지 발신자입니다.
     * @param _operator `safeTransferFrom` 함수를 호출한 주소
     * @param _from 이전에 토큰을 소유한 주소
     * @param _tokenId 전송하고자 하는 NFT 식별자
     * @param _data 특별한 형식이 없는 추가적인 데이터
     * @return bytes4 `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`
     */
    function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4);
}

5.Metadata Extention

메타데이터(NFT에 대한 정보)를 관리할 때에는 아래의 인터페이스를 사용한다.

/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension
/// @dev See https://eips.ethereum.org/EIPS/eip-721
///  Note: the ERC-165 identifier for this interface is 0x5b5e139f.
interface ERC721Metadata /* is ERC721 */ {
    /// @notice A descriptive name for a collection of NFTs in this contract
    function name() external view returns (string _name);

    /// @notice An abbreviated name for NFTs in this contract
    function symbol() external view returns (string _symbol);

    /// @notice A distinct Uniform Resource Identifier (URI) for a given asset.
    /// @dev Throws if `_tokenId` is not a valid NFT. URIs are defined in RFC
    ///  3986. The URI may point to a JSON file that conforms to the "ERC721
    ///  Metadata JSON Schema".
    function tokenURI(uint256 _tokenId) external view returns (string);
}

아래는 ERC-721 Metadata Sample JSON Schema 이다.

{
    "title": "Asset Metadata",
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "description": "Identifies the asset to which this NFT represents"
        },
        "description": {
            "type": "string",
            "description": "Describes the asset to which this NFT represents"
        },
        "image": {
            "type": "string",
            "description": "A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."
        }
    }
}

6. Enumeration Extension (선택사항)

모든 NFT 리스트를 퍼블리시 하거나 탐색할 수 있게 해준다.

/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension
/// @dev See https://eips.ethereum.org/EIPS/eip-721
///  Note: the ERC-165 identifier for this interface is 0x780e9d63.
interface ERC721Enumerable /* is ERC721 */ {
    /// @notice Count NFTs tracked by this contract
    /// @return A count of valid NFTs tracked by this contract, where each one of
    ///  them has an assigned and queryable owner not equal to the zero address
    function totalSupply() external view returns (uint256);

    /// @notice Enumerate valid NFTs
    /// @dev Throws if `_index` >= `totalSupply()`.
    /// @param _index A counter less than `totalSupply()`
    /// @return The token identifier for the `_index`th NFT,
    ///  (sort order not specified)
    function tokenByIndex(uint256 _index) external view returns (uint256);

    /// @notice Enumerate NFTs assigned to an owner
    /// @dev Throws if `_index` >= `balanceOf(_owner)` or if
    ///  `_owner` is the zero address, representing invalid NFTs.
    /// @param _owner An address where we are interested in NFTs owned by them
    /// @param _index A counter less than `balanceOf(_owner)`
    /// @return The token identifier for the `_index`th NFT assigned to `_owner`,
    ///   (sort order not specified)
    function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);
}

참고링크
https://ethereum.org/ko/developers/docs/standards/tokens/erc-721/
https://earthteacher.tistory.com/94

profile
블록체인에 대해 같이 공부하는 채널입니다~

0개의 댓글