ERC-721, NFT 스마트 컨트랙트 이해하기(3) - 거래 함수 및 기타 함수 [TIL / Solidity]

알락·2022년 12월 7일
0

이더리움

목록 보기
13/16

solidity banner

권한 확인과 권한 부여 관련한 함수를 이전 장에서 다뤄봤다. 이제껏 다뤘던 함수를 통해서 이번에는 NFT를 다른 계정에 넘겨주는 Transfer 함수를 살펴보려고 한다. 그리고 나머지 다루지 못한 함수들 이를테면 miniting 이나 burn 함수를 확인해보겠다.


거래

⌞ transferFrom

function transferFrom(address from, address to, uint256 tokenId) public virtual override{
  require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved");
  _transfer(from, to, tokenId);
}

이 거래 함수는 컨트랙트 주소에 대한 NFT 소유권 이전 요청에 대한 방지책이 준비되어있지 않다. 이 때문에 이 함수를 사용하는 것보다는 아래에서 설명할 safeTransferFrom 함수를 이용하는 것을 추천하고 있다. 대상 주소가 컨트랙트 주소가 절대 아니라는 것이 보장된다면 문제 없이 사용가능한 함수다.

⌞ _transfer

function _transfer(address from, address to, uint256 tokenId) internal virtual{
  require(ERC721.ownerOf(tokenId) == from, "ERC721: Transfer from incorrect owner");
  require(to != address(0), "ERC721: transfer to zero address");
  
  _beforeTokenTrasnfer(from, to, tokenId, 1);
  
  require(ERC721.ownerOf(tokenId) == from, "ERC721: Transfer from incorrect owner");
  
  delete _tokenApproval[tokenId];
  
  unchecked{
    _balances[from] -= 1;
    _balances[to] += 1;
  }
  _owners[tokenId] = to;
  
  emit Transfer(from, to, tokenId);
  
  _afterTokenTransfer(from, to, tokenId, 1);
}

최종적으로 NFT가 소유권이 이전 될 때 실행되는 함수다. _owners[tokenId]=to;가 실행되는 순간 해당 NFT는 to 계정에 소유권이 이전되어 블록체인 데이터에 저장된다.

그 이전에 확인해야될 사항들이 존재한다. from 계정 주소가 우선 거래 해당 토큰의 주인이 맞는지 확인한다. 그리고 to 계정 주소가 zero 계정 주소가 아닌지 확인해야 한다. zero에게 전달된다는 것은 해당 NFT가 다시 무로 돌아가 버린다는 것이기 때문이다.

_beforeTokenTransfer_afterTokenTransfer는 이 함수 실행중에 또 다른 명령을 처리할 수 있게 Hook의 역할을 한다. 소유권 이전 전후로 관련 작업을 또 하고 싶다면 이 두 함수를 오버라이드해서 구현하여 사용하면 되겠다.

새로운 주인에게 소유권이 이전되었으니, 기존 주인이 해당 NFT에 설정한 권한 부여를 초기화해야 한다. 이 부분이 delete _tokenApproval[tokenId]; 이다.

기존 주인의 NFT 소유 개수를 차감하고, 새로운 주인의 NFT 소유 개수를 늘리는 작업을 해주고 나서 비로소 주인이 바뀌는 명령문을 실행한다.

블록에 소유권 이전에 대한 데이터를 작성해주기 위한 명령이 emit Transfer(from, to, tokenId); 이다.

⌞ safeTransferFrom

function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override{
	safeTransferFrom(from, to, tokenId, "");
}

function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data){
  require(isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved");
  _safeTransfer(from, to, tokenId, data);
}

이 거래 함수는 현재 오버로딩으로 구현되어 있다. 결과적으로는 data 인자값을 받기 위함이다. 이 data값은 추후에 NFT를 전송받는 계정 주소가 컨트랙트 일 때, 컨트랙트가 NFT를 받아들일 준비가 되었는지 확인하기 위해서 받는 값이다. 이 때문에 이 함수가 기존 transferFrom 보다 사용하기에 안전한 함수이다. 컨트랙트가 NFT를 받아들일 준비가 안되어있다면 NFT는 증발해버리기 때문이다.(받을 수는 있지만 받고나서 사용이 불가능하다. 컨트랙트 계정은 트랜잭션을 발생시키지 못하기 때문이다.)

⌞ _safeTransfer

function _safeTransfer(address from, address to, uint256 tokenId, bytes memory data){
	_transfer(from, to, tokenId);
  	require(_checkOnERC721Received(from, to, tokenId, data), "ERC721: transfer to non ERC721Reeiver implementer");
}

위 함수는 safeTransferFrom으로 실행되는 NFT 거래 중 실행하는 함수 중 최종적인 성격을 띠는 함수다. 안에서 실행되는 _transfer함수는 transferFromsafeTransferFrom 두 거래 함수에서 사용되고 있는 것을 확인할 수 있다. _safeTransfer의 결정적인 차이는 마지막에 _checkOnERC721Received 함수의 리턴값을 확인한다는 것이다. 이 이유는 위 safeTransferFrom 설명에서 설명한 바 있다.


기타 함수

지금부터 소개하는 함수들은 ERC-721 에서 공식적으로 요구하는 사항은 아니다. 하지만 현실적으로 NFT 구현 운영할 때 필요하게 될 함수여서 openZeppelin 에서는 언제나 override하거나 새로운 함수를 만들어 사용할 수 있게 구현해놓고 있다.

⌞ _mint

function _mint(address to, uint256 tokenId) internal virtual{
  require(to != address(0), "ERC721: mint to zero address");
  require(!_exists(tokenId), "ERC721: token already minted");
  
  _beforeTokenTransfer(address(0), to, tokenId, 1);
  
  require(!_exists(tokenId), "ERC721: token already minted");
  
  unchecked{
    _balances[to] += 1;
  }
  _owners[tokenId] = to;
  
  emit Transfer(address(0), to, tokenId);
  
  _afterTokenTransfer(address(0), to, tokenId, 1);
}

위 함수는 사뭇 _transfer 함수와 비슷해 보인다. 사실 인자의 from이 없는 transfer 함수가 맞다. 하지만 아직 발행이 안 되어 있는 토큰에 한해서만 사용 가능하다는 것을 알아야 한다. 이는 require(!_exists(tokenId))에서 검사를 해주고 있다.

관련 함수

⌞ _safeMint

function _safeMint(address to, uint256 tokenId) interanl virtual{
  _safeMint(to, tokenId, data);
}

function _safeMint(address to, uint256 tokenId, bytes memory data) interanl virtual{
  _mint(to, tokenId);
  require(_checkOnERC721Received(address(0), to, tokenId, data), "ERC721 : transfer to non ERC721Receiver implementer");
}

safeTransferFrom 의 mint 함수 버젼이라고 생각하면 된다.

⌞ _burn

function _burn(uint256 tokenId) internal virtual{
  address owner = ERC721.ownerOf(tokenId);
  
  _beforeTokenTransfer(owner, address(0), tokenId, 1);
  
  owner = ERC721.ownerOf(tokenId);
  
  delete _tokenApprovals[tokenId];
  
  unchecked{
    _balances[owner] -= 1;
  }
  delete _owners[tokenId];
  
  emit Transfer(owner, address(0), tokenId);
  _afterTokenTransfer(owner, address(0), tokenId, 1);
}

위 함수는 _mint 함수와 정반대의 함수다. 현 토큰에 대한 소유권을 다 지워버리는 작업을 한다. 해당 tokenId를 갖는 NFT는 _burn 함수 실행 이후 소유자가 zero address로 초기화 된다.


정리

이것으로 ERC721에서 구현하고 있는 모든 함수를 확인해봤다. 이제 이 구조만 잘 인지하면서 추가기능이 필요할 때 기존 NFT의 형식을 유지하면서 스마트 컨트랙트를 운영할 수 있게 될 것이다.

profile
블록체인 개발 공부 중입니다, 프로그래밍 공부합시다!

0개의 댓글