ERC-721 표준은 NFT(대체 불가능한 토큰, Non-fungible Token)을 발행하는 스마트 컨트랙트를 정의하고 있습니다. ERC-20을 기본으로 해서 제작되어 유사한 함수를 가지고 있지만, 특정 자산에 대해 대체 불가능 토큰을 발행한다는 점과, 소유권을 이전할 때 이전가능한 수량을 지정하지 않고, 전체 토큰을 양도하는 점이 다른 부분입니다.
NFT란 같은 스마트 컨트랙트로 발행한 토큰이라고 할지라도, 토큰에 지정되는 대상에 따라 토큰의 가치는 다른 가치를 가지게 되는 것입니다. 만약 동일한 keyboard에 저의 SIGN이 들어간다면 이 키보드는 세상에 1개 밖에 존재하지 않는 유일한 키보드가 됩니다.
ERC-721 함수 구성
ERC-721은 이더리움 블록체인에 배포되는 스마트 컨트랙트를 정의한 기술 표준입니다. NFT를 발행한 후 민팅(블록체인에 올리는 작업)하는 작업과, 발행된 토큰을 다른 계정으로 소유권을 이전하는 작업을 담당하는 여러개의 함수로 구성되어 있습니다.
함수 | 설명 |
balanceOf | Owner가 소유한 NFT 갯수를 반환합니다. |
ownerOf | 특정 tokenID를 가진 NFT의 소유자를 반환합니다. |
approve | 특정 계정으로 NFT를 사용하도록 양도를 허용합니다. |
getApproved | 특정 NFT가 다른 계정에게 사용 승인되었는지 확인합니다. |
setApprovedForAll | 특정 계정으로 소유한 모든 NFT에 대한 사용을 허용합니다. |
isApprovedForAll | owner가 특정 계정에게 모든 NFT 사용을 허용했는지 여부를 반환합니다. |
transferFrom | NFT 소유권을 특정 계정으로 이전합니다. |
safeTransferFrom | 수신 계정이 NFT를 사용할 수 있는지 확인 후 NFT를 전송합니다. |
ERC-721은 openzeppelin에서 ERC-721 API 형식으로 제공하고 있습니다.
ⓐ 변수
ERC-721은 먼저 변수를 정의합니다.
string private _name;
string private _symbol;
mapping(uint256 => address) private _owners;
mapping(address => uint256) private _balances;
mapping(uint256 => address) private _tokenApprovals;
mapping(address => mapping(address => bool)) private _operatorApprovals;
- _name : 토큰의 이름을 정의합니다.
- _symbol : 토큰의 symbol을 정의합니다. 예를 들어 Bored Ape Yacht Club인 경우 BAYC로 심볼을 정의합니다.
- _owners : 해당 토큰의 ID로 토큰 소유자의 주소를 반환합니다.
- _balances : 토큰 소유자 계정 주소로 토큰 소유량을 반환합니다.
- _tokenApprovals : 토큰 ID로 approve를 받은 계정 주소를 반환합니다.
- _operatorApprovals : 토큰 소유자 계정과 operator 주소로 approve 여부를 확인합니다. 예를 들어 0x1234 소유자 계정이 0x5678에게 approve한 경우 아래와 같이 boolean 값을 반환합니다.
_operatorApprovals[0x1234][0x5678] // true || false
ⓑ balanceOf
owner가 소유한 NFT의 갯수를 반환합니다.
사용법 : balanceOf(address owner) => uint256(토큰 갯수)
virtual override 키워드는 컨트랙트 상속과 관련 있습니다. virtual 키워드를 달면 하위 컨트랙트에서 상속받아 오버라이딩이 가능하고, 상속받은 컨트랙트에서 오버라이딩을 하는 경우 override 키워드를 붙여 해당 함수는 상속받은 함수라는 것을 명시적으로 표현합니다.
function balanceOf(address _owner) public view virtual override returns(uint256){
require(_owner != address(0x0), // Address 0x0 is not a valid owner;
return _balances[_owner];
}
ⓒ ownerOf(uint256 tokenId) => address
발행된 모든 NFT들은 해당 컨트랙트 내에서 고유한 TokenID를 가지게 됩니다. 해당 TokenID를 가지고 NFT 정보에 접근해 해당 소유자를 확인할 수 있습니다. Invalid Address(0x00)이 아님을 확인 한 후 해당 소유자 계정을 반환합니다.
function ownerOf(uint256 tokenId) public view virtual override returns(address){
address _owner = _owners[tokenId];
require(_owner != address(0x0);
return _owner;
}
ⓓ approve(address to, utin256 tokenId)
특정 계정에게 NFT를 사용할 수 있도록 허용하는 함수입니다. ERC-20과 마찬가지로 토큰을 전송하는 방법은 2가지가 있습니다. transfer() 함수로 소유자=>수신자 형태로 직배송하는 방법이 있고, 소유자=>위탁판매자=>수신자 형태로 토큰 매매권한을 양도한 후 수신자에게 보내는 방법이 있습니다.
approve() 함수로 권한을 양도받은 위탁자를 operator라고 부릅니다. operator는 해당 토큰을 다른 스마트컨트랙트에 사용하거나 다시 approve() 함수로 권한을 위임할 수 있습니다.
approve() 함수는 토큰의 소유권을 다루고 있는 만큼 owner나 권한을 위임받은 operator만 호출할 수 있습니다. 내장 함수 _approve() 함수를 호출해서 최종적으로 Approval Event를 발생시킵니다.
function approve(address to, uint256 tokenId) public virtual override{
// 토큰 소유권 확인
address _owner = ERC721.ownerOf(tokenID);
require(to != _owner, "ERC721: approval to current owner);
// 함수호출자 owner 확인
// owner이거나 operator여야 합니다.
require(_msgSender() == _owner || isApprovedForAll(_owner, _msgSender()),
"ERC721: approve caller is not token owner nor approved for all"
);
_approve(to, tokenId);
}
function _approve(address to, uint256 tokenId) internal virtual{
_tokenApprovals[tokenId] = to;
emit Approval(ERC721.ownerOf(tokenId), to, tokenId);
}
ⓔ getApproved(uint256 tokenId) => address
특정 Token이 다른 operator에게 승인된 상태인 경우 승인받은 operator의 계정 주소를 반환합니다. 만약 누구에게도 승인되지 않은 경우 0x0 주소를 반환합니다.
function getApproved(uint256 tokenId) public view virtal override returns(address){
return _tokenApprovals[tokenId];
}
ⓕ setApproveForAll(address to, bool approved)
함수를 호출한 msg.sender가 소유하고 있는 모든 NFT를 특정 operator에게 사용권을 승인하는 함수입니다. approved가 true인 경우 모든 NFT 사용권을 승인하고, false 인 경우 사용권을 철회합니다.
ERC-20의 경우 FT(Fungible Token)을 발행하므로 위임하는 토큰의 수량을 정할 수 있지만 ERC-721의 경우 모든 토큰이 다른 가치를 가지고 있으므로, 모든 NFT를 위임하는 함수를 포함하고 있습니다.
내부적으로는 내장 함수 _setApprovalForAll(owner, operator, approved)를 실행합니다. 소유자와 opertator가 달라야 하고, _operatorApprovals 2차원 매핑 변수에 approved를 할당합니다. 마지막으로 ApprovalForAll Event를 발생시킵니다.
function setApprovalForAll(address operator, bool approved) public virtual override{
_setApprovalForAll(_msgSender(), operator, approved);
}
function _setApprovalForAll(address _owner, address _operator, bool approved) internal virtual {
require(_owner != _operator, "ERC721: approve to caller");
_operatorApprovals[_owner][_operator] = approved;
emit ApprovalForAll(_owner, _operator, approved);
}
ⓖ isApprovedForAll(address _owner, address _operator) => bool
소유자가 operator에게 모든 NFT 사용권을 위임했는지 여부를 반환합니다.
function isApprovedForAll(address _owner, address _operator) public view virtal override{
return _operatorApprovals[_owner][_operator];
}
ⓗ transferFrom(address from, address to, uint256 tokenId)
Token의 소유권을 from 으로 부터 to로 이동합니다. from은 정당한 Token의 소유자이거나 사용권을 위임받은 opertoar 여야 합니다.
내부적으로는 _transfer(from, to, tokenId) 를 실행합니다. 이전 소유자가 위임했던 모든 approvals를 제거한 후 토큰의 balance를 조정하고 Transfer Event를 발생시킵니다.
function transferFrom(address from, address to, uint256 tokenId) public virtual override{
require(_isApprovedOrOwner(_msgSender(), tokenId, "ERC721: caller is not token owner nor approved");
_transfer(from, to, tokenId);
}
function _isApprovedOrOwner(address spender, uint 256 tokenId) internal view virtual returns(bool){
address owner = ERC721.ownerOf(tokenId);
return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender);
}
function _transfer(address _from, address _to, uint256 tokenId) internal virtual{
require(ERC721.ownerOf(tokenId)==from, "ERC721: transfer from incorrect owner");
require(to != address(0x0), "ERC721: transfer fo the zero address");
// 이전 owner가 승인했던 approvals를 모두 삭제합니다.
_approve(address(0x0), tokenId);
_balances[from] -= 1;
_balances[to] += 1;
_owners[tokenId] = to;
emit Transfer(from, to, tokenId);
}
ERC-721에서 Token의 소유권을 이전하기 위해 transfer() 함수를 사용하는 것은 바람직하지 않습니다. 그 이유는 이더리움에 존재하는 EOA와 CA 중 EOA만이 NFT를 사용할 수 있기 때문입니다. 따라서 NFT를 수신하는 계정이 NFT를 사용할 수 있는 계정인지 확인 후 소유권을 이전하는 safeTransferFrom() 함수 사용이 권장됩니다.
ⓘ safeTransferFrom(address from, address to, uint256 tokenId)
수신 계정이 NFT를 받을 수 있는 주소인지 확인한 후 NFT 소유권을 이전합니다. 하지만 여기서 문제가 발생합니다.
transferFrom() 함수의 문제
이더리움 블록체인에서 스마트 컨트랙트 역시 계정주소를 가지고 있으며, 컨트랙트는 단순히 코드에 의해서만 작동하는 기계입니다. 따라서 스마트 컨트랙트 계정으로 전송된 NFT를 사용하는 transferFrom() 함수가 호출되지 않는 이상 해당 NFT는 사용이 불가능해집니다. 존재하지만 존재하지 않는 NFT가 되어 버립니다.
safeTransferFrom 함수는 이러한 문제를 해결하기 위해 수신하는 계정이 NFT를 받을 수 있는지 한번더 확인을 거치게 됩니다. 확인을 위해서 ERC-721에서는 onERC721Received 함수를 구현합니다.
수신자 상태 확인하는 방법
safeTransferFrom 함수가 수신자의 상태를 확인하는 방법은 내부적으로 _checkOnERC721Received() 함수를 실행합니다.
function _safeTransfer(address from, address to, uint256, bytes memory data) internal virtual{ _transfer(from, to, tokenId); require(_checkOnERC721Received(from, to, tokenId, data), "ERC721: transfer to non ERC721Received imlementer"); }
require 문 내에 존재하는 _checkOnERC721Received 함수는 수신 컨트랙트에 onERC721Received() 함수가 제대로 구현되었는지 확인합니다.
try 문에서는 수신 컨트랙트의 IERC721Receiver Interface로 구현된 onERC721Received()를 실행한 후 selector를 반환하는데, 반환값인 Selector가 IERC721Receiver Interface 표준에 맞게 구현된 onERC721Received() 함수인지 확인하게 됩니다. 이에 따라 true OR false 값을 반환합니다.
/* _checkOnERC721Received 함수 */
function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory data) private returns(bool){
if(to.isContract()){
try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data) returns(bytes4 retval){
return retval == IERC721Receiver.onERC721Received.selector;
}catch(bytes memory reason){
if(reason.length == 0){
revert("ERC721: transfer to non ERC721Receiver implementer");
}else{
assembly{
revert(add(32, reason), mload(reason));
}
}
}
}else{
return true;
}
}
onERC721Received() 함수에서는 전달받은 NFT를 다루는 로직이 포함됩니다. 즉 수신받는 컨트랙트는 자신이 onERC721Received() 함수를 가지고 있으며, 실행했다는 것을 알려주기 위해 onERC721Received() 함수의 Selector를 반환합니다.
ERC-165(Standard Interface Detection) Selector
모든 컨트랙트 함수들은 고유한 Selector을 가지게 됩니다. 함수 Selector는 함수가 가지는 고유한 ID 값입니다. Selctor를 구하는 방법은 2가지가 있습니다.
1. 함수의 시그니처(함수명 + 파리미터)를 keccak256() 해시한 후 bytes4로 형변환합니다.
// balanceOf(address) 함수의 Selector를 구하는 경우 bytes4(keccak256("balanceOf(address)"))
2. Contract 내부 메서드의 Selector 속성을 이용합니다.function onERC721Received(address msgSender, address nftContractAddress, uint256 _tokenId, bytes calldata _data) public virtual override returns(bytes4){ // NFT를 다루는 로직 return this.onERC721Received.selector; }
/* onERC721Received 함수 */
function onERC721Received(address msgSender, address nftContractAddress, uint256 _tokenId, bytes calldata _data) public virtual override returns(bytes4){
// NFT를 다루는 로직
return this.onERC721Received.selector;
}
ⓙ 더 많은 기능들
openzeppelin에서는 ERC-721에 확장된 기능을 제공합니다.
- ERC721 Mintable : NFT 새로 발행
- ERC721 Burnable : NFT 소각
- ERC721 Pausable : NFT 전송을 일시 정지 합니다.
Reference
'Blockchain' 카테고리의 다른 글
[Blockchain] OpenSea NFT 민팅 하는 방법 (0) | 2022.07.21 |
---|---|
[Blockchain] NFT 만드는 법 (ERC-721 표준 사용) (0) | 2022.07.20 |
[Blockchain] ERC-721 vs ERC-20 차이점 (NFT vs FT) (0) | 2022.07.19 |
[Blockchain] KIP-7 vs ERC-20 차이점? (0) | 2022.07.18 |
[Blockchain] ERC-20 투표로 관리자 owner 설정 하는 방법 (0) | 2022.07.18 |
댓글