Solidity 기본 문법
// SPDX-License-Identifier: GPL-3.0 -> 오픈소스 라이선스관련사항 명시
pragma solidity ^0.5.13;
contract WorkingWithVariables {
// 모든 정수 변수는 null 이아닌 0으로 초기화가 된다.
// bool 은 false 로 초기화된다.
// string 은 빈문자열로 초기화된다.
// uint 는 양의 정수를 말한다.
uint256 public myUint;
// uint 는 utin256 의 가명
function setMyUint(uint _myUint) public {
myUint = _myUint;
}
// bool 은 false 로 초기화된다.
bool public myBool;
function setMyBool(bool _myBool) public {
myBool = _myBool;
}
// 정수 순환을 조심해야한다. uint8 의 경우 2의 8승 즉, 0 ~ 256 까지 증가할 수 있는데 0 에서 감소할 경우 마이너스가 아닌 256 이된다.
// 정수 순환은 ^0.8 이후 버전부터는 error 가 나도록 수정되었다. 만약 똑같이 정수 순환을 구현하고 싶다면
/**
unchecked {
myUint8--;
}
로 감싸주면 된다.
*/
uint8 public myUint8;
function incrementUint() public {
myUint8++;
}
function decrementUint() public {
myUint8--;
}
// int8 의 경우 +-2의 n-1승 즉, -128 ~ 127
int8 public myInt8;
function incrementInt() public {
myInt8++;
}
function decrementInt() public {
myInt8--;
}
/**
기본 단위는 wei 이다.
wei 1wei 1
Kwei(babbage) 1e3wei 1,000
Mwei(lovelace) 1e6wei 1,000,000
Gwei(shannon) 1e9wei 1,000,000,000
microether(szabo) 1e12wei 1,000,000,000,000
milliether(finney) 1e15wei 1,000,000,000,000,000
ether 1e18wei 1,000,000,000,000,000,000
*/
// 일반적으로 주소는 20바이트를 보유한다.
address public myAddress;
function setAddress(address _address) public {
myAddress = _address;
}
function getBlanceOfAddress() public view returns(uint) {
return myAddress.balance;
}
// solidity 는 string 타입의 경우 저장할 공간이 있어야한다. -> memory , 외부 이벤트 등
// 문자열은 내부적으로 바이트 배열로 저장하며 UTF-8 데이터이다.(바이트 배열과는 다르게 인덱스로 접근 불가능) 때문에 솔리디티에서 문자열로 작업하는 것은 가스 비용이 상당히 많이 들어간다.
// 되도록 문자열 저장을 피하고 대신 이벤트를 사용해라
string public myString = 'Hello world';
function setMyString(string memory _myString) public {
myString = _myString;
}
}
이더 단위
Ether를 더 작은 단위로 변환하기 위해 숫자리터럴 뒤에 wei, finney, szabo, ether 라는 접미사가 붙을 수 있습니다. Ether통화를 나타내는 숫자리터럴에 접미사가 붙지 않으면 Wei가 붙어있다고 간주합니다.
예. 2 ether == 2000 finney 는 true 로 평가됩니다.
Booleans
bool: 가능한 값은 상수 true 그리고 false 입니다.
연산자:
- ! (논리 부정)
- && (논리 AND, "and")
- || (논리 OR, "or")
- == (같음)
- != (같지 않음)
정수
int / uint: 다양한 크기의 부호있는 정수 타입, 부호없는 정수 타입이 존재합니다. uint8 에서 uint256 까지, 그리고 int8 부터 int256 까지 8비트 단위로 키워드가 존재합니다. uint 와 int 는 각각 uint256 와 int256 의 별칭입니다.
연산자:
- 비교 연산자: <=, <, ==, !=, >=, > (bool 결과값을 가짐)
- 비트 연산자: &, |, ^ (배타적 비트 or), ~ (비트 보수)
- 산술 연산자: +, -, 단항 -, 단항 +, *, /, % (나머지), ** (거듭제곱), << (왼쪽 시프트), >> (오른쪽 시프트)
부동 소수점 숫자
fixed / ufixed: 다양한 크기의 부호있는 고정 소수점, 부호없는 고정 소수점 타입이 존재합니다. 키워드 ufixedMxN 와 fixedMxN 에서 M 은 타입에 의해 취해진 비트의 수를 나타내며 N 은 소수점이하 자리수를 나타냅니다. M 은 8에서 256비트 사이의 값이며 반드시 8로 나누어 떨어져야 합니다. N 은 0과 80 사이의 값이어야만 합니다. ufixed 와 fixed 는 각각 ufixed128x19 와 fixed128x19 의 별칭입니다.
연산자:
- 비교 연산자: <=, <, ==, !=, >=, > (bool 결과값을 가짐)
- 산술 연산자: +, -, 단항 -, 단항 +, *, /, % (나머지)
Address
address : 20바이트(이더리움 address의 크기)를 담을 수 있습니다. address 타입에는 멤버가 있으며 모든 컨트랙트의 기반이 됩니다.
연산자:
- <=, <, ==, !=, >= and >
- balance 와 transfer
balance 속성을 이용하여 address의 잔고를 조회하고 transfer 함수를 이용하여 다른 address에 Ether를 (wei 단위로) 보낼 수 있습니다:
transfer 코드가 실행될때 가스가 부족하거나 어떤식으로든 실패한다면, Ether 전송은 원상복구되며 현재의 컨트랙트는 예외를 발생하며 중지됩니다.
- send
Send는 low-level 수준에서 transfer 에 대응됩니다. 실행이 실패하면 컨트랙트는 중단되지 않고 대신 send 가 false 를 반환할 것입니다.
send 를 사용할 땐 몇가지 주의사항이 있습니다: call stack의 깊이가 1024라면 전송은 실패하며(이것은 항상 호출자에 의해 강제 될 수 있습니다) 그리고 수신자의 gas가 전부 소모되어도 실패합니다. 그러므로 안전한 Ether 전송을 위해서, 항상 send 의 반환값을 확인하고, transfer 를 사용하세요: 혹은 더 좋은 방법은 수신자가 돈을 인출하는 패턴을 사용하는 것입니다.
동적 크기 바이트 배열
bytes:동적 크기 바이트 배열, 배열 을 참조하세요. 값 타입이 아닙니다!string:동적 크기의 UTF-8 인코딩된 문자열, 배열 을 참조하세요. 값 타입이 아닙니다!
경험에 따르면 임의 길이의 원시 바이트 데이터의 경우에는 bytes 를 사용하고 임의 길이의 문자열(UTF-8) 데이터의 경우에는 string 을 사용하세요. 만약 길이를 특정 바이트만큼 제한할수 있다면, 항상 bytes1 에서 bytes32 중 하나를 사용하세요. 왜냐하면 공간을 더 절약할 수 있기 때문입니다.
string 은 bytes 와 동일하지만 (현재로서는) 길이(length)나 인덱스 접근을 허용하지 않습니다
동적 크기의 스토리지 배열과 bytes (string 은 제외)는 push 라는 멤버 함수를 가지고 있습니다. 이 함수는 배열의 끝에 요소를 추가하는데 사용됩니다. 이 함수는 새로운 length를 반환합니다.
pragma solidity ^0.5.13;
contract SendMoneyExample {
uint public balanceReceived;
// 마지막 입금이 1분 이상 지난 경우에만 출금을 허용하기 위한
uint public lockedUntil;
// 블록체인으로 트랜젝션(msg)에 입력한 코인 받기
function recieveMoney() public payable {
// msg-object는 진행중인 트랜잭션에 대한 몇 가지 정보를 포함하는 항상 존재하는 전역 개체
// 가장 중요한 두 속성은 .value와 .sender
balanceReceived += msg.value;
// 현재 시간에서 1분을 더한 값
lockedUntil = block.timestamp + 1 minutes;
}
// 블록체인으로 들어온 코인 확인
// view 는 읽기 전용
function getBalance() public view returns(uint) {
return address(this).balance;
}
// 트랜젝션(msg)에 입력된 지갑으로 코인 보내기(회수)
function withdrawMoney() public {
address payable to = msg.sender;
// 마지막 입금 후 1분이 초과된 경우에만 출금가능하다.
if(lockedUntil < block.timestamp) {
// check effects interactions 패턴으로 외부와 상호작용하기 전에 모든것을 업데이트
uint value = balanceReceived;
balanceReceived = 0;
to.transfer(value);
}
}
// 원하는 주소로 코인 보내기
function withdrawMoneyTo(address payable _to) public {
// 마지막 입금 후 1분이 초과된 경우에만 출금가능하다.
if(lockedUntil < block.timestamp) {
uint value = balanceReceived;
balanceReceived = 0;
_to.transfer(value);
}
}
}
시간 단위
숫자리터럴 뒤에 붙는 seconds, minutes, hours, days, weeks, years 와 같은 접미사는 시간 단위를 변환하는데 사용될 수 있으며 기본 단위는 seconds이고 다음과 같이 변환됩니다:
- 1 == 1 seconds
- 1 minutes == 60 seconds
- 1 hours == 60 minutes
- 1 days == 24 hours
- 1 weeks == 7 days
- 1 years == 365 days
이 단위들을 사용해 달력에서 계산을 할 땐 주의가 필요합니다. 왜냐하면 윤초 로 인해 모든 1 years가 항상 365 days와 동일한건 아니며 모든 1 days가 항상 24 hours와 동일한건 아니니까요. 윤초는 예측할 수 없기 때문에, 정확한 달력 라이브러리는 신뢰할수 있는 외부로부터 업데이트 되어야 합니다.
MSG (트랜젝션을 발행할때의 내용으로 생각하면 쉽다.)
- msg.data (bytes): 완전한 calldata
- msg.gas (uint): 잔여 가스 - 0.4.21버전에서 제거되었으며 gasleft() 로 대체됨
- msg.sender (address): 메세지 발신자 (현재 호출)
- msg.sig (bytes4): calldata의 첫 4바이트(즉, 함수 식별자)
- msg.value (uint): 메세지와 함께 전송된 wei 수
Address 관련
- <address>.balance (uint256):Address 의 잔액(Wei 단위)
- <address>.transfer(uint256 amount):주어진 양만큼의 Wei를 Address 로 전송합니다. 실패시 에러를 발생시키고 2300 gas를 전달하며 이 값은 변경할 수 없습니다.
- <address>.send(uint256 amount) returns (bool):주어진 양만큼의 Wei를 Address 로 전송합니다. 실패시 false 를 반환하고 2300 gas를 전달하며 이 값은 변경할 수 없습니다.
- <address>.call(...) returns (bool):로우 레벨 수준에서의 CALL 을 수행합니다. 실패시 false 를 반환하고 모든 gas를 전달하며 이 값은 변경가능합니다.
- <address>.callcode(...) returns (bool):로우 레벨 수준에서의 CALLCODE 를 수행합니다. 실패시 false 를 반환하고 모든 gas를 전달하며 이 값은 변경가능합니다.
- <address>.delegatecall(...) returns (bool):로우 레벨 수준에서의 DELEGATECALL 을 수행합니다. 실패시 false 를 반환하고 모든 gas를 전달하며 이 값은 변경가능합니다.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.1;
contract StartStopUpdateExample {
address public owner;
bool public paused;
uint private balance;
// 생성자는 특수 함수입니다. 스마트 계약 배포 중에 자동으로 호출됩니다. 그리고 그 후에는 다시는 호출할 수 없습니다.
constructor() {
// 처음 발행한 사람이 스마트 계약의 주인이 되도록
owner = msg.sender;
}
// sendMoney(): 이 기능은 Ether를 받을 수 있는 기능
function sendMoney() public payable {
balance += msg.value;
}
function setPaused(bool _paused) public {
// owner 가 원한다면 기능이 멈추도록
require(msg.sender == owner, "You are not owner");
paused = _paused;
}
// require 오류를 트리거(또는 예외를 던지는)하는 방법
function withdrawAllMoney(address payable _to) public {
// owner 만 출금 가능하도록
require(owner == msg.sender, "You are not owner.");
require(paused == false, "Contract Paused");
// selfdestruct 공격을 막기위해 address(this).balance 직접 사용보다는 balance 변수 사용
_to.transfer(balance);
}
}
OpenZeppelin은 'Owner'에 대한 라이브러리
-> selfdestruct 가 deprecated 되면서 블록체인이 가지고있는 balance 는 address(this).balance 로 직접 호출하지 말고 uint public balance 변수에 담아 사용할 것을 권장한다.
에러 처리
- assert(bool condition): 조건이 충족되지 않으면 예외를 발생시킵니다 - 내부 에러에 사용됩니다. / 남은 gas 를 돌려주지 않는다.
- require(bool condition): 조건이 충족되지 않으면 예외를 발생시킵니다 - 입력 또는 외부 요소의 에러에 사용됩니다. / 남은 gas 를 돌려준다.
- revert(): 실행을 중단하고 변경된 상태를 되돌립니다. / 남은 gas 를 돌려준다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract SimpleMappingExample {
mapping(uint => bool) public myMapping;
mapping(address => bool) public myAddressMapping;
mapping(uint => mapping(uint => bool)) uintUintBoolMapping;
function setUintUintBoolMapping(uint _index1, uint _index2, bool _value) public {
uintUintBoolMapping[_index1][_index2] = _value;
}
function getUintUintBoolMappint(uint _index1, uint _index2) public view returns (bool) {
return uintUintBoolMapping[_index1][_index2];
}
function setValue(uint _index) public {
myMapping[_index] = true;
}
function setMyAddressToTrue() public {
myAddressMapping[msg.sender] = true;
}
}
매핑
매핑 타입은 mapping(_KeyType => _ValueType) 와 같이 선언됩니다. 여기서 _KeyType 은 매핑, 동적 크기 배열, 컨트랙트, 열거형, 구조체를 제외한 거의 모든 유형이 될 수 있습니다. _ValueType 은 매핑 타입을 포함한 어떤 타입이든 될 수 있습니다.
매핑은 사실상 모든 가능한 키가 초기화되고 byte-representation이 모두 0인 값(타입의 기본 값)에 매핑되는 해시 테이블 로 볼 수 있습니다. 이는 매핑과 해시테이블의 유사한 점이며 차이점은, 키 데이터는 실제로 매핑에 저장되지 않고 오직 keccak256 해시만이 값을 찾기 위해 사용됩니다.
이로 인해, 매핑에는 길이 또는 집합(set)을 이루는 키나 값의 개념을 가지고 있지 않습니다.
매핑은 상태변수(또는 내부 함수에서의 스토리지 참조 타입)에만 허용됩니다.
매핑을 public 으로 표시하고 solidity가 getter 를 생성토록 할 수 있습니다. _KeyType 은 getter의 필수 매개 변수이며 _ValueType 을 반환 합니다.
매핑 역시 _ValueType 이 될 수 있습니다. getter는 각각의 _KeyType 에 대하여 하나의 매개변수를 재귀적으로 가집니다.
매핑은 iterable하진 않지만, 그 위에 자료구조(data structure)를 구현하는건 가능합니다.
예시는 iterable mapping 을 참조하세요.
Structurer 의 활용
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract MappingsStructExample {
struct Payment {
uint amount;
uint timestamps;
}
struct Balance {
uint totalBalance;
uint numPayments;
mapping(uint => Payment) payments;
}
mapping(address => Balance) balanceReceived;
function getBalance() public view returns(uint) {
return balanceReceived[msg.sender].totalBalance;
}
function sendMoney() public payable {
balanceReceived[msg.sender].totalBalance += msg.value;
// 참조 타입은 저장할 공간 필요 -> memory
Payment memory payment = Payment(msg.value, block.timestamp);
balanceReceived[msg.sender].payments[balanceReceived[msg.sender].numPayments] = payment;
balanceReceived[msg.sender].numPayments++;
}
function withdrawMoney(address payable _to, uint _amount) public {
require(balanceReceived[msg.sender].totalBalance >= _amount, " not enough funds");
balanceReceived[msg.sender].totalBalance -= _amount;
_to.transfer(_amount);
}
function withdrawAllMoney(address payable _to) public {
uint value = balanceReceived[msg.sender].totalBalance;
balanceReceived[msg.sender].totalBalance = 0;
_to.transfer(value);
}
}
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.3;
contract FuctionsExample {
mapping(address => uint) public balanceReceived;
address payable public owner;
constructor() {
owner = payable(msg.sender);
}
function getOwner() public view returns(address) {
return owner;
}
function convertWeiToEth(uint _amount) public pure returns(uint) {
return _amount / 1 ether;
}
function destroySmartContract() public {
require(msg.sender == owner, "You are not the owner");
selfdestruct(owner);
}
function receiveMoney() public payable {
assert(balanceReceived[msg.sender] + msg.value >= balanceReceived[msg.sender]);
balanceReceived[msg.sender] += msg.value;
}
function withdrawMoney (address payable _to, uint _amount) public {
require(_amount <= balanceReceived[msg.sender], "not enough funds.");
assert(balanceReceived[msg.sender] >= (balanceReceived[msg.sender] - _amount));
balanceReceived[msg.sender] -= _amount;
_to.transfer(_amount);
}
receive() external payable {
receiveMoney();
}
}