컨트랙트 ABI 명세
컨트랙트 ABI 명세 (Contract ABI Specification)
컨트랙트 응용 이진 인터페이스(Application Binary Interface, ABI)는 이더리움 생태계에서 블록체인 외부에서든 컨트랙트 간 상호작용이든 컨트랙트와 상호작용하는 표준 방식이에요. 데이터는 이 명세에 설명된 대로 타입에 따라 인코딩돼요. 인코딩은 자기 기술적(self-describing)이지 않아서 디코딩하려면 스키마가 필요해요. 컨트랙트 인터페이스 함수는 강타입(strongly typed)이고 컴파일 시점에 알려져 있으며 정적이라고 가정해요.
출처: 문서
본문
기본 설계 (Basic Design)
컨트랙트 응용 이진 인터페이스(ABI)는 이더리움 생태계에서 컨트랙트와 상호작용하는 표준 방식이에요. 블록체인 외부에서든 컨트랙트 간 상호작용이든 말이죠. 데이터는 이 명세에 설명된 대로 타입에 따라 인코딩돼요. 인코딩은 자기 기술적이지 않아서 디코딩하려면 스키마가 필요해요. 컨트랙트의 인터페이스 함수는 강타입이고 컴파일 시점에 알려져 있으며 정적이라고 가정해요. 또한 어떤 컨트랙트든 호출하는 컨트랙트의 인터페이스 정의를 컴파일 시점에 이용할 수 있다고 가정해요. 이 명세는 인터페이스가 동적이거나 런타임에만 알려지는 컨트랙트는 다루지 않아요. 또한 라이브러리의 ABI 명세는 약간 다르다는 점을 참고하세요.
함수 선택자 (Function Selector)
함수 호출의 콜데이터 첫 4바이트가 호출할 함수를 지정해요. 이는 함수 시그니처(signature)의 Keccak-256 해시의 첫(왼쪽, big-endian에서 높은 차수) 4바이트예요. 시그니처는 데이터 위치 지정자(데이터 위치) 없이 기본 프로토타입의 표준 표현으로 정의돼요. 즉 함수 이름과 괄호로 감싼 파라미터 타입 목록이에요. 파라미터 타입은 단일 쉼표로 구분하고 공백은 사용하지 않아요.
참고: 함수의 반환 타입은 이 시그니처의 일부가 아니에요. Solidity 함수 오버로딩에서 반환 타입은 고려되지 않아요. 그 이유는 함수 호출 해석을 컨텍스트 독립적으로 유지하기 위해서예요. 하지만 ABI의 JSON 설명은 입력과 출력을 모두 담아요.
인자 인코딩 (Argument Encoding)
다섯 번째 바이트부터 인코딩된 인자들이 이어져요. 이 인코딩은 다른 곳에서도 사용돼요. 예를 들어 반환 값과 이벤트 인자도 함수를 지정하는 4바이트 없이 같은 방식으로 인코딩돼요.
타입 (Types)
라이브러리 ABI는 non-storage 구조체처럼 아래와 다른 타입을 취할 수 있다는 점을 참고하세요. 자세한 내용은 라이브러리 선택자를 참고하세요. 다음 기본 타입들이 존재해요:
uint<M>: M 비트의 부호 없는 정수 타입.0 < M <= 256,M % 8 == 0. 예:uint32,uint8,uint256.int<M>: M 비트의 2의 보수 부호 있는 정수 타입.0 < M <= 256,M % 8 == 0.address: 해석과 언어 타입 지정을 제외하면uint160과 동등해요. 함수 선택자 계산에는address가 사용돼요.uint,int: 각각uint256,int256의 동의어예요. 함수 선택자 계산에는uint256과int256을 사용해야 해요.bool: 값 0과 1로 제한된uint8과 동등해요. 함수 선택자 계산에는bool이 사용돼요.fixed<M>x<N>: M 비트의 부호 있는 고정소수점 십진수.8 <= M <= 256,M % 8 == 0,0 < N <= 80. 값 v를v / (10 ** N)으로 나타내요.ufixed<M>x<N>:fixed<M>x<N>의 부호 없는 변형.fixed,ufixed: 각각fixed128x18,ufixed128x18의 동의어. 함수 선택자 계산에는fixed128x18과ufixed128x18을 사용해야 해요.bytes<M>: M 바이트의 이진 타입.0 < M <= 32.function: 주소(20바이트) 뒤에 함수 선택자(4바이트)가 따름.bytes24와 동일하게 인코딩돼요.
다음 (고정 크기) 배열 타입이 존재해요:
<type>[M]: 주어진 타입의 M개 요소를 가진 고정 길이 배열.M >= 0. 이 ABI 명세가 0개 요소의 고정 길이 배열을 표현할 수는 있지만, 컴파일러는 이를 지원하지 않아요.
다음 비고정 크기 타입이 존재해요:
bytes: 동적 크기 바이트 시퀀스.string: UTF-8로 인코딩된 것으로 가정한 동적 크기 유니코드 문자열.<type>[]: 주어진 타입 요소의 가변 길이 배열.
타입은 괄호 안에 쉼표로 구분해 넣어 튜플로 결합할 수 있어요:
(T1,T2,...,Tn): 타입T1, …,Tn으로 구성된 튜플.n >= 0.
튜플의 튜플, 튜플의 배열 등을 형성할 수 있어요. 0-튜플(n == 0)도 형성할 수 있어요.
Solidity 타입을 ABI 타입으로 매핑 (Mapping Solidity to ABI types)
Solidity는 튜플을 제외하고 위에 제시된 모든 타입을 같은 이름으로 지원해요. 반면 일부 Solidity 타입은 ABI에서 지원되지 않아요. 아래 표는 왼쪽 열에 ABI에 속하지 않는 Solidity 타입을, 오른쪽 열에 이를 나타내는 ABI 타입을 보여줘요.
| Solidity | ABI |
|---|---|
address payable |
address |
contract |
address |
enum |
uint8 |
| 사용자 정의 값 타입 | 그 밑바탕 값 타입 |
struct |
tuple |
경고: 버전 0.8.0 이전에는 enum이 256개보다 많은 멤버를 가질 수 있었고, 어떤 멤버의 값을 담기에 충분히 큰 가장 작은 정수 타입으로 표현됐어요.
인코딩 설계 기준 (Design Criteria for the Encoding)
인코딩은 특히 일부 인자가 중첩 배열일 때 유용한 다음 속성을 갖도록 설계됐어요:
- 값에 접근하는 데 필요한 읽기 횟수는 인자 배열 구조 안에서 값의 깊이를 넘지 않아요. 즉
a_i[k][l][r]를 가져오는 데 4번의 읽기가 필요해요. ABI의 이전 버전에서는 최악의 경우 읽기 횟수가 동적 파라미터의 총 개수에 선형적으로 비례했어요. - 변수 또는 배열 요소의 데이터는 다른 데이터와 인터리브되지 않고, 재배치 가능해요. 즉 상대 "주소"만 사용해요.
인코딩의 형식 명세 (Formal Specification of the Encoding)
정적(static) 타입과 동적(dynamic) 타입을 구분해요. 정적 타입은 제자리에 인코딩되고, 동적 타입은 현재 블록 뒤의 별도로 할당된 위치에 인코딩돼요.
정의: 다음 타입을 "동적(dynamic)"이라고 불러요:
bytesstring- 임의의 T에 대한
T[] - 임의의 동적 T와 임의의
k >= 0에 대한T[k] - 일부
1 <= i <= k에 대해Ti가 동적인 경우(T1,...,Tk)
다른 모든 타입은 "정적(static)"이라고 불러요.
정의: len(a)는 이진 문자열 a의 바이트 수예요. len(a)의 타입은 uint256으로 가정해요.
**인코딩 enc**를 ABI 타입 값에서 이진 문자열로의 매핑으로 정의하는데, enc(X)의 길이가 X의 타입이 동적인 경우에만 X의 값에 의존하도록 해요.
정의: 임의의 ABI 값 X에 대해, X의 타입이 아래와 같을 때 재귀적으로 enc(X)를 정의해요:
(T1,...,Tk)(k >= 0, 임의의 타입T1, …,Tk):enc(X) = head(X(1)) ... head(X(k)) tail(X(1)) ... tail(X(k)). 여기서X = (X(1), ..., X(k))이고,head와tail은Ti에 대해 다음과 같이 정의돼요:Ti가 정적이면head(X(i)) = enc(X(i))이고tail(X(i)) = ""(빈 문자열)이에요. 그렇지 않으면, 즉Ti가 동적이면head(X(i)) = enc(len(head(X(1)) ... head(X(k)) tail(X(1)) ... tail(X(i-1)))),tail(X(i)) = enc(X(i)). 동적 경우에head(X(i))는 head 부분들의 길이가 타입에만 의존하고 값에는 의존하지 않기 때문에 잘 정의돼요.head(X(i))의 값은enc(X)의 시작점에 대한tail(X(i))의 시작점 오프셋이에요.- 임의의 T와 k에 대한
T[k]:enc(X) = enc((X[0], ..., X[k-1])). 즉 같은 타입의 k개 요소를 가진 튜플인 것처럼 인코딩돼요. - X가 k개 요소를 가진
T[](k는uint256타입이라고 가정):enc(X) = enc(k) enc((X[0], ..., X[k-1])). 즉 같은 타입의 k개 요소(각각 고정 크기 k의 배열)를 가진 튜플인 것처럼 인코딩하고, 요소 수를 앞에 붙여요. - 길이가 k인
bytes(k는uint256타입이라고 가정):enc(X) = enc(k) pad_right(X). 즉 바이트 수를uint256으로 인코딩하고, 그 뒤에 X의 실제 값을 바이트 시퀀스로, 그리고len(enc(X))가 32의 배수가 되도록 하는 최소 개수의 0바이트를 붙여요. string:enc(X) = enc(enc_utf8(X)). 즉 X를 UTF-8로 인코딩하고 이 값을bytes타입으로 해석해 더 인코딩해요. 이 후속 인코딩에 사용되는 길이는 UTF-8 인코딩 문자열의 바이트 수이지 문자 수가 아니라는 점을 참고하세요.uint<M>:enc(X)는 길이가 32바이트가 되도록 높은 차수(왼쪽) 측에 0바이트로 패딩한 X의 big-endian 인코딩이에요.address:uint160의 경우와 같아요.int<M>:enc(X)는 길이가 32바이트가 되도록 음수 X에 대해서는 높은 차수(왼쪽) 측에0xff바이트로, 음이 아닌 X에 대해서는 0바이트로 패딩한 X의 big-endian 2의 보수 인코딩이에요.bool:uint8의 경우와 같고,true에 1,false에 0을 사용해요.fixed<M>x<N>:enc(X)는X * 10**N을int256으로 해석한enc(X * 10**N)이에요.fixed:fixed128x18의 경우와 같아요.ufixed<M>x<N>:enc(X)는X * 10**N을uint256으로 해석한enc(X * 10**N)이에요.ufixed:ufixed128x18의 경우와 같아요.bytes<M>:enc(X)는 길이가 32바이트가 되도록 뒤에 0바이트를 붙인 X의 바이트 시퀀스예요.
임의의 X에 대해 len(enc(X))가 32의 배수라는 점을 참고하세요.
함수 선택자와 인자 인코딩 (Function Selector and Argument Encoding)
종합하면, 파라미터 a_1, ..., a_n을 가진 함수 f에 대한 호출은 다음과 같이 인코딩돼요:
> function_selector(f) enc((a_1, ..., a_n))
그리고 f의 반환 값 v_1, ..., v_k는 다음과 같이 인코딩돼요:
> enc((v_1, ..., v_k))
즉 값들을 튜플로 결합해 인코딩해요.
예시 (Examples)
다음 컨트랙트가 주어졌을 때:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract Foo {
function bar(bytes3[2] memory) public pure {}
function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }
function sam(bytes memory, bool, uint[] memory) public pure {}
}
따라서 Foo 예시에서 인자 ["abc", "def"]로 bar를 호출하려면 총 68바이트를 전달하는데, 다음과 같이 나뉘어요:
0xfce353f6: 메서드 ID. 시그니처bar(bytes3[2])에서 파생돼요.0x6162630000000000000000000000000000000000000000000000000000000000: 첫 파라미터의 첫 부분,bytes3값"abc"(왼쪽 정렬).0x6465660000000000000000000000000000000000000000000000000000000000: 첫 파라미터의 두 번째 부분,bytes3값"def"(왼쪽 정렬).
총합:
0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000
파라미터 69와 true로 baz를 호출하려면 총 68바이트를 전달하는데, 다음과 같이 나뉘어요:
0xcdcd77c0: 메서드 ID. 시그니처baz(uint32,bool)의 ASCII 형태의 Keccak 해시의 첫 4바이트로 파생돼요.0x0000000000000000000000000000000000000000000000000000000000000045: 첫 파라미터, 32바이트로 패딩된uint32값69.0x0000000000000000000000000000000000000000000000000000000000000001: 두 번째 파라미터 - 32바이트로 패딩된 불리언true.
총합:
0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001
단일 bool을 반환해요. 예를 들어 false를 반환한다면 출력은 단일 바이트 배열 0x0000000000000000000000000000000000000000000000000000000000000000, 즉 단일 bool이에요.
인자 "dave", true, [1,2,3]로 sam을 호출하려면 총 292바이트를 전달하는데, 다음과 같이 나뉘어요:
0xa5643bf2: 메서드 ID. 시그니처sam(bytes,bool,uint256[])에서 파생돼요.uint가 표준 표현uint256으로 대체된다는 점을 참고하세요.0x0000000000000000000000000000000000000000000000000000000000000060: 첫 파라미터(동적 타입)의 데이터 부분 위치. 인자 블록 시작점에서 바이트 단위로 측정했고, 이 경우0x60.0x0000000000000000000000000000000000000000000000000000000000000001: 두 번째 파라미터: 불리언true.0x00000000000000000000000000000000000000000000000000000000000000a0: 세 번째 파라미터(동적 타입)의 데이터 부분 위치. 바이트 단위로 측정하고, 이 경우0xa0.0x0000000000000000000000000000000000000000000000000000000000000004: 첫 인자의 데이터 부분. 바이트 배열의 길이부터 시작하며, 이 경우 4.0x6461766500000000000000000000000000000000000000000000000000000000: 첫 인자의 내용:"dave"의 UTF-8(이 경우 ASCII와 같음) 인코딩을 오른쪽에 32바이트로 패딩한 값.0x0000000000000000000000000000000000000000000000000000000000000003: 세 번째 인자의 데이터 부분. 배열의 요소 수부터 시작하며, 이 경우 3.0x0000000000000000000000000000000000000000000000000000000000000001: 세 번째 파라미터의 첫 항목.0x0000000000000000000000000000000000000000000000000000000000000002: 세 번째 파라미터의 두 번째 항목.0x0000000000000000000000000000000000000000000000000000000000000003: 세 번째 파라미터의 세 번째 항목.
총합:
0xa5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003
동적 타입의 사용 (Use of Dynamic Types)
시그니처 f(uint256,uint32[],bytes10,bytes)와 값 (0x123, [0x456, 0x789], "1234567890", "Hello, world!")를 가진 함수 호출은 다음과 같이 인코딩돼요. 먼저 keccak("f(uint256,uint32[],bytes10,bytes)")의 첫 4바이트, 즉 0x8be65246를 취해요. 그 다음 네 인자 모두의 head 부분을 인코딩해요. 정적 타입 uint256과 bytes10은 직접 전달하려는 값이지만, 동적 타입 uint32[]과 bytes는 값 인코딩의 시작점(즉 함수 시그니처 해시를 담은 첫 4바이트는 세지 않음)에서 측정한 데이터 영역 시작점까지의 바이트 오프셋을 사용해요. 다음과 같아요:
0x0000000000000000000000000000000000000000000000000000000000000123(0x123을 32바이트로 패딩)0x0000000000000000000000000000000000000000000000000000000000000080(두 번째 파라미터 데이터 부분 시작점까지의 오프셋, 4*32바이트, 정확히 head 부분의 크기)0x3132333435363738393000000000000000000000000000000000000000000000("1234567890"을 오른쪽에 32바이트로 패딩)0x00000000000000000000000000000000000000000000000000000000000000e0(네 번째 파라미터 데이터 부분 시작점까지의 오프셋 = 첫 동적 파라미터 데이터 부분 시작점 + 첫 동적 파라미터 데이터 부분 크기 = 4*32 + 3*32, 아래 참고)
이후 첫 동적 인자 [0x456, 0x789]의 데이터 부분이 따라와요:
0x0000000000000000000000000000000000000000000000000000000000000002(배열 요소 수, 2)0x0000000000000000000000000000000000000000000000000000000000000456(첫 요소)0x0000000000000000000000000000000000000000000000000000000000000789(두 번째 요소)
마지막으로 두 번째 동적 인자 "Hello, world!"의 데이터 부분을 인코딩해요:
0x000000000000000000000000000000000000000000000000000000000000000d(요소 수, 이 경우 바이트 수: 13)0x48656c6c6f2c20776f726c642100000000000000000000000000000000000000("Hello, world!"를 오른쪽에 32바이트로 패딩)
전부 합치면 인코딩은 (명확성을 위해 함수 선택자와 각 32바이트 뒤에 줄바꿈):
0x8be65246
0000000000000000000000000000000000000000000000000000000000000123
0000000000000000000000000000000000000000000000000000000000000080
3132333435363738393000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000e0
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000456
0000000000000000000000000000000000000000000000000000000000000789
000000000000000000000000000000000000000000000000000000000000000d
48656c6c6f2c20776f726c642100000000000000000000000000000000000000
시그니처 g(uint256[][],string[])와 값 ([[1, 2], [3]], ["one", "two", "three"])를 가진 함수의 데이터를 인코딩할 때도 같은 원리를 적용하되, 인코딩의 가장 원자적인 부분부터 시작해요. 먼저 첫 루트 배열 [[1, 2], [3]]의 첫 내장 동적 배열 [1, 2]의 길이와 데이터를 인코딩해요:
0x0000000000000000000000000000000000000000000000000000000000000002(첫 배열의 요소 수, 2; 요소 자체는 1과 2)0x0000000000000000000000000000000000000000000000000000000000000001(첫 요소)0x0000000000000000000000000000000000000000000000000000000000000002(두 번째 요소)
그 다음 첫 루트 배열 [[1, 2], [3]]의 두 번째 내장 동적 배열 [3]의 길이와 데이터를 인코딩해요:
0x0000000000000000000000000000000000000000000000000000000000000001(두 번째 배열의 요소 수, 1; 요소는 3)0x0000000000000000000000000000000000000000000000000000000000000003(첫 요소)
그 다음 각각의 동적 배열 [1, 2]와 [3]에 대한 오프셋 a와 b를 찾아야 해요. 오프셋을 계산하려면 첫 루트 배열 [[1, 2], [3]]의 인코딩된 데이터를 살펴보고 각 줄을 세어보면 돼요:
0 - a - [1, 2]의 오프셋
1 - b - [3]의 오프셋
2 - 0000000000000000000000000000000000000000000000000000000000000002 - [1, 2]의 개수
3 - 0000000000000000000000000000000000000000000000000000000000000001 - 1의 인코딩
4 - 0000000000000000000000000000000000000000000000000000000000000002 - 2의 인코딩
5 - 0000000000000000000000000000000000000000000000000000000000000001 - [3]의 개수
6 - 0000000000000000000000000000000000000000000000000000000000000003 - 3의 인코딩
오프셋 a는 배열 [1, 2]의 내용 시작점, 즉 줄 2(64바이트)를 가리켜요. 따라서 a = 0x0000000000000000000000000000000000000000000000000000000000000040. 오프셋 b는 배열 [3]의 내용 시작점, 즉 줄 5(160바이트)를 가리켜요. 따라서 b = 0x00000000000000000000000000000000000000000000000000000000000000a0.
그 다음 두 번째 루트 배열의 내장 문자열들을 인코딩해요:
0x0000000000000000000000000000000000000000000000000000000000000003(단어"one"의 문자 수)0x6f6e650000000000000000000000000000000000000000000000000000000000(단어"one"의 utf8 표현)0x0000000000000000000000000000000000000000000000000000000000000003(단어"two"의 문자 수)0x74776f0000000000000000000000000000000000000000000000000000000000(단어"two"의 utf8 표현)0x0000000000000000000000000000000000000000000000000000000000000005(단어"three"의 문자 수)0x7468726565000000000000000000000000000000000000000000000000000000(단어"three"의 utf8 표현)
첫 루트 배열과 평행하게, 문자열은 동적 요소이므로 그 오프셋 c, d, e를 찾아야 해요:
0 - c - "one"의 오프셋
1 - d - "two"의 오프셋
2 - e - "three"의 오프셋
3 - 0000000000000000000000000000000000000000000000000000000000000003 - "one"의 개수
4 - 6f6e650000000000000000000000000000000000000000000000000000000000 - "one"의 인코딩
5 - 0000000000000000000000000000000000000000000000000000000000000003 - "two"의 개수
6 - 74776f0000000000000000000000000000000000000000000000000000000000 - "two"의 인코딩
7 - 0000000000000000000000000000000000000000000000000000000000000005 - "three"의 개수
8 - 7468726565000000000000000000000000000000000000000000000000000000 - "three"의 인코딩
오프셋 c는 문자열 "one"의 내용 시작점, 즉 줄 3(96바이트)을 가리켜요. 따라서 c = 0x0000000000000000000000000000000000000000000000000000000000000060. 오프셋 d는 문자열 "two"의 내용 시작점, 즉 줄 5(160바이트)를 가리켜요. 따라서 d = 0x00000000000000000000000000000000000000000000000000000000000000a0. 오프셋 e는 문자열 "three"의 내용 시작점, 즉 줄 7(224바이트)을 가리켜요. 따라서 e = 0x00000000000000000000000000000000000000000000000000000000000000e0.
루트 배열의 내장 요소 인코딩은 서로 의존하지 않으며, 시그니처 g(string[],uint256[][])를 가진 함수에 대해서도 동일한 인코딩을 가진다는 점을 참고하세요.
그 다음 첫 루트 배열의 길이를 인코딩해요:
0x0000000000000000000000000000000000000000000000000000000000000002(첫 루트 배열의 요소 수, 2; 요소 자체는[1, 2]와[3])
그 다음 두 번째 루트 배열의 길이를 인코딩해요:
0x0000000000000000000000000000000000000000000000000000000000000003(두 번째 루트 배열의 문자열 수, 3; 문자열 자체는"one","two","three")
마지막으로 각각의 루트 동적 배열 [[1, 2], [3]]와 ["one", "two", "three"]에 대한 오프셋 f와 g를 찾고, 부분들을 올바른 순서로 조립해요:
0x2289b18c - 함수 시그니처
0 - f - [[1, 2], [3]]의 오프셋
1 - g - ["one", "two", "three"]의 오프셋
2 - 0000000000000000000000000000000000000000000000000000000000000002 - [[1, 2], [3]]의 개수
3 - 0000000000000000000000000000000000000000000000000000000000000040 - [1, 2]의 오프셋
4 - 00000000000000000000000000000000000000000000000000000000000000a0 - [3]의 오프셋
5 - 0000000000000000000000000000000000000000000000000000000000000002 - [1, 2]의 개수
6 - 0000000000000000000000000000000000000000000000000000000000000001 - 1의 인코딩
7 - 0000000000000000000000000000000000000000000000000000000000000002 - 2의 인코딩
8 - 0000000000000000000000000000000000000000000000000000000000000001 - [3]의 개수
9 - 0000000000000000000000000000000000000000000000000000000000000003 - 3의 인코딩
10 - 0000000000000000000000000000000000000000000000000000000000000003 - ["one", "two", "three"]의 개수
11 - 0000000000000000000000000000000000000000000000000000000000000060 - "one"의 오프셋
12 - 00000000000000000000000000000000000000000000000000000000000000a0 - "two"의 오프셋
13 - 00000000000000000000000000000000000000000000000000000000000000e0 - "three"의 오프셋
14 - 0000000000000000000000000000000000000000000000000000000000000003 - "one"의 개수
15 - 6f6e650000000000000000000000000000000000000000000000000000000000 - "one"의 인코딩
16 - 0000000000000000000000000000000000000000000000000000000000000003 - "two"의 개수
17 - 74776f0000000000000000000000000000000000000000000000000000000000 - "two"의 인코딩
18 - 0000000000000000000000000000000000000000000000000000000000000005 - "three"의 개수
19 - 7468726565000000000000000000000000000000000000000000000000000000 - "three"의 인코딩
오프셋 f는 배열 [[1, 2], [3]]의 내용 시작점, 즉 줄 2(64바이트)를 가리켜요. 따라서 f = 0x0000000000000000000000000000000000000000000000000000000000000040. 오프셋 g는 배열 ["one", "two", "three"]의 내용 시작점, 즉 줄 10(320바이트)을 가리켜요. 따라서 g = 0x0000000000000000000000000000000000000000000000000000000000000140.
이벤트 (Events)
이벤트는 이더리움 로깅/이벤트 감시 프로토콜의 추상화예요. 로그 항목은 컨트랙트의 주소, 최대 4개의 토픽 시리즈, 그리고 임의 길이의 이진 데이터를 제공해요. 이벤트는 기존 함수 ABI를 활용해 (인터페이스 명세와 함께) 이를 적절히 타입화된 구조로 해석해요.
이벤트 이름과 이벤트 파라미터 시리즈가 주어지면, 이를 두 개의 하위 시리즈로 나눠요: 인덱싱된 것과 아닌 것. 인덱싱된 것(비-익명 이벤트는 최대 3개, 익명 이벤트는 4개)은 이벤트 시그니처의 Keccak 해시와 함께 로그 항목의 토픽을 형성해요. 인덱싱되지 않은 것은 이벤트의 바이트 배열을 형성해요. 실질적으로 이 ABI를 사용하는 로그 항목은 다음과 같이 설명돼요:
address: 컨트랙트의 주소(이더리움에 의해 내재적으로 제공).topics[0]:keccak(EVENT_NAME+"("+EVENT_ARGS.map(canonical_type_of).join(",")+")"). (canonical_type_of는 주어진 인자의 표준 타입을 반환하는 함수예요. 예를 들어uint indexed foo에 대해uint256을 반환해요.) 이 값은 이벤트가anonymous로 선언되지 않은 경우에만topics[0]에 존재해요.topics[n]: 이벤트가anonymous로 선언되지 않은 경우abi_encode(EVENT_INDEXED_ARGS[n - 1]), 선언된 경우abi_encode(EVENT_INDEXED_ARGS[n]). (EVENT_INDEXED_ARGS는 인덱싱된EVENT_ARGS의 시리즈예요.)data:EVENT_NON_INDEXED_ARGS의 ABI 인코딩. (EVENT_NON_INDEXED_ARGS는 인덱싱되지 않은EVENT_ARGS의 시리즈이고,abi_encode는 앞서 설명한 대로 함수에서 타입화된 값 시리즈를 반환하는 데 사용되는 ABI 인코딩 함수예요.)
길이가 최대 32바이트인 모든 타입에 대해 EVENT_INDEXED_ARGS 배열은 일반 ABI 인코딩과 마찬가지로 값을 직접 담되, 32바이트로 패딩하거나 (부호 있는 정수의 경우) 부호 확장해요. 그러나 모든 "복잡한" 타입이나 동적 길이 타입(모든 배열, string, bytes, struct 포함)에 대해 EVENT_INDEXED_ARGS는 인코딩된 값을 직접 담는 대신 특수한 제자리 인코딩 값의 Keccak 해시를 담아요(인덱싱된 이벤트 파라미터의 인코딩 참고). 이렇게 하면 애플리케이션은 동적 길이 타입의 값을 효율적으로 질의할 수 있지만(인코딩된 값의 해시를 토픽으로 설정), 질의하지 않은 인덱싱된 값은 디코딩할 수 없어요. 동적 길이 타입의 경우 애플리케이션 개발자는 미리 정해진 값의 빠른 검색(인자가 인덱싱된 경우)과 임의 값의 가독성(인자가 인덱싱되지 않아야 함) 사이에서 트레이드오프를 마주해요. 개발자는 같은 값을 담도록 의도된 두 인자(하나는 인덱싱, 하나는 비인덱싱)를 가진 이벤트를 정의해 이 트레이드오프를 극복하고 효율적 검색과 임의 가독성을 모두 달성할 수 있어요.
에러 (Errors)
컨트랙트 내부에서 실패가 발생하면, 컨트랙트는 특수 opcode를 사용해 실행을 중단하고 모든 상태 변경을 되돌릴 수 있어요. 이러한 효과 외에도 호출자에게 설명 데이터를 반환할 수 있어요. 이 설명 데이터는 함수 호출의 데이터와 같은 방식으로 인코딩된 에러와 그 인자예요.
예를 들어, transfer 함수가 항상 "잔액 부족"의 커스텀 에러로 되돌리는 다음 컨트랙트를 고려해봐요:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract TestToken {
error InsufficientBalance(uint256 available, uint256 required);
function transfer(address /*to*/, uint amount) public pure {
revert InsufficientBalance(0, amount);
}
}
반환 데이터는 함수 InsufficientBalance(uint256,uint256)에 대한 함수 호출 InsufficientBalance(0, amount)와 같은 방식으로 인코딩돼요. 즉 0xcf479181, uint256(0), uint256(amount)이에요. 에러 선택자 0x00000000과 0xffffffff는 향후 사용을 위해 예약되어 있어요.
경고: 에러 데이터를 절대 신뢰하지 마세요. 에러 데이터는 기본적으로 외부 호출 체인을 통해 위로 전파되는데, 이는 컨트랙트가 직접 호출하는 어떤 컨트랙트에도 정의되지 않은 에러를 받을 수 있다는 뜻이에요. 게다가 어떤 컨트랙트든 에러가 어디에도 정의되지 않았더라도 에러 시그니처와 일치하는 데이터를 반환해 어떤 에러든 위조할 수 있어요.
JSON
컨트랙트 인터페이스의 JSON 형식은 함수, 이벤트, 에러 설명의 배열로 주어져요. 함수 설명은 다음 필드를 가진 JSON 객체예요:
type:"function","constructor","receive"("receive Ether" 함수) 또는"fallback"("기본" 함수).name: 함수의 이름.inputs: 객체 배열. 각각 다음을 포함해요:name: 파라미터의 이름.type: 파라미터의 표준 타입(아래 참고).components: 튜플 타입에 사용(아래 참고).outputs:inputs와 유사한 객체 배열.stateMutability: 다음 값 중 하나를 가진 문자열:pure(블록체인 상태를 읽지 않도록 명시),view(블록체인 상태를 수정하지 않도록 명시),nonpayable(함수가 Ether를 받지 않음 - 기본값),payable(함수가 Ether를 받음).
constructor, receive, fallback은 name이나 outputs를 절대 갖지 않아요. receive와 fallback은 inputs도 갖지 않아요.
참고: non-payable 함수에 0이 아닌 Ether를 보내면 트랜잭션이 되돌아가요. 참고: 상태 가변성
nonpayable은 Solidity에서 상태 가변성 수정자를 전혀 지정하지 않음으로써 반영돼요.
이벤트 설명은 상당히 유사한 필드를 가진 JSON 객체예요:
type: 항상"event".name: 이벤트의 이름.inputs: 객체 배열. 각각 다음을 포함해요:name: 파라미터의 이름.type: 파라미터의 표준 타입(아래 참고).components: 튜플 타입에 사용(아래 참고).indexed: 필드가 로그의 토픽의 일부이면true, 로그의 데이터 세그먼트 중 하나이면false.anonymous: 이벤트가anonymous로 선언된 경우true.
에러는 다음과 같아요:
type: 항상"error".name: 에러의 이름.inputs: 객체 배열. 각각 다음을 포함해요:name: 파라미터의 이름.type: 파라미터의 표준 타입(아래 참고).components: 튜플 타입에 사용(아래 참고).
참고: JSON 배열에는 같은 이름을 가진, 심지어 동일한 시그니처를 가진 여러 에러가 있을 수 있어요. 예를 들어 에러가 스마트 컨트랙트의 다른 파일에서 유래하거나 다른 스마트 컨트랙트에서 참조되는 경우예요. ABI에서는 에러 자체의 이름만 관련이 있고 어디에 정의됐는지는 관련이 없어요.
예를 들어,
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract Test {
constructor() { b = hex"12345678901234567890123456789012"; }
event Event(uint indexed a, bytes32 b);
event Event2(uint indexed a, bytes32 b);
error InsufficientBalance(uint256 available, uint256 required);
function foo(uint a) public { emit Event(a, b); }
bytes32 b;
}
는 다음 JSON이 됩니다:
[{
"type":"error",
"inputs": [{"name":"available","type":"uint256"},{"name":"required","type":"uint256"}],
"name":"InsufficientBalance"
}, {
"type":"event",
"inputs": [{"name":"a","type":"uint256","indexed":true},{"name":"b","type":"bytes32","indexed":false}],
"name":"Event"
}, {
"type":"event",
"inputs": [{"name":"a","type":"uint256","indexed":true},{"name":"b","type":"bytes32","indexed":false}],
"name":"Event2"
}, {
"type":"function",
"inputs": [{"name":"a","type":"uint256"}],
"name":"foo",
"outputs": []
}]
튜플 타입 다루기 (Handling tuple types)
이름은 의도적으로 ABI 인코딩의 일부가 아니지만, 최종 사용자에게 표시하기 위해 JSON에 포함시키는 것은 많은 의미가 있어요. 구조는 다음과 같은 방식으로 중첩돼요: name, type, 그리고 잠재적으로 components 멤버를 가진 객체가 타입화된 변수를 설명해요. 튜플 타입에 도달할 때까지 표준 타입이 결정되고, 그 지점까지의 문자열 설명이 단어 tuple과 함께 type 접두사에 저장돼요. 즉 tuple 다음에 정수 k를 가진 []와 [k]의 시퀀스가 오는 형태예요. 그런 다음 튜플의 components는 배열 타입이고 최상위 객체와 같은 구조를 가지며, indexed는 허용되지 않는 components 멤버에 저장돼요.
예를 들어, 코드
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.5 <0.9.0;
pragma abicoder v2;
contract Test {
struct S { uint a; uint[] b; T[] c; }
struct T { uint x; uint y; }
function f(S memory, T memory, uint) public pure {}
function g() public pure returns (S memory, T memory, uint) {}
}
는 다음 JSON이 됩니다:
[
{
"name": "f",
"type": "function",
"inputs": [
{
"name": "s",
"type": "tuple",
"components": [
{
"name": "a",
"type": "uint256"
},
{
"name": "b",
"type": "uint256[]"
},
{
"name": "c",
"type": "tuple[]",
"components": [
{
"name": "x",
"type": "uint256"
},
{
"name": "y",
"type": "uint256"
}
]
}
]
},
{
"name": "t",
"type": "tuple",
"components": [
{
"name": "x",
"type": "uint256"
},
{
"name": "y",
"type": "uint256"
}
]
},
{
"name": "a",
"type": "uint256"
}
],
"outputs": []
}
]
엄격 인코딩 모드 (Strict Encoding Mode)
엄격 인코딩 모드는 위 형식 명세에 정의된 것과 정확히 동일한 인코딩으로 이끄는 모드예요. 즉 오프셋은 데이터 영역에서 겹침을 만들지 않으면서 가능한 한 작아야 하므로, 간격이 허용되지 않아요. 일반적으로 ABI 디코더는 오프셋 포인터를 따라가는 직관적인 방식으로 작성되지만, 일부 디코더는 엄격 모드를 강제할 수 있어요. Solidity ABI 디코더는 현재 엄격 모드를 강제하지 않지만, 인코더는 항상 엄격 모드로 데이터를 생성해요.
비표준 패킹 모드 (Non-standard Packed Mode)
abi.encodePacked()를 통해 Solidity는 다음을 지원하는 비표준 패킹 모드를 제공해요:
- 32바이트보다 짧은 타입은 패딩이나 부호 확장 없이 직접 연결돼요.
- 동적 타입은 길이 없이 제자리에 인코딩돼요.
- 배열 요소는 패딩되지만 여전히 제자리에 인코딩돼요.
게다가 구조체와 중첩 배열은 지원되지 않아요. 예를 들어 int16(-1), bytes1(0x42), uint16(0x03), string("Hello, world!")의 인코딩은 다음과 같습니다:
0xffff42000348656c6c6f2c20776f726c6421
^^^^ int16(-1)
^^ bytes1(0x42)
^^^^ uint16(0x03)
^^^^^^^^^^^^^^^^^^^^^^^^^^ string("Hello, world!") without a length field
더 구체적으로:
- 인코딩 중에 모든 것이 제자리에 인코딩돼요. 이는 ABI 인코딩에서와 같은 head/tail 구분이 없고, 배열의 길이도 인코딩되지 않는다는 뜻이에요.
abi.encodePacked의 직접 인자는 배열(또는string이나bytes)이 아닌 한 패딩 없이 인코딩돼요.- 배열의 인코딩은 요소 인코딩을 패딩과 함께 연결한 것이에요.
string,bytes,uint[]같은 동적 크기 타입은 길이 필드 없이 인코딩돼요.string이나bytes의 인코딩은 배열이나 구조체의 일부가 아닌 한 끝에 패딩을 적용하지 않아요(그 경우 32바이트의 배수로 패딩돼요).
일반적으로 길이 필드가 없기 때문에 동적 크기 요소가 두 개 이상 있으면 인코딩이 모호해져요. 패딩이 필요하면 명시적 타입 변환을 사용할 수 있어요: abi.encodePacked(uint16(0x12)) == hex"0012". 패킹 인코딩은 함수 호출에 사용되지 않기 때문에 함수 선택자를 앞에 붙이는 특별한 지원이 없어요. 인코딩이 모호하므로 디코딩 함수도 없어요.
경고:
keccak256(abi.encodePacked(a, b))를 사용하고a와b모두 동적 타입이라면,a의 일부를b로, 그 반대로 옮겨 해시 값에 충돌을 쉽게 만들 수 있어요. 더 구체적으로abi.encodePacked("a", "bc") == abi.encodePacked("ab", "c")예요. 시그니처, 인증, 데이터 무결성에abi.encodePacked를 사용한다면 항상 같은 타입을 사용하고 그 중 최대 하나만 동적임을 확인하세요. 강력한 이유가 없다면abi.encode를 선호하세요.
인덱싱된 이벤트 파라미터의 인코딩 (Encoding of Indexed Event Parameters)
값 타입이 아닌 인덱싱된 이벤트 파라미터, 즉 배열과 구조체는 직접 저장되지 않고 대신 인코딩의 Keccak-256 해시가 저장돼요. 이 인코딩은 다음과 같이 정의돼요:
bytes와string값의 인코딩은 패딩이나 길이 접두사 없이 문자열 내용 그대로예요.- 구조체의 인코딩은 멤버 인코딩의 연결이며, 항상 32바이트의 배수로 패딩돼요(
bytes와string조차도). - 배열(동적 및 정적 크기 모두)의 인코딩은 요소 인코딩의 연결이며, 항상 32바이트의 배수로 패딩되고(
bytes와string조차도) 길이 접두사가 없어요.
위에서 평소와 같이 음수는 0 패딩이 아닌 부호 확장으로 패딩돼요. bytesNN 타입은 오른쪽으로, uintNN/intNN은 왼쪽으로 패딩돼요.
경고: 구조체의 인코딩은 동적 크기 배열을 두 개 이상 포함하면 모호해져요. 그 때문에 항상 이벤트 데이터를 다시 확인하고 인덱싱된 파라미터만 기반으로 한 검색 결과에 의존하지 마세요.