일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |
- 체인의정석
- ethers typescript
- ethers v6
- 스마트컨트렉트테스트
- SBT표준
- 컨트렉트 동일한 함수이름 호출
- ambiguous function description
- nest.js설명
- vue기초
- 오블완
- 컨트렉트 배포 자동화
- 스마트컨트렉트프록시
- ethers websocket
- chainlink 설명
- 프록시배포구조
- 러스트 기초
- 스마트컨트렉트 예약어 함수이름 중복
- 스마트컨트렉트 함수이름 중복 호출
- ethers type
- ethers
- rust 기초
- git rebase
- 스마트 컨트렉트 함수이름 중복
- 러스트기초
- Vue.js
- multicall
- 러스트 기초 학습
- 머신러닝기초
- Vue
- 티스토리챌린지
- Today
- Total
체인의정석
ERC 4337 유저 지갑 컨트렉트 예제 (다중 오너 지갑 예제) 본문
ERC 4337은 계정추상화에 대한 내용으로서 최근 매우 유명한 ERC 중 하나이다.
https://www.erc4337.io/resources
관련해서 전부터 자체 테스트를 하려했으나 트랜잭션을 계속 실패하여서 먼저 간단한 튜토리얼을 따라서 학습 해 본 후 추가적인 학습을 이어 나가려고 한다.
일단 기본 튜토리얼은 무엇을 할지 계속 살펴보다가 위의 링크에서 추천되어 있는 Trampolin의 예제를 해보도록 하겠다.
https://erc4337.mirror.xyz/r0Sxa_ncYJA8y7_YHeKz9cShDgfTNR1fVBolcBN7VZ4
https://github.com/eth-infinitism/trampoline
일단 해당 깃허브를 받아서 진행을 해보도록 하겠다.
참고로 해당 부분은 아래 브랜치를 기준으로 따라한 실습니다.
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
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'),
},
테스트 코인 수령 후
.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를 사용했다는데 기본실습때는 잘 작동을 하다가 해당 부분을 수정하고 실행하려니 오류가 나는 바람에 일단 여기서는 생략하도록 하겠다.
'블록체인 > 계정추상화' 카테고리의 다른 글
영지식증명, poseidon Hash (0) | 2024.08.14 |
---|---|
영지식 증명, circum & snark js 기초예제 (1) | 2024.08.12 |
계정 추상화 참고할만한 자료 (0) | 2024.04.22 |
ERC4337 컨트렉트 핵심 요소 및 커스터마이징 해야할 부분 정리 (0) | 2024.04.08 |