Solidity

Cryptozombie - Ch1. Making Zombie Factory

Sila 2023. 12. 3. 01:11

컨셉은 좀비 군대를 생성하는 게임 컨트랙트를 만드는 것이다.

 

직접 컨트랙트를 만들어보면서 Solidity라는 언어의 기본적인 문법들과

 

다른 언어들과 비교했을 때 가지는 특성들을 알아보자.

 

챕터1에서는 좀비를 생성하는 공장을 만든다.

1. 목표

- 모든 좀비의 정보를 가진 db를 생성한다.

- 새로운 좀비를 생성하는 함수를 가진다.

- 각 좀비는 고유의 DNA를 가지며, 이 DNA로부터 외모가 결정된다.

 

1-1. DNA로 부터 외모를 결정하는 메커니즘

좀비 DNA는 16자리 정수로 이루어지는데, 첫 두 자리는 머리, 그 다음 두 자리는 눈을 결정한다.

 

첫 두자리를 7로 나눈 후 1을 더한 값이 머리 모양이다.

 

가령 첫 두 자리가 83 이라면 83%7 + 1 = 7, 7번 머리를 가지게 된다. 

 

2. Contract

솔리디티는 컨트랙트를 만드는 언어이다.

 

Eth의 모든 변수, 함수는 컨트랙트에 속하며, 모든 프로젝트의 시작점이 된다.

 

내부에 관련된 메소드, 변수들이 있는게 class와 비슷한 것 같다.

 

2-1. version Pragma

모든 솔리디티 소스 코드는 version pragma로 시작하는데, 이는 해당 코드가 이용하는 솔리디티 버전을 의미한다.

 

새로운 컴파일러 버전이 나와도 기존 코드가 깨지지 않도록 해준다.

 

우리는 0.5.0이상, 0.6.0 미만의 컴파일러 버전을 사용할 수 있도록 한다. 

 

pragma solidity >=0.5.0 <0.6.0

contract MyContract {

}

 

2-2. 변수, 구조체, 배열

1. 변수

상태 변수는 컨트랙트에 영구적으로 저장된다. 즉, 블록체인에 기록되는데 db에 데이터를 쓰는 것과 같다.

 

type과 함께 변수 명을 적고, 값을 대입하면 된다.

uint a = 10;
string b = "aa";

 

uint는 부호가 없는 정수라는 뜻으로, 256바이트를 사용하는 uint256 타입과 동일한 의미이다.

 

uint4, uint8 등의 uint 타입도 있다. 

 

1-1. 타입 변환

종종 타입 변환을 해야할 때가 있다. 

 

uint는 uint256과 동일한 의미인데, 종종 uint8, uint16 등 다른 타입 간의 연산이 필요한 때가 있기 때문.

 

uint8 a = 5;
uint b = 6;

uint8 c = a * b;
// error : 서로 다른 타입간의 연산은 불가

uint8 c = a * uint8(b)
// b를 타입 변환해야 연산 가능

 

2. 구조체

구조체는 js/ts의 그것과 동일하다. 복잡한 자료형을 다루기 위해 객체 타입을 만들어준다고 생각하면 된다.

struct Zombie {
        string name;
        uint dna;
}

 

3. 배열

정적 배열과 동적 배열로 나뉜다. 원소 수가 고정 되어있는지의 여부가 기준이 된다.

 

public으로 선언하면 다른 컨트랙트에서 이를 읽을 수 있게 된다. (쓸 수는 없다.)

Zombie[] public zombies

 

push method를 이용해 배열에 원소를 추가할 수 있다.

 

3. 함수

여러 가지 속성이 있다. 외부에서의 접근성, 데이터를 읽고 쓰는지의 여부 등

 

여러 가지가 있지만 장차 알아가도록 하고, 지금은 기본적인 것들부터 배운다.

 

- 함수 선언

함수 선언은 다음과 같이 한다.

 

function 키워드를 적고, 함수명, 매개변수를 쭉 적는데,

 

타입뿐만 아니라 컨트랙트 외부로의 공개 여부, 매개 변수의 저장 장소, 

 

함수가 컨트랙트의 변수와 상호작용하는 정도 등을 전부 표기한다.

function eatHamburgurs(string memory _name, uint _amount) public {
        // do sth...
}

 

이 함수의 매개변수 _name을 보면, 타입인 string뿐만 아니라, `memory`라는 키워드도 포함되어 있는데,

 

이는 이 매개변수가 메모리에 저장되어야 한다는 것을 의미한다.

 

매개변수 뒤의 public은 이 함수가 외부로 공개된다는 것을 뜻한다.

 

- private/public

함수는 public가 디폴트인데, 이 경우 누구든 그 함수를 호출할 수 있게 된다.

 

이는 보안 상 문제를 야기하므로 떄로는 함수를 private으로 선언해 외부로부터의 접근을 제한해야할 때가 있다.

 

uint[] numbers;

function _addToArray(uint _number) private {
        numbers.push(_number)
}

 

이렇게 private으로 선언하면 동일 컨트랙트 내의 다른 함수들만이 이 함수를 호출할 수 있다.

 

보통 관례상 private 함수는 함수명 앞에 언더바를 붙여준다.

 

- 반환값

함수 리턴 값의 타입도 언급한다.

string greeting = "Hello";

function sayhello() public returns (string memory) {
        return greeting;
}

 

- 함수 제어자

위 함수는 컨트랙트 내의 특정 상태를 바꾸거나 하지는 않는다.

 

이런 함수는 선언 시 `view` 키워드를 붙여준다.

 

데이터를 바꾸지는 않고 보여주기만 한다는 뜻이다.

string greeting = "Hello";

function sayhello() public view returns (string memory) {
        return greeting;
}

 

 

`pure` 라는 키워드도 있는데, 이는 함수가 컨트랙트의 어떤 데이터에도 접근하지 않는다는 것을 의미한다.

 

매개변수를 받아 연산만을 수행하는 함수들이 이렇다.

function _multiply(uint a, uint b) private pure returns(uint) {
        return a * b;
}

 

4. 이벤트

`event`는 블록체인에서 일어나는 일들을 알리기 위해 사용된다.

 

이벤트를 정의하고, 함수 내에서 `emit` 키워드로 호출함으로써 해당 함수가 호출되었다는 것을 알릴 수있다.

 

event IntegersAdded (uint x, uint y, uint result);

function add(uint _x, uint _y) public returns (uint) {
        uint result = _x + _y;

        // 함수가 호출되었다는 걸 app에 알리기 위해 event를 emit
        emit IntegersAdded(_x, _y, result);
        return result;
}

 

이렇게하면 이 방출된 이벤트를 프론트 단에서 감지할 수 있다.

 

5. 컨트랙트 작성

이제 목표에 부합하는 컨트랙트를 작성해보자.

 

1. 우선 솔리디티 파일을 만들고 (확장자 : .sol), 컨트랙트를 생성한다.

pragma solidity >=0.5.0 <0.6.0;

contract ZombieFactory {

}

 

 

2. 좀비 dna는 16자리 정수이며, 특정 값을 10^16으로 나눈 나머지값이 dna가 된다.

pragma solidity >=0.5.0 <0.6.0;

contract ZombieFactory {
        uint dnaDigits = 16;
        uint dnaModulus = 10 ** dnaDigits;
}

 

 

3. 좀비는 이름과 dna를 가진 구조체로 다음과 같이 정의할 수 있다.

 

각 구조체들을 저장할 배열도 만들어준다.

 

struct Zombie {
        string name;
        uint dna;
}

Zombie[] public zombies;

 

 

4. 문자열을 이용해 dna를 만드는 함수를 생성한다.

 

우선 dna부터 만들어야 한다.

 

이더리움은 keccak256이라는 SHA3 버전 내장 해시 함수를 가지고 있다.

 

해시 함수는 인풋을 무작위 256비트 16진수로 매핑한다.

 

단, 매개 변수 타입으로 `bytes`를 요구하므로 매개 변수에 넣기전 추가적인 packing을 거쳐야 한다.

 

cf) bytes는 가변 길이의 바이트 배열을 나타내며, 0~32바이트까지의 데이터를 저장할 수 있다.

 

이미지, 파일 등의 이진 데이터를 저장하는데 적합하다.

keccak256(abi.encodePacked("aaaab"));
//6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5

keccak256(abi.encodePacked("aaaac"));
//b1f078126895a1424524de5321b339ab00408010b7cf0e6ed451514981e58aa9

 

 

이 해시 함수를 이용해 난수를 생성하는 함수는 다음과 같이 만들 수 있다.

 

해시화 후, uint로 타입 변환을 해줘야 나머지 연산을 진행할 수 있다.

 

// private해 외부에서 호출할 수 없고, (private)
// 컨트랙트 내부의 값을 읽어 연산을 진행하되, 그 값을 바꾸지는 않는 view 함수이다.
// 마지막으로 반환값이 정수이므로 uint로 표시해준다.
function _generateRandomDna(string memory _str) private view returns (uint) {
        uint rand = uint(keccak256(abi.encodePacked(_str)));
        return rand % dnaModulus;
}

 

 

5. 이름과 dna를 통해 좀비 struct를 만들고, 이를 배열에 저장하는 함수를 생성한다.

function _createZombie(string memory _name, uint _dna) private {
        uint id = zombies.push(Zombie(_name, _dna)) - 1;
}

 

id는 좀비가 생성된 순서대로 붙는다. 가장 최근에 배열에 푸시된 좀비는 그 배열의 마지막 인덱스에 들어가므로,

 

push 메소드의 결과값에서 1을 빼주면 그게 그 좀비의 id가 된다.

 

 

6. 이제 이름을 통해 dna를 만들고, 다시 그걸 사용해 새 좀비를 만들어 저장하는 함수를 생성한다.

function createRandomZombie(string memory _name) public {
        // 4에서 만든 dna 생성함수 호출
        uint randDna = _generateRandomDna(_name);
        // 5에서 만든 좀비 구조체 생성함수 호출
        _createZombie(_name, randDna);
}

 

7. 마지막으로 이벤트를 추가해보자.

좀비가 생성되면 이 이벤트를 방출해주면 된다.

 

5에서 만든 함수 _createZombie에 이벤트 방출 코드를 추가해주면 된다.

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

function _createZombie(string memory _name, uint _dna) private {
        uint id = zombies.push(Zombie(_name, _dna)) - 1;
        emit NewZombie(id, _name, _dna);
}

 

전체 코드는 다음과 같이 된다.

 

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;

        function _createZombie(string memory _name, uint _dna) private {
                uint id = zombies.push(Zombie(_name, _dna)) - 1;
                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 {
                uint randDna = _generateRandomDna(_name);
                _createZombie(_name, randDna);
        }

}

'Solidity' 카테고리의 다른 글

Ch 3  (0) 2022.08.06
Ch2  (0) 2022.07.24
Ch1  (0) 2022.07.19