Solidity

Ch2

Sila 2022. 7. 24. 00:08

지난 번 글에 이어 계속해 cryptozombie를 해보면서 solidity 문법을 공부해보자.

 

Ch1의 내용들이 주로 나 혼자만을 생각하고 작성했다면

 

Ch2부터는 컨트랙트가 배포되었을 때 다른 사람들은 이 컨트랙트에 어떤 식으로 접근할 것인지에 대해 비중을 약간씩 늘려볼 것이다.

 

// SPDX-License-Identifier: MIT
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);
    }
}

 

1. mapping

struct 이외에도 객체처럼 여러 개 속성을 가진 데이터를 만드는 방법으로 mapping이 있다.

 

key 값과 value값의 데이터 타입을 한 번에 선언해주는데, 보통 데이터를 저장, 검색하는데 이용된다.

 

예를 들어 지갑 주소를 입력하면 그 지갑의 잔고를 보여주도록 mapping을 짜고 싶다면 다음과 같이 해주면 된다.

 

(address는 string, uint처럼 데이터 타입의 하나인데, 특정한 길이의 string을 지정한다. 

 

string의 특수한 케이스라고 생각하면 된다.)

 

mapping (address => uint) public accountBalance;
// address가 key, uint가 value의 데이터 타입, mapping의 이름은 accountbalance가 된다.

 

이걸 가지고 우리가 만들던 컨트랙트에서 생성된 구조체 Zombie의 id (zombies 배열의 index값)와

 

그 구조체(Zombie)를 만든 사람이 누구인지 그 지갑 주소를 연결해주는 zombieToOwner mapping과,

 

지갑 주소를 확인했을 때 그 지갑으로 만든 Zombie 구조체가 몇 개인지를 알려주는 owneerZombieCount mapping을 선언해보자.

 

// SPDX-License-Identifier: MIT
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;
    // zombies 배열의 idx => Zombie 구조체 생성자 
    mapping(address => uint) ownerZombieCount;
    // 지갑 주소 => 해당 지갑 주소가 만든/소유하는 구조체 수
    
    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);
    }
}

 

이제 이 매핑들을 함수에서 호출해 새로운 Zombie 구조체가 만들어질 때 매핑들이 동작하도록 해보자.

 

// SPDX-License-Identifier: MIT
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;
    // zombies 배열의 idx => Zombie 구조체 생성자 
    mapping(address => uint) ownerZombieCount;
    // 지갑 주소 => 해당 지갑 주소가 만든/소유하는 구조체 수
    
    function _createZombie (string memory _name, uint _dna) private {
        uint id = zombies.push(Zombie(_name,_dna)) -1;
        
        zombieToOwner[id] = msg.sender;
        // zombieToOwner.id (uint) = msg.sender (address)
        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 {
        uint randDna = _generateRandomDna(_name);
        _createZombie(_name, randDna);
    }
}

 

 _createZombie 함수가 호출되어 실행되면 매핑도 그 안에서 호출되도록 해주었는데,

 

여기서 msg.sender는 함수를 호출한 사람으로, 어디에서나 사용 가능한 전역 변수이다.

 

zombieToOwner 매핑에서 매핑 실행 전 만들어진 id 값을 msg.sender와 매칭 시켜준 후,

 

ownerZombieCount 매핑에서 함수 호출자(address)에 대응하는 uint 변수의 값을 1 증가시켜주었다.

 

2. require

솔리디티에 있는 조건문의 일종이라고 보면 된다. if문과 다른 점이 있다면 if문은 조건을 만족하지 않는다면 넘어가서

 

다음 코드를 실행하지만, require문의 경우 조건을 만족하지 않으면 아예 다음 코드부터는 실행하지 않는다.

 

사용자가 createRandomZombie 함수에 접근해 이를 실행하려고 할 때, 이미 이 함수를 실행한 적이 있다면

 

(즉, ownerZombieCount[msg.sender]가 0보다 크다면)이 함수의 나머지 코드를 실행하지 못하게 하고 싶다.

 

함수의 코드의 첫줄에 require문을 넣어 이를 구현할 수 있다.

 

// SPDX-License-Identifier: MIT
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;
    // zombies 배열의 idx => Zombie 구조체 생성자 
    mapping(address => uint) ownerZombieCount;
    // 지갑 주소 => 해당 지갑 주소가 만든/소유하는 구조체 수
    
    function _createZombie (string memory _name, uint _dna) private {
        uint id = zombies.push(Zombie(_name,_dna)) -1;
        
        zombieToOwner[id] = msg.sender;
        // zombieToOwner.id (uint) = msg.sender (address)
        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);
        _createZombie(_name, randDna);
    }
}

 

슬슬 컨트랙트의 길이가 길어지므로 코드들을 분리하고 다른 sol 파일에서 이를 import 해 새로운 컨트랙트에

 

상속시킬 필요가 있다.

 

zombiefeeding.sol 파일을 하나 더 만들고 (지금까지 작성한 파일 이름은 zombieFactory.sol)

 

그 안에 다음과 같이 파일을 import 하고 컨트랙트를 상속시킨다.

 

/*  zombiefeeding.sol  */

// SPDX-License-Identifier: MIT
prgma solidity >=0.5.0 <0.6.0;

import "./zombiefactory.sol";

contract ZombieFeeding is ZombieFactory {
    
}

 

이제 부턴 이 컨트랙트 안에 코드를 이어서 작성해 나간다.

 

3. memory & storage

솔리디티에서 변수를 저장할 수 있는 공간은 두 가지로 나뉘는데, 이 또한 변수를 선언할 때 종종 명시적으로

 

작성해 저장 밥법을 알려준다.

 

일시적으로 저장되는 변수는 memory, 블록체인 상에 영구적으로 저장되는 함수는 stoarge 속성을 가진다.

 

이를 적지 않아도 알아서 솔리디티가 처리해 주는 경우도 있지만, 직접 내가 명시를 해주어야 하는 경우도 있다.

 

대표적으로 구조체와 배열을 처리하는 코드에서 그렇다.

 

좀비의 아이디를 통해 좀비의 dna를 가져오고, 먹이의 dna와 적절히 조합해 새로운 dna를 만드는 함수를 작성해보자.

 

/*  zombiefeeding.sol  */

// SPDX-License-Identifier: MIT
prgma solidity >=0.5.0 <0.6.0;

import "./zombiefactory.sol";

contract ZombieFeeding is ZombieFactory {
    function feedAndMultiply(uint _zombieId, uint _targetDna) public {
        require(msg.sender == zombieToOwner[_zombieId])
        // 사용자가 자신의 좀비에게만 먹이를 줄 수 있도록 require문 추가
        // 양 변의 address 값이 같아야 한다.
        
        Zombie storage myZombie = zombies[_zombieId];
        // 원하는 Zombie 구조체를 가져온다
        
        _targetDna = _targetDna % dnaModulus;
        uint newDna = (myZombie.dna + _targetDna) / 2;
        _createZombie("NoName", newDna);
    }
}

 

 이 때, feedAndMultiply 함수에선 _createZombie 함수를 가져올 수가 없다. 이는 private 함수이기 때문.

 

따라서 _createZombie 함수의 속성을 약간 바꿔줄 필요가 있는데, private과 비슷한 속성으로 internal이 있다.

 

internal 함수는 이 함수가 있는 컨트랙트를 상속하는 컨트랙트에서도 함수를 호출 할 수 있게 해준다는 점에서

 

private과 다르다.

 

참고로 external함수는 그 함수가 컨트랙트 바깥에서만 호출될 수 있게 해준다.

 

(컨트랙트 내의 다른 함수는 external함수를 호출할 수 없음)

 

zombiefeeding.sol의 _createZombie 함수의 속성을 private에서 internal로 바꿔주고 넘어가자.

 

4. interface

내가 만든 컨트랙트 - 컨트랙트 간의 상호작용이 아니라 다른 사람이 만든 컨트랙트와 상호작용을 시키고 싶다.

 

이럴 땐 우선 interface를 정의해야한다.

 

일단 contract처럼 정의하긴 하는데, 그 안에 가져오고자 하는 함수를 내용은 제외하고 가져온다.

 

ZombieFeeding 컨트랙트에서 Kitty 컨트랙트의 getKitty 함수를 가져와 실행하고 싶다고 하자.

 

1. 우선 KittyInterface를 선언하고, (contract 로 선언), 그 안에 getKitty 함수를 함수 내용을 제외하고 선언한다.

 

2. 가져오고 싶은 컨트랙트의 CA를 그걸 가져올 컨트랙트 (ZombieFeeding)에 선언한다.

 

3. 가져온 CA를 이용해 이를 가져온 컨트랙트 안에서 초기화한다.

 

/*  zombiefeeding.sol  */

// SPDX-License-Identifier: MIT
prgma solidity >=0.5.0 <0.6.0;

import "./zombiefactory.sol";

contract KittyInterface {
    // contract로 KittyInterface 선언
    
    function getKitty(uint256 _id) external view returns (
        // getKitty가 external 속성이면 외부 컨트랙트에서 호출할 수 있다.
        
        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 contractAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
    // CA를 이용해 컨트랙트를 가져온다.
    KittyInterface kittyContract = KittyInterface(contractAddress);
    // CA, interface를 통해 가져온 컨트랙트르 초기화한다.
    
    function feedAndMultiply(uint _zombieId, uint _targetDna) public {
        require(msg.sender == zombieToOwner[_zombieId])
        // 사용자가 자신의 좀비에게만 먹이를 줄 수 있도록 require문 추가
        // 양 변의 address 값이 같아야 한다.
        
        Zombie storage myZombie = zombies[_zombieId];
        // 원하는 Zombie 구조체를 가져온다
        
        _targetDna = _targetDna % dnaModulus;
        uint newDna = (myZombie.dna + _targetDna) / 2;
        _createZombie("NoName", newDna);
    }
}

 

이렇게 외부 컨트랙트를 가져왔으면 이를 이용한 작업을 할 수 있다.

 

getKitty 함수를 호출해 kittyDna을 다음과 같이 가져올 수 있다.

 

/*  zombiefeeding.sol  */

// SPDX-License-Identifier: MIT
prgma solidity >=0.5.0 <0.6.0;

import "./zombiefactory.sol";

// ...중략

contract ZombieFeeding is ZombieFactory {

    address contractAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
    // CA를 이용해 컨트랙트를 가져온다.
    KittyInterface kittyContract = KittyInterface(contractAddress);
    // CA, interface를 통해 가져온 컨트랙트르 초기화한다.
    
    function feedAndMultiply(uint _zombieId, uint _targetDna) public {
        require(msg.sender == zombieToOwner[_zombieId])
        // 사용자가 자신의 좀비에게만 먹이를 줄 수 있도록 require문 추가
        // 양 변의 address 값이 같아야 한다.
        
        Zombie storage myZombie = zombies[_zombieId];
        // 원하는 Zombie 구조체를 가져온다
        
        _targetDna = _targetDna % dnaModulus;
        uint newDna = (myZombie.dna + _targetDna) / 2;
        _createZombie("NoName", newDna);
    }
    
    function feedOnKitty(uint _zombieId, uint _kittyId) public {
        uint kittyDna;
        (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
        // 리턴 받는 값 중 마지막 값에만 관심이 있으므로 나머지는 빈칸으로 두고,
        // 10번째 리턴값만 kittyDna라고 이름 붙여 가져온다.
        feedAndMultiply(_zombieId, kittyDna);
    }
}

 

만약 이와 같은 과정을 통해 새로운 Zombie 구조체가 만들어 질 때는 그 구조체의 dna의 마지막 두 자리를 99로 바꿔

 

이 구조체는 getKitty함수를 거쳐 생성되었다는 것을 표시하고 싶다면 다음과 같이  함수에 매개변수를 추가해줄 수 있다.

 

/*  zombiefeeding.sol  */

// SPDX-License-Identifier: MIT
prgma solidity >=0.5.0 <0.6.0;

import "./zombiefactory.sol";

// ...중략

contract ZombieFeeding is ZombieFactory {

    address contractAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
    KittyInterface kittyContract = KittyInterface(contractAddress);
    
    function feedAndMultiply(uint _zombieId, uint _targetDna, string memory _species) public {
        // 함수의 매개변수에 _species 추가
        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
        }
        // 조건을 만족하면 마지막 두 자리를 99로 바꾼다.
        _createZombie("NoName", newDna);
    }
    
    function feedOnKitty(uint _zombieId, uint _kittyId) public {
        uint kittyDna;
        (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
        feedAndMultiply(_zombieId, kittyDna, "kitty");
        // 3번째 매개변수 값으로 "kitty" 문자열을 준다.
    }
}

'Solidity' 카테고리의 다른 글

Cryptozombie - Ch1. Making Zombie Factory  (2) 2023.12.03
Ch 3  (0) 2022.08.06
Ch1  (0) 2022.07.19