관리 메뉴

평행우주 : world 1

[크립토좀비] Solidity Path : 3. 고급 솔리디티 개념 본문

텃밭 1 : BlockChain/Solidity

[크립토좀비] Solidity Path : 3. 고급 솔리디티 개념

parallelworlds 2022. 2. 22. 23:56

 

컨트랙트의 불변성

 

이더리움 DApp에는 일반적인 애플리케이션과는 다른 여러가지 특징이 있다.

이더리움에 컨트랙트를 배포하고 나면, 컨트랙트는 변하지 않는다(Immutable).

다시 말하면 컨트랙트를 수정하거나 업데이트할 수 없다

컨트랙트로 배포한 최초의 코드는 항상, 블록체인에 영구적으로 존재한다

바로 솔리디티에 있어서 보안이 굉장히 큰 이슈인 이유

만약 컨트랙트 코드에 결점이 있다면, 고칠 수 있는 방법이 전혀 없다

반대로, 어떤 스마트 컨트랙트의 코드를 읽고 검증을 했다면, 함수를 호출할 때마다, 코드에 쓰여진 그대로 함수가 실행될 것이며

그 누구도 배포 이후에 함수를 수정하거나 예상치 못한 결과를 발생시키지 못한다

 


소유 가능한 컨트랙트

 

setKittyContractAddress 함수는 external이라, 누구든 이 함수를 호출할 수 있고, 아무나 이 함수를 호출해서 크립트키티 컨트랙트의 주소를 바꿀 수 있고, 모든 사용자를 대상으로 우리 앱을 무용지물로 만들 수 있다

컨트랙트에서 이 주소를 바꿀 수 있게끔 하고 싶지만, 그렇다고 모든 사람이 주소를 업데이트할 수 있기를 원하지는 않을 경우,

컨트랙트를 소유 가능하게 만드는 것이 하나의 방법이다. 컨트랙트를 대상으로 특별한 권리를 가지는 소유자가 있음을 의미한다.

 

OpenZeppelin의 Ownable 컨트랙트

 

 OpenZeppelin은 자네의 DApp에서 사용할 수 있는, 안전하고 커뮤니티에서 검증받은 스마트 컨트랙트의 라이브러리

OpenZeppelin 솔리디티 라이브러리에서 가져온 Ownable 컨트랙트

/**
 * @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 public owner;
  event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

  /**
   * @dev The Ownable constructor sets the original `owner` of the contract to the sender
   * account.
   */
  function Ownable() public {
    owner = msg.sender;
  }

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

  /**
   * @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 {
    require(newOwner != address(0));
    OwnershipTransferred(owner, newOwner);
    owner = newOwner;
  }
}

 

  • 생성자(Constructor): function Ownable()는 생성자. 컨트랙트와 동일한 이름을 가진,생략할 수 있는 특별한 함수. 이 함수는 컨트랙트가 생성될 때 딱 한 번만 실행됨
  • 함수 제어자(Function Modifier): modifier onlyOwner(). 제어자는 다른 함수들에 대한 접근을 제어하기 위해 사용되는 일종의 유사 함수. 보통 함수 실행 전의 요구사항 충족 여부를 확인하는 데에 사용. onlyOwner의 경우에는 접근을 제한해서 오직 컨트랙트의 소유자만 해당 함수를 실행할 수 있도록 하기 위해 사용. 

 

 Ownable 컨트랙트

  1. 컨트랙트가 생성되면 컨트랙트의 생성자가 owner에 msg.sender(컨트랙트를 배포한 사람)를 대입한다.
  2. 특정한 함수들에 대해서 오직 소유자만 접근할 수 있도록 제한 가능한 onlyOwner 제어자를 추가한다.
  3. 새로운 소유자에게 해당 컨트랙트의 소유권을 옮길 수 있도록 한다.

 


onlyOwner 함수 제어자

컨트랙트가 상속되는 구조

ZombieFeeding is ZombieFactory
ZombieFactory is Ownable

 

ZombieFeeding 또한 Ownable이고, Ownable 컨트랙트의 함수/이벤트/제어자에 접근할 수 있다.

이건 향후에 ZombieFeeding을 상속하는 다른 컨트랙트들에도 마찬가지로 적용된다

 

 

함수 제어자

 

함수 제어자는 함수처럼 보이지만, function 키워드 대신 modifier 키워드를 사용한다.

그리고 함수를 호출하듯이 직접 호출할 수는 없다.

대신에 함수 정의부 끝에 해당 함수의 작동 방식을 바꾸도록 제어자의 이름을 붙일 수 있다.

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

 

contract MyContract is Ownable {
  event LaughManiacally(string laughter);

  // 아래 `onlyOwner`의 사용 방법을 잘 보게:
  function likeABoss() external onlyOwner {
    LaughManiacally("Muahahahaha");
  }
}

 

likeABoss 함수를 호출하면, onlyOwner의 코드가 먼저 실행되네.

그리고 onlyOwner의 _; 부분을 likeABoss 함수로 되돌아가 해당 코드 실행

일반적으로 제어자를 사용하는 예시 중 하나는 함수 실행 전에 require 체크를 넣는 것

onlyOwner의 경우에는, 함수에 이 제어자를 추가하면 오직 컨트랙트의 소유자(자네가 배포했다면 자네겠지)만이 해당 함수를 호출할 수 있다

참고: 이렇게 소유자가 컨트랙트에 특별한 권한을 갖도록 하는 것은 자주 필요하지만, 이게 악용될 수도 있다.
예를 들어, 소유자가 다른 사람의 좀비를 뺏어올 수 있도록 하는 백도어 함수를 추가할 수도 있기 때문

이더리움에서 돌아가는 DApp이라고 해서 그것만으로 분산화되어 있다고 할 수는 없다.
따라서 반드시 전체 소스 코드를 읽어보고, 자네가 잠재적으로 걱정할 만한, 소유자에 의한 특별한 제어가 불가능한 상태인지 확인해보자
개발자로서는 자네가 잠재적인 버그를 수정하고 DApp을 안정적으로 유지하도록 하는 것과,
사용자들이 그들의 데이터를 믿고 저장할 수 있는 소유자가 없는 플랫폼을 만드는 것 사이에서 균형을 잘 잡는 것이 중요

 


가스(Gas)

가스 - 이더리움 DApp이 사용하는 연료

 

솔리디티에서는 사용자들이 DApp의 함수를 실행할 때마다 _가스_라고 불리는 화폐를 지불한다.

사용자는 이더(ETH, 이더리움의 화폐)를 이용해서 가스를 사기 때문에,

자네의 DApp 함수를 실행하려면 사용자들은 ETH를 소모하게 된다.

함수를 실행하는 데에 얼마나 많은 가스가 필요한지는 그 함수의 로직(논리 구조)이 얼마나 복잡한지에 따라 달라지네.

각각의 연산은 소모되는 가스 비용(gas cost)이 있고, 그 연산을 수행하는 데에 소모되는 컴퓨팅 자원의 양이 이 비용을 결정하네. 예를 들어, storage에 값을 쓰는 것은 두 개의 정수를 더하는 것보다 훨씬 비용이 높네. 자네 함수의 전체 가스 비용은 그 함수를 구성하는 개별 연산들의 가스 비용을 모두 합친 것과 같네.

함수를 실행하는 것은 자네의 사용자들에게 실제 돈을 쓰게 하기 때문에, 이더리움에서 코드 최적화는 다른 프로그래밍 언어들에 비해 훨씬 더 중요하네. 만약 자네의 코드가 엉망이라면, 사용자들은 자네의 함수를 실행하기 위해 일종의 할증료를 더 내야 할 걸세. 그리고 수천 명의 사용자가 이런 불필요한 비용을 낸다면 할증료가 수십 억 원까지 쌓일 수 있지.

 

가스는 왜 필요한가?

이더리움은 크고 느린, 하지만 굉장히 안전한 컴퓨터와 같다고 할 수 있네. 자네가 어떤 함수를 실행할 때, 네트워크상의 모든 개별 노드가 함수의 출력값을 검증하기 위해 그 함수를 실행해야 하지. 모든 함수의 실행을 검증하는 수천 개의 노드가 바로 이더리움을 분산화하고, 데이터를 보존하며 누군가 검열할 수 없도록 하는 요소이지.

이더리움을 만든 사람들은 누군가가 무한 반복문을 써서 네트워크를 방해하거나, 자원 소모가 큰 연산을 써서 네트워크 자원을 모두 사용하지 못하도록 만들길 원했다네. 그래서 그들은 연산 처리에 비용이 들도록 만들었고, 사용자들은 저장 공간 뿐만 아니라 연산 사용 시간에 따라서도 비용을 지불해야 한다네.

참고: 사이드체인에서는 반드시 이렇지는 않다네. 크립토좀비를 만든 사람들이 Loom Network에서 만들고 있는 것들이 좋은 예시가 되겠군. 이더리움 메인넷에서 월드 오브 워크래프트 같은 게임을 직접적으로 돌리는 것은 절대 말이 되지 않을 걸세. 가스 비용이 엄청나게 높을 것이기 때문이지. 하지만 다른 합의 알고리즘을 가진 사이드체인에서는 가능할 수 있지. 우린 다음에 나올 레슨에서 DApp을 사이드체인에 올릴지, 이더리움 메인넷에 올릴지 판단하는 방법들에 대해 더 얘기할 걸세.

 

 

가스를 아끼기 위한 구조체 압축

 

uint에 uint8, uint16, uint32, 기타 등과 같은 타입들이 있다는 것을 배웠다

솔리디티에서는 uint의 크기에 상관없이 256비트의 저장 공간을 미리 잡아놓기 때문에 이런 하위 타입들을 쓰는 것은 아무런 이득이 없다예를 들자면, uint(uint256) 대신에 uint8을 쓰는 것은 가스 소모를 줄이는 데에 아무 영향이  없다

하지만 바로 struct의 안에서 예외가 하나 있다.

만약 구조체 안에 여러 개의 uint를 만든다면, 가능한 더 작은 크기의 uint를 쓰자

솔리디티는 그 변수들을 더 적은 공간을 차지하도록 압축한다

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

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

// `mini`는 구조체 압축을 했기 때문에 `normal`보다 가스를 조금 사용할 것이네.
NormalStruct normal = NormalStruct(10, 20, 30);
MiniMe mini = MiniMe(10, 20, 30); 

 

  • 이런 이유로, 구조체 안에서는 자네는 가능한 한 작은 크기의 정수 타입을 쓰는 것이 좋다
  • 또한 동일한 데이터 타입은 하나로 묶어두는 것이 좋다.
  • 구조체에서 서로 옆에 있도록 선언하면 솔리디티에서 사용하는 저장 공간을 최소화한다
  • 예를 들면, uint c; uint32 a; uint32 b;라는 필드로 구성된 구조체는 uint32 필드가 묶여있기 때문에 uint32 a; uint c; uint32 b; 필드로 구성된 구조체보다 가스를 덜 소모한다. 

시간 단위

 

readyTime 속성은 좀비가 먹이를 먹거나 공격을 하고 나서 다시 먹거나 공격할 수 있을 때까지 기다려야 하는 "재사용 대기 시간"을 추가하는 것. 이 속성 없이는, 좀비는 하루에 천 번 이상 공격하거나 증식할 수 있게 된다

좀비가 다시 공격할 때까지 기다려야 하는 시간을 측정하기 위해,솔리디티의 시간 단위(Time units)를 사용한다

 

시간 단위(Time units)

 

솔리디티는 시간을 다룰 수 있는 단위계를 기본적으로 제공한다.

 

참고: 유닉스 타임은 전통적으로 32비트 숫자로 저장된다. 만약 유닉스 타임스탬프 값이 32비트로 표시가 되지 않을 만큼 커지게 되면, 많은 구형 시스템에 "Year 2038" 문제가 발생하게 될 것이다. 따라서 DApp이 지금부터 20년 이상 운영되길 원한다면, 64비트 숫자를 써야 한다. 하지만 이렇게 되면 유저들은 그동안 더 많은 가스를 소모해야 하게 된다

 

솔리디티는 또한 seconds, minutes, hours, days, weeks, years 같은 시간 단위 또한 포함한다.

이들은 그에 해당하는 길이 만큼의 초 단위 uint 숫자로 변환된다. 

 1 minutes는 60, 1 hours는 3600(60초 x 60 분), 1 days는 86400(24시간 x 60분 x 60초) 같이 변환된다.

이 시간 단위들이 유용하게 사용될 수 있는 예시

uint lastUpdated;

// `lastUpdated`를 `now`로 설정
function updateTimestamp() public {
  lastUpdated = now;
}

// 마지막으로 `updateTimestamp`가 호출된 뒤 5분이 지났으면 `true`를, 5분이 아직 지나지 않았으면 `false`를 반환
function fiveMinutesHavePassed() public view returns (bool) {
  return (now >= (lastUpdated + 5 minutes));
}

이런 시간 단위들은 좀비의 cooldown 기능을 추가할 때 사용

 


좀비 재사용 대기 시간

이제 Zombie 구조체에 readyTime 속성을 가지고 있으니, zombiefeeding.sol로 들어가서 재사용 대기 시간 타이머를 구현해보도록 하지.

우린 feedAndMultiply를 다음과 같이 수정할 것이네:

  1. 먹이를 먹으면 좀비가 재사용 대기에 들어가고,
  2. 좀비는 재사용 대기 시간이 지날 때까지 고양이들을 먹을 수 없네.

이렇게 하면 좀비들이 끊임없이 고양이들을 먹고 온종일 증식하는 것을 막을 수 있지. 나중에 우리가 전투 기능을 추가하면, 다른 좀비들을 공격하는 것도 재사용 대기 시간에 걸리도록 할 것이네.

먼저, 우리가 좀비의 readyTime을 설정하고 확인할 수 있도록 해주는 헬퍼 함수를 정의할 것이네.

구조체를 인수로 전달하기

자네는 private 또는 internal 함수에 인수로서 구조체의 storage 포인터를 전달할 수 있네. 이건 예를 들어 함수들 간에 우리의 Zombie 구조체를 주고받을 때 유용하네.

문법은 이와 같이 생겼네:

function _doStuff(Zombie storage _zombie) internal {
  // _zombie로 할 수 있는 것들을 처리
}

이런 방식으로 우리는 함수에 좀비 ID를 전달하고 좀비를 찾는 대신, 우리의 좀비에 대한 참조를 전달할 수 있네.

Comments