Skip to content

unhhyyeexx/CryptoZombie

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 

Repository files navigation

🧟Crypto Zombie

컨트랙트

  • 이더리움 애플리케이션의 기본적인 구성 요소로, 솔리디티의 모든 변수와 함수는 어느 한 컨트랙트에 속하게 된다.

  • 예시 ; 비어있는 HelloWorld 컨트랙트

    contract HelloWorld {
    
    }

Version Pragma

  • 해당 코드가 이용해야 하는 솔리디티 버전을 선언하는 것으로 모든 솔리디티 소스 코드는 version pragma로 시작한다.

  • 이후에 새로운 컴파일러 버전이 나와도 기존 코드가 깨지지 않도록 예방

    pragma solidity ^0.4.19;
    
    contract HelloWorld{
    
    }

State Variables & Integers

상태변수 & 정수

  • 상태변수

    • 컨트랙트 저장소에 영구적으로 저장된다. 즉, 이더리움 블록체인에 기록되는 변수로 데이터베이스에 데이터를 쓰는 것과 동일
    contract Example {
    	// 이 변수는 블록체인에 영구적으로 저장된다.
    	uint myUnsignedInteger = 100;
    }
    
    // myUnsignedInteger라는 uint을 생성하여 100이라는 값 배정

부호없는 정수 : uint

  • uint자료형은 부호없는 정수로, 값이 음수가 아니어야 한다는 의미.
  • 부호 있는 정수는 int자료형 사용

Math Operations

수학 연산

  • Addition : x+y

  • Subtracton: x-y

  • Multiplication: x*y

  • Division: x/y

  • Modulus / remainder: x%y

  • 지수연산

    uint x = 5**2; // 즉, 5^2 = 25

Structs

구조체

복잡한 자료형이 필요할 때

struct Person{
	uint age;
	string name;
}

Arrays

배열 (고정배열, 동적배열)

// 2개의 원소를 담을 수 있는 고정 길이의 배열:
uint[2] fixedArray;
// 또 다른 고정 배열으로 5개의 스트링을 담을 수 있다:
string[5] stringArray;
// 동적 배열은 고정된 크기가 없으며 계속 크기가 커질 수 있다:
uint[] dynamicArray;

구조체 배열

Person[] people; 
//동적 배열, 원소 계속 추가 가능

동적배열을 생성하면 마치 데이터베이스처럼 컨트랙트에 구조화 된 데이터를 저장하는데 유용

Public 배열

  • public으로 배열 선언 가능

  • 솔리디티는 이런 배열을 위해 getter메소드를 자동적으로 생성

    Person[] public people;
    • 다른 컨트랙트들이 이 배열을 읽을 수 있게 된다.(read only, 사용은 불가)
    • 컨트랙트에 공개 데이터를 저장할 때 유용한 패턴

Function Declarations

함수 선언

function eatHambergers(string _name, uint _amount) {

}

eatHambergers라는 함수. string 과 uint 2개의 인자를 전달받고 있다.

함수의 내용은 비어있다.

함수 인자명을 언더스코어(_)로 시작해서 전역 변수와 구별하는 것이 관례.

위에 선언한 함수 호출은 다음과 같이 한다.

eatHambergers("vitalik", 100);

Working With Structs and Arrays

구조체와 배열 활용하기

새로운 구조체 생성하기

// 미리 생성한 구조체
struct Person {
	uint age;
	string name;
}

Person[] public people;

새로운 Person을 생성, people배열에 추가하려면

// 새로운 사람을 생성한다:
Person satoshi = Person(172, "Satoshi")
// 이 사람을 배열에 추가한다:
people.push(satoshi);

위 두 코드를 한 줄로 표현하면,

people.push(Person(16, "Vitalik"));

Private / Public Functions

  • 솔리디티에서 함수는 기본적으로 public 으로 선언된다.
    • 누구나 (혹 다른 어느 컨트랙트가) 컨트랙트의 함수를 호출하고 코드를 실행할 수 있다는 의미

    • 컨트랙트가 공격에 취약해질 수도 있음

      ⇒ 기본적으로 함수를 private으로 선언하고, 공개할 함수만 public으로 선언하는 것이 좋다.

Private 함수

uint[] numbers;

function _addToArray(uint _number) private{
	numbers.push(_number);
}
  • 컨트랙트 내의 다른 함수들만이 이 함수를 호출하여 numbers 배열로 무언가를 추가할 수 있다
  • private 키워드는 함수명 다음에 적고, 함수 인자명과 마찬가지로 private함수명도 언더바로 시작하는 것이 관례(public함수는 언더스코어 x)

More on Functions

반환값(Return Values)

string greeting = "What's up dog";

function sayHello() public returns (string) {
	return greeting;
}
  • 솔리디티에서 함수 선언은 반환값 종류를 포함한다 (이 경우에는 string)

함수 제어자(Function modifiers)

  • view함수

    • 위의 sayHello()는 솔리디티에서 상태를 변화시키지 않는다. 즉, 어떤 값을 변경하거나 무언가를 사용하지 않는다.
    • 이런 경우 함수를 view함수로 선언하는데, 이는 함수가 데이터를 보기만 하고 변경은 하지 않는다는 의미이다.
    function sayHello() public view returns (string) {
  • pure 함수

    • 함수가 앱에서 어떤 데이터도 접근하지 않는 것을 의미한다.
    function _multiply(uint a, uint b) private pure returns (uint) {
    	return a * b;
    }
    • 앱에서 읽는 것도 하지 않고, 다만 반환값이 전달된 인자값에 따라 달라진다.

Keccak256 and Typecasting

Keccak256과 형 변환

이더리움은 SHA3의 한 버전인 keccak256을 내장 해시 함수로 가진다.

해시 함수는 기본적으로 입력 스트링을 랜덤 256비트 16진수로 매핑

스트링에 약간의 변화라도 있으면 해시값은 크게 달라지게 된다.

  • 해시함수로 pseudo-random number generator(의사 난수 발생기) 만들기

    //6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5
    keccak256("aaaab");
    //b1f078126895a1424524de5321b339ab00408010b7cf0e6ed451514981e58aa9
    keccak256("aaaac");

    한 글자가 달라졌지만 반환값은 완전히 달라진다.

Typecasting(형 변환)

uint8 a = 5;
uint b = 6;
// a * b가 uint8이 아닌 uint를 반환하기 때문에 에러가 뜬다
uint8 c = a * b;
// b를 uint8으로 형 변환해서 코드가 제대로 작동하도록 해야 함
uint8 c = a * uint8(b);

Events

컨트랙트가 블록체인 상에서 앱의 유저 단에서 무언가 액션이 발생했을 때 의사소통하는 방법

컨트랙트는 특정 이벤트가 일어나는지 주목하다가 그 이벤트가 발생하면 행동

// 이벤트 선언
event IntegersAdded(uint x, uint y, uint result);

function add(uint _x, uint _y) public {
	uint result = _x + _y;
	//이벤트를 실행하여 앱에게 add 함수가 실행되었음을 알림
	IntegersAdded(_x, _y, result);
	return result;
}

자바스크립트로 구현하면 다음과 같음

YourContract.IntegersAdded(function(error, result) {
	//결과와 관련된 행동을 취한다.
})

Web3.js

이더리움은 Web3.js라고 하는 자바스크립트 라이브러리를 가진다.

// 만든 컨트랙트에 접근하는 방법 제시
var abi = /* abi generated by the compiler */
var ZombieFactoryContract = web3.eth.contract(abi)
var contractAddress = /* our contract address on Etehreum after deploying */
var ZombieFactory = ZombieFactoryContract.at(contractAddress)
// 'ZombieFactory'는 우리 컨트랙트의 public함수와 이벤트에 접근할 수 있다.

//일종의 이벤트 리스너가 텍스트 입력값을 취한다:
$('#ourButton").click(function(e) {
	var name= $("#nameInput").val()
	// 우리 컨트랙트의 'createRandomZombie'함수를 호출한다:
	ZombieFactory.createRandomZombie(name)
})

// 'Newzombie' 이벤트가 발생하면 사용자 인터페이스를 업데이트
var event = ZombieFactory.NewZombie(function(error, result) {
	if (error) return
	generateZombie(result.zombieId, result.name, result.dna)
})

// 좀비 DNA값을 받아서 이미지를 업데이트
function generateZombie(id, name, dna) {
	let dnaStr = String(dna)
	// DNA값이 16자리 수보다 작은 경우 앞 자리를 0으로 채운다
	while (dnaStr.length < 16)
		dnaStr = "0" + dnaStr

	let zombieDetails = {
		// 첫 2자리는 머리의 타입을 결정한다. 머리 타입에는 7가지가 있다. 그래서 모듈로(%) 7 연산을 하여
    // 0에서 6 중 하나의 값을 얻고 여기에 1을 더해서 1에서 7까지의 숫자를 만든다. 
    // 이를 기초로 "head1.png"에서 "head7.png" 중 하나의 이미지를 불러온다:
		headChoice : dnaStr.substring(0, 2) % 7 + 1,
		// 두번째 2자리는 눈 모양을 결정한다. 눈 모양에는 11가지가 있다:
    eyeChoice: dnaStr.substring(2, 4) % 11 + 1,
    // 셔츠 타입에는 6가지가 있다:
    shirtChoice: dnaStr.substring(4, 6) % 6 + 1,
		// 마지막 6자리는 색깔을 결정하며, 360도(degree)까지 지원하는 CSS의 "filter: hue-rotate"를 이용하여 아래와 같이 업데이트된다:
    skinColorChoice: parseInt(dnaStr.substring(6, 8) / 100 * 360),
    eyeColorChoice: parseInt(dnaStr.substring(8, 10) / 100 * 360),
    clothesColorChoice: parseInt(dnaStr.substring(10, 12) / 100 * 360),
    zombieName: name,
    zombieDescription: "A Level 1 CryptoZombie",
  }
return zombieDetails
}

Mappings and Addresses

매핑과 주소

주소

  • 이더리움 블록체인은 은행 계좌와 같은 계정들(accounts)로 이루어져 있다.

  • 계정은 이더리움 블록체인상의 통화인 Ether의 잔액을 갖고, 계정을 통해 다른 계정과 이더를 주고받을 수 있다.

  • 각 계정은 은행 계좌 번호와 같은 주소(adress)를 가진다. 주소는 특정 계정을 가리키는 고유 식별자로, 다음과 같이 표현한다.

    0x0cE446255506E92DF41614C46F1d6df9Cc969183
  • 주소는 특정 유저 혹은 스마트 컨트랙트가 소유한다. ⇒ 고유 id로 활용 가능

매핑

  • 매핑 : 솔리디티에서 구조화된 데이터를 저장하는 방법 (구조체와 배열처럼)
//금융 앱용으로, 유저의 계좌 잔액을 보유하는 uint를 저장한다:
mapping (address => uint) public accountBalance;
//혹은 userID로 유저 이름을 저장/검색하는 데 매핑을 쓸 수도 있다
mapping (uint => string) userIdToName;
  • 매핑은 기본적으로 key-value 저장소로, 데이터를 저장하고 검색하는데 이용
  • 첫번째 예시 ) 키: address , 값 : uint 두번째 예시 ) 키: uint , 값 : string

Msg.sender

  • 솔리디티에서 모든 함수에서 이용 가능한 특정 전역 변수

  • 현재 함수를 호출한 유저 (혹은 스마트 컨트랙트)의 주소를 가르킨다.

  • 솔리디티에서 함수 실행은 항상 외부 호출자가 시작한다. 컨트랙트는 누군가가 컨트랙트의 함수를 호출할 때까지 블록체인 상에서 아무런 작업도 하지 않는다 ⇒ 항상 msg.sender필요

  • 예시) msg.sender를 이용하고, mapping을 업데이트

    mapping (address => uint) favoriteNumber;
    
    function setMyNumber(uint _myNumber) public {
    	// 'msg.sender'에 대해 '_myNumber'가 저장되도록 'favoriteNumber'매핑을 업데이트
    	favoriteNumber[msg.sender] = _myNumber;
    	// 데이터를 저장하는 구문은 배열로 데이터를 저장할 때와 동일하다.
    }
    
    function whatIsMyNumber() public view returns (uint) {
    	//sender의 주소에 저장된 값을 불러온다
    	//sender가 'setMyNumber'을 아직 호출하지 않았다면 반환값은 '0'이 될 것
    	return favoriteNumber[msg.sender];
    }

    ⇒ setMyNumber을 호출하여 본인의 주소와 연결된 우리 컨트랙트 내에 uint를 저장할 수 있다.

  • msg.sender를 활용하면 이더리움 블록체인의 보안성을 이용할 수 있게 된다.

Require

  • 특정 조건이 참이 아닐 때 함수가 에러 메시지를 발생하고 실행을 멈추게 된다.
function sayHiToVitalik(string _name) public returns (string) {
	// _name이 'Vitalik'인지 비교. 참이 아닐 경우 에러 메시지를 발생하고 함수를 벗어남
	// 참고 : 솔리디티는 고유의 스트링 비교 기능을 가지고 있지 않기 때문에
	// 스트링의 keccak256 해시값을 비교하여 스트링 값이 같은지 판단한다.
	require(keccak256(_name) == keccak256("Vitalik"));
	// 참이면 함수 실행을 진행한다.
	return "Hi!";
  • sayHiToVitalik(”Vitalik”)로 이 함수를 실행하면 “Hi!”반환
  • “Vitalik”이 아닌 다른 값으로 이 함수를 호출할 경우, 에러 메시지가 뜨고 함수가 실행되지 않는다.
  • 함수를 실행하기 전에 참이어야 하는 특정 조건을 확인하는 데 있어 꽤 유용

Inheritance

상속

contract Doge {
	function catchphrase() public returns (string) {
		return "So Wow CryptoDoge";
	}
}

contract BabyDoge is Doge {
	function anotherCatchphrase() public returns (string) {
		return "Such Moon BabyDoge";
	}
}
  • babyDoge 컨트랙트는 Doge컨트랙트를 상속

    즉, BabyDoge컨트랙트를 컴파일해서 구축할 때, BabyDoge 컨트랙트가 catchphrase()함수와 anotherCatchphrase()함수에 모두 접근할 수 있다.

  • 상속개념은 “고양이는 동물이다” 의 경우처럼 부분집합 클래스가 있을 때 논리적 상속을 위해 활용될 수 있다. 하지만 동일한 로직을 다수의 클래스로 분할해서 단순히 코드를 정리할 때도 활용한다.

Import

  • 다수의 파일이 있고 어떤 파일을 다른 파일로 불러오고 싶을 때, 솔리디티는 import 라는 키워드를 이용한다.
import "./someothercontract.sol";

contract newContract is SomeOtherContract {

}

⇒ 이 컨트랙트와 동일한 폴더(./)에 someothercontract.sol이라는 파일이 있을 때, 이 파일을 컴파일러가 불러오게 된다.

Storage vs Memory

솔리디티에는 변수를 저장하는 공간으로 storage와 memory 두 공간이 있다.

  • Storage
    • 블록체인 상에 영구적으로 저장되는 변수
  • Memory
    • 임시적으로 저장되는 변수로, 컨트랙트 함수에 대한 외부 호출들이 일어나는 사이에 지워진다.
  • 상태변수(함수 외부에 선언된 변수)는 초기 설정상 storage에 선언, 블록체인에 영구적으로 저장
  • 함수 내에 선언된 변수는 memory로 자동 선언, 함수 호출이 종료되면 사라진다.
  • 함수 내의 구조체와 배열을 처리할 때는 사용해야 함
contract SandwichFactory {
	struct Sandwich {
		string name;
		string status;
	}

	Sandwich[] sandwiches;

	function eatSandwich(uint _index) public {
		//Sandwich mySandwich = sandwiches[_index];
		// ^ 꽤 간단해 보이나, 솔리디티는 여기서 
    // `storage`나 `memory`를 명시적으로 선언해야 한다는 경고 메시지를 발생한다. 
    // 그러므로 `storage` 키워드를 활용하여 다음과 같이 선언해야 한다:
		Sandwich storage mySandwich = sandwiches[_index];
    // ...이 경우, `mySandwich`는 저장된 `sandwiches[_index]`를 가리키는 포인터이다.
    // 그리고 
    mySandwich.status = "Eaten!";
    // ...이 코드는 블록체인 상에서 `sandwiches[_index]`을 영구적으로 변경한다.
		
		// 단순히 복사를 하고자 한다면 `memory`를 이용하면 된다: 
    Sandwich memory anotherSandwich = sandwiches[_index + 1];
    // ...이 경우, `anotherSandwich`는 단순히 메모리에 데이터를 복사하는 것이 된다. 
    // 그리고 
    anotherSandwich.status = "Eaten!";
    // ...이 코드는 임시 변수인 `anotherSandwich`를 변경하는 것으로 
    // `sandwiches[_index + 1]`에는 아무런 영향을 끼치지 않는다. 그러나 다음과 같이 코드를 작성할 수 있다: 
    sandwiches[_index + 1] = anotherSandwich;
    // ...이는 임시 변경한 내용을 블록체인 저장소에 저장하고자 하는 경우이다.
  }
}

More on Function Visibility

Internal 과 External

솔리디티에는 public과 private 이외에도 internal과 external이라는 함수 접근 제어자가 있다.

  • internal
    • 함수가 정의된 컨트랙트를 상속하는 컨트랙트에서도 접근이 가능하다는 점을 제외하면 private과 동일하다.
  • external
    • 함수가 컨트랙트 바깥에서만 호출될 수 있고, 컨트랙트 내의 다른 함수에 의해 호출될 수 없다는 점을 제외하면 public과 동일하다.
  • 선언은 public, private과 동일
contract Sandwich {
	uint private snadwichesEaten = 0;
	
	function eat() internal {
		snadwichesEaten++;
	}
}

contract BLT is Sandwich {
	uint private baconSandwichesEaten = 0;

	function eatWithBacon() public returns (string) {
		baconSandwichesEaten++;
		// eat 함수가 internal로 선언되었기 때문에 여기서 호출이 가능
		eat();
	}
}

Interacting with other contracts

다른 컨트랙트와 상호작용하기

블록체인 상에 있으면서 소유하지 않은 컨트랙트와 나의 컨트랙트가 상호작용을 하려면 우선 **인터페이스(interface)**를 정의해야 한다.

contract LuckyNumber {
	mapping (address => uint) numbers;
	
	function setNum(uint _num) public {
		numbers[msg.sender] = _num;
	}
	
	function getNum(address _myAddress) public view returns (uint) {
		return numbers[_myAddress];
	}
}
  • 아무나 자신의 행운의 수를 저장할 수 있는 간단한 컨트랙트
  • 각자의 이더리움 주소와 연관, 이 주소를 이용해서 행운의 수를 찾아볼 수 있다.

getNum함수를 이용해 이 컨트랙트에 있는 데이터를 읽고자 하는 external함수가 있으면, 먼저 LuckyNumber 컨트랙트의 인터페이스를 정의해야 한다.

contract NumberInterface {
	function getNum(address _myAddress) public view returns (uint);
}
  • 인터페이스를 정의하는 것은 컨트랙트를 정의하는 것과 유사하다
    • 다른 컨트랙트와 상호작용하고자 하는 함수만을 선언
    • 다른 함수나 상태 변수를 언급하지는 않는다
    • 함수 몸체를 정의하지 않는다
    • 중괄호를 쓰지않고 함수 선언을 세미콜론(;)으로 간단하게 끝낸다.

Using an Interface

인터페이스 활용하기

contract NumberInterface {
	function getNum(address _myAddress) public view returns (uint);
}

위와 같이 인터페이스가 정의되면 다음과 같이 컨트랙트에서 인터페이스를 이용할 수 있다.

contract MyContract {
	address NumberInterfaceAddress = 0xab38...
	// ^ 이더리움상의 FavoriteNumber 컨트랙트 주소
	NumberInterface numberContract = NumberInterface(NumberInterfaceAddress)
	// 이제 'numberContract'는 다른 컨트랙트를 가르킨다.

	function someFunction() public {
		// 이제 'numbercontract'가 가리키는 컨트랙트에서 'getNum' 함수를 호출할 수 있다.
		uint num = numberContract.getNum(msg.sender);
		// ...그리고 여기서 'num'으로 무언가를 할 수 있음

상호작용하는 함수가 public이나 external로 선언되어 있다면 내 컨트랙트는 이더리움 블록체인 상의 다른 어떤 컨트랙트와도 상호작용할 수 있다.

Handling Multiple Return Values

다수의 반환값 처리하기

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

function processMultipleReturns() external {
	uint a;
	uint b;
	uint c;
	// 다음과 같이 다수 값을 할당한다
	(a, b, c) = multipleReturns();
}

// 혹은 단 하나의 값에만 관심이 있을 경우:
function getLastReturnValue() external {
	uint c;
	// 다른 필드는 빈칸으로 놓기만 하면 된다.
	(,,c) = multipleReturns();
}

If statements

솔리디티에서 if문은 자바스크립트의 if문과 동일하다

function  eatBLT(string sandwich) public {
	// 스트링 간의 동일 여부를 판단하기 위해 keccak256해시 함수 이용
	if (keccak256(sandwich) == keccak256("BLT")) {
		eat();
	}
}

Immutability of Contracts

컨트랙트의 불변성

  • 이더리움에 컨트랙트를 배포하고 나면, 컨트랙트는 변하지 않는다. 즉, 수정이나 업데이트가 불가능하다.
  • 컨트랙트로 배포한 최초의 코드는 항상 블록체인에 영구적으로 존재하고, 이것이 바로 솔리디티에 있어 보안이 큰 이슈인 이유

외부 의존성

만약 DApp에 컨트랙트의 주소를 그대로 가져다 쓴다고 가정해보자.

컨트랙트에 버그가 있다면 DApp을 사용할 수 없을 것이다.

이런 이유로 대개의 경우 DApp의 일부를 수정할 수 있도록 하는 함수를 만들어 두는 것이 합리적

Ownable Contract

소유가능한 컨트랙트

컨트랙트를 소유가능하게 만들어 보안을 강화시킬 수 있다.

OpenZeppelin의 Ownable 컨트랙트

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

    • OpenZeppelin은 DApp에서 사용할 수 있는, 안전하고 커뮤니티에서 검증받은 스마트 컨트랙트의 라이브러리
    /**
     * @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는 컨트랙트에 흔히 쓰이는 제어자. 대부분의 솔리디티 DApp들은 Ownble컨트랙트를 복붙하면서 시작한다. 그리고 첫 컨트랙트는 이 컨트랙트를 상속해서 만듬.

onlyOwner Function Modifier

onlyOwner 함수 제어자

일단,

ZombieFeeding is ZombieFactory
ZombieFactory is Ownable

이므로, ZombieFeeding 또한 Ownable이고, Ownable 컨트랙트의 함수, 이벤트, 제어자에 접근 가능

함수 제어자

  • 함수 제어자는 함수처럼 보이지만, 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 제어자 부분을 보면, likeABoss 함수를 호출하면, onlyOwner의 코드가 먼저 실행 그리고 onlyOwner_; 부분을 likeABoss 함수로 되돌아가 해당 코드를 실행
  • 제어자를 사용하는 방법중 가장 일반정으로 쓰는 방법은 함수 실행 전에 require 체크를 넣는 것
  • onlyOwner의 경우에는, 함수에 이 제어자를 추가하면 오직 컨트랙트의 소유자(배포자)만이 해당 함수를 호출할 수 있다.

Gas

Gas - the fuel Ethereum DApps run on

  • 솔리디티에서는 사용자들이 DApp의 함수를 실행시킬 때마다 '가스'라고 부리는 화폐를 지불해야 한다.
  • 사용자는 이더를 이용해서 가스를 구매하기 때문에 DApp함수를 실행하려면 유저들은 이더를 소모
  • 함수의 로직이 얼마나 복잡한지에 따라 가스소모량이 달라진다. 각각의 연산은 소모되는 gas cost가 있고, 그 연산을 수행하는 데 소모되는 컴퓨팅 자원의 양이 이 비용 결정

Why is gas necessary?

  • 유저가 어떤 함수를 실행할 때, 이더리움 네트워크상의 모든 개별 노드가 함수의 출력값을 검증하기 위해 그 함수를 실행한다.
  • 모든 함수의 실행을 검증하는 수천 개의 노드가 바로 이더리움을 분산화하고 데이터를 보존하며 누군가 검열할 수 없도록 하는 요소이다.
  • 연산처리에 비용을 쓰고, 저장 공간 뿐만 아니라 연산 사용 시간에 따라서도 비용을 지불해야 하므로 안전성 높아짐

Struct packing to save gas

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 a; uint c; uint32 b; 필드로 구성된 구조체보다 가스를 덜 소모한다. uint32 필드가 묶여있기 때문.

Time Units

시간 단위

  • 솔리디티는 시간을 다룰 수 있는 단위계를 기본적으로 제공
  • now 변수
    • 현재의 유닉스 타임스탬프 값을 얻을 수 있다
  • seconds, minutes, hours, days, weeks, years 같은 시간 단위 또한 솔리디티에 포함 이들은 그에 해당하는 길이 만큼의 초 단위 uint 숫자로 변환된다. 즉, 1 minutes는 60, 1hours는 3600(60초 * 60분), 1days 는 86400(24시간 * 60분 * 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));
}

Cooldowns

재사용 대기시간

Passing structs as arguments

구조체를 인수로 전달하기

  • private 또는 internal함수에 인수로서 구조체의 storage 포인터를 전달 가능 예를 들어 함수들 간에 구조체를 주고 받을 때 유용
function _doStuff(Zombie stoage _zombie) internal {
// _zombie로 할 수 있는 것들을 처리
}
  • 이런 방식으로 함수에 id를 전달하고 좀비를 찾는 대신 좀비에 대한 참조를 전달 가능

Public Functions & Security

Public 함수 & 보안

  • 보안을 점검하는 좋은 방법은 모든 public과 external함수를 검사하고, 유저들이 그 함수들을 남용할 수 있는 방법을 생각해보는 것. 이 함수들이 onlyOwner 같은 제어자를 갖지 않는 이상, 어떤 사용자든 이 함수들을 호출하고 자신드링 원하는 모든 데이터를 함수에 전달할 수 있다.
  • 함수를 internal로 만들면 필요로 하는 함수에 대해서만 호출이 된다,

More on Function Modifier

함수 제어자의 또 다른 특징

Function modifiers with aruments

인수를 가지는 함수 제어자

함수 제어자는 인수 또한 받을 수 있음

// 사용자의 나이를 저장하기 위한 매핑
mapping (uint => uint) public age;

// 사용자가 특정 나이 이상인지 확인하는 제어자
modifier olderThan(uint _age, uint _userId) {
  require (age[_userId] >= _age);
  _;
}

// 차를 운전하기 위햐서는 16살 이상이어야 함.
// `olderThan` 제어자를 인수와 함께 호출하려면:
function driveCar(uint _userId) public olderThan(16, _userId) {
  // 필요한 함수 내용들
}
  • olderthan 제어자가 함수와 비슷하게 인수를 받는 것을 볼 수 있다. 그리고 driveCar 함수는 받은 인수를 제어자로 전달한다.

Saving Gas With 'View' Functions

View functions don't cost gas

View함수는 가스를 소모하지 않는다

  • view함수는 유저에 의해 외부에서 호출되었을 때 가스를 전혀 소모하지 않는다.
    • view함수가 블록체인 상에서 실제로 어떤 것도 수정하지 않기 때문. 데이터를 읽기만 한다.
    • 어떤 트랜젝션도 만들지 않는다.
  • 가스 사용을 최적화하는 비결은 가능한 모든 곳에 읽기 전용의 external view함수를 쓰는것이다.

Storage is Expensive

  • 솔리디티에서 비싼 연산 중 하나는 storage를 쓰는 것이다, 그중에서도 쓰기 연산
    • 데이터의 일부를 쓰거나 바꿀 때마다, 블록체인에 영구적으로 기록되기 때문
  • 비용을 최소화하기 위해, 진짜 필요한 경우가 아니면 storage에 데이터를 쓰지 않는 것이 좋다.
    • 어떤 배열에서 내용을 빠르게 찾기 위해, 단순히 변수에 저장하는 것 대신 함수가 호출될 때마다 배열으 ㄹmemory에 다시 만드는 것이 나을수도
  • 대부분의 언어에서는 큰 데이터 집합의 개별 데이터에 모두 접근하는 것은 비용이 비싸다. 하지만 솔리디티에서는 그 접근이 external view 함수라면 storage를 사용하는 것보다 더 저렴하다.

Declaring arrays in memory

메모리에 배열 선언하기

  • Storage에 아무것도 쓰지 않고도 함수 안에 새로운 배열을 만들려면 배열에 memory 키워드를 스면 된다.
  • 이 배열은 함수가 끝날 대까지만 존재, storage의 배열을 직접 업데이트 하는 것보다 가스비가 훨씬 저렴
function getArray() external pure returns(uint[]) {
  // 메모리에 길이 3의 새로운 배열을 생성한다.
  uint[] memory values = new uint[](3);
  // 여기에 특정한 값들을 넣는다.
  values.push(1);
  values.push(2);
  values.push(3);
  // 해당 배열을 반환한다.
  return values;
}
  • 메모리 배열은 반드시 길이 인수와 함께 생성되어야 한다. (위 예시에서는 3) 메모리 배열은 현재로서는 storage배열처럼 array.push()로 크기가 조절되지는 않는다.

For Loops

view함수는 외부에서 호출도리 때 가스를 사용하지 않기 때문에, 함수에서 for반복문을 사용해서 모든 요소에 접근한 후 특정 사용자의 요소로 구성된 배열을 만들 수 있을 것이다. 그러고 나면 transfer함수는 훨씬 비용을 적게 쓰게 된다. (storage에서 어떤 배열도 재정렬할 필요가 없기 때문)

Using for Loops

  • 솔리디티에서 for반복문의 문법은 자바스크립트의 문법과 비슷
  • 아래는 짝수로 구성된 배열을 만드는 예시이다
function getEvens() pure external returns(uint[]) {
  uint[] memory evens = new uint[](5);
  // 새로운 배열의 인덱스를 추적하는 변수
  uint counter = 0;
  // for 반복문에서 1부터 10까지 반복함
  for (uint i = 1; i <= 10; i++) {
    // `i`가 짝수라면...
    if (i % 2 == 0) {
      // 배열에 i를 추가함
      evens[counter] = i;
      // `evens`의 다음 빈 인덱스 값으로 counter를 증가시킴
      counter++;
    }
  }
  return evens;
}

Review Function Modifier

  1. 함수가 언제, 어디서 호출될 수 있는지 제어하는 접근 제어자 (visibility modifier)

    • private은 컨트랙트 내부의 다른 함수들에서만 호출될 수 있다
    • internal은 private과 비슷하지만, 해당 컨트랙트를 상속하는 컨트랙트에서도 호출될 수 있다.
    • external은 오직 컨트랙트 외부에서만 호출될 수 있다.
    • public은 내외부 모두에서, 어디서든 호출될 수 있다.
  2. 블록체인과 상호작용하는 방법에 대해 알려주는 상태 제어자 (state modifier)

    • view는 해당함수를 실행해도 어떤 데이터도 저장/변경되지 않음을 알려준다.
    • pure는 해당함수가 어떤 데이터도 블록체인에 저장하지 않을 뿐 아니라, 블록체인으로부터 어떤 데이터도 읽기 않음을 알려준다.
    • 이들 모두 컨트랙트 외부에서 불렸을 때 가스를 전혀 소모하지 않는다. 다만 다른 함수에 의해 내부적으로 호출되었을때는 가스를 소모한다.
  3. 사용자 정의 제어자

    1. onlyOwner, aboveLevel등과 같은 것들
    2. 이런 제어자들을 사용하여 함수에 이 제어자들이 어떻게 영향을 줄 지 결정하는 우리만의 논리를 구성할 수 있다.
  4. 위 제어자들은 함수 하나에 다음처럼 함께 사용 가능

    function test() external view onlyOwner anotherModifier {
    
    }

Payable modifier

  • 이더를 받을 수 있는 특별한 함수 유형
  • 일반적인 웹 서버에서 api함수를 실행할 때는, 함수호출을 통해서 돈이나 코인을 보낼 수 없다.
  • 반면 이더리움에서는, 이더, 데이터(tansaction payload), 컨트랙트 코드 자체 모두가 이더리움 위에 존재하기 때문에, 함수를 실행하는 동시에 컨트랙트에 돈을 지불하는 것이 가능하다. => 함수를 실행하기 위해 컨트랙트에 일정 금액을 지불하게 하는 것과 같은 기능 구현 가능
contract OnlineStore {
  function buySomething() external payable {
    // 함수 실행에 0.001이더가 보내졌는지 확실히 하기 위해 확인:
    require(msg.value == 0.001 ether);
    // 보내졌다면, 함수를 호출한 자에게 디지털 아이템을 전달하기 위한 내용 구성:
    transferThing(msg.sender);
  }
}
  • 위 예시에서 msg.value는 컨트랙트로 이더가 얼마나 보내졌는지 확인하는 방법, ether는 기본적으로 포함된 단위
  • 누군가 web3.js에서 다음과 같이 함수를 실행하면 액션
// `OnlineStore`는 자네의 이더리움 상의 컨트랙트를 가리킨다고 가정:
OnlineStore.buySomething({from: web3.eth.defaultAccount, value: web3.utils.toWei(0.001)})
  • value필드를 보면, 자바스크립트 함수 호출에서 이 필드를 통해 ether를 얼마나 보낼지 결정한다.
  • 트랜잭션을 봉투로 생각하고 함수 호출에 전달하는 매개 변수를 편지내용이라고 생각한다면, value는 봉투 안에 현금을 넣는 것과 같다.
  • 만약 함수가 payable로 표시되지 않았는데 이더를 보내려고 한다면 함수에서 트랜잭션을 거부할 것이다.

Withdraws

출금

이더를 인출하는 함수는 다음과 같이 작성한다,

// Ownable 컨트랙트를 import 햇다고 가정하고 owner, onlyOwner 사용
contract GetPaid is Ownable {
	function withdraw() external onlyOwner {
		owner.transfer(this.balance);
	}
}
  • transfer 함수를 사용하여 이더를 특정 주소로 전달 가능

  • this.balance는 컨트랙트에 저장되어있는 전체 잔액을 반환한다. 100명의 유저가 이 컨트랙트에 1이더를 지불한다면 this.balance는 100이더가 된다.

  • transfer함수를 써서 특정한 이더리움 주소에 돈을 보낼 수도 있다. 예를 들어, 만약 누가 한 아이템에 대해 초과지불을 했다면 이더를 msg.sender로 되돌려주는 함수를 만들 수도 있다.

    uint itemFee = 0.001 ether;
    msg.sender.transfer(msg.value - itemFee);

Random Numbers

Random number generation via keccak256

솔리디티에서 keccak256 해시 함수를 사용하여 난수를 만드는 방식은 다음과 같다.

// Generate a random number between 1 and 100:
uint randNonce = 0;
uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;
randNonce++;
uint random2 = uint(keccak256(now, msg.sender, randNonce)) % 100;
  • 이 예시에서는 now의 타임스탬프 값, msg.sender, 증가하는 nonce(딱 한 번만 사용되는 숫자, 즉, 똑같은 입력으로 두 번 이상 동일한 해시 함수를 실행할 수 없게 함)를 받는다.
  • 그리고 keccak을 사용하여 이 입력들을 임의의 해시값으로 변환하고, 변환한 해시값을 uint로 바꾼 후, %100을 써서 마지막 2자리 숫자만 받도록 한다. 이를 통해 0과 99 사이으이 완전한 난수를 얻는다.

This method is vulnerable to attack by a dishonest node

  • 이더리움에서는 컨트랙트의 함수를 실행하면 트랜잭션(transaction)으로서 네트워크의 노드 하나 혹은 여러 노드에 실행을 알리게 된다. 그 후 네트워크의 노드들은 여러 개의 트랜잭션을 모으고 Proof of Work(작업증명)으로 알려진 계산이 매우 복잡한 수학적 문제를 먼저 풀기 위한 시도를 하게 된다. 그리고서 해당 트랜잭션 그룹을 그들의 PoW와 함께 Block으로 네트워크에 배포한다.
  • 한 노드가 어떤 PoW를 풀면, 다른 노드들은 그 PoW를 풀려는 시도를 멈추고 해당 노드가 보낸 트랜잭션 목록이 유효한 것인지 검증한다. 유효하다면 해당 블록을 받아들이고 다음 블록을 풀기 시작한다.
  • 이것이 난수 함수를 취약하게 만드는데, 만약 내가 노드를 실행하고 있다면, 나는 오직 나의 노드에만 트랜잭션을 알리고 이것을 공유하지 않을 수도 있다.

So how do we generate random numbers safely in Ethereum?

방법이 없진 않다. 아래 글 참고

https://ethereum.stackexchange.com/questions/191/how-can-i-securely-generate-a-random-number-in-my-smart-contract

About

Summary of CryptoZombie

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published