티스토리 뷰

계정 추상화 시리즈

2024.04.17 - [블록체인/Ethereum] - ERC-4337: 계정 추상화 - 테스트 수정 사항

2024.04.17 - [블록체인/Ethereum] - ERC-4337: 계정 추상화 - 테스트를 통한 Account와 EntryPoint의 동작 이해

2024.04.16 - [블록체인/Ethereum] - ERC-4337: 계정 추상화 - Account, EntryPoint, Paymaster


테스트에 참고한 컨트랙트

  • samples/SimpleAccountFactory.sol
 

account-abstraction/contracts at develop · eth-infinitism/account-abstraction

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

github.com

이전 게시글에서 이어집니다.

SimpleAccountFactory의 특징

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.23;

import "@openzeppelin/contracts/utils/Create2.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

import "./SimpleAccount.sol";

/**
 * A sample factory contract for SimpleAccount
 * A UserOperations "initCode" holds the address of the factory, and a method call (to createAccount, in this sample factory).
 * The factory's createAccount returns the target account address even if it is already installed.
 * This way, the entryPoint.getSenderAddress() can be called either before or after the account is created.
 */
contract SimpleAccountFactory {
    SimpleAccount public immutable accountImplementation;

    constructor(IEntryPoint _entryPoint) {
        accountImplementation = new SimpleAccount(_entryPoint);
    }

    /**
     * create an account, and return its address.
     * returns the address even if the account is already deployed.
     * Note that during UserOperation execution, this method is called only if the account is not deployed.
     * This method returns an existing account address so that entryPoint.getSenderAddress() would work even after account creation
     */
    function createAccount(address owner,uint256 salt) public returns (SimpleAccount ret) {
        address addr = getAddress(owner, salt);
        uint256 codeSize = addr.code.length;
        if (codeSize > 0) {
            return SimpleAccount(payable(addr));
        }
        ret = SimpleAccount(payable(new ERC1967Proxy{salt : bytes32(salt)}(
                address(accountImplementation),
                abi.encodeCall(SimpleAccount.initialize, (owner))
            )));
    }

    /**
     * calculate the counterfactual address of this account as it would be returned by createAccount()
     */
    function getAddress(address owner,uint256 salt) public view returns (address) {
        return Create2.computeAddress(bytes32(salt), keccak256(abi.encodePacked(
                type(ERC1967Proxy).creationCode,
                abi.encode(
                    address(accountImplementation),
                    abi.encodeCall(SimpleAccount.initialize, (owner))
                )
            )));
    }
}
  • 생성자에서 SimpleAccount 로직 레이어를 배포하고 주소를 immutable로 선언한 변수에 할당합니다.
  • create2 opcode를 사용하여 스마트 계정을 생성합니다. create2는 계정의 논스값 대신 salt와 initCode를 사용하여 결정론적으로 배포될 컨트랙트의 주소를 계산할 수 있습니다.
  • 따라서 getAddress 함수를 통해 createAccount 함수로 생성될 컨트랙트의 주소를 미리 계산할 수 있습니다.
  • 다만 이미 해당 주소에 컨트랙트가 배포되어 있는 경우(addr.code.length > 0)에는 새로운 컨트랙트가 배포되지 않습니다.

create와 create2

// create
address = keccak256(rlp([sender_address,sender_nonce]))[12:]

// create2
initialisation_code = memory[offset:offset+size]
address = keccak256(0xff + sender_address + salt + keccak256(initialisation_code))[12:]

Foundry 테스트 구성

SimpleAccountFactory.t.sol 파일 생성

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.24;

import {Test, console} from "forge-std/Test.sol";
import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol";
import {SimpleEntryPoint} from "../src/SimpleEntryPoint.sol";
import {SenderCreator} from "account-abstraction/core/SenderCreator.sol";
import {SimpleAccountFactory} from "../src/SimpleAccountFactory.sol";
import {SimpleAccount} from "../src/SimpleAccount.sol";
import {PackedUserOperation} from "account-abstraction/interfaces/PackedUserOperation.sol";
import {Counter} from "../src/Counter.sol";
import {UserOpUtils} from "./utils/UserOpUtils.sol";
import "./utils/Artifacts.sol";
  • Test, console : 테스트를 위한 컨트랙트와 라이브러리
  • IEntryPoint, SimpleEntryPoint : EntryPoint 컨트랙트와 인터페이스
  • SenderCreator : EntryPoint에게 sender 생성 요청을 받아 Factory로 전달하는 브로커
  • SimpleAccountFactory  : 스마트 계정의 팩토리 컨트랙트
  • SimpleAccount  : 계정 추상화 - 스마트 계정 구현체
  • PackedUserOperation : UserOperation을 패킹한 구조체 타입
  • Counter : Foundry 프로젝트 생성 시에 딸려오는 기본 컨트랙트로, 간단한 작업 요청에 사용하기 위한 용도
  • UserOpUtils : 테스트용 UserOp를 생성하고 서명하기 위한 유틸리티 컨트랙트. test/utils/UserOpUtils.sol 파일 참고
  • Artifacts : https://piatoss3612.tistory.com/162 참고. 컨트랙트를 배포해서 사용해도 무방. 이 방식을 사용한다고 해서 별다른 이점은 없습니다.

테스트 변수 및 이벤트 선언

SimpleEntryPoint public entryPoint;
SenderCreator public senderCreator;
SimpleAccountFactory public factory;
SimpleAccount public impl;
Counter public counter;
UserOpUtils public utils;

uint256 public ownerPrivateKey = uint256(keccak256("owner"));
address public owner;
address public deployer;
address public beneficiary;

setUp 함수

function setUp() public {
    owner = vm.addr(ownerPrivateKey);
    vm.label(owner, "Owner");
    vm.deal(owner, 100 ether);

    deployer = makeAddr("deployer");
    vm.label(deployer, "Deployer");

    beneficiary = makeAddr("beneficiary");
    vm.label(beneficiary, "Beneficiary");
    vm.deal(beneficiary, 1 ether);

    entryPoint = SimpleEntryPoint(payable(ENTRYPOINT_ADDRESS));
    vm.etch(address(entryPoint), ENTRYPOINT_BYTECODE);
    vm.label(address(entryPoint), "EntryPoint");

    senderCreator = SenderCreator(SENDERCREATOR_ADDRESS);
    vm.etch(address(senderCreator), SENDERCREATOR_BYTECODE);
    vm.label(address(senderCreator), "SenderCreator");

    factory = SimpleAccountFactory(FACTORY_ADDRESS);
    vm.etch(address(factory), FACTORY_BYTECODE);
    vm.label(address(factory), "SimpleAccountFactory");

    impl = SimpleAccount(payable(IMPL_ADDRESS));
    vm.etch(address(impl), IMPL_BYTECODE);
    vm.label(address(impl), "SimpleAccountImpl");

    vm.startPrank(deployer);
    counter = new Counter();
    vm.label(address(counter), "Counter");

    utils = new UserOpUtils();
    vm.label(address(utils), "UserOpUtils");
    vm.stopPrank();
}

배포된 컨트랙트 목록

  • EntryPoint
  • SenderCreator
  • SimpleAccountFactory
  • SimpleAccount
  • Counter
  • UserOpUtils

테스트

[CASE 1] Factory를 직접 호출하여 스마트 계정 생성

function test_FactoryCreateAccount() public {
    uint256 salt = 10000;
    address expectedAddr = factory.getAddress(owner, salt);

    assertEq(expectedAddr.code.length, 0);

    SimpleAccount simpleAccount = factory.createAccount(owner, salt);

    assertEq(address(simpleAccount), expectedAddr);
    assertGt(expectedAddr.code.length, 0);
    assertEq(simpleAccount.owner(), owner);
    assertEq(address(simpleAccount.entryPoint()), address(entryPoint));
}

1. 임의의 salt와 owner의 주소를 사용해 생성될 스마트 계정의 주소를 미리 계산합니다.

uint256 salt = 10000;
address expectedAddr = factory.getAddress(owner, salt);

 computeAddress 함수의 입력값은 다음과 같습니다.

  • 32바이트형으로 변환된 salt
  • 32바이트 바이트코드해시 : ERC1967 컨트랙트의 creation code와 생성자의 입력값(implementation, data)들을 abi 인코딩한 결과를 패킹한 것의 해시
function getAddress(address owner, uint256 salt) public view returns (address) {
    return Create2.computeAddress(
        bytes32(salt),
        keccak256(
            abi.encodePacked(
                type(ERC1967Proxy).creationCode,
                abi.encode(address(accountImplementation), abi.encodeCall(SimpleAccount.initialize, (owner)))
            )
        )
    );
}

 getAddress에서 호출된 computeAddress는 salt와 bytecodeHash에 더해 Factory 컨트랙트의 주소를 추가하여 다시 computeAddress를 호출합니다.

function computeAddress(bytes32 salt, bytes32 bytecodeHash) internal view returns (address) {
    return computeAddress(salt, bytecodeHash, address(this));
}

 computeAddress 함수는 어셈블리를 통해 생성될 컨트랙트의 주소를 계산합니다. 계산 과정은 다음과 같습니다.

  1. let ptr := mload(0x40) : mload(0x40)을 실행하여 ptr에 저장합니다. 0x40에 저장된 값은 free memory pointer로 일반적으로 0x80이 저장되어 있습니다.
  2. mstore(add(ptr, 0x40), bytecodeHash): 0x80에 0x40을 더한 뒤, 메모리의 해당 위치(0xc0)에 bytecodeHash를 저장합니다.
  3. mstore(add(ptr, 0x20), salt) : 0x80에 0x20을 더한 뒤, 메모리의 해당 위치(0xa0)에 salt를 저장합니다.
  4. mstore(ptr, deployer) : 0x80 위치에 deployer(Factory 컨트랙트의 주소)를 저장합니다. 20바이트 크기의 주소는 왼쪽에 12바이트 패딩을 추가하여 32바이트 크기의 오른쪽 정렬된 상태로 메모리에 저장됩니다.
  5. let start := add(ptr, 0x0b) : 0x80에 0x0b를 더한 뒤, 0x8b를 start에 할당합니다. 이 위치는 deployer 값이 시작되는 위치에서 하나 차이나는 위치로, 비어있는 바이트입니다.
  6. mstore8(start, 0xff) : 0x8b 위치에 0xff를 저장합니다.
  7. addr := keccak256(start, 85) : 0x8b부터 85개의 바이트를 입력으로 keccack256 해시 함수를 실행합니다. 입력은 0xff 1바이트 + deployer 20바이트 + salt 32바이트 + bytecodeHash 32바이트 크기로 85와 일치하는 크기입니다. 그리고 결과 해시를 addr에 할당하는데, 이때 32바이트 해시가 address 타입으로 자동으로 캐스팅됩니다.
function computeAddress(bytes32 salt, bytes32 bytecodeHash, address deployer) internal pure returns (address addr) {
    /// @solidity memory-safe-assembly
    assembly {
        let ptr := mload(0x40) // Get free memory pointer

        // |                   | ↓ ptr ...  ↓ ptr + 0x0B (start) ...  ↓ ptr + 0x20 ...  ↓ ptr + 0x40 ...   |
        // |-------------------|---------------------------------------------------------------------------|
        // | bytecodeHash      |                                                        CCCCCCCCCCCCC...CC |
        // | salt              |                                      BBBBBBBBBBBBB...BB                   |
        // | deployer          | 000000...0000AAAAAAAAAAAAAAAAAAA...AA                                     |
        // | 0xFF              |            FF                                                             |
        // |-------------------|---------------------------------------------------------------------------|
        // | memory            | 000000...00FFAAAAAAAAAAAAAAAAAAA...AABBBBBBBBBBBBB...BBCCCCCCCCCCCCC...CC |
        // | keccak(start, 85) |            ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ |

        mstore(add(ptr, 0x40), bytecodeHash)
        mstore(add(ptr, 0x20), salt)
        mstore(ptr, deployer) // Right-aligned with 12 preceding garbage bytes
        let start := add(ptr, 0x0b) // The hashed data starts at the final garbage byte which we will set to 0xff
        mstore8(start, 0xff)
        addr := keccak256(start, 85)
    }
}

 이렇게 계산된 주소는 배포된 컨트랙트의 주소가 아니므로 코드 크기는 0이어야만 합니다.

assertEq(expectedAddr.code.length, 0);

2. owner의 주소와 salt를 입력으로 Factory 컨트랙트의 createAccount 함수를 호출합니다.

SimpleAccount simpleAccount = factory.createAccount(owner, salt);

 addr의 코드 크기가 0보다 큰 경우는 이미 배포된 스마트 계정으로 간주하고 SimpleAccount로 캐스팅하여 반환합니다. 코드 크기가 0인 경우에는 새로운 계정을 배포합니다. 이 때 new 키워드를 사용하여 컨트랙트를 배포하는데, salt 값을 넣어줌으로써 create2 opcode를 사용해 컨트랙트를 배포하도록 합니다.

function createAccount(address owner, uint256 salt) public returns (SimpleAccount ret) {
    address addr = getAddress(owner, salt);
    uint256 codeSize = addr.code.length;
    if (codeSize > 0) {
        return SimpleAccount(payable(addr));
    }

    ret = SimpleAccount(
        payable(
            new ERC1967Proxy{salt: bytes32(salt)}(
                address(accountImplementation), abi.encodeCall(SimpleAccount.initialize, (owner))
            )
        )
    );
}

3. 계정이 배포되고 나면 다음 사항들을 확인할 수 있습니다.

  • 배포된 계정의 주소와 예상했던 주소가 동일해야 합니다.
  • 배포된 계정의 코드 크기가 0보다 커야 합니다.
  • 배포된 계정의 owner가 owner의 주소와 동일해야 합니다.
  • 배포된 계정의 entryPoint가 entryPoint의 주소와 동일해야 합니다.
assertEq(address(simpleAccount), expectedAddr);
assertGt(expectedAddr.code.length, 0);
assertEq(simpleAccount.owner(), owner);
assertEq(address(simpleAccount.entryPoint()), address(entryPoint));

테스트

$ cd contracts/
piatoss@PIATOSS:~/project/aa-from-scratch/contracts$ forge test --mt test_FactoryCreateAccount -vvvv
[⠒] Compiling...
[⠒] Compiling 1 files with 0.8.24
[⠢] Solc 0.8.24 finished in 3.86s
Compiler run successful!

Ran 1 test for test/SimpleAccountFactory.t.sol:SimpleAccountFactoryTest
[PASS] test_FactoryCreateAccount() (gas: 174382)
Traces:
  [174382] SimpleAccountFactoryTest::test_FactoryCreateAccount()
    ├─ [4669] SimpleAccountFactory::getAddress(Owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 10000 [1e4]) [staticcall]
    │   └─ ← [Return] ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865]
    ├─ [0] VM::assertEq(0, 0) [staticcall]
    │   └─ ← [Return] 
    ├─ [150083] SimpleAccountFactory::createAccount(Owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 10000 [1e4])
    │   ├─ [112211] → new ERC1967Proxy@0x75fe56829E9d4867837306E3bDDdcdC066416865
    │   │   ├─ emit Upgraded(implementation: SimpleAccountImpl: [0xffD4505B3452Dc22f8473616d50503bA9E1710Ac])
    │   │   ├─ [47945] SimpleAccountImpl::initialize(Owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266]) [delegatecall]
    │   │   │   ├─ emit SimpleAccountInitialized(entryPoint: EntryPoint: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], owner: Owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266])
    │   │   │   ├─ emit Initialized(version: 1)
    │   │   │   └─ ← [Stop] 
    │   │   └─ ← [Return] 183 bytes of code
    │   └─ ← [Return] ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865]
    ├─ [0] VM::assertEq(ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865], ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865]) [staticcall]
    │   └─ ← [Return] 
    ├─ [0] VM::assertGt(183, 0) [staticcall]
    │   └─ ← [Return] 
    ├─ [737] ERC1967Proxy::owner() [staticcall]
    │   ├─ [359] SimpleAccountImpl::owner() [delegatecall]
    │   │   └─ ← [Return] Owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266]
    │   └─ ← [Return] Owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266]
    ├─ [0] VM::assertEq(Owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], Owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266]) [staticcall]
    │   └─ ← [Return] 
    ├─ [646] ERC1967Proxy::entryPoint() [staticcall]
    │   ├─ [268] SimpleAccountImpl::entryPoint() [delegatecall]
    │   │   └─ ← [Return] EntryPoint: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]
    │   └─ ← [Return] EntryPoint: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]
    ├─ [0] VM::assertEq(EntryPoint: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], EntryPoint: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall]
    │   └─ ← [Return] 
    └─ ← [Stop] 

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 15.46ms (1.27ms CPU time)

Ran 1 test suite in 777.88ms (15.46ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

[CASE 2] EntryPoint의 handleOps를 통한 스마트 계정 생성

function test_handleOpsWithUndeployedAccount() public {
    uint256 salt = 10000;
    address expectedAddr = factory.getAddress(owner, salt);
    bytes memory initCode =
        abi.encodePacked(address(factory), abi.encodeWithSelector(factory.createAccount.selector, owner, salt));
    bytes memory callData = abi.encodeWithSelector(
        SimpleAccount.execute.selector, address(counter), 0, abi.encodeWithSelector(counter.increment.selector)
    );
    uint256 nonce = entryPoint.getNonce(expectedAddr, 0);

    PackedUserOperation memory packedUserOp = utils.packUserOp(expectedAddr, nonce, initCode, callData);

    bytes32 userOpHash = entryPoint.getUserOpHash(packedUserOp);

    bytes memory signature = utils.signUserOp(ownerPrivateKey, userOpHash);

    packedUserOp.signature = signature;

    PackedUserOperation[] memory ops = new PackedUserOperation[](1);
    ops[0] = packedUserOp;

    vm.prank(owner);
    entryPoint.depositTo{value: 1 ether}(expectedAddr);

    uint256 counterBefore = counter.number();
    uint256 depositBefore = entryPoint.balanceOf(expectedAddr);
    uint256 beneficiaryBalanceBefore = beneficiary.balance;

    entryPoint.handleOps(ops, payable(beneficiary));

    SimpleAccount simpleAccount = SimpleAccount(payable(expectedAddr));
    
    assertGt(expectedAddr.code.length, 0);
    assertEq(simpleAccount.owner(), owner);
    assertEq(address(simpleAccount.entryPoint()), address(entryPoint));
    assertEq(counter.number(), counterBefore + 1);
    assertGt(beneficiary.balance, beneficiaryBalanceBefore);
    assertLt(entryPoint.balanceOf(expectedAddr), depositBefore);
}

1. salt와 owner의 주소를 사용해 예상되는 주소를 계산합니다. 이번에는 EntryPoint의 handleOps를 호출하는 과정에서 스마트 계정이 생성되는 과정을 살펴볼 것입니다. 따라서 Factory 컨트랙트의 createAccount 함수를 호출하기 위한 initCode를 선언합니다. initCode에는 생성자의 주소 20바이트와 생성자에 대한 callData가 패킹되어 있습니다.

uint256 salt = 10000;
address expectedAddr = factory.getAddress(owner, salt);
bytes memory initCode =
    abi.encodePacked(address(factory), abi.encodeWithSelector(factory.createAccount.selector, owner, salt));

2. 이전 게시글에서와 마찬가지로 PackedUserOp와 서명을 생성합니다.

bytes memory callData = abi.encodeWithSelector(
    SimpleAccount.execute.selector, address(counter), 0, abi.encodeWithSelector(counter.increment.selector)
);
uint256 nonce = entryPoint.getNonce(expectedAddr, 0);

PackedUserOperation memory packedUserOp = utils.packUserOp(expectedAddr, nonce, initCode, callData);

bytes32 userOpHash = entryPoint.getUserOpHash(packedUserOp);

bytes memory signature = utils.signUserOp(ownerPrivateKey, userOpHash);

packedUserOp.signature = signature;

PackedUserOperation[] memory ops = new PackedUserOperation[](1);
ops[0] = packedUserOp;

  initCode를 파라미터로 받는 packUserOp 함수를 새로 추가했습니다. 그리고 배포 비용이 포함되므로 혹시 모르는 OutOfGas 문제가 발생하는 것을 방지하기 위해 verificationGasLimit를 상향 조정했습니다. 

function packUserOp(address sender, uint256 nonce, bytes memory initCode, bytes memory data)
    public
    pure
    returns (PackedUserOperation memory userOp)
{
    uint128 verificationGasLimit = 500000;
    uint128 callGasLimit = 50000;
    bytes32 gasLimits = bytes32((uint256(verificationGasLimit)) << 128 | uint256(callGasLimit));

    uint256 maxPriorityFeePerGas = 1 gwei;
    uint256 maxFeePerGas = 20 gwei;
    bytes32 gasFees = bytes32((uint256(maxPriorityFeePerGas)) << 128 | uint256(maxFeePerGas));

    userOp = PackedUserOperation({
        sender: sender,
        nonce: nonce,
        initCode: initCode,
        callData: data,
        accountGasLimits: gasLimits,
        preVerificationGas: 21000,
        gasFees: gasFees,
        paymasterAndData: "",
        signature: ""
    });
}

3. 예상되는 계정 주소로 1 이더의 예치금을 넣어줍니다. 예치금이 없는 경우에는 handleOps 함수의 실행이 revert 되므로 컨트랙트 계정 또한 생성되지 않습니다. 추가된 예치금은 계정의 withdrawDepositTo 함수로만 인출할 수 있습니다.

vm.prank(owner);
entryPoint.depositTo{value: 1 ether}(expectedAddr);
function depositTo(address account) public virtual payable {
    uint256 newDeposit = _incrementDeposit(account, msg.value);
    emit Deposited(account, newDeposit);
}
function _incrementDeposit(address account, uint256 amount) internal returns (uint256) {
    DepositInfo storage info = deposits[account];
    uint256 newAmount = info.deposit + amount;
    info.deposit = newAmount;
    return newAmount;
}

4. handleOps가 호출되기 이전의 상태를 기록해 주고 ops와 beneficiary의 주소를 입력으로 handleOps 함수를 호출합니다.

uint256 counterBefore = counter.number();
uint256 depositBefore = entryPoint.balanceOf(expectedAddr);
uint256 beneficiaryBalanceBefore = beneficiary.balance;

entryPoint.handleOps(ops, payable(beneficiary));

5. 검증 단계에서 계정이 배포되어 있지 않고 initCode의 길이가 0보다 크므로 계정을 배포하기 위한 코드가 실행됩니다.

_createSenderIfNeeded(opIndex, opInfo, op.initCode);
function _createSenderIfNeeded(uint256 opIndex, UserOpInfo memory opInfo, bytes calldata initCode) internal {
    if (initCode.length != 0) {
        address sender = opInfo.mUserOp.sender;
        if (sender.code.length != 0) {
            revert FailedOp(opIndex, "AA10 sender already constructed");
        }
        address sender1 = senderCreator().createSender{gas: opInfo.mUserOp.verificationGasLimit}(initCode);
        
        ...
    }
}

senderCreator는 EntryPoint가 배포될 때 함께 배포된 컨트랙트로, initCode를 분석하여 Factory 컨트랙트를 호출하는 역할을 합니다.

SenderCreator private immutable _senderCreator = new SenderCreator();

function senderCreator() internal view virtual returns (SenderCreator) {
    return _senderCreator;
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.23;

/**
 * Helper contract for EntryPoint, to call userOp.initCode from a "neutral" address,
 * which is explicitly not the entryPoint itself.
 */
contract SenderCreator {
    /**
     * Call the "initCode" factory to create and return the sender account address.
     * @param initCode - The initCode value from a UserOp. contains 20 bytes of factory address,
     *                   followed by calldata.
     * @return sender  - The returned address of the created account, or zero address on failure.
     */
    function createSender(
        bytes calldata initCode
    ) external returns (address sender) {
        address factory = address(bytes20(initCode[0:20]));
        bytes memory initCallData = initCode[20:];
        bool success;
        /* solhint-disable no-inline-assembly */
        assembly ("memory-safe") {
            success := call(
                gas(),
                factory,
                0,
                add(initCallData, 0x20),
                mload(initCallData),
                0,
                32
            )
            sender := mload(0)
        }
        if (!success) {
            sender = address(0);
        }
    }
}

6. initCode에 따라 Factory 컨트랙트의 createAccount 함수가 호출되고 새로 생성된 계정이 반환됩니다. 그리고 반환된 계정의 validateUserOp 함수가 호출됩니다. 이 때 미리 예치금을 넣어놨으므로 missingAccountFunds는 0이 되어 계정에서 EntryPoint로 부족한 수수료를 전송할 필요가 없어집니다.

address paymaster = mUserOp.paymaster;
uint256 missingAccountFunds = 0;
if (paymaster == address(0)) {
    uint256 bal = balanceOf(sender);
    missingAccountFunds = bal > requiredPrefund
        ? 0
        : requiredPrefund - bal;
}
try
    IAccount(sender).validateUserOp{
        gas: verificationGasLimit
    }(op, opInfo.userOpHash, missingAccountFunds)
returns (uint256 _validationData) {
    validationData = _validationData;
} catch {
    revert FailedOpWithRevert(opIndex, "AA23 reverted", Exec.getReturnData(REVERT_REASON_MAX_LEN));
}
if (paymaster == address(0)) {
    DepositInfo storage senderInfo = deposits[sender];
    uint256 deposit = senderInfo.deposit;
    if (requiredPrefund > deposit) {
        revert FailedOp(opIndex, "AA21 didn't pay prefund");
    }
    senderInfo.deposit = deposit - requiredPrefund;
}

7. 나머지 부분은 이전 게시글에서 다룬 부분과 동일합니다. handleOps 함수가 정상적으로 종료되고 나면 다음 사항들을 확인할 수 있습니다.

  • 배포된 계정의 코드 크기가 0보다 커야 합니다.
  • 배포된 계정의 owner가 owner의 주소와 동일해야 합니다.
  • 배포된 계정의 entryPoint가 entryPoint의 주소와 동일해야 합니다.
  • Counter의 number가 handleOps를 실행하기 전보다 1만큼 증가된 상태여야 합니다.
  • beneficiary의 이더 잔액이 handleOps를 실행하기 전보다 증가된 상태여야 합니다. 
  • 계정의 예치금이 handleOps를 실행하기 전보다 감소된 상태여야 합니다.
SimpleAccount simpleAccount = SimpleAccount(payable(expectedAddr));

assertGt(expectedAddr.code.length, 0);
assertEq(simpleAccount.owner(), owner);
assertEq(address(simpleAccount.entryPoint()), address(entryPoint));
assertEq(counter.number(), counterBefore + 1);
assertGt(beneficiary.balance, beneficiaryBalanceBefore);
assertLt(entryPoint.balanceOf(expectedAddr), depositBefore);

테스트

$ forge test --mt test_handleOpsWithUndeployedAccount -vvvv
[⠒] Compiling...
[⠒] Compiling 1 files with 0.8.24
[⠢] Solc 0.8.24 finished in 3.86s
Compiler run successful!

Ran 1 test for test/SimpleAccountFactory.t.sol:SimpleAccountFactoryTest
[PASS] test_handleOpsWithUndeployedAccount() (gas: 353415)
Traces:
  [353415] SimpleAccountFactoryTest::test_handleOpsWithUndeployedAccount()
    ├─ [4669] SimpleAccountFactory::getAddress(Owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 10000 [1e4]) [staticcall]
    │   └─ ← [Return] ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865]
    ├─ [2768] EntryPoint::getNonce(ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865], 0) [staticcall]
    │   └─ ← [Return] 0
    ├─ [3408] UserOpUtils::packUserOp(ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865], 0, 0x2e234dae75c793f67a35089c9d99245e1c58470b5fbfb9cf0000000000000000000000007c8999dc9a822c1f0df42023113edb4fdd5432660000000000000000000000000000000000000000000000000000000000002710, 0xb61d27f60000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000) [staticcall]
    │   └─ ← [Return] PackedUserOperation({ sender: 0x75fe56829E9d4867837306E3bDDdcdC066416865, nonce: 0, initCode: 0x2e234dae75c793f67a35089c9d99245e1c58470b5fbfb9cf0000000000000000000000007c8999dc9a822c1f0df42023113edb4fdd5432660000000000000000000000000000000000000000000000000000000000002710, callData: 0xb61d27f60000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000, accountGasLimits: 0x0000000000000000000000000007a1200000000000000000000000000000c350, preVerificationGas: 21000 [2.1e4], gasFees: 0x0000000000000000000000003b9aca00000000000000000000000004a817c800, paymasterAndData: 0x, signature: 0x })
    ├─ [1975] EntryPoint::getUserOpHash(PackedUserOperation({ sender: 0x75fe56829E9d4867837306E3bDDdcdC066416865, nonce: 0, initCode: 0x2e234dae75c793f67a35089c9d99245e1c58470b5fbfb9cf0000000000000000000000007c8999dc9a822c1f0df42023113edb4fdd5432660000000000000000000000000000000000000000000000000000000000002710, callData: 0xb61d27f60000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000, accountGasLimits: 0x0000000000000000000000000007a1200000000000000000000000000000c350, preVerificationGas: 21000 [2.1e4], gasFees: 0x0000000000000000000000003b9aca00000000000000000000000004a817c800, paymasterAndData: 0x, signature: 0x })) [staticcall]
    │   └─ ← [Return] 0x078cf95fedad878e902f9dd06c1f011f7b283e032d53579ee1f8dffdef8820f0
    ├─ [4015] UserOpUtils::signUserOp(907111799109225873672206001743429201758838553092777504370151546632448000192 [9.071e74], 0x078cf95fedad878e902f9dd06c1f011f7b283e032d53579ee1f8dffdef8820f0) [staticcall]
    │   ├─ [0] VM::sign("<pk>", 0xf41e808641a1b11edb5f70ff675c3795439ff589c5ec2bf2c5e61747a0ab0283) [staticcall]
    │   │   └─ ← [Return] 27, 0x8a6e48e1c3f5eefd976ca1c13be0bf68e043ddc578098abe16263b0e5bd1fd9d, 0x5fa7a14a4201c873a272391eeb043ac121ccd4bc48cd65de74c321c552efe0e6
    │   └─ ← [Return] 0x8a6e48e1c3f5eefd976ca1c13be0bf68e043ddc578098abe16263b0e5bd1fd9d5fa7a14a4201c873a272391eeb043ac121ccd4bc48cd65de74c321c552efe0e61b
    ├─ [0] VM::prank(Owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266])
    │   └─ ← [Return] 
    ├─ [24093] EntryPoint::depositTo{value: 1000000000000000000}(ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865])
    │   ├─ emit Deposited(account: ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865], totalDeposit: 1000000000000000000 [1e18])
    │   └─ ← [Stop] 
    ├─ [2283] Counter::number() [staticcall]
    │   └─ ← [Return] 0
    ├─ [545] EntryPoint::balanceOf(ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865]) [staticcall]
    │   └─ ← [Return] 1000000000000000000 [1e18]
    ├─ [253666] EntryPoint::handleOps([PackedUserOperation({ sender: 0x75fe56829E9d4867837306E3bDDdcdC066416865, nonce: 0, initCode: 0x2e234dae75c793f67a35089c9d99245e1c58470b5fbfb9cf0000000000000000000000007c8999dc9a822c1f0df42023113edb4fdd5432660000000000000000000000000000000000000000000000000000000000002710, callData: 0xb61d27f60000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000, accountGasLimits: 0x0000000000000000000000000007a1200000000000000000000000000000c350, preVerificationGas: 21000 [2.1e4], gasFees: 0x0000000000000000000000003b9aca00000000000000000000000004a817c800, paymasterAndData: 0x, signature: 0x8a6e48e1c3f5eefd976ca1c13be0bf68e043ddc578098abe16263b0e5bd1fd9d5fa7a14a4201c873a272391eeb043ac121ccd4bc48cd65de74c321c552efe0e61b })], Beneficiary: [0x5c4d2bd3510C8B51eDB17766d3c96EC637326999])
    │   ├─ [151221] SenderCreator::createSender(0x2e234dae75c793f67a35089c9d99245e1c58470b5fbfb9cf0000000000000000000000007c8999dc9a822c1f0df42023113edb4fdd5432660000000000000000000000000000000000000000000000000000000000002710)
    │   │   ├─ [150083] SimpleAccountFactory::createAccount(Owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], 10000 [1e4])
    │   │   │   ├─ [112211] → new ERC1967Proxy@0x75fe56829E9d4867837306E3bDDdcdC066416865
    │   │   │   │   ├─ emit Upgraded(implementation: SimpleAccountImpl: [0xffD4505B3452Dc22f8473616d50503bA9E1710Ac])
    │   │   │   │   ├─ [47945] SimpleAccountImpl::initialize(Owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266]) [delegatecall]
    │   │   │   │   │   ├─ emit SimpleAccountInitialized(entryPoint: EntryPoint: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], owner: Owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266])
    │   │   │   │   │   ├─ emit Initialized(version: 1)
    │   │   │   │   │   └─ ← [Stop] 
    │   │   │   │   └─ ← [Return] 183 bytes of code
    │   │   │   └─ ← [Return] ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865]
    │   │   └─ ← [Return] ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865]
    │   ├─ emit AccountDeployed(userOpHash: 0x078cf95fedad878e902f9dd06c1f011f7b283e032d53579ee1f8dffdef8820f0, sender: ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865], factory: SimpleAccountFactory: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], paymaster: 0x0000000000000000000000000000000000000000)
    │   ├─ [5466] ERC1967Proxy::validateUserOp(PackedUserOperation({ sender: 0x75fe56829E9d4867837306E3bDDdcdC066416865, nonce: 0, initCode: 0x2e234dae75c793f67a35089c9d99245e1c58470b5fbfb9cf0000000000000000000000007c8999dc9a822c1f0df42023113edb4fdd5432660000000000000000000000000000000000000000000000000000000000002710, callData: 0xb61d27f60000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000, accountGasLimits: 0x0000000000000000000000000007a1200000000000000000000000000000c350, preVerificationGas: 21000 [2.1e4], gasFees: 0x0000000000000000000000003b9aca00000000000000000000000004a817c800, paymasterAndData: 0x, signature: 0x8a6e48e1c3f5eefd976ca1c13be0bf68e043ddc578098abe16263b0e5bd1fd9d5fa7a14a4201c873a272391eeb043ac121ccd4bc48cd65de74c321c552efe0e61b }), 0x078cf95fedad878e902f9dd06c1f011f7b283e032d53579ee1f8dffdef8820f0, 0)
    │   │   ├─ [4925] SimpleAccountImpl::validateUserOp(PackedUserOperation({ sender: 0x75fe56829E9d4867837306E3bDDdcdC066416865, nonce: 0, initCode: 0x2e234dae75c793f67a35089c9d99245e1c58470b5fbfb9cf0000000000000000000000007c8999dc9a822c1f0df42023113edb4fdd5432660000000000000000000000000000000000000000000000000000000000002710, callData: 0xb61d27f60000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000, accountGasLimits: 0x0000000000000000000000000007a1200000000000000000000000000000c350, preVerificationGas: 21000 [2.1e4], gasFees: 0x0000000000000000000000003b9aca00000000000000000000000004a817c800, paymasterAndData: 0x, signature: 0x8a6e48e1c3f5eefd976ca1c13be0bf68e043ddc578098abe16263b0e5bd1fd9d5fa7a14a4201c873a272391eeb043ac121ccd4bc48cd65de74c321c552efe0e61b }), 0x078cf95fedad878e902f9dd06c1f011f7b283e032d53579ee1f8dffdef8820f0, 0) [delegatecall]
    │   │   │   ├─ [3000] PRECOMPILES::ecrecover(0xf41e808641a1b11edb5f70ff675c3795439ff589c5ec2bf2c5e61747a0ab0283, 27, 62614029293978420973838307993815819350807706801450197576689413610371332308381, 43265897258377528468146152536913015124369766459514712728710887964094409728230) [staticcall]
    │   │   │   │   └─ ← [Return] 0x0000000000000000000000007c8999dc9a822c1f0df42023113edb4fdd543266
    │   │   │   └─ ← [Return] 0
    │   │   └─ ← [Return] 0
    │   ├─ emit BeforeExecution()
    │   ├─ [28654] EntryPoint::innerHandleOp(0xb61d27f60000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000, UserOpInfo({ mUserOp: MemoryUserOp({ sender: 0x75fe56829E9d4867837306E3bDDdcdC066416865, nonce: 0, verificationGasLimit: 500000 [5e5], callGasLimit: 50000 [5e4], paymasterVerificationGasLimit: 0, paymasterPostOpGasLimit: 0, preVerificationGas: 21000 [2.1e4], paymaster: 0x0000000000000000000000000000000000000000, maxFeePerGas: 20000000000 [2e10], maxPriorityFeePerGas: 1000000000 [1e9] }), userOpHash: 0x078cf95fedad878e902f9dd06c1f011f7b283e032d53579ee1f8dffdef8820f0, prefund: 11420000000000000 [1.142e16], contextOffset: 96, preOpGas: 212272 [2.122e5] }), 0x)
    │   │   ├─ [22088] ERC1967Proxy::execute(Counter: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 0, 0xd09de08a)
    │   │   │   ├─ [21689] SimpleAccountImpl::execute(Counter: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b], 0, 0xd09de08a) [delegatecall]
    │   │   │   │   ├─ [20340] Counter::increment()
    │   │   │   │   │   └─ ← [Stop] 
    │   │   │   │   └─ ← [Stop] 
    │   │   │   └─ ← [Return] 
    │   │   ├─ emit UserOperationEvent(userOpHash: 0x078cf95fedad878e902f9dd06c1f011f7b283e032d53579ee1f8dffdef8820f0, sender: ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865], paymaster: 0x0000000000000000000000000000000000000000, nonce: 0, success: true, actualGasCost: 237748000000000 [2.377e14], actualGasUsed: 237748 [2.377e5])
    │   │   └─ ← [Return] 237748000000000 [2.377e14]
    │   ├─ [0] Beneficiary::fallback{value: 237748000000000}()
    │   │   └─ ← [Stop] 
    │   └─ ← [Stop] 
    ├─ [0] VM::assertGt(183, 0) [staticcall]
    │   └─ ← [Return] 
    ├─ [737] ERC1967Proxy::owner() [staticcall]
    │   ├─ [359] SimpleAccountImpl::owner() [delegatecall]
    │   │   └─ ← [Return] Owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266]
    │   └─ ← [Return] Owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266]
    ├─ [0] VM::assertEq(Owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266], Owner: [0x7c8999dC9a822c1f0Df42023113EDB4FDd543266]) [staticcall]
    │   └─ ← [Return] 
    ├─ [646] ERC1967Proxy::entryPoint() [staticcall]
    │   ├─ [268] SimpleAccountImpl::entryPoint() [delegatecall]
    │   │   └─ ← [Return] EntryPoint: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]
    │   └─ ← [Return] EntryPoint: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]
    ├─ [0] VM::assertEq(EntryPoint: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], EntryPoint: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall]
    │   └─ ← [Return] 
    ├─ [283] Counter::number() [staticcall]
    │   └─ ← [Return] 1
    ├─ [0] VM::assertEq(1, 1) [staticcall]
    │   └─ ← [Return] 
    ├─ [0] VM::assertGt(1000237748000000000 [1e18], 1000000000000000000 [1e18]) [staticcall]
    │   └─ ← [Return] 
    ├─ [545] EntryPoint::balanceOf(ERC1967Proxy: [0x75fe56829E9d4867837306E3bDDdcdC066416865]) [staticcall]
    │   └─ ← [Return] 999762252000000000 [9.997e17]
    ├─ [0] VM::assertLt(999762252000000000 [9.997e17], 1000000000000000000 [1e18]) [staticcall]
    │   └─ ← [Return] 
    └─ ← [Stop] 

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.61ms (1.60ms CPU time)

Ran 1 test suite in 844.82ms (3.61ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

정리

  • SimpleAccountFactory는 create2 opcode를 사용하여 사용자가 원하는 주소로 계정을 배포합니다.
    • 따라서 아직 존재하지 않는 계정의 주소를 사용해 EntryPoint에 미리 수수료를 예치해 놓을 수 있습니다. 
    • 의문점 : CA와 충돌하는 경우는 코드 길이 검사를 통해 확인되지만, EOA와 충돌하는 경우는 어떻게 될까요?
  • 아직 배포되지 않은 스마트 계정은 EntryPoint의 handleOps를 실행하는 과정(검증 단계)에서 생성됩니다.
    • 예치금이 부족한 경우 : 트랜잭션이 revert된다. 이 경우에는 Paymaster에게 도움을 요청해야 합니다. 이는 다음 시간에!

전체 코드

 

aa-from-scratch/contracts at main · piatoss3612/aa-from-scratch

Contribute to piatoss3612/aa-from-scratch development by creating an account on GitHub.

github.com


참조

 

account-abstraction/contracts at develop · eth-infinitism/account-abstraction

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

github.com

 

 

EVM Codes

An Ethereum Virtual Machine Opcodes Interactive Reference

www.evm.codes

 

최근에 올라온 글
최근에 달린 댓글
«   2024/11   »
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
Total
Today
Yesterday
글 보관함