계정 추상화의 등장

이더리움에서 사용하는 계정, CA는 개인키가 없고 EOA는 코드를 담을 수 없는 특징으로 인해 제약과 한계를 가지고 있음

→ 이러한 한계를 극복하기위해 CA와 EOA를 하나로 합쳐 사용할 수 있도록 Account Abstraction이 등장

 

자세한 설명은 아래를 참고해주세요


Tx info

실제 AA 관련 tx를 자세히 뜯어서 본적은 없어서 이번 기회에 조금 뜯어서 살펴보았습니다. (entrypoint 0.6버전 기준)

해당 Tx를 확인해보면 0x664eFbF41038dd0Dcf71cbCd3be4Dfc3224Cea48

→ Entry point로 calldata를 날려서 NFT가 mint된 것을 확인할 수 있음.

 

분석하며 찾아볼 주소

  • bundler
  • paymaster
  • user account
  • account factory

UserOperation 구조체

UserOperation ⇒ 계정 추상화 전용 transaction 이라고 생각하시면 됩니다 (혼동을 피하기 위해 UserOp로 만듬)

userOp 확인해보면 paymaster를 설정해 놓은 부분을 확인 할 수 있었습니다.

paymaster 이용시 호출 흐름

 

이제 handleOps 함수 구현체에서 주요 함수들 call trace를 따라가보며 분석을 진행해보겠습니다.

handleOps

Validate Loop  [1.  _validatePrepayment]

 

_getRequiredPrefund

paymaster 유/무에 따라 계산되는 가스량의 가중치가 달라지며 결과적으로 예상되는 Gas를 계산해서 반환합니다.

 

_validateAccountPrepayment

initcode가 있으면 계정 새로 생성하며 결과적으로 데이터 검증 및 자금 확인하는 로직을 수행합니다.

_createSenderIfNeeded -> createSender -> createAccount 순으로 호출
실제 Tx initcode

initcode 구성 : factory주소 +createAccount selector + owner주소 + salt 로 구성됩니다.

factory 주소 뒤에 붙어있는 func 4bytes

이렇게 생성된 주소는 sender 주소가 되며 user account로 사용하게 됩니다

sender = proxy 형태
implementation 주소

createAccount 함수에서 확인할 수 있듯이 proxy 형태로 배포되어있으며 implementation 주소에는 LightAccount가 배포되어있는 것을 확인할 수 있습니다

LightAccount Contract

sender 주소로 validateUserOp를 호출 -> userOp의 데이터를 검증하고 필요시 자금을 충전하는 로직을 거치게 됨

  • paymaster 주소가 0 → 사용자가 직접 지불해야하니까 필요시 자금을 충전해야함

실제 Tx에서는 결과적으로 정상적인 흐름이니 반환되는 데이터는

 

_validatePaymasterPrepayment

userOp에 paymaster가 존재할 때만 호출

_validateAccountPrepayment 함수와 유사하게 데이터 검증 및 자금 확인합니다.

  •  _validateAccountPrepayment -> sender를 통해 데이터를 검증
  •   _validatePaymasterPrepayment -> paymaster를 통해 데이터를 검증

paymaster bytecode

paymaster 역시 proxy로 되어있고 validatePaymasterUserOp 함수를 살펴보겠습니다.

validatePaymasterUserOp 1

entrypoint에서 호출한건지 확인 후 검증 로직으로 넘어갑니다.

validatePaymasterUserOp 2

서명길이가 ECDSA 표준에 맞게 되어있는지 서명자의 주소가 맞는지 검사합니다.

검사 결과에 따라 반환되는 데이터가 다르며 UserOp 데이터를 가공하는 것으로 보이는데, 이 코드로만 정확하게 알기 힘든 것 같아서 유사한 코드를 좀 찾아봤습니다.

비슷한 것 같은 구성

결과적으로 context -> 0 , validationData -> uint256(0 + validUntil + validAfter) 이렇게 반환된 것으로 보입니다.

종합해보면 userOp의 paymasterAndData를 가공한 데이터는 다음과 같습니다. 

  • paymaster → 0x4fd9098af9ddcb41da48a1d78f91f1398965addc
  • validUntil → 0
  • validAfter → 0x00677ac8ea
  • r → 0x20a2eb3485515625beda040f99074654a1ec1284a077795b353d71e3a401da827
  • s → 0x873382361a90afe7ac8b9077f8f3f23c07cd562b92443bcc6b8230e44a486951
  • v → 0x1b

결과적으로 _validatePrepayment 함수는 userOp 실행시 필요한 가스를 계산, 자금 확인, 데이터 검증 진행 하는 것을 알 수 있었습니다.


Validate Loop  [2.  _validateAccountAndPaymasterValidationData]

두 번째로 호출되는 함수는 이전에 저장된 데이터(validateData, pmvalidateData)를 검증하는 로직을 수행합니다.

두 번째로 호출되는 함수는 이전에 저장된 데이터(validateData, pmvalidateData)를 검증하는 로직을 수행합니다.

_validateAccountPrepayment 호출 후 반환된 값 -> 0 , _getValidationData 호출시 반환 데이터는 address(0), false

  • 첫 번째 조건문 → handleOps로 호출해서 expectedAggregator -> 0이므로 통과
  • 두 번째 조건문 → false 이므로 통과

_validatePaymasterPrepayment 호출 후 반환된 값 -> 0,

uint256(0 + validUntil + validAfter),  _getValidationData호출시 반환 데이터는  address(0), false

  • 첫 번째 조건문 → aggregator은 address(0)이므로 통과 (0이 아니면 서명 검증을 통과 못한 것으로 추정)
  • 두 번째 조건문 → block.timstamp 기준으로 유효한 기간인지 평가함 → false 이므로 통과

Execution Loop  [1.  _executeUserOp]

context -> 0 이후 innerHandleOp 함수 호출

  • innerHandleOp가 internal 호출로 실행 ->msg.sender는 tx.origin
  • this.innerHandleOp를 통해 호출하면, msg.sender는 항상 address(this)로 설정

innerHandleOp

innerHandleOp 함수에서 calldata대로 함수를 실행하게 됩니다.

Tx calldata

스캐너에서 봤던 NFT 민팅 요청을 여기서 호출하는 것을 확인 할 수 있었습니다.

이후 execute에 필요한 가스 + 검증 과정에 사용한 가스를 계산해서 _handlePostOp 함수로 넘겨줍니다.

 

_handlePostOp

가스’비’를 계산하고 paymaster의 postOp 함수를 호출 (paymaster x → 쓰고 남은 가스 환불)

  • EIP-1559에서는 maxFeePerGas와 maxPriorityFeePerGas가 분리되기 때문에 고려한 코드로 보임 maxFeePerGas와 maxPriorityFeePerGas + block.basefee 둘 중 더 작은 값 채택

postOp

별거 없다

데이터 길이, 모드, entrypoint에서 호출한지 확인하고 이상 없으면 넘어가는 걸로 보여집니다.

postOp 호출 이후 ~

이후 남는 건 _incrementDeposit 함수 호출해서 반환해줍니다. 

 

_executeUserOp 호출 -> _handlePostOp두 번 호출되는 형태로 구성되어 있어 의문이 들었습니다.

_handlePostOp를 두 번 호출할까?

첫 번째 호출 → 정상적인 처리

  • execute 호출 후 postOp가 정상적으로 실행되면 문제 X → paymaster는 사용된 가스비를 결제하고 끝

두 번째 호출 → 예외 상황 처리

  • 만약 postOp에서 revert → UserOp 실행 자체도 돌아가버림 → 이때 entrypoint는 paymaster가 사용된 가스비만큼 결제할 수 있도록 다시 postOp를 호출

결과적으로 _handlePostOp를 두 번 호출하는 이유는 가스비 회수를 보장하기 위함으로 보여집니다.


bundler : 0x664eFbF41038dd0Dcf71cbCd3be4Dfc3224Cea48

paymaster : 0x4fd9098af9ddcb41da48a1d78f91f1398965addc

user account : 0xA64D52f0f91a82452F0C9566193d56f92598416E

account factory : 0x00004EC70002a32400f8ae005A26081065620D20

'Analyze' 카테고리의 다른 글

Bybit 해킹  (3) 2025.03.08
Compound V2 donation attack  (0) 2024.11.10

+ Recent posts