---
title: "ERC-4337 UserOperation 패킹 취약점"
description: "2023년 3월 7일, Alchemy와 오픈 소스 커뮤니티 멤버들은 ERC-4337 EntryPoint 컨트랙트와 예제 VerifyingPaymaster 컨트랙트에서 calldata 디코딩 문제를 발견했습니다."
---

# ERC-4337 UserOperation 패킹 취약점

**추가 기여자**:
[fangting@alchemy.com](mailto:fangting@alchemy.com), @drortirosh, @Gooong, @taylorjdawson, @leekt, @livingrockrises

## 개요

2023년 3월 7일, Alchemy와 [@Gooong](https://github.com/Gooong), [@taylorjdawson](https://github.com/taylorjdawson), [@leekt](https://github.com/leekt), [@livingrockrises](https://github.com/livingrockrises)를 포함한 오픈소스 개발자 커뮤니티의 다른 구성원들이 ERC-4337 EntryPoint 컨트랙트와 예시 VerifyingPaymaster 컨트랙트에서 calldata 디코딩 문제를 발견했습니다.

이 컨트랙트들은 현재 여러 체인에 배포되어 있으며 user operation에 대한 해시를 생성합니다. 해당 구현은 서명 방식에 따라 일관되지 않은 해시를 생성했으며, 이는 동일한 UserOperations에 대해 서로 다른 해시가 나오거나 서로 다른 UserOperations에서 해시가 충돌하는 등 여러 2차적인 영향으로 이어질 수 있습니다.

논의는 [@drortirosh ](https://github.com/drortirosh)의 주도로 진행되었으며 [이 Github 이슈](https://github.com/eth-infinitism/account-abstraction/issues/237#issuecomment-1466686252)에 기록되어 있습니다.

## 상세 분석

아래는 영향을 받은 코드에 대한 분석, EntryPoint Packing 취약점과 VerifyingPaymaster Packing 취약점에 대한 설명, 그리고 각각의 영향입니다.

### 영향을 받은 코드

문제가 되는 코드 구간은 다음과 같습니다:

#### ‍[UserOperation.sol:61-75](https://github.com/eth-infinitism/account-abstraction/blob/556f03fadcaba0d7d04cd901e6eb9601db50a998/contracts/interfaces/UserOperation.sol#L61-l75)

#### [VerifyingPaymaster.sol:35-49](https://github.com/eth-infinitism/account-abstraction/blob/556f03fadcaba0d7d04cd901e6eb9601db50a998/contracts/samples/VerifyingPaymaster.sol#L35-l49)

참고로, `UserOperation` 구조체는 다음과 같이 정의됩니다:

이 두 코드 구간 모두 assembly를 사용해 calldata의 상당 부분을 메모리로 복사하며, user operation의 일부를 캡처해 해시하려는 목적을 가지고 있습니다.

`UserOperationLib`의 `pack` 메서드는 `sender`부터 `maxPriorityFeePerGas`까지 user operation의 모든 필드를 캡처하려 하며, 여기에는 가변 크기 필드(ABI 인코딩에서 dynamic field라고 부름)인 `initCode`, `callData`, `paymasterAndData`이 포함됩니다.

`VerifyingPaymaster`의 `pack` 메서드는 `paymasterAndData` 필드를 제외한 나머지 모든 필드를 포함하는데, 이는 해당 필드가 아직 정의되지 않았기 때문입니다.

이를 구현하기 위해 두 메서드 모두 calldata 내 dynamic type에 제공되는 [Yul](https://docs.soliditylang.org/en/v0.8.17/yul.html)의 편의 필드인 `.offset`을 사용합니다. 이는 구조체의 ABI 인코딩에서 제공되는 값을 가리키며, [Solidity 명세의 이 부분](https://docs.soliditylang.org/en/v0.8.18/abi-spec.html#formal-specification-of-the-encoding)에 정의되어 있습니다. **(실제로는 오프셋 다음의 메모리 워드를 가리키지만, 로드 시 편의를 위한 것입니다).**

표준 ABI 인코더는 dynamic field들의 값(ABI 코더에서는 tail이라고 부름)을 나타나는 순서대로 인코딩합니다.

간결한 설명을 위해, calldata에서 생성될 수 있는 다음과 같은 user operation 인코딩을 살펴보겠습니다:

<CalloutBlock>

💡 **참고:** 이 예시는 간결함을 위해 모든 dynamic field가 한 워드 미만의 길이인 user operation을 보여줍니다.

</CalloutBlock>

<CalloutBlock>

💡‍ **\*‍**\***참고:** 여기서 메모리 주소 공간은 user operation 구조체 자체 내부입니다. 실제 calldata에서는 메서드 셀렉터와 인자 튜플이 차지하는 공간 때문에 다른 위치에 배치됩니다.

</CalloutBlock>

이 예시에서, user operation의 패킹을 생성하기 위해 `pnd.offset`를 따라가면 다음과 같은 calldata "슬라이스"가 나옵니다:

이는 우리가 원하는 것과 정확히 일치합니다!

하지만 ABI 인코딩된 인자를 사용하는 컨트랙트는 필드가 어떤 순서로 정의되었는지, 혹은 오프셋이 유효한지조차 검증하지 않습니다.

`signature.offset`이나 `pnd.offset`를 사용하면 해당 "오프셋" 값을 calldata에서 직접 읽어옵니다.

이를 경계로 사용함으로써, 특이한 해시 속성을 갖는 user operation의 유효한 표현을 calldata에서 구성하는 것이 가능해집니다.

이것이 EntryPoint와 VerifyingPaymaster에 각각 어떤 영향을 미치는지 살펴보겠습니다.

### EntryPoint packing 취약점

이 취약점을 보여주기 위해서는, 제공된 `SimpleAccount.sol`와는 다른 wallet 컨트랙트를 고려해야 합니다. 해당 샘플은 `EntryPoint`의 취약한 코드를 재사용하기 때문입니다.

이 해시 불일치는 EntryPoint와 wallet 컨트랙트 사이에 서로 다른 해싱 방식이 사용되거나, wallet이 비표준 user operation 인코딩에 서명할 때 실질적인 문제가 됩니다.

`EntryPoint`에 발생하는 위험은, 하나의 user operation이 여러 개의 "user op hash"로 표현될 수 있고, 동일한 "user op hash"가 여러 user operation을 나타낼 수 있다는 점입니다.

`ExampleAccount`이라 불리는, 자체 `ExampleAccountFactory`를 가진 이 계정을 살펴보겠습니다. 이 예시 계정은 user operation을 검증하기 위해 단일 서명자를 사용합니다.

user operation을 실행할 권한을 부여하기 위해, 서명 자체를 제외한 user operation의 모든 필드에 대한 해시가 생성되고 서명됩니다.

`validateUserOp` 메서드는 다음과 같이 정의됩니다:

이는 비교적 단순한 서명 검증 구현으로, `userOp`의 모든 필드와 entrypoint 주소, chain id를 확인합니다.

account abstraction의 목표 중 하나로서, `validateUserOp` 메서드는 (스토리지 접근에 대한 제약이 있긴 하지만) 임의의 로직을 포함할 수 있습니다. 이 메서드가 user operation이 시작될 수 있는 조건을 나타내기 때문입니다.

이 예시 계정에서는 user operation이 소유자의 서명에서 시작됩니다. 하지만 더 일반적으로는, user operation은 임의의 조건(온체인 상태, 다중 서명, 또는 앱별 서명)에서 시작될 수 있습니다 – 이것이 account abstraction의 특징입니다.

이 취약점을 보여주기 위해, `EntryPoint.handleOps`에 대한 악의적인 calldata를 구성하여 `EntryPoint`에서 발생하는 `UserOperationEvent`가 예상치 못한 값을 갖도록 해보겠습니다.

샘플 `UserOperation memory uo` 구조체를 정의한 후, calldata를 다음과 같이 구성할 수 있습니다:

`rightPadBytes`은 `bytes` 타입을 가장 가까운 전체 워드 길이로 정렬하기 위해 작성된 헬퍼 함수입니다.

정의는 다음과 같습니다:

이제 `handleOps`를 호출할 때, 발생하는 이벤트와 `EntryPoint.getUserOpHash\(\)`의 결과가 서로 다르게 됩니다.

#### 영향

악의적인 번들러, 또는 `EntryPoint.handleOps`을 호출하는 번들러가 아닌 EOA는 calldata에서 `UserOp`의 표현을 수정하여 발생하는 이벤트의 UO 해시를 변경할 수 있습니다. 이는 발생하는 이벤트와 연동하는 오프체인 시스템을 손상시킬 수 있는데, 주어진 UO에 대해 이벤트가 결정적이지 않다는 사실이 드러나기 때문입니다.

또한, 번들러는 `EntryPoint` 컨트랙트에서 발생하는 userOpHash를 읽을 때 비결정성을 다뤄야 합니다. `EntryPoint`에서 발생한 `UserOperationEvent`가 번들러의 로컬 mempool에 있는 user operation과 일치하는지 확인하려면, `handleOps`에 대한 calldata가 해시 값을 변경하도록 수정될 수 있으므로 해시 값 비교만으로는 더 이상 충분하지 않습니다.

대신, 번들러는 트랜잭션 영수증을 조회하고, `handleOps`에 전송된 calldata를 가져와 디코딩한 다음, 표준 ABI 코더를 통해 재인코딩하고 `EntryPoint.getUserOpHash\(...\)`을 호출하여 "정규" 해시를 얻어야 합니다. 이는 로컬 mempool의 user operation이 채굴되었는지 여부를 판단하는 데 필요합니다. 또한, `EntryPoint.handleOps`에 대한 호출은 다른 컨트랙트 호출 내부에서 발생할 수 있으므로, 디코딩이 호출 스택 깊숙이에서 이루어져야 할 수도 있습니다.

이 불일치는 `eth\_getUserOperationByHash`과 `eth\_getUserOperationReceipt`에서 user op hash가 식별에 사용되므로 번들러 RPC 메서드 구현에도 영향을 미칩니다.

번들러는 이벤트에서 발생한 해시를 `EntryPoint.getUserOpHash\(...\)`의 "정규" 해시로 변환하기 위해 `EntryPoint.handleOps\(...\)`에 대한 calldata를 비용이 많이 드는 방식으로 검색, 파싱, 디코딩해야 할 것입니다.

<CalloutBlock>

💡 **참고:** 이 취약점은 rogue SCW가 user operation을 재사용할 수 있다는 사실과는 별개입니다. 재사용된 user operation, 그리고 더 일반적으로는 모든 user operation은 결정적인 해시를 가져야 합니다. ERC-4337 위에 구축되는 다른 애플리케이션과 서비스들은 이 문제가 해결되지 않는 한 자체적인 완화 조치를 구현해야 할 것입니다.

</CalloutBlock>

ERC-4337이 전반적으로 초기 도입 단계에 있기 때문에, 이 취약점이 더 넓은 생태계에 미칠 잠재적 영향을 설명하기는 어렵습니다. 영향의 범위는 번들러, user operation 탐색기, 인덱서, 그 밖의 오프체인 서비스의 구현 방식에 따라 달라집니다.

**최소한**으로는, user operation 해시(트랜잭션 해시와 유사)가 제출 시점과 포함 시점 사이에 바뀔 수 있어 혼란스러운 사용자 경험을 초래할 수 있으며, 일부 wallet은 이러한 차이를 고려하지 못해 사용자에게 업데이트를 제대로 표시하지 못할 수 있습니다.

**중간 위험 사례**에서는, wallet이 모든 user op hash를 동일하게 설정하여 의도적으로 인덱싱을 회피하도록 설계될 수 있습니다 ([@leekt가 제공한 이 예시](https://github.com/leekt/foundry-4337/compare/PoC/sameUserOpHash) 참고).

**고위험 사례**에서는, user op 포함 여부를 모니터링하는 오프체인 서비스가 특정 user operation의 포함을 놓치고, 데이터와 키를 재전송하거나 잘못 처리하려 시도할 수 있습니다.

#### 개념 증명

전체 개념 증명은 [이 저장소](https://github.com/alchemyplatform/entrypoint-hash-poc)에서 확인할 수 있습니다.

### VerifyingPaymaster packing 취약점

`VerifyingPaymaster`에 발생하는 위험은, user operation이 서명 시점과 온체인 포함 시점 사이에 서로 다른 내용을 가질 수 있다는 점입니다. 이는 두 개의 서로 다른 user operation이 `VerifyingPaymaster.getHash\(UserOperation userOp, uint48 validUntil, uint48 validAfter\)`에서 동일한 해시를 반환할 때 발생할 수 있습니다.

이 함수에 대한 calldata를 구성하여 이것이 어떻게 가능한지 보여드리겠습니다:

#### Calldata 1
이 인코딩이 dynamic field의 순서를 바꾸지만 그 외에는 여전히 유효하다는 점에 주목하세요 - 오프셋은 올바른 위치를 가리키고 길이도 모두 유효합니다. 하지만 `paymasterAndData`의 오프셋이 예상치 못한 값이기 때문에, `pack\(\)`에서 얻는 슬라이스는 다음과 같습니다:

`initCode`과 `callData`가 슬라이스에서 제외된 것을 확인할 수 있습니다!

이번에는 두 필드 모두를 악의적으로 수정하여 두 번째 입력 calldata를 구성해보겠습니다:

- `initCode`이 `1517c0de`에서 `1517c0de02`로 변경됩니다
- `callData`이 `ca11dada1`에서 `ca11data02`로 변경됩니다

이렇게 하면 다음과 같은 결과가 나옵니다:

#### Calldata 2
이 다른 calldata에 대해 동일한 `pack` 연산을 수행하면 이전과 동일한 슬라이스가 나옵니다! 그리고 다음을 통해 해시가 동일한지 확인할 수 있습니다:

foundry의 테스트 환경에서 이를 실행하면 두 user op가 동일한 해시를 가짐을 알 수 있습니다: `0x736f86d224bab46a95ae119947e172efa694379d9ac682d4ca780b7640a89b06`

전체 POC는 [이 테스트 파일](https://github.com/alchemyplatform/entrypoint-hash-poc/blob/main/test/VerifyingPaymasterHashPOC.t.sol)을 참고하세요.

#### 영향

이 취약점에서는 해시가 예상보다 적은 요소를 포함하도록 수정될 수 있어, initCode, callData, 그리고 다른 static field들이 해시에서 제외되어 서명 시점과 사용 시점 사이에 달라질 수 있습니다. 이는 paymaster 후원 서명이 의도된 것과 다른 목적으로 사용되는 결과를 초래할 수 있습니다.

예를 들어, wallet 컨트랙트의 배포자 factory와 wallet의 `execute` 함수 호출이 대체될 수 있습니다. paymaster가 이전에는 자신의 wallet 사용자만 후원하고, 특정 NFT를 민팅할 때만 후원하려 했다면, 그러한 규칙이 우회될 수 있습니다.

이러한 규칙을 우회하기 위해, sender는 서명을 받은 후 `userOp.initCode`와 `userOp.callData`을 변경할 것입니다. 그러면 paymaster의 네이티브 토큰(ETH 등)이 가스 없는 NFT 민팅이라는 원래 의도와는 다른 목적으로 사용됩니다.

ABI 인코딩된 형식으로 서명할 user operation을 받는 오프체인 서명자, 또는 서명을 위한 데이터를 준비하기 위해 컨트랙트와 연동하는 서명자는 취약합니다. 이들은 본질적으로 "스스로를 공격"하는 셈이므로 범위는 제한적이지만, paymaster 서비스 운영에는 위험을 초래합니다.

이에 대한 방어 조치로는 `VerifyingPaymaster`의 업데이트된 버전을 배포하거나, 사용자 입력으로부터 ABI 인코딩 과정을 직접 처리하는 방법이 있습니다.

## 결론

생태계 구성원들과 여러 차례 좋은 논의를 거친 끝에, **@drortirosh**가 이 취약점을 해결하기 위한 최적화되고 가독성 높은 [Entrypoint 컨트랙트 패치](https://github.com/eth-infinitism/account-abstraction/commit/6a7cbe7c6c786d6302d52b94405b88c6596f95cb)를 병합했습니다. 재배포가 이루어지면, Entrypoint 컨트랙트는 더 이상 위에서 설명한 동작을 보이지 않게 됩니다.

또한, Entrypoint에서 nonce 지원을 추상화하여 이 시스템을 한층 더 강화하는 [제안](https://docs.google.com/document/d/1MywdH_TCkyEjD3QusLZ_kUZg4ZEI00qp97mBze9JI4k/edit#heading=h.gyhqxhuyd59n)도 있습니다. paymaster에 대한 위험이 제한적이기 때문에, 공식적인 upstream 패치는 아직 이루어지지 않았으며 paymaster 운영자는 자신의 구현에 맞게 필요에 따라 이를 처리하는 방법을 결정할 수 있습니다.

이 취약점을 저희와 함께 해결해 주신 **@drortirosh**, **@Gooong**, **@taylorjdawson**, **@leekt**, **@livingrockrises**를 비롯한 4337 커뮤니티에 감사드립니다.

궁금한 점이나 논의하고 싶은 주제가 있으신가요?

account-abstraction@alchemy.com으로 연락해 주세요.
