솔리디티(Solidity) 사용자 정의 오류(custom error)의 되돌림 데이터(revert data)는 오류의 정규 시그니처를 Keccak-256으로 해시한 값의 앞 4바이트 선택자 뒤에 ABI로 인코딩한 인수를 붙인 형식입니다. 예를 들어 InsufficientBalance(uint256,uint256)의 선택자 뒤에는 requested와 available을 담은 32바이트 워드 두 개가 옵니다. Error(string)은 0x08c379a0, Panic(uint256)은 0x4e487b71로 시작합니다. 가스 부족·잘못된 옵코드·반환 데이터가 없는 저수준 호출 실패에서는 빈 바이트가 돌아올 수도 있습니다. 외부 계약은 어떤 되돌림 바이트든 만들어 낼 수 있으므로, 선택자가 ABI의 오류 이름과 맞는다는 사실만으로 오류의 출처나 진위를 믿어서는 안 됩니다.
첫 네 바이트는 오류 시그니처의 선택자입니다
사용자 정의 오류를 error Unauthorized(address caller)로 선언하면 정규 시그니처 문자열은 Unauthorized(address)이고, 이를 keccak256으로 해시한 결과의 첫 4바이트가 선택자입니다. 되돌릴 때는 선택자 뒤에 caller 값을 ABI 워드로 인코딩해 붙입니다. 매개변수 이름은 시그니처에 들어가지 않으며, 튜플·배열은 정규 타입 표기를 따라야 합니다.
함수 선택자와 같은 4바이트 공간을 쓰기 때문에 서로 겹칠 수 있고, 선택자 데이터베이스가 내놓는 이름 후보는 증거가 아닙니다. 검증된 소스나 배포 바이트코드에 대응하는 ABI에서 오류 정의를 가져오고, 반환 데이터의 길이·오프셋·패딩이 전체 구조와 맞는지 확인합니다.
되돌림 선택자는 오류 이름표의 후보일 뿐입니다. 어느 호출 프레임이 어떤 조건에서 그 바이트를 만들었는지는 실행 추적(trace)이 증명합니다.
JOBCOIN 해설

그림은 이 주제의 공통 개념을 단순화한 설명입니다. 아래 항목에서 이 글의 구체적인 조건과 예외를 함께 읽어보세요.
- 첫 네 바이트는 오류 시그니처의 선택자입니다
사용자 정의 오류를 error Unauthorized(address caller)로 선언하면 정규 시그니처 문자열은 Unauthorized(address)이고, 이를 keccak256으로 해시한 결과의 첫 4바이트가 선택자입니다.
- 표준 형식 세 가지와 빈 데이터를 구분합니다
require(false, string)와 revert(string)는 보통 Error(string) 선택자 0x08c379a0 뒤에 동적 문자열의 오프셋·길이·바이트를 돌려줍니다.
- 잔액 부족 예제를 워드 단위로 풀어 봅니다
error InsufficientBalance(uint256 requested,uint256 available)를 선언하고 requested=100, available=40으로 되돌렸다고 해 보겠습니다.
설명을 위한 개념도 · 실제 가격·거래 내역·통계가 아닙니다.
표준 형식 세 가지와 빈 데이터를 구분합니다
require(false, string)와 revert(string)는 보통 Error(string) 선택자 0x08c379a0 뒤에 동적 문자열의 오프셋·길이·바이트를 돌려줍니다. assert 실패나 산술 오버플로 같은 컴파일러 패닉은 Panic(uint256) 선택자 0x4e487b71과 패닉 코드를 씁니다. 사용자 정의 오류는 선언마다 고유한 선택자와 인수를 담습니다.
코드가 없는 주소에 인터페이스 호출을 했거나 가스 부족, 잘못된 옵코드, 반환 데이터 복사 전 실패 같은 경우에는 빈 바이트가 보일 수 있습니다. 빈 데이터만 보고 원인을 하나로 단정하지 말고 호출 대상 코드, 가스, 옵코드 추적, 클라이언트 오류를 서로 대조합니다.
| 형식 | 앞부분 | 다음 확인 |
|---|---|---|
| Error(string) | 0x08c379a0 | 오프셋·길이·UTF-8 바이트 |
| Panic(uint256) | 0x4e487b71 | 패닉 코드의 의미 |
| 사용자 정의 오류 | keccak(signature)[0:4] | 신뢰할 수 있는 ABI와 인수 |
| 빈 바이트 | 길이 0 | 가스·대상 코드·추적 |
| 알 수 없는 바이트 | 기타 선택자 | 충돌·전달·형식 오류 |
잔액 부족 예제를 워드 단위로 풀어 봅니다
error InsufficientBalance(uint256 requested,uint256 available)를 선언하고 requested=100, available=40으로 되돌렸다고 해 보겠습니다. 반환 데이터 앞 4바이트를 시그니처 해시와 비교하고, 남은 64바이트를 uint256 두 개로 디코딩합니다. 정적 타입이라 오프셋이 없지만, string이나 bytes 인수가 있으면 머리 부분의 오프셋을 따라가 꼬리 부분의 길이와 범위를 검사합니다.
디코더가 선택자를 떼지 않고 전체 바이트를 abi.decode에 넣으면 워드 정렬이 4바이트 밀립니다. 반대로 길이가 짧은 데이터를 억지로 잘라 내면 또 다른 예외가 생겨 원래 오류를 가립니다. 최소 길이와 모든 동적 오프셋이 반환 데이터 범위 안에 있는지 확인한 뒤에야 사람이 읽을 메시지를 만듭니다.
- 성공 플래그와 반환 데이터의 헥스 값·길이를 먼저 보존합니다.
- 4바이트 선택자를 떼어 내 알려진 세 형식으로 분류합니다.
- 배포 주소·코드 해시에 맞는 ABI를 고릅니다.
- 정적 워드와 동적 오프셋의 범위를 검증합니다.
- 실패한 프레임과 호출자의 catch·전파 동작을 추적합니다.
오류는 외부 계약에서 위조돼 올라올 수 있습니다
솔리디티 ABI 명세는 오류 데이터가 호출 사슬을 타고 올라오며, 어떤 계약이든 다른 계약의 오류처럼 보이는 바이트를 만들 수 있다고 경고합니다. 라우터가 토큰의 사용자 정의 오류를 그대로 올려 보낼 수도 있고, 악성 토큰이 라우터 자체의 오류 선택자를 돌려줄 수도 있습니다. 그래서 화면에서 ‘라우터가 Unauthorized를 일으켰다’고 단정하지 않습니다.
try/catch는 Error(string)와 Panic(uint256) 같은 알려진 형식을 나눠 받을 수 있지만, 사용자 정의 오류는 저수준 바이트로 받아 선택자를 직접 디코딩해야 할 수 있습니다. catch가 실패를 정상 대체 경로처럼 처리한다면 상태 전이가 안전한지도 검토합니다. 오류 설명은 권한 판단의 근거가 아닙니다.
eth_call 오류와 채굴된 영수증을 같은 증거로 보지 않습니다
eth_call·estimateGas는 노드가 고른 블록 상태에서 실행해 되돌림 데이터를 돌려줄 수 있습니다. 하지만 대기 상태나 보낸 주소·금액·가스 설정이 실제 거래와 다르면 결과도 달라집니다. RPC 제공자가 error.data를 감싸거나 빼 버리는 방식도 클라이언트마다 다를 수 있습니다.
채굴된 영수증의 status 0은 실패를 증명하지만, 영수증 자체에는 되돌림 데이터가 저장되지 않습니다. 거래를 같은 과거 블록 상태에서 다시 실행하거나 디버그 추적을 해야 할 수 있습니다. 상태 덮어쓰기(state override)로 재현한 결과는 원본 실행의 증거라고 부르지 않습니다. 블록 해시·클라이언트 버전과 호출 매개변수를 함께 기록합니다.
자주 묻는 질문
영수증에서 사용자 정의 오류를 바로 읽을 수 있나요?
일반 영수증에는 되돌림 데이터가 없습니다. RPC 실행 오류나 과거 상태 재실행·추적에서 얻어야 하며, 제공자마다 지원 범위가 다릅니다.
선택자 데이터베이스가 찾은 이름은 확정인가요?
아닙니다. 4바이트 충돌과 위조가 일어날 수 있으므로 해당 배포 코드에 맞는 신뢰할 수 있는 ABI와 인수 디코딩을 확인해야 합니다.
빈 되돌림 데이터는 가스 부족 때문인가요?
여러 원인 중 하나일 뿐입니다. 대상 코드 유무, 잘못된 옵코드, 가스와 호출 추적을 함께 확인해야 합니다.
더 깊이 읽기
본문에서 다룬 개념과 확인 절차를 다음 글에서 이어서 살펴보세요.



