곰투 크립토
guide전체 47편 중 47편

이더리움 리버트 데이터 완전 정리: 오류 문자열·패닉 코드·커스텀 에러

이더리움 리버트 데이터의 ABI 인코딩, Solidity Error·Panic·커스텀 에러의 차이와 선택자를 신뢰하면 안 되는 이유를 설명해요.

GOMTU
GOMTU
크립토 리서치 · 발행 · 약 8분
공유𝕏in
이더리움 리버트 데이터 완전 정리: 오류 문자열·패닉 코드·커스텀 에러

컨트랙트 호출은 실패했고 블록 탐색기에는 빨간 상태가 뜨는데, RPC 클라이언트는 긴 16진수만 보여 준 적이 있나요? 그 바이트열이 가장 중요한 단서일 수 있어요. **이더리움 리버트 데이터(Revert Data)**는 오류 문자열, 컴파일러 패닉, 타입이 있는 커스텀 에러를 나타낼 수 있습니다. 하지만 비어 있거나 잘못 만들어졌거나 누군가 의도적으로 위조했을 수도 있어요. 이를 정확히 읽으면 블록체인 인프라 기초를 Solidity 디버깅 실무와 연결할 수 있습니다.

리버트된 호출을 취소된 배송에 비유해 볼게요. 이더리움은 배송물을 원래 자리로 되돌리지만, 배송 기사가 취소 이유를 적은 기계 판독용 메모를 붙일 수 있어요. 메모는 문제를 찾는 데 유용하지만 누가 썼는지, 내용이 정직한지까지 증명하지는 않습니다.

이 글에서는 EVM의 리버트 원리, Solidity의 대표적인 세 가지 오류 형식, 안전한 디코딩, 오류 전파와 한계를 설명해요. 교육 목적이며 투자 조언이 아닙니다(NFA). 오류 디코더가 안심되는 이름을 보여 준다는 이유만으로 자금이 움직이는 거래에 서명하거나 재시도하지 마세요.

이더리움 리버트 데이터란 무엇인가요?

광고

EVM의 REVERT 명령은 현재 호출을 중단하고 실패로 표시하며, 해당 호출의 상태 변경을 되돌립니다. 그리고 메모리에서 선택한 바이트 구간을 실패 데이터로 호출자에게 돌려줘요. EIP-140은 남은 가스를 자동으로 모두 소모하지 않고도 실행을 중단하고 이유를 전달할 수 있도록 이 동작을 도입했습니다.

따라서 리버트 데이터의 본질은 바이트예요. 문장이라는 보장은 없습니다. Solidity는 그 위에 애플리케이션 바이너리 인터페이스(ABI) 규칙을 적용합니다. 예상되는 오류 시그니처를 아는 도구는 페이로드를 이름과 타입이 있는 인자로 해석할 수 있어요.

전달 흐름은 다음과 같습니다.

  1. 피호출 컨트랙트가 계속 실행할 수 없다고 판단해 메모리 오프셋과 길이로 REVERT를 실행해요.
  2. 실패한 호출 프레임의 상태 변경과 로그가 폐기됩니다.
  3. 호출자는 실패 상태를 받고 EIP-211의 반환 데이터 버퍼에서 전체 실패 페이로드를 읽을 수 있어요.
  4. 고수준 Solidity 호출은 보통 실패를 위로 전파하지만, 저수준 call, delegatecall, staticcall은 성공 여부와 바이트를 반환합니다.
  5. RPC 시뮬레이터나 개발 도구가 이 바이트를 표시하고 디코딩을 시도할 수 있어요.

이는 트랜잭션 콜데이터와 반대 방향입니다. 콜데이터는 호출에 들어가는 요청이고, 리버트 데이터는 실패하면서 나오는 응답이에요.

Solidity의 대표적인 세 가지 오류 형식

Solidity는 주로 Error(string), Panic(uint256), 사용자 정의 커스텀 에러(Custom Error)를 만듭니다. 모두 ABI 형태의 페이로드를 쓰지만 뜻은 달라요.

형식대표적인 발생 원인페이로드 구조해석 방법
Error(string)revert("이유"), require(조건, "이유")4바이트 셀렉터 + ABI 문자열사람이 읽는 애플리케이션 조건
Panic(uint256)실패한 assert, 산술 오류, 잘못된 배열 연산4바이트 셀렉터 + 32바이트 코드컴파일러가 구분한 내부 결함 유형
커스텀 에러revert MyError(인자) 또는 지원되는 require 형식4바이트 셀렉터 + 타입별 ABI 인자작고 구조화된 애플리케이션 오류
빈 데이터·알 수 없는 바이트revert(), 예외 종료, 어셈블리, 다른 언어, 손상된 데이터공통 구조 없음문맥이 필요하며 추측 금지

오류 문자열 Error(string)

Error(string)0x08c379a0 셀렉터 뒤에 동적 문자열의 표준 ABI 인코딩을 붙여요. 오프셋, 바이트 길이, 32바이트 단위로 패딩된 UTF-8 데이터 순서입니다. "권한 없음" 같은 문장은 사람과 테스트 도구가 이해하기 쉽지만, 긴 문자열은 배포 바이트코드와 복사할 리버트 데이터 크기를 키울 수 있어요.

문자열도 신뢰할 수 없는 입력입니다. 컨트랙트가 실패하면서 Error("전송 성공")을 반환할 수도 있고, 사용자에게 도달한 문장을 더 깊은 호출 단계의 컨트랙트가 만들었을 수도 있어요.

패닉 코드 Panic(uint256)

Panic(uint256)의 셀렉터는 0x4e487b71입니다. Solidity는 버그가 없는 코드라면 발생하지 않아야 하는 조건에 이 형식을 사용해요. 숫자 코드는 실패 유형을 구분합니다. 현재 Solidity 오류 처리 문서의 대표 사례는 다음과 같아요.

코드의미
0x01assert(false) 또는 실패한 단언
0x11unchecked 밖의 산술 언더플로·오버플로
0x120으로 나누기 또는 나머지 연산
0x31빈 배열에서 .pop() 호출
0x32배열·고정 바이트·슬라이스 범위를 벗어난 접근
0x41과도한 메모리 할당
0x51초기화하지 않은 내부 함수 포인터 호출

패닉 코드는 실패 범위를 좁혀 주지만 소스 코드 줄을 직접 알려 주지는 않아요. 검증된 소스, 컴파일러 메타데이터, 실행 추적(Trace), 실행 당시의 정확한 상태가 함께 필요합니다.

커스텀 에러

커스텀 에러는 목적지가 없는 함수 호출처럼 인코딩합니다. keccak256("ErrorName(canonicalTypes)")의 앞 4바이트 뒤에 ABI 인자를 붙여요. Solidity 커스텀 에러 문서에 따르면 인자가 없는 오류는 페이로드가 4바이트면 충분합니다.

error InsufficientBalance(uint256 available, uint256 required);
 
function withdraw(uint256 amount) external {
    uint256 available = balances[msg.sender];
    if (amount > available) {
        revert InsufficientBalance(available, amount);
    }
    balances[msg.sender] = available - amount;
}

타입이 있는 필드는 문장보다 인터페이스가 번역하고 처리하기 쉬워요. 컨트랙트 ABI는 오류 이름과 인자 타입을 설명하고, NatSpec은 매번 리버트 페이로드에 긴 문장을 저장하지 않고도 의미를 문서화할 수 있습니다.

리버트 데이터를 안전하게 디코딩하는 순서

셀렉터 검색 결과를 곧바로 믿지 말고 증거 사슬을 만들어야 해요.

1. 원본 바이트와 실행 문맥을 보존하세요

체인 ID, 블록 태그나 번호, 대상 주소, 발신자, 전송 가치, 콜데이터, 원본 오류 바이트를 함께 기록합니다. latest 상태의 시뮬레이션은 이후 상태가 바뀌면 다른 결과를 낼 수 있어요. 재시도가 성공했다고 앞선 디코더가 틀렸다는 뜻도 아닙니다.

2. 내장 셀렉터부터 분류하세요

데이터가 최소 4바이트라면 Error(string)Panic(uint256) 접두부를 확인해요. 이후 ABI 구조와 전체 길이도 검증해야 합니다. 셀렉터만 같고 오프셋이나 데이터 워드가 잘린 페이로드는 정상 오류가 아니라 손상된 데이터예요.

3. 신뢰할 수 있는 ABI로 커스텀 에러를 찾으세요

정확한 체인과 구현 컨트랙트의 검증된 ABI를 사용합니다. 대상이 프록시라면 해당 블록의 호출을 처리한 구현 주소를 먼저 확인하세요. 공개 셀렉터 데이터베이스는 후보를 제시할 뿐이에요. 4바이트 충돌이 가능하므로 증명이 되지 않습니다.

4. 빈 데이터와 알 수 없는 데이터를 그대로 표현하세요

인자 없는 revert(), 가스 부족, 잘못된 옵코드, ABI 디코딩 실패, 호출 프레임 생성 전 잔액 부족, 페이로드를 노출하지 않은 인프라 모두 빈 데이터를 남길 수 있어요. 서로 같은 원인이 아닙니다. 이유를 만들어 내지 말고 “디코딩 가능한 리버트 데이터 없음”으로 표시한 뒤 트레이스나 통제된 재현을 사용하세요.

5. 복사와 디코딩 전에 크기를 제한하세요

신뢰하지 않는 피호출자가 큰 페이로드를 반환할 수 있어요. 이를 무조건 복사하고 전파하는 컨트랙트는 메모리 확장과 복사 비용을 부담합니다. 저수준 라우터와 모니터링 시스템에는 크기 정책을 두고, 필요하면 제한된 접두부나 해시를 보존하세요. 길다는 이유만으로 정상 ABI라고 가정하면 안 됩니다.

오류 전파·try/catch·저수준 호출

고수준 외부 호출은 보통 예외를 위로 전파해요. 중첩 호출이 리버트되고 아무도 처리하지 않으면 바깥 호출도 실패합니다. Solidity try/catch는 외부 호출과 컨트랙트 생성 실패를 처리할 수 있어요.

try vault.withdraw(amount) returns (uint256 received) {
    emit WithdrawalCompleted(received);
} catch Error(string memory reason) {
    emit WithdrawalFailed(reason);
} catch Panic(uint256 code) {
    emit VaultPanicked(code);
} catch (bytes memory lowLevelData) {
    emit UnknownFailure(keccak256(lowLevelData));
}

마지막 바이트 처리 구문이 중요합니다. catch Errorcatch Panic은 커스텀 에러, 빈 페이로드, 잘못된 데이터를 포괄하지 못해요. 또한 Solidity의 try/catch는 외부 호출이나 생성 표현식에 적용됩니다. 임의의 내부 코드 실패를 모두 잡는 일반 예외 처리 장치는 아니에요.

저수준 호출은 다르게 동작합니다.

(bool ok, bytes memory data) = target.call(payload);
if (!ok) {
    // 문서화된 신뢰 정책 아래서만 디코딩하거나 신중하게 전파하세요.
}

ok를 무시하면 실패한 작업이 성공한 것처럼 보일 수 있어요. data를 그대로 전파하면 진단 정보는 보존되지만, 안쪽 컨트랙트가 만든 바이트가 바깥 경계에서 나타납니다. 이 출처 문제가 안전한 오류 처리의 핵심이에요.

리버트 데이터가 신뢰할 수 있는 증거가 아닌 이유

Solidity ABI 명세는 오류 데이터를 절대 신뢰하지 말라고 명시합니다. 어떤 컨트랙트든 원하는 오류 시그니처와 일치하는 바이트를 만들 수 있어요. 오류가 직접 호출한 주소보다 여러 단계 안쪽에서 발생했을 수도 있습니다.

식당이 공급업체의 불만을 손님에게 그대로 전달하는 상황을 떠올려 보세요. 문장은 손님에게 도달하지만 식당이 직접 쓴 것은 아닐 수 있습니다. 마찬가지로 프록시, 라우터, 토큰 훅, 콜백, 악성 의존성이 바깥 컨트랙트의 선언된 커스텀 에러처럼 보이는 데이터를 만들 수 있어요.

실무 한계는 분명합니다.

  • 셀렉터는 인증된 신원이 아니에요. 오류 이름 공간에는 컨트랙트 주소나 소스 파일이 들어가지 않습니다.
  • 디코딩된 이름은 상태 증명이 아니에요. InsufficientBalance(1, 2)는 리버트한 주체가 인코딩한 주장이지 잔액 증명이 아닙니다.
  • 오류 이유는 권한이 아니에요. 신뢰하지 않는 리버트 바이트만 보고 자금을 내보내거나 권한을 주거나 특권 분기를 선택하지 마세요.
  • 시뮬레이션은 블록 포함이 아니에요. 실제 거래 전까지 상태, 순서, 가스, 호출자 문맥이 바뀔 수 있습니다.
  • 영수증은 오류 이유를 저장하지 않아요. 표준 이더리움 트랜잭션 영수증은 상태, 가스, 로그를 기록하지만 일반 리버트 데이터 필드는 없습니다. 과거 이유를 복원하려면 필요한 상태를 가진 재실행이나 추적 인프라가 보통 필요해요.

커스텀 에러는 출처를 이미 신뢰할 수 있을 때 훌륭한 진단 정보입니다. 인증 신호로는 약해요.

리스크와 흔한 실수

잘못된 ABI로 디코딩해요

프록시 업그레이드, 같은 셀렉터 충돌, 다른 체인의 주소, 오래된 결과물이 그럴듯하지만 틀린 이름을 만들 수 있어요. 구현 주소와 블록 문맥을 고정하세요.

모든 실패에 이유가 있다고 생각해요

가스 부족과 다른 예외 종료에는 유용한 페이로드가 없을 수 있습니다. 디코더는 모든 것을 “execution reverted” 하나로 줄이지 말고 빈 데이터, 손상된 데이터, 인식한 데이터, 알 수 없는 데이터를 구분해야 해요.

catch로 잡았으니 안전하다고 생각해요

catch에 도달하면 실패한 하위 호출의 상태는 되돌아가지만 호출자는 계속 실행합니다. 의도한 동작일 수도 있지만 바깥 작업이 위험한 중간 상태로 이어질 수도 있어요. 계속 실행하는 경로를 별도로 설계하고 테스트하세요.

공격자가 정한 페이로드를 제한 없이 반환해요

포워더와 프록시는 동작 보존을 위해 데이터를 전파하곤 합니다. 특히 사용자가 피호출자를 고를 수 있다면 최대 복사 크기와 메모리 비용을 검토하세요. 높은 오프셋과 비선형 확장 비용은 EVM 메모리 가이드에 정리돼 있어요.

오류 문자열에 비밀을 담아요

호출자, RPC 제공자, 디버거, 시뮬레이션 시스템이 리버트 데이터를 볼 수 있어요. 개인 키, 자격 증명, 민감한 개인정보, 내부 비밀을 오류 메시지에 넣지 마세요.

개발자 체크리스트

  • 디코더를 적용하기 전에 원본 리버트 바이트를 기록했나요?
  • 체인·블록·대상·호출자·전송 가치·콜데이터 문맥을 보존했나요?
  • 디코딩 전 전체 길이·오프셋·ABI 경계를 검증하나요?
  • 구현별로 신뢰할 수 있는 ABI에서 커스텀 에러를 찾나요?
  • Error·Panic·커스텀·빈 데이터·손상·알 수 없음 모두 처리하나요?
  • 모든 저수준 호출의 성공 플래그를 확인하나요?
  • Solidity try/catch에 마지막 저수준 catch가 있나요?
  • 신뢰하지 않는 리버트 바이트를 인증이나 권한 부여에 쓰지 않나요?
  • 신뢰하지 않는 피호출자의 오류 복사·로그 크기를 제한하나요?
  • 중첩 호출·프록시 업그레이드·가스 부족·위조 셀렉터를 테스트했나요?

자주 묻는 질문

리버트된 트랜잭션도 가스를 쓰나요?

네. REVERT는 상태 변경을 되돌리고 남은 가스를 자동으로 모두 태우지는 않지만, 리버트 전에 실행한 연산과 사용한 메모리에는 가스가 들어요. 블록에 포함된 실패 트랜잭션은 이미 수행한 작업 비용을 부담합니다.

리버트 데이터가 트랜잭션 영수증에 저장되나요?

일반적인 영수증 필드에는 저장되지 않아요. 영수증에는 성공 상태, 가스 정보, 살아남은 로그가 들어갑니다. 노드나 추적 서비스가 호출을 재실행하거나 추가 진단 정보를 보관할 수 있지만 합의 영수증과는 별개예요.

커스텀 에러는 항상 문자열보다 저렴한가요?

특히 고정 크기 인자가 적으면 작게 표현하도록 설계됐고, 인자가 없는 커스텀 에러에는 셀렉터만 필요합니다. 하지만 총비용은 배포 코드, 인자의 수와 타입, 호출 경로, 메모리 사용에 따라 달라요. 컴파일 결과와 실제 경로를 측정하세요.

try/catch가 커스텀 에러 이름을 직접 잡을 수 있나요?

현재 Solidity의 catch 구문은 Error(string), Panic(uint256), 일반 바이트 폴백을 직접 구분해요. 커스텀 또는 알 수 없는 오류에는 catch (bytes memory data)를 사용하고 안전한 ABI·출처 정책 아래서만 디코딩하세요.

두 오류가 같은 셀렉터를 가질 수 있나요?

네. 셀렉터는 4바이트뿐이어서 충돌할 수 있어요. 충돌이 없어도 어느 컨트랙트든 같은 바이트를 위조할 수 있습니다. 정확한 주소, 구현, 체인, 검증된 ABI, 트레이스 문맥을 함께 사용하세요.

주요 1차 출처

마무리

이더리움 리버트 데이터는 원시 EVM 바이트 위에 만든 구조화된 진단 채널이에요. Error(string)은 사람이 읽는 애플리케이션 실패를, Panic(uint256)은 컴파일러가 정한 결함 유형을, 커스텀 에러는 작고 타입이 있는 문맥을 전달합니다. 하지만 어느 형식도 작성자의 신원을 인증하지 않아요.

원본 데이터를 보존하고 정확한 ABI로 디코딩하며 실행 문맥을 함께 기록하세요. 비어 있거나 손상된 페이로드는 그대로 표현해야 합니다. 무엇보다 오류는 실패를 설명하는 데 사용하고, 신원 증명·자금 권한·이후 거래 결과 보장에 쓰지 마세요. 이 글은 교육 목적이며 투자 조언이 아닙니다(NFA). 대상 체인에서 직접 시험하고 잃어도 감당할 수 없는 자금을 보호하며 반드시 스스로 조사하세요(DYOR).

광고

함께 읽으면 좋은 글

관련 주제 살펴보기

GOMTU의 다른 글