KoreanFoodie's Study

Solidity 튜토리얼 #5 : ERC721 과 SafeMath 본문

Tutorials/Solidity

Solidity 튜토리얼 #5 : ERC721 과 SafeMath

GoldGiver 2022. 9. 22. 16:57

크립토 좀비에서 제공하는 튜토리얼을 통해 배우는 Solidity 문법을 정리하고 있습니다!

Lesseon 5 에서는 ERC721 의 적용과 SafeMath 에 대한 간단한 예제를 다루고 있다.
기존 코드에 zombieownership.sol 을 추가했다.

1. ERC721

우리가 흔히 NFT 라고 부르는 토큰은, 각 토큰이 'unique' 하다는 특징을 가진다. 예를 들어, "Steve" 라는 이름의 좀비가 있고, 이것을 토큰으로 만들면, 다른 이름을 가진 토큰들과는 구별되어야(distinguishable) 한다.
또한 ERC721 토큰은 쪼갤 수 없다. "Steve" 라는 좀비가 있다고 했을 때, 0.237 만큼의 "Steve" 로 쪼개어 보낼 수 없다는 뜻이다(물론 최근에는 NFT 도 쪼개는 논의가 이루어지고 있긴 하지만, 그 부분은 여기서는 다루지 않는 것으로 한다).

먼저, ERC721 표준 인터페이스를 한 번 살펴보자.

pragma solidity >=0.5.0 <0.6.0;

contract ERC721 {
  event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
  event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);

  function balanceOf(address _owner) external view returns (uint256);
  function ownerOf(uint256 _tokenId) external view returns (address);
  function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
  function approve(address _approved, uint256 _tokenId) external payable;
}

우리가 실제 ERC721 토큰을 구현하게 되면, _transfer 같은 internal 함수를 구현하고, transferFrom 에서 _transfer 을 호출해 여러가지 작업을 하게 만들어야 할 것이다.

ERC721 에는 토큰을 보내는 두 가지 방법이 있다.

function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

또는

function approve(address _approved, uint256 _tokenId) external payable;

function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

첫 번째 방법은 _from 의 주소로부터 _to 의 주소로 주어진 _tokenId 에 맞는 토큰을 보내는 것이다.
두 번째 방법은 먼저 approve 를 호출해 transferFrom 을 할 수 있는 주소를 등록한 후, transferFrom 을 호출하는 것이다.

approve 와 _transfer 을 구현할 때는 erc721.sol 에 정의된 이벤트(각각 Approval, Transfer) 을 fire 해 주어야 한다.

function _transfer(address _from, address _to, uint256 _tokenId) private {
  ownerZombieCount[_to]++;
  ownerZombieCount[_from]--;
  zombieToOwner[_tokenId] = _to;
  // 여기서 Transfer 이벤트를 fire
  emit Transfer(_from, _to, _tokenId);
}

function transferFrom(address _from, address _to, uint256 _tokenId) external payable {
  require (zombieToOwner[_tokenId] == msg.sender || zombieApprovals[_tokenId] == msg.sender);
  _transfer(_from, _to, _tokenId);
}

function approve(address _approved, uint256 _tokenId) external payable onlyOwnerOf(_tokenId) {
  zombieApprovals[_tokenId] = _approved;
  // 여기서 Approval 이벤트를 fire
  emit Approval(msg.sender, _approved, _tokenId);
}


2. SafeMath

Overflow 와 Underflow 문제를 해결하기 위해, OpenZepplin 에서 SafeMath 라는 라이브러리를 제공하고 있다. 이 라이브러리를 통해 연산을 하면 Overflow 와 Underflow 문제를 해결할 수 있다. SafeMath 라이브러리의 예시를 보자.

pragma solidity >=0.5.0 <0.6.0;

/**
 * @title SafeMath
 * @dev Math operations with safety checks that throw on error
 */
library SafeMath {

  /**
  * @dev Multiplies two numbers, throws on overflow.
  */
  function mul(uint256 a, uint256 b) internal pure returns (uint256) {
    if (a == 0) {
      return 0;
    }
    uint256 c = a * b;
    assert(c / a == b);
    return c;
  }

  /**
  * @dev Integer division of two numbers, truncating the quotient.
  */
  function div(uint256 a, uint256 b) internal pure returns (uint256) {
    // assert(b > 0); // Solidity automatically throws when dividing by 0
    uint256 c = a / b;
    // assert(a == b * c + a % b); // There is no case in which this doesn't hold
    return c;
  }

  /**
  * @dev Subtracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend).
  */
  function sub(uint256 a, uint256 b) internal pure returns (uint256) {
    assert(b <= a);
    return a - b;
  }

  /**
  * @dev Adds two numbers, throws on overflow.
  */
  function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    assert(c >= a);
    return c;
  }
}

위의 SafeMath 는 uint256 타입을 기본으로 받는다. 참고로, uint256 은 uint 와 동치이다. 그리고 uint32 와 uint16 을 위한 SafeMath32, SafeMath16 이 있다. 내부 구현은 기본적인 SafeMath 와 동일하다.

사용법은 다음과 같다.

using SafeMath for uint256;
using SafeMath32 for uint32;
using SafeMath16 for uint16;


uint myUint = 0;
uint myUint32 = 0;
uint myUint16 = 0;

// myUint++; 대신 아래와 같이 표현
myUint = myUint.add(1);
myUint32 = myUint32.add(1);
myUint16 = myUint16.add(1);

3. natspec

마지막으로, Solidity 에서 권장하는 주석 스타일을 알아보자. 예시는 다음과 같다.

/// @title A contract for basic math operations
/// @author H4XF13LD MORRIS 💯💯😎💯💯
/// @notice For now, this contract just adds a multiply function
contract Math {
  /// @notice Multiplies 2 numbers together
  /// @param x the first uint.
  /// @param y the second uint.
  /// @return z the product of (x * y)
  /// @dev This function does not currently check for overflows
  function multiply(uint x, uint y) returns (uint z) {
    // This is just a normal comment, and won't get picked up by natspec
    z = x * y;
  }
}

모든 태그는 쓸 수도, 안 쓸 수도 있지만, 각 함수가 어떤 동작을 하는지 @dev 에 설명을 남겨주면 좋다.


이제 완성된 코드를 보면서 Wrap-Up 을 하자! 이전 글에서 추가된 파일만 아래에 정리했다.

zombieownership.sol

pragma solidity >=0.5.0 <0.6.0;

import "./zombieattack.sol";
import "./erc721.sol";
import "./safemath.sol";

/// TODO: Replace this with natspec descriptions
contract ZombieOwnership is ZombieAttack, ERC721 {

  using SafeMath for uint256;

  mapping (uint => address) zombieApprovals;

  function balanceOf(address _owner) external view returns (uint256) {
    return ownerZombieCount[_owner];
  }

  function ownerOf(uint256 _tokenId) external view returns (address) {
    return zombieToOwner[_tokenId];
  }

  function _transfer(address _from, address _to, uint256 _tokenId) private {
    ownerZombieCount[_to] = ownerZombieCount[_to].add(1);
    ownerZombieCount[msg.sender] = ownerZombieCount[msg.sender].sub(1);
    zombieToOwner[_tokenId] = _to;
    emit Transfer(_from, _to, _tokenId);
  }

  function transferFrom(address _from, address _to, uint256 _tokenId) external payable {
    require (zombieToOwner[_tokenId] == msg.sender || zombieApprovals[_tokenId] == msg.sender);
    _transfer(_from, _to, _tokenId);
  }

  function approve(address _approved, uint256 _tokenId) external payable onlyOwnerOf(_tokenId) {
    zombieApprovals[_tokenId] = _approved;
    emit Approval(msg.sender, _approved, _tokenId);
  }

}




 
Comments