1. ERC-721 스마트 컨트랙트 작성
Last updated
Was this helpful?
Last updated
Was this helpful?
Was this helpful?
MyERC721Card.sol
의 전체 코드는 아래에서 확인할 수 있습니다.
pragma solidity ^0.5.0;
// https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v2.3.0/contracts/utils/Address.sol
/**
* @dev 주소 타입과 관련된 함수의 모음,
*/
library Address {
/**
* @dev 만일 `account` 가 컨트랙트라면 참(true)를 반환합니다.
*
* 이 테스트는 불완전하며, false-negatives가 있을 수 있습니다:
* 컨트랙트의 생성자를 실행하는 중, 주소는 컨트랙트를 포함하지
* 않은 것으로 보고될 것입니다.
*
* > 이 함수가 거짓(false)을 반환하는 주소가 외부 소유 계정(EOA)
* 이며 컨트랙트가 아니라고 가정하는 것은 확실하지 않습니다.
*/
function isContract(address account) internal view returns (bool) {
// 코드는 생성자 실행이 완료되고 나서 저장되므로, 생성 중인
// 컨트랙트에 대해 0을 반환하는 extcodesize에
// 의존합니다.
uint256 size;
// solhint-disable-next-line no-inline-assembly
assembly { size := extcodesize(account) }
return size > 0;
}
}
// https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v2.3.0/contracts/math/SafeMath.sol
/**
* @dev 오버플로우 검사를 포함한 솔리디티의 산술 연산에 대한
* 래퍼입니다.
*
* 솔리디티의 산술 연산은 오버플로우됩니다. 프로그래머는 일반적으로
* 고수준 프로그래밍 언어의 일반적인 동작인 오버플로우가 에러를 발생시키는 것
* 으로 가정하기 때문에, 이는 버그의 결과일 수 있습니다.
* `SafeMath`는 연산이 오버플로우될 때 트랜잭션을 되돌려
* 직관적으로 복원합니다.
*
* 확인되지 않은 연산 대신에 이 라이브러리를 사용하면
* 버그가 제거되므로, 항상 사용하는 것이 좋습니다.
*/
library SafeMath {
/**
* @dev 부호 없는 정수 두 개를 더한 값을 반환하고, 오버플로우를
* 예외처리합니다.
*
* Counterpart to Solidity's `+` operator.
*
* Requirements:
* - Addition cannot overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "SafeMath: addition overflow");
return c;
}
/**
* @dev Returns the subtraction of two unsigned integers, reverting on
* overflow (when the result is negative).
*
* Counterpart to Solidity's `-` operator.
*
* Requirements:
* - Subtraction cannot overflow.
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
require(b <= a, "SafeMath: subtraction overflow");
uint256 c = a - b;
return c;
}
/**
* @dev Returns the multiplication of two unsigned integers, reverting on
* overflow.
*
* Counterpart to Solidity's `*` operator.
*
* Requirements:
* - Multiplication cannot overflow.
*/
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the
// benefit is lost if 'b' is also tested.
// See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
if (a == 0) {
return 0;
}
uint256 c = a * b;
require(c / a == b, "SafeMath: multiplication overflow");
return c;
}
/**
* @dev Returns the integer division of two unsigned integers. Reverts on
* division by zero. The result is rounded towards zero.
*
* Counterpart to Solidity's `/` operator. Note: this function uses a
* `revert` opcode (which leaves remaining gas untouched) while Solidity
* uses an invalid opcode to revert (consuming all remaining gas).
*
* Requirements:
* - The divisor cannot be zero.
*/
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// Solidity only automatically asserts when dividing by 0
require(b > 0, "SafeMath: division by zero");
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
/**
* @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
* Reverts when dividing by zero.
*
* Counterpart to Solidity's `%` operator. This function uses a `revert`
* opcode (which leaves remaining gas untouched) while Solidity uses an
* invalid opcode to revert (consuming all remaining gas).
*
* Requirements:
* - The divisor cannot be zero.
*/
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
require(b != 0, "SafeMath: modulo by zero");
return a % b;
}
}
// https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v2.3.0/contracts/drafts/Counters.sol
/**
* @title Counters
* @author Matt Condon (@shrugs)
* @dev 오직 1씩만 증가 또는 감소할 수 있는 카운터(counter)를 제공합니다. 이는 가령 매핑의 원소 수 추적,
* ERC721 ID 발행, 또는 요청 ID 개수를 세는 데 사용할 수 있습니다.
*
* `using Counters for Counters.Counter;`으로 포함시킵니다.
* 1씩 증가시키는 것으로 256 bit 정수를 오버플로우 시킬 수 없으므로, `increment`는 SafeMath
* 오버플로우 체크를 스킵하고, 가스를 절약할 수 있습니다. 그러나 이는 기본적으로 `_value`가 직접 액세스되지 않는다는 가정하에 올바른 사용을 상정하고 있습니다.
*/
library Counters {
using SafeMath for uint256;
struct Counter {
// 이 변수는 라이브러리의 사용자로부터 직접 액세스되어서는 안 됩니다: 상호작용은 라이브러리의 함수들로만
// 제한되어져야 합니다. 솔리디티 v0.5.2부터, 비록 이 기능을 추가하는 제안이 있지만, 이는 강제될 수 없습니다:
// https://github.com/ethereum/solidity/issues/4637를 참조하세요.
uint256 _value; // 기본값: 0
}
function current(Counter storage counter) internal view returns (uint256) {
return counter._value;
}
function increment(Counter storage counter) internal {
counter._value += 1;
}
function decrement(Counter storage counter) internal {
counter._value = counter._value.sub(1);
}
}
/**
* @dev [EIP](https://eips.ethereum.org/EIPS/eip-165)에 정의된
* ERC165 표준의 인터페이스입니다.
*
* 구현체는 지원하는 컨트랙트 인터페이스를 선언할 수 있으며,
* 외부에서 (`ERC165Checker`) 이 함수를 호출해 지원 여부를 조회할 수 있습니다.
*
* 구현에 대해서는 `ERC165`를 참조하세요.
*/
interface IERC165 {
/**
* @dev 만일 컨트랙트가 `interfaceId`로 정의된 인터페이스를 구현했으면,
* 참(true)을 반환합니다. ID 생성 방법에 대한 자세한 내용은 해당
* [EIP section](https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified)
* 을 참조하세요.
*
* 이 함수 호출은 30000 가스보다 적게 사용할 것입니다.
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
/**
* @dev `IERC165` 인터페이스의 구현체.
*
* 컨트랙트는 이를 상속받을 수 있으며 `_registerInterface`를 호출해 인터페이스 지원을
* 선언할 수 있습니다.
*/
contract ERC165 is IERC165 {
/*
* bytes4(keccak256('supportsInterface(bytes4)')) == 0x01ffc9a7
*/
bytes4 private constant _INTERFACE_ID_ERC165 = 0x01ffc9a7;
/**
* @dev 지원 여부에 대한 인터페이스 ID의 매핑
Mapping of interface ids to whether or not it's supported.
*/
mapping(bytes4 => bool) private _supportedInterfaces;
constructor () internal {
// 파생된 컨트랙트는 고유한 인터페이스에 대한 지원만 등록하면 됩니다.
// ERC165 자체에 대한 지원만 여기에서 등록합니다.
_registerInterface(_INTERFACE_ID_ERC165);
}
/**
* @dev `IERC165.supportsInterface`를 참조하세요.
*
* 시간 복잡도는 O(1)이며, 항상 30000 가스 미만을 사용하도록 보장합니다.
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool) {
return _supportedInterfaces[interfaceId];
}
/**
* @dev 컨트랙트가 `interfaceId`로 정의한 인터페이스를 구현했음을
* 등록합니다. 실제 ERC165 인터페이스의 지원은 자동으로 되며
* 이 인터페이스 ID의 등록은 필요하지 않습니다.
*
* `IERC165.supportsInterface`를 참조하세요.
*
* 요구사항:
*
* - `interfaceId`는 ERC165 유효하지 않은 인터페이스(`0xffffffff`)일 수 없습니다.
*/
function _registerInterface(bytes4 interfaceId) internal {
require(interfaceId != 0xffffffff, "ERC165: invalid interface id");
_supportedInterfaces[interfaceId] = true;
}
}
/**
* @dev ERC721 호환 컨트랙트의 필수 인터페이스
*/
contract IERC721 is IERC165 {
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);
/**
* @dev `owner` 계정의 NFT의 개수를 반환합니다.
*/
function balanceOf(address owner) public view returns (uint256 balance);
/**
* @dev `tokenId`에 의해 명시된 NFT의 소유자를 반환합니다.
*/
function ownerOf(uint256 tokenId) public view returns (address owner);
/**
* @dev 특정 NFT (`tokenId`)를 한 계정(`from`)에서
* 다른 계정(`to`)으로 전송합니다.
*
*
*
* 요구사항:
* - `from`, `to`는 0일 수 없습니다.
* - `tokenId`는 `from`이 소유하고 있어야 합니다.
* - 만일 호출자가 `from`이 아니라면, `approve` 또는
* `setApproveForAll`를 통해 이 NFT의 전송을 허가받았어야 합니다.
*/
function safeTransferFrom(address from, address to, uint256 tokenId) public;
/**
* @dev 특정 NFT (`tokenId`)를 한 계정 (`from`)에서
* 다른 계정(`to`)으로 전송합니다.
*
* 요구사항:
* - 만일 호출자가 `from`이 아니라면, `approve` 또는
* `setApproveForAll`를 통해 이 NFT를 전송을 허가받았어야 합니다.
*/
function transferFrom(address from, address to, uint256 tokenId) public;
function approve(address to, uint256 tokenId) public;
function getApproved(uint256 tokenId) public view returns (address operator);
function setApprovalForAll(address operator, bool _approved) public;
function isApprovedForAll(address owner, address operator) public view returns (bool);
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public;
}
// https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v2.3.0/contracts/token/ERC721/IERC721Receiver.sol
/**
* @title ERC721 토큰 수신자 인터페이스
* @dev ERC721 자산 컨트랙트로부터 safeTransfers를 지원하고 싶은
* 컨트랙트를 위한 인터페이스입니다.
*/
contract IERC721Receiver {
/**
* @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 memory data)
public returns (bytes4);
}
// https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v2.3.0/contracts/token/ERC721/ERC721.sol
contract ERC721 is ERC165, IERC721 {
using SafeMath for uint256;
using Address for address;
using Counters for Counters.Counter;
// `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`와 동일
// `IERC721Receiver(0).onERC721Received.selector`로부터 얻을 수도 있습니다
bytes4 private constant _ERC721_RECEIVED = 0x150b7a02;
// 토큰 ID에서 소유자로의 매핑
mapping (uint256 => address) private _tokenOwner;
// 토큰 ID에서 승인된 주소로의 매핑
mapping (uint256 => address) private _tokenApprovals;
// 소유자에서 소유한 토큰 개수로의 매핑
mapping (address => Counters.Counter) private _ownedTokensCount;
// 소유자에서 운영자(operator) 승인 여부로의 매핑
mapping (address => mapping (address => bool)) private _operatorApprovals;
/*
* bytes4(keccak256('balanceOf(address)')) == 0x70a08231
* bytes4(keccak256('ownerOf(uint256)')) == 0x6352211e
* bytes4(keccak256('approve(address,uint256)')) == 0x095ea7b3
* bytes4(keccak256('getApproved(uint256)')) == 0x081812fc
* bytes4(keccak256('setApprovalForAll(address,bool)')) == 0xa22cb465
* bytes4(keccak256('isApprovedForAll(address,address)')) == 0xe985e9c
* bytes4(keccak256('transferFrom(address,address,uint256)')) == 0x23b872dd
* bytes4(keccak256('safeTransferFrom(address,address,uint256)')) == 0x42842e0e
* bytes4(keccak256('safeTransferFrom(address,address,uint256,bytes)')) == 0xb88d4fde
*
* => 0x70a08231 ^ 0x6352211e ^ 0x095ea7b3 ^ 0x081812fc ^
* 0xa22cb465 ^ 0xe985e9c ^ 0x23b872dd ^ 0x42842e0e ^ 0xb88d4fde == 0x80ac58cd
*/
bytes4 private constant _INTERFACE_ID_ERC721 = 0x80ac58cd;
constructor () public {
// ERC165를 통해 ERC721을 준수하도록 지원되는 인터페이스를 등록하세요
_registerInterface(_INTERFACE_ID_ERC721);
}
/**
* @dev 명시한 주소의 잔액을 얻습니다.
* @param owner 잔액을 요청하는 주소
* @return uint256 전달받은 주소가 보유한 수량
*/
function balanceOf(address owner) public view returns (uint256) {
require(owner != address(0), "ERC721: balance query for the zero address");
return _ownedTokensCount[owner].current();
}
/**
* @dev 명시된 토큰 ID의 소유자를 얻습니다.
* @param tokenId uint256 소유자를 요청하는 토큰의 ID
* @return address 주어진 토큰 ID의 현재 표시된 소유자
*/
function ownerOf(uint256 tokenId) public view returns (address) {
address owner = _tokenOwner[tokenId];
require(owner != address(0), "ERC721: owner query for nonexistent token");
return owner;
}
/**
* @dev 주어진 토큰 ID의 전송을 다른 주소에게 허가합니다.
* 영(zero) 주소는 승인된 주소가 없음을 나타냅니다.
* 한 번에 하나의 승인된 주소만 있을 수 있습니다.
* 토큰 소유자나 승인된 운영자만이 호출할 수 있습니다.
* @param to address 주어진 토큰 ID에 대해 승인할 주소
* @param tokenId uint256 승인하고자 하는 토큰 ID
*/
function approve(address to, uint256 tokenId) public {
address owner = ownerOf(tokenId);
require(to != owner, "ERC721: approval to current owner");
require(msg.sender == owner || isApprovedForAll(owner, msg.sender),
"ERC721: approve caller is not owner nor approved for all"
);
_tokenApprovals[tokenId] = to;
emit Approval(owner, to, tokenId);
}
/**
* @dev 토큰 ID에 대해 승인된 주소를, 만일 설정된 주소가 없으면 0을 얻습니다.
* 만일 토큰 ID가 존재하지 않는 경우 되돌려집니다.
* @param tokenId uint256 승인된 주소를 요청하는 토큰의 ID
* @return address 주어진 토큰 ID에 대해 현재 승인된 주소
*/
function getApproved(uint256 tokenId) public view returns (address) {
require(_exists(tokenId), "ERC721: approved query for nonexistent token");
return _tokenApprovals[tokenId];
}
/**
* @dev 주어진 운영자의 승인을 설정 또는 해제합니다.
* 운영자는 발신자를 대신해 모든 토큰을 전송할 수 있도록 허가되었습니다.
* @param to 승인을 설정하고자 하는 운영자의 주소
* @param approved 설정하고자 하는 승인의 상태를 나타냅니다
*/
function setApprovalForAll(address to, bool approved) public {
require(to != msg.sender, "ERC721: approve to caller");
_operatorApprovals[msg.sender][to] = approved;
emit ApprovalForAll(msg.sender, to, approved);
}
/**
* @dev 주어진 소유자에 대해 운영자가 승인되었는지 여부를 말해줍니다.
* @param owner 승인을 조회하고자 하는 소유자 주소
* @param operator 승인을 조회하고자 하는 운영자 주소
* @return bool 주어진 운영자가 주어진 소유자로부터 승인되었는지 여부
*/
function isApprovedForAll(address owner, address operator) public view returns (bool) {
return _operatorApprovals[owner][operator];
}
/**
* @dev 주어진 토큰 ID의 소유권을 다른 주소로 전송합니다.
* 이 메소드는 사용하지 않는 것이 좋습니다. 가능하다면 `safeTransferFrom`을 사용하세요.
* msg.sender는 소유자, 승인된 주소, 또는 운영자여야 합니다.
* @param from 토큰의 현재 소유자
* @param to 주어진 토큰 ID의 소유권을 받을 주소
* @param tokenId 전송할 토큰의 uint256 ID
*/
function transferFrom(address from, address to, uint256 tokenId) public {
//solhint-disable-next-line max-line-length
require(_isApprovedOrOwner(msg.sender, tokenId), "ERC721: transfer caller is not owner nor approved");
_transferFrom(from, to, tokenId);
}
/**
* @dev 주어진 토큰 ID의 소유권을 다른 주소로 안전하게 전송합니다.
* 만일 목표 주소가 컨트랙트라면, 컨트랙트는 `onERC721Received`를 구현했어야만 합니다.
* 이는 안전한 전송으로부터 호출되며 마법의 값
* `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`를 반환합니다;
* 만일 다른 경우에는 전송이 되돌려집니다.
* msg.sender는 소유자, 승인된 주소, 운영자여야 합니다
* @param from 토큰의 현재 소유자
* @param to 주어진 토큰 ID의 소유권을 받을 주소
* @param tokenId 전송할 토큰의 uint256 ID
*/
function safeTransferFrom(address from, address to, uint256 tokenId) public {
safeTransferFrom(from, to, tokenId, "");
}
/**
* @dev 주어진 토큰 ID의 소유권을 다른 주소로 안전하게 전송합니다.
* 만일 목표 주소가 컨트랙트라면, 컨트랙트는 `onERC721Received`를 구현했어야만 합니다.
* 이는 안전한 전송으로부터 호출되며 마법의 값
* `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`를 반환합니다;
* 만일 다른 경우에는 전송이 되돌려집니다.
* msg.sender는 소유자, 승인된 주소, 운영자여야 합니다
* @param from 토큰의 현재 소유자
* @param to 주어진 토큰 ID의 소유권을 받을 주소
* @param tokenId 전송할 토큰의 uint256 ID
* @param _data 안전한 전송 검사와 함께 전송하고자 하는 바이트 데이터
*/
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public {
transferFrom(from, to, tokenId);
require(_checkOnERC721Received(from, to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer");
}
/**
* @dev 지정한 토큰이 존재하는지 여부를 반환합니다.
* @param tokenId uint256 존재를 조회하고자 하는 토큰의 ID
* @return bool 토큰의 존재 여부
*/
function _exists(uint256 tokenId) internal view returns (bool) {
address owner = _tokenOwner[tokenId];
return owner != address(0);
}
/**
* @dev 지정된 납부자가 주어진 토큰 ID를 전송할 수 있는지 여부를 반환합니다.
* @param spender 조회하고자 하는 납부자의 주소
* @param tokenId uint256 전송하고자 하는 토큰 ID
* @return bool msg.sender가 주어진 토큰 ID에 대해 승인되었는지,
* 운영자인지, 또는 토큰의 소유자인지 여부
*/
function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) {
require(_exists(tokenId), "ERC721: operator query for nonexistent token");
address owner = ownerOf(tokenId);
return (spender == owner || getApproved(tokenId) == spender || isApprovedForAll(owner, spender));
}
/**
* @dev 새 토큰을 발행하기 위한 내부 함수.
* 주어진 토큰 ID가 이미 존재하면 되돌립니다.
* @param to 발행된 토큰을 소유할 주소
* @param tokenId uint256 발행될 토큰의 ID
*/
function _mint(address to, uint256 tokenId) internal {
require(to != address(0), "ERC721: mint to the zero address");
require(!_exists(tokenId), "ERC721: token already minted");
_tokenOwner[tokenId] = to;
_ownedTokensCount[to].increment();
emit Transfer(address(0), to, tokenId);
}
/**
* @dev 특정 토큰을 소각하기 위한 내부 함수.
* 토큰이 존재하지 않으면 되돌립니다.
* 더 이상 사용되지 않으며, _burn(uint256)을 대신 사용하세요.
* @param owner 소각할 토큰의 소유자
* @param tokenId uint256 소각할 토큰의 ID
*/
function _burn(address owner, uint256 tokenId) internal {
require(ownerOf(tokenId) == owner, "ERC721: burn of token that is not own");
_clearApproval(tokenId);
_ownedTokensCount[owner].decrement();
_tokenOwner[tokenId] = address(0);
emit Transfer(owner, address(0), tokenId);
}
/**
* @dev 특정 토큰을 소각하기 위한 내부 함수.
* Reverts if the token does not exist.
* @param tokenId uint256 소각할 토큰의 ID
*/
function _burn(uint256 tokenId) internal {
_burn(ownerOf(tokenId), tokenId);
}
/**
* @dev 주어진 토큰 ID의 소유권을 다른 주소로 전송하기 위한 내부 함수.
* transferFrom과 달리, msg.sender에 제한이 없습니다.
* @param from 토큰의 현재 소유자
* @param to 주어진 토큰 ID의 소유권을 받고자 하는 주소
* @param tokenId uint256 전송될 토큰의 ID
*/
function _transferFrom(address from, address to, uint256 tokenId) internal {
require(ownerOf(tokenId) == from, "ERC721: transfer of token that is not own");
require(to != address(0), "ERC721: transfer to the zero address");
_clearApproval(tokenId);
_ownedTokensCount[from].decrement();
_ownedTokensCount[to].increment();
_tokenOwner[tokenId] = to;
emit Transfer(from, to, tokenId);
}
/**
* @dev 목표 주소에서 `onERC721Received`를 호출할 내부 함수.
* 대상 주소가 컨트랙트가 아닌 경우 호출이 실행되지 않습니다.
*
* 이 기능은 더 이상 사용되지 않습니다.
* @param from 주어진 토큰 ID의 이전 소유자를 나타내는 주소
* @param to 토큰을 받을 목표 주소
* @param tokenId uint256 전송될 토큰의 ID
* @param _data bytes 호출과 함께 전송할 추가 데이터
* @return bool 호출이 예상한 값(magic value)을 반환했는지 여부
*/
function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data)
internal returns (bool)
{
if (!to.isContract()) {
return true;
}
bytes4 retval = IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, _data);
return (retval == _ERC721_RECEIVED);
}
/**
* @dev 주어진 토큰 ID의 현재 승인을 지우는 개인 함수.
* @param tokenId uint256 전송할 토큰의 ID
*/
function _clearApproval(uint256 tokenId) private {
if (_tokenApprovals[tokenId] != address(0)) {
_tokenApprovals[tokenId] = address(0);
}
}
}
contract MyERC721Card is ERC721{
struct Card {
string name; // 카드의 이름
uint256 level; // 카드의 레벨
}
Card[] public cards; // 첫 아이템의 인덱스는 0입니다
address public owner;
constructor () public {
owner = msg.sender; // 새 카드를 생성할 수 있는 MyERC721Card 컨트랙트의 소유자
}
function mintCard(string memory name, address account) public {
require(owner == msg.sender); // 소유자만이 카드를 생성할 수 있습니다
uint256 cardId = cards.length; // 유일한 카드 ID
cards.push(Card(name, 1));
_mint(account, cardId); // 새 카드를 발행
}
}
MyERC721Card.sol
은 하나의 인터페이스(IERC165
), 세 라이브러리(Address
, SafeMath
그리고 Counters
) 그리고 네 컨트랙트(ERC165
, IERC721
, IERC721Receiver
그리고 MyERC721Card
)로 구성됩니다.
IERC165
인터페이스는 ERC-165 스펙에 명시된 인터페이스를 정의합니다.
Address
라이브러리는 account
가 컨트랙트인지 여부를 테스트하는 isContract
메소드를 정의합니다.
SafeMath
library defines wrappers over Solidity's arithmetic operations with added overflow checks for safe calculation of uint256
type of Solidity.
Counters
라이브러리는 오직 1만큼만 증가하거나 감소할 수 있는 카운터를 정의합니다. 이는 ERC721 ID를 발행할 때 원소의 개수를 추적하는 것에 사용됩니다.
ERC165
는 IERC165
인터페이스를 구현합니다.
IERC721
는 ERC-165를 포함한 ERC-721 스펙에 명시된 인터페이스를 정의합니다.
IERC721Receiver
는 MyERC721Card
컨트랙트에 사용된 onERC721Received
를 정의합니다.
ERC721
는 IERC721
및 ERC165
인터페이스를 구현합니다.
MyERC721Card
는 ERC721
을 사용한, 이름과 레벨을 포함한 카드 타입의 대체 불가능한 토큰을 구현합니다. MyERC721Card
컨트랙트의 소유자만이 새로운 카드를 발행할 수 있습니다.
Let's take a look at some important methods in detail.
constructor
of ERC721 and _INTERFACE_ID_ERC721
constructor
는 아래의 ERC-721 인터페이스로부터 구해진 4바이트 해시인 _INTERFACE_ID_ERC721
을 등록합니다.
/*
* bytes4(keccak256('balanceOf(address)')) == 0x70a08231
* bytes4(keccak256('ownerOf(uint256)')) == 0x6352211e
* bytes4(keccak256('approve(address,uint256)')) == 0x095ea7b3
* bytes4(keccak256('getApproved(uint256)')) == 0x081812fc
* bytes4(keccak256('setApprovalForAll(address,bool)')) == 0xa22cb465
* bytes4(keccak256('isApprovedForAll(address,address)')) == 0xe985e9c
* bytes4(keccak256('transferFrom(address,address,uint256)')) == 0x23b872dd
* bytes4(keccak256('safeTransferFrom(address,address,uint256)')) == 0x42842e0e
* bytes4(keccak256('safeTransferFrom(address,address,uint256,bytes)')) == 0xb88d4fde
*
* => 0x70a08231 ^ 0x6352211e ^ 0x095ea7b3 ^ 0x081812fc ^
* 0xa22cb465 ^ 0xe985e9c ^ 0x23b872dd ^ 0x42842e0e ^ 0xb88d4fde == 0x80ac58cd
*/
bytes4 private constant _INTERFACE_ID_ERC721 = 0x80ac58cd;
constructor () public {
// ERC165를 통한 ERC721의 확인을 위한 지원 인터페이스 등록
_registerInterface(_INTERFACE_ID_ERC721);
}
등록 후, _INTERFACE_ID_ERC721
에 대해 호출될 때 supportsInterface
인 ERC-721 및 ERC-165의 인터페이스는 true
를 반환하고, 이 컨트랙트가 ERC-721 인터페이스를 구현하고 있음을 알려줍니다.
function balanceOf(address owner) public view returns (uint256 balance);
balanceOf
는 ERC-721의 필수 메소드입니다. balanceOf
는 owner
의 계정에 들어 있는 NFT의 개수를 반환합니다.
function balanceOf(address owner) public view returns (uint256) {
require(owner != address(0), "ERC721: balance query for the zero address");
return _ownedTokensCount[owner].current();
}
balanceOf
는 단지 owner
가 _ownedTokensCount
에서 유지하는 Counter
객체로부터 현재 카운트를 반환합니다.
// 소유자로부터 소유한 토큰의 개수로의 매핑
mapping (address => Counters.Counter) private _ownedTokensCount;
safeTransferFrom
and transferFrom
이 함수는 주어진 토큰 ID의 소유권을 다른 주소로 넘겨줍니다. ERC-721로부터 요구되는 두 개의 safeTransferFrom
메소드가 있습니다. 하나는 data
가 있으며, 다른 하나는 data
가 없습니다. data
가 없는 메소드는 data
를 ""
으로 설정한다는 점을 제외하면, 두 메소드 모두 동일하게 작동합니다. safeTransferFrom
는 아래의 더 많은 검사와 함께 transferFrom
를 호출하며, safeTransferFrom
는 또 다른 ERC-721의 필수 메소드인 transferFrom
보다 선호됩니다.
function safeTransferFrom(address from, address to, uint256 tokenId) public {
safeTransferFrom(from, to, tokenId, "");
}
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public {
transferFrom(from, to, tokenId);
require(_checkOnERC721Received(from, to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer");
}
function transferFrom(address from, address to, uint256 tokenId) public {
//solhint-disable-next-line max-line-length
require(_isApprovedOrOwner(msg.sender, tokenId), "ERC721: transfer caller is not owner nor approved");
_transferFrom(from, to, tokenId);
}
safeTransferFrom
은 to
주소가 토큰을 받을 수 있는지를 검사합니다. _checkOnERC721Received
는 검증 로직을 가집니다. 만일 to
주소가 컨트랙트라면 ERC-721의 onERC721Received
인터페이스를 구현해야 하며, 아래와 같이 ERC-721 토큰을 받기 위해 올바른 4바이트 해시를 반환해야 합니다.
function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data)
internal returns (bool)
{
if (!to.isContract()) {
return true;
}
bytes4 retval = IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, _data);
return (retval == _ERC721_RECEIVED);
}
_transferFrom
은 아래와 같이 주어진 tokenId
의 소유권을 실제로 이전합니다.
function _transferFrom(address from, address to, uint256 tokenId) internal {
require(ownerOf(tokenId) == from, "ERC721: transfer of token that is not own");
require(to != address(0), "ERC721: transfer to the zero address");
_clearApproval(tokenId);
_ownedTokensCount[from].decrement();
_ownedTokensCount[to].increment();
_tokenOwner[tokenId] = to;
emit Transfer(from, to, tokenId);
}
function _mint(address to, uint256 tokenId) internal
_mint
는 ERC-721의 일부가 아닙니다. 그러나 새로운 ERC-721 토큰을 생성할 방법이 필요하며, 이 구현체에서 새 토큰을 생성하기 위해 아래와 같은 _mint
가 필요합니다.
function _mint(address to, uint256 tokenId) internal {
require(to != address(0), "ERC721: mint to the zero address");
require(!_exists(tokenId), "ERC721: token already minted");
_tokenOwner[tokenId] = to;
_ownedTokensCount[to].increment();
emit Transfer(address(0), to, tokenId);
}
_mint
is an internal method and can be invoked inside of this contract. MyERC721Card.sol
에서 _mint
는 오직 MyERC721Card
의 mintCard
메소드에서만 호출됩니다. 스마트 컨트랙트의 소유자만이 mintCard
를 호출할 수 있습니다.
function mintCard(string name, address account) public {
require(owner == msg.sender); // Only the Owner can create Items
uint256 cardId = cards.length; // Unique card ID
cards.push(Card(name, 1));
_mint(account, cardId); // Mint a new card
}