Solidity Docs

kyuu·2023년 5월 1일
0

[ Ethereum Virtual Machine ]

Account

  • External accounts : public-private key 쌍에 의해 제어됨.
    : 주소는 생성된 public-key에 의해 생성됨.

  • contract accounts : account에 저장된 code에 의해 제어됨.
    : 주소는 계약이 생성되는 시간에 의해 생성됨.

  • 모든 계정은 storage (256-bit to 256-bit key-value mapping 저장소)를 가지고 있음.

  • 모든 계정은 balance를 가지고 있음.

Transactions

  • Transactionpayloadether를 포함하는 message임.
  • Target Account가 code를 포함하고 있다면, 그 code는 실행되며 payload는 입력 데이터로써 작용함.
  • Target Account가 설정되지 않은 경우(수신자가 없거나 null인 경우) 새 컨트랙트를 생성함.

Gas

  • DApp이 구동될 때 사용하는 연료
  • 구성한 계약 로직의 복잡도에 의해 가스 비용이 결정됨(각 개별 연산들의 가스 비용의 합)

가스피를 아끼기 위한 구조체 압축

  • 선언되는 변수의 크기와 상관 없이, 솔리디티에서는 256-bit의 공간을 미리 할당함.
  • 하지만, struct 안에서 여러 변수들을 구성하고, 압축하게 된다면 그 변수들이 더 적은 공간을 차지하도록 솔리디티가 최적화해줌.
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); 
  • 동일한 데이터 타입은 되도록 나열해서 선언하는 것이 좋음.
struct MiniNotEfficient {
  uint32 a;
  uint c;
  uint32 b;
}

struct MiniMe {
  uint32 a;
  uint32 b;
  uint c;
}

// `NotEfficient`는 uint32 필드가 묶여있지 않아 `Mini`보다 가스를 조금 사용함.

view함수는 가스를 소모하지 않음.

  • view 함수는 외부에서 호출되었을 때 블록체인 상 어떤 것도 수정하지 않기 때문에, 트랜잭션을 만들 지 않음. 따라서 가스 소모도 없음.
  • 가능한 모든 곳에 external view 함수를 쓰는 것이 중요함.

    동일 컨트랙트 내에 있는 view함수가 아닌, 다른 함수에서 내부적으로 호출될 경우, 가스가 소모됨!

storage는 비싸다.

  • Storage 접근 연산은 비싸다. 특히 write 연산은 더 비쌈.
  • 따라서, 단순하게 변수를 배열에 저장하는 것 보다, 호출될 때 마다, 배열을 다시 구성하는 비효율 적인 방법이 비용 면에선 효율적일 수 있음.
  • storage 대신, memory를 사용하여, 함수 안에서 호출될 때 마다 배열을 새로구성하고, external view 함수로 외부 호출로 인해 데이터를 볼 수 있는 방법으로 구성하자.

Storage, Memory and the Stack

Storage

  • persistent(블록 체인 내부), 256-256-bit key-value pair store
  • 다른 저장소에 비해 수정, 읽기, 초기화에 비용이 비싸기 때문에, 계약을 작성할 때 이 저장소에 저장되는 데이터를 최소화 해야 함.
  • Contract는 자기 계약이 아닌 다른 계약의 storage의 접근 권한이 없음(읽고 쓰기 불가능)

Memory

  • message call의 인스턴스가 생성될 때 초기화되는 데이터 영역.
  • Memory는 선형적이며, 바이트 단위 주소 지정이 가능
  • 읽기는 256-bit 폭으로 제한되며 쓰기는 8-bit 또는 256-bit 폭으로 가능.
  • 메모리 영역은 접근(읽기 || 쓰기)이 이루어 질 때 256-bit단위로 확장됨.
  • 메모리 영역은 크기가 커질수록 quadratically costly.

Stack

  • EVM은 stack machine, 모든 계산 및 수행은 stack 데이터 영역에서 이루어짐.
  • 최대 1024 element,

언제 어떻게 사용해야 하는가?

  • 대부분의 변수들은 솔리디티 컴파일러가 알아서 메모리 영역을 잘 분할해 줌.
  • struct와, array의 경우, 명시적으로 저장 공간을 구별해 주어야 할 떄가 있음.

[ Type ]

솔리디티는 Statically typed language, 각 변수의 모든 타입 명세되어야함. 솔리디티의 타입은, Value TypeReference Type이 존재하며, Reference type은 struct, array, mapping이 존재함.

Literals

Address literals

체크섬 테스트를 통과한 16진수 리터럴, 39 ~ 41 자리의 리터럴은 체크섬 테스트를 통과하지 못하면 에러남. eg) 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF

Rational and Integer Literals

  • 과학적 표기 기법 지원 : MeE => M * 10**E

  • 가독성을 위한 Underscore 지원 : 123_000 or 0x2eff_abde or 1_2e345_678

    String Literals

  • Single-quotes('') or double-qoutes("")

  • 여러 연속적 문자열 사용 가능("foo""bar" = "foobar")

  • bytes1 ~ bytes32와 호환 가능함. 예를 들어, byte32 same = "stringliteral"은, raw byte로 해석되어 할당됨.

  • Printable ASCII 사용 가능

    Unicode Literals

  • unicode 키워드는 어떠한 UTF-8 sequence를 포함할 수있음.

Hexadecimal Literals

  • keywork hex와 single or double quotes 붙여서 사용
    (hex"00122ff", hex'0011_22_ff')

  • 사이 공백이 있는 여러개의 hexadecimal literals은 한개로 합칠 수 있음
    (hex"00112233" hex"44556677" is equivalent to hex"0011223344556677")

    Reference Type

    참조형 타입을 사용할 땐, 명시적으로 데이터 저장 위치를 지정해 주어야 함. 컨트랙트 내부 최외각 부분에서의 변수 위치에서는 data location이 생략 가능함.

memory : Lifetime is limited to an external function call
storage : state variable이 저장되는 곳과 동일, contract의 lifetime과 동일함.
calldata : function arguments를 저장하는 special location, 읽기 전용 공간으로 수정할 수 없음.

  • 각 저장소의 타입 변환은 copy가 동반됨.
  • memorycalldata는 0.6.9 이상의 컴파일러 버젼에서 가시성에 상관 없이 사용 가능함.
  • memory to memory 할당은 reference를 생성함.
  • storage to memory 할당은 copy.
  • storage to local storage variable은 reference
  • 이 외 다른 모든 storage 할당은 copy.

Boolean

  • !, &&, ||, ==, !=

Integer

Array

컴파일 타임에 고정 크기로 할당되거나, 동적으로 사이즈가 변동될 수 있음. 예를 들어, 자연수 k에 대하여 배열 TT[k] or T[]로 표현되며 전자는 고정크기, 후자는 동적 크기를 나타냄.

T는 타입을 나타내며, T에는 배열 자체가 될 수 있음. uint[][5]는 5개의 uint 타입이 들어간 고정 크기 배열을 갖는, 동적 크기 배열이란 뜻.

배열 타입은 구조체와 매핑을 포함하여 어떤 유형이던 사용할 수 있음. 단, mapping은 스토리지 데이터 위치에만 저장할 수 있으며, public-visible 함수의 파라미터는 ABI type이어야 함.

Array Slices

x[start:end] : x[start] ~ x[end-1]
start default 0, end default is length of array. both optional.

bytesstring

bytesstring은 배열임. bytesbytes1[]과 비슷하나,

Fixed-size byte arrays

bytes1, bytes2... bytes32 : 1 ~ 32byte의 연속적인 데이터

  • member : .length
  • Operators

Dynamically-sizes byte array

bytes : Dynamically-size byte array, value-type이 아님!
string : Dynamically-sized UTF-8 encoded string, value-type이 아님!!

Mapping Type

파이썬의 딕셔너리와 같음.
mapping(KeyType KeyName? => ValueType ValueName?);

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.3;

contract Mapping {
    mapping(address => uint) public balances; 
    // Nested mapping
    mapping(address => mapping(address => bool)) public isFriend;

    function examples() external {
        balances[msg.sender] = 123;
        uint bal = balances[msg.sender];
        uint bal2 = balances[address(1)]; // Not setted, return default vlue

        balances[msg.sender] += 456; // 123 + 456 = 579

        delete balances[msg.sender]; // Delete balances[msg.sender], reset to default 0

        isFriend[msg.sender][address(this)] = true;
    }
}

매핑은 storage 저장될 수 있으며 이는 상태 변수만 취급한다는 의미이다. 따라서 함수의 parameter나 return parameter로 사용되지 못한다. 이는 매핑을 포함한 배열이나 구조체에도 적용됨.

  • KeyType : Built-in type (bytes, string, uint..) or contract or enum (user-defined complex types are not allowed)
  • KeyName or ValueName은 Optional, 적지 않아도 됨.
  • public 선언이 가능하며, getter funciton의 parameter는 Keytype의 Keyname로, return 값은 ValueType의 ValueName으로 정해진다.
  • 정해지지 않은 Key로 접근시, default value를 return한다.

Address

  • address : 20byte 주소 값(이더리움 주소)
  • address payable : address와 동일하지만, transfer send의 멤버가 포함됨.
  • 이더리움을 전송받을 수 있는 주소가 아닐 경우(smart contract 주소일 수도 있기 때문)
  • address payable to address 의 경우 Implicit type conversion 가능,
    address to address payable의 경우 반드시 payable(<address>)를 통하여 명시적(payable())으로 바꿔 주어야 함.

멤버

  • balance : 이더리움의 잔고 파악(units of wei)
  • transfer : payable address로의 이더리움 전송, 전송 금액이 부족하거나, 전송할 계정으로부터 거절이 이뤄질 경우 전송 실패.
    address payable x = payable(0x123);
    address myAddress = address(this);
    if(x.balance < 10 && myAddress.balance>=10) x.transfer(10);	

[ Solidity Structure ]

  • contract definition + import + pragma

SPDX License Identifier

  • 모든 Sourcer filed은 라이센스 식별자와 함께 시작.
// SPDX-License-Identifier : MIT

Pragmas

  • Compiler의 특정 기능 및 확인등을 사용하기 위한 keyword.
  • pragma directive는 local하기 때문에 1개의 Source file에 1개의 pragma가 적용됨.
  • 파일을 import 한다고 해서, 그 파일의 pragma directive까지 import 되지 않음.
    • Version Pragma
    • ABI Coder Pragma
    • Experimental Pragma
    • ABIEncoderV2
    • SMTChecker

Import other files

  • Modulization
  • Javascript ES6와 비슷하지만 default export 지원하지 않음.
import "filename";
import * as symbolName from "filename";
import "filename" as symbolName; // Same
import {symbol1 as alias, symbol2} from "filename";
// Rename symbol name (symbol1 -> alias)
  • 다양한 플랫폼 환경에서 구동되기 위해, import paths는 host의 filesystem을 직접적으로 사용하여 접근하지 않고, 컴파일러는 VFS(Virtual filesystem : 내부 데이터베이서)를 통해 파일에 접근함.

Comments

// This is a single-line comment.

/*
This is a
multi-line comment.
*/

[ Data Locations ]

Overview

  • EVM은 5개의 주요 데이터 공간으로 나뉘어져있음.

    Storage : 각 계약이 가지고 있는 메모리 공간으로, 256-bits words : 256-bit words의 key-value store임. 저장된 데이터는 블록체인에 올라가기 때문에 영구적임. Low-level에서 SSTORE, SLOAD의 opcode를 사용. 비쌈.

    Memory : 임시 변수 공간으로(volatile), 실행되는 컨트랙트에 한정된 공간으로, 실행이 끝나면 없어짐. 새로운 메모리 인스턴스는 새로운 message call이 들어오면 얻을 수 있음.

    Calldata : read-only, 트랜젝션의 데이터 또는 외부 함수 호출의 매개변수가 저장되는 공간. memory와 비슷하며 byte-addressable 공간임. 정확한 바이트별 접근으로 읽기 연산을 수행할 수 있음.

    Stack : 작은 로컬 변수를 저장하는 공간, 읽기 쓰기 요금이 거의 없음. 제한된 공간 - 제한된 크기 영역, 컨트랙트 내부 로컬 변수의 대부분이 이 공간에 저장됨.

    Code : 컨트랙트의 소스 코드가 저장되는 공간.

데이터 공간

Data locations - rules

  • Default location을 정하는 2가지 요소
    1. Solidity 내부 변수가 어디에 위치해 있는가?
    2. 변수 이름 앞 keyword는 무엇인가?
  • constant 키워드로 정의된 변수 -> code by default
    : immutable, 컨트랙트가 deploy 되면 변경할 수 없음. read-only이며 컨트랙트 bytecode에 inline으로 새겨짐.
  • state 변수(함수 외부 선언) -> storage by default
    : 상태 변수로 불리며, 블록체인에 영원히 남아있음.
  • local variables(함수 내부에 선언됨) -> stack by default
    : uint256, bytes8, address
  • 대부분의 변수는 Solidity 컴파일러가 알아서 잘 배치해 주지만, structarray는 키워드로 명시해줘야함(storage or memory or calldata)
    : 실제 데이터가 저장되있지 않고, C의 포인터와 같이 참조만 하는 자료구조를 reference type variable이라 함.
    : 따라서 array와 같은 자료구조는 데이터가 어디에 저장될 지 명시해줘야함.

아래 3가지 상황에 맞춰, data location은 반드시 명시를 해줘야함.

  1. 함수 정의에 있어, 받는 parameter (function definition)
  2. local variables inside function (function body)
  • return value는 항상 memory 안에 존재.

< Rules for function parameters >

  • 아래 이미지 참고
    Rules for function parameters

< Rules for function body >

  • storage, memory, calldata 모두 함수의 visibility와 상관 없이 사용 가능.
  • 값을 할당하는데에 있어 아래 3가지 규칙이 있음.
    1. storage는 다른 storage 및 직접 할당 외, memory또는 calldata reference에 저장된 데이터 할당은 불가능함.
    2. memory는 키워드로 상관 없이 모두 할당 가능함. 메모리로의 할당은 항상 복사로 수행되기 때문에 원본에 영향을 미치지 않음.
    3. calldatacalldata reference로부터면 할당 가능함.

storage assignment rule

// SPDX-License-Identifier: Apache-2
pragma solidity ^0.8.0;

contract StorageReferences {

    bytes someData;

    function storageReferences() public {
        bytes storage a = someData;
        bytes memory b;
        bytes calldata c;

        // storage variables can reference storage variables as long as the storage reference they refer to is initialized.
        bytes storage d = a;

        // if the storage reference it refers to was not initiliazed, it will lead to an error
        //  "This variable (refering to a) is of storage pointer type and can be accessed without prior assignment, 
        //  which would lead to undefined behaviour."
        // basically you cannot create a storage reference that points to another storage reference that points to nothing
        // f -> e -> (nothing) ???
        bytes storage e;
        bytes storage f = e;

        // storage pointers cannot point to memory pointers (whether the memory pointer was initialized or not
        bytes storage x = b;
        bytes memory r = new bytes(3);
        bytes storage s = r;

        // storage pointer cannot point to a calldata pointer (whether the calldata pointer was initialized or not).
        bytes storage y = c;
        bytes calldata m = msg.data;
        bytes storage n = m;
    }

}

memory assignment rule

// SPDX-License-Identifier: Apache-2
pragma solidity ^0.8.0;

contract DataLocationsReferences {

    bytes someData;

    function memoryReferences() public {
        bytes storage a = someData;
        bytes memory b;
        bytes calldata c;

        // this is valid. It will copy from storage to memory
        bytes memory d = a;

        // this is invalid since the storage pointer x is not initialized and does not point to anywhere.
        /// bytes storage x;
        /// bytes memory y = x;

        // this is valid too. `e` now points to same location in memory than `b`;
        // if the variable `b` is edited, so will be `e`, as they point to the same location
        // same the other way around. If the variable `e` is edited, so will be `b`
        bytes memory e = b;

        // this is invalid, as here c is a calldata pointer but is uninitialized, so pointing to nothing.
        /// bytes memory f = c;

        // a memory reference can point to a calldata reference as long as the calldata reference
        // was initialized and is pointing to somewhere in the calldata.
        // This simply result in copying the offset in the calldata pointed by the variable reference
        // inside the memory
        bytes calldata g = msg.data[10:];
        bytes memory h = g;

        // this is valid. It can copy the whole calldata (or a slice of the calldata) in memory
        bytes memory i = msg.data;
        bytes memory j = msg.data[4:16];
    }

}

calldata assignment rule

// SPDX-License-Identifier: Apache-2
pragma solidity ^0.8.0;

contract DataLocationsReferences {

    bytes someData;

    function calldataReferences() public {
        bytes storage a = someData;
        bytes memory b;
        bytes calldata c;

        // for calldata, the same rule than for storage applies.
        // calldata pointers can only reference to the actual calldata or other calldata pointers.
    }
}

< Examples >

// 1. memory <- state variable
//    : memory 할당은 항상 복사, 할당된 데이터를 변경해도 원본엔 영향이 없음.
contract MemoryCopy {
    bytes someData;
    constructor() {
        someData = bytes("All About Solidity");
    }
    function copyStorageToMemory() public {
        
        // assigning memory <-- storage
        // this load the value from storage and copy in memory
        bytes memory value = someData;
        // changes are not propagated down in the contract storage
        value = bytes("abcd");
    
    }
}

// 2. storage pointer
//   : storage pointer가 참초하는 someData, value의 값을 바꾸면 someData의 값도 바뀜.
contract StoragePointer {
    uint256[] public someData;
    function pushToArrayViaPointer() public {
       
        uint256[] storage value = someData;
        value.push(1);
    }
}

// 3. memory <- storage pointer
//     : 여러 단계를 거쳐도 memory 할당은 복사, someData에 영향을 미치지 않음.
contract MemoryCopy {
    uint256[] public someData;
    function copyStorageToMemoryViaRef() public {
    
        uint256[] storage storageRef = someData;
        uint256[] memory copyFromRef = storageRef;
    }
}

Data locations - rules

[ Interface ]

인터페이스란

인터페이스는 abstract contract와 비슷하지만, 몇가지 제한점이 존재함. 인터페이스는 추상 함수로만 구성되며, 함수의 내용은 상속받는 쪽에서 구현한다.

  1. 다른 계약을 상속받을 수 없음. 다른 인터페이스는 상속받을 수 있음.
  2. 모든 선언 함수는 external이여야 함.
  3. 생성자를 선언할 수 없음.
  4. 상태 변수를 갖을 수 없음.
  5. modifier를 선언할 수 없음.

인터페이스의 사용

인터페이스는 말 그대로 다른 contract와 상호 작용을 하기 위한 계층에 불과함. Deploy된 계약의 함수 내용을 전부 다 가져오지 않고, 인터페이스를 선언하여 인터페이스를 통해 해당 계약의 주소를 불러오기만 하면, 그 계약에 정의된 함수를 사용할 수 있음.

예를들어, Counter.sol이라는 배포된 함수가 존재한다고 하고, 내용은 아래와 같다.

// Counter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Counter {
    uint public count;

    function inc() external {
        count += 1;
    }
    function dec() external {
        count -= 1;
    }
}

위 계약에서 사용된, inc, dec 함수를 다른 MyCounter.sol에서 사용하고 싶다고 해보자. 정의 내용을 전부 가져오는것보다, Counter의 인터페이스인, ICounter를 정의하여 배포된 Counter의 컨트랙트를 가져올 수 있음.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface ICounter {
    function count() external view returns (uint);
    function inc() external;
}

contract MyCounter {
    uint public count;

    function examples(address _counter) external {
        ICounter(_counter).inc();
        count = ICounter(_counter).count();
    }
}

배포된 Counter.sol의 주소를 넘겨주게 되면, interface ICounter에 선언된 함수의 내용이 Counter.sol에 적힌 내용으로 채워져, 호출 시 Counter.solinc 함수를 사용할 수 있다.

추가로, public으로 정의된 Counter.solcount 상태 변수는, getter 함수가 자동으로 생성되기 때문에 count() 함수를 호출하게 되면 Counter.sol의 상태값인 count를 가져올 수 있게 되는 것이다.

인터페이스는 아래와 같이 파일을 나눠 선언할 수도 있음. 인터페이스 자체가 선언된 ICounter.sol은 그 자체로만으로는 배포할 수 없나봄(remix에서 안됨)

// ICounter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface ICounter {
    function count() external view returns (uint);
    function inc() external;
    function dec() external;
}

// Counter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./ICounter.sol"

contract Counter is ICounter {
    uint public count;

    function inc() external {
        count += 1;
    }
    function dec() external {
        count -= 1;
    }
}

// MyCounter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./ICounter.sol";

contract MyCounter {
    uint public count;

    function examples(address _counter) external {
        ICounter(_counter).inc();
    }
}

[ Units and Globally Available Variables ]

Ether Units

  • wei = 1
  • gwei = 1e9
  • ether = 1e18

Time Units

  • seconds
  • minutes
  • hours
  • days
  • weeks
  • 변수에 적용될 수 없음
function f(uint start, uint daysAfter) public {
  if(block.timestamp >= start + daysAfter * 1 days) {
  // do Something...
  }
}

Special Variables and Functions

Block and Transaction Properties

  • blockhash(uint blockNumber) returns (bytes32)
    : 주어진 block의 hash값, blockNumber은 256개의 최근 block 중 한개, 아닐 경우 0 return

  • block.basefee : 현재 블럭의 베이스 요금

  • block.chainid | (uint) : 현재 체인 아이디

  • block.coinbase | (address payable) : 현재 블록 채굴자 주소

  • block.difficulty | (uint) : 현재 블록의 난이도

  • block.gaslimit | (uint) : 현재 블록의 숫자

  • block.prevrandao | (uint) : beacon chain에 의한 난수 ( >= Paris)

  • block.timestamp | (uint) : 현재 타임스탬프 (seconds since unix epoch)

  • gasleft() return | (uint256) : remaining gas

  • msg.data | (bytes calldata) : complete calldata

  • msg.sender | (address) : message의 sender 주소 (현재 call)

  • msg.sig | (bytes4) : calldata의 초기 4 byte

  • msg.value | (uint) : message의 number of wei

  • tx.gasprice | (uint) : Transaction 가스피

  • tx.origin | (address) : Transaction의 sender 주소 (full call chain)

  • tx.origin vs msg.sender ?

Type Information

타입에 대한 정보를 제공

  • type(C).name : 컨트랙트의 이름
  • type(C).creationCode : 컨트랙트의 creation bytecode
  • type(C).runtimeCode : 컨트랙트의 runtime bytecode
  • type(I).interfaceId : 주어진 인터페이스 EIP-165 식별자, bytes4 value

[ Contract ]

What is contract?

  • Immutable : Code is the law, 배포 이후 컨트랙트는 수정 및 업데이트 불가능.
  • 외부 의존성 : 다른 컨트랙트에 의존적인 컨트랙트를 작성중이라면, 언제던 그 컨트렉트를 바꿀 수 있는 수단을 만들어 놔야 함.

[ Structure of the Contract ]

  • Object-oriented languages
  • libraries, interface와 같은 특별 구조의 계약도 존재

State Variables

  • contract storage에 영원히 저장되어 있는 변수
  • Visibility
    • public : Compiler가 getter function을 자동으로 생성, 다른 계약에서도 변수 접근 가능. 변수가 선언된 계약 내부의 접근은 storage에서 값을 가져오며, 계약 외부의 접근은 getter를 유발함. Setter는 자동으로 정의되지 않기 때문에 다른 계약이 변수의 값을 바꾸지 못함.
    • internal : 변수가 정의된 Contract와 파생된 Contract 내에서만 접근 가능.
    • private : 파생된 계약에서는 볼 수 없음.

    참고 : privateinternal은 다른 계약에서의 읽기 및 쓰기를 제한하는 것이지만, blockchain 밖의 세상에서는 변수 볼 수 있음.

Function

  • Contract 내 / 외부에서 선언 가능.
  • 2가지 호출 방식 존재
    External calls : EVM message call을 생성
    Interal : EVM message call 생성하지 않음.
  • Visibility

    external : 다른 Contract나 Transaction에서 호출 가능하나,
    public :
    internal :
    private :

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;

contract SimpleAuction {
    function bid() public payable { // Function
        // ...
    }
}

// Helper function defined outside of a contract
function helper(uint x) pure returns (uint) {
    return x * 2;
}

Event

  • TODO

Error

  • TODO

Struct Types

contract Ballot {
  struct Voter { // struct
    uint weight;
    bool voted;
    address delegate;
    uint vote;
  }
}

Enum Type

  • 유한한 constant value의 set 형식의 사용자 정의 커스텀 타입
contract Purchase {
	enum State { Created, Locked, Inactive }
}

[ Array ]

  • Fixed ArrayDynamic array로 나뉨.
// Array with a fixed length of 2 elements:
uint[2] fixedArray;
// another fixed Array, can contain 5 strings:
string[5] stringArray;
// a dynamic Array - has no fixed size, can keep growing:
uint[] dynamicArray;

[ Function ]

Function Declarations

  • 함수 선언 구조
function eatHamburgers(string memory _name, uint _amount) public {

}
  • argument 값을 넘겨주는 방법으로, by value, by reference 2가지 방법이 있음.

함수 파라미터 이름 앞에 _를 추가하여 전역 변수와의 차별성을 두는 관행이 있음.

Return values from function

  • Typescript처럼, return에 대한 타입 명시.
string greeting = "Hey! what's up?";

function sayHello() public returns (string memory) {
	return greeting;
}

Typecasting

  • Typecasting
uint8 a = 5;
uint b = 6;
// throws an error because a * b returns a uint, not uint8:
uint8 c = a * b;
// we have to typecast b as a uint8 to make it work:
uint8 c = a * uint8(b);

[ Function Modifiers ]

Visibility Modifiers(접근 제어자)

  1. private : 컨트랙트 내부 다른 함수들에서만 호출 가능.
  2. internal : 컨트랙트를 상속하는 컨트랙트에서도 호출될 수 있음.
  3. external : "오직" 컨트랙트 외부에서만 호출 가능
  4. public : 내 외부 어디에서든 호출 가능함.
  • functions are public by default.
  • public 함수는 어떤 누구나 함수 호출이 가능하기 때문에 불필요한 함수를 public화 시키지 않아야 함.
  • 함수명 또한, private 함수는 _를 사용하는 관행이 있음.

State Modifiers(상태 제어자)

  • 블록체인과 상호 작용하는 방법에 대한 지시자.
    1. view : 해당 함수가 실행되어도 어떠한 데이터의 저장 및 변경이 없음을 알려줌
    2. pure : 해당 함수가 데이터의 수정 + 읽기까지 수행하지 않음을 알려줌.
// View function example
function sayHello() public view returns(string memory) {
}
// Pure function example
function _multiply(uint a, uint b) public pure returns(uint) {
  return a * b;
}
  • 위 2가지 상태 제어자는 컨트랙트 외부에서 호출되었을 때 가스를 전혀 소모하지 않음. (다른 함수에 의해 내부적으로 호출되었을 땐, 가스를 소모함.)

사용자 정의 제어자

  • 사용자가 만든 커스텀 제어자, OpenZeppelin Ownable Contract가 대표적임. (onlyOwner)

  • 사용자 정의 modifier는 인수를 갖을 수 있음.

  • 함수의 반복되는 확인 로직 등을 modifier로 직접 만들어 모듈화 시킬 수 있음.

mapping (uint => uint) public age;

modifier olderThan(uint _age, uint _userId) {
  require (age[_userId] >= _age);
  _; // require 통과 이후, 함수의 나머지 내용 실행 (필수)
}
// 선언한 Modifier에 의해 require() age 검수 시행.
// 함수 인자로 받는 _userId를 modifier 인자로 넘겨줄 수 있음.
function driveCar(uint _userId) public olderThan(16, _userId) {
  // do something...
}

payable Modifiers

  • ether를 받을 수 있는 함수 유형
  • payable modifier를 추가하여 함수 실행에 컨트랙트에 비용이 지불되도록 할 수 있음.
  • 만약 payable 지시자가 없을 때 비용을 지불하려 하면, 함수가 트랜젝션을 거부함.
  • msg.value로 트랜젝션으로 들어온 금액을 확인할 수 있음.
  • Ownable 사용시, owner.transfer 함수를 사용하여 컨트랙트에 저장된 이더를 인출할 수 있음.
contract GetPaid is Ownerble {
  function withdraw() external onlyOwner {
    owner.transfer(this.balance);
  }
}

[ Event ]

  • 솔리디티의 Event는 EVM(Etherium Virtual Machine) 위의 logging functionality의 추상화를 제공함, logging function은 EVM의 logging data struncture로 데이터를 프린트 하는 기능임.
  • 최대 3개의 매개변수에 인덱싱 속성(indexed)을 추가할 수 있음. 이를 통해 로그의 데이터 부분이 아닌, 토픽 구조에 매개변수가 표시됨. 매개변수에 인덱싱 속성이 없는 경우, 해당 매개변수는 데이터 부분에 ABI 로 인코딩됨.
  • indexed 키워드가 붙은 이벤트 로깅 데이터는, 특정 데이터로 이벤트 쿼리를 할 수 있음. 특정 이벤트의 특정 주소만 가지고 오는 등의 필터링이 가능해짐.

예를 들어, IUniswapV2Pair interface에는 아래와 같은 event가 있음
event Approval(address indexed owner, address indexed spender, uint value);
이로 인해, 출력되는 로그는, 아래와 같음. indexed 키워드가 사용된 address spender, address owner는 Topics에, value는 Data에 로깅되는 것을 볼 수 있음.
etherscan.io 예시

  • EventContract의 상속 가능한 멤버로, 이벤트가 발생하면 트랜잭션 로그에 전달된 인수를 블록체인에 저장하며, 컨트랙트 주소를 사용하여 액세스 할 수 있음. 생성된 이벤트는 컨트랙트 내에서 접근할 수 없으며, 이벤트를 생성하고 발신한 컨트랙트에서도 접근할 수 없음.
  • 각 Contract는, 특정 이벤트가 발생 하는지 listen, 이벤트 발생 시 Action을 수행함.
// Declare Event
event Deposit(address indexed _from, byte32 indexed _id, uint _value);

// Emit an event
emit Deposit(msg.sender, _id, msg.value);
event IntegerAdded(uint x, uint y, uint result);

function add(uint _x, uint _y) public {
  uint result = _x + _y;
  // IntegerAdded Event가 실행됨.
  IntegerAdded(_x, _y, result);
  return result;
}

위 이벤트가 수행되면, 앱단에서의 접근은,

contractName.IntegersAdded(function(error, result) {
	// Actions...
})

[ Random Numbers ]

keccak256 hash function

  • keccak256 : built in function, version of SHA3, get single parameter of bytes returns a random 256-bit(32bytes) hexadecimal number.
  • 아래 예시는, now의 타임스탬프 값, msg.sender, nonce(함수 실행에 딱 한번만 사용되는 숫자, 사용 이후 업데이트됨)
// Generate a random number between 1 and 100
uint randNonce = 0;
uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;
//abi.encodePacked -> string to bypes packing
keccak256(abi.encodePacked("aaab"));

abi.encode, abi.encodePacked

둘 다 인코딩의 기능을 담당하지만, encodePacked는 압축함. encodePacked는 두 개의 dynamic input이 들어갈 경우, 경우에 따라 동일한 output을 도출하는 경우가 종종 있음.
예를 들어, "aaa", "bbb"과, "aa", "abbb"라는 2개의 입력을 encodePacked로 연산 할 경우, 동일한 결과값을 도출함.
따라서, 이를 keccak256의 입력으로 사용할 경우, 입력값이 달라졌지만 출력값이 동일하여, hash collision을 초래할 수 있음.

이를 해결하는 경우는, 사이에 정적인 데이터 입력값을 추가하거나, abi.encode()를 사용하여야 함.

[ Token ]

  • 토큰은, 기본적으로 공통 규약을 따르는 스마트 컨트랙트일 뿐. 내부에서 누가 얼마나 많은 토큰을 가지고 있고, 그 토큰을 다른 토큰으로 전송 시 토큰이 얼만큼 변했는지에 대한 계약 로직일 뿐.
  • ERC20 or ERC721 토큰이 대표적임.
  • ERC721 토큰은, nonfungible, 각 토큰이 분할이 불가능하며, 고유의 identifier를 가지고 있음. 표준에 의해, 경매나 중계 로직을 직접 구현하지 않아도 ERC721 자산을 거래할 수 있는 거래소에서 공통 로직을 사용할 수 있음.
contract ERC721 {
  event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
  event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);

  function balanceOf(address _owner) public view returns (uint256 _balance);
  function ownerOf(uint256 _tokenId) public view returns (address _owner);
  function transfer(address _to, uint256 _tokenId) public;
  function approve(address _to, uint256 _tokenId) public;
  function takeOwnership(uint256 _tokenId) public;
}

[ OpenZeppelin ]

Access Control

컨트랙트에 대한 권한을 선정할 수 있음.

1. Ownable

특정 컨트랙트에 대한 소유권자를 선정하고, 함수에 대하여 실행.

  • modifier onlyOwner : 소유자만 실행할 수 있는 권한.
  • transferOwnership(address newOwner) : 새로운 Owner에 대하여 소유권 이전
  • renounceOwnership() : address(0)으로 소유권 이전, onlyOwner의 함수는 실행할 수 없음.
/**
 * @dev Throws if called by any account other than the owner.
 */
modifier onlyOwner() {
  require(msg.sender == owner);
  _;
}

2. Ownable2Step

Ownable 상속, 기존 Ownable 함수 사용 가능하지만, 소유권 이전에 대한 방식이 변경됨. 단지 한 단계의 함수 실행으로 소유권이 변경되었던 Ownable과 달리, Ownable2Step은 소유권 이전 요청 -> 소유권 이전 허가로 2단계의 소유권 이전 단계를 나눔. accept되지 않은 소유권 이전은, 실제로 반영되지 않기 때문에 소유권 이전에서 발생한 실수를 미연에 방지할 수 있음.

  • _pendingOwner : 소유권을 받을 예정의 임시 Owner
  • pendingOwner() : get pendingOwner.
  • transferOwnership : 소유권 이전 작업의 시작, event로 명시함.
  • acceptOwnership : 소유권을 이전 받는 사람이 호출, 소유권 이전을 확정함.

3. Role-Based Access Control

권한에 대한 이름을 명명하여 역할-기반의 access control 메커니즘 사용 가능.

  • role : public constant로 선언, bytes32 identifier로 선언 시 keccak256 자주 사용.
  • grantRole(role, account) : 계정에 역할 부여
  • revokeROle(role, account) : 계정이 역할 박탈

Upgradeable Contracts

배포되는 컨트랙트 앞에, proxy, proxyAdmin 컨트랙트를 두어서, l

SafeMath

오버플로우, 언더플로우가 방지된 Math library

  • 라이브러리 사용 : using SafeMath for uint256; , then call functions below.
  • library 키워드는, 사용하는 contract 내부에서 using 키워드를 통해 라이브러리를 사용할 수 있게 해줌.
library SafeMath {

  function mul(uint256 a, uint256 b) internal pure returns (uint256) {
    if (a == 0) {
      return 0;
    }
    uint256 c = a * b;
    assert(c / a == b);
    return c;
  }

  function div(uint256 a, uint256 b) internal pure returns (uint256) {
    // assert(b > 0); // Solidity automatically throws when dividing by 0
    uint256 c = a / b;
    // assert(a == b * c + a % b); // There is no case in which this doesn't hold
    return c;
  }

  function sub(uint256 a, uint256 b) internal pure returns (uint256) {
    assert(b <= a);
    return a - b;
  }

  function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    assert(c >= a);
    return c;
  }
}
contract foo {
  using SafeMath for uint;
  uint test = 2;
  test = test.mul(3); // Use SafeMath methods
}

[ App frontend & Web3.js ]

Call & Send

  • call은, 컨트랙트의 viewpure 함수를 사용, 로컬 노드에서 트랜잭션 발생하지 않기 때문에 트랜잭션 서명 및 가스비 지불할 필요 없음.

  • sendviewpure를 제외한 함수에 대하여 사용. 가스를 지불하여 트랜잭션을 만들고, 서명이 이루어지면 함수 호출이 이루어짐.

  • public 변수는 자동으로 getter 함수가 생성됨. 따라서 public 변수는 함수처럼 인자를 입력하여 호출할 수 있음.

[ Validation and Assertion ]

  • require, revert, assert, 가스피는 환불되며, 변경된 상태 변화는 다시 되돌려진다.
  • assert는 Nested 조건문 안에서 사용이 많이 됨.
  • Error Message가 길수록, gas 소비량이 커짐.
  • Custom Error를 만들어 가스 소비량을 적게 만들 수 있음
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Error {
    // require
    function testRequire(uint _i) public pure {
        require(_i <= 10, "i > 10");
        // pass do something
    }
	// revert
    function testRevert(uint _i) public pure {
        if(_i > 10) {
            if(_i > 2) {
                revert("i > 10"); // Nested
            }
        }
    }
	// assert
    uint public num = 123;
    function testAssert() public view {
        assert(num == 123);
    }
  
	// Custom Error
    error MyError(address caller, uint i); //only Revert
    
    function testCustomError(uint _i) public view {
        if(_i > 10) {
            revert MyError(msg.sender, _i);
        }
    }
}

[ Inline Assembly ]

Yul

Inline Assembly 코드를 작성하기 위해 사용하는 언어, assembly{ ... } 코드블럭 안에서 사용하는 코드이다.

  • 각각의 assembly 코드블럭은 별도의 namespace를 가지지 않기에, 다른 코드 블럭에서 정의된 변수를 호출한다던지, 함수를 호출하는 것은 불가능하다.

[ Creating Contract ]

작성한 solidity 컨트랙트를 ethers.js나 기타 방법으로 만드는 것 외에, solidity 코드로 컨트랙트를 생성하는 방법도 존재한다.

new 키워드로 만들기

Contract 내부에서 new 키워드로 다른 컨트랙트를 생성할 수 있다.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract D {
    uint public x;
    constructor(uint a) payable {
        x = a;
    }
}

contract C {
    D d = new D(4); // will be executed as part of C's constructor, 계약 C가 생성되면 계약 D도 생성됨.

    function createD(uint arg) public {
        D newD = new D(arg);
        newD.x();
    }

    function createAndEndowD(uint arg, uint amount) public payable {
        // Send ether along with the creation
        D newD = new D{value: amount}(arg);
        newD.x();
    }
}

Salted contract creation create2

byte32 값인 option salt를 사용하면 salt 값과, 만들어 지는 계약의 bytecode와, 제작자 contract의 주소를 사용하여 생성될 계약의 주소를 만든다.
이 방식은, 계약을 생성하기 이전에 계약이 생성 될 주소를 미리 계산해 볼 수 있다.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract D {
    uint public x;
    constructor(uint a) {
        x = a;
    }
}

contract C {
    function createDSalted(bytes32 salt, uint arg) public {
        // This complicated expression just tells you how the address
        // can be pre-computed. It is just there for illustration.
        // You actually only need ``new D{salt: salt}(arg)``.
        address predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
            bytes1(0xff),
            address(this),
            salt,
            keccak256(abi.encodePacked(
                type(D).creationCode,
                abi.encode(arg)
            ))
        )))));

        D d = new D{salt: salt}(arg);
        require(address(d) == predictedAddress);
    }
}

[ Fallback ]

fallbackreceive, 컨트랙트 콜에서 실행시킨 함수가 존재하지 않을 때 실행되는 함수. 보통 컨트랙트가 직접적으로 ETH를 전송받을 때 사용한다. 직접 컨트랙트에 이더를 전송하게 되면 fallback 함수가 실행되는 방식.(payable로 선언해야 함.)

  • fallback : 이더가 직접 전송되었을 때 msg.data가 존재하지 않거나, 존재하더라도 receive 함수가 정의되지 않았을 때 실행됨
  • receive : 이더가 직접 전송되었을 때 msg.data가 존재하고, 선언되었을 떄 실행됨.

[ Contract에 ETH 보내기 ]

  • transfer : 2300 gas를 가지고 주소에 전송, 실패시 reverts
  • send : 2300 gas를 가지고 주소에 전송, 실패시 returns bool
    (잘 사용하지 않음.)
  • call : 모든 가스를 가지고 returns bool and data
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.3;

contract SendETH {
    constructor() payable {} // deployed payable

    fallback() external payable {}

    function sendViaTransfer(address payable _to) external payable {
        _to.transfer(123);
    }
    // 잘 사용하지 않음.
    function sendViaSend(address payable _to) external payable {
        bool sent = _to.send(123);
        require(sent, "send failed");
    }

    function sendViaCall(address payable _to) external payable {
        (bool success, ) = _to.call{value: 123}("");
        require(success, "call failed");
    }
}

contract EthReceiver {
    event Log(uint amount, uint gas);

    receive() external payable {
        emit Log(msg.value, gasleft());
    }
}

[ Deleting Contract ]

selfdestruct : 컨트랙트 삭제, Ether를 어느 주소로 보낼 수 있음. 받는 주소가 컨트랙트이며, fallback 함수가 없다고 해도 이더를 받을 수 있음.
아래 Helper 컨트랙트는 fallback 함수가 없기 때문에 직접적인 이더리움 전송을 받을 수 없음. 하지만, selfdestruct에 의한 이더 전송은 가능함.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.3;

contract Kill { 
    constructor() payable {
    }
    function kill() external {
        selfdestruct(payable(msg.sender));
    }
    function testCall() external pure returns (uint) {
        return 123;
    }
}

contract Helper {
    function getBalance() external view returns (uint) {
        return address(this).balance;
    }
    function kill(Kill _kill) external {
        _kill.kill();
    }
}

[ Call vs DelegateCall ]

유저 foo와, 스마트 컨트랙트 A, B가 있다고 하자.
A가, B의 함수를 call한다면, B의 입장에서는 msg.sender = address(A)가 될 것이다. 만약 B에서 호출된 함수가 상태 변수를 변경하게 된다면, B의 상태 변수가 변경될 것이다.

A가 B의 함수를 delegateCall하게 된다면, A는 B의 함수를, 마치 자신의 것 마냥 사용하게 된다. B의 함수를 가져와 실행하지만, 자신의 함수인것마냥 실행하기 때문에 B의 호출에서 msg.sender = address(foo)가 된다. 또한, 이 함수에서 변경되는 상태 변수는 A의 상태변수가 된다.

  • 이를 위해, caller 스마트컨트랙트는 B의 스마트 컨트랙트와 동일한 상태 변수를 가지고 있어야 한다.

Upgradable Smart Contract 기법에 사용

Proxy 역할을 하는 스마트 컨트랙트가, 실질 로직만 담겨있는 스마트 컨트랙트를 delegateCall하게 되면, 외부에 노출된 Proxy와의 통신은 그대로 유지되며, 만약 로직이 업데이트 된다면, 로직이 담긴 스마트 컨트랙트의 배포와, Proxy 내부에 포인트 주소만 변경하게되면 된다.

[ Style Guide ]

Contract or Library naming

  • Contract와 library는 Cap-Words style : SimpleToken, SmartBank, Player...
  • Contract와 library 이름은 파일 이름과 같아야 함.(should)
  • 한 개의 솔리디티 파일이 여러개의 Contract 및 Library를 가지고 있다면, 그 중에서 core-contract or library와 이름이 같아야 함. 되도록 이렇게 작성하지 말 것.

Underscore Prefix for Non-external functions and variables

Natspec

  • /// or /** ... */
  • Contract 상단부나, function 상단부에 위치

[ Coding Patterns ]

Withdrawal from Contracts

  • transfer 함수의 direct call은 잠재적인 보안 위험으로 인해 권장하지 않음.

Access Control

컨트랙트 내부에서 사용자의 역할을 정의, 역할에 따라 권한을 부여할 수 있음.

profile
!..!

0개의 댓글