‘이더리움 댑 개발’ 세미나 11. 솔리디티 공식 문서(버전 0.8.x) – Language Description 1

이번 세미나는 솔리디티 공식 문서(버전 0.8.x)의 Language Description에서 다음과 같은 부분을 다룹니다.

  • Layout of a Solidity Source File
  • Types
  • Units and Globally Available Variables
  • Expressions and Control Structures

 

Layout of a Solidity Source File

소스 파일은 컨트랙트 정의, 임포트, pragma, 구조체, 열거형, 함수, 상수 정의를 포함할 수 있습니다. 파일 수준에 포함될 수 있다는 것은 분리된 파일로 작성해도 된다는 것입니다. 임포트나 program에 대해서는 아니겠지만, 구조체나 열거형, 함수, 상수 정의를 별도 파일에 작성하고  임포트할 수 있다는 것입니다.

 

SPDX-License-Identifier

컨트랙트는 오픈소스를 기본으로 하기 때문에 라이센스를 명시해 주는 것이 좋습니다. 솔리디티는 주석으로 SPDX 방법에 따라 라이센스를 작성합니다. 오픈소스가 아니거나 라이센스를 명시하지 않고 싶을 때는 UNLICENSED로 작성합니다. 

  • 예) // SPDX-License-Identifier: MIT

 

Pragmas

The pragma keyword is used to enable certain compiler features or checks.

pragma 지시문은 소스 파일에 로컬하게 적용되기 때문에 pragma 지시문을 사용한 다른 파일을 임포트 했다고 해서 pragma 지시문 내용을 가져오는 것은 아닙니다.

  • Version Pragma
    • 컴파일러 버전을 명시할 수 있습니다.
      • 예) pragma solidity ^0.5.2;
        • ^는 명시한 버전 이상을 요구하는데 0.5.x 만 허용하지, 0.6.x는 허용하지 않는다는 의미입니다.
        • 솔리디티는 0.x에서 x 부분이 메이저 부분을 나타내기 때문에 ^로 버전을 명시하는 것은 새로운 버전이 나오더라도 버전 호환성이 지켜지는 컴파일러까지만 사용하겠다는 것입니다.
        • 좀 더 복잡한 방식으로 버전을 명시할 수 있는데 npm에서 사용하는 문법을 사용합니다.
        • 정확한 컴파일러 버전을 명시하는 것을 추천합니다.
  • ABI Coder Pragma
    • ABI 인코더와 디코더 버전을 선택합니다. 두 가지 버전 v1과 v2를 지원합니다.
      • v2에서는 중첩된 배열과 구조체를 허용합니다.
      • 솔리디티 버전 0.8.2에서는  v2가 기본입니다.
        • v1을 사용하려면 명시적으로 “pragma abicoder v1;”와 같이 선언해야 합니다.
  • SMTChecker
    • 컴파일 시에 추가적인 safety warning을 제공합니다.
    • 이 기능을 사용하려면 “pragma experimental SMTChecker;”와 같이 선언해야 합니다.
      • experimental은 이름 그대로 ‘실험 중’이라는 것입니다. 정식으로 채택되지 않았다는 것입니다.

 

Importing other Source Files

솔리디티는 자바스크립트에서 사용하는 비슷한 방법으로 import를 사용해서 모듈화를 가능하게 합니다.

  • 다음과 같이 임포트 문을 사용할 수 있습니다.
    • import “filename”;
      • “filename”에 작성된 파일로 부터 모든 전역 심볼들을 현재 전역 스코프로 임포트합니다.
      • 이 방식은 네임스페이스를 예측할 수 없을 정도로 오염시킬 수 있기 때문에 추천하지는 않습니다.
    • import * as symbolName from “filename”;
      • “filename”의 모든 전역 심볼들을 멤버로 갖는 새로운 전역 심볼을 만듭니다. symbolName에 새로운 전역 심볼 이름을 작성합니다.
        • symbolName.symbol과 같이 임포트한 심볼들에 접근할 수 있습니다.
      • import “filename” as symbolName;과 같이 작성할 수도 있습니다.
    • import {symbol1, symbol2} from “filename”;
      • 원하는 심볼 들만 임포트할 수 있습니다.
      • 이름 충돌이 생기면 별칭을 사용할 수 있습니다.
        • import {symbol1 as s1, symbol2} from “filename”;
  • Paths
    • filename은 항상 패스로 다루어집니다.
    • /는 디렉토리 분리자로 사용됩니다.
    • .와 ..는 상대 디렉토리를 나타내는 것으로 현재 디렉토리와 부모 디렉토리를 나타냅니다.
      • 예) import “./filename” as symbolName;
        • 현재 파일과 같은 디렉토리에 있는 파일을 임포트
    • 컴파일러를 실행할 때 어떻게 경로의 첫 번째 요소를 발견할 수 있을지와 path prefix 재매핑을 명시할 수 있습니다.
      • ipfs, http, git과 같은 자원과 경로를 매핑할 수 있습니다.
        • 예) import 경로에 작성된 github.com/ethereum/dapp-bin/library에서 prefix에 해당하는 github.com/ethereum/dapp-bin을 로컬 경로 /usr/local/dapp-bin으로 매핑할 수 있습니다.
      • 경로를 재매핑은 solc 실행시 인자 설정으로 가능합니다.
        • context:prefix=target
          • context는 선택적입니다.
            • 컨텍스트를 작성함으로 동이 이름의 다른 버전의 라이브러리를 사용하는 것이 가능합니다.
          • prefix 경로는 target 경로로 대체됩니다.
              • source.sol의 import에서 github.com/ethereum/dapp-bin/ 부분을 /usr/local/dapp-bin/로 대체합니다.

 

Comments
  • //와 /*…*/ 지원
  • 문서화를 위한 주석 지원
    • /// 또는 /**…*/
      • Doxygen 스타일 태그를 사용할 수 있습니다.

Types

  • 솔리디티는 정적 타입 언어입니다.
    • 상태 변수나 로컬 변수의 타입이 명시되어야 합니다.
      • 선언된 변수는 항상 기본 값을 갖기 때문에 undefined나 null이 없습니다.
  • 솔리디티는 값 타입과 참조 타입을 구분합니다.

 

Value Types
  • 값 타입 변수는 항상 값에 의해 전달(passed by value) 됩니다. 즉 할당이나 함수 인자로 사용될 때 항상 값이 직접 복사됩니다.
  • 값 타입으로는 불린, 정수 타입, 고정 소수점 수 타입, 주소 타입, 컨트랙트 타입, 함수 타입, 열거형, 고정 크기 바이트 배열이 있습니다.
    • 불린은 bool 키워드를 사용하고 상수 true, false가 가능합니다. !, &&, ||, ==, !=와 같은 논리 연산자를 지원합니다.
      • The operators || and && apply the common short-circuiting rules. This means that in the expression f(x) || g(y), if f(x) evaluates to true, g(y) will not be evaluated even if it may have side-effects.
    • 정수형은 부호가 있는 것과 그렇지 않은 것으로 구분되고, 8비트 크기에서 256비트 크기까지 세분화된 타입을 지원한다.
      • uint8 ~ uint 256, int8 ~ int 256
      • uint와 int는 uint256와 int256의 별칭으로 사용할 수 있습니다.
      • 비교 연산자(<=, <, ==, !=, >=, >), 비트 연산자(&, |, ^, ~), 시프트 연산자(<<, >>), 산술 연산자(+, -, *, /, %, **)를 지원합니다. 음수 표시를 위해 unary -를 지원합니다.
      • 부호 있는 나머지 연산(%)은 부호가 없다고 생각하고 값을 구하고, 왼쪽 연산자의 부호를 따릅니다.
        • 예를 들어 5 % 2는 1이고, 5 % -2는 1이고, -5 % 2는 -1이고, -5 % -2는 -1입니다.
      • Bit operations are performed on the two’s complement representation of the number. This means that, for example ~int256(0) == int256(-1).
      • 양수나 음수 x에 대해서, x << y는 x * 2**y와 같습니다.
        양수 x 에 대해서, x >> y는 x / 2**y와 같습니다.
        음수 x에 대해서, x >> y는 (x + 1) / 2**y – 1와 같습니다. which is the same as dividing x by 2**y while rounding down towards negative infinity). Before version 0.5.0 a right shift x >> y for negative x was equivalent to x / 2**y, i.e., right shifts used rounding up (towards zero) instead of rounding down (towards negative infinity).
      • type(X).min, type(X).max로 최솟 값과 최댓 값을 구할 수 있다.
    • 고정 소수점 타입은 정수 타입과 같이 부호 있는 것과 그렇지 않은 것을 구분하고, 8비트 크기에서 256비트 크기까지, 0에서 80까지 소수점 자리수까지 세분화됩니다. 고정 소수점 타입은 이름 그대로 소수점 자리수가 타입에 따라 고정된다는 것입니다. 대응되는 개념으로 부동 소수점(floating point) 타입이 있습니다.
      • ufixedMxN, fixedMxN
      • ufixed와 fixed는 ufixed128x18과 fixed128x18의 별칭으로 사용할 수 있습니다.
      • 비교 연산자와 산술 연산자와 나머지 연산과 unary -을 지원합니다.
      • Scientific notation is also supported, where the base can have fractions and the exponent cannot.
        • 예) 2e10, 2e-10
      • Fixed point numbers are not fully supported by Solidity yet. They can be declared, but cannot be assigned to or from.
    • 주소 타입(address)은 계정 주소를 나타내는 타입입니다.
      • 20바이트 크기를 갖고, balance, code, codehash를 멤버로 갖습니다. 비교 연산자를 지원합니다.
      • 이더를 받을 수 있는 주소와 그렇지 않은 주소를 구분하기 위해 address payable 타입을 지원합니다.
        • address payable은 address 타입과 같은데 이더를 받을 수 있도록 transfer 함수가 추가된 주소 타입입니다.
          • transfer는 전송에 실패하면 revert합니다.
        • transfer 대신 저수준 함수인 send를 사용할 수 있습니다. send는 전송 실패시 false를 리턴합니다.
      • 주소 타입은 payload를 직접적으로 사용해서 저 수준 call, delegatecall, staticcall 함수를 호출할 수도 있습니다.
        • 컨트랙트 주소를 지원하기 위한 것입니다.
          • In order to interface with contracts that do not adhere to the ABI, or to get more direct control over the encoding, the functions call, delegatecall and staticcall are provided. They all take a single bytes memory parameter and return the success condition (as a bool) and the returned data (bytes memory). The functions abi.encode, abi.encodePacked, abi.encodeWithSelector and abi.encodeWithSignature can be used to encode structured data.
      • address payable에서 address로의 형변환은 암시적으로 이루어지지만, address에서 address payable로 형변환은 payable(…)를 사용해서 명시적으로 이뤄져야 합니다.
      • address는 uint160, 정수형 리터럴, byte20, 컨트랙트 타입과 명시적으로 형 변환 가능합니다.
      • 비교 연산자(<=, <, ==, !=, >=, >)를 지원합니다.
      • Hexadecimal literals that pass the address checksum test, for example 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF are of address type. Hexadecimal literals that are between 39 and 41 digits long and do not pass the checksum test produce an error. You can prepend (for integer types) or append (for bytesNN types) zeros to remove the error.
    • 컨트랙트 타입 변수를 선언할 수 있습니다.
      • 명시적으로 address 타입으로 형 변환 가능합니다.
      • 이더를 받을 수 있는 컨트랙트 타입(receive나 payable fallback 함수를 갖는)은 address payable로 명시적으로 형 변환할 수 있습니다.
        • payable(0)은 예외적으로 가능합니다.
      • 상위 컨트랙트 타입으로 암시적으로 형 변환 가능합니다.
      • 컨트랙트 타입의 변수를 사용해서 컨트랙트 함수를 호출할 수 있습니다.
        • You can also instantiate contracts (which means they are newly created).
      • The data representation of a contract is identical to that of the address type and this type is also used in the ABI.
      • Contracts do not support any operators.
      • The members of contract types are the external functions of the contract including any state variables marked as public.
      • type(c)로 컨트랙트 타입 정보에 접근할 수 있습니다.
    • 함수 타입 변수를 선언할 수 있습니다.
      • Function types come in two flavours – internal and external functions:
        • internal 함수는 현재 컨트랙트 내부에서만 호출 가능합니다.
        • external 함수는 외부 함수호출로 전달되거나 외부함수에서 리턴될 수 있습니다.
        • external 함수는 다음과 같은 멤버를 갖습니다.
          • address
            • returns the address of the contract of the function.
          • selector
          • returns the ABI function selector
      • 함수 타입은 다음과 같이 작성됩니다.
        • function (<parameter types>) {internal|external} [pure|view|payable] [returns (<return types>)]
        • 함수 타입은 기본이 internal입니다.
        • 함수 타입은 다음과 같은 기준에 의해 암시적으로 형변환 가능합니다.
          • 매개변수 타입이 같고, 리턴 타입이 같다.
          • internal/external 이 같다.
          • 상태 변경 가능성 정도에 대한 제약이 약하다.
            • A 타입 변수가 암시적으로 B 타입으로 형변환 가능할 때:
              • A is not more restrictive than the state mutability of  B
                • pure functions can be converted to view and non-payable functions
                • view functions can be converted to non-payable functions
                • payable functions can be converted to non-payable functions
        • external, public 함수인 경우 컨트랙트 주소를 리턴하는 address와 ABI function selector를 리턴하는 selector를 멤버로 갖습니다.
        • external, public 함수인 경우 호출 시 가스와 이더 수량을 명시할 수 있습니다.
    • 열거형은 정수형과 명시적으로 형변환 가능합니다. 열거형은 적어도 하나의 멤버를 가져야 하고, 최대 256 멤버를 가질 수 있습니다.
    • 고정 크기 바이트 배열은 이름 그대로 고정된 길이를 갖는 바이트 배열입니다.
      • 1에서 32바이트까지 바이트의 시퀀스를 갖는 타입입니다.
        • bytes1 ~ bytes32까지 타입에 해당하는 키워드를 가집니다.
      • length 멤버를 갖고, 인덱스 접근을 지원합니다.
      • 비교 연산자, 비트 연산자, 시프트 연산자를 지원합니다.
      • 인덱스 접근을 지원합니다.
        • x[k]
  • Number literal expressions retain arbitrary precision until they are converted to a non-literal type (i.e. by using them together with a non-literal expression or by explicit conversion).
    • This means that computations do not overflow and divisions do not truncate in number literal expressions.
      • For example, (2**800 + 1) – 2**800 results in the constant 1 (of type uint8) although intermediate results would not even fit the machine word size. Furthermore, .5 * 8 results in the integer 4 (although non-integers were used in between).
      • 5 / 2는 2가 아니라 2.5가 됩니다.
    • Solidity has a number literal type for each rational number. Integer literals and rational number literals belong to number literal types. Moreover, all number literal expressions (i.e. the expressions that contain only number literals and operators) belong to number literal types. So the number literal expressions 1 + 2 and 2 + 1 both belong to the same number literal type for the rational number three.
Reference Types
  • 값 타입은 할당이나 인자로 전달 될 때 값 자체가 복사되기(get an independent copy) 때문에 변수 값을 변경할 경우 해당 변수 값만 영향을 받습니다. 참조 타입은 이와는 달리 값을 참조하는 참조(메모리 위치)만 복사되기 때문에 하나의 값을 여러 곳에서 참조할 수 있고, 변수 값을 변경할 경우 의도치 않은 곳에 영향을 줄 수 있습니다. 참조 타입을 사용할 때는 좀 더 주의를 기울여야 합니다.
  • 참조 타입으로는 구조체, 배열, 매핑이 있습니다.
  • 솔리디티는 참조 타입을 사용할 경우 데이터를 어디에 저장할 지(memory, storage, calldata)를 명시하도록 하고 있습니다.
    • Calldata는 external 함수 인자를 저장하기 위한 것으로 읽기 전용이라는 점을 제외하고는 메모리와 같습니다. external이 아닌 경우 함수 인자는 기본적으로 메모리에 저장됩니다.
      • 읽기 전용 변수 값을 저장하기 위해 사용할 수도 있습니다.
      • Arrays and structs with calldata data location can also be returned from functions, but it is not possible to allocate such types.
    • Data locations are not only relevant for persistency of data, but also for the semantics of assignments:
      • storage와 memory 사이의 할당 시에는 값 복사(an independent copy)가 일어납니다.
      • calldata에서 memory나 storage로의 할당 시에는 값 복사가 일어납니다.
      • memory에서 memory로의 할당 시에는 참조 복사가 일어납니다.
      • storage에서 local storage로의 할당 시에는 참조 복사가 일어납니다.
      • All other assignments to storage always copy. Examples for this case are assignments to state variables or to members of local variables of storage struct type, even if the local variable itself is just a reference.
    • 상태 변수는 storage에 저장되기 때문에 데이터 위치를 생략할 수 있습니다.
    • 데이터 위치를 어떻게 선택하느냐에 따라 가스비가 달라지기 때문에 데이터 위치를 선택할 수 있는 경우는 주의를 기울여야 합니다.
  • 배열은 고정 길이를 갖거나 동적 길이를 가질 수 있습니다.
    • The type of an array of fixed size k and element type T is written as T[k], and an array of dynamic size as T[].
    • 배열로 선언된 public 상태 변수에 대해서는 컴파일 시 인덱스를 매개변수로 갖는 getter가 자동 생성됩니다.
    •  배열은 length를 멤버로 갖습니다.
    • An array literal is a comma-separated list of one or more expressions, enclosed in square brackets ([…]).
      • 예) [1, 2, 3], [uint(1), 2, 3]
      • 배열 리터럴은 항상 정적 크기 메모리 배열이 됩니다.
      • Fixed size memory arrays cannot be assigned to dynamically-sized memory arrays. If you want to initialize dynamically-sized arrays, you have to assign the individual elements.
    • Accessing an array past its end causes a failing assertion. Methods .push() and .push(value) can be used to append a new element at the end of the array, where .push() appends a zero-initialized element and returns a reference to it.
  • bytes와 string은 특별한 배열입니다. bytes는 byte[]와 비슷하지만 calldata와 memory에 tightly하게 packed 됩니다. string은 bytes와 같지만 length나 index 액세스가 허용되지 않습니다.
    •  As a general rule, use bytes for arbitrary-length raw byte data and string for arbitrary-length string (UTF-8) data.
    •  You should use bytes over byte[] because it is cheaper, since byte[] adds 31 padding bytes between the elements.
    • 솔리디티는 문자열을 다루는 함수들을 갖지 않기 때문에 third-party 문자열 라이브러리를 사용해야 합니다. 두 문자를 비교하거나 결합할 때 keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2))와 abi.encodePacked(s1, s2)를 사용할 수 있습니다.
    • 문자열 리터럴은 ” ” 또는 ‘ ‘를 사용해서 나타낼 수 있습니다.
      • 유니코드를 사용하려면 문자열 앞에 unicode 키워드를 작성해야 합니다. 그렇지 않으면 ASCII 문자열만 작성할 수 있습니다.
      • 16진수 문자열은 문자열 앞에 hex 키워드를 작성해야 합니다.
  • 동적 길이를 갖는 memory 배열은 new 연산자를 사용해서 만들 수 있습니다. storage 배열과는 달리 memory 배열은 크기를 조정할 수 없습니다.
    • 예) uint[] memory a = new uint[](7);
  • 동적  storage 배열과 bytes는 push(), push(x), pop()을 멤버로 갖습니다.
    • push()는 0으로 초기화된 요소를 배열의 끝에 추가하고, 추가된 요소의 참조를 리턴합니다.
      • push(x)는 x를 추가하고 아무것도 리턴하지 않습니다.
    • pop()은 배열 끝의 요소를 제거하고, 제거한 요소를 암시적으로 delete합니다.
  • Array slices are a view on a contiguous portion of an array.
    • They are written as x[start:end], where start and end are expressions resulting in a uint256 type (or implicitly convertible to it). The first element of the slice is x[start] and the last element is x[end – 1].
    • Array slices do not have any members. They are implicitly convertible to arrays of their underlying type and support index access. Index access is not absolute in the underlying array, but relative to the start of the slice.
    • Array slices do not have a type name which means no variable can have an array slices as type, they only exist in intermediate expressions.
    • As of now, array slices are only implemented for calldata arrays.
  • Struct types can be used inside mappings and arrays and they can themselves contain mappings and arrays.
    • 구조체는 자신 타입 멤버를 포함할 수 없습니다.
  • 매핑 타입은 다음과 같이 작성됩니다.
    • mapping (_KeyType => _ValueType)
      • The _KeyType can be any built-in value type, bytes, string, or any contract or enum type. Other user-defined or complex types, such as mappings, structs or array types are not allowed.
      • _ValueType can be any type, including mappings, arrays and structs.
    • The key data is not stored in a mapping, only its keccak256 hash is used to look up the value. Because of this, mappings do not have a length or a concept of a key or value being set, and therefore cannot be erased without extra information regarding the assigned keys.
    • Mappings can only have a data location of storage and thus are allowed for state variables, as storage reference types in functions, or as parameters for library functions. They cannot be used as parameters or return parameters of contract functions that are publicly visible. These restrictions are also true for arrays and structs that contain mappings.
    • You can mark state variables of mapping type as public and Solidity creates a getter for you. The _KeyType becomes a parameter for the getter. If _ValueType is a value type or a struct, the getter returns _ValueType. If _ValueType is an array or a mapping, the getter has one parameter for each _KeyType, recursively.
    • You cannot iterate over mappings, i.e. you cannot enumerate their keys. It is possible, though, to implement a data structure on top of them and iterate over that.

 

  • a가 LValue이면 다음과 같은 연산자들이 가능합니다.
    • +=, -=, *=, /=, %=, |=, &=, ^=
    • a++, a–, –a, ++a
  • delete a는 a를 초기화 합니다. a가 동적 배열인 경우 0길이 배열로 할당하고, 정적 배열인 경우 모든 배열 요소를 초기화합니다. a가 구조체인 경우 구조체 멤버들을 모두 초기화 합니다. a가 참조타입인 경우 참조만 초기화합니다.
    • delete a[x]
      • 배열 값만 초기화 합니다. 크기는 그대로 유지합니다.
    • delete는 매핑에 대해서는 효과가 없습니다 (as the keys of mappings may be arbitrary and are generally unknown).
      • However, individual keys and what they map to can be deleted: If a is a mapping, then delete a[x] will delete the value stored at x.
      • “delete y” is not valid
        • as assignments to local variables referencing storage objects can only be made from existing storage objects.
Conversions between Elementary Types
  • 할당이나 함수 인자 전달이나 연산에 대해 컴파일러는 필요 시에 암시적인 형변환을 합니다.
    • 암시적인 형변환은 일반적으로 값타입 사이에서 가능하고, 의미적으로 같고 정보 손실이 없을 때 이루어집니다.
      • For example, uint8 is convertible to uint16 and int128 to int256, but int8 is not convertible to uint256, because uint256 cannot hold values such as -1.
      • If an operator is applied to different types, the compiler tries to implicitly convert one of the operands to the type of the other (the same is true for assignments). This means that operations are always performed in the type of one of the operands.
        • y는 uint16으로, y + z의 결과는 uint32로 암시적으로 형변환 됩니다.
  • 암시적인 형변환이 가능하지 않지만 형변환이 요구될 때가 있습니다. 이런 경우 명시적으로 형변환해야 합니다. 명시적인 형변환은 원하지 않는 결과를 얻을 수 있기 때문에 주의를 기울여 사용해야 합니다.
    • If an integer is explicitly converted to a smaller type, higher-order bits are cut off.
    • If an integer is explicitly converted to a larger type, it is padded on the left (i.e., at the higher order end). The result of the conversion will compare equal to the original integer:
    • Fixed-size bytes types behave differently during conversions. They can be thought of as sequences of individual bytes and converting to a smaller type will cut off the sequence:
    • If a fixed-size bytes type is explicitly converted to a larger type, it is padded on the right. Accessing the byte at a fixed index will result in the same value before and after the conversion (if the index is still in range):
    • Since integers and fixed-size byte arrays behave differently when truncating or padding, explicit conversions between integers and fixed-size byte arrays are only allowed, if both have the same size. If you want to convert between integers and fixed-size byte arrays of different size, you have to use intermediate conversions that make the desired truncation and padding rules explicit:

 

 

Conversions between Literals and Elementary Types
  • 10진수나 16진수 리터럴은 짤림 없이 표현할 수 있을 정도로 큰 경우 정수 타입으로 암시적으로 변환될 수 있습니다.
  • 10진수 리터럴은 고정크기 바이트 배열로 암시적으로 형변환될 수 없습니다. 16진수 리터럴은 정확하게 같은 크기인 경우 가능합니다.
    • 0인 경우는  모두 가능합니다.
  • 문자열 리터럴과 16진수 문자열 리터럴은 크기가 같으면 고정크기 바이트배열로 암시적으로 형변환 가능합니다.
  • 16진수 리터럴은 올바른 크기를 갖고 checksum 테스트를 통과한다면 주소 타입이 될 수 있습니다.
    • No other literals can be implicitly converted to the address type.
  • Explicit conversions from bytes20 or any integer type to address result in address payable.
  • An address a can be converted to address payable via payable(a).

Units and Globally Available Variables

이더 단위로 wei, gwei, ether를 지원합니다. 숫자 다음에 이 단위들을 추가하면 해당 단위가 적용됩니다. 기본은 wei입니다.

시간 단위로 seconds, minutes, hours, days, weeks를 지원합니다. 기본은 seconds입니다.

  • Take care if you perform calendar calculations using these units, because not every year equals 365 days and not even every day has 24 hours because of leap seconds. Due to the fact that leap seconds cannot be predicted, an exact calendar library has to be updated by an external oracle.
  • These suffixes cannot be applied to variables. For example, if you want to interpret a function parameter in days, you can in the following way:

 

Special Variables and Functions

전역적으로 접근할 수 있는 변수와 함수를 제공합니다. 이들은 주로 블록체인에 대한 정보를 제공하는데 사용되거나 일반적으로 사용할 수 있는 유틸리티 기능을 제공하기 위한 것입니다.

  • blockhash(uint blockNumber) returns (bytes32)
    • 현재 블록을 제외하고 가장 최근의 256 블록에 대해서만 값을 구할 수 있습니다.
  • gasleft() returns (uint256)
  • block.coinbase, block.difficulty, block.gaslimit, block.number, block.timestamp
  • msg.data, msg.sender, msg.sig, msg.value
    • msg.sig – calldata의 첫 4바이트 – 함수 식별자
  • tx.gasprice, tx.origin

 

ABI Encoding and Decoding Functions
  • abi.decode(bytes memory encodedData, (…)) returns (…)
    • ABI-decodes the given data, while the types are given in parentheses as second argument.
  • abi.encode(…) returns (bytes memory)
  • abi.encodePacked(…) returns (bytes memory)
    • Performs packed encoding of the given arguments. Note that packed encoding can be ambiguous!
  • abi.encodeWithSelector(bytes4 selector, …) returns (bytes memory)
    • ABI-encodes the given arguments starting from the second and prepends the given four-byte selector
  • abi.encodeWithSignature(string memory signature, …) returns (bytes memory)
    • Equivalent to abi.encodeWithSelector(bytes4(keccak256(bytes(signature))), …)

 

These encoding functions can be used to craft data for external function calls without actually calling an external function. Furthermore, keccak256(abi.encodePacked(a, b)) is a way to compute the hash of structured data

Error Handling

에러 처리는 에러 발생을 확인하는 단계와 처리하는 단계로 구분됩니다. 에러 발생을 확인하는 방법으로 assert, require를 사용할 수 있습니다. assert와 require는 에러가 발생하는 경우 revert 합니다.

컨트랙트 코드에서 if문 등으로 에러 조건을 확인하고 revert 할 수도 있습니다.

Mathematical and Cryptographic Functions
  • addmod, mulmod
    • 나머지 연산의 덧셈과 곱셈
  • keccak256, sha256, ripemd160
    • 해시 함수들
  • ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address)
    • 서명에서 주소 구하기
        • r = first 32 bytes of signature
        • s = second 32 bytes of signature
        • v = final 1 byte of signature
    • OpenZeppelin have a ECDSA helper library that you can use as a wrapper for ecrecover without this issue.

 

Contract Related
  • this
    • 현재 컨트랙트를 나타냅니다. 명시적으로 address 타입으로 형변환 할 수 있습니다.
  • selfdestruct(address payable recipient)
    • Destroy the current contract, sending its funds to the given Address and end execution.
    • Note that selfdestruct has some peculiarities inherited from the EVM:
      • the receiving contract’s receive function is not executed.
      • the contract is only really destroyed at the end of the transaction and revert s might “undo” the destruction.
  • In order to interface with contracts that do not adhere to the ABI, or to get more direct control over the encoding, the functions call, delegatecall and staticcall are provided. They all take a single bytes memory parameter and return the success condition (as a bool) and the returned data (bytes memory). The functions abi.encode, abi.encodePacked, abi.encodeWithSelector and abi.encodeWithSignature can be used to encode structured data.
  • call 함수 호출 시 가스(gas)나 이더 수량(value)을 선택적으로 명시할 수 있습니다. 이더를 전송할 수 있다는 것은 해당 함수가 payable이라는 것을 의미합니다. payable 함수가 아닌 경우 value 옵션을 사용할 수 없습니다.
    • address(nameReg).call{gas: 1000000}(abi.encodeWithSignature(“register(string)”, “MyName”));
    • address(nameReg).call{value: 1 ether} …
    • address(nameReg).call{gas: 1000000, value: 1 ether} …
  • delegatecall은 주어진 주소에서 코드만 사용하고 모든 다른 면들(storage, balance, …)은 현재 컨트랙트에서 취합니다.
    • value를 사용 못합니다.
  • staticcall은 호출된 함수가 상태를 변경하면 revert되는 점만 다르고 call과 같습니다.
    • byzantium이후 사용 가능합니다.
  • The low-level functions call, delegatecall and staticcall return true as their first return value if the account called is non-existent, as part of the design of the EVM. Account existence must be checked prior to calling if needed.

 

Type Information

type(x)는 x의 타입 정보를 리턴합니다. 현재는 컨트랙트와 정수 타입으로 제한됩니다.

  • 컨트랙트 타입인 경우 다음과 같은 컨트랙트 정보에 접근할 수 있습니다.
    • type(C).name
      • 컨트랙트 이름
    • type(C).creationCode
      • 컨트랙트의 생성 바이트코드를 포함하는 메모리 배열
      • This can be used in inline assembly to build custom creation routines, especially by using the create2 opcode. 
      • 컨트랙트와 하위 컨트랙트에서는 액세스 할 수 없음
    • type(C).runtimeCode
      • 컨트랙트의 런타임 바이트코드를 포함하는 메모리 바이트 배열
      • creationCode와 같은 제약사항이 적용됨
    • type(I).interfaceId
      • A bytes4 value containing the EIP-165 interface identifier of the given interface I.
      • This identifier is defined as the XOR of all function selectors defined within the interface itself – excluding all inherited functions.

Expressions and Control Structures

Control Structures

C나 자바스크립트에서 사용하는 대부분의 제어 구조를 지원합니다.

  • if, else, while, do, for, break, continue, return
  • try/catch
    • 외부 함수 호출과 컨트랙트 생성 호출에서만 가능
Function Calls
  • 컨트랙트의 함수이고 컨트랙트 내부에서 호출된다고 하더라도, 외부 함수 호출 시에는 this를 사용해야 합니다. 외부 함수를 호출하면 실행 흐름이 직접적으로 jump되지 않고 외부 컨트랙트에서 호출된 것과 같이 message call이 이루어집니다.
    • When calling functions of other contracts, you can specify the amount of Wei or gas sent with the call with the special options {value: 10, gas: 10000}.
  • Due to the fact that the EVM considers a call to a non-existing contract to always succeed, Solidity uses the extcodesize opcode to check that the contract that is about to be called actually exists (it contains code) and causes an exception if it does not.
  • 함수 호출 시 인자 이름을 사용하면 함수 매개변수 순서와 상관 없이 인자를 작성할 수 있습니다. 인자 이름을 사용할 경우 { }로 둘러싸서 작성합니다.
    • 예) set({value: 2, key: 3});
  • 함수 선언 시 매개변수 이름은 생략 가능합니다.

 

 

Creating Contracts via new
  • 컨트랙트의 풀 코드를 알고 있다면 다른 컨트랙트를 new로 생성할 수 있습니다. 실제적인 컨트랙트 생성은 컨트랙트를 생성하는 컨트랙트의 생성자가 실행될 때 일어납니다.

 

 

Salted contract creations / create2
  • 컨트랙트 주소는 컨트랙트가 생성될 때 리턴됩니다. 컨트랙트 주소는 컨트랙트 생성 트랜잭션을 전송하는 주소와 이 주소로 부터 만들어진 컨트랙트 수를 가지고 계산합니다. 컨트랙트 수를 사용하기 때문에 컨트랙트가 생성되어야 주소를 계산할 수 있습니다.
  • salt(32바이트 값) 옵션을 사용해서 컨트랙트를 생성하면 컨트랙트 생성전에 컨트랙트 주소를 계산할 수 있습니다.
    • 예) D d = new D{salt: salt}(arg);
    • 이것은 내부적으로는 create2 함수를 사용합니다.

 

Assignment

솔리디티는 내부적으로 튜플을 사용합니다.

  • 다수의 리턴 값이 가능한 것도 리턴으로 튜플을 사용하기 때문입니다.
  • 튜플은 새롭게 선언된 변수나 이미 존재하는 변수에 값을 할당할 수 있도록 합니다.
  • Tuples are not proper types in Solidity, they can only be used to form syntactic groupings of expressions.
  • It is not possible to mix variable declarations and non-declaration assignments.

 

Scoping and Declarations
  • A variable which is declared will have an initial default value whose byte-representation is all zeros. The “default values” of variables are the typical “zero-state” of whatever the type is.

 

 

Checked or Unchecked Arithmetic

솔리디티 0.8.0 부터는 기본적으로 언더플로어와 오버플로어 발생 시 산술 연산은 revert 됩니다. 오버플로어를 다루기 위해 SafeMath와 같은 라이브러리는 더 이상 필요 없습니다. 언더플로어나 오버플로어를 revert로 처리하지 않기 위해서는 unchecked 블록을 사용해야 합니다.

다음과 같은 연산 시에 언더플로어와 오버플로어가 체크됩니다.

  • ++, –, +, binary -, unary -, *, /, %, **
  • +=, -=, *=, /=, %=

 

Division by zero causes a Panic error. This check can not be disabled through unchecked { … }

The expression type(int).min / (-1) is the only case where division causes an overflow.

 

 

Error handling: Assert, Require, Revert and Exceptions

  • Solidity uses state-reverting exceptions to handle errors. Such an exception undoes all changes made to the state in the current call (and all its sub-calls) and flags an error to the caller.
  • When exceptions happen in a sub-call, they “bubble up” (i.e., exceptions are rethrown) automatically.
    • 이 규칙에 예외적인 경우는 send, call, delegatecall, staticcall과 같이 false를 리턴하는 경우입니다.
  • Exceptions in external calls can be caught with the try/catch statement.
    • 예외에는 4바이트 selector와 ABI-encoded data가 포함되어 있습니다.
    • Solidity supports two error signatures: Error(string) and Panic(uint256)
      • The first (“error”) is used for “regular” error conditions while the second (“panic”) is used for errors that should not be present in bug-free code.
  • The convenience functions assert and require can be used to check for conditions and throw an exception if the condition is not met.
    • assert는 Panic 타입 에러를 만듭니다.
    • Assert 는 내부 에러를 테스트하고 invariants를 체크하기 위해서만 사용되어야 합니다.
      • 적당하게 기능하는 코드는 결코 Panic을 만들어서는 안 됩니다. Panic이 발생한다는 것은 fix할 버그가 있다는 것입니다.
      • The same error is created by the compiler in certain situations as listed below.
        • A Panic exception is generated in the following situations. The error code supplied with the error data indicates the kind of panic.
          • 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.
    • require는 Error 타입 에러를 만듭니다. 실행 시점에서야 발견할 수 있는 valid 조건을 ensure하기 위해 사용됩니다.
      • This includes conditions on inputs or return values from calls to external contracts.
      • Error(string) exception (or an exception without data) is generated in the following situations:
        1. Calling require with an argument that evaluates to false.
        2. If you perform an external function call targeting a contract that contains no code.
        3. If your contract receives Ether via a public function without payable modifier (including the constructor and the fallback function).
        4. If your contract receives Ether via a public getter function.
    • For the following cases, the error data from the external call (if provided) is forwarded. This mean that it can either cause an Error or a Panic (or whatever else was given):
      • If a .transfer() fails.
      • If you call a function via a message call but it does not finish properly (i.e., it runs out of gas, has no matching function, or throws an exception itself), except when a low level operation call, send, delegatecall, callcode or staticcall is used. The low level operations never throw exceptions but indicate failures by returning false.
      • If you create a contract using the new keyword but the contract creation does not finish properly.
    • caller는 두 경우에 try/catch로 실패에 대해 대응할 수 있지만, 변경은 항상 revert 됩니다.
  • The revert function is another way to trigger exceptions from within other code blocks to flag an error and revert the current call. The function takes an optional string message containing details about the error that is passed back to the caller and it will create an Error(string) exception. The provided message can be retrieved by the caller using try/catch as shown below.
  • try 뒤에는 외부 함수 호출이나 컨트랙트 생성을 나타내는 표현식만이 올 수 있습니다. try는 함수와 같이 returns를 가질 수 있는데 이를 통해 외부 호출이 성공적으로 일어났을 때 리턴되는 값을 받을 수 있습니다.
  • 현재 솔리디티는 에러 타입에 따라 서로 다른 catch 블록을 지원합니다.
    • catch Error(string memory reason)
      • 에러 메시지를 전달하는 revert나 require에 의한 예외를 캐치하는 경우
    • catch (bytes memory lowLevelData)
      • 에러 타입에 맞는 catch 절이 없는 경우, assert에 실패한 경우, 에러 데이터가 없는 경우
    •  catch
      • 에러 데이터에 관심이 없는 경우
  • assert-style exceptions consume all gas available to the call, while require-style exceptions do not consume any gas starting from the Metropolis release.
  • The require function is evaluated just as any other function. This means that all arguments are evaluated before the function itself is executed. In particular, in require(condition, f()) the function f is executed even if condition is true.
  • The provided string is abi-encoded as if it were a call to a function Error(string). In the above example, revert(“Not enough Ether provided.”); returns the following hexadecimal as error return data:

 

assert는 로직의 실행 결과를 확인함으로 로직 실행에 문제가 없는지를 체크합니다. 이런 문제는 버그 때문에 발생할 수도 있고 오버플로어와 같이 내부적인 실행에 의해 발생할 수도 있습니다. 버그는 고쳐야 하고, 내부적인 실행 문제는 개발자가 대처할 수 없음으로 예외처리해야 합니다.

외부 컨트랙트 함수 호출에 대한 결과도 체크할 조건이 아니고 로직 실행에 문제가 없는지를 체크하는 것임으로 assert를 사용하도록 합니다.  

require는 실행 조건을 체크합니다.

About the Author
(주)뉴테크프라임 대표 김현남입니다. 저에 대해 좀 더 알기를 원하시는 분은 아래 링크를 참조하세요. http://www.umlcert.com/kimhn/

Leave a Reply

*