블록체인: 스마트 컨트랙트 문법

RK_NFT_HOLDER·2022년 11월 28일
0

BEB_07_RK_NFT

목록 보기
10/10

스마트 컨트랙트를 만들기 위해서 특별한 게 필요한 것은 아니다. 스마트 컨트랙트는 사실 우리가 배웠던 다른 개발 언어와 비슷하며, 다만 블록체인 위에서 구동하기 위해 만들어진 부분에 대한 차이가 있을 뿐이다.

컨트랙트 구조

상태 변수(State Variables)
구조체(Struct Types)
열거형(Enum Types)
함수(Functions)
함수 제어자(Function Modifiers)
이벤트(Events)
에러(Errors)
상속(inheritance)

솔리디티(Solidity)

객체 지향 프로그래밍 언어

이더리움 블록체인 플랫폼에서 다양한 스마트 컨트랙트(계약 로직)를 작성할 때 사용
ex) 투표, 크라우드 펀딩, 블라인드 경매, 다중 서명 지갑 등

정적 타입의 중괄호 언어
솔리디티는 정적 타입(static-typed)의 중괄호(curly-braces) 프로그래밍 언어
자바스크립트와 달리 컴파일 시에 변수에 타입이 결정
그렇기에 소스 코드에 타입을 명확하게 정의해 줘야 함

튜링 완전 언어
이더리움은 gas를 통한 과금 메커니즘을 도입하여 
블록체인 네트워크에서 튜링 완전 언어를 실현
솔리디티는 비트코인 스크립트와 다르게 반복문 등의 작업을 수행 가능
이더리움 네트워크에서 다양한 스마트 컨트랙트를 구현하는 기반이 됨

세미콜론(;)
마지막으로 솔리디티는 문장의 끝에 반드시 세미콜론(; )을 붙여야 함
세미콜론은 문장의 끝을 구분해주는 역할을 하여, 이를 붙이지 않을 시
컴파일 에러가 발생    

SPDX License Identifier

스마트 컨트랙트에 대한 신뢰를 높임
저작권과 같은 문제를 해소하기 위해 솔리디티 코드의 최상단에 SPDX 라이센스를 명시

SPDX 라이센스는 주석으로 표기

// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: GPL-3.0

SPDX 라이센스 리스트는 https://spdx.org/licenses/ 에서 확인할 수 있습니다.

Pragma

pragma 키워드는 특정 컴파일러의 버전을 표기할 때 사용

솔리디티 컴파일러와의 호환성을 지칭하는 것
“이 코드에서는 해당 버전의 컴파일러가 필요하다"라는 것을 명시

pragma는 모든 소스 코드 파일에 있어야 함
다른 파일을 임포트 하더라도, pragma는 자동으로 임포트 되지 않음

일반적으로 다음과 같이 코드 최상단에 작성합니다.

// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.14;  // 0.8.14 버전을 사용

특정 버전 이상의 pragma를 사용할 때는 ^

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.14;  // 0.8.14 이상의 버전을 사용
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0; // 0.4.0 이상 0.9.0 미만의 버전을 사용

그 외에도 다양한 버전 규칙을 사용할 수 있으며, 버전 규칙은 npm 문법과 동일
npm 버전 규칙: https://docs.npmjs.com/cli/v6/using-npm/semver

Contract

솔리디티 계약은 이더리움 블록체인의
특정 주소를 가진 기능(코드)와 상태(데이터)의 모음
여기서 특정 기능은 함수로 표현, 상태는 변수로 표현
일반적인 객체 지향 언어의 클래스를 정의하듯
하나의 contract(계약)를 정의

contract SimpleStorage {
	uint storedData; // 상태 변수

		// 함수
	function set(uint x) public {
    	storedData = x;
	}
		// 함수
	function get() public view returns (uint) {
 	   return storedData;
	}
}

Import

파일을 임포트 하는 방식은 자바스크립트에서 사용하는 방식과 동일

import "파일이름";

// 임포트하는 파일을 symbolName이라는 이름으로 사용
import * as symbolName from "파일이름";
import "파일이름" as symbolName;

// 파일의 일부분만 임포트 하는 경우
import {symbol1 as alias, symbol2} from "파일이름";

주석

단일 라인 주석 처리는(//), 여러 라인 주석 처리는(/.../)로 진행합니다.

// 단일 라인 주석입니다.

/*
이것은
여러 라인 주석입니다.
*/

변수

데이터 저장 위치

보통의 프로그래밍 언어라면 변수는 스택, 힙 등 메모리에 저장되는 것이 기본
솔리디티는 변수를 메모리뿐 아닌 하드 디스크 등과 같은 스토리지에 저장하기도 함
또한 calldata 라는 영역에 저장하기도 하는데, 여기에는 함수 인자가 저장
calldata 역시 메모리처럼 동작하지만 수정 불가능하고 비영구적인 영역

솔리디티의 데이터 저장 위치

메모리
스토리지	
calldata

강제 데이터 위치

외부 함수의 매개 변수(반환 값 미포함): calldata
상태 변수: 스토리지

기본 데이터 위치

함수의 매개변수(반환 값 포함): 메모리
모든 지역 변수: 스토리지

변수의 종류

솔리디티에서 변수는 크게 상태변수, 지역변수, 전역변수로 나누어진다.
상태변수와 지역변수는 일반적인 프로그래밍 언어에서의 변수를 생각하면 된다.

선언 및 초기화 방식

{데이터타입} {변수명}; // 변수명으로 선언
{데이터타입} {변수명} = {초기화할 값};  // 선언 및 초기화

전역변수는 솔리디티만의 특수한 변수(또는 함수)로써 주로 블록체인에 관한 정보를 제공합니다.

전역변수는 block.number(현재 블록의 번호), msg.sender(Tx 송신자의 address) 등 컨트랙트를 생성할 당시에는 알 수 없는 블록체인에 관한 정보를 제공하여 컨트랙트의 사용성을 높여줍니다.

전역변수는 따로 선언이나 초기화 없이 불러와서 사용합니다.

상태변수

상태변수란 컨트랙트 저장소(이더리움 블록체인)에 영구적으로 저장되는 변수를 말합니다.
즉, 항상 스토리지에 저장됩니다.
보통 컨트랙트 최상위 단에 선언합니다.

1
2
3
4
5
6
pragma solidity ^0.8.14;

contract SimpleStorage {
uint storedData; // 상태변수 선언
uint storedData2 = 20; // 상태변수 선언 및 초기화
}
세부적인 자료형에 대해서는 이후에 다룹니다.

상태 변수 접근 수준

컨트랙트 내의 상태(state) 변수를 선언할 때는 지정할 수 있는 접근 수준을 함께 표기합니다.

접근 수준은 public, internal, private, constant / immutable, 네 가지로 나뉩니다.

internal(default):
상태 변수에 기본적으로 사용
컨트랙트 및 해당 컨트랙트를 상속받은 컨트랙트만 접근 가능
외부에서 액세스 불가능
public:
컴파일러가 자동으로 getter 함수를 생성해줌
컨트랙트 내에서 직접 퍼블릭 상태 변수를 사용 가능
외부 컨트랙트나 클라이언트 코드에서도 getter 함수를 통해 퍼블릭 상태 변수에 접근 가능
private:
동일한 컨트랙트 멤버만 프라이빗 상태 변수에 접근 가능
constant / immutable:
선언될 때 값을 할당해야 함
상수화 = 변경 불가능

지역변수

지역변수란 함수가 실행될 때까지만 존재하는 변수
지역변수 역시 기본값으로는 스토리지에 저장(레퍼런스 타입의 경우 재정의 가능)
보통 함수 아래에 선언되는 변수

pragma solidity ^0.8.14;

contract SimpleStorage {
	...
		function simpleFunction() public pure returns(uint) {
    	uint a; //  지역변수 선언
			uint b = 1; // 지역변수 선언 및 초기화
			a = 1;
			uint result = a + b;
			return result;
		}
}

전역변수

전역변수란 글로벌한 블록체인 안에 있는 특수 변수
블록체인 및 트랜잭션에 대한 속성을 가지고 올 수 있음

function f(uint start, uint daysAfter) public {
    if (block.timestamp >= start + daysAfter * 1 days) {
      // 여기서 block.timestamp는 전역변수
    }
}

block: 블록에 대한 정보를 가지고 있습니다.
msg: 컨트랙트를 시작한 트랜잭션 콜이나 메시지 콜에 대한 정보를 가지고 있습니다.
tx: 트랜잭션 데이터를 가지고 있습니다.
This: 현재 컨트랙트를 참조합니다. 현재 컨트랙트 주소로 암시적으로 변환됩니다.

자료형

값 형 데이터 타입(Value Types)
참조형 데이터 타입(Reference Types)

값 형 데이터 타입(Value Types)

1. 불(bool)

bool(boolean)로 선언된 변수는 true나 false 값을 가집니다.

bool isOpen = true;
bool isSold = false;

2. 정수(int, uint)

정수의 경우에는 int, 0을 포함한 양의 정수 값에는 uint를 사용합니다.

int, uint 뒤에 8 ~ 256의 8의 배수의 숫자를 붙여 변수의 크기를 비트 단위로 지정할 수도 있습니다.
(예. int8, int16, int24, uint64, uint128, …)

각 숫자에 따라 정수는 특정 범위로 제한됩니다.
int16는 -32768~32767 사이의 정수를 의미하며, uint16은 0~65535 사이의 정수를 의미합니다.

일반적으로 뒤에 숫자가 없는 int, utint는 int256, uint256을 의미합니다.

int8 seoulTemp = -20
uint16 myAge = 30

3. 주소(address)

주소 타입은 크게 두 가지 유형으로 나뉩니다.

address: 20바이트의 이더리움 주소 값을 가짐
address payable: address와 동일한 값을 가지지만 추가 멤버인 transfer, send를 가짐
주소(address) 객체는 0x로 시작하고 최대 40자리의 16진수로 구성되는 문자열을 값으로 가집니다.

address yourAddress = 0x10abb5efEcdc01234f7b2384912398798E089Ab2;

중요한 것은, 0.8 버전부터 address 형식은 송금이 불가능한 주소값이라는 점입니다.

스마트 컨트랙트에서 특정 주소 값으로 송금하기 위해서는 address payable 형식을 사용해야 합니다. address payable 형식에는 이더 송금을 위한 transfer()와 send() 함수가 내장되어 있습니다.

address 형식 데이터를 payable() 함수에 인자로 담아 address payable 형식 데이터를 만들 수 있습니다.

address addr1;
address payable p_addr1 = payable(addr1);

uint160 또는 bytes20 형식의 데이터를 address payable로 바꾸기 위해서는 먼저 address()를 사용하여 주소 형태로 만들고, 다시 payable()을 사용해 address payable 형식으로 바꿀 수 있습니다.

uint160 num;
address addr = address(num);
address payable p_addr = payable(addr);

컨트랙트를 address payable로 변환할 수도 있습니다.

만약 컨트랙트가 이더를 받을 수 있는 컨트랙트인 경우,
address(컨트랙트)를 수행했을 때 address payable 형식의 주소값을 반환합니다.

contract C  {  // 이더를 받을 수 있는 컨트랙트
	constructor () payable { }
}
address payable addr = address(C);  // address(C)는 address payable 형식의 주소값을 반환한다

반면, 컨트랙트가 이더를 받지 않는 컨트랙트인 경우,
address(컨트랙트)를 수행했을 때 address 형식의 주소값을 반환합니다.

이 경우 결과값을 payable()에 넣어 address payable 형식으로 만들 수 있습니다.

contract D {. // 이더를 받지 않는 컨트랙트
	 constructor () { }
}

address addr = address(D); // address(D)는 address 형식의 주소값을 반환한다
address payable addr_p = payable(addr); // payable()을 사용해 address payable 형식의 주소값을 만들 수 있다.

4. 바이트 배열(고정 크기)

데이터를 바이너리 형태로 저장하기 위해 사용합니다.
bytes1 ~ bytes32까지의 고정된 크기의 배열을 선언합니다.
정해진 바이트 크기와 값의 크기가 다르면 에러가 납니다.

bytes3 alphabets = 'abc';

alphabets[0] // 'a'
alphabets[1] // 'b'
alphabets[2] // 'c'

바이트 배열(가변 크기)는 참조형 타입으로 추후 다룹니다.

5. 열거형(enum)

열거형(enum)은 특정 값들로 집합을 지정하고, 집합에 있는 데이터만을 값으로 가집니다.
각 집합의 데이터는 내부적으로는 순서에 따라 0부터 1씩 올라가는 정수를 값으로 가집니다.

enum EvalLevel { Bad, Soso, Great } // 열거형 집합을 지정합니다.
EvalLevel kimblock = EvalLevel.Bad // 열거형으로 변수를 선언합니다.
int16 kimblockValue = int16(kimblock); // kimblock 열거형 값 0을 정수형으로 변환합니다.

추가적으로 (u)fixedMxN 형식으로 표현되는 고정소수점 타입이 존재하지만 현재 솔리디티에서는 완전히 지원되지 않기에 다루지 않았습니다.

참고: https://docs.soliditylang.org/en/v0.8.14/types.html#fixed-point-numbers3

참조형 데이터 타입(Reference Types)

참조형 변수(reference type)는 마치 배열과 같이, 연속되어 저장되는 값의 첫번째 메모리의 주소를 값으로 가지는 변수 타입입니다. 이를 '참조한다'라고 표현합니다.

데이터 저장 영역에는 세 종류가 있다고 위에서 다룬 적이 있습니다.

메모리: 프로그램이 동작하는 동안에만 값을 기억하고, 종료되면 값을 잃는 데이터 영역
스토리지: 블록체인에 기록되어 영구적으로 값이 유지되는 데이터 영역
calldata: 메모리와 비슷하지만 수정 불가능하고 비영구적인 데이터 영역

참조형 변수를 선언할 때는 메모리에 저장할지 스토리지에 저장할지 명시해야 합니다.

(상태변수는 무조건 스토리지에 저장됩니다.)

function f() {
     // 5개의 int32 형태의 데이터를 메모리에 저장하는 변수 fixedSlots 선언
    int32[5] memory fixedSlots;
    fixedSlots[0] = 13;
}

참조형 변수의 유형은 다음과 같습니다.

배열(Array)
바이트 배열(가변 크기, Dynamically-sized byte array)
문자열(String)
구조체(Struct)
매핑(Mapping)

1. 배열(array)

배열(array)은 저장하고자 하는 데이터 형식에 []를 붙여 선언합니다.

가령 uint8 형식의 데이터를 저장하는 배열을 만드는 경우, uint8[]과 같이 자료형을 작성합니다.

배열에는 정적 배열과 동적 배열이 존재합니다.

정적 배열: uint[4] {배열 이름} 과 같은 형식으로 사용할 배열의 크기를 지정하여 선언합니다.
동적 배열: uint[] {배열 이름} 과 같은 형식으로 배열의 크기를 지정하지 않고 선언합니다.
		5개의 동적 배열로 구성된 배열은 uint[][5]로 작성됩니다.

new 키워드를 사용해 동적 배열을 메모리에 할당할 수도 있습니다.

1-2. 바이트 배열(가변 크기)

바이트(bytes)는 특수한 형태의 배열입니다.
고정 크기 바이트배열과 다르게 크기를 정해놓지 않고, 입력값에 따라 크기가 달라집니다.

bytes로 가변 크기의 배열을 선언합니다.

	bytes alphabets = 'abc';
    

1-3. 문자열(string)

문자열(string) 역시 특수한 형태의 배열로써, 바이트 배열(가변 크기)에 기반한 문자열 타입입니다.

string은 bytes와 동일하지만, index, push, length, concat 등을 지원하지 않습니다.
문자열 리터럴로 초기화합니다.

string name = 'kimblock';

2. 구조체(struct)

구조체(struct)는 서로 다른 유형의 항목을 포함하는 집합으로, 사용자 정의 형식입니다.
구조체는 배열과 매핑의 내부에서 사용될 수 있으며, 반대로 구조체에 배열과 매핑을 포함할 수도 있습니다.

하지만 구조체가 동일한 구조체 타입의 멤버를 포함할 순 없습니다.

구조체는 다음과 같이 정의합니다.

contract exmapleC {
    struct User {
        address account;
        string lastName;
        string firstName;
            mapping (uint => Funder) funders;
    }

    mapping (uint => User) users;
}

구조체를 사용할 때는 각 항목에 대한 값을 객체 형식으로 추가합니다.

contract exmapleC {

    struct User {
        address account;
        string lastName;
        string firstName;
    }

    function newUser (address newAddress, string newLastName, string newFirstName){
        User memory newOne = User({account: newAddress, lastName: newLastName, firstName: newFirstName});
    }
}

3. 매핑(mapping)

매핑(mapping)은 스토리지 데이터 영역에서 키-값 구조로 데이터를 저장할 때 사용하는 참조형입니다.

mapping(키 형식=> 값 형식) 변수명 형태로 선언합니다.

여기서 키 형식은 매핑, 구조체, 배열 제외한 유형의 값이 다 될 수 있습니다.

여기서 키, 값 형식은 매핑, 구조체, 배열을 포함한 모든 유형의 값이 다 될 수 있습니다.

mapping(address => int) public userAddress;

매핑은 일반적인 프로그래밍 언어에서의 해시 테이블 또는 딕셔너리(자바스크립트는 객체)와 유사합니다.

키 자체가 실제로 저장되지는 않고, 키의 keccak256 해시를 이용해 값에 접근합니다.

매핑은 오직 스토리지 영역에만 저장될 수 있으므로 상태 변수, 내부 함수에서의 스토리지 참조 타입, 라이브러리 함수의 매개 변수에만 허용됩니다.

profile
blockchain

0개의 댓글