KoreanFoodie's Study

Solidity 튜토리얼 #3 : 심화 문법 본문

Tutorials/Solidity

Solidity 튜토리얼 #3 : 심화 문법

GoldGiver 2022. 9. 21. 16:24

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

Lesseon 3 에서는 가스 소모와 코드 효율성 등에 대해 공부할 수 있었다.

기존에 zombiefactory.sol 과 zombiefeeding.sol 로 나누어진 코드에 zombiehelper.sol 과 ownable.sol 을 추가했다.

 

 

이번 장을 정리한 내용은 다음과 같다.

1. Immutability of Contracts

이더리움에 컨트랙트를 배포하면, 해당 컨트랙트는 불변(immutable) 이 된다. 따라서, contract 에 문제가 생기면 이를 수정하는 것이 아니라 다른 smart contract 의 주소를 유저에게 제공하는 식으로 패치를 해야 한다.

 

2. Ownable Contracts

만약 어떤 함수를 사용하는데, 주체가 해당 contract 의 소유자만 사용할 수 있게 하려면 어떻게 코드를 짜야 할까?

간단하게 생각하면 함수 내부에 require 한 줄을 넣는 방법도 있겠지만, modifier 라고 불리는 일종의 half-function 을 사용하면 범용적이며 더 간단하게 구현할 수 있다.

실제로 많은 contract 들이 Ownable 이라는, onlyOwner 라는 modifier(함수 호출 시 contract 소유자만 호출할 수 있도록 함) 를 제공하는 contract 를 상속하기도 한다. 해당 Ownable contract 는 OpenZeppelin Solidity Library 에서 제공하고 있다.

Ownable contract sms 다음과 같은 3 가지 기능을 제공한다.

  • 생성자가 owner 를 msg.sender 로 설정
  • owner 만 사용할 수 있는 onlyOwner modifier 추가
  • 새로운 owner 에게 contract 를 transfer 할 수 있게 해줌

이제 코드를 보자.

/**
 * @title Ownable
 * @dev The Ownable contract has an owner address, and provides basic authorization control
 * functions, this simplifies the implementation of "user permissions".
 */
contract Ownable {
  address private _owner;

  event OwnershipTransferred(
    address indexed previousOwner,
    address indexed newOwner
  );

  /**
   * @dev The Ownable constructor sets the original `owner` of the contract to the sender
   * account.
   */
  constructor() internal {
    _owner = msg.sender;
    emit OwnershipTransferred(address(0), _owner);
  }

  /**
   * @return the address of the owner.
   */
  function owner() public view returns(address) {
    return _owner;
  }

  /**
   * @dev Throws if called by any account other than the owner.
   */
  modifier onlyOwner() {
    require(isOwner());
    _;
  }

  /**
   * @return true if `msg.sender` is the owner of the contract.
   */
  function isOwner() public view returns(bool) {
    return msg.sender == _owner;
  }

  /**
   * @dev Allows the current owner to relinquish control of the contract.
   * @notice Renouncing to ownership will leave the contract without an owner.
   * It will not be possible to call the functions with the `onlyOwner`
   * modifier anymore.
   */
  function renounceOwnership() public onlyOwner {
    emit OwnershipTransferred(_owner, address(0));
    _owner = address(0);
  }

  /**
   * @dev Allows the current owner to transfer control of the contract to a newOwner.
   * @param newOwner The address to transfer ownership to.
   */
  function transferOwnership(address newOwner) public onlyOwner {
    _transferOwnership(newOwner);
  }

  /**
   * @dev Transfers control of the contract to a newOwner.
   * @param newOwner The address to transfer ownership to.
   */
  function _transferOwnership(address newOwner) internal {
    require(newOwner != address(0));
    emit OwnershipTransferred(_owner, newOwner);
    _owner = newOwner;
  }
}

여기서 modifier onlyOwner() 을 보면, require 구문과 _; 가 있다. "_;" 는 해당 modifier 가 쓰인 함수에서, modifier 에 정의된 구현을 실행 한 후, 실제 해당 함수를 코드를 동작시키라는 뜻이다. 아래는 해당 modifier 를 적용한 예시이다.

function setKittyContractAddress(address _address) external onlyOwner {
  kittyContract = KittyInterface(_address);
}

 

3. Gas

이더리움 네트워크를 이용해 연산을 수행하려면 gas 라는 비용을 지불해야 한다. 블록체인 네트워크의 storage 에 데이터를 쓰는 것은 큰 비용을 필요로 하는데, 타입을 적절히 설정해서 이 비용을 줄여볼 수 있다. 예시를 보자.

struct NormalStruct {
  uint a;
  uint b;
  uint c;
}

struct MiniMe {
  uint32 a;
  uint32 b;
  uint c;
}

// `mini` will cost less gas than `normal` because of struct packing
NormalStruct normal = NormalStruct(10, 20, 30);
MiniMe mini = MiniMe(10, 20, 30);

 

4. Time Units

"now" 라는 변수는 current unix timestamp of the latest block (since January 1st 1970) 을 리턴한다. 그리고 Solidity 는 이를 변환한 seconds, minutes, hours, days, weeks, years 라는 시간 관련 변수를 제공하고 있다.

uint lastUpdated;

// Set `lastUpdated` to `now`
function updateTimestamp() public {
  lastUpdated = now;
}

// Will return `true` if 5 minutes have passed since `updateTimestamp` was 
// called, `false` if 5 minutes have not passed
function fiveMinutesHavePassed() public view returns (bool) {
  return (now >= (lastUpdated + 5 minutes));
}

 

5. More on Function Modifiers

Function Modifiers 는 매개 변수를 가질 수 있다.

// A mapping to store a user's age:
mapping (uint => uint) public age;

// Modifier that requires this user to be older than a certain age:
modifier olderThan(uint _age, uint _userId) {
  require(age[_userId] >= _age);
  _;
}

// Must be older than 16 to drive a car (in the US, at least).
// We can call the `olderThan` modifier with arguments like so:
function driveCar(uint _userId) public olderThan(16, _userId) {
  // Some function logic
}

 

6. callData

원래 함수가 호출될 때 매개변수가 memory 에 저장된다고 했었다. 그런데 external 함수에 있어서는 memory 대신 callData 에 저장된다.

function changeName(uint _zombieId, string calldata _newName) external aboveLevel(2, _zombieId) {
  require(msg.sender == zombieToOwner[_zombieId]);
  zombies[_zombieId].name = _newName;
}

 

7. Saving Gas With 'View' Functions, because Storage is Expensive!

이전에도 이야기 했지만, 이더리움 네트워크에 값을 쓰거나 변경하는 행위(storage 를 건드리는 행위) 는 매우 큰 gas cost 를 유발한다. 따라서 최대한 gas cost 를 줄일 수 있도록 contract 를 구현해야 한다.

그런데 view 함수는 블록체인 내의 데이터를 수정하지 않으므로, gas cost 가 들지 않는다(web.js 가 local Ethereum node 를 실행). 따라서 사용자가 external view 함수를 사용하도록 구현할 것을 권장한다. 물론 gas 를 지불하는 또 다른 함수 내부에서 호출되는 view 함수는 gas 를 지불한다.

storage 수정 비용이 비싸다 보니, C++ 같은 언어에서는 배열을 리턴하는 함수가 있을 때 기존 데이터를 읽어 참조자로 전달하겠지만, Solidity 에서는 배열을 임시 객체로 새로 생성하는 것이 더 싸게 먹힌다. 다음과 같이 말이다.

function getArray() external pure returns(uint[] memory) {
  // Instantiate a new array in memory with a length of 3
  uint[] memory values = new uint[](3);

  // Put some values to it
  values[0] = 1;
  values[1] = 2;
  values[2] = 3;

  return values;
}

 

 

이제 완성된 코드를 보면서 Wrap-Up 을 하자!

zombiehelper.sol

pragma solidity >=0.5.0 <0.6.0;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

  modifier aboveLevel(uint _level, uint _zombieId) {
    require(zombies[_zombieId].level >= _level);
    _;
  }

  function changeName(uint _zombieId, string calldata _newName) external aboveLevel(2, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].name = _newName;
  }

  function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    zombies[_zombieId].dna = _newDna;
  }

  function getZombiesByOwner(address _owner) external view returns(uint[] memory) {
    uint[] memory result = new uint[](ownerZombieCount[_owner]);
    // Start here
    return result;
  }

}

 

zombiefeeding.sol

pragma solidity >=0.5.0 <0.6.0;

import "./zombiefactory.sol";

contract KittyInterface {
  function getKitty(uint256 _id) external view returns (
    bool isGestating,
    bool isReady,
    uint256 cooldownIndex,
    uint256 nextActionAt,
    uint256 siringWithId,
    uint256 birthTime,
    uint256 matronId,
    uint256 sireId,
    uint256 generation,
    uint256 genes
  );
}

contract ZombieFeeding is ZombieFactory {

  KittyInterface kittyContract;

  function setKittyContractAddress(address _address) external onlyOwner {
    kittyContract = KittyInterface(_address);
  }

  function _triggerCooldown(Zombie storage _zombie) internal {
    _zombie.readyTime = uint32(now + cooldownTime);
  }

  function feedAndMultiply(uint _zombieId, uint _targetDna, string memory _species) public {
    require(msg.sender == zombieToOwner[_zombieId]);
    Zombie storage myZombie = zombies[_zombieId];
    _targetDna = _targetDna % dnaModulus;
    uint newDna = (myZombie.dna + _targetDna) / 2;
    if (keccak256(abi.encodePacked(_species)) == keccak256(abi.encodePacked("kitty"))) {
      newDna = newDna - newDna % 100 + 99;
    }
    _createZombie("NoName", newDna);
    _triggerCooldown(myZombie);
  }

  function feedOnKitty(uint _zombieId, uint _kittyId) public {
    uint kittyDna;
    (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
    feedAndMultiply(_zombieId, kittyDna, "kitty");
  }

}

 

zombiefactory.sol

pragma solidity >=0.5.0 <0.6.0;

import "./ownable.sol";

contract ZombieFactory is Ownable {

    event NewZombie(uint zombieId, string name, uint dna);

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;
    uint cooldownTime = 1 days;

    struct Zombie {
      string name;
      uint dna;
      uint32 level;
      uint32 readyTime;
    }

    Zombie[] public zombies;

    mapping (uint => address) public zombieToOwner;
    mapping (address => uint) ownerZombieCount;

    function _createZombie(string memory _name, uint _dna) internal {
        uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime))) - 1;
        zombieToOwner[id] = msg.sender;
        ownerZombieCount[msg.sender]++;
        emit NewZombie(id, _name, _dna);
    }

    function _generateRandomDna(string memory _str) private view returns (uint) {
        uint rand = uint(keccak256(abi.encodePacked(_str)));
        return rand % dnaModulus;
    }

    function createRandomZombie(string memory _name) public {
        require(ownerZombieCount[msg.sender] == 0);
        uint randDna = _generateRandomDna(_name);
        randDna = randDna - randDna % 100;
        _createZombie(_name, randDna);
    }

}

 

ownable.sol

pragma solidity >=0.5.0 <0.6.0;

/**
* @title Ownable
* @dev The Ownable contract has an owner address, and provides basic authorization control
* functions, this simplifies the implementation of "user permissions".
*/
contract Ownable {
  address private _owner;

  event OwnershipTransferred(
    address indexed previousOwner,
    address indexed newOwner
  );

  /**
  * @dev The Ownable constructor sets the original `owner` of the contract to the sender
  * account.
  */
  constructor() internal {
    _owner = msg.sender;
    emit OwnershipTransferred(address(0), _owner);
  }

  /**
  * @return the address of the owner.
  */
  function owner() public view returns(address) {
    return _owner;
  }

  /**
  * @dev Throws if called by any account other than the owner.
  */
  modifier onlyOwner() {
    require(isOwner());
    _;
  }

  /**
  * @return true if `msg.sender` is the owner of the contract.
  */
  function isOwner() public view returns(bool) {
    return msg.sender == _owner;
  }

  /**
  * @dev Allows the current owner to relinquish control of the contract.
  * @notice Renouncing to ownership will leave the contract without an owner.
  * It will not be possible to call the functions with the `onlyOwner`
  * modifier anymore.
  */
  function renounceOwnership() public onlyOwner {
    emit OwnershipTransferred(_owner, address(0));
    _owner = address(0);
  }

  /**
  * @dev Allows the current owner to transfer control of the contract to a newOwner.
  * @param newOwner The address to transfer ownership to.
  */
  function transferOwnership(address newOwner) public onlyOwner {
    _transferOwnership(newOwner);
  }

  /**
  * @dev Transfers control of the contract to a newOwner.
  * @param newOwner The address to transfer ownership to.
  */
  function _transferOwnership(address newOwner) internal {
    require(newOwner != address(0));
    emit OwnershipTransferred(_owner, newOwner);
    _owner = newOwner;
  }
}

 

 

Comments