체인의정석

ERC 4337 유저 지갑 컨트렉트 예제 (다중 오너 지갑 예제) 본문

블록체인/계정추상화

ERC 4337 유저 지갑 컨트렉트 예제 (다중 오너 지갑 예제)

체인의정석 2023. 9. 23. 09:40
728x90
반응형

ERC 4337은 계정추상화에 대한 내용으로서 최근 매우 유명한 ERC 중 하나이다.

https://www.erc4337.io/resources

 

ERC-4337

The official ERC-4337 website - useful information about the Ethereum Account Abstraction protocol

www.erc4337.io

관련해서 전부터 자체 테스트를 하려했으나 트랜잭션을 계속 실패하여서 먼저 간단한 튜토리얼을 따라서 학습 해 본 후 추가적인 학습을 이어 나가려고 한다.

일단 기본 튜토리얼은 무엇을 할지 계속 살펴보다가 위의 링크에서 추천되어 있는 Trampolin의 예제를 해보도록 하겠다.

https://erc4337.mirror.xyz/r0Sxa_ncYJA8y7_YHeKz9cShDgfTNR1fVBolcBN7VZ4

 

Building a custom wallet with Trampoline - a step-by-step guide

Follow this hands-on approach to building a custom SCW with Trampoline, and save precious time when you build with ERC-4337 during your next hackathon

erc4337.mirror.xyz


https://github.com/eth-infinitism/trampoline

 

GitHub - eth-infinitism/trampoline

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

github.com

일단 해당 깃허브를 받아서 진행을 해보도록 하겠다.

참고로 해당 부분은 아래 브랜치를 기준으로 따라한 실습니다.

https://github.com/eth-infinitism/trampoline/tree/trampoline-demo-two-owner

먼저 풀을 받고나서 기본 세팅을 해주고 난 후에 컨트렉트 부분 부터 구현을한다.

 

1. 유저 지갑 컨트렉트 생성 및 배포하기

첫번째 다루는 컨트렉트는 TwoOwnerAccount.sol이다.

TwoOwnerAccount .sol

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

import '@account-abstraction/contracts/samples/SimpleAccount.sol';

contract TwoOwnerAccount is SimpleAccount {
    using ECDSA for bytes32;
    address public ownerOne;
    address public ownerTwo;

    constructor(IEntryPoint anEntryPoint) SimpleAccount(anEntryPoint) {}

    function initialize(
        address _ownerOne,
        address _ownerTwo
    ) public virtual initializer {
        super._initialize(address(0));
        ownerOne = _ownerOne;
        ownerTwo = _ownerTwo;
    }

    function _validateSignature(
        UserOperation calldata userOp,
        bytes32 userOpHash
    ) internal view override returns (uint256 validationData) {
        (userOp, userOpHash);

        bytes32 hash = userOpHash.toEthSignedMessageHash();

        (bytes memory signatureOne, bytes memory signatureTwo) = abi.decode(
            userOp.signature,
            (bytes, bytes)
        );

        address recoveryOne = hash.recover(signatureOne);
        address recoveryTwo = hash.recover(signatureTwo);

        bool ownerOneCheck = ownerOne == recoveryOne;
        bool ownerTwoCheck = ownerTwo == recoveryTwo;

        if (ownerOneCheck && ownerTwoCheck) return 0;

        return SIG_VALIDATION_FAILED;
    }

    function encodeSignature(
        bytes memory signatureOne,
        bytes memory signatureTwo
    ) public pure returns (bytes memory) {
        return (abi.encode(signatureOne, signatureTwo));
    }
}

여기서 Simple Account는 @account-abstraction 모듈에 있는 기본 컨트렉트인데

여기서 _validateSignature 부분을 오버라이딩 해서 오너를 2개로 만든 부분이 차이이다. 

여기서 사용된 함수들은

  • toEthSignedMessageHash produces a eth message hash from any bytes32 value (which itself might be a hash). This is an operation that is required by ERC191 68.
  • recover takes (1) a signed message that can either be produced using toEthSignedMessageHash (for ERC191 messages) or by toTypedDataHash (for ERC712 typed structs) and (2) a signature of this signed message by an EOA. It returns the address of the EOA that produced the signature.


ERC191에서 사용되는 서명을 만드는 부분이라고 하고 이에 대한 recover는
1. toEthSignMessageHash에 대한 결과 값과 toTypedDataHash (각각 EIP191 과 712에 따른 결과 값)

2. 서명된 메세지에 대한 Signature 

이렇게 2개를 받아오는 함수라고 한다.

따라서 2가지는
1. userOP에 대한 해시 값을 만들고 이에 대한 데이터를 서명하여 나온 서명 값 (여기서는 191 서명이나 712 서명으로도 커스터마이징 가능해 보임)
2. userOPHash

이다.

그리고 이렇게 해시 값을 넣어주어야 한다면 사실 해시를 만들어주는 조회 함수도 같이 넣어주어야 (쓰는 사람 입장에서) 편하다.

그래서 넣어둔 것이 바로 "encodeSignature" 이 함수다.

        (bytes memory signatureOne, bytes memory signatureTwo) = abi.decode(
            userOp.signature,
            (bytes, bytes)
        );

이 부분을 보면 UserOP에 있는 signature 부분에 encodeSignature 한 것을 Decode 하는 로직이 있는 것을 확인할 수 있다.

userOP의 signature 부분을 제외한 부분은 그럼 어떻게 만들까?

일일히 만들어도 되겠지만 해당 부분은 편리한 모듈을 발견했다. 이건 일단 나중에 써볼 예정이다.
https://docs.stackup.sh/docs/useropjs

 

Introduction | userop.js

userop.js is a simple JavaScript library for intuitively building ERC-4337 UserOperations and sending them to a Bundler.

docs.stackup.sh

 

TwoAccountFactory.sol

두번째로 살펴볼 내용은 create2와 ERC1967Proxy를 이용해서 위의 컨트렉트를 배포해 주는 로직이다.

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

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

import './TwoOwnerAccount.sol';

contract TwoOwnerAccountFactory {
    TwoOwnerAccount public immutable accountImplementation;

    constructor(IEntryPoint _entryPoint) {
        accountImplementation = new TwoOwnerAccount(_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 _ownerOne,
        address _ownerTwo,
        uint256 salt
    ) public returns (TwoOwnerAccount ret) {
        address addr = getAddress(_ownerOne, _ownerTwo, salt);
        uint256 codeSize = addr.code.length;
        if (codeSize > 0) {
            return TwoOwnerAccount(payable(addr));
        }
        ret = TwoOwnerAccount(
            payable(
                new ERC1967Proxy{salt: bytes32(salt)}(
                    address(accountImplementation),
                    abi.encodeCall(
                        TwoOwnerAccount.initialize,
                        (_ownerOne, _ownerTwo)
                    )
                )
            )
        );
    }

    /**
     * calculate the counterfactual address of this account as it would be returned by createAccount()
     */
    function getAddress(
        address _ownerOne,
        address _ownerTwo,
        uint256 salt
    ) public view returns (address) {
        return
            Create2.computeAddress(
                bytes32(salt),
                keccak256(
                    abi.encodePacked(
                        type(ERC1967Proxy).creationCode,
                        abi.encode(
                            address(accountImplementation),
                            abi.encodeCall(
                                TwoOwnerAccount.initialize,
                                (_ownerOne, _ownerTwo)
                            )
                        )
                    )
                )
            );
    }
}

여기서 ERC1967이 생소하여 찾아봤는데

https://eips.ethereum.org/EIPS/eip-1967

ERC1967은 로직 컨트렉트 주소들을 저장하였다가 보여주는 역할을 하는데생성자 안에 새로운 주소와 콜을 날릴 데이터를 넣게 되면 다음과 같이 delegate call을 날리게 된다.

            Address.functionDelegateCall(newImplementation, data);

일단 해당 포스팅은 계정 추상화에 대한 내용이기 때문에 계정 추상화에서는 결국 컨트렉트 지갑을 만들어서 사용하기 때문에 다음과 같이 create2와 ERC1967을 같이 사용하여 생성 규칙을 만들고 관리한다 정도로만 이해하고 넘어가겠다.

이때 위의 모든 로직에서의 entryPointAddress는 src에 있는 exconfig에 있는 entryPointAddress를 공통으로 사용한다.

이제

MNEMONIC_FILE=~/.secret/testnet-mnemonic.txt INFURA_ID=<infura_id> npx hardhat deploy --network sepolia

 이렇게 니모닉 파일 부분을 내가 원하는것으로 바꿔주고 infura 아이디를 넣어둔 후에 네트워크를 지정해서 배포해 주면 된다고 한다.

근데 난 config 파일에서 그냥 네트워크 이름을 수정해 주고

  networks: {
    sepolia: getNetwork('sepolia'),
  },

https://sepoliafaucet.com/

 테스트 코인 수령 후 

.env 파일을 만들고

INFURA_ID=
MNEMONIC_FILE=pk

infura 회원가입후 아이디와 니모닉 키에 대한 파일만 따로 지정해 주었다.

배포파일은 sec/exconfig.ts 에 있는 entryPoint를 받아서 컨트렉트 지갑을 배포해 주는데

// eslint-disable-next-line import/no-anonymous-default-export
export default {
  enablePasswordEncryption: false,
  showTransactionConfirmationScreen: true,
  factory_address: '0x9406Cc6185a346906296840746125a0E44976454',
  stateVersion: '0.1',
  network: {
    chainID: '11155111',
    family: 'EVM',
    name: 'Sepolia',
    provider: 'https://sepolia.infura.io/v3/bdabe9d2f9244005af0f566398e648da',
    entryPointAddress: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789',
    bundler: 'https://sepolia.voltaire.candidewallet.com/rpc',
    baseAsset: {
      symbol: 'ETH',
      name: 'ETH',
      decimals: 18,
      image:
        'https://ethereum.org/static/6b935ac0e6194247347855dc3d328e83/6ed5f/eth-diamond-black.webp',
    },
  },
};

entryPoint에 대한 정보는 다음과 같은 정보로 나와있다. 나중에 다른 네트워크에서 배포할때는 entryPoint 컨트렉트를 배포한 후 이 부분을 좀 수정하면 될 것 같다.

*UI 부분은 wagmi를 사용했다는데 기본실습때는 잘 작동을 하다가 해당 부분을 수정하고 실행하려니 오류가 나는 바람에 일단 여기서는 생략하도록 하겠다.

 

728x90
반응형
Comments