ERC721 & NFT
ERC721 & NFT
ERC-721 함수의 기능
ERC-721 표준에 정의된 함수 9개
함수명 | 속성 |
---|---|
balanceOf | owner가 소유한 NFT의 갯수 반환 |
ownerOf | 특정 tokenId를 가진 NFT의 소유주 주소를 반환 |
approve | 특정 계정에게 자신이 소유한 NFT 하나를 사용하도록 허용 |
getApproved | 특정 NFT가 다른 계정에게 사용 승인되었는지의 여부 반환 |
setApprovalForAll | 특정 계정에게 자신이 소유한 NFT에 대한 사용을 허용 |
isApprovedForAll | owner가 특정 계정에게 자신의 모든 NFT에 대한 사용을 허용했는지의 여부 반환 |
transferFrom | NFT 소유권 전송 |
safeTransferFrom | 받는 주소가 NFT를 받을 수 있는지 확인 후 NFT 소유권 전송 |
safeTransferFrom | 받는 주소가 NFT를 받을 수 있는지 확인 후 NFT 소유권 전송 |
ERC-721에는 9개의 표준 함수가 정의되어 있는데, 이 표준 함수를 포함한 컨트랙트를 통해 민팅된 토큰이 바로 NFT 이다.
→ OpenZeppeling에서 제공하는 ERC-721 API에 구현된 코드를 사용하여 각각의 함수가 하는 역할에 대해 확인.
변수
1 | string private _name; |
name
- 토큰의 이름을 저장
_symbol
- 토큰의 심볼을 저장
- 이름이 Bored Ape Yacht Club인 토큰의 심볼은 BAYC이다.
- 토큰의 심볼을 저장
_owners
- 각 토큰의 ID와 토큰 소유자의 주소를 매핑
_balances
- 토큰 소유자의 주소와 소유자가 가지고 있는 토큰의 갯수를 매핑
_tokenApprovals
- 토큰 ID와 approved 된 주소를 매핑
_operatorApprovals
- 토큰 소유자와 operator 주소의 approval 여부를 저장
e.g. 0x1234가 0x5678에게 자신의 모든 토큰을 관리할 수 있는 operator 권한을 준 경우
1
_operatorApprovals[0x1234][0x5678] // true
e.g. 0x1234가 0x5678의 operator 권한을 취소한 경우
1
_operatorApprovals[0x1234][0x5678] // false
- 토큰 소유자와 operator 주소의 approval 여부를 저장
balanceOf(address owner) → uint256
balanceOf()
함수는 owner 주소가 가지고 있는 NFT의 갯수를 리턴한다.
1 | function balanceOf(address owner) public view virtual override returns (uint256) { |
ownerOf(uint256 tokenId) → address
모든 NFT는 발행된 컨트랙트 내에서 고유한 token ID를 가지고 있다. 따라서 컨트랙트 주소와 token ID만 있으면 NFT의 정보에 접근할 수 있다.
ownerOf()
함수는 token ID를 통해 토큰 owner의 주소를 반환한다.
1 | function ownerOf(uint256 tokenId) public view virtual override returns (address) { |
approve(address to, uint256 tokenId)
approve()
함수는 특정 tokenId를 제삼자가 사용할 수 있도록 승인(approve) 할 수 있다.
approve()
를 통해 tokenId 사용을 승인받은 제삼자는 operator라고 부르고, operator는 이 tokenId를 다른 스마트 컨트랙트에 사용하거나 다시 approve 할 수 있다.
approve()
함수는 소유권을 승인하는 행위라서 tokenId의 owner나 operator만 호출 가능하다.
1 | function approve(address to, uint256 tokenId) public virtual override { |
getApproved(uint256 tokenid) → address
getApproved()
함수는 token ID가 누구에게 승인(approve)된 상태면, 해당 승인된 operator 주소를 반환한다.
1 | function getApproved(uint256 tokenId) public view virtual override returns (address) { |
setApprovalForAll(address to, bool approved)
setApprovalForAll()
함수는 함수를 호출한 msg.sender
가 이 컨트랙트에서 가지고 있는 모든 NFT를 특정 operator 에게 승인하는 함수.
ApprovalForAll 이벤트 실행 함수.
- 첫번째 인자
to
: operator의 주소 - 두번째 인자
approved
: 승인 여부- approved →
true
:to
주소에게 모든 NFT의 사용을 승인 - approved →
false
: operator인to
주소로부터 NFT 사용 승인 철회
- approved →
1 | function setApprovalForAll(address operator, bool approved) public virtual override { |
❓more
ERC-20에는 setApprovalForAll() 함수가 표준으로 지정되어 있지 않으나, ERC-721에는 표준 함수로 등장함. ERC-20에서는 왜 setApprovalForAll() 함수를 사용하지 않을까?
ERC-721에서 setApprovalForAll 함수는 함수를 호출한 sender가 해당 컨트랙트에서 가진 모든 NFT 토큰 전송 권한을 특정 operator에게 승인하거나 해제하는 용도로 사용 된다. 또한, 대체불가능을 위해 소유자와 operator가 동일하지는 않은지 확인하고, 전송 단계에서 송신주소에서 수신주소로 권한과 tokenId를 변경하는 과정이 있다. ERC-20에서는 FT, 즉, 대체 가능한 토큰을 발행한다. 전송시 송신주소에서 수신주소로 원하는 토큰 양을 설정하여 토큰을 보내기 때문에 위임할 토큰 수량을 정해둘 수 있다.
isApprovedForAll(address owner, address operator) → bool
isApprovedForAll()
함수는 첫번째 인자 owner가 두번째 인자 operator 주소에게 setApprovalForAll()
함수를 통하여 모든 NFT를 승인했는가 여부를 전달.
- return값 →
true
:setApprovalForAll()
함수 호출해서 owner의 모든 NFT에 대해 operator에게 승인한 상태임. - return값 →
false
:setApprovalForAll()
함수를 호출한 적 없거나, setApprovalForAll() 을 호출해서 승인 철회한 상태임.
1 | function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) { |
transferFrom(address from, address to, uint256 tokenId)
transferFrom()
은 from
주소에서 to
주소로 tokenId를 옮긴다. from
주소는 옮기려는 토큰의 owner 혹은 승인받은 operator여야 함.
1 | function transferFrom(address from, address to, uint256 tokenId) public virtual override { |
safeTransferFrom(address from, address to, uint256 tokenId)
safeTransferFrom() 은 NFT를 받는 주소가 NFT를 받을 수 있는 주소인지 확인하는 특징이 있음.
🔪**
transferFrom()
함수의 문제**transferFrom()은 받는 주소가 NFT를 사용할 수 있는지 확인을 거치지 않고 보낸다.
A 컨트랙트에서 I의 주소 0x1234 로 NFT를 전송
→ A 컨트랙트 내부에 해당 NFT의 token ID의 owner가 0x1234로 설정됨
→ I가 NFT를 다른 곳에 재전송을 원할시 A 컨트랙트의 transferFrom() 호출 트랜잭션을 생성하면 됨
A 컨트랙트에서 B 컨트랙트의 주소록 NFT를 전송
⇒ 사용자 계정(EOA) 뿐만 아닌, 스마트 컨트랙트도 계정을 가짐. 이를 Contract Account(;CA)라고 하는데, CA 또한 계정이라 계정 주소로 ERC-20 or ERC-721 토큰을 전송할 수 있음.
→ A 컨트랙트에서 B 컨트랙트 주소 0x5678에 NFT를 전송하면, A 컨트랙트 내부에 해당 NFT의 token ID의 owner가 0x5678로 설정됨.
→ 컨트랙트 계정은 ‘코드’에 의해 동작하기 때문에, 앞선 I 예시로는 I가 A 컨트랙트의 함수를 호출했지만, 컨트랙트 계정에는 ‘A 컨트랙트의 함수 transferFrom() 호출’ 수행 코드가 없다면 수신받은 NFT를 사용할 수 없다.
→ B 컨트랙트에 수신받은 NFT를 다루는 코드가 없을시,
→ NFT 소유권은 B 컨트랙트에 넘어왔으나, B 컨트랙트는 이 NFT를 다룰 수 있는 코드가 없어서 NFT를 사용할 수 없다. 해당 NFT는 존재해도 어디에서도 사용할 수 없이 잠기는 것이다.
⇒ 해당 문제 발생 방지를 위하여 A 컨트랙트는 B 컨트랙트가 NFT를 다룰 준비가 됐는지 여부를 검증함.
# onERC721Received
함수
→ A 컨트랙트에서 safeTransferFrom()
함수 실행
1 | // safeTransferFrom() 내부적 구현 |
→ _transfer()
함수를 실행해서 token ID의 소유권 변경
→ require문 안의 _checkOnERC721Received()
함수 실행
→ _checkOnERC721Received()
함수는 B 컨트랙트에 onERC721Received()
함수가 제대로 구현됐는지 확인하는 함수
1 | // _checkOnERC721Received 함수 |
try
구문 확인하기1
2
3
4
5// _checkOnERC721Received 함수
try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data) returns (bytes4 retval) {
// ...
}to
→ NFT를 수신하는 B 컨트랙트의IERC721Receiver
인터페이스를 통해 구현된**onERC721Received()**
실행
→ B 컨트랙트의 onERC721Received()
함수 구현 확인
1 | // B 컨트랙트 |
→ onERC721Received()
함수 안에 전달받은 NFT를 다루는 함수 작성
→ NFT를 송신한 A 컨트랙트에게 자신이 onERC721Received() 함수를 가지고 있으며, 실행했음을 알려주기 위해 onERC721Received() 함수의 Selector를 반환한다.
- 🔪 모든 컨트랙트 함수는 고유한 Selector 보유 ERC-165(Standard Interface Detection)를 통해서 모든 컨트랙트의 함수는 고유한 Selector를 가진다. 함수의 Selector는 함수의 아이디로 생각하며, Selector를 구하는 두 가지 방식이 있다.
**1. 함수의 시그니처(함수명과 파라미터의 타입)**를 통해 함수의 시그니처를 keccak256으로 암호화하고, bytes4로 형변환.
1
2// balanceOf(address) 함수의 Selector를 구하는 경우
bytes4(keccak256(”balanceOf(address)”))- 컨트랙트 내부 메서드의 Selector 속성을 통함.
1
2
3
4function onERC721Received(address msgSender, address nftContractAddress, uint256 _tokenId, bytes calldata _data) public virtual override returns (bytes4) {
// 전달받은 NFT를 다루는 함수..
return this.onERC721Received.selector;
}
→ B 컨트랙트가 onERC721Received()
함수를 실행한 후, A 컨트랙트는 B 컨트랙트의 onERC721Received()
가 반환된 Selector가 IERC721Receiver
인터페이스 표준에 맞춰 구현된 onERC721Received()
함수인지 확인.
1 | // _checkOnERC721Received 함수 |
→ _checkOnERC721Received()
함수는 인터페이스 표준에 맞게 구현됐는지 여부에 따라서 true
or false
값을 리턴.
1 | function _safeTransfer(address from, address to, uint256 tokenId, bytes memory data) internal virtual { |
→ _checkOnERC721Received()
를 호출한 _safeTransferFrom()
은 **_checkOnERC721Received()
의 반환값**에 따라 정상적으로 transfer가 완료 or 실패로 판단되어 트랜잭션이 취소.
흐름
1 | function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override { |