Patch Thursday — 이더리움 계정 추상화의 모든 것

ChainLight
Theori BLOG
Published in
33 min readSep 1, 2023

--

본 글은 이더리움상의 탈중앙화 서비스를 이용하는 웹3 사용자의 편의성과 진입 장벽을 개선할 수 있는 ‘계정 추상화(Account Abstraction)’에 대해 소개합니다.

들어가기에 앞서,

이더리움 네트워크와 상호작용하기 위해서는 수수료의 개념과 개인 키(private key) 관리 등의 블록체인 전반에 대한 지식과 경험이 필요합니다. 사용자가 서비스를 이용하기 위해서는 웹3 지갑을 다운받고, ETH를 거래소에서 구매하고, 서명이 필요한 작업마다 일일이 서명해야 하는 등의 불편함이 존재합니다. 이러한 절차는 사용자 경험 및 진입 장벽 문제와 직결됩니다.

계정 추상화는 앞서 언급한 문제를 해결할 수 있는 개념으로, 최근 이더리움 생태계에서 각광받고 있는 기술입니다. 계정 추상화는 이더리움 내 사용자의 개인 키로 제어되는 EOA(Externally Owned Accou)와 스마트 컨트랙트인 CA(Contract Address) 사이의 경계를 없애 둘의 역할을 동시에 수행할 수 있도록 계정을 추상화*하는 것을 의미합니다.

*추상화는 프로그래밍의 복잡도와 사용자 경험을 개선하는 방식의 일종입니다. 사용자가 프로그램이 어떻게 구현되어 있는지 알지 못해도 동작시킬 수 있도록 단순화하는 것을 ‘추상화’한다고 표현합니다. 계정 추상화는 트랜잭션 전송, 지갑 관리 등 계정의 다양하고 복잡한 동작을 단순화한 것을 의미합니다.

계정 추상화가 실현된다면, 하기와 같은 개선점을 통해 사용자 경험 개선 및 진입 장벽 해소를 기대할 수 있습니다.

  • 이메일을 통해 웹3 서비스와 상호 작용
  • 서명의 편리성
  • 신용카드를 통한 가스비 지불

이더리움 생태계 내에서는 그간 EIP-2938 등 다양한 형태의 계정 추상화 방안이 제안되어 왔지만, 이더리움 프로토콜에 중대한 수정을 요구한다는 단점이 계정 추상화 도입의 장애물이었습니다. 본 글에서는 계정 추상화 중에서도, 이더리움 컨센서스의 변경 없이 탈중앙화 형태의 계정 추상화 기능을 구축할 수 있어 많은 관심을 받은 동시에, 이더리움 정식 제안으로 채택된 ERC-4337을 자세히 살펴봅니다.

ERC-4337의 구성 요소

상기 그림은 ERC-4337을 통해 제출된 트랜잭션이 이더리움 네트워크에 도달하기까지의 과정을 대략적으로 보여줍니다.

사용자는 UserOperation이라는 트랜잭션과 비슷한 형태의 객체를 별도의 멤풀(mempool)인 대체 멤풀(alternative mempool)에 전송합니다. 제출된 UserOperation은 번들러(Bundler)라고 불리는 별도의 행위자에 의해 선택되는데, 번들러는 대체 멤풀에서 여러 UserOperation을 묶어 하나의 트랜잭션으로 만들고, 이를 네트워크에 제출합니다. 이 과정에서 트랜잭션에 대한 가스비는 트랜잭션 제출 주체인 번들러가 지불하게 되는데, 번들러는 사용자가 미리 엔트리포인트에 입금해 뒀던 ETH를 지급받거나, 사용자의 수수료를 대납해 줄 페이마스터(Paymaster)라는 컨트랙트에서 여러 형태의 수수료를 지급받을 수 있습니다.

하기의 이어지는 섹션에서는 ERC-4337을 구성하는 각 요소를 소개합니다.

1. UserOperation

UserOperation은 쉽게 말해 추상화된 트랜잭션이라고 할 수 있습니다.

  • EIP-4337에서는, UserOperation을 사용자가 전송하고자 하는 트랜잭션에 대해 묘사하는 구조체로 설명합니다.

UserOperation은 기존 트랜잭션이 담고 있는 입력값에 더해, 번들러나 페이마스터 등등 ERC-4337을 구성하는 여러 요소에 전달해야 하는 정보를 담고 있습니다.

UserOperation 객체(이하 UserOp)는 트랜잭션과 비슷하게 sender, nonce와 같은 필드를 가지고 있지만, 기존 트랜잭션에는 포함되지 않던 initCode, callData, verificationGasLimit, paymasterAndData 등의 필드를 포함합니다.

struct UserOperation {
address sender;
uint256 nonce;
bytes initCode;
bytes callData;
uint256 callGasLimit;
uint256 verificationGasLimit;
uint256 preVerificationGas;
uint256 maxFeePerGas;
uint256 maxPriorityFeePerGas;
bytes paymasterAndData;
bytes signature;
}

2. 번들러(Bundler)

번들러는 사용자 대신 이더리움 네트워크에 트랙잭션을 제출하는 역할을 하는 객체입니다. 번들러는 대체 멤풀에 제출된 UserOp 객체들을 묶어 묶음 트랜잭션(bundle transaction)을 생성합니다. 번들러는 제출한 묶음 트랜잭션이 정상적으로 실행되면 사용자나 페이마스터로부터 수수료를 받습니다.

하지만 이러한 *환급(reimbursement)은 UserOp가 정상적으로 실행될 때만 받을 수 있습니다.

*환급(reimbursement)이란, 번들러가 트랜잭션 제출 과정에서 소모한 수수료를 사용자 혹은 페이마스터로부터 돌려받는 개념을 뜻합니다.

3. 엔트리포인트(EntryPoint)

엔트리포인트(Entrypoint)는 번들러가 제출한 묶음 트랜잭션의 유효성을 검증하고, 이후 이를 실제 온체인에서 실행시키는 역할을 수행합니다. 엔트리포인트는 UserOp의 검증 단계와 실행 단계를 나눠 진행하며, 위 그림의 2~3번 처럼 모든 UserOp의 검증이 끝난 이후 실행 단계가 진행됩니다. UserOp의 각 단계에 대해서는 “ERC-4337의 동작 방식” 섹션에서 소개해 드릴 예정입니다.

4. 페이마스터(Paymaster)

페이마스터는 사용자 대신 트랜잭션 제출에 대한 수수료를 지불하는 컨트랙트입니다. 이 컨트랙트는 엔트리포인트와 상호 작용하며, 수수료 지불 의사 확인 및 수수료를 번들러에게 전달하는 역할을 합니다.

ERC-4337의 동작 방식

본 섹션은, ERC-4337의 트랜잭션 제출부터 실행까지의 일련의 과정을 설명합니다. UserOp가 사용자에 의해 대체 멤풀에 제출되면, 번들러는 제출된 UserOp에 대해 시뮬레이션(simulation) 과정을 거칩니다. 번들러는 시뮬레이션에 성공한 UserOp들을 묶음 트랜잭션의 형태로 엔트리포인트에 제출합니다. 엔트리포인트는 UserOp에 대한 유효성을 검증한 뒤 이를 실행합니다. 이후 페이마스터 지정 여부에 따라 페이마스터, 혹은 엔트리포인트가 번들러에게 수수료를 환급하는 과정을 거칩니다.

동작 방식의 각 과정은 다음과 같이 추릴 수 있습니다.

  1. 사용자가 UserOp을 대체 멤풀에 제출
  2. 번들러는 제출된 UserOp에 대해 시뮬레이션(simulation) 과정 수행
  3. 번들러는 시뮬레이션에 성공한 UserOp들을 묶음 트랜잭션의 형태로 엔트리포인트에 제출
  4. 엔트리포인트는 UserOp에 대한 유효성을 검증한 뒤 이를 실행
  5. 이후 페이마스터 지정 여부에 따라 페이마스터, 혹은 엔트리포인트가 번들러에게 수수료를 환급하는 과정 수행

이어지는 섹션은 각 과정의 상세 사항을 설명합니다.

1. 사용자의 UserOp 제출

사용자는 어떠한 트랜잭션이 실행되어야 하는지를 UserOp에 채워 대체 멤풀에 제출합니다.

2. 번들러의 묶음 트랜잭션 구성 및 제출

번들러는 사용자가 대체 멤풀에 제출한 UserOp들을 묶어 묶음 트랜잭션을 생성하며, 생성한 묶음 트랜잭션을 엔트리포인트에 전송하는 역할을 수행합니다. 트랜잭션을 전송하는 역할을 수행하는 과정에서, 소모한 가스비를 환급받을 수 있을지 여부를 판단하기 위해 오프체인에서 UserOp에 대한 시뮬레이션을 진행합니다. 번들러는 시뮬레이션 이후 엔트리포인트 컨트랙트의 handleOps 함수를 호출하는 과정을 통해, 생성한 묶음 트랜잭션을 제출합니다.

UserOp의 시뮬레이션에 대한 더 깊은 이해를 위해, 시뮬레이션의 각 단계와 시뮬레이션을 통과한 UserOp가 실제 온체인 제출 시 실패하는 경우를 방지하는 ERC-4337의 예방책에 대해 설명드리겠습니다.

번들러의 묶음 트랜잭션 구성과 제출 과정에서 발생하는 시뮬레이션 과정은 다음과 같이 기술할 수 있습니다.

UserOp에 대한 번들러의 시뮬레이션

UserOp의 실행방식은 검증 단계(Verification phase)와 실행 단계(Execution phase)로 나뉘어져 있습니다. UserOp가 정상적으로 실행 가능한지를 확인하기 위해 오프체인에서 모든 작업을 수행하는 것은 많은 자원이 필요하고, 특히 유효하지 않은 트랜잭션이 많다면 이를 검증하는 데 오랜 시간이 소요되기 때문입니다.

번들러는 UserOp가 검증 단계만 정상적으로 통과하면 수수료 환급을 받을 수 있습니다. 이는 실행 단계가 컨트랙트에서 별도로 실행되고, 실행 단계의 실패가 트랜잭션의 실패로 연결되지 않기 때문입니다. 따라서 번들러는 멤풀에 있는 UserOp에 대해 오프체인에서 검증 단계를 통과 여부를 빠르게 검사하고, 통과된 UserOp만으로 묶음 트랜잭션을 생성합니다.

시뮬레이션 성공 여부와는 상관 없이, UserOp의 온체인 실행의 실패가 발생할 수 있습니다. 실패 방지를 위한 방지책은 무엇일까요?

UserOp의 온체인 실행 실패에 대한 방지책

시뮬레이션은 성공했지만, 실제 온체인에서 UserOp 실행이 실패하는 경우 번들러는 금전적인 손실을 보게 됩니다. 따라서 시뮬레이션과 실제 온체인(On-Chain) 제출 결과가 달라지지 않게 하는 것이 중요합니다. 예를 들어 검증 단계에서 블록의 타임스탬프(timestamp) 값이 1000 이하일 때만 통과시키는 UserOp이 있다고 가정해 봅시다. 시뮬레이션 시에는 이와 같은 조건이 통과될 수 있지만, 실제 트랜잭션이 블록에 포함될 때의 타임스탬프가 1000보다 높다면 온체인에서는 해당 트랜잭션이 실패할 수 있습니다.

이러한 상황을 방지하고자, EIP-4337에서는 시뮬레이션과 실제 실행 결과에 차이를 줄 수 있는 모든 정보에 대한 접근을 검증 단계에서 허용하지 않습니다. 따라서 검증 단계에서는 block time, block number, block hash와 같은 opcode를 사용할 수 없습니다. 이뿐만 아니라, 검증 단계에서는 *트랜잭션 송신자 주소와 연관된 스토리지만 접근할 수 있는 제약이 있습니다. 만약 여러 개의 UserOp이 같은 스토리지에 접근할 수 있다면, 하나의 UserOp이 스토리지에 접근한 결과 때문에 같은 스토리지를 참고하여 연산을 수행하는 다른 UserOp들이 유효하지 않게 바뀔 수 있기 때문입니다. 이러한 제약 조건의 준수 여부는 시뮬레이션 시 번들러 소프트웨어(노드)가 검증해야 합니다.

*https://eips.ethereum.org/EIPS/eip-4337#:~:text=Storage%20is%20enabled.-,Storage%20associated%20with%20an%20address,-We%20define%20storage

3. 엔트리포인트의 묶음 트랜잭션 처리

번들러는 묶음 트랜잭션의 제출을 위해 엔트리포인트(EntryPoint) 컨트랙트의 handleOps() 함수를 호출합니다. 이 함수에서는 내부적으로 UserOp이 유효한지 확인하고, UserOp을 제출한 계정으로부터 수수료를 받습니다. 검증 단계에서 계정이 수수료를 먼저 지불하기 때문에, 실행 단계가 실패하더라도 번들러는 수수료를 받을 수 있습니다. 검증이 끝난 묶음 트랜잭션은 엔트리포인트에 의해 실행됩니다.

아래는 엔트리포인트 컨트랙트의 handleOps() 함수의 소스 코드입니다.

function handleOps(UserOperation[] calldata ops, address payable beneficiary) public nonReentrant {
uint256 opslen = ops.length;
UserOpInfo[] memory opInfos = new UserOpInfo[](opslen);
unchecked {
for (uint256 i = 0; i < opslen; i++) {
UserOpInfo memory opInfo = opInfos[i];
(uint256 validationData, uint256 pmValidationData) = _validatePrepayment(i, ops[i], opInfo);
_validateAccountAndPaymasterValidationData(i, validationData, pmValidationData, address(0));
}
uint256 collected = 0;
emit BeforeExecution();
for (uint256 i = 0; i < opslen; i++) {
collected += _executeUserOp(i, ops[i], opInfos[i]);
}
_compensate(beneficiary, collected);
}
}

UserOp에 대한 유효성 검증

위 코드에서 UserOp의 유효 여부, 즉 _validatePrepayment() 내에서 account.validateUserOp()가 호출될 때의 검증 로직을 각 사용자가 원하는 대로 설계하고 동작하도록 할 수 있다는 것이 중요합니다.

예를 들어, 사용자는 이더리움의 타원곡선 디지털서명 알고리즘(ECDSA, Elliptic Curve Digital Signature Algorithm) 방식을 벗어나 다양한 서명 알고리즘을 활용할 수 있습니다. 다만 앞서 번들러의 검증 과정에서도 언급했듯, 시뮬레이션과 실제 온체인 실행 결과가 달라지지 않도록 검증 단계에선 opcode와 스토리지 접근에 대한 제약사항이 존재합니다. 또한 서로 다른 엔트리포인트 컨트랙트, 혹은 서로 다른 체인에서 재사용 공격(replay attack)이 발생하는 것을 방지하기 위해 서명은 chainId와 엔트리포인트 컨트랙트의 주소에 종속되어야 합니다.

contract EntryPoint {
function getUserOpHash(UserOperation calldata userOp) public view returns (bytes32) {
return keccak256(abi.encode(userOp.hash(), address(this), block.chainid));
}
}

// ref: https://www.youtube.com/watch?v=edPJaUYWlhY&list=LL&index=1
contract Test {
function testSignature(UserOpration memory op, EntryPoint entryPoint) public {
bytes32 userOpHash = entryPoint.getUserOpHash(op);
bytes32 signHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", userOpHash));
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePk, signHash);
op.signautre = abi.encodePacked(r, s, v);
}
}

위 코드는 사용자가 UserOp에 대한 서명을 생성하는 방법의 예시입니다. 사용자는, 지정한 엔트리포인트의 getUserOpHash() 함수를 통해 UserOp과 엔트리포인트의 주소, 그리고 chainId에 종속되는 서명을 생성할 수 있습니다.

앞서 언급했듯이, UserOp을 제출한 계정(Account)은 사용자가 정의한 검증 로직을 사용할 수 있습니다. 엔트리포인트에서 사용자 계정의 validateUserOp() 함수를 호출하면, 계정은 UserOp을 받아 서명이 유효한지 확인합니다. 서명이 유효하다면, missingAccountFunds 만큼의 ETH를 엔트리포인트에 입금하여 가스비를 번들러에게 지불합니다. 이때 missingAccountFunds 보다 더 큰 ETH를 입금할 수 있고, 남은 금액은 다음 UserOp을 위해 사용됩니다. 계정은 특정 엔트리포인트 컨트랙트를 지정하여 화이트리스트로 등록해야 하며, 화이트리스트로 등록된 엔트리포인트만이 validateUserOp() 함수를 호출할 수 있습니다.

function validateUserOp(
UserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) external virtual override returns (uint256 validationData) {
_requireFromEntryPoint();
validationData = _validateSignature(userOp, userOpHash);
_validateNonce(userOp.nonce);
_payPrefund(missingAccountFunds);
}

또한 엔트리포인트 컨트랙트는 페이마스터의 동작을 호출하는 역할을 담당합니다. UserOp이 별도의 페이마스터를 지정할 경우, 검증 단계에서 사용자에게 수수료를 징수하는 대신 페이마스터의 수수료 지불 의사를 확인하고, 이를 허용할 경우 수수료를 받아 번들러에게 지급합니다.

4. 페이마스터의 수수료 환급 및 postOp 수행

엔트리포인트가 제출한 트랜잭션이 정상적으로 실행될 경우, 페이마스터는 지정된 로직을 통해 번들러에게 수수료를 지급합니다. 또한 수수료 지급 이후 실제 소모한 가스비 측정 등 사전에 정의한 동작을 수행할 수 있는데, 이를 postOp라고 합니다.

페이마스터는 여러 가지 형태로 구현될 수 있으며, 현재는 크게 두 가지 시나리오가 존재합니다.

1) 온체인 서명 검증용 페이마스터

비자(Visa)는 작년부터 계정 추상화를 이용한 자동 결제 및 이더리움의 가스비를 비자 카드로 결제할 수 있는 방안을 고안해 왔으며, 8월 11일 비자가 구현해 배포한 페이마스터의 동작 구조와 과정을 설명하겠습니다.

출처: https://usa.visa.com/solutions/crypto/paying-blockchain-gas-fees-with-card.html

동작 구조

위 그림에서 확인할 수 있듯이, 비자(Visa)가 구현한 페이마스터는 다음과 같은 순서로 동작합니다.

  1. 비자는 사용자의 신용카드를 이용해 오프체인 상에서 가스비에 해당하는 금액을 결제합니다.
  2. 결제가 정상적으로 완료될 경우, 비자의 페이마스터 웹 애플리케이션은 페이마스터의 서명을 사용자에게 반환합니다.
  3. 사용자가 페이마스터의 서명과 함께 UserOp을 제출합니다.
  4. UserOp의 검증 단계에서 엔트리포인트가 페이마스터의 가스비 지불 의사를 확인합니다. 이 과정에서 페이마스터의 validatePaymasterUserOp() 함수를 호출합니다.
  5. 비자의 페이마스터는 온체인에서 사용자의 서명을 검증하고 가스비를 대신 지불합니다. 이때 페이마스터는 충분한 수량의 ETH를 엔트리포인트에 미리 입금해 놓아야 합니다.

validatePaymasterUserOp() 의 동작은 이더리움 고얼리(Goerli) 테스트넷에 *배포된 비자 페이마스터 컨트랙트를 통해 확인할 수 있습니다.

*https://goerli.etherscan.io/address/0x810a1797ffe00936c7da5723e474fe23cecdd6e9#code

function validatePaymasterUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost) external override returns (bytes memory context, uint256 validationData) {
_requireFromEntryPoint();
return _validatePaymasterUserOp(userOp, userOpHash, maxCost);
}

function _validatePaymasterUserOp(UserOperation calldata userOp, bytes32 /*userOpHash*/, uint256 requiredPreFund) internal override returns (bytes memory context, uint256 validationData) {
(requiredPreFund);
(uint48 validUntil, uint48 validAfter, bytes calldata signature) = parsePaymasterAndData(userOp.paymasterAndData);

// ECDSA library supports both 64 and 65-byte long signatures.
// we only "require" it here so that the revert reason on invalid signature will be of "VerifyingPaymaster", and not "ECDSA"
require(signature.length == 64 || signature.length == 65, "VerifyingPaymaster: invalid signature length in paymasterAndData");
bytes32 hash = ECDSA.toEthSignedMessageHash(getHash(userOp, validUntil, validAfter));
senderNonce[userOp.getSender()]++;

// don't revert on signature failure: return SIG_VALIDATION_FAILED
if (verifyingSigner != ECDSA.recover(hash, signature)) {
return ("",_packValidationData(true,validUntil,validAfter));
}

// no need for other on-chain validation: entire UserOp should have been checked
// by the external service prior to signing it.
return ("",_packValidationData(false,validUntil,validAfter));
}

페이마스터는 _requireFromEntryPoint() 함수를 통해 사전에 지정된 엔트리포인트만 호출할 수 있도록 합니다. 하기의 시나리오는 _requireFromEntryPoint() 함수의 호출 과정과 서명 검증 과정을 포함하고 있습니다.

  1. paymasterAndData를 파싱(parsing)한 뒤, ECDSA 암호 알고리즘을 통해 데이터 서명자(signer)를 복구하여 서명자가 페이마스터에 등록되었는지 여부를 확인합니다.
  2. _validatePaymasterUserOp는 서명 검증이 성공할 경우 context와 validationData를 리턴(return)합니다. 여기서는 context에 아무런 값이 들어가지 않으므로 페이마스터의 postOp 호출 절차는 생략됩니다.
  3. aggregator가 false(0)이면 서명 검증에 성공했음을 의미합니다.

참고 사항: 엔트리포인트는 block.timestamp와 같은 특정 opcode를 사용하지 못하는 제약이 없으며, 이러한 제약은 call depth가 2 이상일 때에만 적용됩니다. 즉 account, paymaster, account factory 등에 대해서만 검증 단계에서 이러한 제약이 강제됩니다.

validationDataaggregator, validUntil, validAfter가 압축된 형태로, 서명에 대한 검증이 성공했는지 여부와 검증의 유효 기간을 담고 있습니다.

validUntilvalidAfter는 각각 서명이 언제까지 유효한지, 언제부터 유효한지를 나타내며 이는 엔트리포인트에서 현재의 블록 타임스탬프와 비교함으로써 유효성이 검증됩니다.

위의 시나리오에서 페이마스터가 고려해야 할 사항은 페이마스터의 가스대납에 대한 보상으로써, 사용자로부터 얼마나 많은 돈을 지불받을지입니다.

후불 결제의 경우는 UserOp이 실행된 시점을 기준으로 페이마스터가 대납한 가스비를 산정하여 사용자에게 청구하면 되지만, 비자의 시나리오의 경우 UserOp이 실행되기 전에 결제가 진행되므로 지불 시점에 UserOp이 얼마나 많은 가스비를 소비할지 정확히 알 수 없습니다.

따라서 페이마스터는 UserOp에 지불할 수 있는 최대 가스비가 얼마인지를 먼저 산정하고 이를 기반으로 오프체인 결제를 진행해야 합니다.

페이마스터는 엔트리포인트가 계산한 *선금(prefund)만큼의 ETH를 우선 지불하고, UserOp의 실행 단계가 끝난 후 실제 사용된 가스비를 제한 나머지를 돌려받습니다.

*선금은 UserOp 검증 단계가 정상적으로 수행될 경우를 위해 미리 지불하는 금액이며, 페이마스터는 실제 사용될 가스비 이상의 선금을 예치해야 합니다. 선금의 지불은 아래의 _getRequiredPrefund()를 통해 이루어집니다.

따라서 간단한 방법의 하나는 페이마스터가 선금만큼의 금액을 오프체인 결제에서 지불받으면 됩니다. 즉, 페이마스터는 UserOp에 기재된 가스비 관련 값을 기준으로 UserOp을 실행하지 않더라도 선금의 액수를 계산할 수 있으며, 실제 UserOp이 실행되는 시점 블록의 기본 수수료(base fee)에 대한 고려는 번들러의 몫입니다. 번들러는 현재 블록의 기본 수수료 기준 가스비가 사용자가 지정한 가스비의 최댓값을 넘지 않는지 블록마다 확인할 필요가 있습니다.

function _getRequiredPrefund(
MemoryUserOp memory mUserOp
) internal pure returns (uint256 requiredPrefund) {
unchecked {
// When using a Paymaster, the verificationGasLimit is used also to as a limit for the postOp call.
// Our security model might call postOp eventually twice.
uint256 mul = mUserOp.paymaster != address(0) ? 3 : 1;
uint256 requiredGas = mUserOp.callGasLimit +
mUserOp.verificationGasLimit *
mul +
mUserOp.preVerificationGas;
requiredPrefund = requiredGas * mUserOp.maxFeePerGas;
}
}

2) 토큰 페이마스터

이 시나리오에서는 페이마스터가 계정으로부터 수수료에 상응하는 ERC-20 토큰을 수령합니다. 동작 과정은 아래와 같습니다.

  1. 사용자가 USDC 지불을 허용하는 페이마스터를 포함하도록 UserOp을 생성하고 대체 멤풀에 제출합니다. 사용자의 계정은 페이마스터에 대해 USDC approval을 미리 수행했다고 가정합니다.
  2. 번들러에 의해 엔트리포인트의 handleOps() 함수가 호출되고, 페이마스터가 예치한 ETH가 충분한지 확인합니다. ETH가 충분할 경우, 페이마스터의 validatePaymasterUserOp() 함수를 호출하여 사용자 대신 가스비를 지불할 의사가 있는지 확인합니다.
  3. 페이마스터는 사용자의 계정이 충분한 USDC를 페이마스터에 대해 approve 했는지를 확인하고, 충분한 USDC를 approve 한 경우 가스비를 대신 지불합니다. 실제 USDC를 전송하는 작업은 UserOp 실행 이후 postOp() 함수에서 이루어집니다.
  4. 엔트리포인트의 _executeUserOp()에서는 UserOp의 본집행(main execution)을 수행합니다. 본집행이 정상적으로 종료될 경우 페이마스터의 postOp()가 호출됩니다.
  5. 페이마스터의 postOp() 은 실제로 사용된 가스비만큼의 USDC를 계산하여 사용자의 계정으로부터 USDC를 전송받는 함수를 호출합니다.

위 시나리오에서 페이마스터가 항상 UserOp를 제출한 계정으로부터 USDC를 받을 수 있는지가 중요합니다. 4번의 본집행 수행 과정에서 UserOp가 페이마스터에 대한 USDC approve를 취소하거나, USDC를 모두 다른 주소로 전송함으로써 페이마스터에 대한 ERC-20 토큰의 지불을 회피하는 공격을 생각해 봅시다. 이처럼 UserOp의 실행 단계에서의 영향으로 페이마스터의 postOp이 실패하는 경우, UserOp의 본집행 수행 단계를 건너뛰고 postOp만을 다시 실행합니다. 아래 코드를 통해 이를 자세히 확인할 수 있습니다.

function _executeUserOp(
uint256 opIndex,
UserOperation calldata userOp,
UserOpInfo memory opInfo
) private returns (uint256 collected) {

try this.innerHandleOp(userOp.callData, opInfo, context) returns (
uint256 _actualGasCost
) {
collected = _actualGasCost;
} catch {

uint256 actualGas = preGas - gasleft() + opInfo.preOpGas;
collected = _handlePostOp(
opIndex,
IPaymaster.PostOpMode.postOpReverted,
opInfo,
context,
actualGas
);
}
}

ERC-4337의 보안 고려사항

계정 추상화의 사용을 위해서는 위에서 설명한 컴포넌트(component)별 보안 고려사항 외에도 추가로 고려해야 할 사항들이 존재합니다.

번들러

  • 블록 내 이전 트랜잭션으로 인한 충돌: 번들 트랜잭션이 블록에 포함될 때, 같은 블록 내 이전 트랜잭션들이 공격의 의도가 없어도(프런트 러닝, 샌드위치 공격 등) UserOp를 실패하도록 만들 수 있습니다. UserOp의 실패를 방지하기 위해서는 번들러가 블록의 첫 번째 트랜잭션으로 UserOp의 번들을 위치시켜야 합니다. 따라서, 번들러는 블록 빌더이거나, MEV-Boost와 같은 블록 빌딩 인프라를 사용해야 합니다.
  • 레이어2 네트워크 문제: 아비트럼과 옵티미즘 같은 L2에서는 하나의 시퀀서(sequencer)가 블록을 생성하기 때문에 번들러가 UserOps 번들을 블록 내 첫번째 트랜잭션으로 배치하기가 어렵습니다. 트랜잭션이 다른 블록 내 트랜잭션과 충돌하여 번들 트랜잭션이 실패할 경우, 번들러에 금전적 손실이 발생해 번들러가 활동할 동기를 저하시킵니다. 이를 해결하기 위해 번들 트랜잭션의 성공을 특정 조건(account values, storage slots, block numbers, timestamps)에 따라 보장할 수 있는 별도의 RPC 엔드포인트가 제안되었습니다.
  • 가스비 예측 문제: 시뮬레이션에서 측정한 가스 비용이 실제 온체인에서의 UserOp 실행 가스 비용보다 작은 경우 트랜잭션이 가스비 미달로 실패할 수 있습니다. 이 경우에 번들러는 가스 비용을 돌려받지 못해 손해를 입게 됩니다.
  • 평판 시스템: 페이마스터가 시뮬레이션과는 달리 postOp을 의도적으로 실패시키는 경우 트랜잭션이 취소(revert)되고, 번들러는 가스비를 돌려받지 못하게됩니다. 이에 대한 해결 방안으로 페이마스터, Factory, Aggregator에 대한 오프체인 상의 평판 시스템이 제안되었습니다. 특정 페이마스터를 포함한 번들이 반복적으로 실패할 경우, 번들러는 해당 페이마스터를 사용하는 UserOp을 무시할 수 있습니다.

페이마스터

  • 오라클 가격 조작 위험: ERC20 토큰을 사용자로부터 지불받는 페이마스터는 ERC20 토큰의 가격을 오라클(oracle)로부터 정확히 받아와야 합니다. 예를 들어 페이마스터가 대신 지불해 주는 가스비 대비 10%를 추가로 징수한다고 했을 때, 오라클로부터 받은 ERC20 토큰의 가격이 실제 가격보다 10% 높다면 페이마스터는 손해를 입게 됩니다. 또한 페이마스터 구현 시 오라클 가격(oracle price) 업데이트 주기에 대해 면밀히 고려해야 합니다. 매 postOp이 실행될 때마다 오라클로부터 가격을 업데이트 받는 경우, 마지막 postOp을 통해 업데이트된 가격과 실제 가격 차이가 클 때는 오프체인(Off-Chain) 모니터링 툴을 통해 오라클 가격 업데이트 함수를 별도로 호출해 주어야 합니다.

사용자

  • 실행 과정에서 사용자의 ERC-20 토큰 소모: 토큰 페이마스터의 시나리오에서, UserOp은 지급하려는 ERC-20 토큰이 실행 과정에서 소모되지 않도록 해야 합니다. 예를 들어 ERC-20 토큰인 USDC를 페이마스터에게 가스비 명목으로 지급하는 경우, 모든 USDC를 다른 지갑으로 옮기는 UserOp을 실행한다면 postOp의 동작이 실패할 것입니다. postOp의 동작이 실패할 경우 UserOp의 실행 단계를 건너뛰고 postOp만 재호출되어 페이마스터가 USDC를 가져가므로, 사용자는 가스비만 지불하는 상황이 발생하게 됩니다. 이러한 과정이 반복되면 사용자는 트랜잭션을 처리하지 못한 채 계속 가스비만 소모하는 상황이 발생하게 됩니다. 따라서 계정 추상화를 지원하는 인터페이스상에서 이러한 경우가 발생하지 않도록 유의해야 합니다.
  • 엔트리포인트에 대한 가스비 설정: 사용자는 UserOp을 제출할 때 PVG(PreVerificationGas), VerificationGasLimit, CallGasLimit의 세 가지 가스 한도를 설정합니다. PVG는 여러 개의 UserOp을 묶어 엔트리포인트 컨트랙트에 보내는 과정에서 소모되는 가스비를 의미하는데, PVG는 사전에 계측되지 않기 때문에 주의하여 설정하지 않으면 금전적 손실을 발생시킬 수 있습니다. PVG를 너무 높게 설정할 경우 사용자가 과다한 가스비를 번들러에게 지불하게 되며, 너무 낮게 설정할 경우 번들러가 가스비를 감당하지 못해 트랜잭션이 취소되어 가스비를 무의미하게 소모할 수 있습니다.
  • 실행 로직: 계정 추상화 아래에서의 실행 로직에 대해서는 아직 많은 논의가 되고 있지 않습니다. 실행하고자 하는 작업과 EOA가 연결되어 있는 현재의 계정 추상화 특성상 새로운 기능을 추가하기 위해서는 계정을 복제하고 다시 빌드해야 하고, 코드가 지속적으로 재사용되기 때문에 취약점 발생 시 연쇄적인 문제가 발생할 수 있다는 문제가 있습니다. 이러한 단점을 해소하기 위해 단순히 지갑으로서 외부의 로직을 delegatecall 혹은 call로 호출하여 수행하는 것 대신 애플리케이션의 로직 자체를 모듈식(modular)으로 구성하고 지갑 그 자체에서 실행하는 것을 목표로 하는 모듈식 계정 추상화 방안이 최근에 제안된 바 있습니다.
  • 번들러에 의한 MEV 문제: 계정 추상화의 구조상 번들러는 사용자가 제출한 UserOp을 재정렬할 수 있습니다. 따라서 MEV와 계정 추상화를 연계해 발생할 수 있는 문제를 고려해야 합니다.

엔트리포인트

  • 취소 사유 폭증 취약점(Revert Reason Bombing Vulnerability): 외부 함수 호출 시 트랜잭션 취소가 발생할 때, 엔트리포인트는 취소 사유를 memory bytes의 형태로 받아옵니다. 하지만 취소 사유가 매우 긴 길이의 바이트일 시 엔트리포인트에서 이를 복사하는 데 많은 가스비가 소모되어 강제적인 트랜잭션 실패를 유도할 수 있습니다. 위 공격은 실행 단계에서 발생할 수 있으며, 내부 함수 호출이 아닌 엔트리포인트 컨트랙트 함수의 호출 과정에서 실패가 발생하기 때문에 트랜잭션의 실패를 시뮬레이션 단계에서 예측할 수 없습니다. 해당 취약점은 이미 패치되었지만, 이처럼 엔트리포인트에서의 예상치 못한 트랜잭션 실패는 번들러의 경제적 손실로 이어질 수 있어 별도의 엔트리포인트 컨트랙트를 설계하는 경우 이를 반드시 유의해야 합니다.

기존 DeFi / NFT 컨트랙트와의 상호작용

  • 다수의 컨트랙트는 msg.sender가 컨트랙트일 경우 상호작용을 거부합니다. ERC4337에서의 계정은 EOA가 아닌 컨트랙트로 취급되기 때문에 기존의 디파이(DeFi) / NFT 컨트랙트와의 상호작용에 장벽이 있습니다. 또한 기존의 디파이 및 NFT 컨트랙트들이 계정 추상화를 지원하기 위해 msg.sender가 컨트랙트이더라도 상호작용이 가능하도록 로직을 변경할 경우, 사용자 컨트랙트로부터의 재진입 공격을 고려해야 합니다.

보안 감사를 요청하시려면 우측 링크를 참조하시기를 바랍니다. 👉 https://chainlight.io/

✨ We are ChainLight!

ChainLight 팀은 풍부한 실전 경험과 깊은 기술 이해를 바탕으로 새롭고 효과적인 블록체인 보안 기술을 연구합니다. 연구 결과를 바탕으로 웹3 생태계의 각종 보안 위험 요소와 취약점을 사전 파악하여 제거하는 혁신적인 보안 감사 서비스를 제공합니다. 보안 감사 이후에도 온체인 데이터 모니터링 및 취약점 탐지 자동화 서비스를 이용한 지속적인 디지털 자산 위험 관리 솔루션을 제공합니다.

ChainLight 팀은 사용자들이 탈중앙화 서비스를 안전하게 활용할 수 있도록 웹3 생태계 위협으로부터의 보호에 힘쓰고 있습니다.

  • ChainLight의 더 다양한 정보를 보고 싶으시다면? 👉 Twitter 계정도 방문해주세요.

🌐 Website: chainlight.io | 📩 TG: @chainlight | 📧 chainlight@theori.io

--

--

Established in 2016, ChainLight's award-winning experts provide tailored security solutions to fortify your smart contract and help you thrive on the blockchain