ERC-1271 검증은 왜 magic value만 보면 안 될까: Gnosis Pay 사고로 보는 fail-closed 서명 검증
스마트컨트랙트 지갑이 “이 서명이 유효하다”는 4바이트를 돌려줬다면, 우리는 정말 그 호출을 믿어도 될까?
ERC-1271 검증은 왜 magic value만 보면 안 될까: Gnosis Pay 사고로 보는 fail-closed 서명 검증
- 카테고리: blockchain_dev
- 예상 읽기 시간: 10분
- 오늘의 질문: 스마트컨트랙트 지갑이 “이 서명이 유효하다”는 4바이트를 돌려줬다면, 우리는 정말 그 호출을 믿어도 될까?
- 핵심 출처:
- Gnosis - Post-Mortem: Gnosis Pay Vulnerability Exploit - 2026-07-04, 확인일 2026-07-07
- Gnosis Guild Engineering - ERC-1271 Authorization Bypass in Zodiac Roles/Delay - 2026-06-19, 확인일 2026-07-07
- CertiK - GnosisPay Incident Analysis - 2026-06-04, 확인일 2026-07-07
- Zodiac commit 9a9e380 - 2023-10-01, 확인일 2026-07-07
- Zodiac v5.0.0 PR #178 - 2026-06-18, 확인일 2026-07-07
1. 왜 지금 봐야 하나
2026년 7월 4일 Gnosis는 Gnosis Pay 취약점 exploit에 대한 공식 post-mortem을 공개했다. 사건 자체는 6월 1일 발생했다. 공격자는 Gnosis Pay card safe 인프라가 사용하는 Zodiac Delay Module과 Roles Module의 서명 검증 취약점을 이용했고, Gnosis 발표 기준 약 150만 달러가 인출됐으며 약 30만 달러는 접근 불가능한 상태가 됐다. Gnosis는 손실을 흡수했고 사용자 잔액을 복구했다고 밝혔다.
이 사건을 “어떤 프로토콜이 해킹됐다”로만 읽으면 남는 것이 적다. 개발자에게 더 중요한 질문은 이것이다.
컨트랙트 서명 검증에서
0x1626ba7e라는 ERC-1271 magic value를 봤다면, 그 값이 성공한 호출의 반환값인지까지 확인했는가?
Gnosis와 Gnosis Guild Engineering의 설명에 따르면 취약점의 핵심은 좁고 구체적이다. Zodiac 모듈의 ERC-1271 contract-signature check가 staticcall의 성공 여부를 확인하지 않고, 반환 또는 revert data 앞부분의 magic value만 보고 서명을 유효하다고 판단했다. 공격자는 의도적으로 실패하는 컨트랙트를 만들면서도 “유효한 서명”처럼 보이는 4바이트를 돌려주도록 구성했다.
오늘 가져갈 한 문장은 이것이다.
외부 컨트랙트가 “valid”라고 말했는지보다 먼저, 그 질문이 정상적으로 끝났는지를 확인해야 한다.
2. 핵심 개념
ERC-1271은 EOA가 아니라 스마트컨트랙트가 서명 주체가 될 수 있게 해주는 표준이다. 일반 ECDSA 서명은 ecrecover로 주소를 복구해 signer와 비교한다. 하지만 Safe, account abstraction wallet, module-based wallet처럼 “계정 자체가 컨트랙트”인 경우에는 서명 유효성 판단이 계정 내부 로직에 있다. 그래서 검증자는 signer contract에 물어본다.
bytes4 constant EIP1271_MAGIC_VALUE = 0x1626ba7e;
function isValidSignature(bytes32 hash, bytes memory signature)
external
view
returns (bytes4 magicValue);
안전한 직관은 단순하다.
1. signer 컨트랙트에 staticcall로 질문한다.
2. 호출이 성공했는지 확인한다.
3. 성공한 호출의 반환값이 magic value인지 확인한다.
4. 둘 다 참일 때만 서명을 유효하다고 본다.
문제는 2번을 빼면 생긴다. Solidity의 low-level call 계열은 보통 (bool success, bytes memory returnData)를 돌려준다. 호출이 revert되어도 상위 컨트랙트가 자동으로 revert되지 않을 수 있다. 개발자가 success를 버리고 returnData만 읽으면, “실패한 호출이 남긴 데이터”와 “성공한 호출의 반환값”을 같은 것으로 착각할 수 있다.
Gnosis post-mortem이 제시한 취약한 형태는 다음과 같은 패턴이다.
// 취약한 직관: call success를 버린다.
(, bytes memory returnData) = signer.staticcall(
abi.encodeWithSelector(isValidSignature, hash, signature)
);
return bytes4(returnData) == EIP1271_MAGIC_VALUE;
수정된 직관은 한 줄 차이다.
(bool success, bytes memory returnData) = signer.staticcall(...);
return success && bytes4(returnData) == EIP1271_MAGIC_VALUE;
이 한 줄이 access control의 fail-open과 fail-closed를 가른다. fail-open은 애매하거나 실패한 상태를 허용으로 해석하는 것이다. fail-closed는 호출 실패, 짧은 반환값, 알 수 없는 selector, 예상 밖 fallback을 모두 거부로 해석하는 것이다.
3. 최신 이슈와 연결
Gnosis Guild Engineering의 6월 19일 post-mortem은 이 취약점을 “ERC-1271 Authorization Bypass”로 분류하고 severity를 Critical로 표시했다. 영향을 받은 컴포넌트는 Zodiac Delay Modifier v1.1.0과 Roles Modifier v2.1.0이었다. exploit 가능한 조건도 중요하다. 단순히 모든 Safe나 모든 Zodiac 모듈이 위험했던 것이 아니라, 영향을 받은 모듈이 켜져 있고 Safe CompatibilityFallbackHandler가 Delay의 module 또는 Roles의 role member로 배정된 구성이 필요했다.
CertiK 분석은 공격 흐름을 더 낮은 레벨에서 보여준다. 공격자는 Delay.execTransactionFromModule() 호출에서 attacker-controlled msg.data 끝부분의 r,s,v 값을 조작했다. 첫 번째 signer처럼 해석된 주소는 Biconomy Safe였고, Safe 검증 경로를 거친 뒤 두 번째 signer로 공격자 컨트랙트가 등장했다. 이 공격자 컨트랙트는 ERC-1271 magic value를 돌려주도록 준비됐다. 핵심은 그 staticcall이 revert했는데도, 상위 모듈이 success를 확인하지 않아 magic value를 유효한 승인처럼 받아들였다는 점이다.
Gnosis 공식 글은 사건 대응도 기록한다. NOCA의 모니터링으로 공격이 빠르게 감지됐고, Gnosis는 root cause를 2시간 안에 식별했다고 설명한다. card transaction processing, authorization system, 신규 onboarding을 멈추고, bridge validator를 통해 Gnosis Chain bridge를 pause했으며, 외부 프로젝트에도 같은 취약점 가능성을 알렸다. 이후 ChainSecurity의 focused review와 단계적 복구를 거쳐 대부분의 사용자 서비스를 재개했다.
여기서 개발자가 읽어야 할 최신성은 “Gnosis Pay만의 문제”가 아니다. ERC-1271, Safe fallback handler, module permission, low-level call은 account abstraction과 smart wallet 생태계에서 점점 더 흔해지는 조합이다. EIP-7702, ERC-4337, session key, delegated account 같은 흐름이 커질수록 “서명 검증을 다른 컨트랙트에 위임한다”는 패턴도 늘어난다. 그만큼 return value와 call success를 헷갈리는 작은 버그가 access control 전체를 우회할 수 있다.
4. 개발자 관점 해석
1) magic value는 인증서가 아니라 응답 형식이다
0x1626ba7e는 “이 호출이 성공했고 이 signer가 승인했다”는 종합 인증서가 아니다. 표준이 정한 응답 형식일 뿐이다. 그 값이 의미를 가지려면 호출 대상, 호출 성공, 반환 길이, 반환값 decode가 모두 맞아야 한다.
따라서 ERC-1271 wrapper를 만들 때는 다음을 한 덩어리로 봐야 한다.
target is contract?
+ staticcall success?
+ returnData length >= 4?
+ bytes4(returnData) == 0x1626ba7e?
+ hash/signature domain이 기대한 컨텍스트인가?
이 중 하나라도 false면 “유효하지 않음”이어야 한다.
2) low-level call은 오류를 값으로 돌려준다
Solidity에서 signer.staticcall(...)은 외부 함수 호출처럼 실패를 자동 전파하지 않는다. 실패 여부를 success라는 값으로 돌려준다. 이 특징은 proxy, fallback, optional interface, try/catch-like flow를 만들 때 유용하다. 하지만 access control에서는 위험하다. 실패가 값으로 내려온다면, 그 값을 반드시 정책으로 처리해야 한다.
보안 리뷰에서 call, staticcall, delegatecall을 보면 항상 이렇게 물어야 한다.
success를 확인하는가?
실패 시 revert하는가, false를 반환하는가?
returnData가 짧을 때 bytes4/abi.decode가 어떻게 동작하는가?
revert data를 정상 return data처럼 읽는 경로가 있는가?
3) fallback handler는 보안 경계다
이번 exploit 조건에는 Safe CompatibilityFallbackHandler가 들어간다. fallback handler는 “없는 함수를 받는 편의 기능”처럼 보이지만, 실제로는 계정이 외부 메시지에 어떻게 응답하는지를 바꾸는 확장 지점이다. ERC-1271도 결국 특정 selector에 대한 응답이다. 어떤 fallback handler가 연결되어 있고, 그 handler가 어떤 내부 서명 검증으로 이어지는지는 access control의 일부다.
스마트 지갑을 제품에 붙일 때 “Safe를 쓴다”는 말만으로 충분하지 않다. Safe version, fallback handler, module, guard, owner, threshold, role member, delegate permission을 모두 계정의 effective authority로 봐야 한다.
4) patch는 repository가 아니라 production bytecode에 도착해야 한다
Gnosis 공식 글은 취약점이 Zodiac code version 3.4.0, 2023년 10월 30일 릴리스에서 signature support가 추가될 때 들어갔다고 설명한다. Gnosis Guild는 이후 affected module의 patched version으로 Delay v1.1.1, Roles v2.1.1을 제시했고, Zodiac v5.0.0 PR은 mastercopy와 versioned artifact 관리를 강화하는 방향을 보여준다.
개발자에게 중요한 것은 “GitHub에 fix가 있다”와 “내 온체인 인스턴스가 fix를 실행한다”는 완전히 다른 말이라는 점이다. 컨트랙트는 배포된 bytecode가 현실이다. 특히 모듈·라이브러리·mastercopy·proxy·factory가 섞인 생태계에서는 patch propagation inventory가 없으면 같은 취약점이 오래 살아남을 수 있다.
5. 내 프로젝트에 적용할 체크포인트
스마트컨트랙트 지갑, Safe module, role manager, bridge executor, account abstraction bundler를 만들거나 통합한다면 아래를 점검하자.
- ERC-1271 wrapper:
success && returnData.length >= 4 && bytes4(returnData) == MAGIC_VALUE를 명시적으로 확인하는가? - 실패 정책: signer 호출 실패, revert, out-of-gas, malformed return은 모두 invalid로 닫히는가?
- calldata 신뢰 경계: signature를
msg.data끝에서 파싱한다면, 앞부분 parser가 소비하지 않은 attacker-controlled tail을 어떻게 제한하는가? - fallback handler inventory: Safe나 smart wallet의 fallback handler, module, guard, owner threshold가 문서화되어 있는가?
- nested signature 경로: contract signer가 다시 다른 contract signer를 부를 때 recursion, depth, gas, success handling 정책이 있는가?
- domain separation: hash가 chain id, module address, nonce, target, value, operation, calldata를 충분히 묶는가?
- dependency lineage: production bytecode가 어떤 package와 commit에서 컴파일됐는지 추적 가능한가?
- patch propagation: upstream security fix가 나오면 “라이브러리 업데이트”가 아니라 “배포된 인스턴스 영향도 조사” 티켓이 자동으로 생기는가?
- monitoring: delay queue, role execution, module transaction에서 비정상 signer contract, revert-but-magic 응답, 대량 queueing을 감지하는가?
6. 오늘 10분 액션
오늘은 내가 쓰는 ERC-1271 검증 코드를 하나 찾아 아래 미니 테스트 이름을 추가해보자. 실제 구현은 나중에 해도 좋다. 이름을 붙이면 보안 조건이 코드 리뷰 언어가 된다.
test_erc1271_revertingSignerWithMagicValue_isInvalid
test_erc1271_emptyReturnData_isInvalid
test_erc1271_shortReturnData_isInvalid
test_erc1271_wrongMagicValue_isInvalid
test_erc1271_successAndMagicValue_isValid
test_moduleCalldataTail_cannotForgeSigner
test_safeFallbackHandler_isPartOfAuthorityInventory
그리고 wrapper를 이렇게 생긴 함수 하나로 모아라.
function _isValid1271(address signer, bytes32 hash, bytes memory sig)
internal
view
returns (bool)
{
if (signer.code.length == 0) return false;
(bool success, bytes memory ret) = signer.staticcall(
abi.encodeWithSelector(IERC1271.isValidSignature.selector, hash, sig)
);
return success && ret.length >= 4 && bytes4(ret) == IERC1271.isValidSignature.selector;
}
실제 magic value constant 이름은 프로젝트에 맞게 쓰면 된다. 핵심은 success, length, magic value를 분리하지 말고 하나의 fail-closed predicate로 묶는 것이다.
7. 더 볼 자료
- Gnosis - Post-Mortem: Gnosis Pay Vulnerability Exploit: Gnosis Pay 관점의 피해 규모, 대응 타임라인, 사용자 복구, root cause 요약을 제공한다.
- Gnosis Guild Engineering - Zodiac ERC-1271 Authorization Bypass: affected version, exploit 조건, patched version,
success && magic value수정 원리를 가장 직접적으로 설명한다. - CertiK - GnosisPay Incident Analysis: attacker-controlled calldata, nested signature parsing, Biconomy Safe 경유, Delay queue 실행 흐름을 낮은 레벨로 볼 수 있다.
- Zodiac commit 9a9e380: 2023년 signature checker 변경 맥락과 테스트 변경을 추적할 수 있다.
- Zodiac v5.0.0 PR #178: incident 이후 mastercopy/versioned artifact 관리가 왜 중요해졌는지 읽을 수 있는 maintenance tooling 변화다.
중복 회피 메모
최근 blockchain_dev 글은 Arbitrum BoLD fraud proof finality, EIP-8184 encrypted mempool/MEV, EIP-7702 wallet delegation, Syscoin·Axelar bridge parsing/source authentication, EIP-7732 ePBS, blob fee, OP Stack interop, Gnosis Pay 모듈 control-plane 보안을 다뤘다. 이번 글은 Gnosis Pay 사고를 다시 “사고 소식”으로 반복하지 않고, 2026-07-04 공식 post-mortem 공개를 바탕으로 ERC-1271의 staticcall success + magic value fail-closed 검증 원리, Safe fallback handler가 authority graph에 들어가는 이유, repository patch와 production bytecode 사이의 gap에 집중해 관점을 분리했다.
핵심 출처
로그인하면 이 글을 북마크하고, 나만 보는 한 줄 메모를 남길 수 있어요.
댓글 0
최신순 ▾혹시 이 글을 읽는 동료 개발자가 있다면, GitHub으로 로그인하고 한 줄 흔적을 남겨줘요. (스팸 방지용 로그인이에요)