[솔리디티] 크립토좀비 레슨2: Zombies Attack Their Victims

가영·2021년 2월 28일
0

솔리디티

목록 보기
2/7
post-thumbnail

Mapping과 Address : 새로운 자료형

데이터베이스에 저장된 좀비들에게 주인을 설정해서 게임을 멀티 플레이어 게임으로 만들어보자. 이걸 위해서는 mappingaddress라는 새로운 자료형이 필요하다!

Address

이더리움 블록체인은 은행 계좌와 같은 계정들로 이뤄져있다. 계정은 이더리움 블록체인 상의 화폐인 이더의 잔액을 가진다. 내 은행계좌에서 다른 계좌롤 돈을 송금할 수 있듯이, 계정을 통해 다른 계정과 이더를 주고 받는 것이다.

각 계정은 은행 계좌 번호와 같은 고유한 address를 가지고 있다. 다음과 같이 표현된다: 0x0cE446255506E92DF41614C46F1d6df9Cc969183

그니까, 이런 address를 좀비 객체의 주인을 식별하는 데 활용할 수 있겠다❣

Mapping

레슨 1에서 구조체와 배열을 살펴 봤는데, mapping은 구조화된 데이터를 저장하는 또 다른 자료형이라고 보면된다. (그냥 Map인 것 같기도.)

아래와 같이 mapping을 정의한다.

mapping ([key 자료형] => [value 자료형]) [접근제어자] [변수명]

mapping (address => unit) public accountBalnce;
mapping (uint => string) userIdToName;

사용은 이렇게 한다.

accountBalance[주소]; // 계좌 잔액
userIdToName[유저아이디]; // 유저 이름

우리는 좀비 소유자를 추적하는 매핑을 다음과 같이 만들 수 있다. 이 매핑을 쓰면 좀비의 주인만 좀비를 다룰 수 있게 할 수 있겠지😊

mapping (uint => address) public zombieToOwner;

Msg.sender

좀비 소유자를 추적하는 매핑을 만들었다. 그럼 행위를 하려는(함수를 호출한) 사용자의 주소는 어떻게 가져올까? 정답은 msg.sender를 사용하는 것이다.

이런 식으로 하면 될 것이다.

function _createZombie(string _name, uint _dna) private {
	uint id = zombies.push(Zombie(_name, _dna)) - 1;
	zombieToOwner[id] = msg.sender;
	NewZombie(id, _name, _dna); // emit event
}

require

유저가 createRandomZombie 함수를 호출해서 무제한으로 좀비를 생성하게 되면 게임이 재미가 없을 것이다. 🤭

그래서 각 플레이어가 이 함수를 한 번만 호출할 수 있도록 만들어보자. 이로써 새로운 플레이어들이 게임을 처음 시작할 때 좀비 군대를 구성할 첫 좀비를 생성하기 위해 딱 한 번 createRandomZombie 함수를 호출할 것이다.

어떻게?

바로 require를 활용하는 것이다. require를 쓰면 설정한 조건이 참이 아닐 때 함수가 에러메시지를 발생하고 실행을 멈춘다.

우리는 유저가 소유한 좀비의 수가 0일 때만 함수가 실행되게 해야하므로 일단 이를 구현하기 위해 사용자 주소와 좀비수의 mapping을 만들어준다.

mapping (address => uint) ownerZombieCount;

그리고 나서 createRandomZombie를 수정해준다.

function crateRandomZombie(string _name) public {
	require(ownerZombieCount[msg.sender] == 0);
	// 가지고 있는 좀비가 있을 경우 함수가 실행을 중단한다.
	createZombie(_name, _generateRandomDna(_name));
}

상속

엄청나게 긴 컨트랙트 하나를 만들기보다는 코드를 잘 정리해서 여러 컨트랙트에 코드 로직을 나누는 것이 합리적이다. 우리는 상속을 이용해서 로직을 나눌 때가 있다. 상속이나 구현을 이용하면 같은 메서드, 변수들을 손쉽게 옮겨줄 수 있다. 솔리디티에서도, 다른 언어처럼 상속 기능을 제공한다!

상속은 다음과 같이 할 수 있다. ✨

contract Doge {
  function catchphrase() puvlic returns (string) {
    return "So Wow CryptoDoge";
  }
}
contract BabyDoge is Doge { // Doge를 상속 받음
  function anotherCathphrase() public returns (string) {
    return "Such Moon BabyDoge";
  }
}

쉽다!

우리는 좀비들이 먹이를 먹고 번식하도록 하는 기능을 새로운 컨트랙트에서 구현할 것이다. 그 기능의 로직을 ZombieFactory의 모든 메소드를 상속하는 클래스에 넣어볼것이다.

contract ZombieFeeding is ZombieFactory {
  // ...
}

그냥 is ZombieFactory만 컨트랙트명 뒤에 붙여주면 된다. 참 쉽죠? 🤸🏻‍♀️

Import

코드가 꽤나 길어지고 있으므로, 코드를 여러 파일로 나누어 관리해야할 필요가 생겼다. 다른 언어들과 마찬가지로, 솔리디티도 외부 파일을 불러오는 키워드를 가지는데, 바로 import다.

파일 여러개가 있을 때 import '파일경로'; 를 해주면 그 파일을 컴파일러가 불러오게 된다. 방금 새로 만든 ZombieFeeding 컨트랙트를 다른 파일에 놓으려면 ZombieFeeding이 상속받은 ZombieFactory의 메서드와 변수들을 불러와야할 것이다. 그래서 다음처럼 해주면 된다.

// zombiefeeding.sol
import "./zombiefactory.sol";

contract ZombieFeeding is ZombieFactory {
  // ...
}

Storage vs Memory 키워드

솔리디티에는 변수를 저장할 수 있는 공간으로 storagememory 두 가지가 있다. 변수를 선언할 때 원하는 키워드를 앞에 붙여주면 된다.

Storage(스토리지)는 블록체인에 영구적으로 저장되는 변수 앞에 붙이는 키워드다. Memory는 반대로 임시적으로 저장되는 변수다. 컨트랙트 함수에 대한 외부 함수 호출 동안에 지워진다.. (?) (Memory variables are temporary, and are erased between external function calls to your contract.)

대부분의 경우, 이런 키워드를 우리가 직접 쓸 필요는 없다. 솔리디티가 기본적으로 인식하고 추가해주기 때문이다. 기본적으로, 상태변수는 storage로, 함수 안의 변수들은 memory로 선언된다. 그리고 함수안의 변수들은 함수 호출이 끝나면 사라진다!

하지만 이런 키워드들을 명시적으로 써줘야하는 경우가 있는데, 바로 구조체와 배열을 다룰 때이다. 구조체와 배열을 선언할 때는 키워드를 써줘야 컴파일이 된다. 쓰지 않을 경우 솔리디티가 워닝을 준다. 🙄

배열을 예시로 보면,

function eatSandwich(uint _index) public {

  //1. in which case 'mySandwich' is a pointer to sandwiches[_index] on the blockchain.
  Sandwich storage mySandwich = sandwiches[_index];
  mySandwich.status = "Eaten!";
  
  //2. in which case 'anotherSnadwich' will simply copy of sandwiches[_index + 1]
  Sandwich memory anotherSandwich = sandwiches[_index + 1];  
  anotherSandwich.status = "Eaten!";
  sandwiches[_index + 1] = anotherSandwich;
}

먼저 storage 변수인 mySandwich를 보면, mySandwich.status="Eaten!"으로 상태변수의 값을 직접 바꾸는 state임을 알 수 있다.

반면, memory 변수인 anotherSandwich는 단순히 값 자체만 같은 변수이기 때문에 sandwiches 배열의 값을 바꾸려면 직접 대입해줘야한다. (sandwiches[_index + 1] = anotherSandwich)


이제 우리의 좀비가 먹이를 먹고 수를 늘릴수 있는 능력을 주자!

좀비가 다른 생물을 먹으면 좀비의 DNA는 그 먹이의 DNA와 합쳐져 다른 새로운 좀비를 만들게 할 것이다.

// zombiefeeding.sol
function feedAndMultiply(uint _zombieId, uint _targetDna) public {
  require(msg.sender == zombieToOwner[_zombieId]);
  Zombie storage myZombie = zombies[_zombieId];
  
  _targetDna = _targetDna % dnaModulus;
  uint newDna = (myZombie.dna + _targetDna) / 2;
  _crateZombie("NoName", newDna);
}

More on Function Visibility

Intener and External

앞선 _createZombie()함수는 ZombieFactory 컨트랙트에서 private으로 선언했기 때문에 사실 위의 코드는 오류가 날 수밖에 없다. 😏

사실 함수 접근제어자는 두가지가 더 있는데, 바로 internalexternal이다.

Internal

internal은 private과 거의 같다. 다른 점은 함수가 선언돼있는 컨트랙트를 상속받은 컨트랙트에서는 internal로 선언된 함수를 사용할 수 있다. (자바의 protected와 비슷한건가!)

External

external은 public과 비슷하지만 컨트랙트 바깥에서만 사용될 수 있다는 것이다. 컨트랙트 안의 다른 함수들에서는 사용될 수가 없다. public과 구분되어야만 하는 이유는 다른 레슨에서 알 수 있다고 한다🥰


아무튼, 그래서 우리가 위에서 _createZombie() 함수를 사용하려면 다시 ZombieFactory 컨트랙트로 돌아가서 _createZombie함수의 접근제어자를 internal로 바꿔줘야 하는 것이다.

// zombiefactory.sol

contract ZombieFactory {
  ...
  function _createZombie(string _name, uint _dna) internal {
    ...
  }
}

Getter 사용하기 - external

좀비가 가장 좋아하는 먹이는?

크립토키티! 😻

우리는 좀비가 키티를 먹었을 때 생겨나는 새로운 좀비의 정보를 만들기 위해 키티의 dna를 얻어야할 것이다. dna와 같은 크립토키티의 정보는 볼록체인 상에 데이터로 존재한다. 크립토키티 컨트랙트를 가지고 있지 않은 우리가 어떻게 키티의 정보를 가져와야할까?

Interface

블록체인 상에 있으면서 우리가 소유하지 않은 컨트랙트의 변수를 가져오기 위해서는 인터페이스를 정의해야한다.

우리는 크립토키티 컨트랙트의 getKitty()를 사용하여 키티의 정보를 빼와야한다. 그러기 위해서는 인터페이스에 getKitty()함수를 선언해야한다.

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
  );
}

자바의 인터페이스처럼, 함수의 바디는 쓰지 않고 파라미터를 뒤에 세미콜론을 써주면 된다. 저렇게 키티 인터페이스를 작성해주면 우리 컨트랙트에서 getKitty() 함수를 쓸 수 있게 된다.

Handling Multiple Return Values

솔리디티는 다른 언어들과는 다르게 리턴값이 여러 개일 수 있다. 리턴값이 여러개인 함수를 어떻게 사용할 수 있을까?

예시다!

function multipleReturns() internal returns(uint a, uint b, uint c) {
  return (1, 2, 3);
}

function processMultipleReturns() external {
  uint a, b, c;
  // 이렇게 하면 multiple assignment가 된다.
  (a, b, c) = multipleReturns();
}

function getLastReturnValue() external {
  uint c;
  (,,c) = multipleReturns();
}

그럼 우리가 아까 인터페이스에 선언해놓았던 getKitty()를 이용해서 gene만 쏙 빼오는 것도 가능해진 것이다~

다음과 같이 하면 된다.

function feedOnKitty(uint _zombieId, uint _kittyId) public {
    uint kittyDna;
    // 앞에 파라미터가 9개 있으므로 dna만 얻기 위해 컴마를 9개 찍는다.
    (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
    // And modify function call here:
    feedAndMultiply(_zombieId, kittyDna);
  }

그럼 끝!


전체 코드 🤩

//zombiefactory.sol

pragma solidity >=0.5.0 <0.6.0;

contract ZombieFactory {

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

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    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;
        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);
    }

}
//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 {

  address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
  KittyInterface kittyContract = KittyInterface(ckAddress);

  // Modify function definition here:
  function feedAndMultiply(uint _zombieId, uint _targetDna) public {
    require(msg.sender == zombieToOwner[_zombieId]);
    Zombie storage myZombie = zombies[_zombieId];
    _targetDna = _targetDna % dnaModulus;
    uint newDna = (myZombie.dna + _targetDna) / 2;
    // Add an if statement here
    _createZombie("NoName", newDna);
  }

  function feedOnKitty(uint _zombieId, uint _kittyId) public {
    uint kittyDna;
    (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
    // And modify function call here:
    feedAndMultiply(_zombieId, kittyDna);
  }

}

0개의 댓글