///
Search
🚐

(220401) Klaytn IDE를 활용한 ERC-721 안전 전송

참고문헌

이전에 포스팅 했던 내용에서 safeTransferFrom 함수는 말그대로 토큰을 안전하게 보내는 함수이다.
안전하게 토큰을 보내는 의미는 즉 그동안은 안전하지 못했다라는 것을 의미한다.
이전에는 어떻게 토큰을 보냈을까?
ERC721이 나오기 ERC20토큰이 있었는데 문제점은 ERC20토큰을 컨트랙 주소로 보냈을때 분실하는 경우가 많았다.
토큰을 교환하는 방식은 월렛과 월렛사이를 주고받을 수 있는 일반계정 방식이 있고 두번째는 컨트랙 계정으로 보내는 방식이 있다.
컨트랙을 배포했을때 나오는 주소를 계정이라고도 하는데, 일반계정들끼리는 토큰이 분실되는 위험이 적지만 컨트랙 계정으로 전송했을때 만약 그 컨트랙에 토큰을 다루는 기능이 없다면 토큰이 사라지게 된다.
이 문제를 해결하기위해 ERC721에서 safeTransferFrom()이라는 함수가 나오게 되었다.
safeTransferFrom()함수는 이전에 배웠던 transeferFrom()함수와 로직은 똑같다.
한가지 다른점은 safeTransferFrom()함수에 토큰을 받게되는 _to계정이 컨트랙계정일 경우에 해당 컨트랙이 ERC721호환성이 있는지 체크를 해줄수 있다.

실습

지난 코드
pragma solidity >=0.4.24 <=0.5.6; interface ERC721 { 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) public view returns (uint256); function ownerOf(uint256 _tokenId) public view returns (address); //function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) public; // function safeTransferFrom(address _from, address _to, uint256 _tokenId) public; function transferFrom(address _from, address _to, uint256 _tokenId) public; // function approve(address _approved, uint256 _tokenId) public; //function setApprovalForAll(address _operator, bool _approved) public; // function getApproved(uint256 _tokenId) public view returns (address); // function isApprovedForAll(address _owner, address _operator) public view returns (bool); } contract ERC721Implementation is ERC721{ mapping(uint256 => address) tokenOwner; mapping(address => uint256) ownedTokensCount; function mint(address _to, uint _tokenId) public { tokenOwner[_tokenId] = _to; ownedTokensCount[_to] += 1; } function balanceOf(address _owner) public view returns (uint256){ return ownedTokensCount[_owner]; } function ownerOf(uint256 _tokenId) public view returns (address){ return tokenOwner[_tokenId]; } function transferFrom(address _from, address _to, uint256 _tokenId) public{ address owner = ownerOf(_tokenId); require(msg.sender == owner); require(_from != address(0)); require(_to != address(0)); ownedTokensCount[_from] -= 1; tokenOwner[_tokenId] = address(0); ownedTokensCount[_to] += 1; tokenOwner[_tokenId] = _to; } }
Solidity
복사
본코드에서 아래의 코드 부분의 주석을 풀어준다.
function safeTransferFrom(address _from, address _to, uint256 _tokenId) public;
Solidity
복사
아래의 컨트랙 안에 아래의 코드를 작성한다.
function safeTransferFrom(address _from, address _to, uint256 _tokenId) public{ transferFrom(_from, _to, _tokenId); if (isContract(_to)) { } }
Solidity
복사
상기의 코드 내용을 보면 매개변수로 받은 애들을 transferFrom에 넣어주고, 보낼 계정이 컨트랙계정인지 아닌지 확인하기위해서, if조건절로 isContract함수를 넣었다.
다음처럼 isContract함수를 작성하였다.
function isContract(address _addr) private view returns (bool){ uint256 size; assembly { size:= extcodesize(_addr)} return size >0; }
Solidity
복사
주소를 받았을때 이 주소가 Contract 주소이면 True를 리턴하고, 일반계정이면 False를 리턴하는 함수이다. 이를 판별하기위해서 확인하는방법은 assembly code를 사용해야한다.
주소를 extcodesize에 넘겨서 사이즈가 0이면 일반계정이고, 0보다 크면 컨트랙 계정이라는 뜻이다.
이제 주소가 일반 계정인지 Contract 계정인지 판별을 했으면, 우리는 만약 Contract 계정일 경우, 이 Contract ERC721을 받을 수 있는 계정인지 확인해줘야한다.
상기의 내용을 위해 작성하는 함수는 다음과 같다.
/// @dev Note: the ERC-165 identifier for this interface is 0x150b7a02. interface ERC721TokenReceiver { /// @notice Handle the receipt of an NFT /// @dev The ERC721 smart contract calls this function on the recipient /// after a `transfer`. This function MAY throw to revert and reject the /// transfer. Return of other than the magic value MUST result in the /// transaction being reverted. /// Note: the contract address is always the message sender. /// @param _operator The address which called `safeTransferFrom` function /// @param _from The address which previously owned the token /// @param _tokenId The NFT identifier which is being transferred /// @param _data Additional data with no specified format /// @return `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))` /// unless throwing function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4); }
Solidity
복사
상기 함수는 아래의 링크에서 확인할 수 있다.
ERC721TokenReceiver함수는 onERC721Received함수를 구현하여, '제대로된4bytes 타입의 값'을 리턴을 했다면, 토큰을 받을 수 있는 컨트랙이라고 판별하는 함수라고 판단한다. 여기서 '제대로된 리턴값'이란 magic value라고부르며, 각 인터페이스마다 유티크한 식별자가 있는데, 이 식별자 값을 리턴해야만, 제대로된 값을 리턴했다고 판단한다. 위 경우 매직 벨류는, 맨위에 주석처리된 "0x150b7a02" 이다.
이값(매직 벨류는, 맨위에 주석처리된 "0x150b7a02")은 어디서 생성된 값일까요?
이 값은 ERC721TokenReceiver 인터페이스안에 있는 함수 이름과, 매개변수 타입들을 암호화시키고 4bytes로 자른 값입니다.
더 자세히말하면 bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")) 이 처럼 함수이름, 매개변수 타입들을 keccak256 알고리즘을 사용하여 해쉬로 변환하고 그 값을 4bytes로 잘라낸 값입니다.

최종 코드

// Klaytn IDE uses solidity 0.4.24, 0.5.6 versions. pragma solidity >=0.4.24 <=0.5.6; interface ERC721 { 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) public view returns (uint256); function ownerOf(uint256 _tokenId) public view returns (address); //function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) public; function safeTransferFrom(address _from, address _to, uint256 _tokenId) public; function transferFrom(address _from, address _to, uint256 _tokenId) public; // function approve(address _approved, uint256 _tokenId) public; //function setApprovalForAll(address _operator, bool _approved) public; // function getApproved(uint256 _tokenId) public view returns (address); // function isApprovedForAll(address _owner, address _operator) public view returns (bool); } contract ERC721Implementation is ERC721{ mapping(uint256 => address) tokenOwner; mapping(address => uint256) ownedTokensCount; function mint(address _to, uint _tokenId) public { tokenOwner[_tokenId] = _to; ownedTokensCount[_to] += 1; } function balanceOf(address _owner) public view returns (uint256){ return ownedTokensCount[_owner]; } function ownerOf(uint256 _tokenId) public view returns (address){ return tokenOwner[_tokenId]; } function transferFrom(address _from, address _to, uint256 _tokenId) public{ address owner = ownerOf(_tokenId); require(msg.sender == owner); require(_from != address(0)); require(_to != address(0)); ownedTokensCount[_from] -= 1; tokenOwner[_tokenId] = address(0); ownedTokensCount[_to] += 1; tokenOwner[_tokenId] = _to; } function safeTransferFrom(address _from, address _to, uint256 _tokenId) public{ transferFrom(_from, _to, _tokenId); if (isContract(_to)) { } } function isContract(address _addr) private view returns (bool){ uint256 size; assembly { size:= extcodesize(_addr)} return size >0; } }
Solidity
복사