티스토리 뷰
계정 추상화 시리즈
2024.04.17 - [블록체인/Ethereum] - ERC-4337: 계정 추상화 - 테스트를 통한 Account와 EntryPoint의 동작 이해
2024.04.16 - [블록체인/Ethereum] - ERC-4337: 계정 추상화 - Account, EntryPoint, Paymaster
1. UserOperation 패킹 순서 수정
수정 전
uint128 verificationGasLimit = 500000;
uint128 callGasLimit = 21000;
bytes32 gasLimits = bytes32(uint256(callGasLimit) << 128 | (uint256(verificationGasLimit)));
uint256 maxPriorityFeePerGas = 1 gwei;
uint256 maxFeePerGas = 20 gwei;
bytes32 gasFees = bytes32(uint256(maxFeePerGas) << 128 | (uint256(maxPriorityFeePerGas)));
수정 후
uint128 verificationGasLimit = 200000;
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));
- verificationGasLimit가 상위 16바이트로 가고 callGasLimit는 하위 16바이트로
- maxPriorityFeePerGas가 상위 16바이트로 가고 maxFeePerGas는 하위 16바이트로
놓치기 쉬운 디테일이고 그게 그거 아닌가 간과할 수도 있는 부분인 것 같습니다. 가스가 부족하다는 에러가 계속 발생하는데 도대체 어디서 발생하는지 추적하다가 발견했습니다. UserOperationLib 라이브러리 코드에서 언패킹 하는 코드를 참고하여 수정하였습니다.
효과
테스트 도중 발생하던 가스 부족 오류(EVM: OutOfGas)를 해결할 수 있었습니다.
2. callGasLimit 상향 조정
수정 전
uint128 callGasLimit = 21000;
수정 후
uint128 callGasLimit = 50000;
verificationGasLimit와 callGasLimit의 패킹 순서가 변경됨에 따라 스마트 계정의 execute 함수를 호출함에 있어서 callGasLimit로 인한 OutOfGas 문제가 발생하여 상향 조정하였습니다.
3. vm.etch를 사용하여 반복적인 컨트랙트 배포 줄이기
function etch(address target, bytes calldata newRuntimeBytecode) external;
vm.etch는 target 주소에 런타임 바이트코드를 붙여서 해당 주소가 특정 컨트랙트인 것 마냥 동작하게 만들어주는 치트코드입니다.
이 치트코드를 사용하기 위해서는 우선 사전에 정의된 주소와 특정 컨트랙트의 런타임 바이트코드가 필요합니다. 저는 별도의 스크립트를 작성하여 json 파일로 주소와 바이트코드를 정리하였습니다.
우선은 Foundry에게 파일시스템 접근 권한을 부여하기 위해 다음 항목을 foundry.toml 파일에 추가합니다.
fs_permissions = [{ access = "read-write", path = "./artifacts" }]
그리고 Foundry 프로젝트 루트 디렉터리에 artifacts 디렉터리를 생성해 줍니다.
$ mkdir artifacts
script 디렉터리에 GenerateArtifactsScript.s.sol 파일을 생성합니다. 그리고 아래의 내용을 복사/붙여 넣습니다.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.24;
import "forge-std/Script.sol";
import {SimpleEntryPoint} from "../src/SimpleEntryPoint.sol";
import {SimpleAccountFactory} from "../src/SimpleAccountFactory.sol";
import {SimpleAccount} from "../src/SimpleAccount.sol";
import {SenderCreator} from "account-abstraction/core/SenderCreator.sol";
contract GenerateArtifactsScript is Script {
function run() public {
SimpleEntryPoint entryPoint = new SimpleEntryPoint();
SenderCreator senderCreator = entryPoint.getSenderCreator();
SimpleAccountFactory factory = new SimpleAccountFactory(entryPoint);
SimpleAccount impl = factory.accountImplementation();
string memory entryPointObject = "EntryPoint";
string memory senderCreatorObject = "SenderCreator";
string memory factoryObject = "SimpleAccountFactory";
string memory implObject = "SimpleAccount";
saveJson(entryPointObject, address(entryPoint));
saveJson(senderCreatorObject, address(senderCreator));
saveJson(factoryObject, address(factory));
saveJson(implObject, address(impl));
}
function saveJson(string memory key, address addr) internal {
string memory addrStr = vm.serializeAddress(key, "address", addr);
string memory bytecode = vm.serializeBytes(key, "code", addr.code);
string memory fileName = string(abi.encodePacked("./artifacts/", key, ".json"));
vm.writeJson(addrStr, fileName);
vm.writeJson(bytecode, fileName);
}
}
스크립트의 동작 순서는 다음과 같습니다.
- 각 컨트랙트를 foundry default sender로 테스트 환경에 배포합니다.
- 배포된 컨트랙트를 나타내는 키와 주소를 사용해 saveJson 함수를 호출합니다.
- saveJson 함수는 컨트랙트의 주소와 런타임 바이트코드를 직렬화하여 `key`.json 파일로 artifacts 디렉터리에 저장됩니다.
이렇게 생성된 json 파일들을 사용하여 각 주소와 바이트코드를 상수로 선언한 하나의 solidity 파일을 작성해 줍니다. vm.writeFile을 사용하면 바로 solidity 파일을 생성할 수도 있을 것 같긴 한데 여기까지 온 이상 그냥 진행합니다.
생성한 Artifacts.sol 파일을 테스트에서 불러와 다음과 같이 사용합니다.
entryPoint = IEntryPoint(payable(ENTRYPOINT_ADDRESS));
vm.etch(address(entryPoint), ENTRYPOINT_BYTECODE);
vm.label(address(entryPoint), "EntryPoint");
import "./utils/Artifacts.sol";
contract SimpleAccountTest is Test {
...
function setUp() public {
...
entryPoint = IEntryPoint(payable(ENTRYPOINT_ADDRESS));
vm.etch(address(entryPoint), ENTRYPOINT_BYTECODE);
vm.label(address(entryPoint), "EntryPoint");
simpleAccountImpl = SimpleAccount(payable(IMPL_ADDRESS));
vm.etch(address(simpleAccountImpl), IMPL_BYTECODE);
vm.label(address(simpleAccountImpl), "SimpleAccountImpl");
vm.startPrank(deployer);
counter = new Counter();
vm.label(address(counter), "Counter");
utils = new UserOpUtils();
vm.label(address(utils), "UserOpUtils");
vm.stopPrank();
}
}
여기서 주의해야 할 점은 vm.etch를 통해 생성된 컨트랙트들은 foundry default sender의 주소와 논스(0과 1)를 사용해 생성되었으므로 테스트에서 새로운 컨트랙트를 배포할 때 foundry default sender를 사용하게 되면 논스 0으로 배포된 컨트랙트의 런타임 바이트코드를 배포된 컨트랙트로 덮어쓰게 됩니다. 따라서 다른 사용자(deployer)의 주소를 사용하여 컨트랙트를 배포하였습니다.
효과
가스가 부족하다는 오류가 계속 발생해서 해결책의 일환으로 사용해 보았지만, 앞서 말씀드렸다시피 1, 2번 수정 사항을 통해 해당 문제는 해결이 되었습니다. 즉, 삽질만 한 것이죠. 그래서 foundry를 사용해서 json 파일을 생성하고 vm.etch 치트코드를 사용하는 방법을 새로 익힐 수 있었습니다. 하...
4. vm.pauseGasMetering 제거
앞에서 적용한 수정 사항들 덕분에 더 이상 OutOfGas 오류가 발생하지 않으므로 제거했습니다.
5. 그 외 수정 사항
빨간색은 삭제한 부분이고 초록색은 추가한 부분입니다. 알고 계시겠지만! 이것으로 하루 반나절 걸린 삽질을 마무리하겠습니다.
'블록체인 > Ethereum' 카테고리의 다른 글
ERC-4337: 계정 추상화 - 테스트를 통한 Paymaster와 LegacyTokenPaymaster의 동작 이해 (0) | 2024.04.18 |
---|---|
ERC-4337: 계정 추상화 - 테스트를 통한 Account Factory의 동작 이해 (0) | 2024.04.18 |
ERC-4337: 계정 추상화 - 테스트를 통한 Account와 EntryPoint의 동작 이해 (0) | 2024.04.17 |
ERC-4337: 계정 추상화 (0) | 2024.04.16 |
소셜 로그인 + 계정 추상화를 사용한 dApp 만들기 (0) | 2024.04.13 |