솔리디티(Solidity) 깨부수기
- 목차
Hello Solidity
솔리디티(Solidity)란
스마트 컨트랙을 개발하기 위한 언어
스마트 컨트랙이란
미리 정의된 조건이 충족이 되면 미리 저장된 블록체인 프로그램이 작동하는 것
예) 3의 배수 번째 사람에게 돈을 준다. 스마트 컨트랙은 그 사람이 3번째 사람인지 확인, 아니면 돈을 주지 않음
스마트 컨트랙 작성 방법
리믹스 IDE 사용
라이센스를 제일 윗 줄에 작성해야 함
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract Hello {
string public hi = "Hello solidity";
}
compile → deploy 하면 Deployed Contracts 에서 확인 가능
솔리디티 타입
1. 데이터 타입
- boolean: true/false
- ! || == && 사용 가능
bool public b = false;
bool public b1 = !false; // true
bool public b3 = false == ture; // false
bool public b4 = false && true; // false
- byte: 1~32 바이트 저장 가능
bytes4 public bt = 0x12345678; // 4byte
bytes public bt2 = "STRING"; // 문자가 byte화 되어서 저장 됨 // 솔리디티에서는 string 쓰는 것 지양
- address: 배포가 되면 20byte의 address가 할당
- 주소를 통해서 디지털 코인을 보내기도 하고, 스마트 컨트랙을 불러오기도 한다. 은행 계좌 번호 정도로 생각
address public addr = 0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B;
- int vs uintint 범위 -2^7~2^7-1
- uint 범위 0~2^8-1
- +, -, *, / 가능
int8 public it = 4;
uint256 public uit = 132213;
uint8 public uit2 = 256; // 범위 넘기면 에러
2. 레퍼런스 타입
3. 매핑 타입
이더 단위와 가스(Ether/Gwei/Wei)
1 Ether = 10^9 Gwei = 10^18 Wei
Gwei
가스 단위
가스 (Gas)
스마트 컨트랙을 이용할 때 드는 비용, 스마트 컨트랙 운영시키는 연료
contract lec3 {
uint256 public value = 1 ether; // uint256: 1000000000000000000
uint256 public value2 = 1 wei; // uint256: 1
uint256 public value3 = 1 gwei; // uint256: 1000000000
}
코드 길이에 따라 배포(Deploy)할 때마다 가스가 소비 됨.
어떻게 하면 가스를 줄여서 스마트 컨트랙을 개발할 수 있을지 고민 해야하는 문제 (string, modifer 가스 많이 소비됨)
함수 (Function)
(public, private, internal, external) 접근 제한자 변경 가능
function 이름() public {
// 내용
}
- 파라미터와 리턴 값이 없는 경우
uint256 public a = 3;
function changeA1() public {
a = 5;
}
- 파라미터는 있고, 리턴 값이 없는 경우
function changeA2(uint256 _value) public {
a = _value;
}
2개 이상의 파라미터
function changeA(uint256 _value1, uint256 _value2) public{
a =_value1 + _value2;
}
- 파라미터도 있고, 리턴 값도 있는 경우
function changeA3(uint256 _value) public returns (uint256) {
a = _value;
return a;
}
접근제한자 (public/private/internal/external)
- public: 모든 곳에서 접근 가능
- external: 오직 밖에서만 접근 가능. 모든 곳에서 접근 가능하나, external이 정의된 자기자신 컨트랙 내에서 접근 불가
- private: 오직 private이 정의된 자기 컨트랙에서만 가능(상속받은 자식도 불가능)
- internal: private 처럼 오직 internal이 정의된 자기 컨트랙에서만 가능하고, internal이 정의된 컨트랙을 상속 받을 수 있음
public/private 예제
contract Hello {
// 1. public
uint256 public a = 5;
// 2. private
uint256 private a2 = 5; // 배포하면 보이지 않음, lec5.sol을 배포했기 때문?
}
external 예제
contract Public_example {
uint256 external a = 3; // 접근 불가로 에러
function changeA(uint256 _value) external {
a = _value;
}
function get_a() view external returns (uint256) {
return a;
}
}
contract Public_example_2 {
Public_example instance = new Public_example(); // 인스턴스화
function changeA_2(uint256 _value) public {
instance.changeA(_value);
}
function use_public_example_a() view public returns (uint256) {
return instance.get_a();
}
}
view와 pure
uint256 public a = 1;
function read_a() public view returns(uint256) {
return a+2;
}
view
function 밖의 변수들을 읽을 수 있으나 변경 불가능
pure
function 밖의 변수들을 읽지 못하고, 변경도 불가능
순수하게 function 내에서만 쓸 수 있음
function read_a2() public pure returns(uint256) {
uint256 b = 1;
return 4+2+b;
}
view와 pure 둘 다 명시 안할 때
function 밖에 변수들을 읽어서 변경할 때 사용
function read_a3() public returns (uint256) {
a = 13;
return a;
}
String
솔리디티 메모리 영역
storage
대부분의 변수, 함수들이 저장되며, 영속적으로 저장이 되어 가스 비용이 비싸다. (함수와 함수 밖의 변수들)
memory
함수의 파라미터, 리턴값, 레퍼런스 타입이 주로 저장된다. (함수 안의 변수들) 그러나, storage 처럼 영속적이지 않고, 함수 내에서만 유효하기에 storage보다 가스 비용이 싸다.
Calldata
주로 external function의 파라미터에서 사용 됨
stack
EVM(Ethereum Virtual Machine)에서 stack data를 관리할 때 쓰는 영역인데 1024Mb 제한적
String 저장 영역 - memory
string은 기본 타입이 아니라 레퍼런스에 들어감. memory라고 지정해 줘야 함.
function get_string(string memory _str) public pure returns(string memory) {
return _str;
}
기본 타입은 자동이기 때문에 memory라고 지정해줄 필요가 없음
function get_uint(uint256 _str) public pure returns(uint256) {
return _str;
}
인스턴스 (Instance)
여러 개의 컨트랙을 이어줄 때 사용
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract A {
uint256 public a = 5;
function change(uint256 _value) public {
a = _value;
}
}
contract B {
A instance = new A();
function get_A() public view returns(uint256) {
return instance.a();
}
function change_A(uint256 _value) public {
instance.change(_value);
}
}
a 본연의 값을 가져온는 것이 아니라, a의 분신을 새로 생성한 것
A와 별개의 것 이며 구조는 같다.
생성자 (Constructor)
기존의 값을 초기화할 때 쓰임
처음 생성, 배포, 인스턴스화 될 때 생성 됨
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract A {
string public name;
uint256 public age;
constructor(string memory _name, uint256 _age) {
name = _name;
age = _age;
}
function change(string memory _name, uint256 _age) public {
name = _name;
age = _age;
}
}
contract B {
A instance = new A("Alice", 52);
function change(string memory _name, uint256 _age) public {
instance.change(_name, _age);
}
function get() public view returns(string memory, uint256) {
return (instance.name(), instance.age());
}
}
B에서 생성한 A는 컨트랙트 A와 독립적이다.
B안에서 A자체를 다 가져오는 것이기 때문에 인스턴스화하면 가스가 많이 소비 된다.
이 가스에 대한 비용적인 문제도 있지만, 그 보다 블록마다 가스를 소비할 수 있는 양은 제한적이라는 것을 알아야 한다. 제한하는 양을 초과한다면 이더리움 자체에서 에러가 발생하고, 스마트 컨트랙 배포도 불가하다.
이를 개선하는 방법으로 클론 팩토리 패턴을 사용하는 방법이 있다. 가스 소비량을 획기적으로 줄일 수 있다.
상속
자신의 재산을 대가 없이 주는 것
is 키워드 사용
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract Father {
string public familyName = "Kim";
string public givenName = "Jung";
uint256 public money = 100;
function getFamilyName() view public returns(string memory) {
return familyName;
}
function getGivenName() view public returns(string memory) {
return givenName;
}
function getMoney() view public returns(uint256) {
return money;
}
}
contract Son is Father {
}
상속받을 컨트랙트에 constructor가 있을 때
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract Father {
string public familyName = "Kim";
string public givenName = "Jung";
uint256 public money = 100;
constructor(string memory _givenName) public {
givenName = _givenName;
}
function getFamilyName() view public returns(string memory) {
return familyName;
}
function getGivenName() view public returns(string memory) {
return givenName;
}
function getMoney() view public returns(uint256) {
return money;
}
}
contract Son is Father("James"){
}
상속받는 또 다른 방법
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract Father {
...
}
contract Son is Father{
constructor() Father("James") {
}
}
두 개 이상 상속하기
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract Father {
uint256 public fatherMoney = 100;
function getFatherName() public pure returns(string memory) {
return "KimJung";
}
function getMoney() public view virtual returns(uint256) {
return fatherMoney;
}
}
contract Mother {
uint256 public motherMoney = 500;
function getMotherName() public pure returns(string memory) {
return "Leesol";
}
function getMoney() public view virtual returns(uint256) {
return motherMoney;
}
}
contract Son is Father, Mother {
// overriding
function getMoney() public view override(Father, Mother) returns(uint256) {
return fatherMoney+motherMoney;
}
}
오버라이딩 (Overriding)
덮어 씌우기
virtual - override 키워드는 항상 써줘야 한다.
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract Father {
string public familyName = "Kim";
string public givenName = "Jung";
uint256 public money = 100;
constructor(string memory _givenName) public {
givenName = _givenName;
}
function getFamilyName() view public returns(string memory) {
return familyName;
}
function getGivenName() view public returns(string memory) {
return givenName;
}
function getMoney() view virtual public returns(uint256) {
return money;
}
}
contract Son is Father("James"){
uint256 public earning = 0;
function work() public {
earning += 100;
}
function getMoney() view override public returns(uint256) {
return money+earning;
}
}
이벤트 (event)
블록체인 네트워크의 블록에 특정값을 기록하는 것
송금하기 라는 함수가 있다고 가정하면, 송금 버튼을 누르면 계좌와 금액이 이벤트로 출력되어서 블록체인 네트워크 안에 기록 됨 (로그로 출력 됨)
contract lec13 {
event info(string name, uint256 money); // 출력
function sendMoney() public {
emit info("KimDadJin", 1000);
}
}
indexd
이벤트의 내에서만 사용 가능한 키워드
특정한 이벤트의 값을 들고 올 때 사용 가능
async function getEvent (){
let events = await lecture14.getPastEvents('numberTracker2',{ filter:{num:[2,1]},fromBlock: 1, toBlock:'latest'});
console.log(events) // num 이 1일 때, 혹은 2일 때만 filter
let events2 = await lecture14.getPastEvents('numberTracker',{ filter:{num:[2,1]},fromBlock: 1, toBlock:'latest'});
console.log(events2) // numberTracker는 indexed를 안줬기 때문에 filter 안됨
}
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract lec14 {
event numberTracker(uint256 num, string str);
event numberTracker2(uint256 indexed num, string str);
uint256 num = 0;
function PushEvent(string memory _str) public {
emit numberTracker(num, _str);
emit numberTracker2(num, _str);
num++;
}
}
super
오버라이딩 할 때 원래 함수를 갖고 옴
super.who()
contract Father {
event FatherName(string name);
function who() public virtual {
emit FatherName("KimDaeho");
}
}
contract Son is Father {
event sonName(string name);
function who() public override{
super.who();
emit sonName("KimJin");
}
}
상속의 순서
contract Father {
event FatherName(string name);
function who() public virtual {
emit FatherName("KimDaeho");
}
}
contract Mother {
event MotherName(string name);
function who() public virtual {
emit MotherName("leeSol");
}
}
contract Son is Father, Mother {
event sonName(string name);
function who() public override(Father,Mother){
super.who();
}
} {
event sonName(string name);
function who() public override(Father,Mother){
super.who();
}
}
contract Son is Father, Mother
가장 최신의 Mother의 who를 상속받는다.
매핑 (Mapping)
key-value로 이루어짐
특정한 키를 넣어주면 그에 해당되는 value값을 반환
mapping(uint256=>uint256)
키 타입: uint256, 밸류타입: uint256
length 기능이 없음
contract lec17 {
mapping(uint256=>uint256) private ageList;
mapping(string=>uint256) private priceList;
mapping(uint256=>string) private nameList;
function setAgeList(uint256 _index, uint256 _age) public {
ageList[_index] = _age;
}
function getAge(uint256 _index) public view returns(uint256) {
return ageList[_index];
}
function setNameList(uint256 _index, string memory _name) public {
nameList[_index] = _name;
}
function getNameList(uint256 _index) public view returns(string memory) {
return nameList[_index];
}
function setPriceList(string memory itemName, uint256 _price) public {
priceList[itemName] = _price;
}
function getPriceList(string memory _index) public view returns(uint256) {
return priceList[_index];
}
}
배열 (Array)
length 가능
순회 가능
그러나 솔리디티에서는 배열보다 매핑 선호, 왜냐하면 순환하는 것은 디도스 공격에 취약할 수 있어서
배열의 값을 50으로 지정해서 사용하는 것이 좋음
contract lec18 {
uint256[] public ageArray;
uint256[10] public ageFixedSizeArray; // 사이즈 제한
string[] public nameArray = ["Kal", "Jhon", "Kerri"];
function AgeLength() public view returns(uint256) {
return ageArray.length;
}
function AgePush(uint256 _age) public {
ageArray.push(_age);
}
function AgeGet(uint256 _index) public view returns(uint256) {
return ageArray[_index];
}
function AgePop() public {
ageArray.pop();
}
function AgeDelete(uint256 _index) public {
delete ageArray[_index]; // 0으로 채워지고 length는 그대로
}
function AgeChange(uint256 _index, uint256 _age) public {
ageArray[_index] = _age;
}
}
array.pop() → 제일 최신의 값 삭제
delete ageArray[_index]; → 0으로 채워지고 length는 그대로
Mapping과 Array 주의할 점
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract lec19 {
uint256 num = 89;
mapping(uint256 => uint256) numMap;
uint256[] numArray;
function changeNum(uint256 _num) public{
num = _num;
}
function showNum() public view returns(uint256){
return num;
}
function numMapAdd() public{
numMap[0] = num;
}
function showNumMap() public view returns(uint256){
return numMap[0];
}
function UpdateMap() public{
numMap[0] = num;
}
function numArrayAdd() public{
numArray.push(num);
}
function showNumArray() public view returns(uint256){
return numArray[0];
}
function updateArray() public {
numArray[0] = num;
}
}
구조체 (struct)
나만의 타입을 만드는 것
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract lec20 {
struct Character {
uint256 age;
string name;
string job;
}
function createCharacter(uint256 _age, string memory _name, string memory _job) pure public returns(Character memory) {
return Character(_age, _name, _job);
}
}
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract lec20 {
struct Character {
uint256 age;
string name;
string job;
}
mapping(uint256=>Character) public CharacterMapping;
Character[] public CharacterArray;
function createCharacter(uint256 _age, string memory _name, string memory _job) pure public returns(Character memory) {
return Character(_age, _name, _job);
}
function createCharacterMapping(uint256 _key, uint256 _age, string memory _name, string memory _job) public {
CharacterMapping[_key] = Character(_age, _name, _job);
}
function getCharacterMapping(uint256 _key) public view returns(Character memory) {
return CharacterMapping[_key];
}
function createCharacterArray(uint256 _age, string memory _name, string memory _job) public {
CharacterArray.push(Character(_age, _name, _job));
}
function getCharacterArray(uint256 _index) public view returns(Character memory) {
return CharacterArray[_index];
}
}
if 조건문
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract lec21 {
string private outcome = "";
function isIt5(uint256 _number) public returns(string memory) {
if(_number == 5) {
outcome = "Yes, it is 5";
return outcome;
}
else {
outcome = "No, it is not 5";
return outcome;
}
}
function isIt5or3or1(uint256 _number) public returns(string memory){
if(_number == 5){
outcome = "Yes, it is 5";
return outcome;
}
else if(_number == 3){
outcome = "Yes, it is 3";
return outcome;
}
else if(_number == 1){
outcome = "Yes, it is 1";
return outcome;
}
else{
outcome = "No, it is not 5, 3 or 1";
return outcome;
}
}
}
loop 반복문
for
contract lec22 {
event CountryIndexName(uint256 indexed _index, string _name);
string[] private countryList = ["South Korea", "North Korea", "USA", "China", "Japan"];
function forLoopEvents() public {
for(uint256 i = 0; i < countryList.length; i++) {
emit CountryIndexName(i, countryList[i]);
}
}
}
while
function whileLoopEvents() public {
uint256 i = 0;
while(i < countryList.length) {
emit CountryIndexName(i, countryList[i]);
i++;
}
}
do-while
function doWhileLoopEvents() public {
uint256 i = 0;
do {
emit CountryIndexName(i, countryList[i]);
i++;
}
while(i < countryList.length);
}
break, continue
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract lec23 {
event CountryIndexName(uint256 indexed _index, string _name);
string[] private countryList = ["South Korea", "North Korea", "USA", "China", "Japan"];
function useContinue() public {
for(uint256 i = 0; i < countryList.length; i++) {
if(i%2==0) {
continue;
}
emit CountryIndexName(i, countryList[i]);
}
}
function useBreack() public {
for(uint256 i = 0; i < countryList.length; i++) {
if(i==2) {
break;
}
emit CountryIndexName(i, countryList[i]);
}
}
}
linear search
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract lec24 {
event CountryIndexName(uint256 indexed _index, string _name);
string[] private countryList = ["South Korea", "North Korea", "USA", "China", "Japan"];
function linearSearch(string memory _search) public view returns(uint256, string memory) {
for(uint256 i=0; i<countryList.length; i++) {
if(keccak256(bytes(countryList[i])) == keccak256(bytes(_search))) {
return (i, countryList[i]);
}
}
return (0, "Nothing");
}
}
솔리디티 내에서 문자를 비교하려면 바이트화 → keccak256 내장함수를 이용해서 해시화를 시켜줘야 한다. → 값이 같으면 똑같은 해시가 나온다.
에러 핸들러 assert, revert, require 0.4.22 ~ 0.7.x 버전
*스마트컨트랙에서 여러 라이브러리의 버전이 다 다르기 때문에 다 알아야 함
revert, require: 스마트컨트랙 작성 시 사용
assert: 가스를 다 소비하기 때문에 실용적이지 x, test용으로 사용, 보통 스마트컨트랙을 다 만들고 나서 트러플 내에서 테스트 할 때 사용
assert
gas를 다 소비한 후, 특정한 조건에 부합하지 않으면 (false일 때) 에러를 발생시킨다.
가스를 다 소비하고 조건문을 확인해서 에러를 발생시킴
// 3000000 gas
function assertNow() public pure {
assert(false); // assert(조건문)
}
revert
조건없이 에러를 발생시키고, gas를 환불 시켜준다.
예상 가스비용을 지불하고 revert사용시 소비하는 가스 비용만 내고 환불해 줌
// 21322 gas
function revertNow() public pure {
revert("error!!"); // 예상 가스비용을 지불하고 revert사용시 소비하는 가스 비용만 내고 환불해줌
}
조건문이 없기 때문에 if와 같이 씀
function onlyAdults(uint256 _age) public pure returns(string memory) {
if(_age < 19) {
revert("You are not allowed to pay for the cigarette");
}
return "Your payment is succeeded";
}
require
특정한 조건에 부합하지 않으면(false일 때) 에러를 발생시키고, gas를 환불 시켜준다.
function requireNow() public pure {
require(false, "occurred");
}
function onlyAdults2(uint256 _age) public pure returns(string memory) {
require(_age > 19, "You are not allowed to pay for the cigarette");
return "Your payment is secceeded";
}
함수 실행 시 프로세스
1. 특정 함수를 선택해서 버튼 누른다.
2. 솔리디티는 "이 함수를 돌리고 싶으면, 이만큼의 가스를 지불해라" 라고 요구한다.
3. 가스 비용을 지불한다.
4. 함수 실행중, 어느 부분에서 revert 발생하여 에러가 난다.
5. 에러가 나서 함수의 모든 부분이 실행이 안되었으니 실행 안된만큼 gas를 환불한다.
에러 핸들러 0.8 버전
assert: 오직 내부적 에러 테스트 용도, 불변성 체크 용도
assert가 에러를 발생하면, Panic(uint256) 이라는 에러타입의 에러 발생
이전 버전과 다르게 가스를 환불 받을 수 있음
8버전에서 assertNow()예제를 실행해 보면 gas를 환불 받았음을 알 수 있음
에러 핸들러 try/catch
*0.6 버전 이후
try/catch 왜 써야 하는가?
기존의 에러 핸들러 assert/revert/require는 에러를 발생시키고 프로그램을 끝냄
그러나, try/catch를 사용해서 에러가 났어도, 프로그램을 종료시키지 않고 어떠한 대처를 하게 만들 수 있다.
try/catch 특징
- try/catch문 안에서 assert/revert/require을 통해 에러가 난다면 catch는 에러를 잡지 못하고 개발자가 의도한줄 알고 정상적으로 프로그램이 끝난다.
- try/catch문 밖에서 assert/revert/require을 통해 에러가 난다면 catch는 에러를 잡고, 에러를 핸들할 수 있다.
- 3가지 catch
- catch Error(string memory reason) { … } : revert 나 require을 통해 생성된 에러 용도
- catch Panic(uint errorCode) { … } : assert를 통해 생성된 에러가 날 때 이 catch에 잡힘
- catch(bytesmemoryLowLevelData){…} : 이 catch는 로우 레벨에러를 잡습니다.
errorCode
errorCode는 솔리디티에서 정의 Panic 에러 별로 나온다.
- 0x00: Used for generic compiler inserted panics.
- 0x01: If you call assert with an argument that evaluates to false.
- 0x11: If an arithmetic operation results in underflow or overflow outside of an unchecked { ... } block.
- 0x12; If you divide or modulo by zero (e.g. 5 / 0 or 23 % 0).
- 0x21: If you convert a value that is too big or negative into an enum type.
- 0x22: If you access a storage byte array that is incorrectly encoded.
- 0x31: If you call .pop() on an empty array.
- 0x32: If you access an array, bytesN or an array slice at an out-of-bounds or negative index (i.e. x[i] where i >= x.length or i < 0).
- 0x41: If you allocate too much memory or create an array that is too large.
- 0x51: If you call a zero-initialized variable of internal function type.
간단히 예를들어 4번에 0으로 나눌시 division error를 언급하며, 7번에 배열의 길이가 0인데 pop을 하여 배열안의 값을 뽑을때도 panic이 발생한다고합니다.
Try/Catch 사용
1. 외부 스마트 컨트랙 함수를 부를 때
다른 스마트 컨트랙을 인스턴스화 해서, try/catch문이 있는 스마트 컨트랙의 함수를 불러와서 사용
contract math {
function division(uint256 _num1, uint256 _num2) public pure returns (uint256) {
require(_num1<10, "num1 should not be more than 10");
return _num1/_num2;
}
}
contract runner {
event catchErr(string _name, string _err);
event catchPanic(string _name, uint256 _err);
event catchLowLevelErr(string _name, bytes _err);
math public mathInstance = new math();
function playTryCatch(uint256 _num1, uint256 _num2) public returns(uint256, bool) {
try mathInstance.division(_num1, _num2) returns(uint256 value) {
return(value, true);
} catch Error(string memory _err) {
emit catchErr("revert/require", _err);
return (0, false);
} catch Panic(uint256 _errorCode) {
emit catchPanic("assertError/Panic", _errorCode);
return (0, false);
} catch (bytes memory _errorCode) {
emit catchLowLevelErr("LowlevelError", _errorCode);
return (0, false);
}
}
}
2. 외부 스마트 컨트랙을 생성할 때
다른 스마트 컨트랙을 인스턴스화 생성할 때 씀
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
// 외부 스마트 컨트랙을 생성 할 때
contract character {
string private name;
uint256 private power;
constructor(string memory _name, uint256 _power) {
revert("error");
name = _name;
power = _power;
}
}
contract runner {
event catchOnly(string _name, string _err);
function playTryCatch(string memory _name ,uint256 _power) public returns(bool) {
try new character(_name, _power) {
revert("errors in the tyr/catch block");
return(true);
}
catch {
emit catchOnly("catch", "Errors!!");
return(false);
}
}
}
3. 스마트 컨트랙 내에서 함수를 부를 때
this를 통해 try/catch를 씀
return 값 변수 명시
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract lec29 {
function add(uint256 _num1, uint256 _num2) public pure returns(uint256) {
uint256 total = _num1 + _num2;
return total;
}
function add2(uint256 _num1, uint256 _num2) public pure returns(uint256 total) {
total = _num1 + _num2;
return total;
}
}
방법 1의 경우 return값이 여러 개일 경우 헷갈릴 수 있음
modifier 모디파이어
function onlyAdults2(uint256 _age) public pure returns(string memory) {
require(_age>19, "You are not allowed to pay for the cigarette");
return "Your payment is succeeded";
}
function onlyAdults32(uint256 _age) public pure returns(string memory) {
require(_age>19, "You are not allowed to pay for the cigarette");
return "Your payment is succeeded";
}
_age>19 부분을 수정하는 경우
modifier onlyAdults{ // == onlyAdults(), 보통 빈 배열은 써주지 않음
revert("You are not allowed to pay for the cigarette");
_; // BuyCigarette() 함수가 어느 자리에 적용이 되는지
}
function BuyCigarette() public onlyAdults returns(string memory) {
return "Your payment is succeded";
}
_; : 함수가 실행되는 자리
modifier에서 파라미터 값 받을 때
modifier onlyAdults2(uint256 _age) {
require(_age>18, "You are not allowed to pay for the cigarette");
_;
}
function BuyCigarette2(uint256 _age) public onlyAdults2(_age) returns(string memory) {
return "Your payment is succeded";
}
modifier numChange{
_;
num = 10;
}
function numChangeFunction() public numChange{
num = 15;
}
SPDX 라이센스/주석
목적
- 라이센서를 명시해줌으로써 스마트컨트랙에 대한 신뢰감을 눞일 수 있음
- 스마트 컨트랙 소스코드가 워낙 오픈되어 있으니, 저작권과 같은 관련된 문제를 해소
주석
- 블록단위: 보통 블록단위의 주석은 스마트컨트랙, 함수 등 많은 양의 설명
- 행단위: 행단위는 변수 바로 앞에 쓰여서, 짤막짝막한 설명
payable, msg, value 와 이더를 보내는 3가지 함수
payable
이더/토큰과 상호작용시 필요한 키워드
send, transfer, call을 이용하여, 이더를 보낼 때 Payable이라는 키워드가 필요 함
주로 함수, 주소, 생성자에 붙여서 사용된다.
msg.value
송금보낸 코인의 값
이더를 보내는 3가지 함수
- send: 2300 gas를 소비, 성공여부를 true 또는 false로 리턴
- transfer: 2300 gas 소비, 실패시 에러를 발생
- call: 가변적인 gas 소비(gas값 지정 가능), 성공여부를 true 또는 false로 리턴
- 재진입(reentrancy) 공격 위험성 있음, 2019년 12월 이후 call 사용을 추천
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.6.0 < 0.9.0;
contract lec31 {
event howMuch(uint256 _value);
function sendNow(address payable _to) public payable {
bool sent = _to.send(msg.value); // return true or false
require(sent, "Fail to send ether");
emit howMuch(msg.value);
}
function transferNow(address payable _to) public payable {
_to.transfer(msg.value);
emit howMuch(msg.value);
}
function callNow(address payable _to) public payable {
// ~ 0.7
// (bool sent, ) = _to.call.gas(1000).value(msg.value)("");
// require(sent, "Fail to send ether");
(bool sent, ) = _to.call{value: msg.value, gas: 1000}("");
require(sent, "Fail to send Ether");
emit howMuch(msg.value);
}
}
balance 와 msg.sender
주소.balance
특정 주소의 현재 갖고있는 이더의 잔액 (msg.value는 송금액)
msg.sender
스마트컨트랙을 사용하는 주체
앞으로 설명할 call vs delegate call에서 주요 내용이니 관심있게 보기
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract MobileBanking {
event SendInfo(address _msgSender, uint256 _currentValue);
event MyCurrentValue(address _msgSender, uint256 _value);
event CurrentValueOfSomeone(address _msgSender, address _to, uint256 _value);
function sendEther(address payable _to) public payable{
require(msg.sender.balance >= msg.value, "Your balance is not enough");
_to.transfer(msg.value);
emit SendInfo(msg.sender, (msg.sender).balance);
}
function checkValueNow() public {
emit MyCurrentValue(msg.sender, msg.sender.balance);
}
function checkUserMoney(address _to) public {
emit CurrentValueOfSomeone(msg.sender, _to, _to.balance);
}
}
payable2 생성자 적용시, msg.sender2 owner 적용
payable을 생성자에 넣을 때
constructor() payable {
// 스마트컨트랙에 이더 넣기 가능
}
특정 주소에게만 권한 주기
contract MobileBanking {
address owner;
constructor() payable {
owner = msg.sender;
}
event SendInfo(address _msgSender, uint256 _currentValue);
event MyCurrentValue(address _msgSender, uint256 _value);
event CurrentValueOfSomeone(address _msgSender, address _to, uint256 _value);
function sendEther(address payable _to) public payable{
require(msg.sender == owner, "Only Owner!"); // msg.sender가 owner여야 함
require(msg.sender.balance >= msg.value, "Your balance is not enough");
_to.transfer(msg.value);
emit SendInfo(msg.sender, (msg.sender).balance);
}
}
modifier로 변경
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract MobileBanking {
address owner;
constructor() payable {
owner = msg.sender;
}
modifier onlyOwner{
require(msg.sender == owner, "Only Owner!");
_;
}
event SendInfo(address _msgSender, uint256 _currentValue);
event MyCurrentValue(address _msgSender, uint256 _value);
event CurrentValueOfSomeone(address _msgSender, address _to, uint256 _value);
function sendEther(address payable _to) public payable{
require(msg.sender.balance >= msg.value, "Your balance is not enough");
_to.transfer(msg.value);
emit SendInfo(msg.sender, (msg.sender).balance);
}
function checkValueNow() public {
emit MyCurrentValue(msg.sender, msg.sender.balance);
}
function checkUserMoney(address _to) public {
emit CurrentValueOfSomeone(msg.sender, _to, _to.balance);
}
}
fallback / receive 함수
fallback 함수
0.6 이전
특징
- 무기명 함수, 이름 없는 함수
- external 필수, 이더가 외부에서 들어오기 때문에
- payable
왜 쓰는가?
- 스마트 컨트랙이 이더를 받을 수 있게 한다.
- 이더 받고 난 후 어떠한 행동을 취할 수 있다.
- call 함수로 없는 함수가 불려질 때 어떠한 행동을 취하게 할 수 있다. (외부 스마트컨트랙의 함수를 부를 수 있음)
function() external payable{
}
전체 예제 코드
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 < 0.9.0;
contract Bank {
event JustFallbackWithFunds(address _from, uint256 _value, string message);
// fallback 함수
function() external payable{
emit JustFallbackWithFunds(msg.sender, msg.value, "JustFallbackWithFunds is called");
}
}
// Bank에게 이더를 보내려고 함
contract You {
// receive
function DepositWithSend(address payable _to) public payable {
bool success = _to.send(msg.value);
require(success, "Failed");
}
function DepositWithTransfer(address payable _to) public payable{
_to.transfer(msg.value);
}
function DepositWithCall(address payable _to) public payable{
(bool sent, ) = _to.call.value(msg.value)("");
require(sent, "Failed to send ether");
}
//fallback()
function JustGiveMessage(address _to) public {
(bool sent, ) = _to.call("HI"); // call: 이더뿐만 아니라 함수를 보내는 기능도 있음, Bank에는 HI가 없기 때문에 fallback에 걸림
require(sent, "Failed to send ether");
}
}
0.6 이후
receive와 fallback 두 가지로 나뉨
- receive: 순수하게 이더만 받을 때 작동
- fallback: 함수를 실행하면서 이더를 보낼 때, 불려진 함수가 없을 때 작동
fallback 함수 기본형
fallback() external {
}
payable 적용 시: 이더를 받고 나서도 fallback 함수가 발동합니다.
fallback() external payable {
}
그러나 이 기능을 receive에서 대신하기 때문에 payable을 잘 안쓴다.
receive() external payable{
}
전체 예제 코드
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 < 0.9.0;
contract Bank {
event JustFallback(address _from, string message);
event ReceiveFallback(address _from, uint256 _value);
// event ReceiveFallback(address _from, uint256 _value, string message);
event JustFallbackWithFunds(address _from, uint256 _value, string message);
// fallback() external {
// emit JustFallback(msg.sender, "JustFallback is called"); // msg.sender: You 스마트컨트랙
// }
// receive 함수를 통해서 이더 받음
receive() external payable{
emit ReceiveFallback(msg.sender, msg.value);
}
// receive() external payable{
// emit ReceiveFallback(msg.sender, msg.value, "ReceviedFallback is called"); // string을 추가하면 2300 gas가 부족함, 0.7버전은 잘 작동함, 즉 0.8버전 부터 가스의 가격이 올랐다. 그래서 가스가 가변적인 call의 사용을 권장하는 것임
// }
fallback() external payable{
emit JustFallbackWithFunds(msg.sender, msg.value, "JustFallbackWithFunds is called");
}
}
// Bank에게 이더를 보내려고 함
contract You {
// receive()
function DepositWithSend(address payable _to) public payable{
bool success = _to.send(msg.value);
require(success, "Failed");
}
function DepositWithTransfer(address payable _to) public payable{
_to.transfer(msg.value);
}
function DepositWithCall(address payable _to) public payable{
(bool sent, ) = _to.call{value: msg.value}("");
require(sent, "Failed");
}
// fallback()
function JustGiveMessage(address _to) public {
(bool success, ) = _to.call("HI");
require(success, "Failed");
}
function JustGiveMessageWithFunds(address payable _to) public payable{
(bool success, ) = _to.call{value: msg.value}("HI");
require(success, "Failed");
}
}
call 함수
call: 로우레벨 함수
- 송금하기
- 외부 스마트컨트랙 함수 부르기
- 가변적인 gas 소비
- 이스탄불 하드포크, 2019년 12월 이후, gas 가격 상승에 따른 call 사용 권장/ send transfer = 2300 gas
- re-entrancy(재진입) 공격 위험이 있기에, Checks_Effects_Interactions_pattern 사용
abi
이더리움 환경 안에서 스마트컨트랙을 상호작용 시키는 표준 방법
데이터가 코드화 되어있고 설명이 되어 있음
abi object 안에는 encodeWithSignature() 메소드가 있음
이 메소드가 작동하여 외부 스마트컨트랙의 함수를 부름
전체 예제 코드
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract add{
event JustFallback(string _str);
event JustReceive(string _str);
function addNumber(uint256 _num1, uint256 _num2) public pure returns(uint256){
return _num1 + _num2;
}
fallback() external payable {
emit JustFallback("JustFallback is called");
}
receive() external payable {
emit JustReceive("JustReceive is called");
}
}
contract caller{
event calledFunction(bool _success, bytes _output);
//1. 송금하기
function transferEther(address payable _to) public payable{
(bool success,) = _to.call{value:msg.value}("");
require(success,"failed to transfer ether");
}
//2. 외부 스마트 컨트랙 함수 부르기
function callMethod(address _contractAddr,uint256 _num1, uint256 _num2) public{
(bool success, bytes memory outputFromCalledFunction) = _contractAddr.call(
abi.encodeWithSignature("addNumber(uint256,uint256)",_num1,_num2)
);
require(success,"failed to transfer ether");
emit calledFunction(success,outputFromCalledFunction);
}
function callMethod3(address _contractAddr) public payable{
(bool success, bytes memory outputFromCalledFunction) = _contractAddr.call{value:msg.value}(
abi.encodeWithSignature("Nothing()")
);
require(success,"failed to transfer ether");
emit calledFunction(success,outputFromCalledFunction);
}
}
call vs delegate call
delegate call
- msg.sender가 본래의 스마트컨트랙 사용자를 나타낸다.
- delegate call이 정의된 스마트컨트랙(즉 caller)이 외부 컨트랙의 함수들을 마치 자신의 것 처럼 사용(실질적인 값도 caller에 저장)외부 스마트컨트랙과 caller 스마트컨트랙은 같은 변수를 갖고 있어야 한다.
- 조건
call의 과정
Alice가 스마트컨트랙A의 함수인 CallB()를 실행시킴 → 이 때 msg.sender는 Alice → CallB()가 실행되면서 스마트컨트랙B에 있는 changeB()가 실행 됨 → 스마트컨트랙B 안에서 num값이 변경 됨 → 이 때 스마트컨트랙B의 msg.sender는 스마트컨트랙A
Delegate Call 과정
Alice가 스마트컨트랙A의 CallB()를 실행시킴 → 이 때 스마트컨트랙A의 msg.sender는 Alice → CallB()가 스마트컨트랙B의 changeB()를 실행시킴 → changeB()는 다시 스마트컨트랙A로 돌아와서 num 값을 변경시킴 → 스마트컨트랙B의 msg.sender는 Alice
*delegate call의 특징 1: msg.sender가 본래의 스마트컨트랙 사용자를 나타낸다.
caller는 스마트컨트랙A, 스마트컨트랙A가 changeB()를 실행시키고 num값은 스마트컨트랙A에 저장된다.
스마트컨트랙A와 스마트컨트랙B 모두 num 변수를 갖고 있어야 한다.
*delegate call의 특징 2: delegate call이 정의된 스마트컨트랙(즉 caller)이 외부 컨트랙의 함수들을 마치 자신의 것 처럼 사용(실질적인 값도 caller에 저장)
Delegate call이 왜 쓰이는가?
upgradable smart contract 용도
고객이 물건을 사면 5 point 적립
고객 → 스마트컨트랙A → 스마트컨트랙B 주요 로직
3 point 적립으로 변경한다고 가정
여기서 문제는 스마트컨트랙A, B가 모두 블록체인 상에 배포가 된 상태라는 것
이미 배포가 되면 개발자는 스마트컨트랙 A, B의 코드를 변경할 수 없음
위변조를 할 수 없다는 블록체인의 특징 때문
그래서 스마트컨트랙A, B를 새롭게 재배포하는 방법 사용
여기서 발생하는 문제,
- 스마트컨트랙A, B에는 기존 유저들의 정보들이 저장되어 있음모든 정보는 블록체인 내부에 저장되어 있으므로 이를 긁어오려면 비용 문제
- 따라서 재배포를 하면 정보들이 초기화 됨
- 고객들은 스마트컨트랙A를 통한 주소로 포인트를 적립 받았기 때문에 재배포시 모두 새로운 주소를 부여 받음
- 모든 고객에게 새로운 주소를 알려주는 데 드는 비용 문제
이 때 사용하는 것이 upgradable smart contract, delegate call, upgradable pettern
그렇다면 적립 포인트가 스마트컨트랙B에 저장되는 것이 아니라, A에 저장 됨
또 addPoint의 방침이 바뀌면 기존의 스마트컨트랙B를 버리고 새롭게 스마트컨트랙B2를 만듦
스마트컨트랙A의 Setaddr(address B) 는 delegate call을 사용할 때 앞에 있는 컨트랙 주소를 변경해줌
따라서 스마트컨트랙B2 주소를 넣어주면 새롭게 연결
스마트컨트랙A는 새롭게 재배포 되지 않음
여기서 발생하는 이점,
- 주소 그대로 사용 가능
- 데이터 이전 필요 x
enum
사람이 읽을 수 있게 사용자/개발자에 의해 정의된 상수세트 타입(uint8 = 0~255(2^8-1)
enum 이름 {
...
}
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract lec38 {
enum CarStatus{
TurnOff, // 0
TurnOn, // 1
Driving, // 2
Stop // 3
}
CarStatus public carStatus;
constructor(){
carStatus = CarStatus.TurnOff;
}
event carCurrentStatus(CarStatus _carStatus, uint256 _carStatusInInt);
function turnOnCar() public{
require(carStatus == CarStatus(0), "To turn on, your car must be turned off");
carStatus = CarStatus.TurnOn;
emit carCurrentStatus(carStatus, uint256(carStatus));
}
function DrivingCar() public{
require(carStatus == CarStatus.TurnOn, "To drive a car, your car must be turned on");
carStatus = CarStatus.Driving;
emit carCurrentStatus(carStatus, uint256(carStatus));
}
function StopCar() public {
require(carStatus == CarStatus.Driving, "To drive a car, your car must be turned on");
carStatus = CarStatus.Stop;
emit carCurrentStatus(carStatus, uint256(carStatus));
}
function turnOffCar() public{
require(carStatus == CarStatus.TurnOn
|| carStatus == CarStatus.Stop, "To turn off, your car must be turned on or driving");
carStatus = CarStatus.TurnOff;
emit carCurrentStatus(carStatus, uint256(carStatus));
}
function CheckStatus() public view returns(CarStatus) {
return carStatus;
}
}
Interface 인터페이스
스마트컨트랙 내에서 정의 되어야 할 필요한 것
- 함수는 external로 표시
- enum, structs 가능
- 변수, 생성자 불가(constructor X)
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
interface ItemInfo{
struct item{
string name;
uint256 price;
}
function addItemInfo(string memory _name, uint256 _price) external;
function getItemInfo(uint256 _index) external view returns(item memory);
}
contract lec39 is ItemInfo {
item[] public itemList;
function addItemInfo(string memory _name, uint256 _price) override public{
itemList.push(item(_name, _price));
}
function getItemInfo(uint256 _index) override public view returns (item memory){
return itemList[_index];
}
}
library 라이브러리
기존에 만들던 스마트 컨트랙과 다른 종류의 스마트 컨트랙이라고 할 수 있음
공통으로 쓰이는 코드를 라이브러리에 넣음
이점
- 재사용: 블록체인에 라이브러리가 배포되면, 다른 스마트 컨트랙들에 적용 가능
- 가스 소비 줄임: 라이브러리는 재사용 가능한 코드왜냐하면, 가스는 스마트 컨트랙의 사이즈/길이에 영향을 많이 받기 때문이다.
- 즉, 여러 개의 스마트 컨트랙에서 공통으로 쓰이는 코드를 따로 라이브러리 통해서 배포하기에, 다른 스마트 컨트랙에 명시를 해주는 것이 아니라, 라이브러리를 적용만 하면 되기에 가스 소비량을 줄일 수 있다.
- 데이터 타입 적용: 라이브러리의 기능들은 데이터 타입에 적용할 수 있기에 좀 더 쉽게 사용할 수 있다.
제한사항
- fallback 함수 불가: fallback 함수를 라이브러리 안에 정의를 못 하기에 이더를 갖고 있을 수 없습니다.
- 상속 불가
- payable 함수 정의 불가
import 임포트
전체 예제 코드
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
import "./lec41_1.sol";
// import "../lec41_1.sol";
// / import "<https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.4/contracts/math/SafeMath.sol>";
contract lec41 is HiSolidity{
using SafeMath0 for uint8;
uint8 public a;
function becomeOverflow(uint8 _num1, uint8 _num2) public{
a = _num1.add(_num2);
}
}
3의 배수 번째 사람에게 적립된 이더 주는 스마트 컨트랙
Money Box : 3의 배수 번째 사람이게 적립된 이더를 준다.
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
/*
1. 1 이더만 내야한다.
2. 중복해서 참여 불가(단, 누군가 적립금을 받으면 초기화)
3. 관리자만 적립된 이더 볼 수 있다.
4. 3의 배수 번째 사람에게만 적립 이더를 준다.
*/
contract MoneyBox{
event WhoPaid(address indexed sender, uint256 payment);
address owner;
mapping(uint256 => mapping(address => bool)) paidMemberList; // uint256: round
uint256 round = 1;
/*
1 round : A:true, B:true, C:true paidMemberList
2 round : E, R, D paidMemberList
3 round : A, R, B paidMemberList
4 round : All false
*/
// 1 round에 참가 했어도 다음 round에 참가할 수 있음
constructor(){
owner = msg.sender;
}
receive() external payable{
require(msg.value == 1 ether, "Must be 1 ether.");
require(paidMemberList[round][msg.sender] == false, "Must be a new player in each game.");
paidMemberList[round][msg.sender] = true;
emit WhoPaid(msg.sender, msg.value);
if(address(this).balance == 3 ether){ // 3 번째 사람에게
(bool sent,) = payable(msg.sender).call{value: address(this).balance}(""); // 스마트컨트랙의 모든 balance를 전달
require(sent, "Failed to pay");
round++;
}
}
function checkRound() public view returns(uint256){
return round;
}
function checkValue() public view returns(uint256){
require(owner == msg.sender, "Only Owner can check the value");
return address(this).balance;
}
}
리믹스와 메타마스크 연결, 스마트 컨트랙 testnet에 배포하기
메타마스크(MetaMask): 이더리움 블록체인과 상호 작용하는 데 사용되는 소프트웨어 암호 화폐 지갑
메타마스크에 스마트 컨트랙 배포
하려면, 메타마스크에 이더가 필요함
메인넷에 배포를 하면 진짜 돈이 필요하기 때문에 테스트넷에 배포
테스트넷에서 이더를 받기 힘듦. 시간이 걸릴 수도, 아예 안들어올 수 있음. 따라서 구글에 ropsten faucet ether 검색
메타마스크로 환경세팅(Environment) 후 배포(Deploy)
이더스캔: 이더리움 블록체인을 위한 블록 탐색기
블록 탐색기는 유저가 이더리움 블록체인에서 발생한 트랜젝션을 확인하고 찾아볼 수 있게 하는 검색 엔진이다.
Transaction Hash로 검색 가능
솔리디티(Solidity) 깨부수기
- 목차
Hello Solidity
솔리디티(Solidity)란
스마트 컨트랙을 개발하기 위한 언어
스마트 컨트랙이란
미리 정의된 조건이 충족이 되면 미리 저장된 블록체인 프로그램이 작동하는 것
예) 3의 배수 번째 사람에게 돈을 준다. 스마트 컨트랙은 그 사람이 3번째 사람인지 확인, 아니면 돈을 주지 않음
스마트 컨트랙 작성 방법
리믹스 IDE 사용
라이센스를 제일 윗 줄에 작성해야 함
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract Hello {
string public hi = "Hello solidity";
}
compile → deploy 하면 Deployed Contracts 에서 확인 가능
솔리디티 타입
1. 데이터 타입
- boolean: true/false
- ! || == && 사용 가능
bool public b = false;
bool public b1 = !false; // true
bool public b3 = false == ture; // false
bool public b4 = false && true; // false
- byte: 1~32 바이트 저장 가능
bytes4 public bt = 0x12345678; // 4byte
bytes public bt2 = "STRING"; // 문자가 byte화 되어서 저장 됨 // 솔리디티에서는 string 쓰는 것 지양
- address: 배포가 되면 20byte의 address가 할당
- 주소를 통해서 디지털 코인을 보내기도 하고, 스마트 컨트랙을 불러오기도 한다. 은행 계좌 번호 정도로 생각
address public addr = 0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B;
- int vs uintint 범위 -2^7~2^7-1
- uint 범위 0~2^8-1
- +, -, *, / 가능
int8 public it = 4;
uint256 public uit = 132213;
uint8 public uit2 = 256; // 범위 넘기면 에러
2. 레퍼런스 타입
3. 매핑 타입
이더 단위와 가스(Ether/Gwei/Wei)
1 Ether = 10^9 Gwei = 10^18 Wei
Gwei
가스 단위
가스 (Gas)
스마트 컨트랙을 이용할 때 드는 비용, 스마트 컨트랙 운영시키는 연료
contract lec3 {
uint256 public value = 1 ether; // uint256: 1000000000000000000
uint256 public value2 = 1 wei; // uint256: 1
uint256 public value3 = 1 gwei; // uint256: 1000000000
}
코드 길이에 따라 배포(Deploy)할 때마다 가스가 소비 됨.
어떻게 하면 가스를 줄여서 스마트 컨트랙을 개발할 수 있을지 고민 해야하는 문제 (string, modifer 가스 많이 소비됨)
함수 (Function)
(public, private, internal, external) 접근 제한자 변경 가능
function 이름() public {
// 내용
}
- 파라미터와 리턴 값이 없는 경우
uint256 public a = 3;
function changeA1() public {
a = 5;
}
- 파라미터는 있고, 리턴 값이 없는 경우
function changeA2(uint256 _value) public {
a = _value;
}
2개 이상의 파라미터
function changeA(uint256 _value1, uint256 _value2) public{
a =_value1 + _value2;
}
- 파라미터도 있고, 리턴 값도 있는 경우
function changeA3(uint256 _value) public returns (uint256) {
a = _value;
return a;
}
접근제한자 (public/private/internal/external)
- public: 모든 곳에서 접근 가능
- external: 오직 밖에서만 접근 가능. 모든 곳에서 접근 가능하나, external이 정의된 자기자신 컨트랙 내에서 접근 불가
- private: 오직 private이 정의된 자기 컨트랙에서만 가능(상속받은 자식도 불가능)
- internal: private 처럼 오직 internal이 정의된 자기 컨트랙에서만 가능하고, internal이 정의된 컨트랙을 상속 받을 수 있음
public/private 예제
contract Hello {
// 1. public
uint256 public a = 5;
// 2. private
uint256 private a2 = 5; // 배포하면 보이지 않음, lec5.sol을 배포했기 때문?
}
external 예제
contract Public_example {
uint256 external a = 3; // 접근 불가로 에러
function changeA(uint256 _value) external {
a = _value;
}
function get_a() view external returns (uint256) {
return a;
}
}
contract Public_example_2 {
Public_example instance = new Public_example(); // 인스턴스화
function changeA_2(uint256 _value) public {
instance.changeA(_value);
}
function use_public_example_a() view public returns (uint256) {
return instance.get_a();
}
}
view와 pure
uint256 public a = 1;
function read_a() public view returns(uint256) {
return a+2;
}
view
function 밖의 변수들을 읽을 수 있으나 변경 불가능
pure
function 밖의 변수들을 읽지 못하고, 변경도 불가능
순수하게 function 내에서만 쓸 수 있음
function read_a2() public pure returns(uint256) {
uint256 b = 1;
return 4+2+b;
}
view와 pure 둘 다 명시 안할 때
function 밖에 변수들을 읽어서 변경할 때 사용
function read_a3() public returns (uint256) {
a = 13;
return a;
}
String
솔리디티 메모리 영역
storage
대부분의 변수, 함수들이 저장되며, 영속적으로 저장이 되어 가스 비용이 비싸다. (함수와 함수 밖의 변수들)
memory
함수의 파라미터, 리턴값, 레퍼런스 타입이 주로 저장된다. (함수 안의 변수들) 그러나, storage 처럼 영속적이지 않고, 함수 내에서만 유효하기에 storage보다 가스 비용이 싸다.
Calldata
주로 external function의 파라미터에서 사용 됨
stack
EVM(Ethereum Virtual Machine)에서 stack data를 관리할 때 쓰는 영역인데 1024Mb 제한적
String 저장 영역 - memory
string은 기본 타입이 아니라 레퍼런스에 들어감. memory라고 지정해 줘야 함.
function get_string(string memory _str) public pure returns(string memory) {
return _str;
}
기본 타입은 자동이기 때문에 memory라고 지정해줄 필요가 없음
function get_uint(uint256 _str) public pure returns(uint256) {
return _str;
}
인스턴스 (Instance)
여러 개의 컨트랙을 이어줄 때 사용
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract A {
uint256 public a = 5;
function change(uint256 _value) public {
a = _value;
}
}
contract B {
A instance = new A();
function get_A() public view returns(uint256) {
return instance.a();
}
function change_A(uint256 _value) public {
instance.change(_value);
}
}
a 본연의 값을 가져온는 것이 아니라, a의 분신을 새로 생성한 것
A와 별개의 것 이며 구조는 같다.
생성자 (Constructor)
기존의 값을 초기화할 때 쓰임
처음 생성, 배포, 인스턴스화 될 때 생성 됨
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract A {
string public name;
uint256 public age;
constructor(string memory _name, uint256 _age) {
name = _name;
age = _age;
}
function change(string memory _name, uint256 _age) public {
name = _name;
age = _age;
}
}
contract B {
A instance = new A("Alice", 52);
function change(string memory _name, uint256 _age) public {
instance.change(_name, _age);
}
function get() public view returns(string memory, uint256) {
return (instance.name(), instance.age());
}
}
B에서 생성한 A는 컨트랙트 A와 독립적이다.
B안에서 A자체를 다 가져오는 것이기 때문에 인스턴스화하면 가스가 많이 소비 된다.
이 가스에 대한 비용적인 문제도 있지만, 그 보다 블록마다 가스를 소비할 수 있는 양은 제한적이라는 것을 알아야 한다. 제한하는 양을 초과한다면 이더리움 자체에서 에러가 발생하고, 스마트 컨트랙 배포도 불가하다.
이를 개선하는 방법으로 클론 팩토리 패턴을 사용하는 방법이 있다. 가스 소비량을 획기적으로 줄일 수 있다.
상속
자신의 재산을 대가 없이 주는 것
is 키워드 사용
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract Father {
string public familyName = "Kim";
string public givenName = "Jung";
uint256 public money = 100;
function getFamilyName() view public returns(string memory) {
return familyName;
}
function getGivenName() view public returns(string memory) {
return givenName;
}
function getMoney() view public returns(uint256) {
return money;
}
}
contract Son is Father {
}
상속받을 컨트랙트에 constructor가 있을 때
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract Father {
string public familyName = "Kim";
string public givenName = "Jung";
uint256 public money = 100;
constructor(string memory _givenName) public {
givenName = _givenName;
}
function getFamilyName() view public returns(string memory) {
return familyName;
}
function getGivenName() view public returns(string memory) {
return givenName;
}
function getMoney() view public returns(uint256) {
return money;
}
}
contract Son is Father("James"){
}
상속받는 또 다른 방법
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract Father {
...
}
contract Son is Father{
constructor() Father("James") {
}
}
두 개 이상 상속하기
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract Father {
uint256 public fatherMoney = 100;
function getFatherName() public pure returns(string memory) {
return "KimJung";
}
function getMoney() public view virtual returns(uint256) {
return fatherMoney;
}
}
contract Mother {
uint256 public motherMoney = 500;
function getMotherName() public pure returns(string memory) {
return "Leesol";
}
function getMoney() public view virtual returns(uint256) {
return motherMoney;
}
}
contract Son is Father, Mother {
// overriding
function getMoney() public view override(Father, Mother) returns(uint256) {
return fatherMoney+motherMoney;
}
}
오버라이딩 (Overriding)
덮어 씌우기
virtual - override 키워드는 항상 써줘야 한다.
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract Father {
string public familyName = "Kim";
string public givenName = "Jung";
uint256 public money = 100;
constructor(string memory _givenName) public {
givenName = _givenName;
}
function getFamilyName() view public returns(string memory) {
return familyName;
}
function getGivenName() view public returns(string memory) {
return givenName;
}
function getMoney() view virtual public returns(uint256) {
return money;
}
}
contract Son is Father("James"){
uint256 public earning = 0;
function work() public {
earning += 100;
}
function getMoney() view override public returns(uint256) {
return money+earning;
}
}
이벤트 (event)
블록체인 네트워크의 블록에 특정값을 기록하는 것
송금하기 라는 함수가 있다고 가정하면, 송금 버튼을 누르면 계좌와 금액이 이벤트로 출력되어서 블록체인 네트워크 안에 기록 됨 (로그로 출력 됨)
contract lec13 {
event info(string name, uint256 money); // 출력
function sendMoney() public {
emit info("KimDadJin", 1000);
}
}
indexd
이벤트의 내에서만 사용 가능한 키워드
특정한 이벤트의 값을 들고 올 때 사용 가능
async function getEvent (){
let events = await lecture14.getPastEvents('numberTracker2',{ filter:{num:[2,1]},fromBlock: 1, toBlock:'latest'});
console.log(events) // num 이 1일 때, 혹은 2일 때만 filter
let events2 = await lecture14.getPastEvents('numberTracker',{ filter:{num:[2,1]},fromBlock: 1, toBlock:'latest'});
console.log(events2) // numberTracker는 indexed를 안줬기 때문에 filter 안됨
}
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract lec14 {
event numberTracker(uint256 num, string str);
event numberTracker2(uint256 indexed num, string str);
uint256 num = 0;
function PushEvent(string memory _str) public {
emit numberTracker(num, _str);
emit numberTracker2(num, _str);
num++;
}
}
super
오버라이딩 할 때 원래 함수를 갖고 옴
super.who()
contract Father {
event FatherName(string name);
function who() public virtual {
emit FatherName("KimDaeho");
}
}
contract Son is Father {
event sonName(string name);
function who() public override{
super.who();
emit sonName("KimJin");
}
}
상속의 순서
contract Father {
event FatherName(string name);
function who() public virtual {
emit FatherName("KimDaeho");
}
}
contract Mother {
event MotherName(string name);
function who() public virtual {
emit MotherName("leeSol");
}
}
contract Son is Father, Mother {
event sonName(string name);
function who() public override(Father,Mother){
super.who();
}
} {
event sonName(string name);
function who() public override(Father,Mother){
super.who();
}
}
contract Son is Father, Mother
가장 최신의 Mother의 who를 상속받는다.
매핑 (Mapping)
key-value로 이루어짐
특정한 키를 넣어주면 그에 해당되는 value값을 반환
mapping(uint256=>uint256)
키 타입: uint256, 밸류타입: uint256
length 기능이 없음
contract lec17 {
mapping(uint256=>uint256) private ageList;
mapping(string=>uint256) private priceList;
mapping(uint256=>string) private nameList;
function setAgeList(uint256 _index, uint256 _age) public {
ageList[_index] = _age;
}
function getAge(uint256 _index) public view returns(uint256) {
return ageList[_index];
}
function setNameList(uint256 _index, string memory _name) public {
nameList[_index] = _name;
}
function getNameList(uint256 _index) public view returns(string memory) {
return nameList[_index];
}
function setPriceList(string memory itemName, uint256 _price) public {
priceList[itemName] = _price;
}
function getPriceList(string memory _index) public view returns(uint256) {
return priceList[_index];
}
}
배열 (Array)
length 가능
순회 가능
그러나 솔리디티에서는 배열보다 매핑 선호, 왜냐하면 순환하는 것은 디도스 공격에 취약할 수 있어서
배열의 값을 50으로 지정해서 사용하는 것이 좋음
contract lec18 {
uint256[] public ageArray;
uint256[10] public ageFixedSizeArray; // 사이즈 제한
string[] public nameArray = ["Kal", "Jhon", "Kerri"];
function AgeLength() public view returns(uint256) {
return ageArray.length;
}
function AgePush(uint256 _age) public {
ageArray.push(_age);
}
function AgeGet(uint256 _index) public view returns(uint256) {
return ageArray[_index];
}
function AgePop() public {
ageArray.pop();
}
function AgeDelete(uint256 _index) public {
delete ageArray[_index]; // 0으로 채워지고 length는 그대로
}
function AgeChange(uint256 _index, uint256 _age) public {
ageArray[_index] = _age;
}
}
array.pop() → 제일 최신의 값 삭제
delete ageArray[_index]; → 0으로 채워지고 length는 그대로
Mapping과 Array 주의할 점
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract lec19 {
uint256 num = 89;
mapping(uint256 => uint256) numMap;
uint256[] numArray;
function changeNum(uint256 _num) public{
num = _num;
}
function showNum() public view returns(uint256){
return num;
}
function numMapAdd() public{
numMap[0] = num;
}
function showNumMap() public view returns(uint256){
return numMap[0];
}
function UpdateMap() public{
numMap[0] = num;
}
function numArrayAdd() public{
numArray.push(num);
}
function showNumArray() public view returns(uint256){
return numArray[0];
}
function updateArray() public {
numArray[0] = num;
}
}
구조체 (struct)
나만의 타입을 만드는 것
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract lec20 {
struct Character {
uint256 age;
string name;
string job;
}
function createCharacter(uint256 _age, string memory _name, string memory _job) pure public returns(Character memory) {
return Character(_age, _name, _job);
}
}
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract lec20 {
struct Character {
uint256 age;
string name;
string job;
}
mapping(uint256=>Character) public CharacterMapping;
Character[] public CharacterArray;
function createCharacter(uint256 _age, string memory _name, string memory _job) pure public returns(Character memory) {
return Character(_age, _name, _job);
}
function createCharacterMapping(uint256 _key, uint256 _age, string memory _name, string memory _job) public {
CharacterMapping[_key] = Character(_age, _name, _job);
}
function getCharacterMapping(uint256 _key) public view returns(Character memory) {
return CharacterMapping[_key];
}
function createCharacterArray(uint256 _age, string memory _name, string memory _job) public {
CharacterArray.push(Character(_age, _name, _job));
}
function getCharacterArray(uint256 _index) public view returns(Character memory) {
return CharacterArray[_index];
}
}
if 조건문
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract lec21 {
string private outcome = "";
function isIt5(uint256 _number) public returns(string memory) {
if(_number == 5) {
outcome = "Yes, it is 5";
return outcome;
}
else {
outcome = "No, it is not 5";
return outcome;
}
}
function isIt5or3or1(uint256 _number) public returns(string memory){
if(_number == 5){
outcome = "Yes, it is 5";
return outcome;
}
else if(_number == 3){
outcome = "Yes, it is 3";
return outcome;
}
else if(_number == 1){
outcome = "Yes, it is 1";
return outcome;
}
else{
outcome = "No, it is not 5, 3 or 1";
return outcome;
}
}
}
loop 반복문
for
contract lec22 {
event CountryIndexName(uint256 indexed _index, string _name);
string[] private countryList = ["South Korea", "North Korea", "USA", "China", "Japan"];
function forLoopEvents() public {
for(uint256 i = 0; i < countryList.length; i++) {
emit CountryIndexName(i, countryList[i]);
}
}
}
while
function whileLoopEvents() public {
uint256 i = 0;
while(i < countryList.length) {
emit CountryIndexName(i, countryList[i]);
i++;
}
}
do-while
function doWhileLoopEvents() public {
uint256 i = 0;
do {
emit CountryIndexName(i, countryList[i]);
i++;
}
while(i < countryList.length);
}
break, continue
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract lec23 {
event CountryIndexName(uint256 indexed _index, string _name);
string[] private countryList = ["South Korea", "North Korea", "USA", "China", "Japan"];
function useContinue() public {
for(uint256 i = 0; i < countryList.length; i++) {
if(i%2==0) {
continue;
}
emit CountryIndexName(i, countryList[i]);
}
}
function useBreack() public {
for(uint256 i = 0; i < countryList.length; i++) {
if(i==2) {
break;
}
emit CountryIndexName(i, countryList[i]);
}
}
}
linear search
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract lec24 {
event CountryIndexName(uint256 indexed _index, string _name);
string[] private countryList = ["South Korea", "North Korea", "USA", "China", "Japan"];
function linearSearch(string memory _search) public view returns(uint256, string memory) {
for(uint256 i=0; i<countryList.length; i++) {
if(keccak256(bytes(countryList[i])) == keccak256(bytes(_search))) {
return (i, countryList[i]);
}
}
return (0, "Nothing");
}
}
솔리디티 내에서 문자를 비교하려면 바이트화 → keccak256 내장함수를 이용해서 해시화를 시켜줘야 한다. → 값이 같으면 똑같은 해시가 나온다.
에러 핸들러 assert, revert, require 0.4.22 ~ 0.7.x 버전
*스마트컨트랙에서 여러 라이브러리의 버전이 다 다르기 때문에 다 알아야 함
revert, require: 스마트컨트랙 작성 시 사용
assert: 가스를 다 소비하기 때문에 실용적이지 x, test용으로 사용, 보통 스마트컨트랙을 다 만들고 나서 트러플 내에서 테스트 할 때 사용
assert
gas를 다 소비한 후, 특정한 조건에 부합하지 않으면 (false일 때) 에러를 발생시킨다.
가스를 다 소비하고 조건문을 확인해서 에러를 발생시킴
// 3000000 gas
function assertNow() public pure {
assert(false); // assert(조건문)
}
revert
조건없이 에러를 발생시키고, gas를 환불 시켜준다.
예상 가스비용을 지불하고 revert사용시 소비하는 가스 비용만 내고 환불해 줌
// 21322 gas
function revertNow() public pure {
revert("error!!"); // 예상 가스비용을 지불하고 revert사용시 소비하는 가스 비용만 내고 환불해줌
}
조건문이 없기 때문에 if와 같이 씀
function onlyAdults(uint256 _age) public pure returns(string memory) {
if(_age < 19) {
revert("You are not allowed to pay for the cigarette");
}
return "Your payment is succeeded";
}
require
특정한 조건에 부합하지 않으면(false일 때) 에러를 발생시키고, gas를 환불 시켜준다.
function requireNow() public pure {
require(false, "occurred");
}
function onlyAdults2(uint256 _age) public pure returns(string memory) {
require(_age > 19, "You are not allowed to pay for the cigarette");
return "Your payment is secceeded";
}
함수 실행 시 프로세스
1. 특정 함수를 선택해서 버튼 누른다.
2. 솔리디티는 "이 함수를 돌리고 싶으면, 이만큼의 가스를 지불해라" 라고 요구한다.
3. 가스 비용을 지불한다.
4. 함수 실행중, 어느 부분에서 revert 발생하여 에러가 난다.
5. 에러가 나서 함수의 모든 부분이 실행이 안되었으니 실행 안된만큼 gas를 환불한다.
에러 핸들러 0.8 버전
assert: 오직 내부적 에러 테스트 용도, 불변성 체크 용도
assert가 에러를 발생하면, Panic(uint256) 이라는 에러타입의 에러 발생
이전 버전과 다르게 가스를 환불 받을 수 있음
8버전에서 assertNow()예제를 실행해 보면 gas를 환불 받았음을 알 수 있음
에러 핸들러 try/catch
*0.6 버전 이후
try/catch 왜 써야 하는가?
기존의 에러 핸들러 assert/revert/require는 에러를 발생시키고 프로그램을 끝냄
그러나, try/catch를 사용해서 에러가 났어도, 프로그램을 종료시키지 않고 어떠한 대처를 하게 만들 수 있다.
try/catch 특징
- try/catch문 안에서 assert/revert/require을 통해 에러가 난다면 catch는 에러를 잡지 못하고 개발자가 의도한줄 알고 정상적으로 프로그램이 끝난다.
- try/catch문 밖에서 assert/revert/require을 통해 에러가 난다면 catch는 에러를 잡고, 에러를 핸들할 수 있다.
- 3가지 catch
- catch Error(string memory reason) { … } : revert 나 require을 통해 생성된 에러 용도
- catch Panic(uint errorCode) { … } : assert를 통해 생성된 에러가 날 때 이 catch에 잡힘
- catch(bytesmemoryLowLevelData){…} : 이 catch는 로우 레벨에러를 잡습니다.
errorCode
errorCode는 솔리디티에서 정의 Panic 에러 별로 나온다.
- 0x00: Used for generic compiler inserted panics.
- 0x01: If you call assert with an argument that evaluates to false.
- 0x11: If an arithmetic operation results in underflow or overflow outside of an unchecked { ... } block.
- 0x12; If you divide or modulo by zero (e.g. 5 / 0 or 23 % 0).
- 0x21: If you convert a value that is too big or negative into an enum type.
- 0x22: If you access a storage byte array that is incorrectly encoded.
- 0x31: If you call .pop() on an empty array.
- 0x32: If you access an array, bytesN or an array slice at an out-of-bounds or negative index (i.e. x[i] where i >= x.length or i < 0).
- 0x41: If you allocate too much memory or create an array that is too large.
- 0x51: If you call a zero-initialized variable of internal function type.
간단히 예를들어 4번에 0으로 나눌시 division error를 언급하며, 7번에 배열의 길이가 0인데 pop을 하여 배열안의 값을 뽑을때도 panic이 발생한다고합니다.
Try/Catch 사용
1. 외부 스마트 컨트랙 함수를 부를 때
다른 스마트 컨트랙을 인스턴스화 해서, try/catch문이 있는 스마트 컨트랙의 함수를 불러와서 사용
contract math {
function division(uint256 _num1, uint256 _num2) public pure returns (uint256) {
require(_num1<10, "num1 should not be more than 10");
return _num1/_num2;
}
}
contract runner {
event catchErr(string _name, string _err);
event catchPanic(string _name, uint256 _err);
event catchLowLevelErr(string _name, bytes _err);
math public mathInstance = new math();
function playTryCatch(uint256 _num1, uint256 _num2) public returns(uint256, bool) {
try mathInstance.division(_num1, _num2) returns(uint256 value) {
return(value, true);
} catch Error(string memory _err) {
emit catchErr("revert/require", _err);
return (0, false);
} catch Panic(uint256 _errorCode) {
emit catchPanic("assertError/Panic", _errorCode);
return (0, false);
} catch (bytes memory _errorCode) {
emit catchLowLevelErr("LowlevelError", _errorCode);
return (0, false);
}
}
}
2. 외부 스마트 컨트랙을 생성할 때
다른 스마트 컨트랙을 인스턴스화 생성할 때 씀
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
// 외부 스마트 컨트랙을 생성 할 때
contract character {
string private name;
uint256 private power;
constructor(string memory _name, uint256 _power) {
revert("error");
name = _name;
power = _power;
}
}
contract runner {
event catchOnly(string _name, string _err);
function playTryCatch(string memory _name ,uint256 _power) public returns(bool) {
try new character(_name, _power) {
revert("errors in the tyr/catch block");
return(true);
}
catch {
emit catchOnly("catch", "Errors!!");
return(false);
}
}
}
3. 스마트 컨트랙 내에서 함수를 부를 때
this를 통해 try/catch를 씀
return 값 변수 명시
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract lec29 {
function add(uint256 _num1, uint256 _num2) public pure returns(uint256) {
uint256 total = _num1 + _num2;
return total;
}
function add2(uint256 _num1, uint256 _num2) public pure returns(uint256 total) {
total = _num1 + _num2;
return total;
}
}
방법 1의 경우 return값이 여러 개일 경우 헷갈릴 수 있음
modifier 모디파이어
function onlyAdults2(uint256 _age) public pure returns(string memory) {
require(_age>19, "You are not allowed to pay for the cigarette");
return "Your payment is succeeded";
}
function onlyAdults32(uint256 _age) public pure returns(string memory) {
require(_age>19, "You are not allowed to pay for the cigarette");
return "Your payment is succeeded";
}
_age>19 부분을 수정하는 경우
modifier onlyAdults{ // == onlyAdults(), 보통 빈 배열은 써주지 않음
revert("You are not allowed to pay for the cigarette");
_; // BuyCigarette() 함수가 어느 자리에 적용이 되는지
}
function BuyCigarette() public onlyAdults returns(string memory) {
return "Your payment is succeded";
}
_; : 함수가 실행되는 자리
modifier에서 파라미터 값 받을 때
modifier onlyAdults2(uint256 _age) {
require(_age>18, "You are not allowed to pay for the cigarette");
_;
}
function BuyCigarette2(uint256 _age) public onlyAdults2(_age) returns(string memory) {
return "Your payment is succeded";
}
modifier numChange{
_;
num = 10;
}
function numChangeFunction() public numChange{
num = 15;
}
SPDX 라이센스/주석
목적
- 라이센서를 명시해줌으로써 스마트컨트랙에 대한 신뢰감을 눞일 수 있음
- 스마트 컨트랙 소스코드가 워낙 오픈되어 있으니, 저작권과 같은 관련된 문제를 해소
주석
- 블록단위: 보통 블록단위의 주석은 스마트컨트랙, 함수 등 많은 양의 설명
- 행단위: 행단위는 변수 바로 앞에 쓰여서, 짤막짝막한 설명
payable, msg, value 와 이더를 보내는 3가지 함수
payable
이더/토큰과 상호작용시 필요한 키워드
send, transfer, call을 이용하여, 이더를 보낼 때 Payable이라는 키워드가 필요 함
주로 함수, 주소, 생성자에 붙여서 사용된다.
msg.value
송금보낸 코인의 값
이더를 보내는 3가지 함수
- send: 2300 gas를 소비, 성공여부를 true 또는 false로 리턴
- transfer: 2300 gas 소비, 실패시 에러를 발생
- call: 가변적인 gas 소비(gas값 지정 가능), 성공여부를 true 또는 false로 리턴
- 재진입(reentrancy) 공격 위험성 있음, 2019년 12월 이후 call 사용을 추천
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.6.0 < 0.9.0;
contract lec31 {
event howMuch(uint256 _value);
function sendNow(address payable _to) public payable {
bool sent = _to.send(msg.value); // return true or false
require(sent, "Fail to send ether");
emit howMuch(msg.value);
}
function transferNow(address payable _to) public payable {
_to.transfer(msg.value);
emit howMuch(msg.value);
}
function callNow(address payable _to) public payable {
// ~ 0.7
// (bool sent, ) = _to.call.gas(1000).value(msg.value)("");
// require(sent, "Fail to send ether");
(bool sent, ) = _to.call{value: msg.value, gas: 1000}("");
require(sent, "Fail to send Ether");
emit howMuch(msg.value);
}
}
balance 와 msg.sender
주소.balance
특정 주소의 현재 갖고있는 이더의 잔액 (msg.value는 송금액)
msg.sender
스마트컨트랙을 사용하는 주체
앞으로 설명할 call vs delegate call에서 주요 내용이니 관심있게 보기
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract MobileBanking {
event SendInfo(address _msgSender, uint256 _currentValue);
event MyCurrentValue(address _msgSender, uint256 _value);
event CurrentValueOfSomeone(address _msgSender, address _to, uint256 _value);
function sendEther(address payable _to) public payable{
require(msg.sender.balance >= msg.value, "Your balance is not enough");
_to.transfer(msg.value);
emit SendInfo(msg.sender, (msg.sender).balance);
}
function checkValueNow() public {
emit MyCurrentValue(msg.sender, msg.sender.balance);
}
function checkUserMoney(address _to) public {
emit CurrentValueOfSomeone(msg.sender, _to, _to.balance);
}
}
payable2 생성자 적용시, msg.sender2 owner 적용
payable을 생성자에 넣을 때
constructor() payable {
// 스마트컨트랙에 이더 넣기 가능
}
특정 주소에게만 권한 주기
contract MobileBanking {
address owner;
constructor() payable {
owner = msg.sender;
}
event SendInfo(address _msgSender, uint256 _currentValue);
event MyCurrentValue(address _msgSender, uint256 _value);
event CurrentValueOfSomeone(address _msgSender, address _to, uint256 _value);
function sendEther(address payable _to) public payable{
require(msg.sender == owner, "Only Owner!"); // msg.sender가 owner여야 함
require(msg.sender.balance >= msg.value, "Your balance is not enough");
_to.transfer(msg.value);
emit SendInfo(msg.sender, (msg.sender).balance);
}
}
modifier로 변경
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract MobileBanking {
address owner;
constructor() payable {
owner = msg.sender;
}
modifier onlyOwner{
require(msg.sender == owner, "Only Owner!");
_;
}
event SendInfo(address _msgSender, uint256 _currentValue);
event MyCurrentValue(address _msgSender, uint256 _value);
event CurrentValueOfSomeone(address _msgSender, address _to, uint256 _value);
function sendEther(address payable _to) public payable{
require(msg.sender.balance >= msg.value, "Your balance is not enough");
_to.transfer(msg.value);
emit SendInfo(msg.sender, (msg.sender).balance);
}
function checkValueNow() public {
emit MyCurrentValue(msg.sender, msg.sender.balance);
}
function checkUserMoney(address _to) public {
emit CurrentValueOfSomeone(msg.sender, _to, _to.balance);
}
}
fallback / receive 함수
fallback 함수
0.6 이전
특징
- 무기명 함수, 이름 없는 함수
- external 필수, 이더가 외부에서 들어오기 때문에
- payable
왜 쓰는가?
- 스마트 컨트랙이 이더를 받을 수 있게 한다.
- 이더 받고 난 후 어떠한 행동을 취할 수 있다.
- call 함수로 없는 함수가 불려질 때 어떠한 행동을 취하게 할 수 있다. (외부 스마트컨트랙의 함수를 부를 수 있음)
function() external payable{
}
전체 예제 코드
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 < 0.9.0;
contract Bank {
event JustFallbackWithFunds(address _from, uint256 _value, string message);
// fallback 함수
function() external payable{
emit JustFallbackWithFunds(msg.sender, msg.value, "JustFallbackWithFunds is called");
}
}
// Bank에게 이더를 보내려고 함
contract You {
// receive
function DepositWithSend(address payable _to) public payable {
bool success = _to.send(msg.value);
require(success, "Failed");
}
function DepositWithTransfer(address payable _to) public payable{
_to.transfer(msg.value);
}
function DepositWithCall(address payable _to) public payable{
(bool sent, ) = _to.call.value(msg.value)("");
require(sent, "Failed to send ether");
}
//fallback()
function JustGiveMessage(address _to) public {
(bool sent, ) = _to.call("HI"); // call: 이더뿐만 아니라 함수를 보내는 기능도 있음, Bank에는 HI가 없기 때문에 fallback에 걸림
require(sent, "Failed to send ether");
}
}
0.6 이후
receive와 fallback 두 가지로 나뉨
- receive: 순수하게 이더만 받을 때 작동
- fallback: 함수를 실행하면서 이더를 보낼 때, 불려진 함수가 없을 때 작동
fallback 함수 기본형
fallback() external {
}
payable 적용 시: 이더를 받고 나서도 fallback 함수가 발동합니다.
fallback() external payable {
}
그러나 이 기능을 receive에서 대신하기 때문에 payable을 잘 안쓴다.
receive() external payable{
}
전체 예제 코드
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 < 0.9.0;
contract Bank {
event JustFallback(address _from, string message);
event ReceiveFallback(address _from, uint256 _value);
// event ReceiveFallback(address _from, uint256 _value, string message);
event JustFallbackWithFunds(address _from, uint256 _value, string message);
// fallback() external {
// emit JustFallback(msg.sender, "JustFallback is called"); // msg.sender: You 스마트컨트랙
// }
// receive 함수를 통해서 이더 받음
receive() external payable{
emit ReceiveFallback(msg.sender, msg.value);
}
// receive() external payable{
// emit ReceiveFallback(msg.sender, msg.value, "ReceviedFallback is called"); // string을 추가하면 2300 gas가 부족함, 0.7버전은 잘 작동함, 즉 0.8버전 부터 가스의 가격이 올랐다. 그래서 가스가 가변적인 call의 사용을 권장하는 것임
// }
fallback() external payable{
emit JustFallbackWithFunds(msg.sender, msg.value, "JustFallbackWithFunds is called");
}
}
// Bank에게 이더를 보내려고 함
contract You {
// receive()
function DepositWithSend(address payable _to) public payable{
bool success = _to.send(msg.value);
require(success, "Failed");
}
function DepositWithTransfer(address payable _to) public payable{
_to.transfer(msg.value);
}
function DepositWithCall(address payable _to) public payable{
(bool sent, ) = _to.call{value: msg.value}("");
require(sent, "Failed");
}
// fallback()
function JustGiveMessage(address _to) public {
(bool success, ) = _to.call("HI");
require(success, "Failed");
}
function JustGiveMessageWithFunds(address payable _to) public payable{
(bool success, ) = _to.call{value: msg.value}("HI");
require(success, "Failed");
}
}
call 함수
call: 로우레벨 함수
- 송금하기
- 외부 스마트컨트랙 함수 부르기
- 가변적인 gas 소비
- 이스탄불 하드포크, 2019년 12월 이후, gas 가격 상승에 따른 call 사용 권장/ send transfer = 2300 gas
- re-entrancy(재진입) 공격 위험이 있기에, Checks_Effects_Interactions_pattern 사용
abi
이더리움 환경 안에서 스마트컨트랙을 상호작용 시키는 표준 방법
데이터가 코드화 되어있고 설명이 되어 있음
abi object 안에는 encodeWithSignature() 메소드가 있음
이 메소드가 작동하여 외부 스마트컨트랙의 함수를 부름
전체 예제 코드
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract add{
event JustFallback(string _str);
event JustReceive(string _str);
function addNumber(uint256 _num1, uint256 _num2) public pure returns(uint256){
return _num1 + _num2;
}
fallback() external payable {
emit JustFallback("JustFallback is called");
}
receive() external payable {
emit JustReceive("JustReceive is called");
}
}
contract caller{
event calledFunction(bool _success, bytes _output);
//1. 송금하기
function transferEther(address payable _to) public payable{
(bool success,) = _to.call{value:msg.value}("");
require(success,"failed to transfer ether");
}
//2. 외부 스마트 컨트랙 함수 부르기
function callMethod(address _contractAddr,uint256 _num1, uint256 _num2) public{
(bool success, bytes memory outputFromCalledFunction) = _contractAddr.call(
abi.encodeWithSignature("addNumber(uint256,uint256)",_num1,_num2)
);
require(success,"failed to transfer ether");
emit calledFunction(success,outputFromCalledFunction);
}
function callMethod3(address _contractAddr) public payable{
(bool success, bytes memory outputFromCalledFunction) = _contractAddr.call{value:msg.value}(
abi.encodeWithSignature("Nothing()")
);
require(success,"failed to transfer ether");
emit calledFunction(success,outputFromCalledFunction);
}
}
call vs delegate call
delegate call
- msg.sender가 본래의 스마트컨트랙 사용자를 나타낸다.
- delegate call이 정의된 스마트컨트랙(즉 caller)이 외부 컨트랙의 함수들을 마치 자신의 것 처럼 사용(실질적인 값도 caller에 저장)외부 스마트컨트랙과 caller 스마트컨트랙은 같은 변수를 갖고 있어야 한다.
- 조건
call의 과정
Alice가 스마트컨트랙A의 함수인 CallB()를 실행시킴 → 이 때 msg.sender는 Alice → CallB()가 실행되면서 스마트컨트랙B에 있는 changeB()가 실행 됨 → 스마트컨트랙B 안에서 num값이 변경 됨 → 이 때 스마트컨트랙B의 msg.sender는 스마트컨트랙A
Delegate Call 과정
Alice가 스마트컨트랙A의 CallB()를 실행시킴 → 이 때 스마트컨트랙A의 msg.sender는 Alice → CallB()가 스마트컨트랙B의 changeB()를 실행시킴 → changeB()는 다시 스마트컨트랙A로 돌아와서 num 값을 변경시킴 → 스마트컨트랙B의 msg.sender는 Alice
*delegate call의 특징 1: msg.sender가 본래의 스마트컨트랙 사용자를 나타낸다.
caller는 스마트컨트랙A, 스마트컨트랙A가 changeB()를 실행시키고 num값은 스마트컨트랙A에 저장된다.
스마트컨트랙A와 스마트컨트랙B 모두 num 변수를 갖고 있어야 한다.
*delegate call의 특징 2: delegate call이 정의된 스마트컨트랙(즉 caller)이 외부 컨트랙의 함수들을 마치 자신의 것 처럼 사용(실질적인 값도 caller에 저장)
Delegate call이 왜 쓰이는가?
upgradable smart contract 용도
고객이 물건을 사면 5 point 적립
고객 → 스마트컨트랙A → 스마트컨트랙B 주요 로직
3 point 적립으로 변경한다고 가정
여기서 문제는 스마트컨트랙A, B가 모두 블록체인 상에 배포가 된 상태라는 것
이미 배포가 되면 개발자는 스마트컨트랙 A, B의 코드를 변경할 수 없음
위변조를 할 수 없다는 블록체인의 특징 때문
그래서 스마트컨트랙A, B를 새롭게 재배포하는 방법 사용
여기서 발생하는 문제,
- 스마트컨트랙A, B에는 기존 유저들의 정보들이 저장되어 있음모든 정보는 블록체인 내부에 저장되어 있으므로 이를 긁어오려면 비용 문제
- 따라서 재배포를 하면 정보들이 초기화 됨
- 고객들은 스마트컨트랙A를 통한 주소로 포인트를 적립 받았기 때문에 재배포시 모두 새로운 주소를 부여 받음
- 모든 고객에게 새로운 주소를 알려주는 데 드는 비용 문제
이 때 사용하는 것이 upgradable smart contract, delegate call, upgradable pettern
그렇다면 적립 포인트가 스마트컨트랙B에 저장되는 것이 아니라, A에 저장 됨
또 addPoint의 방침이 바뀌면 기존의 스마트컨트랙B를 버리고 새롭게 스마트컨트랙B2를 만듦
스마트컨트랙A의 Setaddr(address B) 는 delegate call을 사용할 때 앞에 있는 컨트랙 주소를 변경해줌
따라서 스마트컨트랙B2 주소를 넣어주면 새롭게 연결
스마트컨트랙A는 새롭게 재배포 되지 않음
여기서 발생하는 이점,
- 주소 그대로 사용 가능
- 데이터 이전 필요 x
enum
사람이 읽을 수 있게 사용자/개발자에 의해 정의된 상수세트 타입(uint8 = 0~255(2^8-1)
enum 이름 {
...
}
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
contract lec38 {
enum CarStatus{
TurnOff, // 0
TurnOn, // 1
Driving, // 2
Stop // 3
}
CarStatus public carStatus;
constructor(){
carStatus = CarStatus.TurnOff;
}
event carCurrentStatus(CarStatus _carStatus, uint256 _carStatusInInt);
function turnOnCar() public{
require(carStatus == CarStatus(0), "To turn on, your car must be turned off");
carStatus = CarStatus.TurnOn;
emit carCurrentStatus(carStatus, uint256(carStatus));
}
function DrivingCar() public{
require(carStatus == CarStatus.TurnOn, "To drive a car, your car must be turned on");
carStatus = CarStatus.Driving;
emit carCurrentStatus(carStatus, uint256(carStatus));
}
function StopCar() public {
require(carStatus == CarStatus.Driving, "To drive a car, your car must be turned on");
carStatus = CarStatus.Stop;
emit carCurrentStatus(carStatus, uint256(carStatus));
}
function turnOffCar() public{
require(carStatus == CarStatus.TurnOn
|| carStatus == CarStatus.Stop, "To turn off, your car must be turned on or driving");
carStatus = CarStatus.TurnOff;
emit carCurrentStatus(carStatus, uint256(carStatus));
}
function CheckStatus() public view returns(CarStatus) {
return carStatus;
}
}
Interface 인터페이스
스마트컨트랙 내에서 정의 되어야 할 필요한 것
- 함수는 external로 표시
- enum, structs 가능
- 변수, 생성자 불가(constructor X)
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
interface ItemInfo{
struct item{
string name;
uint256 price;
}
function addItemInfo(string memory _name, uint256 _price) external;
function getItemInfo(uint256 _index) external view returns(item memory);
}
contract lec39 is ItemInfo {
item[] public itemList;
function addItemInfo(string memory _name, uint256 _price) override public{
itemList.push(item(_name, _price));
}
function getItemInfo(uint256 _index) override public view returns (item memory){
return itemList[_index];
}
}
library 라이브러리
기존에 만들던 스마트 컨트랙과 다른 종류의 스마트 컨트랙이라고 할 수 있음
공통으로 쓰이는 코드를 라이브러리에 넣음
이점
- 재사용: 블록체인에 라이브러리가 배포되면, 다른 스마트 컨트랙들에 적용 가능
- 가스 소비 줄임: 라이브러리는 재사용 가능한 코드왜냐하면, 가스는 스마트 컨트랙의 사이즈/길이에 영향을 많이 받기 때문이다.
- 즉, 여러 개의 스마트 컨트랙에서 공통으로 쓰이는 코드를 따로 라이브러리 통해서 배포하기에, 다른 스마트 컨트랙에 명시를 해주는 것이 아니라, 라이브러리를 적용만 하면 되기에 가스 소비량을 줄일 수 있다.
- 데이터 타입 적용: 라이브러리의 기능들은 데이터 타입에 적용할 수 있기에 좀 더 쉽게 사용할 수 있다.
제한사항
- fallback 함수 불가: fallback 함수를 라이브러리 안에 정의를 못 하기에 이더를 갖고 있을 수 없습니다.
- 상속 불가
- payable 함수 정의 불가
import 임포트
전체 예제 코드
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
import "./lec41_1.sol";
// import "../lec41_1.sol";
// / import "<https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.4/contracts/math/SafeMath.sol>";
contract lec41 is HiSolidity{
using SafeMath0 for uint8;
uint8 public a;
function becomeOverflow(uint8 _num1, uint8 _num2) public{
a = _num1.add(_num2);
}
}
3의 배수 번째 사람에게 적립된 이더 주는 스마트 컨트랙
Money Box : 3의 배수 번째 사람이게 적립된 이더를 준다.
// SPDX-License-Identifier: GPL-30
pragma solidity >= 0.7.0 < 0.9.0;
/*
1. 1 이더만 내야한다.
2. 중복해서 참여 불가(단, 누군가 적립금을 받으면 초기화)
3. 관리자만 적립된 이더 볼 수 있다.
4. 3의 배수 번째 사람에게만 적립 이더를 준다.
*/
contract MoneyBox{
event WhoPaid(address indexed sender, uint256 payment);
address owner;
mapping(uint256 => mapping(address => bool)) paidMemberList; // uint256: round
uint256 round = 1;
/*
1 round : A:true, B:true, C:true paidMemberList
2 round : E, R, D paidMemberList
3 round : A, R, B paidMemberList
4 round : All false
*/
// 1 round에 참가 했어도 다음 round에 참가할 수 있음
constructor(){
owner = msg.sender;
}
receive() external payable{
require(msg.value == 1 ether, "Must be 1 ether.");
require(paidMemberList[round][msg.sender] == false, "Must be a new player in each game.");
paidMemberList[round][msg.sender] = true;
emit WhoPaid(msg.sender, msg.value);
if(address(this).balance == 3 ether){ // 3 번째 사람에게
(bool sent,) = payable(msg.sender).call{value: address(this).balance}(""); // 스마트컨트랙의 모든 balance를 전달
require(sent, "Failed to pay");
round++;
}
}
function checkRound() public view returns(uint256){
return round;
}
function checkValue() public view returns(uint256){
require(owner == msg.sender, "Only Owner can check the value");
return address(this).balance;
}
}
리믹스와 메타마스크 연결, 스마트 컨트랙 testnet에 배포하기
메타마스크(MetaMask): 이더리움 블록체인과 상호 작용하는 데 사용되는 소프트웨어 암호 화폐 지갑
메타마스크에 스마트 컨트랙 배포
하려면, 메타마스크에 이더가 필요함
메인넷에 배포를 하면 진짜 돈이 필요하기 때문에 테스트넷에 배포
테스트넷에서 이더를 받기 힘듦. 시간이 걸릴 수도, 아예 안들어올 수 있음. 따라서 구글에 ropsten faucet ether 검색
메타마스크로 환경세팅(Environment) 후 배포(Deploy)
이더스캔: 이더리움 블록체인을 위한 블록 탐색기
블록 탐색기는 유저가 이더리움 블록체인에서 발생한 트랜젝션을 확인하고 찾아볼 수 있게 하는 검색 엔진이다.
Transaction Hash로 검색 가능
'블록체인' 카테고리의 다른 글
[블록체인] 블록체인 이더리움 Dapp 개발에 하드햇과 오픈제펠린 활용하기 (0) | 2022.09.02 |
---|---|
[블록체인] 이더리움 & 솔리디티 기반의 투표 dApp 구현하기 (0) | 2022.09.01 |
[블록체인] Klaytn 클레이튼 블록체인 어플리케이션 만들기 - 이론과 실습 (2) | 2022.08.31 |
[블록체인] 이더리움 입문 바이블 (0) | 2022.08.29 |
[블록체인] 블록체인과 클레이튼 (0) | 2022.08.28 |