1. 솔리디티 컨트랙트 기본 구조
1) 솔리디티 소스 파일 레이아웃
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
// contract 키워드를 사용한다.
contract Storage {
// 상태 변수
uint256 number;
// 함수
function store(uint256 num) public {
number = num;
}
function retrieve() public view returns (uint256){
return number;
}
}
- 라이센스 표현 (필수)
- 컴파일러 버전 명시
- 소스코드가 이용하는 컴파일러 버전
contract 키워드 + 컨트랙트 이름
으로 컨트랙트 생성- 상태변수: 블록 체인 (contract storage)에 값이 저장되는 변수이다.
- 상태 변수의 접근 제어자를 지정할 수 있다. (external, public, private)
- 기본형, 구조체, 배열 등 다양한 자료형이 존재한다.
- 함수: 컨트랙트의 단위 기능
- 매개변수, 제어자, 반환값이 지정 가능하다.
- 함수 내부에서 상태 변수의 값을 변경하고 읽을 수 있다. (함수를 통해 상태 변수를 제어한다.)
2. 기본 문법
1) 자료형
✅ 원시 자료형
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Primitives {
// Unassigned variables have a default value
bool public defaultBool; // false
uint public defaultUint; // 0
int public defaultInt; // 0
address public defaultAddr; // 0x0000000000000000000000000000000000000000
string public defaultString; // ''
// addr value assignment
address public addr = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
/*
non negative integers
different sizes are available
uint8 ranges from 0 to 2 ** 8 - 1
uint16 ranges from 0 to 2 ** 16 - 1
...
uint256 ranges from 0 to 2 ** 256 - 1
uint is same as uint256
*/
uint8 public u8 = 1;
uint public u = 123;
uint256 public u256 = 456;
uint public maxUint = type(uint).max;
uint public maxUint256 = type(uint256).max;
/*
integer
different sizes are available, like uint
int256 ranges from -2 ** 255 to 2 ** 255 - 1
int128 ranges from -2 ** 127 to 2 ** 127 - 1
*/
int8 public i8 = -1;
int public i = -123;
int256 public i256 = 456;
// minimum and maximum of int
int public minInt = type(int).min;
int public maxInt = type(int).max;
/*
In Solidity, the data type byte represent a sequence of bytes.
Two bytes types in Solidity:
- fixed-sized byte arrays: bytes#
- dynamically-sized byte arrays. byte[]
*/
bytes1 a = 0xb5; // [10110101]
bytes1 b = 0x56; // [01010110]
}
자료형 | 예시 |
---|---|
논리형 | bool : true, flase |
정수형 | uint : 음수가 아닌 integer int : integer8 ~ 256 bit를 표현할 수 있으며, uint는 uint256과 같다. |
주소형 | address : 이더리움의 주소를 의미한다.이더리움 블록체인은 은행 계좌와 같은 계좌로 이루어져 있다. 주소는 특정 계정을 가르키는 고유 식별자이다. |
바이트형 | bytes# or byte[] : 데이터를 바이트로 표현할 수 있다. |
문자형 | string : 임의의 길이를 가진 UTF-8 데이터를 의미한다. |
✅ 배열
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Array {
// Dynamic sized array: 배열의 사이즈를 선언하지 않음.
uint[] public arr; // Not initialized
uint[] public arr2 = [1, 2, 3]; // Initialized
// Fixed sized array, all elements initialize to 0: 배열의 사이즈를 선언.
uint[10] public fixedSizeArr;
// Compare with accessing state variable
function get(uint i) public view returns (uint) {
return arr2[i];
}
// Append new element to array
// Check array size after calling this function.
function push(uint i) public {
arr.push(i);
}
// Remove last element from array
// Check array size after calling this function.
function pop() public {
arr.pop();
}
// Reset the value at index: 값이 제거 되는 것이 아니라 0으로 초기화 된다.
// Check array size after calling this function.
function remove(uint index) public {
delete arr[index];
}
// returns the length of array.
function getLength() public view returns (uint) {
return arr.length;
}
// returns the entire array.
function getArr() public view returns (uint[] memory) {
return arr;
}
function createArray() external pure returns (uint[] memory){
// create array in memory, only fixed size can be created
uint[] memory a = new uint[](5);
return a;
}
}
- 정적 배열:
자료형[배열 길이]
- 동적 배열:
자료형[]
- 고정된 크기가 없으며 계속 크기가 커질 수 있다.
function getArray() external pure returns(uint[]) {
// 메모리에 길이 3의 새로운 배열을 생성한다.
uint[] memory values = new uint[](3);
// 여기에 특정한 값들을 넣는다.
values.push(1);
values.push(2);
values.push(3);
// 해당 배열을 반환한다.
return values;
}
new 타입[](배열 길이)
은 새로운 배열을 생성한다.
✅ Mapping
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.7.0 <0.9.0;
contract Mapping {
// Mapping from address to uint: Mapping 선언.
// mapping(기준 키 => 값): 키는 중복이 될 수 없다.
mapping(address => uint) public addrToUint;
// Access value with key in Mapping.
// If there is no key, it will return the default value, 0.
function get(address _addr) public view returns (uint) {
return addrToUint[_addr];
}
// Update the value at the address
function set(address _addr, uint _i) public {
addrToUint[_addr] = _i;
}
// Reset the value to the default value.: 0으로 초기화한다.
function reset(address _addr) public {
delete addrToUint[_addr];
}
}
- Mapping에 저장된 key 목록을 얻을 수 있는 방법은 제공하지 않는다.
- 구조화된 데이터를 저장하는 또 다른 방법이다.
✅ Struct
사용자 선언 자료형으로 여러 자료형을 하나의 관점으로 묶어서 관리하고자 할 때 선언한다.
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.7.0 <0.9.0;
contract Struct {
struct MyStruct {
string text;
bool boolean;
}
// An array of structs: MyStruct를 array로 선언할 수 있다.
MyStruct[] public structArray;
// A mapping from address to Todo: MyStruct를 mapping으로 선언할 수 있다.
mapping(address => MyStruct) public addrToStruct;
// Create a new struct
// method 1: () 사용
function create1(string memory _text) public {
structArray.push(MyStruct(_text, false));
}
// method 2: {} 사용
function create2(string memory _text) public {
structArray.push(MyStruct({text: _text, boolean: false}));
}
// method 3: Struct타입을 별도의 로컬변수로 선언하여 array로 push
function create3(string memory _text) public {
MyStruct memory s;
s.text = _text;
structArray.push(s);
}
// Update text
function updateText(uint _index, string memory _text) public {
// 해당 index의 struct값을 가져와서 재할당한다.
MyStruct storage s = structArray[_index];
s.text = _text;
}
// Switch Boolean
function updateBoolean(uint _index) public {
MyStruct storage s = structArray[_index];
bool current = s.boolean;
s.boolean = !current;
}
}
// 구조체 압축하기
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);
- 기본적으로 하위 타입을 쓰는 것인 아무런 이득이 없다. 하지만 구조체는 다르다.
2) 접근 제어자
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Parent {
// State variables
string private privateVar = "private variable";
string internal internalVar = "internal variable";
string public publicVar = "public variable";
// Private function: 직접 접근이 불가능하다.
function privateFunc() private pure returns (string memory) {
return "private function called";
}
// privateFunc는 직접 접근이 불가능 하기 때문에, 이렇게 함수를 호출해서 사용할 수 있다.
function testPrivateFunc() public pure returns (string memory) {
return privateFunc();
}
// Internal function
function internalFunc() internal pure returns (string memory) {
return "internal function called in Parent Contract";
}
function testInternalFunc() public pure virtual returns (string memory) {
return internalFunc();
}
// Public functions: 내부, 자식, 외부에서 호출이 가능하다.
function publicFunc() public pure returns (string memory) {
return "public function called";
}
// External functions: 외부에서만 호출을 할 수 있다.
function externalFunc() external pure returns (string memory) {
return "external function called";
}
}
// Child가 Parent를 상속 받음
contract Child is Parent {
// Internal function call be called inside child contracts: 부모의 internal 함수를 호출.
function testInternalFunc() public pure override returns (string memory) {
return internalFunc();
}
}
private | internal | public | external | |
---|---|---|---|---|
설명 | 컨트랙트 내에서만 접근이 가능하다. | 현재의 컨트랙트와 자식 컨트랙트에서 접근이 가능하다. | 현재 컨트랙트, 자식 컨트랙트, 외부 컨트랙트 및 주소에서 접근이 가능하다. | 컨트랙트 외부에서 접근이 가능하다. |
State Variables | O | X | O | O |
Functions | O | O | O | O |
3) 함수
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.7.0 <0.9.0;
contract Function {
uint public num = 1;
uint public a = 1;
string public s = "hello solidity";
bool public b = true;
// No parameter and return value
function addOne() public {
num++;
}
// One parameter and a return value
function addNumber(uint x) public returns (uint) {
num += x;
return num;
}
// view - not to modify the state variable, but read.
function addAndReturn(uint x) public view returns (uint) {
return num + x;
}
// pure - not to modify or read the state variable.
function add(uint x, uint y) public pure returns (uint) {
return x + y;
}
// Return many values: 2개 이상의 값을 반환할 수 있다.
function returnMany() public view returns (uint, string memory, bool) {
return (a, s, b);
}
}
- 함수 인자명을
언더스코어(_)
로 시작해서 전역 변수와 구별하는 것이 관례이다. - private 함수명도
언더스코어(_)
로 시작하는 것이 관례이다.
함수 상태 제어자 | 설명 |
---|---|
view | 블록체인에서 데이터를 읽기만 한다. 순수하게 데이터에 접근해서 변경하지 않고 값만 가져온다. (gas를 소비하지 않는다.) 즉, 이더리움 없이도 호출할 수 있다. 경제적 프로그래밍이 가능하다. |
pure | 블록체인에서 데이터를 쓰고, 수정하고, 읽지 않는다. 상태 변수에 접근하지 않아도 실행할 수 있는 함수이다. |
payable | 이더를 받을 수 있는 특별한 함수 유형이다. 해당 함수를 호출하면 이더를 요청할 수 있다. msg.value 라는 글로벌 변수가 있다. (보낸 이더 금액 확인) |
4) 조건, 반복문
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.7.0 <0.9.0;
contract IfElse {
function foo(uint x) public pure returns (uint) {
if (x < 10) {
return 0;
} else if (x < 20) {
return 1;
} else {
return 2;
}
}
function ternary(uint _x) public pure returns (uint) {
return _x < 10 ? 1 : 2;
}
}
contract Loop {
function loop1() public pure {
for (uint i = 0; i < 10; i++) {
if (i == 3) {
continue;
}
if (i == 5) {
break;
}
}
}
function loop2() public pure {
uint i;
while (i < 10) {
i++;
}
}
}
- 이더리움이 튜링 완전 머신이라고 할 수 있는 이유는 반복문이 있기 때문이다.
- 블록체인에서 반복문은 사실 리스크가 크다. (모든 컴퓨터에 영향을 주기 때문)
- gas라는 개념이 등장하게 되면서, 무한 루프를 방지할 수 있기 때문에 반복문이 도입이 가능할 수 있었던 것.
- gas limit까지 사용하면 자동으로 반복문이 취소된다.
5) 화폐 단위
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.7.0 <0.9.0;
contract EtherUnits {
uint public oneWei = 1 wei;
uint public oneGwei = 1 gwei;
uint public oneEther = 1 ether;
// 1 wei is equal to 1
bool public isOneWei = 1 wei == 1;
// 1 ether is equal to 10^18 wei
bool public isOneEther1 = oneEther == 1e18;
// 1 ether is equals to 10^9 gwei.
bool public isOneEther2 = oneEther == 10**9 * oneGwei;
// 1 gwei is equals to 10^9 wei.
bool public isOneGwei = oneGwei == 10**9 * oneWei;
}
- 이더리움 버추얼 머신에서 소숫점을 허용하지 않는다.
- wei * (10**18) = Ether
단어 | 설명 |
---|---|
Wei | 기본적으로 인식할 수 없을 정도의 작은 단위로, 기술적인 경우나 코드 작성에만 사용된다. |
Ether | 이더리움의 액면가치로 실질적인 거래는 Ehter의 관점에서 진행된다. 1,000,000,000,000,000,000 wei이다. |
6) Data Location
- 모든 복합 타입은 자신이 메모리 나 스토리지 중 어디에 저장되었는지를 나타내는 “데이터 위치”가 추가적으로 존재한다.
- 컨텍스트에 따라 항상 기본값이 존재하지만, 타입에
storage
,memory
,calldata
를 추가하여 재정의 할 수 있다. - 데이터 위치는 변수가 할당되는 방식을 변경하기 때문에 중요하다.
- 함수 내부에 선언된 변수의 기본값은
memory
이다. - 지역 변수의 기본값은
storage
이며 상태 변수의 위치는storage
로 강제되어 있다.
- 함수 내부에 선언된 변수의 기본값은
- 구조체, 배열을 사용할 때는 명시적으로 작성해야 한다.
단어 | 설명 |
---|---|
storage | 변수가 블록체인에 기록되어 영구적이다. 모든 계약에는 자체 저장소가 있기에 변수는 영구적이다. 따라서 어디서든 스토리지 변수에 접근을 할 수 있으며 값을 수정할 수 있다. 단, 위치는 영구적이다. 영구적이다. |
memory | 메모리에 저장된 변수는 함수 내에서 선언된다. 이는 영구적이지 않고 일시적으로 존재하는 변수이다. memory 변수의 목적은 계산을 돕는 것으로 다른 곳에서는 해당 변수에 접근할 수 없다. 지속성이 없다. |
call data | 임시 데이터 로 선언된 함수 내에서만 사용할 수 있다. 거의 memory 처럼 작동한다. 수정 불가능 지속성이 없다. => 외부 함수의 함수 매개변수 |
7) Keccak256과 형변환
이더리움은 keccak256을 내장 해시 함수로 가지고 있다.
해시 함수는 기본적으로 입력 스트링을 랜덤 256비트 16진스로 매핑한다. 스트링에 약간의 변화라도 있으면 해시 값은 크게 달라진다.
해시 함수는 이더리움에서 여러 용도로 활용되지만 의사 난수 발생기로 이용할 수 있다.
// 예시
//6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5
keccak256("aaaab");
//b1f078126895a1424524de5321b339ab00408010b7cf0e6ed451514981e58aa9
keccak256("aaaac");
// 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;
- 블록 체인에서 안전한 의사 난수 발생기는 어려운 문제이다.
- 보안에 있어서 큰 문제가 없을 경우에만 사용하는 것을 추천한다.
- 솔리디티는 고유의 스트링 비교 기능을 가지고 있지 않기 때문에 스트링의 keccak256 해시 값을 비교하여 스트링 값이 같은지 판단한다.
8) Event
컨트랙트에서 특저어 이벤트가 발생하는지 귀를 기울이고 그 이벤트가 발생하면 행동을 취한다.
// 이벤트를 선언한다
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;
}
9) 인터페이스
블록체인 상에 있으면서 우리가 소유하지 않은 컨트랙트와 우리 컨트랙트가 상호작용을 하려면 인터페이스를 정의해야 한다.
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`으로 무언가를 할 수 있다
}
}
10) 시간 단위
솔리디티는 시간을 다룰 수 있는 단위를 기본적으로 제공한다.
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));
}
- now 변수를 사용하면 현재의 유닉스 타임스탬프 값을 얻을 수 있다.
- 유닉스 타임은 32비트의 숫자로 저장된다.
- 솔리디티는 seconds
,
minutes,
hours,
days,
weeks,
years와 같은 시간 단위가 있다.- 이에 해당하는 길이만큼의 초단위uint숫자로 변환된다.
- ex) 1 minutes = 60, 1hours = 3600
11) Ownable 컨트랙트
컨트랙트를 대상으로 특별한 권리를 가지는 소유자가 있음을 의미하는 컨트랙트이다.
OpenZeppelin 솔리디티 라이브러리에서 가져온 Ownable
컨트랙트
contract Ownable {
address public owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
// 생성자 함수
function Ownable() public {
owner = msg.sender;
}
// 접근을 제어하기 위한 modifier 함수
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
// onlyOwner를 충족하면 해당 함수를 실행할 수 있다.
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0));
OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
}
- 컨트랙트가 생성되면 컨트랙트의 생성자가
owner
에msg.sender
(컨트랙트를 배포한 사람)를 대입한다. - 특정한 함수들에 대해서 오직
소유자
만 접근할 수 있도록 제한 가능한onlyOwner
제어자를 추가한다. - 해당 컨트랙트를 상속받아서 사용하는 경우가 많다.
- DApp을 안정적으로 유지하도록 하는 것과, 사용자들이 그들의 데이터를 믿고 저장할 수 있는 소유자가 없는 플랫폼을 만드는 것 사이에서 균형을 잘 잡는 것이 중요하다.
3. Solidity 연습: FundRaising 구현하기
특정 사람에게 모금을 해서 기부를 하는 프로세스이다.
내가 모금한 것이 정확히 잘 전달 되었는가에 대한 의심을 없앨 수 있다.
- 일회성으로 동작하는 모금 컨트랙트이다.
- 일정 기간 동안만 이더를 지불하여 모금에 참여할 수 있다.
- 모금, 현재 모금액 확인, 모금액 수령 기능을 제공한다.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract FundRaising{
// 모금 종료 시각
uint public fundRaisingCloses;
// 모금 수령자
address public beneficiary;
// 모금자
address[] funders;
// 최소 모금액
// 1e16 wei = 10**16 wei = 0.01 ether
uint public constant MINIMUM_AMOUNT = 1e16;
// 컨트랙트 배포시 모금 기간과 모금액 수령자를 지정하도록 한다.
// 3600 = 1시간
constructor (uint _duration, address _beneficiary) {
// block.timestamp: 현재 블록의 타임스탬프 값 (현재 시각)
fundRaisingCloses = block.timestamp + _duration;
beneficiary = _beneficiary;
}
// 모금 함수
// 1. 0.01 ehter이상으로만 모금에 참여할 수 있다.
// 2. 지정된 모금 시간 이내에 참여할 수 있다.
// 3. 모듬이 완료되면 모금자를 저장한다.
// payable 키워드: ether를 주고 받는 함수를 의미 한다.
// payable 키워드를 가진 함수는 msg.value 글로벌 변수를 갖고 있다. => 트랜잭션에 얼마를 보냈는 지를 알 수 있는 전역 변수
function funnd() public payable {
// if 문보다는 require를 사용하는 것을 추천한다. => 경제적 프로그래밍에 도움이 된다.
// require(판별문, "에러 메시지")로 판별문이 true가 아닌 경우 에러메시지를 출력 후 함수를 바로 종료 시킨다.
require(msg.value >= MINIMUM_AMOUNT, "MINIMUN AMOUNT: 0.01 ether");
require(block.timestamp < fundRaisingCloses, "FUND RAISING CLOSED");
// msg.sender: 함수를 호출하는 사람의 주소를 의미하는 전역 변수
// 솔리디티 함수 실행을 항상 외부 호출자가 하기 떄문에 msg.sender는 항상 존재한다.
address funder = msg.sender;
// 모금자 저장하기
funders.push(funder);
}
// 현재 모금액을 확인하는 함수
function currentCollection() public view returns(uint256) {
return address(this).balance;
}
// 함수 modifier 작성 => 재 사용성 good~
modifier onlyBeneficiary(){
require(msg.sender == beneficiary);
// 다시 함수로 돌아간다.
_;
}
// 모금 전달
// 1. 지정된 수령자만 호출할 수 있다.
// 2. 모금 종료 이후에만 호출할 수 있다.
// 3. 수령자에게 컨트랙트가 보유한 이더를 송금한다.
function withdraw() public payable onlyBeneficiary{
require(block.timestamp > fundRaisingCloses);
// 요청 주소에게 컨트랙트 보유 이더 송금: <address payable>.tranfer(uint256 amount)
payable(msg.sender).transfer(address(this).balance);
}
}
msg.sender
: 함수를 호출하는 사람의 주소를 의미하는 전역 변수modifier
는 함수 제어자이다.- 조건에 맞는지 확인 하는 용으로 다른 함수에서 사용된다.
4. Solidity 연습: ToDo 구현하기
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract ToDo{
struct ToDoItem {
string title;
string description;
bool isSucceed;
}
ToDoItem[] public toDoArray;
// 제목, 설명을 매개변수로 전달하여 toDoItem 생성
function create(string memory _title, string memory _desc) public {
toDoArray.push(ToDoItem({title: _title, description: _desc, isSucceed: false}));
}
// 인덱스를 매개변수로 전달하여 해당 인덱스의 타이틀을 수정
function updateTitle(uint _index, string memory _title) public{
ToDoItem storage item = toDoArray[_index];
item.title = _title;
}
// 인덱스를 매개변수로 전달하여 해당 인덱스의 toDo 성공 여부를 수정
function toggleSuccess(uint _index) public {
ToDoItem storage item = toDoArray[_index];
bool current = item.isSucceed;
item.isSucceed = !current;
}
// 인덱스를 매개변수로 전달하여 해당 인덱스의 desription을 반환
function getToDoDesc(uint _index) public view returns (string memory) {
ToDoItem storage item = toDoArray[_index];
return item.description;
}
}