ERC-6492 배포 전 컨트랙트의 서명 검증 (Signature Validation for Predeploy Contracts)

Final · 서명·인증 · 2023-02-10 작성

아직 배포되지 않은 컨트랙트 지갑이 만든 서명을 검증할 수 있게 합니다. 서명 뒤에 배포 정보를 붙여 보내면, 검증하는 쪽이 필요할 때 컨트랙트를 먼저 배포한 뒤 ERC-1271 검증을 이어 갑니다.

왜 필요한가

계정 추상화(account abstraction) 지갑이 퍼지면서 컨트랙트 지갑은 사용자가 첫 거래를 보낼 때 비로소 컨트랙트를 배포하는 방식을 많이 택합니다. 지갑을 쓰기 전에 배포 단계를 따로 거치는 부담을 덜기 위해서입니다. 그런데 많은 앱은 거래뿐 아니라 로그인에도 서명을 요구합니다. 컨트랙트가 만든 서명은 ERC-1271 에 따라 그 컨트랙트에 직접 물어 검증하는데, 컨트랙트가 아직 배포되지 않았으면 물어볼 대상이 없어 검증할 수 없습니다. 그래서 컨트랙트 지갑은 실제 배포가 이루어지기 전까지 메시지에 서명하는 능력이 제한되어 왔습니다. 배포 전 컨트랙트가 서명을 못 하는 것은 ERC-1271 의 오랜 한계이기도 했습니다.

구현해야 하는 것

심층 설명

감싸기 서명 형식과 magicBytes

배포 전 컨트랙트는 서명을 `concat(abi.encode((create2Factory, factoryCalldata, originalERC1271Signature), (address, bytes, bytes)), magicBytes)` 형태로 감쌉니다. magicBytes 는 `0x6492649264926492649264926492649264926492649264926492649264926492` 로 정해야 하며(MUST), 검증자는 서명이 이 값으로 끝나는지 보고 감싸기 형식을 알아냅니다. 감싸기 형식이 감지되면 검증자는 isValidSignature 를 호출하기 전에 반드시 컨트랙트를 배포해야 합니다(MUST). salt 와 bytecode 대신 factoryCalldata 를 넘기는 이유는 어떤 팩토리 인터페이스와도 맞추기 위해서입니다. ERC-1271 검증은 이미 계정 주소를 알고 있다는 전제이므로 create2Factory, salt, bytecode 로 주소를 다시 계산할 필요가 없습니다. 배포 주소가 항상 예측 가능한 CREATE2 컨트랙트와 함께 쓰는 것이 권장됩니다(RECOMMENDED).

서명자 쪽 세 가지 경우

서명하는 컨트랙트는 보통 컨트랙트 지갑이지만 ERC-1271 을 구현하고 카운터팩추얼로 배포되는 어떤 컨트랙트든 될 수 있습니다. 첫째, 이미 배포되어 있으면 일반 ERC-1271 서명을 만듭니다. 둘째, 아직 배포되지 않았으면 create2Factory 와 factoryCalldata 로 감쌉니다. 셋째, 배포는 되었지만 ERC-1271 검증 준비가 안 된 상태라면 `concat(abi.encode((prepareTo, prepareData, originalERC1271Signature), (address, bytes, bytes)), magicBytes)` 로 감쌉니다. 이때 prepareTo 와 prepareData 에는 컨트랙트를 ERC-1271 검증 가능한 상태로 만드는 거래(예: migrate 나 update 호출)가 들어가야 합니다.

검증자 쪽 필수 순서

전체 검증은 다음 순서를 반드시(MUST) 따릅니다. 1) 서명이 magicBytes 로 끝나면 멀티콜 컨트랙트에 eth_call 을 보내, 먼저 팩토리를 factoryCalldata 로 호출해 컨트랙트가 없을 때 배포하고 이어서 감싸기를 벗긴 서명으로 isValidSignature 를 호출합니다. 2) 주소에 컨트랙트 코드가 있으면 평소처럼 isValidSignature 로 ERC-1271 검증을 합니다. 3) ERC-1271 검증이 실패했고 지갑에 이미 코드가 있어 팩토리 배포 호출을 건너뛰었다면, factoryCalldata 거래를 실행한 뒤 isValidSignature 를 다시 시도합니다. 4) 주소에 컨트랙트 코드가 없으면 ecrecover 검증을 시도합니다.

순서를 이렇게 정한 이유

magicBytes 확인은 일반 ERC-1271 확인보다 먼저 해야 합니다(MUST). 그래야 컨트랙트가 배포된 뒤에도 배포 전에 만든 서명이 유효하게 남습니다. magicBytes 확인은 ecrecover 보다도 먼저 해야 하는데(MUST), 배포 전 컨트랙트 서명임이 분명한 것을 ecrecover 로 검증하려 드는 일을 막기 위해서입니다. ecrecover 는 ERC-1271 검증보다 앞서면 안 됩니다(MUST NOT). 컨트랙트가 쓰는 서명 형식이 우연히 다른 주소의 외부 소유 계정(EOA) 서명으로도 유효할 수 있기 때문입니다. 그 EOA 가 제어하는 지갑 컨트랙트가 그런 예입니다. 지갑에 이미 코드가 있는데 배포 접두가 붙어 있으면, 배포 전에 서명했는지 준비가 덜 된 상태인지 구분할 수 없으므로 두 가능성을 모두 시도합니다. magicBytes 가 `0x92` 로 끝나는 것은 r, s, v 형식의 ecrecover 서명과 충돌하지 않게 하기 위함입니다. `0x92` 는 유효한 v 값이 아닙니다. bytes32 길이로 둔 것도 일반 ERC-1271 서명과의 충돌을 피하려는 것입니다.

참조 구현 UniversalSigValidator

참조 구현은 온체인과 오프체인 모두에서 쓸 수 있는 범용 검증 컨트랙트로, 싱글턴 배포를 전제합니다. 이 표준, ERC-1271, 전통적인 ecrecover 서명을 모두 검증합니다. 최종 다이제스트(_hash)를 검증하므로 EIP-712 도 함께 지원됩니다. isValidSigImpl 은 서명 끝 32바이트가 ERC6492_DETECTION_SUFFIX 와 같은지로 카운터팩추얼 여부를 판단하고 컨트랙트 코드 길이가 0 이거나 tryPrepare 가 참이면 create2Factory.call(factoryCalldata) 를 실행합니다. 실패하면 ERC6492DeployFailed 로 되돌립니다. 이어서 isValidSignature 결과가 ERC1271_SUCCESS(0x1626ba7e) 인지 확인합니다. 실패했거나 호출이 되돌려졌는데 코드가 있으면 tryPrepare 를 참으로 바꿔 한 번 더 시도합니다. ecrecover 단계에서는 서명 길이가 65 바이트여야 하고 v 는 27 또는 28 만 허용합니다.

온체인 검증 두 가지 방식

온체인에서는 두 함수를 쓸 수 있습니다. isValidSig(_signer, _hash, _signature) 는 서명 유효 여부를 bool 로 돌려주며 재진입에 안전합니다. 내부적으로 isValidSigImpl 을 allowSideEffects 를 false 로 호출하고 배포로 상태가 바뀌었을 때는 결과를 한 바이트 revert 데이터로 받아 상태 변경을 되돌립니다. 길이가 1 인 revert 데이터는 결과값으로 해석하고 그 외 오류는 그대로 전달합니다. isValidSigWithSideEffects(_signer, _hash, _signature) 는 같은 결과를 내지만 재진입에 안전하지 않은 대신 일부 경우 가스 효율이 더 좋습니다. 두 함수 모두 내부 호출이 되돌려지면 함께 되돌려질 수 있습니다.

오프체인 검증은 eth_call 한 번

ValidateSigOffchain 은 생성자 안에서 UniversalSigValidator 를 새로 만들고 isValidSigWithSideEffects 를 호출한 뒤 결과를 한 바이트로 반환합니다. 미리 배포된 컨트랙트 없이 eth_call 한 번으로 범용 검증을 끝낼 수 있습니다. ethers 예시는 ValidateSigOffchain 바이트코드 뒤에 signer, hash, signature 를 ['address', 'bytes32', 'bytes'] 로 인코딩해 이어 붙인 데이터를 provider.call 로 보내고 반환값이 '0x01' 이면 유효하다고 봅니다. Ambire 의 signature-validator 같은 라이브러리를 써도 됩니다.

호환성과 일반 컨트랙트 서명에의 활용

이 표준은 ERC-1271 등 기존 서명 검증 작업과 호환됩니다. EOA 서명과 EIP-712 타입 데이터를 포함한 모든 서명 유형을 쉽게 검증할 수 있게 합니다. 감싸기 형식은 배포 전 컨트랙트만이 아니라 모든 컨트랙트 서명에 일반 ERC-1271 대신 써도 됩니다. 그러면 magicBytes 덕분에 블록체인을 조회하지 않고도 컨트랙트 서명임을 바로 알 수 있고 ecrecover 처럼 create2Factory 와 factoryCalldata 만으로 서명에서 주소를 복원할 수 있습니다.

쓸 때 주의할 점

ERC 원문 · ← ERCs