ERC165

CHOYEAH·2023년 10월 23일
0
post-thumbnail

ERC165


ERC165의 목적은 컨트랙이 어떤 인터페이스를 상속받고있나 확인하는 기능을 제공한다.

interface ERC165 {
    /// @notice Query if a contract implements an interface
    /// @param interfaceID The interface identifier, as specified in ERC-165
    /// @dev Interface identification is specified in ERC-165. This function
    ///  uses less than 30,000 gas.
    /// @return `true` if the contract implements `interfaceID` and
    ///  `interfaceID` is not 0xffffffff, `false` otherwise
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
  • 인터페이스 아이디를 인자로 전달하면 여부를 boolean으로 리턴한다.

  • 인터페이스 아이디는 식별자이다.

  • 식별자는 byte4 타입이며 magic value라고 부른다.

  • 인터페이스마다 유니크한 식별자가 정해져있다.

  • 식별자 구하는 방법은 다음과 같다.

    bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))

0x150b7a02와 같은 결과값은 함수명인 onERC721Received와 매개변수 타입들 address, address, uint256, bytes 들을 해시하여 4바이트로 자른 값이다. (keccak256()는 해시 함수이다.)
이렇게 리턴되는 매직 밸류를 함수 시그니처라고 부른다.

인터페이스 식별자 구하는 방법


bytes4(keccak256("balanceOf(address)")) ^
bytes4(keccak256("ownerOf(uint256)")) ^
bytes4(keccak256("transferFrom(address,address,uint256)")) ^
bytes4(keccak256("safeTransferFrom(address,address,uint256)")) ^
bytes4(keccak256("safeTransferFrom(address,address,uint256,bytes)")) ^
bytes4(keccak256("approve(address,uint256)")) ^
bytes4(keccak256("getApproved(uint256)")) ^
bytes4(keccak256("setApprovalForAll(address,bool)")) ^
bytes4(keccak256("isApprovedForAll(address,address)")) ^
bytes4(keccak256("isContract(address)"))

인터페이스 함수들을 각각 해싱, bytes4로 잘라내여 함수 시그니처 값을 구한 후 모두 XOR한다.
ERC721 시그니처 결과 = 0x8ac58cd

supportsInterface 구현


contract ERC721Implementation is ERC721 {
    
    // 인터페이스 식별자를 키로 불리언을 리턴한다. 즉, ERC721Implementation이 특정 인터페이스를 쓰는지 확인할때 사용된다.
    mapping (bytes4 => bool) supportedInterface;

		// 생성자에서 ERC721 식별자를 맵핑에 저장
    constructor() public {
        supportedInterface[0x80ac58cd] = true;
    }

		// 외부에서 이 함수를 호출(ERC721 식별자를 인자로)하여 ERC721을 상속받았는지 확인 가능
		function supportsInterface(bytes4 interfaceID) public view returns (bool){
        return supportedInterface[interfaceID];
    }

...

전체코드

pragma solidity >=0.4.24 <=0.5.6;

/// @title ERC-721 Non-Fungible Token Standard
/// @dev See https://eips.ethereum.org/EIPS/eip-721
///  Note: the ERC-165 identifier for this interface is 0x80ac58cd.
interface ERC721 /* is ERC165 */ {
   
    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 calldata data) external ;
    function safeTransferFrom(address _from, address _to, uint256 _tokenId) external ;
    function transferFrom(address _from, address _to, uint256 _tokenId) external ;
    function approve(address _approved, uint256 _tokenId) external ;
    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);
}

interface ERC721TokenReceiver {
    function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes calldata _data) external returns(bytes4);
}

interface ERC165 {
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

contract ERC721Implementation is ERC721 {
    
    // 토큰의 아이디를 키값으로 해당 주소를 리턴 
    mapping (uint256 => address) tokenOwner;
    // 계정 주소에 해당하는 토큰 수를 리턴
    mapping (address => uint256) owendTokensCount;
    mapping (uint256 => address) tokenApprovals;
    // 누가 누구에게 권한 부여를 했는가에 대한 맵핑, 1:N으로 권한을 부여할 수 있음.
    mapping (address => mapping (address => bool)) operatorApprovals;
    // 인터페이스 식별자를 키로 불리언을 리턴한다. 즉, ERC721Implementation이 특정 인터페이스를 쓰는지 확인할때 사용된다.
    mapping (bytes4 => bool) supportedInterface;

    constructor() public {
        supportedInterface[0x80ac58cd] = true;
    }

    // 토큰 발행
    function mint(
        address _to, // 발행된 토큰의 소유자
        uint _tokenId // 몇 번째 토큰인지
        ) public {
            tokenOwner[_tokenId] = _to;
            owendTokensCount[_to] += 1; // 토큰 보유 개수 1 증가시킴
    }

    // 특정 계정의 토큰 수를 리턴
    function balanceOf(address _owner) public view returns (uint256) {
        return owendTokensCount[_owner];
    }

    // 토큰의 주인 주소를 리턴
    function ownerOf(uint256 _tokenId) public view returns (address) {
        return tokenOwner[_tokenId];
    }

    // 토큰 전송
    function transferFrom(address _from, address _to, uint256 _tokenId) public {
        // 이 함수를 호출한 계정이 _tokenId의 소유자인지 검사
        address owner = ownerOf(_tokenId);
        // 1.토큰의 소유자 계정이거나 2.특정 토큰에 권한이 있는 계정이 접근했을때 3.소유자의 전체 토큰의 전송 권한을 가진  계정이 호출했을때
        require(msg.sender == owner || getApproved(_tokenId) == msg.sender || isApprovedForAll(owner, msg.sender));
        // 빈 인자 검사 
        require(_from != address(0)); // address(0)는 비어있다는 의미
        require(_to != address(0));
        
        // 보내는 측의 토큰 보유 수량을 1차감
        owendTokensCount[_from] -= 1;
        // 기존 토큰 보유자의 토큰 소유 정보를 제거
        tokenOwner[_tokenId] = address(0);
        // 전달받는 측의 토큰 보유 수량을 1증가
        owendTokensCount[_to] += 1;
        // 전달받는 측에 토큰 소유정보 추가 
        tokenOwner[_tokenId] = _to;     
    }

    // 토큰 안전 정송
    function safeTransferFrom(address _from, address _to, uint256 _tokenId) public {
         transferFrom(_from, _to, _tokenId); 
         if(isContract(_to)){
            bytes4 returnValue = ERC721TokenReceiver(_to).onERC721Received(msg.sender, _from, _tokenId, '');
            require(returnValue == 0x150b7a02);
         }
    }

    // 토큰 안전 전송2
    function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes memory data) public {
        transferFrom(_from, _to, _tokenId); 
        if(isContract(_to)){
            bytes4 returnValue = ERC721TokenReceiver(_to).onERC721Received(msg.sender, _from, _tokenId, data);
            require(returnValue == 0x150b7a02);
        }

    }

    function approve(address _approved, uint256 _tokenId) public {
        // 권한을 받게되는 계정은 토큰 오너가 아니여야 한다.
        address owner = ownerOf(_tokenId);
        require(_approved != owner);
        // 토큰 주인이 호출해야함 
        require(msg.sender == owner);
        tokenApprovals[_tokenId] = _approved;
    }
    
    function getApproved(uint256 _tokenId) public view returns (address) {
        return tokenApprovals[_tokenId];
    }

    function setApprovalForAll(address _operator, bool _approved) external {
        // _operator: 계정이 소유한 모든 토큰들을 대신 운영할 계정
        // _approved: 권한 부여 여부
        require(_operator != msg.sender);
        operatorApprovals[msg.sender][_operator] = _approved;
    }
    
    function isApprovedForAll(address _owner, address _operator) public view returns (bool) {
        return operatorApprovals[_owner][_operator];
    }

    // 외부에서 이 함수를 호출(ERC721 식별자를 인자로)하여 ERC721을 상속받았는지 확인 가능
    function supportsInterface(bytes4 interfaceID) public view returns (bool){
        return supportedInterface[interfaceID];
    }

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

contract Auction is ERC721TokenReceiver {
    function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes memory _data) public returns(bytes4) {
        return bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"));
    }
}

테스트


  1. 테스트를 위해 특정 컨트랙에 인터페이스 상속을 확인하는 함수를 만든다.
contract Auction is ERC721TokenReceiver {
    function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes memory _data) public returns(bytes4) {
        return bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"));
    }

    function checkSupportsInterface(address _to, bytes4 interfaceID) public view returns (bool) {
        return ERC721Implementation(_to).supportsInterface(interfaceID);
    } 
}
  1. Auction, ERC721Implementation 컴파일 및 배포
  2. Auction 컨트랙트의 checkSupportsInterface(ERC721Implementation 컨트랙트 주소, 0x80ac58cd) 호출
  3. 결과값이 true인지 확인

정리


ERC 표준은 여러가지가 있고 각 표준들은 인터페이스 형태로 정의되어있다.

컨트랙의 주소만 가지고 그 컨트랙트가 어떤 표준을 구현했는지 알수가 없기 때문에

그 부분을 알려주기 위해 ERC165의 supportsInterface() 함수를 사용하는것이다.

profile
Move fast & break things

0개의 댓글