곰투 크립토
guide전체 41편 중 32편

Solidity abi.encode vs abi.encodePacked: 차이와 충돌 위험

Solidity abi.encode와 abi.encodePacked의 구조 차이, 동적 값의 패킹 충돌 원리, 해시와 컨트랙트 호출에 맞는 안전한 선택법을 설명합니다.

GOMTU
GOMTU
크립토 리서치 · 2026년 9월 2일 · 약 5분
공유𝕏in
Solidity abi.encode vs abi.encodePacked: 차이와 충돌 위험

서로 달라 보이는 두 입력이 같은 해시를 만들었다면 Keccak-256이 깨진 걸까요? Solidity에서는 해시 함수보다 해시에 넣기 전 바이트 구성이 원인인 경우가 있습니다. 이 바이트를 권한 확인, 허용 목록, 재사용 방지에 쓴다면 작은 인코딩 선택이 보안 경계가 돼요.

이 글은 해당 문제를 블록체인 기초와 연결해 abi.encode, abi.encodePacked, abi.encodeCall, 명시적 이어 붙이기를 비교합니다. 기술 교육 자료이며 투자 조언이 아니에요. 자산을 다루는 코드는 독립적인 검토와 테스트가 필요합니다.

abi.encodeabi.encodePacked란 무엇인가요?

광고

두 함수 모두 Solidity 값을 바이트로 바꾸지만 보존하는 정보가 다릅니다.

두 물건을 배송한다고 생각해 보세요. 표준 ABI 인코딩은 각 물건을 규격 상자에 넣고 가변 길이 화물의 위치도 기록합니다. 패킹 인코딩(Packed Encoding)은 빈 공간과 메타데이터를 상당 부분 없애 하나의 촘촘한 흐름으로 만들어요. 더 작을 수 있지만 구분선이 사라지면 한 물건이 어디서 끝나고 다음 물건이 시작하는지 알기 어렵습니다.

abi.encode(...)는 표준 ABI 형식을 사용해요. 고정 값은 32바이트 워드를 차지합니다. 문자열(string), 동적 바이트(bytes), 동적 배열은 오프셋(Offset)과 길이를 기록한 뒤 실제 데이터를 배치해요. 디코더가 예상 자료형을 알면 abi.decode로 되돌릴 수 있습니다.

abi.encodePacked(...)는 Solidity의 비표준 패킹 모드예요. 32바이트보다 짧은 직접 인자는 일반 패딩 없이 이어지고, 동적 값은 길이 정보 없이 그 자리에 놓입니다. 구조체와 중첩 배열은 지원하지 않으며, 경계가 모호할 수 있어 대응하는 범용 패킹 디코더도 없습니다.

핵심 차이 비교

질문abi.encodeabi.encodePacked
표준 ABI 배치인가요?아니요, 비표준 패킹 모드
동적 길이와 오프셋을 보존하나요?아니요
abi.decode로 복원 가능한가요?일치하는 자료형이면 가능범용 복원 불가
일반 함수 호출 인자에 맞나요?아니요
선택적 바이트 구성에 작나요?상대적으로 큼대체로 작음
여러 동적 값을 인증 해시에 써도 되나요?경계를 보존모호하므로 피해야 함

바이트가 작다고 전체 가스비가 반드시 줄지는 않습니다. 인코딩 위치, 복사·해시·저장·콜데이터 전송 여부, 옵티마이저 결과에 따라 비용이 달라져요. 패킹을 만능 최적화로 보기보다 전체 연산을 측정해야 합니다.

동적 값 패킹은 왜 충돌하나요?

Solidity ABI 명세의 핵심 경고는 간단합니다. 길이 정보가 빠지므로 동적 크기 요소가 두 개 이상이면 패킹 결과가 모호해질 수 있어요.

bytes memory x = abi.encodePacked("a", "bc");
bytes memory y = abi.encodePacked("ab", "c");
 
assert(keccak256(x) == keccak256(y));

두 바이트 흐름은 모두 abc입니다. 이는 암호학적 해시 충돌이 아니라 **인코딩 충돌(Encoding Collision)**이에요. Keccak-256에 들어가기 전 입력 바이트가 이미 같습니다. 보이지 않는 경계를 사이에 두고 글자를 옮기면 다른 논리적 튜플이 같은 패킹 결과를 만들 수 있어요.

컨트랙트가 이 해시를 권한 증명으로 사용하면 문제가 구체화됩니다.

function digest(string memory role, string memory action)
    external
    pure
    returns (bytes32)
{
    return keccak256(abi.encodePacked(role, action)); // 위험한 경계
}

공격자가 두 동적 필드에 모두 영향을 줄 수 있다면, 서명자나 관리자가 의도한 것과 다른 튜플이 인증될 수 있어요. 눈에 보이는 구분자를 넣는 방법도 모든 필드에서 해당 문자를 금지하고 하나의 명확한 이스케이프 규칙을 강제하지 않으면 취약합니다. 일반적으로 표준 인코딩이 더 명료한 해결책이에요.

return keccak256(abi.encode(role, action));

공식 문서는 패킹 데이터가 서명, 인증, 무결성을 보호할 때 자료형을 고정하고 동적 인자를 최대 하나만 사용하라고 안내합니다. 특별한 이유가 없다면 abi.encode를 우선하라고 명시해요.

정적 자료형도 정확히 고정해야 해요

문자열이 없어도 패킹 인코딩은 자료형 구조를 덜 보존합니다. 직접 인자인 uint16 같은 값은 32바이트 워드가 아니라 짧은 표현을 사용해요. 명시적 형변환만으로 바이트가 달라질 수 있습니다.

abi.encodePacked(uint16(0x12))  // 0x0012
abi.encodePacked(uint256(0x12)) // 32바이트 표현

프로토콜 코드는 정확한 자료형과 순서를 정의해야 합니다. 오프체인 서비스가 숫자를 uint16, uint256, 원시 바이트 중 무엇으로 해석할지 추측하게 두면 안 돼요. 교차 언어 테스트에서는 사람이 보는 값뿐 아니라 최종 16진수 바이트를 비교하세요.

어떤 인코딩 함수를 선택해야 하나요?

구조화 값을 해시할 때

튜플 경계가 중요하면 keccak256(abi.encode(...))를 우선하세요. 서명할 구조화 메시지에는 자료형 해시와 도메인 분리를 포함하는 EIP-712 타입 데이터 서명 같은 검토된 방식을 사용합니다. 다만 EIP-712도 재사용 공격 방지를 자동 제공하지 않아요. 논스(Nonce), 만료 시각, 체인, 컨트랙트, 애플리케이션 맥락을 별도로 설계해야 합니다.

외부 컨트랙트 호출을 만들 때

자료형이 지정된 함수 포인터가 있다면 abi.encodeCall(functionPointer, (...))을 우선하세요. Solidity 문서에 따르면 전체 자료형 검사를 수행하고 셀렉터와 표준 인코딩 인자를 생성합니다. abi.encodeWithSelector, abi.encodeWithSignature도 호출을 만들 수 있지만 컴파일 시 보장이 더 약해요. 문자열 시그니처 오타는 잘못된 셀렉터를 조용히 만들 수 있습니다.

이더리움 콜데이터 가이드에서는 4바이트 셀렉터와 표준 ABI 인자가 트랜잭션 입력에 들어가는 구조를 설명합니다.

바이트나 문자열을 이어 붙일 때

목적 자체가 연결이라면 bytes.concat(...) 또는 string.concat(...)이 의도를 더 명확히 보여 줍니다. 나중에 여러 필드로 다시 나눌 데이터라면 이 경우에도 구분과 프레이밍 규칙을 정의해야 해요.

고정 폭 프로토콜 필드를 만들 때

프로토콜이 정확한 고정 폭 배치를 정의했거나 동적 꼬리가 하나뿐이라면 패킹 바이트가 적합할 수 있습니다. 각 바이트 범위를 문서화하고 길이를 검증하며 지원 자료형별 테스트 벡터를 만드세요. 다른 라이브러리가 Solidity 패킹 규칙을 그대로 재현한다고 가정하지 말고 문서와 출력을 확인해야 합니다.

보안 체크리스트

  1. 인코딩하는 모든 필드의 순서와 정확한 Solidity 자료형을 적어요.
  2. string, bytes, 동적 배열을 모두 표시합니다.
  3. 공격자가 조절할 수 있는 동적 값 두 개가 인증·무결성 해시에 들어가면 abi.encodePacked를 피하세요.
  4. 위협 모델에 따라 체인 ID, 검증 컨트랙트, 행동, 논스, 기한 같은 도메인 맥락을 넣습니다.
  5. 자료형이 있는 호출 생성에는 abi.encodeCall을 우선해요.
  6. Solidity와 오프체인 구현의 바이트 테스트 벡터를 비교합니다.
  7. 필드 경계 사이로 바이트를 옮기는 공격적 테스트를 추가해요.
  8. 권한과 자산을 다루는 코드는 독립 검토를 받습니다.

리스크와 흔한 실수

  • 인코딩 충돌과 해시 충돌을 혼동해요. 같은 패킹 바이트는 당연히 같은 해시를 만듭니다. Keccak-256이 깨진 것이 아닙니다.
  • 도메인 분리 없이 해시해요. 튜플 경계가 명확해도 범위를 빠뜨리면 다른 컨트랙트·체인·행동에서 재사용될 수 있습니다.
  • 셀렉터만으로 호출이 완성된다고 생각해요. 콜데이터에는 4바이트 셀렉터와 표준 ABI 인자가 필요합니다.
  • 패킹 바이트를 복원 가능하다고 가정해요. 동적 경계와 길이가 사라졌을 수 있습니다.
  • 측정 전에 최적화해요. 중간 바이트가 작다고 전체 가스비 절감이 증명되지는 않습니다.
  • 정상 경로 하나만 테스트해요. 빈 문자열, 최대값, 배열, 경계 이동, 교차 언어 출력을 확인해야 합니다.
  • 인코딩 정확성을 감사로 오해해요. 접근 제어, 서명 복구, 논스 소비, 만료, 업그레이드, 외부 호출은 별도 리스크예요.

스마트 컨트랙트 오류는 되돌리기 어려운 손실로 이어질 수 있습니다. 로컬 네트워크나 테스트넷에서 검증하고, 유지보수되는 도구를 사용하며, 노출 자산을 제한하고 스스로 조사하세요(DYOR).

FAQ

abi.encodePacked는 폐기됐나요?

현재 Solidity 문서에는 여전히 정의되어 있습니다. 향후 주요 버전에서 제거하자는 공개 논의가 있었지만 열린 논의는 배포된 언어 변경이 아니에요. 사용하는 컴파일러 버전의 문서를 확인하세요.

주소 두 개는 안전하게 패킹할 수 있나요?

순서가 고정된 두 address 값은 폭이 정해져 있어 동적 문자열 두 개의 길이 누락 문제는 피합니다. 그래도 권한 해시라면 도메인 분리, 고정 자료형, 오프체인 인코더와의 일치가 필요해요.

구분자를 넣으면 동적 문자열 두 개도 안전한가요?

이스케이프와 입력 검증까지 포함해 경계가 증명 가능해야 합니다. 보안에 중요한 코드라면 표준 ABI 인코딩이나 검토된 타입 데이터 표준이 더 이해하기 쉽습니다.

abi.decodePacked는 왜 없나요?

패킹 인코딩은 패딩, 오프셋, 동적 길이를 생략할 수 있습니다. 서로 다른 원본 튜플이 같은 바이트를 만들 수 있어 범용 역함수가 의도한 경계를 알아낼 수 없어요.

주요 출처

2026년 9월 2일 확인:

보존해야 할 경계에 맞춰 표현을 고르세요. 표준 ABI 인코딩은 구조와 복원 가능성을, 패킹 인코딩은 엄격한 제약 아래의 작은 바이트 구성을 우선합니다. 이 글은 교육 목적이며 투자 조언이 아닙니다(NFA).

광고

함께 읽으면 좋은 글

관련 주제 살펴보기

GOMTU의 다른 글