티스토리 뷰

EOA가 스마트 컨트랙트처럼 동작한다고?

EIP7702는 25년 5월에 이루어진 Pectra 업그레이드에서 활성화된 코어 프로토콜 단의 변경으로, EOA가 이미 배포된 스마트 컨트랙트를 위임받아 자신의 context에서 실행할 수 있게 한다. 즉, EOA주소가 실행 대상이 되어, EOA 계정 context에서 코드가 실행된다.

등장배경

💩 EIP7702는 왜 필요했을까? 표준이 제안된 흐름을 먼저 이해해보자.

ERC-4337, Safe wallet 등 스마트 지갑 생태계가 많이 발전했음에도 불구하고, EOA의 구조적 한계 때문에 UX개선이 충분히 확산되지 못했다. 따라서 EIP-7702는 EOA에 기능 개선을 추가하는데 초점을 맞춰서, UX개선이 애플리케이션 전체 스택에 퍼질 수 있도록 한다. 즉, EOA가 스마트 컨트랙트를 직접 위임함으로써 구조적 한계를 극복하려는 것이다.

 

해당 EIP가 중점적으로 여긴 세 가지 기능은 다음과 같다. (기능을 스펙에 구현했다는 뜻 X)

  • Batching : 여러 작업을 atomic tx 하나로 묶어 실행 (DEX 토큰 승인 + 거래)
  • Sponsorship : 수수료 대납
  • Privilege de-escalation : 자신의 계정에 대해 제한된 권한을 가진 서브키 발급 (ERC-20 토큰만 사용가능하고 ETH 사용 제한, 하루 거래 금액 제한)

💩 EOA가 겪는 구조적 한계가 무엇이길래 위의 세 가지 기능을 기존 EOA는 사용하지 못하는 것일까?

 

EOA와 스마트 컨트랙트 상태는 다음과 같다. 

https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf

EOA는 개인키로 통제되고 codeHash 슬롯이 비어있는 상태의 계정이다. 알다시피 EOA는 스마트 컨트랙트와 같이 프로그래밍된 코드를 수행할 수 없다. 즉, 앞서 말한 배칭, 스폰서쉽, 권한 축소 등의 기능을 EOA주소에서는 사용할 수 없는 것이다.

 

EOA의 구조적 한계

  • 단일 서명만 지원 - EOA는 오직 하나의 개인키로만 제어되므로 권한 분리, 조건부 승인 같은 기능을 직접 구현할 수 없음
  • 트랜잭션의 단일성 - 여러 작업을 묶어서 처리 불가능
  • 가스 지불 방식 제한 - ETH로만 가스비를 제출할 수 있음
  • 권한 관리 부재 - 세분화된 권한 제어를 지원하지 못함

반면 스마트 컨트랙트는 위 기능들을 로직으로 구현 가능했고, ERC-4337은 EntryPoint, Bundler, Paymaster 인프라 구축을 바탕으로 로직이 구현된 컨트랙트를 지갑으로서 활용할 수 있게했다.

 

💩 그러면 기존 EOA 사용자들이 스마트 지갑을 만들면 되는거 아닌가?

스마트 지갑 생성 자체는 어렵지 않다. 하지만 내가 항상 사용하던 메타마스크 지갑(EOA)에는 코인, NFT, 각종 DeFi 프로토콜들과의 권한 승인 기록 그리고 ENS 등이 있는데, 스마트 지갑을 새로 만들면 모두 새 주소로 이전해야하는 번거로움이 있다. 

 

💩 내가 사용하던  EOA를 signer로 하여 스마트 지갑을 생성하는 경우도 고려할 수 없는가?

물론 내가 사용하던 메타마스크 주소를 signer로 하여 스마트 지갑 생성이 가능하다. 그러나 나의 EOA 주소가 쓰이는 건 UserOp에 대한 서명까지이고, 결국 네트워크에서 다른 컨트랙트들과 상호작용하는건 새롭게 만들어진 스마트 지갑 주소(=msg.sender)이다.

 

이때 기존 EOA 주소를 유지하면서 앞선 세가지 기능들을 쓸 수 있도록 제안한 것이 EIP-7702이다. 기존 EOA 사용자들은 EIP-7702를 통해 스마트 지갑(Kernel) 컨트랙트를 가리킴(위임함)으로써, 자금 이전없이 스마트 지갑의 기능을 EOA context에서 그대로 쓸 수 있다.

 

어떻게 구현했는가?

새로운 트랜잭션 타입 set code transaction(0x04, EIP-2718)를 만들었다. 하는 일은 EOA 코드 슬롯에 위임 지시자(Delegation indicator; 위임할 컨트랙트)를 쓰는 것이다. 즉, 이미 배포된 코드를 가리키는 포인터를 설정하는 것이다.

 

트랜잭션 type 0x04는 기존 type 0x02에 권한 부여 목록(authorization_list) 필드가 새롭게 추가되었다. 0x04 transaction payload는 다음과 같다.

rlp(
[chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit,
destination, value, data, access_list, 
authorization_list, 
signature_y_parity,
signature_r, 
signature_s]
)

 

signature_y_parity, signature_r, signature_s는 전체 트랜잭션에 대한 서명 값으로, 트랜잭션 제출자는keccak256(SET_CODE_TX_TYPE(0x04) || TransactionPayload)에 서명해야 한다.

 

geth에는 다음과 같이 트랜잭션 필드가 구성되어 있다.

 

목록의 각 권한 부여 튜플은 다음과 같이 구성된다.

authorization_list = [[chain_id, address, nonce, y_parity, r, s], ...]

 

  • address : 위임할 스마트 컨트랙트 주소
  • nonce : EOA 계정의 nonce값
  • y_parity, r, s : EOA(authority)의 개인키에 의해 서명된 값

각 튜플은 EOA가 특정 컨트랙트 코드 실행을 위임한다는 선언과 그에 대한 서명이고, authorization_list는 이 튜플들의 집합이다. 즉, 하나의 type 0x04 트랜잭션 안에 여러 계정에 대한 위임을 동시에 처리할 수 있다. 쉽게 말해서 계정 A가 컨트랙트 a를 위임하고, 계정 B가 컨트랙트 b를 위임한다는 튜플을 모아서 제 3자가 제출할 수도 있다는 얘기이다. 중요한 건 각 튜플이 독립적으로 계정(EOA)에 대한 서명을 포함하고 있다는 점이다.

 

💩 그런데 EOA 서명을 제 3자가 제출했는데 각 튜플을 어떻게 검증하는 걸까?

우선 각 튜플에는 계정의 서명이 들어있고, 검증 과정은 다음과 같다.

 

1. chain_id가 현재 체인과 맞는지 검사

2. authority(EOA) = erecover(msg, y_parity, r, s)

   이때, msg는 keccak256(MAGIC(0x05) || rlp([chain_id, address, nonce]))이다. EOA는 서명 시에 MAGIC prefix를 붙여서 해당 서명이 위임 선언 관련 서명임을 알려야 한다.

3.  accessed_addresses에 authority(EOA) 등록

4. nonce값이 authority(EOA)의 nonce값과 같은지 검사(replay 공격 방지)

5. authority(EOA) 코드 슬롯에 0xef0100 || address(delegation indicator) 기록

    만약 address가 '0x0000000000000000000000000000000000000000'이면 코드 슬롯을 리셋 empty code hash값인 ' 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470'으로 리셋

6. authority(EOA) nonce값 1 증가

 

위 과정 중 하나라도 실패하면 다음 튜플로 넘어가며, 같은 authority가 여러 번 등장하면 마지막 유효한 튜플의 address로 delegation indicator를 기록한다.

 

🐝geth 코드에서 튜플의 서명을 복원하여 검증하는 과정

 

💩 EOA 코드 슬롯에 address만 기록하면 되지, 왜 0xef0100를 붙여서 delegation indicator란걸 만들까?

위임 지시자(delegation indicator)는 EOA 코드 슬롯에 넣는 23바이트 실행 포인터이다. 계정에서 코드를 실행할 때, EVM은 해당 바이트를 실행하지 않고 가리키는 주소의 코드를 가져와 실행한다. 

 

형식은 0xef0100 || address (3바이트 prefix + 20바이트 주소)이다.

 

왜 0xef일까? 0xef는 금지된 opcode로 일반 배포 코드와 충돌하지 않고, 이건 실행할 바이트코드가 아니라 포인터라고 알려주는 역할이다. 0x01은 종류(EIP-7702), 0x00은 버전을 의미한다.

 

CODESIZE와 EXTCODESIZE(authority)가 리턴하는 값도 달라진다. CODESIZE의 경우 현재 실행중인 프레임의 바이트코드 크기를 묻는다. EOA가 컨트랙트 A를 위임했을 경우 CODESIZE는 A 컨트랙트의 코드 크기를 리턴한다. EXTCODESIZE(authority)는 위임 지시자 크기 23바이트를 리턴한다.

 

💩 위임  대상이 다시 EIP-7702가 적용된 EOA이면 호출이 성공하나?(loop 문제)

A → B → C 또는 A → B  → A 같이 체인이나 루프를 형성할 경우 첫 지시자만 따라간다.

 

A(EOA) 코드 = 0xef0100 || B

B(EOA) 코드  = 0xef0100 || 컨트랙트 C

 

위와 같이 EIP-7702가 적용된 2개의 EOA가 있을때, call(A)는 결국 B의 위임 지시자를 따라가지 않고 코드 그 자체를 실행함으로써 revert된다. 무한루프와 한 없이 증가하는 조회를 막기 위함이다. 

 

보안 고려사항

EIP-7702를 통해 특정 컨트랙트를 위임하면 해당 EOA의 보안은 컨트랙트 구현체 코드의 보안과 같다. 프로토콜은 포인터만 설치하고, 권한, 배칭, 초기화등의 기능 구현은 모두 구현체 몫이기 때문이다. 어떠한 위험사항이 있는지 알아보겠다.

 

1. 악성, 가변(프록시) 구현체를 위임하는 경우

 

문제점 :

  • 악성 구현체에 위임하면, 해당 컨트랙트가 EOA 컨텍스트에서 ETH 전송, transfer, approve, storage 쓰기를 전부할 수 있다.
  • chain_id가 0이면 모든 체인에서 유효한데, 같은 주소이더라도 체인마다 다른 코드가 있을 수 있다.
  • 업그레이드 가능한 프록시를 우임했을 경우 위임 이후 코드가 바뀔 수 있다. 

대책 :

  • 감사받은 안전한 컨트랙트를 위임할 수 있다. (관련목록)
  • dapp이 authorization을 직접 요청하는 패턴은 금지한다.
  • chain_id를 해당 체인으로 고정한다.

2. 위임 컨트랙트 구현 시 주의사항(Implementation of secure delegate contracts)

 

위임 대상 컨트랙트의 실행 api가 "무엇을 실행할지"를 사용자(EOA)서명에 포함하지 않으면, 스폰서(paymaset, 릴레이더 등)가 실행 내용을 바꿀 수 있는 위험이 존재한다. 

 

구현체 코드를 Alice(EOA)가 위임했을때, Alice가 execute(target, value, data, sig)가 구현된 eip-7702 구현체를 위임했다고 해보자. 이때 가스비 대납을 위해 제3자가 execute()를 호출하도록 두면, alice는 오프체인에서 메시지를 서명하여 가스비 대납자에게 전달한다. 이때 서명에는 실행 내용과 관련된 어떠한 값도 포함되어 있지 않다고 가정하자. Alice가 최초에 의도한 실행 내용이 execute(Router, 0, swap(USDC→ETH), sig)이었어도, 가스비 대납자는 execute(Router, 0, swap(USDC→ETH), sig)과 같이 실행 내용을 변경해도 정상적인 호출이 이루어진다. Alice의 서명이 있어도 서명한 해시값이 keccak256("대납 허용")과 같으면 의미가 없는 것이다. 즉, 서명에는 반드시 다음과 같은 필드가 포함되어야 한다.

  • nonce : 같은 서명 재사용 방지
  • value : 호출 대상의 예상치 못한 효과 방지
  • gas : OOS, gas griefing 방지
  • target / calldata : 악의적인 행위자가 임의의 컨트랙트에서 임의의 함수 호출하는 것 방지

eth-infinitism에서 작성한 eip-7702 구현체 Simple7702Account.sol에서는 이러한 위험에 대해 호출자를 제한하는 방식으로 방어했다. 해당 구현체에서 execute() / executeBatch() 안에서 바로 _requireForExecute()를 호출한다. 

contracts/accounts/Simple7702Account.sol#L48

EntryPoint, 자기자신이 호출하는 트랜잭션에는 기본적으로 실행내용을 포함한 서명을 사용하므로, 실행내용이 바뀐 채 실행될 여지가 없다.

 

3. 프론트러닝 초기화(Front running initialization)

 

일반 컨트랙트의 경우 배포시 initcode 실행으로 코드와 스토리지를 안전하게 초기화한다. 하지만 set code에서는 initcode가 없기 때문에 초기화 과정이 취약할 수 있다. 구현체를 위임하는 과정과 초기화하는 과정이 하나의 배포 동작으로 묶이지 않는다. 예시를 들어서 설명해보겠다.

 

Alice가 특정 구현체를 위임하고 초기화하는 트랜잭션을 네트워크에 제출했는데, 멤풀을 지켜보고 있던 bob이 alice의 위임 서명을 가로채서 자기 tx에 넣을 수 있다. 이후 가스비를 더 높이고, 변경한 매개변수로 init 함수를 bob이 먼저 호출하여 owner를 자신으로 설정할 수 있다. 

 

어떻게 완화할 수 있을까?

 

첫번째로 초기화 함수 자체를 없앨 수 있다. owner 슬롯 없이, address(this)(EOA주소)와 서명이 가리키는 주소가 같은지를 확인한다.

contracts/accounts/Simple7702Account.sol#L44

 

두번째 방법으로 initWithSig함수를 구현할 수 있다. 초기화 파라미터를 EOA가 서명한 것만 받는 방식이다. 무단으로 초기화하는것을 막을 수 있다.

 

세번째로는 EntryPoint만 호출하도록 제한할 수 있다. 이때는 초기화 관련 calldata를 포함한 UserOp Hash를 EOA가 이미 서명한 상태이기 때문에 임의로 변경된 초기화가 불가능하다.

 

4. 저장소 충돌(Storage Collision)

 

위임하는 구현체를 바꿔도 EOA의 저장소는 지워지지 않는다. 새로 위임한 구현체가 같은 슬롯을 다른 타입으로 읽을 경우 의도치 않은 동작이 일어날 수 있다. address를 0x0으로 하여 위임지시자를 지워도 저장소는 남는다. 어떻게 완화할 수 있을까?

 

첫번째 방법으로 아예 구현체를 바꾸지 않거나, 기존 구현체와 스토리지 슬롯이 동일한 업그레이드 버전으로만 바꾸는 방법이 있다. 

 

두번째 방법으로 ERC-7201 네임스페이스를 활용하는 방법이 있다. 이는 당장의 저장소 충돌을 줄일 수 있지만, 과거 슬롯은 여전히 남아있어 가스와 상태 크기를 늘릴 수 있다.

 

마지막으로 사용중인 구현체를 구현한 팀의 공식 마이그레이션 함수를 사용하는 방법도 있다.

 

5. tx.origin 불변식 깨짐(Setting code as tx.origin)

require(msg.sender==tx.origin)

 

어디서 많이 본 코드 스니펫이지 않은가? EIP-7702가 나오기 전에 위 스니펫은 참일때 "EOA가 직접 나를 호출했다"를 의미한다. 하지만 EIP-7702 제안 이후에 위와 같은 판별식이 깨질 수 있다. EOA가 위임한 코드를 EOA가 보낸 트랜잭션 안에서 실행하면 안쪽 호출에서도 msg.sender가 EOA이다.

 

그러면 어떤 점이 문제가 되나? 기존에 위 코드 스니펫을 활용했던 상황들을 통해 알아보자.

 

첫번째로 tx.origin에 의존하는 atomic sandwich protection을 깨트릴 수 있다. 샌드위치 공격은 간단히 말해 사용자의 트랜잭션 앞,뒤로 공격자의 트랜잭션을 끼워넣어 가격 변동이나 상태 변화를 이용해 이익을 취하는 공격이다. 이 공격을 기존에는 특정 함수 실행 시 위 코드 스니펫을 조건으로 걸어 방어했다. 트랜잭션이 반드시 EOA에 의해 호출되도록 하여 중간에 다른 컨트랙트가 끼어들지 못하게 하는 방안이었다. 하지만 이 같은 안전장치가 EIP-7702이후 깨질 수 있기 때문에 이에 의존하면 안된다.

 

두번째로, 위 코드 스니펫을 활용한 reentrancy guard가 깨질 수 있다. 컨트랙트의 receive / fallback 함수에서 재진입 하는 것을 막을 수는 있지만, 코드를 위임한 EOA도 receive / fallback을 가질 수 있기 때문에 안전하지 못하다. Openzeppelin의 ReentrancyGuard를 사용하도록 하자


References

EIP-7702: Set Code for EOAs

 

EIP-7702: Set Code for EOAs

Add a new tx type that permanently sets the code for an EOA

eips.ethereum.org

Pectra EIP-7702 guidelines | ethereum.org

 

Pectra EIP-7702 guidelines | ethereum.org

펙트라 릴리스의 7702에 대해 자세히 알아보세요

ethereum.org

Ethereum EIP-7702 코드와 데이터 분석하기 (Account Abstraction, Pectra)

 

Ethereum EIP-7702 코드와 데이터 분석하기 (Account Abstraction, Pectra)

들어가기 전에​2025년 5월 7일 Ethereum에서 Pectra Hardfork가 예정되어 있습니다. 그중 핵심 기능으로 EIP-7702에 제안된 계정 추상화(Account Abstraction, AA)가 관련된 변화가 포함되어 있습니다. 이 글에서

bbaktaeho-95.tistory.com

account-abstraction/contracts/accounts/Simple7702Account.sol at develop · eth-infinitism/account-abstraction

 

account-abstraction/contracts/accounts/Simple7702Account.sol at develop · eth-infinitism/account-abstraction

Contribute to eth-infinitism/account-abstraction development by creating an account on GitHub.

github.com

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/09   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함