├── img ├── 1-1.png ├── 1-2.png ├── 1-3.png ├── 1-4.png ├── 1-5.png ├── 2-1.png ├── 3-1.png ├── 4-1.png ├── 4-2.png ├── flow1.png ├── flow2.png └── upgradeable.png ├── .gitignore ├── args.js ├── verify.ps1 ├── scripts ├── deploySimpleAccount.js └── deploySimpleAccountV2.js ├── contracts ├── core │ ├── SenderCreator.sol │ ├── Helpers.sol │ ├── BaseAccount.sol │ ├── StakeManager.sol │ └── EntryPoint.sol ├── interfaces │ ├── IAggregator.sol │ ├── IAccount.sol │ ├── IPaymaster.sol │ ├── UserOperation.sol │ ├── IStakeManager.sol │ └── IEntryPoint.sol ├── utils │ └── Exec.sol ├── SimpleAccountV2.sol └── SimpleAccount.sol ├── package.json ├── hardhat.config.js ├── README.md ├── docs ├── uups.md ├── 3.md ├── 4.md ├── sample-account.md ├── 2.md ├── 1.md └── eip-4337.md └── test ├── userOp.js └── SimpleAccount-test.js /img/1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyd-dev/account-abstraction/HEAD/img/1-1.png -------------------------------------------------------------------------------- /img/1-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyd-dev/account-abstraction/HEAD/img/1-2.png -------------------------------------------------------------------------------- /img/1-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyd-dev/account-abstraction/HEAD/img/1-3.png -------------------------------------------------------------------------------- /img/1-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyd-dev/account-abstraction/HEAD/img/1-4.png -------------------------------------------------------------------------------- /img/1-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyd-dev/account-abstraction/HEAD/img/1-5.png -------------------------------------------------------------------------------- /img/2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyd-dev/account-abstraction/HEAD/img/2-1.png -------------------------------------------------------------------------------- /img/3-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyd-dev/account-abstraction/HEAD/img/3-1.png -------------------------------------------------------------------------------- /img/4-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyd-dev/account-abstraction/HEAD/img/4-1.png -------------------------------------------------------------------------------- /img/4-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyd-dev/account-abstraction/HEAD/img/4-2.png -------------------------------------------------------------------------------- /img/flow1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyd-dev/account-abstraction/HEAD/img/flow1.png -------------------------------------------------------------------------------- /img/flow2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyd-dev/account-abstraction/HEAD/img/flow2.png -------------------------------------------------------------------------------- /img/upgradeable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyd-dev/account-abstraction/HEAD/img/upgradeable.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .env 4 | 5 | # Hardhat files 6 | cache 7 | artifacts 8 | 9 | 10 | keys.js 11 | .openzeppelin -------------------------------------------------------------------------------- /args.js: -------------------------------------------------------------------------------- 1 | // constructor arguments 2 | //const ENTRY_POINT = "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"; // V1 3 | const ENTRY_POINT = "0xdD870fA1b7C4700F2BD7f44238821C26f7392148"; //V2 4 | 5 | 6 | module.exports = [ENTRY_POINT] -------------------------------------------------------------------------------- /verify.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [string]$proxy, 3 | [string]$impl 4 | ) 5 | 6 | if (-not $PSBoundParameters.ContainsKey('proxy')) { 7 | Write-Host "Missing parameter: proxy" 8 | exit 1 9 | } 10 | 11 | if (-not $PSBoundParameters.ContainsKey('impl')) { 12 | Write-Host "Missing parameter: impl" 13 | exit 1 14 | } 15 | 16 | 17 | npx hardhat verify $impl --constructor-args args.js --network sepolia 18 | npx hardhat verify $proxy --network sepolia 19 | 20 | 21 | -------------------------------------------------------------------------------- /scripts/deploySimpleAccount.js: -------------------------------------------------------------------------------- 1 | const hre = require("hardhat"); 2 | 3 | async function main() { 4 | 5 | const ENTRY_POINT = "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"; // dummy address V1 6 | const signers = await hre.ethers.getSigners(); 7 | const simpleAccount = await hre.ethers.getContractFactory("SimpleAccount", signers[0]); 8 | 9 | const owner = await signers[0].getAddress(); 10 | const contract = await hre.upgrades.deployProxy(simpleAccount, [owner], {kind: "uups", unsafeAllow: ["constructor","state-variable-immutable"], constructorArgs: [ENTRY_POINT]}); 11 | 12 | await contract.deployed(); 13 | 14 | console.log( 15 | `SimpleAccount deployed to ${contract.address}` 16 | ); 17 | } 18 | 19 | // We recommend this pattern to be able to use async/await everywhere 20 | // and properly handle errors. 21 | main().catch((error) => { 22 | console.error(error); 23 | process.exitCode = 1; 24 | }); 25 | -------------------------------------------------------------------------------- /scripts/deploySimpleAccountV2.js: -------------------------------------------------------------------------------- 1 | const hre = require("hardhat"); 2 | 3 | async function main() { 4 | 5 | const ENTRY_POINT = "0xdD870fA1b7C4700F2BD7f44238821C26f7392148"; // dummy address V2 6 | const signers = await hre.ethers.getSigners(); 7 | const simpleAccountV2 = await hre.ethers.getContractFactory("SimpleAccountV2", signers[0]); 8 | 9 | const proxyAddress = "0xf05A1f128412D78e13e037ED04A53d4670b044D7"; 10 | const contract = await hre.upgrades.upgradeProxy(proxyAddress, simpleAccountV2, {kind: "uups", unsafeAllow: ["constructor","state-variable-immutable"], constructorArgs: [ENTRY_POINT]}); 11 | 12 | await contract.deployed(); 13 | 14 | console.log( 15 | `SimpleAccount deployed to ${contract.address}` 16 | ); 17 | } 18 | 19 | // We recommend this pattern to be able to use async/await everywhere 20 | // and properly handle errors. 21 | main().catch((error) => { 22 | console.error(error); 23 | process.exitCode = 1; 24 | }); 25 | -------------------------------------------------------------------------------- /contracts/core/SenderCreator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity =0.8.17; 3 | 4 | /** 5 | * helper contract for EntryPoint, to call userOp.initCode from a "neutral" address, 6 | * which is explicitly not the entryPoint itself. 7 | */ 8 | contract SenderCreator { 9 | 10 | /** 11 | * call the "initCode" factory to create and return the sender account address 12 | * @param initCode the initCode value from a UserOp. contains 20 bytes of factory address, followed by calldata 13 | * @return sender the returned address of the created account, or zero address on failure. 14 | */ 15 | function createSender(bytes calldata initCode) external returns (address sender) { 16 | address factory = address(bytes20(initCode[0 : 20])); 17 | bytes memory initCallData = initCode[20 :]; 18 | bool success; 19 | /* solhint-disable no-inline-assembly */ 20 | assembly { 21 | success := call(gas(), factory, 0, add(initCallData, 0x20), mload(initCallData), 0, 32) 22 | sender := mload(0) 23 | } 24 | if (!success) { 25 | sender = address(0); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "account-abstract", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "clean": "powershell Remove-Item .\\artifacts, .\\cache -recurse", 8 | "deploy": "npx hardhat run .\\scripts\\deploySimpleAccount.js --network sepolia", 9 | "upgrade": "npx hardhat run .\\scripts\\deploySimpleAccountV2.js --network sepolia" 10 | }, 11 | "devDependencies": { 12 | "@ethersproject/abi": "^5.4.7", 13 | "@ethersproject/providers": "^5.4.7", 14 | "@nomicfoundation/hardhat-chai-matchers": "^1.0.0", 15 | "@nomicfoundation/hardhat-network-helpers": "^1.0.0", 16 | "@nomicfoundation/hardhat-toolbox": "^2.0.0", 17 | "@nomiclabs/hardhat-ethers": "^2.0.0", 18 | "@nomiclabs/hardhat-etherscan": "^3.0.0", 19 | "@openzeppelin/contracts": "^4.8.2", 20 | "@openzeppelin/hardhat-upgrades": "^1.22.1", 21 | "@typechain/ethers-v5": "^10.1.0", 22 | "@typechain/hardhat": "^6.1.2", 23 | "chai": "^4.2.0", 24 | "ethers": "^5.4.7", 25 | "hardhat": "^2.11.1", 26 | "hardhat-gas-reporter": "^1.0.8", 27 | "solidity-coverage": "^0.8.0", 28 | "typechain": "^8.1.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("@nomicfoundation/hardhat-toolbox"); 2 | require("@nomiclabs/hardhat-etherscan"); 3 | require("@openzeppelin/hardhat-upgrades"); 4 | 5 | const keys = require("./keys"); 6 | const { getImplementationAddress } = require("@openzeppelin/upgrades-core"); 7 | 8 | task("getimpl", "Print implementation contract address") 9 | .addParam("proxy", "Proxy address") 10 | .setAction(async (args, hre) => { 11 | const provider = new ethers.providers.JsonRpcProvider(keys.RPC_ENDPOINT_SEPOLIA); 12 | const proxy = args.proxy; 13 | console.log(`${await getImplementationAddress(provider, proxy)}`); 14 | }); 15 | 16 | 17 | /** @type import('hardhat/config').HardhatUserConfig */ 18 | module.exports = { 19 | 20 | networks: { 21 | 22 | hardhat: { 23 | allowUnlimitedContractSize: true 24 | }, 25 | 26 | goerli: { 27 | url: keys.RPC_ENDPOINT_GOERLI, 28 | accounts: keys.pk 29 | }, 30 | 31 | sepolia: { 32 | url: keys.RPC_ENDPOINT_SEPOLIA, 33 | accounts: keys.pk 34 | } 35 | }, 36 | 37 | solidity: { 38 | compilers: [ 39 | {version: "0.8.17", settings: {optimizer: {enabled: true}}}, 40 | {version: "0.8.18", settings: {optimizer: {enabled: true}}} 41 | ] 42 | }, 43 | 44 | etherscan: { 45 | apiKey: keys.ETHERSCAN_API_KEY 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Account Abstraction 2 | 3 | 이더리움의 계정은, 특히 EOA라고 하는 것은 지갑을 의미합니다. 그래서 애플리케이션 관점에서 할 수 있는 것이 별로 없습니다. 현재 지갑의 기능이란 개인키를 생성하고 4 | 트랜잭션 전송이나 메시지에 서명하는 일이 전부입니다. 5 | 6 | 그래서 EOA로부터 지갑을 분리해서 지갑에 다양한 기능을 추가해보자는 것이 "계정 추상화(Account Abstraction, AA)"의 방향이 되겠습니다. 7 | 계정 추상화를 설계하고 있는 이더리움 재단의 요압 바이스(Yoav Weiss)는 "abstraction"이라는 용어가 마음에 안든다고 합니다. 이 용어는 8 | 의미의 혼란을 일으키기 때문에 "스마트 계정"이 오히려 더 맞는 것 같다고 합니다. 9 | 10 | 바이스에 의하면 "abstraction"이란 순전히 프로토콜 관점에서 붙여진 이름으로, 현재 프로토콜 레벨에 있는(즉 EOA에 통합된) 계정으로부터 11 | "추출"하자는 뜻이라고 합니다("The Road to Account Abstraction", WalletCon in 2023). 12 | 13 | 14 | * 알케미 블로그에 실린 [계정 추상화](https://www.alchemy.com/blog/account-abstraction)에 대한 번역입니다. 15 | 16 | 1. [Part 1: You Could Have Invented Account Abstraction](./docs/1.md) 17 | 2. [Part 2: Sponsoring Transactions Using Paymasters](./docs/2.md) 18 | 3. [Part 3: Wallet Creation](./docs/3.md) 19 | 4. [Part 4: Aggregate Signatures](./docs/4.md) 20 | 21 | * ERC-4337 22 | 1. [표준(요약)](./docs/eip-4337.md) 23 | 2. 인터페이스 24 | - [`IAccount`](./contracts/interfaces/IAccount.sol) 25 | - [`IAggregator`](./contracts/interfaces/IAggregator.sol) 26 | - [`IEntryPoint`](./contracts/interfaces/IEntryPoint.sol) 27 | - [`IPaymaster`](./contracts/interfaces/IPaymaster.sol) 28 | - [`IStakeManager`](./contracts/interfaces/IStakeManager.sol) 29 | - [`UserOperation`](./contracts/interfaces/UserOperation.sol) 30 | 3. 구현 예제 31 | - [SimpleAccount](./docs/sample-account.md) -------------------------------------------------------------------------------- /contracts/interfaces/IAggregator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.12; 3 | 4 | import "./UserOperation.sol"; 5 | 6 | /** 7 | * Aggregated Signatures validator. 8 | */ 9 | interface IAggregator { 10 | 11 | /** 12 | * validate aggregated signature. 13 | * revert if the aggregated signature does not match the given list of operations. 14 | */ 15 | function validateSignatures(UserOperation[] calldata userOps, bytes calldata signature) external view; 16 | 17 | /** 18 | * validate signature of a single userOp 19 | * This method is should be called by bundler after EntryPoint.simulateValidation() returns (reverts) with ValidationResultWithAggregation 20 | * First it validates the signature over the userOp. Then it returns data to be used when creating the handleOps. 21 | * @param userOp the userOperation received from the user. 22 | * @return sigForUserOp the value to put into the signature field of the userOp when calling handleOps. 23 | * (usually empty, unless account and aggregator support some kind of "multisig" 24 | */ 25 | function validateUserOpSignature(UserOperation calldata userOp) 26 | external view returns (bytes memory sigForUserOp); 27 | 28 | /** 29 | * aggregate multiple signatures into a single value. 30 | * This method is called off-chain to calculate the signature to pass with handleOps() 31 | * bundler MAY use optimized custom code perform this aggregation 32 | * @param userOps array of UserOperations to collect the signatures from. 33 | * @return aggregatedSignature the aggregated signature 34 | */ 35 | function aggregateSignatures(UserOperation[] calldata userOps) external view returns (bytes memory aggregatedSignature); 36 | } -------------------------------------------------------------------------------- /contracts/interfaces/IAccount.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.12; 3 | 4 | import "./UserOperation.sol"; 5 | 6 | interface IAccount { 7 | 8 | /** 9 | * Validate user's signature and nonce 10 | * the entryPoint will make the call to the recipient only if this validation call returns successfully. 11 | * signature failure should be reported by returning SIG_VALIDATION_FAILED (1). 12 | * This allows making a "simulation call" without a valid signature 13 | * Other failures (e.g. nonce mismatch, or invalid signature format) should still revert to signal failure. 14 | * 15 | * @dev Must validate caller is the entryPoint. 16 | * Must validate the signature and nonce 17 | * @param userOp the operation that is about to be executed. 18 | * @param userOpHash hash of the user's request data. can be used as the basis for signature. 19 | * @param missingAccountFunds missing funds on the account's deposit in the entrypoint. 20 | * This is the minimum amount to transfer to the sender(entryPoint) to be able to make the call. 21 | * The excess is left as a deposit in the entrypoint, for future calls. 22 | * can be withdrawn anytime using "entryPoint.withdrawTo()" 23 | * In case there is a paymaster in the request (or the current deposit is high enough), this value will be zero. 24 | * @return validationData packaged ValidationData structure. use `_packValidationData` and `_unpackValidationData` to encode and decode 25 | * <20-byte> sigAuthorizer - 0 for valid signature, 1 to mark signature failure, 26 | * otherwise, an address of an "authorizer" contract. 27 | * <6-byte> validUntil - last timestamp this operation is valid. 0 for "indefinite" 28 | * <6-byte> validAfter - first timestamp this operation is valid 29 | * If an account doesn't use time-range, it is enough to return SIG_VALIDATION_FAILED value (1) for signature failure. 30 | * Note that the validation code cannot use block.timestamp (or block.number) directly. 31 | */ 32 | function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) external 33 | returns (uint256 validationData); 34 | 35 | } -------------------------------------------------------------------------------- /contracts/utils/Exec.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-only 2 | pragma solidity >=0.7.5 <0.9.0; 3 | 4 | // solhint-disable no-inline-assembly 5 | 6 | /** 7 | * Utility functions helpful when making different kinds of contract calls in Solidity. 8 | */ 9 | library Exec { 10 | 11 | function call( 12 | address to, 13 | uint256 value, 14 | bytes memory data, 15 | uint256 txGas 16 | ) internal returns (bool success) { 17 | assembly { 18 | success := call(txGas, to, value, add(data, 0x20), mload(data), 0, 0) 19 | } 20 | } 21 | 22 | function staticcall( 23 | address to, 24 | bytes memory data, 25 | uint256 txGas 26 | ) internal view returns (bool success) { 27 | assembly { 28 | success := staticcall(txGas, to, add(data, 0x20), mload(data), 0, 0) 29 | } 30 | } 31 | 32 | function delegateCall( 33 | address to, 34 | bytes memory data, 35 | uint256 txGas 36 | ) internal returns (bool success) { 37 | assembly { 38 | success := delegatecall(txGas, to, add(data, 0x20), mload(data), 0, 0) 39 | } 40 | } 41 | 42 | // get returned data from last call or calldelegate 43 | function getReturnData(uint256 maxLen) internal pure returns (bytes memory returnData) { 44 | assembly { 45 | let len := returndatasize() 46 | if gt(len, maxLen) { 47 | len := maxLen 48 | } 49 | let ptr := mload(0x40) 50 | mstore(0x40, add(ptr, add(len, 0x20))) 51 | mstore(ptr, len) 52 | returndatacopy(add(ptr, 0x20), 0, len) 53 | returnData := ptr 54 | } 55 | } 56 | 57 | // revert with explicit byte array (probably reverted info from call) 58 | function revertWithData(bytes memory returnData) internal pure { 59 | assembly { 60 | revert(add(returnData, 32), mload(returnData)) 61 | } 62 | } 63 | 64 | function callAndRevert(address to, bytes memory data, uint256 maxLen) internal { 65 | bool success = call(to,0,data,gasleft()); 66 | if (!success) { 67 | revertWithData(getReturnData(maxLen)); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /docs/uups.md: -------------------------------------------------------------------------------- 1 | ### Upgradeable Contract 2 | 3 | 스마트 컨트랙트는 한번 배포되면 수정할 수 없습니다. 그래서 "업그레이드 가능한(Upgradeable)"의 의미는 4 | 컨트랙트의 코드를 변경한다는 의미가 아니라 컨트랙트를 교체한다는 의미입니다. 5 | 6 | 업그레이드 가능한 컨트랙트의 기본적인 패턴은 "프록시(proxy)" 패턴으로, 변하지 않는 프록시 컨트랙트가 업그레이드 가능한 "로직(또는 구현, implementation) 컨트랙트의 7 | 주소를 저장하고 있기 때문에 이 주소를 변경하면 새로운 로직 컨트랙트를 가리키게 되고 변경된 코드를 실행할 수 있게 됩니다. 8 | 9 | 프록시 컨트랙트는 자신에게 없는 함수를 포워딩하는 `fallback` 함수에서 `delegatecall` 통해 로직 컨트랙트의 함수를 호출합니다. 10 | `delegatecall`은 호출된 컨트랙트의 함수, 즉 로직 컨트랙트의 함수가 실행되지만 상태변수가 저장된 스토리지는 호출한 컨트랙트의 것을 11 | 사용하므로 로직 컨트랙트가 교체되더라도 상태 값들을 그대로 유지할 수 있습니다. 12 | 13 | ![upgradeable](../img/upgradeable.png) 14 | 15 | 업그레이드 컨트랙트에서 가장 중요한 문제는 프록시 컨트랙트와 로직 컨트랙트의 스토리지 레이아웃을 일관성있게 유지하는 일입니다. 16 | 왜냐하면 스토리지 레이아웃이 달라지거나 타입이 변경되면 저장된 상태변수가 사라지거나 엉뚱한 값이 될 수 있기 때문입니다. 17 | 또 프록시 컨트랙트에서 관리자 권한을 제어하여 프록시가 가리키는 로직 컨트랙트의 주소를 아무나 변경할 수 없도록 해야 합니다. 18 | 19 | 업그레이드 컨트랙트는 스토리지 레이아웃을 담당하는 컨트랙트를 프록시와 로직 컨트랙트가 상속받아서 서로의 레이아웃을 공유하거나 미리 정해진 20 | 슬롯에 값을 저장하여 서로 겹치지 않게 하는 방법을 사용합니다. 이것과 관련된 표준이 바로 [ERC-1967](https://eips.ethereum.org/EIPS/eip-1967)입니다. 21 | 22 | 예를 들어 로직 컨트랙트의 주소는 아래와 같은 슬롯번호에 저장합니다. 23 | 24 | ``` 25 | Storage slot 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc 26 | (obtained as bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)). 27 | ``` 28 | 29 | 업그레이드 메커니즘은 스토리지와 코드를 분리하는 패턴이므로, 배포할 때 로직 컨트랙트에서 실행되는 생성자(`constructor`)는 30 | 의미가 없습니다. 왜냐하면 로직 컨트랙트에서 직접 실행되는 함수들은 자신의 스토리지만 변경할 뿐 프록시에서 보존되는 상태변수들에게는 31 | 영향을 주지 않기 때문입니다. 32 | 33 | 따라서 배포 과정 중에 초기화를 담당할 일반 함수 호출이 필요하고 이것은 단 한번만 호출되도록 제약을 두어야 합니다. 오픈제펠린 업그레이드의 디폴트 34 | 초기화 함수는 `initialize`입니다. 대략 다음 형태가 됩니다. 반드시 modifier `initializer`를 주어야 합니다. 35 | 36 | ```solidity 37 | function initialize(address anOwner) public virtual initializer { 38 | _initialize(anOwner); 39 | } 40 | ``` 41 | 42 | 오픈제펠린에서 제공되는 업그레이드 컨트랙트에서는 ERC-1967에서 정한 슬롯번호들을 사용합니다. 그리고 UUPS(ERC-1822)에 의해서 43 | 업그레이드를 하는 코드(`upgradeTo`, `upgradeToAndCall`)가 로직 컨트랙트에 배치합니다. 이 함수들은 권한을 가진 계정만이 44 | 실행할 수 있어야 하므로 권한 검사가 반드시 선행되어야 합니다. 이를 위해 `UUPSUpgradeable`에 정의된 다음 함수를 상속받아 구현해야 합니다. 45 | 46 | ```solidity 47 | function _authorizeUpgrade(address newImplementation) internal virtual; 48 | ``` 49 | 50 | 이것을 구현하면 `UUPSUpgradeable`는 다음과 같이 권한을 검사한 후에 업그레이드를 수행합니다. 51 | 52 | ```solidity 53 | function upgradeTo(address newImplementation) external virtual onlyProxy { 54 | _authorizeUpgrade(newImplementation); 55 | _upgradeToAndCallUUPS(newImplementation, new bytes(0), false); 56 | } 57 | ``` 58 | 59 | 보안 측면에서 `onlyProxy`를 적용되어 업그레이드가 반드시 프록시를 통해서 실행되는 것을 보장합니다. 60 | 61 | 62 | -------------------------------------------------------------------------------- /contracts/interfaces/IPaymaster.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.12; 3 | 4 | import "./UserOperation.sol"; 5 | 6 | /** 7 | * the interface exposed by a paymaster contract, who agrees to pay the gas for user's operations. 8 | * a paymaster must hold a stake to cover the required entrypoint stake and also the gas for the transaction. 9 | */ 10 | interface IPaymaster { 11 | 12 | enum PostOpMode { 13 | opSucceeded, // user op succeeded 14 | opReverted, // user op reverted. still has to pay for gas. 15 | postOpReverted //user op succeeded, but caused postOp to revert. Now it's a 2nd call, after user's op was deliberately reverted. 16 | } 17 | 18 | /** 19 | * payment validation: check if paymaster agrees to pay. 20 | * Must verify sender is the entryPoint. 21 | * Revert to reject this request. 22 | * Note that bundlers will reject this method if it changes the state, unless the paymaster is trusted (whitelisted) 23 | * The paymaster pre-pays using its deposit, and receive back a refund after the postOp method returns. 24 | * @param userOp the user operation 25 | * @param userOpHash hash of the user's request data. 26 | * @param maxCost the maximum cost of this transaction (based on maximum gas and gas price from userOp) 27 | * @return context value to send to a postOp 28 | * zero length to signify postOp is not required. 29 | * @return validationData signature and time-range of this operation, encoded the same as the return value of validateUserOperation 30 | * <20-byte> sigAuthorizer - 0 for valid signature, 1 to mark signature failure, 31 | * otherwise, an address of an "authorizer" contract. 32 | * <6-byte> validUntil - last timestamp this operation is valid. 0 for "indefinite" 33 | * <6-byte> validAfter - first timestamp this operation is valid 34 | * Note that the validation code cannot use block.timestamp (or block.number) directly. 35 | */ 36 | function validatePaymasterUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost) 37 | external returns (bytes memory context, uint256 validationData); 38 | 39 | /** 40 | * post-operation handler. 41 | * Must verify sender is the entryPoint 42 | * @param mode enum with the following options: 43 | * opSucceeded - user operation succeeded. 44 | * opReverted - user op reverted. still has to pay for gas. 45 | * postOpReverted - user op succeeded, but caused postOp (in mode=opSucceeded) to revert. 46 | * Now this is the 2nd call, after user's op was deliberately reverted. 47 | * @param context - the context value returned by validatePaymasterUserOp 48 | * @param actualGasCost - actual gas used so far (without this postOp call). 49 | */ 50 | function postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost) external; 51 | } -------------------------------------------------------------------------------- /test/userOp.js: -------------------------------------------------------------------------------- 1 | const hre = require("hardhat"); 2 | 3 | const userOpType = "tuple(address,uint256,bytes,bytes,uint256,uint256,uint256,uint256,uint256,bytes,bytes)"; 4 | const abi = ["function handleOps(" + userOpType + "[],address)"]; 5 | const iface = new hre.ethers.utils.Interface(abi); 6 | 7 | 8 | const BUNDLER_DUMMY = "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB"; 9 | 10 | const initCode = "0x"; 11 | const callData = "0x2b1ea9aa"; // noname() 12 | const callGasLimit = "21000"; 13 | const verificationGasLimit = "500000"; 14 | const preVerificationGas = "100000"; 15 | const maxFeePerGas = "20000000000"; //20 gwei 16 | const maxPriorityFeePerGas = "1000000000"; //1 gwei 17 | const paymasterAndData = "0x"; 18 | const signature = "0x"; 19 | 20 | const handleOpsCalldata = (userOp, signature) => { 21 | 22 | return iface.encodeFunctionData("handleOps", 23 | [ 24 | [ 25 | [ 26 | userOp.sender, 27 | userOp.nonce, 28 | userOp.initCode, 29 | userOp.callData, 30 | userOp.callGasLimit, 31 | userOp.verificationGasLimit, 32 | userOp.preVerificationGas, 33 | userOp.maxFeePerGas, 34 | userOp.maxPriorityFeePerGas, 35 | userOp.paymasterAndData, 36 | signature 37 | ] 38 | ], 39 | BUNDLER_DUMMY 40 | ] 41 | ); 42 | } 43 | 44 | const getUnsignedUserOp = (sender, nonce) => { 45 | 46 | return { 47 | sender, 48 | nonce, 49 | initCode, 50 | callData, 51 | callGasLimit, 52 | verificationGasLimit, 53 | preVerificationGas, 54 | maxFeePerGas, 55 | maxPriorityFeePerGas, 56 | paymasterAndData, 57 | signature 58 | } 59 | } 60 | const getUserOpHash = (sender, nonce, entryPoint, chainId) => { 61 | 62 | const userOp = getUnsignedUserOp(sender, nonce); 63 | 64 | let encodedUserOp = hre.ethers.utils.defaultAbiCoder.encode([userOpType], 65 | [ 66 | [ 67 | userOp.sender, 68 | userOp.nonce, 69 | userOp.initCode, 70 | userOp.callData, 71 | userOp.callGasLimit, 72 | userOp.verificationGasLimit, 73 | userOp.preVerificationGas, 74 | userOp.maxFeePerGas, 75 | userOp.maxPriorityFeePerGas, 76 | userOp.paymasterAndData, 77 | userOp.signature 78 | ] 79 | ]); 80 | 81 | // remove leading word (total length) and trailing word (zero-length signature) 82 | // packed 하면서 원래 서명 대상인 길이 0인 signature 를 제외한 메시지 83 | encodedUserOp = '0x' + encodedUserOp.slice(66, encodedUserOp.length - 64); 84 | 85 | return { 86 | message: hre.ethers.utils.keccak256(hre.ethers.utils.defaultAbiCoder.encode( 87 | ["bytes32", "address", "uint256"], 88 | [hre.ethers.utils.keccak256(encodedUserOp), entryPoint, chainId] 89 | )), 90 | userOp 91 | }; 92 | } 93 | 94 | 95 | module.exports = { 96 | handleOpsCalldata, 97 | getUserOpHash 98 | } -------------------------------------------------------------------------------- /contracts/core/Helpers.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity =0.8.17; 3 | 4 | /** 5 | * returned data from validateUserOp. 6 | * validateUserOp returns a uint256, with is created by `_packedValidationData` and parsed by `_parseValidationData` 7 | * @param aggregator - address(0) - the account validated the signature by itself. 8 | * address(1) - the account failed to validate the signature. 9 | * otherwise - this is an address of a signature aggregator that must be used to validate the signature. 10 | * @param validAfter - this UserOp is valid only after this timestamp. 11 | * @param validaUntil - this UserOp is valid only up to this timestamp. 12 | */ 13 | struct ValidationData { 14 | address aggregator; 15 | uint48 validAfter; 16 | uint48 validUntil; 17 | } 18 | 19 | //extract sigFailed, validAfter, validUntil. 20 | // also convert zero validUntil to type(uint48).max 21 | function _parseValidationData(uint validationData) pure returns (ValidationData memory data) { 22 | address aggregator = address(uint160(validationData)); 23 | uint48 validUntil = uint48(validationData >> 160); 24 | if (validUntil == 0) { 25 | validUntil = type(uint48).max; 26 | } 27 | uint48 validAfter = uint48(validationData >> (48 + 160)); 28 | return ValidationData(aggregator, validAfter, validUntil); 29 | } 30 | 31 | // intersect account and paymaster ranges. 32 | function _intersectTimeRange(uint256 validationData, uint256 paymasterValidationData) pure returns (ValidationData memory) { 33 | ValidationData memory accountValidationData = _parseValidationData(validationData); 34 | ValidationData memory pmValidationData = _parseValidationData(paymasterValidationData); 35 | address aggregator = accountValidationData.aggregator; 36 | if (aggregator == address(0)) { 37 | aggregator = pmValidationData.aggregator; 38 | } 39 | uint48 validAfter = accountValidationData.validAfter; 40 | uint48 validUntil = accountValidationData.validUntil; 41 | uint48 pmValidAfter = pmValidationData.validAfter; 42 | uint48 pmValidUntil = pmValidationData.validUntil; 43 | 44 | if (validAfter < pmValidAfter) validAfter = pmValidAfter; 45 | if (validUntil > pmValidUntil) validUntil = pmValidUntil; 46 | return ValidationData(aggregator, validAfter, validUntil); 47 | } 48 | 49 | /** 50 | * helper to pack the return value for validateUserOp 51 | * @param data - the ValidationData to pack 52 | */ 53 | function _packValidationData(ValidationData memory data) pure returns (uint256) { 54 | return uint160(data.aggregator) | (uint256(data.validUntil) << 160) | (uint256(data.validAfter) << (160 + 48)); 55 | } 56 | 57 | /** 58 | * helper to pack the return value for validateUserOp, when not using an aggregator 59 | * @param sigFailed - true for signature failure, false for success 60 | * @param validUntil last timestamp this UserOperation is valid (or zero for infinite) 61 | * @param validAfter first timestamp this UserOperation is valid 62 | */ 63 | function _packValidationData(bool sigFailed, uint48 validUntil, uint48 validAfter) pure returns (uint256) { 64 | return (sigFailed ? 1 : 0) | (uint256(validUntil) << 160) | (uint256(validAfter) << (160 + 48)); 65 | } -------------------------------------------------------------------------------- /contracts/interfaces/UserOperation.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.12; 3 | 4 | /* solhint-disable no-inline-assembly */ 5 | 6 | /** 7 | * User Operation struct 8 | * @param sender the sender account of this request. 9 | * @param nonce unique value the sender uses to verify it is not a replay. 10 | * @param initCode if set, the account contract will be created by this constructor/ 11 | * @param callData the method call to execute on this account. 12 | * @param callGasLimit the gas limit passed to the callData method call. 13 | * @param verificationGasLimit gas used for validateUserOp and validatePaymasterUserOp. 14 | * @param preVerificationGas gas not calculated by the handleOps method, but added to the gas paid. Covers batch overhead. 15 | * @param maxFeePerGas same as EIP-1559 gas parameter. 16 | * @param maxPriorityFeePerGas same as EIP-1559 gas parameter. 17 | * @param paymasterAndData if set, this field holds the paymaster address and paymaster-specific data. the paymaster will pay for the transaction instead of the sender. 18 | * @param signature sender-verified signature over the entire request, the EntryPoint address and the chain ID. 19 | */ 20 | struct UserOperation { 21 | 22 | address sender; 23 | uint256 nonce; 24 | bytes initCode; 25 | bytes callData; 26 | uint256 callGasLimit; 27 | uint256 verificationGasLimit; 28 | uint256 preVerificationGas; 29 | uint256 maxFeePerGas; 30 | uint256 maxPriorityFeePerGas; 31 | bytes paymasterAndData; 32 | bytes signature; 33 | } 34 | 35 | /** 36 | * Utility functions helpful when working with UserOperation structs. 37 | */ 38 | library UserOperationLib { 39 | 40 | function getSender(UserOperation calldata userOp) internal pure returns (address) { 41 | address data; 42 | //read sender from userOp, which is first userOp member (saves 800 gas...) 43 | assembly {data := calldataload(userOp)} 44 | return address(uint160(data)); 45 | } 46 | 47 | //relayer/block builder might submit the TX with higher priorityFee, but the user should not 48 | // pay above what he signed for. 49 | function gasPrice(UserOperation calldata userOp) internal view returns (uint256) { 50 | unchecked { 51 | uint256 maxFeePerGas = userOp.maxFeePerGas; 52 | uint256 maxPriorityFeePerGas = userOp.maxPriorityFeePerGas; 53 | if (maxFeePerGas == maxPriorityFeePerGas) { 54 | //legacy mode (for networks that don't support basefee opcode) 55 | return maxFeePerGas; 56 | } 57 | return min(maxFeePerGas, maxPriorityFeePerGas + block.basefee); 58 | } 59 | } 60 | 61 | function pack(UserOperation calldata userOp) internal pure returns (bytes memory ret) { 62 | //lighter signature scheme. must match UserOp.ts#packUserOp 63 | bytes calldata sig = userOp.signature; 64 | // copy directly the userOp from calldata up to (but not including) the signature. 65 | // this encoding depends on the ABI encoding of calldata, but is much lighter to copy 66 | // than referencing each field separately. 67 | assembly { 68 | let ofs := userOp 69 | let len := sub(sub(sig.offset, ofs), 32) 70 | ret := mload(0x40) 71 | mstore(0x40, add(ret, add(len, 32))) 72 | mstore(ret, len) 73 | calldatacopy(add(ret, 32), ofs, len) 74 | } 75 | } 76 | 77 | function hash(UserOperation calldata userOp) internal pure returns (bytes32) { 78 | return keccak256(pack(userOp)); 79 | } 80 | 81 | function min(uint256 a, uint256 b) internal pure returns (uint256) { 82 | return a < b ? a : b; 83 | } 84 | } -------------------------------------------------------------------------------- /contracts/interfaces/IStakeManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.12; 3 | 4 | /** 5 | * manage deposits and stakes. 6 | * deposit is just a balance used to pay for UserOperations (either by a paymaster or an account) 7 | * stake is value locked for at least "unstakeDelay" by the staked entity. 8 | */ 9 | interface IStakeManager { 10 | 11 | event Deposited( 12 | address indexed account, 13 | uint256 totalDeposit 14 | ); 15 | 16 | event Withdrawn( 17 | address indexed account, 18 | address withdrawAddress, 19 | uint256 amount 20 | ); 21 | 22 | /// Emitted when stake or unstake delay are modified 23 | event StakeLocked( 24 | address indexed account, 25 | uint256 totalStaked, 26 | uint256 unstakeDelaySec 27 | ); 28 | 29 | /// Emitted once a stake is scheduled for withdrawal 30 | event StakeUnlocked( 31 | address indexed account, 32 | uint256 withdrawTime 33 | ); 34 | 35 | event StakeWithdrawn( 36 | address indexed account, 37 | address withdrawAddress, 38 | uint256 amount 39 | ); 40 | 41 | /** 42 | * @param deposit the entity's deposit 43 | * @param staked true if this entity is staked. 44 | * @param stake actual amount of ether staked for this entity. 45 | * @param unstakeDelaySec minimum delay to withdraw the stake. 46 | * @param withdrawTime - first block timestamp where 'withdrawStake' will be callable, or zero if already locked 47 | * @dev sizes were chosen so that (deposit,staked, stake) fit into one cell (used during handleOps) 48 | * and the rest fit into a 2nd cell. 49 | * 112 bit allows for 10^15 eth 50 | * 48 bit for full timestamp 51 | * 32 bit allows 150 years for unstake delay 52 | */ 53 | struct DepositInfo { 54 | uint112 deposit; 55 | bool staked; 56 | uint112 stake; 57 | uint32 unstakeDelaySec; 58 | uint48 withdrawTime; 59 | } 60 | 61 | //API struct used by getStakeInfo and simulateValidation 62 | struct StakeInfo { 63 | uint256 stake; 64 | uint256 unstakeDelaySec; 65 | } 66 | 67 | /// @return info - full deposit information of given account 68 | function getDepositInfo(address account) external view returns (DepositInfo memory info); 69 | 70 | /// @return the deposit (for gas payment) of the account 71 | function balanceOf(address account) external view returns (uint256); 72 | 73 | /** 74 | * add to the deposit of the given account 75 | */ 76 | function depositTo(address account) external payable; 77 | 78 | /** 79 | * add to the account's stake - amount and delay 80 | * any pending unstake is first cancelled. 81 | * @param _unstakeDelaySec the new lock duration before the deposit can be withdrawn. 82 | */ 83 | function addStake(uint32 _unstakeDelaySec) external payable; 84 | 85 | /** 86 | * attempt to unlock the stake. 87 | * the value can be withdrawn (using withdrawStake) after the unstake delay. 88 | */ 89 | function unlockStake() external; 90 | 91 | /** 92 | * withdraw from the (unlocked) stake. 93 | * must first call unlockStake and wait for the unstakeDelay to pass 94 | * @param withdrawAddress the address to send withdrawn value. 95 | */ 96 | function withdrawStake(address payable withdrawAddress) external; 97 | 98 | /** 99 | * withdraw from the deposit. 100 | * @param withdrawAddress the address to send withdrawn value. 101 | * @param withdrawAmount the amount to withdraw. 102 | */ 103 | function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external; 104 | } -------------------------------------------------------------------------------- /test/SimpleAccount-test.js: -------------------------------------------------------------------------------- 1 | const hre = require("hardhat"); 2 | const {expect, assert} = require("chai"); 3 | const { getImplementationAddress } = require("@openzeppelin/upgrades-core"); 4 | const {handleOpsCalldata, getUserOpHash} = require("./userOp"); 5 | 6 | let signers; 7 | let owner; 8 | let proxy; 9 | let entryPoint; 10 | let chainId; 11 | const provider = hre.ethers.provider; 12 | const ENTRY_POINT_DUMMY = "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"; 13 | const BUNDLER_DUMMY = "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB"; 14 | const IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; 15 | 16 | let test; 17 | 18 | describe("SimpleAccount Test", function () { 19 | 20 | before(async () => { 21 | 22 | chainId = (await provider.getNetwork()).chainId; 23 | signers = await hre.ethers.getSigners(); 24 | 25 | // entry point 컨트랙트 26 | const f = await hre.ethers.getContractFactory("EntryPoint"); 27 | entryPoint = await f.deploy(); 28 | await entryPoint.deployed(); 29 | 30 | //const ep = ENTRY_POINT_DUMMY; 31 | const ep = entryPoint.address; 32 | 33 | // SimpleAccount 컨트랙트 34 | const simpleAccount = await hre.ethers.getContractFactory("SimpleAccount", signers[0]); 35 | owner = await signers[0].getAddress(); // signer[0] is owner 36 | proxy = await hre.upgrades.deployProxy(simpleAccount, [owner], {kind: "uups", unsafeAllow: ["constructor","state-variable-immutable"], constructorArgs: [ep]}); 37 | await proxy.deployed(); 38 | 39 | 40 | }); 41 | 42 | it ("Confirm the Entry Point address", async () => { 43 | assert.equal(entryPoint.address, (await proxy.entryPoint())); 44 | }); 45 | 46 | it ("Confirm the implementation address", async () => { 47 | const impl = await getImplementationAddress(provider, proxy.address); 48 | const impl_slot = await provider.getStorageAt(proxy.address, IMPLEMENTATION_SLOT); 49 | expect(impl).to.be.equal(hre.ethers.utils.getAddress(hre.ethers.utils.hexStripZeros(impl_slot))); 50 | }); 51 | 52 | it ("Wallet owner should be able to send ETH", async () => { 53 | await signers[0].sendTransaction({to: proxy.address, value: hre.ethers.utils.parseEther("5")}); 54 | const recipient = await signers[2].getAddress(); 55 | await proxy.execute(recipient, hre.ethers.utils.parseEther("1"), "0x"); 56 | 57 | const balance = await provider.getBalance(proxy.address); 58 | assert.equal(hre.ethers.utils.parseEther("4").toString(), balance.toString()); 59 | }); 60 | 61 | it ("Wallet should be able to deposit ETH to EntryPoint", async () => { 62 | const deposit = hre.ethers.utils.parseEther("0.1"); 63 | await proxy.addDeposit({value: deposit}); 64 | const depositInfo = await entryPoint.getDepositInfo(proxy.address); 65 | assert.equal(depositInfo.deposit.toString(),deposit.toString()); 66 | }); 67 | 68 | 69 | it ("Wallet should be able to call handleOps", async () => { 70 | 71 | const {message, userOp} = getUserOpHash(proxy.address, 0, entryPoint.address, chainId); 72 | //console.log(`messageToBeSigned=${message}`); 73 | 74 | // 전달해야 하는 메시지는 이더리움 메시지로 만들어서 다시 해시한다. 75 | //console.log(hre.ethers.utils.hashMessage(hre.ethers.utils.arrayify(message))); 76 | 77 | // signMessage 에는 해시하기 전 메시지를 전달한다. 78 | const signature = await signers[0].signMessage(hre.ethers.utils.arrayify(message)); 79 | //console.log(hre.ethers.utils.verifyMessage(hre.ethers.utils.arrayify(message), signature)); 80 | 81 | const data = handleOpsCalldata(userOp, signature); 82 | // TODO 번들러를 signers[5] 으로 가정 83 | //await signers[5].sendTransaction({to: entryPoint.address, data}); 84 | 85 | await expect(signers[5].sendTransaction({to: entryPoint.address, data})) 86 | .to.emit(entryPoint, "UserOperationEvent"); 87 | 88 | }); 89 | 90 | 91 | }) -------------------------------------------------------------------------------- /contracts/core/BaseAccount.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity =0.8.17; 3 | 4 | /* solhint-disable avoid-low-level-calls */ 5 | /* solhint-disable no-inline-assembly */ 6 | /* solhint-disable reason-string */ 7 | 8 | import "../interfaces/IAccount.sol"; 9 | import "../interfaces/IEntryPoint.sol"; 10 | import "./Helpers.sol"; 11 | 12 | /** 13 | * Basic account implementation. 14 | * this contract provides the basic logic for implementing the IAccount interface - validateUserOp 15 | * specific account implementation should inherit it and provide the account-specific logic 16 | */ 17 | abstract contract BaseAccount is IAccount { 18 | using UserOperationLib for UserOperation; 19 | 20 | //return value in case of signature failure, with no time-range. 21 | // equivalent to _packValidationData(true,0,0); 22 | uint256 constant internal SIG_VALIDATION_FAILED = 1; 23 | 24 | /** 25 | * return the account nonce. 26 | * subclass should return a nonce value that is used both by _validateAndUpdateNonce, and by the external provider (to read the current nonce) 27 | */ 28 | function nonce() public view virtual returns (uint256); 29 | 30 | /** 31 | * return the entryPoint used by this account. 32 | * subclass should return the current entryPoint used by this account. 33 | */ 34 | function entryPoint() public view virtual returns (IEntryPoint); 35 | 36 | /** 37 | * Validate user's signature and nonce. 38 | * subclass doesn't need to override this method. Instead, it should override the specific internal validation methods. 39 | */ 40 | function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) 41 | external override virtual returns (uint256 validationData) { 42 | _requireFromEntryPoint(); 43 | validationData = _validateSignature(userOp, userOpHash); 44 | if (userOp.initCode.length == 0) { 45 | _validateAndUpdateNonce(userOp); 46 | } 47 | _payPrefund(missingAccountFunds); 48 | } 49 | 50 | /** 51 | * ensure the request comes from the known entrypoint. 52 | */ 53 | function _requireFromEntryPoint() internal virtual view { 54 | require(msg.sender == address(entryPoint()), "account: not from EntryPoint"); 55 | } 56 | 57 | /** 58 | * validate the signature is valid for this message. 59 | * @param userOp validate the userOp.signature field 60 | * @param userOpHash convenient field: the hash of the request, to check the signature against 61 | * (also hashes the entrypoint and chain id) 62 | * @return validationData signature and time-range of this operation 63 | * <20-byte> sigAuthorizer - 0 for valid signature, 1 to mark signature failure, 64 | * otherwise, an address of an "authorizer" contract. 65 | * <6-byte> validUntil - last timestamp this operation is valid. 0 for "indefinite" 66 | * <6-byte> validAfter - first timestamp this operation is valid 67 | * If the account doesn't use time-range, it is enough to return SIG_VALIDATION_FAILED value (1) for signature failure. 68 | * Note that the validation code cannot use block.timestamp (or block.number) directly. 69 | */ 70 | function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) 71 | internal virtual returns (uint256 validationData); 72 | 73 | /** 74 | * validate the current nonce matches the UserOperation nonce. 75 | * then it should update the account's state to prevent replay of this UserOperation. 76 | * called only if initCode is empty (since "nonce" field is used as "salt" on account creation) 77 | * @param userOp the op to validate. 78 | */ 79 | function _validateAndUpdateNonce(UserOperation calldata userOp) internal virtual; 80 | 81 | /** 82 | * sends to the entrypoint (msg.sender) the missing funds for this transaction. 83 | * subclass MAY override this method for better funds management 84 | * (e.g. send to the entryPoint more than the minimum required, so that in future transactions 85 | * it will not be required to send again) 86 | * @param missingAccountFunds the minimum value this method sho uld send the entrypoint. 87 | * this value MAY be zero, in case there is enough deposit, or the userOp has a paymaster. 88 | */ 89 | function _payPrefund(uint256 missingAccountFunds) internal virtual { 90 | if (missingAccountFunds != 0) { 91 | (bool success,) = payable(msg.sender).call{value : missingAccountFunds, gas : type(uint256).max}(""); 92 | (success); 93 | //ignore failure (its EntryPoint's job to verify, not account.) 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /contracts/SimpleAccountV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity =0.8.17; 3 | 4 | /* solhint-disable avoid-low-level-calls */ 5 | /* solhint-disable no-inline-assembly */ 6 | /* solhint-disable reason-string */ 7 | 8 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 9 | import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; 10 | import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; 11 | 12 | import "./core/BaseAccount.sol"; 13 | 14 | /** 15 | * minimal account. 16 | * this is sample minimal account. 17 | * has execute, eth handling methods 18 | * has a single signer that can send requests through the entryPoint. 19 | */ 20 | contract SimpleAccountV2 is BaseAccount, UUPSUpgradeable, Initializable { 21 | using ECDSA for bytes32; 22 | 23 | //filler member, to push the nonce and owner to the same slot 24 | // the "Initializeble" class takes 2 bytes in the first slot 25 | bytes28 private _filler; 26 | 27 | //explicit sizes of nonce, to fit a single storage cell with "owner" 28 | uint96 private _nonce; 29 | address public owner; 30 | 31 | IEntryPoint private immutable _entryPoint; 32 | 33 | modifier onlyOwner() { 34 | _onlyOwner(); 35 | _; 36 | } 37 | 38 | /// @inheritdoc BaseAccount 39 | function nonce() public view virtual override returns (uint256) { 40 | return _nonce; 41 | } 42 | 43 | /// @inheritdoc BaseAccount 44 | function entryPoint() public view virtual override returns (IEntryPoint) { 45 | return _entryPoint; 46 | } 47 | 48 | // solhint-disable-next-line no-empty-blocks 49 | receive() external payable {} 50 | 51 | constructor(IEntryPoint anEntryPoint) { 52 | _entryPoint = anEntryPoint; 53 | } 54 | 55 | function _onlyOwner() internal view { 56 | //directly from EOA owner, or through the account itself (which gets redirected through execute()) 57 | require(msg.sender == owner || msg.sender == address(this), "only owner"); 58 | } 59 | 60 | /** 61 | * execute a transaction (called directly from owner, or by entryPoint) 62 | */ 63 | function execute(address dest, uint256 value, bytes calldata func) external { 64 | _requireFromEntryPointOrOwner(); 65 | _call(dest, value, func); 66 | } 67 | 68 | /** 69 | * execute a sequence of transactions 70 | */ 71 | function executeBatch(address[] calldata dest, bytes[] calldata func) external { 72 | _requireFromEntryPointOrOwner(); 73 | require(dest.length == func.length, "wrong array lengths"); 74 | for (uint256 i = 0; i < dest.length; i++) { 75 | _call(dest[i], 0, func[i]); 76 | } 77 | } 78 | 79 | 80 | // Require the function call went through EntryPoint or owner 81 | function _requireFromEntryPointOrOwner() internal view { 82 | require(msg.sender == address(entryPoint()) || msg.sender == owner, "account: not Owner or EntryPoint"); 83 | } 84 | 85 | /// implement template method of BaseAccount 86 | function _validateAndUpdateNonce(UserOperation calldata userOp) internal override { 87 | require(_nonce++ == userOp.nonce, "account: invalid nonce"); 88 | } 89 | 90 | /// implement template method of BaseAccount 91 | function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) 92 | internal override virtual returns (uint256 validationData) { 93 | bytes32 hash = userOpHash.toEthSignedMessageHash(); 94 | if (owner != hash.recover(userOp.signature)) 95 | return SIG_VALIDATION_FAILED; 96 | return 0; 97 | } 98 | 99 | function _call(address target, uint256 value, bytes memory data) internal { 100 | (bool success, bytes memory result) = target.call{value : value}(data); 101 | if (!success) { 102 | assembly { 103 | revert(add(result, 32), mload(result)) 104 | } 105 | } 106 | } 107 | 108 | /** 109 | * check current account deposit in the entryPoint 110 | */ 111 | function getDeposit() public view returns (uint256) { 112 | return entryPoint().balanceOf(address(this)); 113 | } 114 | 115 | /** 116 | * deposit more funds for this account in the entryPoint 117 | */ 118 | function addDeposit() public payable { 119 | entryPoint().depositTo{value : msg.value}(address(this)); 120 | } 121 | 122 | /** 123 | * withdraw value from the account's deposit 124 | * @param withdrawAddress target to send to 125 | * @param amount to withdraw 126 | */ 127 | function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public onlyOwner { 128 | entryPoint().withdrawTo(withdrawAddress, amount); 129 | } 130 | 131 | function _authorizeUpgrade(address) internal view override { 132 | _onlyOwner(); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /contracts/core/StakeManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity =0.8.17; 3 | 4 | import "../interfaces/IStakeManager.sol"; 5 | 6 | /* solhint-disable avoid-low-level-calls */ 7 | /* solhint-disable not-rely-on-time */ 8 | /** 9 | * manage deposits and stakes. 10 | * deposit is just a balance used to pay for UserOperations (either by a paymaster or an account) 11 | * stake is value locked for at least "unstakeDelay" by a paymaster. 12 | */ 13 | abstract contract StakeManager is IStakeManager { 14 | 15 | /// maps paymaster to their deposits and stakes 16 | mapping(address => DepositInfo) public deposits; 17 | 18 | /// @inheritdoc IStakeManager 19 | function getDepositInfo(address account) public view returns (DepositInfo memory info) { 20 | return deposits[account]; 21 | } 22 | 23 | // internal method to return just the stake info 24 | function _getStakeInfo(address addr) internal view returns (StakeInfo memory info) { 25 | DepositInfo storage depositInfo = deposits[addr]; 26 | info.stake = depositInfo.stake; 27 | info.unstakeDelaySec = depositInfo.unstakeDelaySec; 28 | } 29 | 30 | /// return the deposit (for gas payment) of the account 31 | function balanceOf(address account) public view returns (uint256) { 32 | return deposits[account].deposit; 33 | } 34 | 35 | receive() external payable { 36 | depositTo(msg.sender); 37 | } 38 | 39 | function _incrementDeposit(address account, uint256 amount) internal { 40 | DepositInfo storage info = deposits[account]; 41 | uint256 newAmount = info.deposit + amount; 42 | require(newAmount <= type(uint112).max, "deposit overflow"); 43 | info.deposit = uint112(newAmount); 44 | } 45 | 46 | /** 47 | * add to the deposit of the given account 48 | */ 49 | function depositTo(address account) public payable { 50 | _incrementDeposit(account, msg.value); 51 | DepositInfo storage info = deposits[account]; 52 | emit Deposited(account, info.deposit); 53 | } 54 | 55 | /** 56 | * add to the account's stake - amount and delay 57 | * any pending unstake is first cancelled. 58 | * @param unstakeDelaySec the new lock duration before the deposit can be withdrawn. 59 | */ 60 | function addStake(uint32 unstakeDelaySec) public payable { 61 | DepositInfo storage info = deposits[msg.sender]; 62 | require(unstakeDelaySec > 0, "must specify unstake delay"); 63 | require(unstakeDelaySec >= info.unstakeDelaySec, "cannot decrease unstake time"); 64 | uint256 stake = info.stake + msg.value; 65 | require(stake > 0, "no stake specified"); 66 | require(stake <= type(uint112).max, "stake overflow"); 67 | deposits[msg.sender] = DepositInfo( 68 | info.deposit, 69 | true, 70 | uint112(stake), 71 | unstakeDelaySec, 72 | 0 73 | ); 74 | emit StakeLocked(msg.sender, stake, unstakeDelaySec); 75 | } 76 | 77 | /** 78 | * attempt to unlock the stake. 79 | * the value can be withdrawn (using withdrawStake) after the unstake delay. 80 | */ 81 | function unlockStake() external { 82 | DepositInfo storage info = deposits[msg.sender]; 83 | require(info.unstakeDelaySec != 0, "not staked"); 84 | require(info.staked, "already unstaking"); 85 | uint48 withdrawTime = uint48(block.timestamp) + info.unstakeDelaySec; 86 | info.withdrawTime = withdrawTime; 87 | info.staked = false; 88 | emit StakeUnlocked(msg.sender, withdrawTime); 89 | } 90 | 91 | 92 | /** 93 | * withdraw from the (unlocked) stake. 94 | * must first call unlockStake and wait for the unstakeDelay to pass 95 | * @param withdrawAddress the address to send withdrawn value. 96 | */ 97 | function withdrawStake(address payable withdrawAddress) external { 98 | DepositInfo storage info = deposits[msg.sender]; 99 | uint256 stake = info.stake; 100 | require(stake > 0, "No stake to withdraw"); 101 | require(info.withdrawTime > 0, "must call unlockStake() first"); 102 | require(info.withdrawTime <= block.timestamp, "Stake withdrawal is not due"); 103 | info.unstakeDelaySec = 0; 104 | info.withdrawTime = 0; 105 | info.stake = 0; 106 | emit StakeWithdrawn(msg.sender, withdrawAddress, stake); 107 | (bool success,) = withdrawAddress.call{value : stake}(""); 108 | require(success, "failed to withdraw stake"); 109 | } 110 | 111 | /** 112 | * withdraw from the deposit. 113 | * @param withdrawAddress the address to send withdrawn value. 114 | * @param withdrawAmount the amount to withdraw. 115 | */ 116 | function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external { 117 | DepositInfo storage info = deposits[msg.sender]; 118 | require(withdrawAmount <= info.deposit, "Withdraw amount too large"); 119 | info.deposit = uint112(info.deposit - withdrawAmount); 120 | emit Withdrawn(msg.sender, withdrawAddress, withdrawAmount); 121 | (bool success,) = withdrawAddress.call{value : withdrawAmount}(""); 122 | require(success, "failed to withdraw"); 123 | } 124 | } -------------------------------------------------------------------------------- /docs/3.md: -------------------------------------------------------------------------------- 1 | *원문: https://www.alchemy.com/blog/account-abstraction-wallet-creation* 2 | 3 | ## Wallet creation 4 | 5 | 지금 아직 다루지 않은 것은 각 사용자의 지갑 컨트랙트를 처음에 어떻게 올려야 하는가라는 것입니다. 6 | 컨트랙트를 배포하는 "고전적인" 방식은 EOA가 수신자가 없는 배포 코드 트랜잭션을 전송하는 것입니다. 그런데 그런 방법은 여기서 7 | 만족스럽지 않습니다. 왜냐하면 우리가 만들었던 것들은 EOA 없이도 블록체인과 연결하는 방식이었기 때문입니다. 8 | 사용자가 항상 EOA 계정을 만들고나서야 시작할 수 있다면 그게 무슨 소용일까요? 9 | 10 | 우리가 하고자 하는 것은, 지갑이 필요한, 그러나 아직 지갑이 없는 누군가에게 새로운 온체인 지갑을 가질 수 있도록 11 | 하는 것인데, 이 과정에서 이더로 가스비를 지불할 수도 있고(설령 지갑이 없더라도), 아니면 가스비를 대신 내줄 paymaster를 찾아서(두 번째 글에서 다룬 적이 있음), 12 | EOA 계정을 생성하지 않고도 할 수 있기를 바라는 것입니다. 13 | 14 | 매우 중요하지만 잘 드러나지 않은 또 다른 목표도 있습니다. 15 | 16 | 새로운 EOA 계정을 생성할 때, 로컬에서 개인 키를 생성하기만 하면 그 어떤 트랜잭션을 보내지 않아도 계정을 가질 수 있습니다. 17 | 한번도 거래를 일으킨 적이 없어도 내 계정 주소를 다른 사람에게 말하면 이더나 토큰을 받을 수 있습니다. 18 | 19 | 그래서 지갑(컨트랙트)도 같은 식으로 동작했으면 좋겠습니다. 다른 사람에게 우리의 (지갑)주소를 말하면, 실제 지갑 컨트랙트를 배포하기 전이라도 20 | 자산을 받을 수 있어야 한다는 것입니다. 21 | 22 | ### Prerequisite: Deterministic contract addresses with CREATE2 23 | 24 | 컨트랙트를 실제로 배포하기 전에 어떤 주소로 자산을 받을 수 있다는 것이 이것을 구현하는데 필요한 힌트입니다. 이것은 배포된 25 | 지갑 컨트랙트가 없다고 해도 나중에 배포하고나면 얻게 될 지갑주소를 알아야 한다는 것을 의미합니다. 26 | 27 | >💡아직 배포하지 않았지만 배포되면 만들어질 주소를 "counterfactual" 주소라고 합니다. 멋진 이름입니다. 28 | (역주: "counterfactual wallet"은 레이어2에서 종종 등장하는 개념으로 새로운 것은 아님) 29 | 30 | 이것을 가능하게 하는 주요 구성요소는 `CREATE2` opcode인데 바로 이런 목적을 위해 만들어졌습니다. 이것은 다음 항목들을 입력값으로 해서, 미리 계산되어 결정된(deterministically) 31 | 주소로 컨트랙트를 배포합니다: 32 | 33 | - `CREATE2`를 호출하는 컨트랙트의 주소 34 | - 32 바이트의 난수값(salt) 35 | - 배포될 컨트랙트의 init 코드 36 | 37 | init 코드는 EVM 바이트코드의 바이너리 데이터로, 다른 EVM 바이트코드를 리턴해서 신규로 생성된 스마트 컨트랙트로 저장시키는 38 | 기능을 합니다(역주: init 코드는 생성자 코드가 포함되어 배포 시점에 실행되고 그 후에는 생성자 코드가 제외된 바이트 코드가 블록체인에 저장되는 것을 의미하는 듯). 39 | 40 | 이것은 사람들이 잘 모르는 흥미로운 부분입니다: **컨트랙트를 배포할 때 전송된 코드와 저장되는(역주: 배포된 후) 코드가 같지 않습니다.** 41 | 특히 동일한 init 코드를 계속 배포한다고 해도 배포된 컨트랙트가 같은 코드를 가지고 있다고 보장할 수 없습니다. 왜냐하면 init 코드가 (다른) 스토리지를 참조하거나 42 | `TIMESTAMP` 같은 opcode를 사용하는 경우가 있을 수 있기 때문입니다. 43 | 44 | ### First attempt: Entry point deploys arbitrary contracts 45 | 46 | `CREATE2`에 대해 알았으니 첫 번째 계획은 간단해졌습니다. 사용자들로부터 init 코드를 받아서 아직 존재하지 않으면 `entry point`가 그 컨트랙트를 배포하도록 합니다. 47 | 먼저 사용자 요청에 다음 항목을 추가합니다. 48 | 49 | ```solidity 50 | struct UserOperation { 51 | // ... 52 | bytes initCode; 53 | } 54 | ``` 55 | 56 | 그 다음에는 `entry point`의 유효성 검사 부분인 `handleOps`를 아래와 같이 수정합니다: 57 | 58 | 사용자 요청 검사 과정에서, `initCode`가 비어있지 않으면 `CREATE2`를 사용하여 컨트랙트를 배포합니다. 그리고 나머지 검사를 실행합니다: 59 | 60 | - 방금 배포된 지갑 컨트랙트의 `validateOp` 메소드를 호출합니다. 61 | - 요청에 paymaster가 있다면 paymaster의 `validatePaymasterOp` 메소드를 호출합니다. 62 | 63 | 정말 괜찮은 시도 같습니다! 64 | 65 | 위에서 언급했던 모든 목표를 달성했습니다: 66 | 사용자는 임의의 컨트랙트를 배포할 수 있고 배포될 주소를 미리 알 수 있습니다. 또 paymaster가 배포를 후원해줄 수도 67 | 있고 사용자가 직접 가스비를 낼 수도 있습니다(배포될 지갑 주소에 이더를 보낸다면). 68 | 69 | 그러나 결함이 몇가지 있는데, 사용자가 임의의 바이트코드를 보내고 `entry point`가 그것을 검사해야 한다는 부분이 문제입니다. 70 | 71 | - paymaster가 사용자 요청을 보고 가스비 지불 여부를 결정하는 과정에서 바이트코드 바이너리 데이터를 분석하기 어렵습니다. 72 | - 사용자가 배포용 바이트코드를 전송할 때 해당 바이트코드가 제대로 동작할지 바로 검증할수 없습니다. 사용자가 배포 도구를 사용한다면 73 | 만약 그 도구가 악성 코드를 가지고 있거나 해킹되면 배포 컨트랙트에 백도어가 삽입될 수 있습니다. 이러한 속임수는 금방 알아차릴 수 없습니다. 74 | - 첫 번째 글을 상기해봅시다. 번들러는 각 요청들에 대해 유효성 검사 시뮬레이션을 수행해서 번들 안에 실패할 요청들이 75 | 포함되어 가스비를 손해보는 경우를 방지했습니다. 하지만 init 코드는 그 어떤 코드도 가능하므로 시뮬레이션에서 문제가 없어도 실제 실행에서는 실패할 수 있습니다. 76 | 77 | 사용자가 임의의 바이트코드를 보내지 않아도 컨트랙트를 배포할 방법이 필요합니다. 그리고 다른 참여자들이 배포 과정에 대해 78 | 어떤 보장을 해줄 수 있는 방안도 있었으면 좋겠습니다. 79 | 80 | 이번에도 마찬가지로 더 많은 보증인이 필요한 시점이고, 새로운 컨트랙트를 도입할 때가 된 것 같습니다. 81 | 82 | ### Better attempt: Introducing factories 83 | 84 | `entry point`가 임의의 배포 코드를 받아서 `CREATE2`를 호출하는 대신, 사용자가 컨트랙트를 하나를 선택해서 `CREATE2`를 85 | 호출하도록 하려고 합니다. 이들 컨트랙트들을 **팩토리(factories)** 라고 하는데, 여러 다른 종류의 지갑 컨트랙트를 생성하는데 특화된 컨트랙트입니다. 86 | 87 | 예를 들면, Carbonated Courage 토큰을 보관하는 지갑을 생성하는 팩토리가 있을 수 있고, 5개의 키중 3개의 키로 서명을 해야 하는 88 | 지갑을 만드는 팩토리가 있을 수도 있습니다. 89 | 90 | **팩토리는 컨트랙트를 생성할 때 호출해야 하는 메소드를 외부에 제공합니다.** 91 | 92 | ```solidity 93 | contract Factory { 94 | function deployContract(bytes data) returns (address); 95 | } 96 | ``` 97 | >💡팩토리는 새로 배포된 주소를 리턴하기 때문에 사용자는 이 메소드를 시뮬레이션해서 배포된 후에 가지게 될 컨트랙트의 주소를 알 수 있습니다. 98 | 99 | **사용자 요청에 항목을 추가하여 만약 지갑을 배포하는 요청이라면 어느 팩토리를 사용할지, 그리고 그 팩토리에 전달할 테이터를 받을 수 있도록 합니다:** 100 | 101 | ```solidity 102 | struct UserOperation { 103 | // ... 104 | address factory; 105 | bytes factoryData; 106 | } 107 | ``` 108 | 109 | ![3-1.png](../img/3-1.png) 110 | 111 | 이렇게 해서 앞에서 제기된 두 가지 문제를 해결할 수 있었습니다! 112 | 113 | - 사용자가 Carbonated Courage를 보관하는 지갑 팩토리를 호출하면 그 팩토리 컨트랙트는 검증된(audited) 것이고, 백도어 같은 것이 114 | 없다고 판단할 수 있습니다. 그래서 바이트 코드를 검사하지 않아도 됩니다. 115 | - paymaster는 승인된 팩토리로 배포되는 경우 선택적으로 가스비를 내줄 수 있습니다. 116 | 117 | 아직 남은 문제는 배포 코드가 시뮬레이션에서는 성공했지만 실제 실행에서 실패하는 경우입니다. 118 | 119 | 이것은 paymaster의 `validatePaymasterOp`를 만들 때 부딪힌 문제와 완전히 동일한 경우이고 같은 방식으로 해결할 수 있습니다. 120 | 121 | 번들러는 팩토리와 지갑이 그에 연관된 스토리지에만 접근할수 있도록 제한합니다. 그리고 `TIMESTAMP` 같은 금지된 코드들이 122 | 있는지 확인합니다. 123 | 124 | 또한 팩토리가 `entry point`의 `addStake` 메소드를 사용하여 이더를 예치하게 할 수도 있을 것입니다. 번들러는 125 | 최근 시뮬레이션에서 얼마나 자주 문제를 일으킨 팩토리인지 추적할 수 있고 이들을 차단하거나 금지할 수 있을 것입니다. 126 | 127 | >💡paymaster에서 처럼, 배포 메소드가 팩토리 자신과 연관된 스토리지가 아닌, 배포되는 지갑과 관련된 스토리지만 사용하다면 스테이킹을 할 필요는 없습니다. 128 | 129 | And we’ve done it! 130 | 131 | 지갑 생성을 이보다 더 좋게 만들 수는 없을 것 같군요. 132 | 133 | 여기까지 해서, 우리가 설계한 구조가 실제 ERC-4337의 모든 기능들을 수행할 수 있습니다! 134 | 135 | 마지막 네 번째 글에서 다루게 될 내용은 압축(aggregating) 서명에 관한 것인데, 이것은 가스비를 최적화하는데 도움을 136 | 주지만 새로운 기능을 추가하는 것은 아닙니다. 137 | 138 | 여기서 그만 하고 성공을 자축해도 될 것 같습니다. 하지만 가스비를 절약하고 싶다면 다음 글을 계속... 139 | 140 | 141 | [이전](./2.md) | [다음](./4.md) -------------------------------------------------------------------------------- /contracts/SimpleAccount.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity =0.8.17; 3 | 4 | /* solhint-disable avoid-low-level-calls */ 5 | /* solhint-disable no-inline-assembly */ 6 | /* solhint-disable reason-string */ 7 | 8 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 9 | import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; 10 | import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; 11 | 12 | import "./core/BaseAccount.sol"; 13 | 14 | 15 | /** 16 | * minimal account. 17 | * this is sample minimal account. 18 | * has execute, eth handling methods 19 | * has a single signer that can send requests through the entryPoint. 20 | */ 21 | contract SimpleAccount is BaseAccount, UUPSUpgradeable, Initializable { 22 | using ECDSA for bytes32; 23 | 24 | //filler member, to push the nonce and owner to the same slot 25 | // the "Initializeble" class takes 2 bytes in the first slot 26 | bytes28 private _filler; 27 | 28 | //explicit sizes of nonce, to fit a single storage cell with "owner" 29 | uint96 private _nonce; 30 | address public owner; 31 | 32 | IEntryPoint private immutable _entryPoint; 33 | 34 | event SimpleAccountInitialized(IEntryPoint indexed entryPoint, address indexed owner); 35 | 36 | modifier onlyOwner() { 37 | _onlyOwner(); 38 | _; 39 | } 40 | 41 | /// @inheritdoc BaseAccount 42 | function nonce() public view virtual override returns (uint256) { 43 | return _nonce; 44 | } 45 | 46 | /// @inheritdoc BaseAccount 47 | function entryPoint() public view virtual override returns (IEntryPoint) { 48 | return _entryPoint; 49 | } 50 | 51 | 52 | // solhint-disable-next-line no-empty-blocks 53 | receive() external payable {} 54 | 55 | constructor(IEntryPoint anEntryPoint) { 56 | _entryPoint = anEntryPoint; 57 | _disableInitializers(); 58 | } 59 | 60 | function _onlyOwner() internal view { 61 | //directly from EOA owner, or through the account itself (which gets redirected through execute()) 62 | require(msg.sender == owner || msg.sender == address(this), "only owner"); 63 | } 64 | 65 | /** 66 | * execute a transaction (called directly from owner, or by entryPoint) 67 | */ 68 | function execute(address dest, uint256 value, bytes calldata func) external { 69 | _requireFromEntryPointOrOwner(); 70 | _call(dest, value, func); 71 | } 72 | 73 | /** 74 | * execute a sequence of transactions 75 | */ 76 | function executeBatch(address[] calldata dest, bytes[] calldata func) external { 77 | _requireFromEntryPointOrOwner(); 78 | require(dest.length == func.length, "wrong array lengths"); 79 | for (uint256 i = 0; i < dest.length; i++) { 80 | _call(dest[i], 0, func[i]); 81 | } 82 | } 83 | 84 | /** 85 | * @dev The _entryPoint member is immutable, to reduce gas consumption. To upgrade EntryPoint, 86 | * a new implementation of SimpleAccount must be deployed with the new EntryPoint address, then upgrading 87 | * the implementation by calling `upgradeTo()` 88 | */ 89 | function initialize(address anOwner) public virtual initializer { 90 | _initialize(anOwner); 91 | } 92 | 93 | function _initialize(address anOwner) internal virtual { 94 | owner = anOwner; 95 | emit SimpleAccountInitialized(_entryPoint, owner); 96 | } 97 | 98 | // Require the function call went through EntryPoint or owner 99 | function _requireFromEntryPointOrOwner() internal view { 100 | require(msg.sender == address(entryPoint()) || msg.sender == owner, "account: not Owner or EntryPoint"); 101 | } 102 | 103 | /// implement template method of BaseAccount 104 | function _validateAndUpdateNonce(UserOperation calldata userOp) internal override { 105 | require(_nonce++ == userOp.nonce, "account: invalid nonce"); 106 | } 107 | 108 | /// implement template method of BaseAccount 109 | function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) 110 | internal override virtual returns (uint256 validationData) { 111 | 112 | bytes32 hash = userOpHash.toEthSignedMessageHash(); 113 | //console.logBytes32(hash); 114 | if (owner != hash.recover(userOp.signature)) 115 | return SIG_VALIDATION_FAILED; 116 | return 0; 117 | } 118 | 119 | function _call(address target, uint256 value, bytes memory data) internal { 120 | (bool success, bytes memory result) = target.call{value : value}(data); 121 | if (!success) { 122 | assembly { 123 | revert(add(result, 32), mload(result)) 124 | } 125 | } 126 | } 127 | 128 | /** 129 | * check current account deposit in the entryPoint 130 | */ 131 | function getDeposit() public view returns (uint256) { 132 | return entryPoint().balanceOf(address(this)); 133 | } 134 | 135 | /** 136 | * deposit more funds for this account in the entryPoint 137 | */ 138 | function addDeposit() public payable { 139 | entryPoint().depositTo{value : msg.value}(address(this)); 140 | } 141 | 142 | /** 143 | * withdraw value from the account's deposit 144 | * @param withdrawAddress target to send to 145 | * @param amount to withdraw 146 | */ 147 | function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public onlyOwner { 148 | entryPoint().withdrawTo(withdrawAddress, amount); 149 | } 150 | 151 | function _authorizeUpgrade(address) internal view override { 152 | _onlyOwner(); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /docs/4.md: -------------------------------------------------------------------------------- 1 | *원문: https://www.alchemy.com/blog/account-abstraction-aggregate-signatures* 2 | 3 | ## Aggregate signatures 4 | 5 | 현재 구현에서는 번들 안에 있는 각 사용자들의 요청을 개별적으로 검증하도록 되어 있습니다. 이런 방식은 유효성을 검사하는 6 | 직접적인 방식이고 다소 비효율적인 면이 있습니다. 서명을 검증하는 것은 암호학적인 연산을 필요로 하기 때문에 가스 소비 7 | 측면에서 비싼 작업입니다. 8 | 9 | **모든 서명을 하나씩 검증하는 대신 한번 검증으로 다수의 사용자 요청들을 동시에 처리하면 좋지 않을까요?** 10 | 11 | 그렇게 하기 위해서는 압축 서명(aggregate signatures)이라는 암호학적인 개념을 가져와야 합니다. 12 | 13 | 압축 서명을 지원하는 서명 구조는, 서로 다른 키로 서명된 여러 개의 메시지가 있을 때, 서명을 합쳐서 한 개의 서명을 14 | 생성하는 방법입니다. 이 서명을 검증하면 그 안에 있는 모든 서명들이 유효하다는 것을 증명할 수 있습니다. 15 | 16 | 이러한 서명 구조가 가능한 것들 중 하나가 BLS입니다(역주: BLS는 보네-린-샤참이 제안한 타원곡선 암호 기반 디지털 서명 방식). 17 | 18 | 이 방식은 데이터를 압축하면서 서명까지 압축할 수 있어서 롤업을 구현할 때 특히 유용합니다. 19 | 20 | 압축 서명에 대한 설명은 [비탈릭의 트윗](https://twitter.com/VitalikButerin/status/1554983955182809088)을 참고하기 바랍니다. 21 | 22 | ### Introducing aggregators 23 | 24 | (하지만) 번들 안에 있는 사용자 요청의 서명들을 전부 압축할 수 없다는 것을 깨닫게 되는데, 지갑은 서명을 확인하는 자신만의 로직을 가지고 있고, 그래서 25 | 다양한 서명 방식들이 존재할 수 있기 때문입니다. 26 | 27 | 다른 구조를 지닌 서명은 같이 압축할 수 없기 때문에 번들은 사용자 요청의 그룹들로 나누어지게 될 것입니다. 각 그룹들은 다른 압축 서명 구조를 가지거나 또는 28 | 그런 구조가 아예 없을 수도 있습니다. 29 | 30 | 각자 로직에 맞는 다양한 압축 서명 방식이 온체인에 필요하므로 각 구조들을 처리하는 `aggregator`라고 하는 컨트랙트가 있어야 합니다. 31 | 32 | 압축 방식은 다수의 서명을 어떻게 합쳐서 하나로 만들고 검증할 수 있는지를 정의합니다. 그래서 `aggregator`는 다음 두 개의 메소드를 가지고 있습니다. 33 | 34 | ```solidity 35 | contract Aggregator { 36 | function aggregateSignatures(UserOperation[] ops) returns (bytes aggregatedSignature); 37 | function validateSignatures(UserOperation[] ops, bytes signature); 38 | } 39 | ``` 40 | ![4-1.png](../img/4-1.png) 41 | 42 | 각 지갑들은 자신만의 서명 구조를 정의하고 있으므로 어느 `aggregator`를 사용할지는 지갑에 달려 있습니다. 43 | 44 | **지갑이 압축 서명에 참여하려면 어떤 `aggregator`를 선택했는지 알려주는 메소드를 제공해야 합니다:** 45 | 46 | ```solidity 47 | contract Wallet { 48 | // ... 49 | 50 | function getAggregator() returns (address); 51 | } 52 | ``` 53 | 새로운 메소드인 `getAggregator`을 사용하여 번들러는 같은 서명 방식을 사용하는 요청들로 묶을 수 있습니다. 그리고 54 | `aggregator`의 `aggregateSignatures` 메소드로 서명들을 합치면 됩니다. 55 | 56 | 서명 그룹은 다음과 같은 형식이 될 겁니다: 57 | 58 | ```solidity 59 | struct UserOpsPerAggregator { 60 | UserOperation[] ops; 61 | address aggregator; 62 | bytes combinedSignature; 63 | } 64 | ``` 65 | 66 | >💡번들러가 오프체인에서 특정 `aggregator`에 대해 알고 있다면 EVM에서 `aggregateSignatures`을 실행하는 대신 서명 알고리즘을 네이티브하게 구현해서 압축 서명을 최적화 할 수도 있습니다. 67 | 68 | 다음에는, `entry point` 컨트랙트가 새로운 압축 서명을 활용할 수 있도록 수정하는 일입니다. 69 | `entry point`은 `handleOps` 메소드에서 요청 목록을 사용한다는 것을 상기해봅시다. 70 | 71 | **새로운 메소드 `handleAggregatedOps`를 만듭니다. 이 메소드는 같은 작업을 수행하지만 `aggregator`에 의해 그룹핑된 요청들을 전달받습니다:** 72 | 73 | ```solidity 74 | contract EntryPoint { 75 | function handleOps(UserOperation[] ops); 76 | 77 | function handleAggregatedOps(UserOpsPerAggregator[] ops); 78 | 79 | // ... 80 | } 81 | ``` 82 | 83 | `handleAggregatedOps`은 `handleOps`은 많은 부분에서 같지만 서명 확인 단계에서 차이가 있습니다. 84 | `handleOps`는 각 지갑의 `validateOp`을 호출하면서 유효성 검사를 수행하는 반면, `handleAggregatedOps`은 각 그룹에 적용된 서명 구조에 따라 합쳐진 서명을 `aggregator`의 `validateSignatures`에 85 | 전달하여 검증하게 됩니다. 86 | 87 | ![4-2.png](../img/4-2.png) 88 | 89 | 거의 다 완성했습니다! 90 | 91 | 그런데 여기서 자주 부딪힌 한가지 문제가 있습니다. 92 | 93 | 번들러는 유효성 검사 시뮬레이션을 수행해야 하고 요청들이 번들에 포함되기 전에 `aggregator`가 요청 그룹을 검증하도록 할 것입니다. 왜냐하면 94 | 유효성 검사가 실패하면 번들러가 가스비를 부담해야 하기 때문입니다. 그러나 임의의 로직을 가진 `aggregator`는 시뮬레이션에서 성공하지만 실제 실행에서 실패할 수 있습니다. 95 | 96 | 이런 문제는 paymaster나 팩토리와 같은 방식으로 해결할 것입니다: `aggregator`가 접근할 수 있는 스토리지와 opcode에 제한을 두고 이더를 97 | `entry point`에 스테이킹하게 만드는 것입니다. 98 | 99 | 이것으로 압축 서명에 대한 이야기를 마무리 하겠습니다! 100 | 101 | ### Wrap up 102 | 103 | 지금까지 ERC-4337의 구조와 거의 유사한 계정 추상화를 만들어보았습니다! 메소드 이름이나 전달인자 같은 상세 부분에서 몇 가지 차이점이 있기는 합니다. 104 | 하지만 구조적인 차이는 없을 것 같습니다. 이 글의 내용이 잘 전달되었다면 실제 ERC-4337을 보고 어떻게 돌아가는지 이해하는데 문제가 없을 것입니다. 105 | 106 | 지금까지 읽어주셔서 감사합니다. 이 글을 쓰는 것이 저에게 도움된 것처럼 여러분에게도 도움이 되었기를 바랍니다. 107 | 108 | ## Addendum: Differences from ERC-4337 109 | **부록: ERC-4337과의 차이점** 110 | 111 | 계정 추상화의 전반적인 구조에 대해서 알아보았는데, ERC-4337을 설계한 스마트한 사람들의 생각과 이 글에서 설명한 것이 다소 차이나는 부분들이 있습니다. 112 | 113 | 한번 살펴보겠습니다! 114 | 115 | ### 1. Validation time ranges 116 | 117 | 앞 글에서는 지갑의 `validateOp`과 paymaster의 `validatePaymasterOp`의 리턴 타입에 대해 특별히 언급하지 않았습니다. 118 | ERC-4337 에서는 이것을 활용하는 방법을 제시합니다. 119 | 120 | 지갑이 제일 잘 하는 일은 사용자 요청을 일정한 시간 동안만 유효하게 하는 것입니다. 그렇지 않으면 나쁜 번들러가 그 요청을 121 | 계속 가지고 있다가 나중에 이익을 보는 시점에 번들에 포함시킬 수도 있습니다. 122 | 123 | 지갑은 요청이 곧 실행될 것임을 확인하기 위해, 유효성 승인 중에 `TIMESTAMP`를 검사함으로써 이러한 상황에 대비하기를 원할 것입니다. 124 | 그러나 유효성 검사에서는 시뮬레이션의 부정확성을 막기위해 `TIMESTAMP`를 금지했기 때문에 그렇게 하기 어렵습니다. 125 | 이것은 요청이 유효한 시간을 가리키는 다른 방법이 필요하다는 것을 의미합니다. 126 | 127 | **그래서 ERC-4337에서는 `validateOp`에서 지갑이 시간 간격을 선택할 수 있도록 값을 리턴합니다:** 128 | 129 | ```solidity 130 | contract Wallet { 131 | function validateOp(UserOperation op, uint256 requiredPayment) returns (uint256 sigTimeRange); 132 | // ... 133 | } 134 | ``` 135 | 이 값은 8바이트짜리 정수 두 개를 사용하여 해당 요청의 유효한 시간 범위를 나타냅니다. 136 | (역주: ERC-4337에는 다음과 같이 되어 있음: `validUntil` is 8-byte timestamp value, or zero for "infinite". The UserOp is valid only up to this time. 137 | `validAfter` is 8-byte timestamp. The UserOp is valid only after this time). 138 | 139 | 이것 외에도 ERC-4337은 다른 점이 있습니다: 지갑은 `validateOp`에서, 유효성 검사에 실패한 경우 revert 하기 보다는 140 | 감시자 값(sentinel value, 역주: 여기서는 실패했음을 알려주는 값)을 리턴해야 한다고 되어 있습니다. 이것은 가스를 산정하는데 필요한데, 왜냐하면 revert 가 되어버리면 141 | `eth_estimateGas`으로 얼마의 가스가 들었는지 알 수 없기 때문입니다. 142 | 143 | ### 2. Arbitrary call data for wallets and factories 144 | 145 | 우리가 만든 지갑의 인터페이스는 아래와 같았습니다. 146 | 147 | ```solidity 148 | contract Wallet { 149 | function validateOp(UserOperation op, uint256 requiredPayment); 150 | function executeOp(UserOperation op); 151 | } 152 | ``` 153 | 154 | ERC-4337에는 `executeOp`이라는 메소드는 없습니다. 155 | 156 | **대신에 사용자 요청에 `callData` 항목이 있습니다:** 157 | 158 | ```solidity 159 | struct UserOperation { 160 | // ... 161 | bytes callData; 162 | } 163 | ``` 164 | 일반적인 스마트 컨트랙트에서는 (콜데이터의) 첫 번째 4바이트는 함수 셀렉터이고 나머지가 함수에 전달되는 파라미터들입니다. 165 | 166 | 이것은 `validateOp`를 제외하고, 지갑은 자신들의 인터페이스를 정의할 수 있고 사용자 요청으로 지갑의 임의의 (이름을 가진) 메소드를 호출할 수 있다는 것을 167 | 의미합니다. 168 | 169 | 같은 맥락으로, ERC-4337에서 팩토리 컨트랙트는 `deployContract`라는 메소드를 정의하지 않습니다. 사용자 요청에 `initCode` 항목이 있는 경우, 임의의 콜데이터를 받습니다. 170 | 171 | ### 3. Compact data for paymasters and factories 172 | 173 | 앞선 글에서 사용자 요청에는 paymaster를 지정하는 항목과 거기에 전달하는 데이터 항목이 있다고 했습니다: 174 | 175 | ```solidity 176 | struct UserOperation { 177 | // ... 178 | address paymaster; 179 | bytes paymasterData; 180 | } 181 | ``` 182 | **ERC-4337에서는 이들 항목들이 최적화의 이유로 하나로 합쳐져 있습니다. 첫 20바이트는 paymaster의 주소이며 나머지는 데이터입니다:** 183 | 184 | ```solidity 185 | struct UserOperation { 186 | // ... 187 | bytes paymasterAndData; 188 | } 189 | ``` 190 | 191 | 팩토리도 같은 식으로 되어 있습니다. 우리는 두 개의 항목, `factory`와 `factoryData`를 만들었지만 ERC-4337에서는 192 | 이것을 `initCode` 항목에 넣었습니다. 193 | 194 | 자, 이제 다 되었습니다! 195 | 계정 추상화에 대해 많은 것을 알게 되었기를 바랍니다. 196 | 197 | [이전](./3.md) -------------------------------------------------------------------------------- /docs/sample-account.md: -------------------------------------------------------------------------------- 1 | *SimpleAccount: https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/samples/SimpleAccount.sol* 2 | 3 | ### 스마트 계정 예제 4 | `SimpleAccount`는 ERC-4337의 표준에 맞게 구현한 단순한 SC Wallet 입니다. 가장 기본이 되는 지갑 인터페이스인 [`IAccount`](../contracts/interfaces/IAccount.sol)의 구현체가 되겠습니다. 5 | 6 | 표준에서 권고한 것처럼, 지갑을 변경할 수 있는 업그레이드 패턴을 사용하고 있습니다. 여기서는 오픈제펠린 UUPS(Universal Upgradeable Proxy Standard) 패턴을 이용하여 작성되었습니다. UUPS는 7 | 오픈제펠린 업그레이드의 디폴트 패턴인 Transparent proxy 패턴과는 다르게, 업그레이드를 하는 코드(`upgradeTo`)가 로직 컨트랙트 쪽에 있습니다. 8 | 따라서 `SimpleAccount`는 `Initializable`과 함께 `UUPSUpgradeable`를 상속 받아 구현합니다. 9 | 10 | ```solidity 11 | import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; 12 | import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; 13 | 14 | contract SimpleAccount is BaseAccount, UUPSUpgradeable, Initializable { 15 | ... 16 | } 17 | ``` 18 | 19 | UUPS에 대한 설명은 [여기](./uups.md)를 참조하기 바랍니다. 20 | 21 | `BaseAccount`는 `IAccount` 인터페이스를 구현한 추상 컨트랙트로, 표준에서 제안한 기본적인 메소드를 실행하고, 상세 구현은 다시 22 | 이를 상속받은 `SimpleAccount`에서 구현합니다. 즉 `IAccount`에서 구현해야 하는 메소드는 하나인데, `validateUserOp`은 `BaseAccount`에 아래와 같이 23 | 되어 있습니다. 24 | 25 | ```solidity 26 | function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) 27 | external override virtual returns (uint256 validationData) { 28 | _requireFromEntryPoint(); 29 | validationData = _validateSignature(userOp, userOpHash); 30 | if (userOp.initCode.length == 0) { 31 | _validateAndUpdateNonce(userOp); 32 | } 33 | _payPrefund(missingAccountFunds); 34 | } 35 | ``` 36 | `_requireFromEntryPoint`, `_validateSignature`, `_validateAndUpdateNonce`, 등은 `virtual`로 선언되어 있고 37 | 상속받는 컨트랙트 `SimpleAccount`에서 오버라이드 할 수 있습니다. 38 | 39 | 외부에서 호출되는 `validateUserOp`은 번들러가 사용자 요청을 시뮬레이션하고 번들링을 할 때, 지갑에 전송하기 직전에 entry point를 통해 호출됩니다. 40 | 이 메소드를 호출할 수 있는 것은 entry point 뿐이기 때문에, `_requireFromEntryPoint`라는 조건이 걸려 있습니다. 41 | 42 | ```solidity 43 | function _requireFromEntryPoint() internal virtual view { 44 | require(msg.sender == address(entryPoint()), "account: not from EntryPoint"); 45 | } 46 | ``` 47 | `entryPoint()`는 `SimpleAccount`에 하드코딩되어 있는 entry point의 주소를 반환합니다. 만약 entry point 주소가 바뀌면 48 | 업그레이드를 통해 새로운 entry point를 가리키는 다른 지갑으로 대체해야 합니다. 49 | 50 | `_validateSignature`는 사용자 요청인 `userOp`의 서명을 검사합니다. `UserOperation`은 구조체로 정의되어 있으며 `signature`라는 51 | 항목을 가지고 있습니다. 여기서는 이더리움의 기본 전자서명 방식인 ECDSA를 사용하여(`ecrecover`) 서명을 검증합니다. 전자서명은 지갑마다 얼마든지 다를 수 있고, 변경 가능하므로 52 | `SimpleAccount`에서 오버라이드 됩니다. 53 | 54 | ```solidity 55 | function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) internal override virtual returns (uint256 validationData) { 56 | bytes32 hash = userOpHash.toEthSignedMessageHash(); 57 | if (owner != hash.recover(userOp.signature)) 58 | return SIG_VALIDATION_FAILED; 59 | return 0; 60 | } 61 | ``` 62 | 표준에서 정의된 것처럼 전자서명이 유효하지 않으면 `SIG_VALIDATION_FAILED`을 리턴합니다(revert 하지말고). 63 | 64 | `userOp.initCode`가 없으면, 즉 이미 배포된 지갑이라면 지갑의 nonce를 증가시킵니다. nonce는 업그레이드 메커니즘에 의해 코드가 업그레이드 되더라도 보존됩니다. 65 | 당연히 사용자 요청의 nonce와 일치하지 않으면 revert 됩니다. 66 | 67 | ```solidity 68 | function _validateAndUpdateNonce(UserOperation calldata userOp) internal override { 69 | require(_nonce++ == userOp.nonce, "account: invalid nonce"); 70 | } 71 | ``` 72 | 73 | `_payPrefund`는 entry point에게 지급하는 수수료에 해당하는 것으로, 전달받은 사용자 요청의 가스비가 이미 예치되어 있는 74 | 잔액보다 큰 경우에 모자란 가스비를 송금하는 함수 입니다. 75 | 76 | ```solidity 77 | function _payPrefund(uint256 missingAccountFunds) internal virtual { 78 | if (missingAccountFunds != 0) { 79 | (bool success,) = payable(msg.sender).call{value : missingAccountFunds, gas : type(uint256).max}(""); 80 | (success); 81 | //ignore failure (its EntryPoint's job to verify, not account.) 82 | } 83 | } 84 | ``` 85 | 86 | 이 함수는 entry point가 호출하게 되므로 `msg.sender`는 entry point 가 됩니다. 가스 `gas`가 사실상 무한대인 것은 87 | 이 메소드가 반드시 끝까지 수행되어야 한다는 의미로, 계정은 송금 트랜잭션을 호출하고 바로 종료합니다(리턴 값을 검사하지 않고). 그 이후 처리는 entry point가 수행합니다. 88 | 89 | 업그레이드 컨트랙트는 생성자를 사용하지 않고 별도의 일반 함수를 초기화 함수로 사용합니다. 오픈제펠린의 초기화 함수 이름은 90 | `initialize`입니다. 91 | 92 | ```solidity 93 | function initialize(address anOwner) public virtual initializer { 94 | _initialize(anOwner); 95 | } 96 | 97 | function _initialize(address anOwner) internal virtual { 98 | owner = anOwner; 99 | emit SimpleAccountInitialized(_entryPoint, owner); 100 | } 101 | ``` 102 | 103 | UUPS는 로직 컨트랙트가 업그레이드를 수행하는 함수를 가지고 있으므로 특정 관리자 권한을 가진 계정만이 실행해야 합니다. 그래서 104 | `UUPSUpgradeable._authorizeUpgrade`를 상속하여 구현합니다. 105 | 106 | ```solidity 107 | function _authorizeUpgrade(address) internal view override { 108 | _onlyOwner(); 109 | } 110 | ``` 111 | 112 | entry point는 지갑의 `execute` 함수를 호출하여 사용자 요청을 전달합니다. 이 함수는 `_requireFromEntryPointOrOwner`의 조건이 113 | 적용되어 entry point 또는 지갑 소유자만이 실행할 수 있습니다. 114 | 115 | ```solidity 116 | function execute(address dest, uint256 value, bytes calldata func) external { 117 | _requireFromEntryPointOrOwner(); 118 | _call(dest, value, func); 119 | } 120 | ``` 121 | 지갑은 low-level 호출함수인 `call`을 통해 사용자 요청을 실행합니다. 이 요청은 단순 송금일 수도 있고 컨트랙트 호출일 수도 있습니다. 122 | 123 | 지갑을 통해 entry point에 이더를 예치하거나 인출할 수 있는 함수도 구현되어 있습니다. 124 | 125 | ```solidity 126 | function addDeposit() public payable { 127 | entryPoint().depositTo{value : msg.value}(address(this)); 128 | } 129 | 130 | function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public onlyOwner { 131 | entryPoint().withdrawTo(withdrawAddress, amount); 132 | } 133 | ``` 134 | 당연한 것이지만 이더를 받을 수 있도록 `receive`가 있어야 합니다. 지갑 소유자는 직접 이더를 보낼 수 있습니다. 135 | 136 | ```solidity 137 | receive() external payable {} 138 | ``` 139 | 140 | ### 컴파일과 배포 141 | 142 | 오픈제펠린은 업그레이드 컨트랙트 배포를 관리해주는 하드햇 플러그인 [hardhat-upgrades](https://docs.openzeppelin.com/upgrades-plugins/1.x/)을 143 | 제공합니다. 로직 컨트랙트만 작성하면 플러그인이 나머지 업그레이드 관련 컨트랙트들을 자동으로 생성합니다. 그리고 144 | 업그레이드 과정 중에 발생하는 스토리지 충돌이나 기타 보안적인 문제들고 함께 체크해주기 때문에 안전하고 편리하기도 합니다. 145 | 146 | 이 예제에서는 `SimpleAccount`를 처음에 배포하고 entry point 주소를 변경한 `SimpleAccountV2`를 새로 배포하여 147 | 로직 컨트랙트를 교체합니다. 148 | 149 | 두 번의 배포에서 프록시 컨트랙트의 주소는 변경되지 않지만 로직 컨트랙트의 주소는 달라집니다. 최초 배포는 `upgrades.deployProxy`을 통해서 프록시와 150 | 관련 컨트랙트를 배포하고 그 이후에는 `upgrades.upgradeProxy`으로 로직 컨트랙트를 배포하면 프록시가 새로운 컨트랙트를 가리키게 됩니다. 151 | 152 | `SimpleAccount`는 entry point의 주소를 하드코딩하는데 `immutable`로 선언되어 있습니다. `immutable`은 생성자에서 초기화되기 때문에 153 | 로직 컨트랙트에 생성자를 작성합니다. 154 | 155 | ```solidity 156 | IEntryPoint private immutable _entryPoint; 157 | 158 | constructor(IEntryPoint anEntryPoint) { 159 | _entryPoint = anEntryPoint; 160 | _disableInitializers(); 161 | } 162 | ``` 163 | 164 | 플러그인에서는 로직 컨트랙트의 생성자를 디폴트로 막기 때문에 배포 스크립트에서 `unsafeAllow` 옵션을 주어야 정상적으로 배포가 가능합니다. 165 | 166 | ```javascript 167 | upgrades.deployProxy(simpleAccount, [owner], {kind: "uups", unsafeAllow: ["constructor","state-variable-immutable"], constructorArgs: [ENTRY_POINT]}) 168 | ``` 169 | 170 | 배포 후에 출력되는 컨트랙트 주소는 프록시 컨트랙트 주소이고 이더스캔에서 조회해보면 로직 컨트랙트의 주소를 알 수 있습니다. 171 | 또는 `@openzeppelin/upgrades-core` 패키지에 있는 `getImplementationAddress`을 사용하여 알 수도 있습니다. 172 | 173 | * 컨트랙트 174 | [SimpleAccount](https://github.com/boyd-dev/account-abstraction/blob/main/contracts/SimpleAccount.sol) 175 | * 배포 스크립트 176 | [depolySimpleAccount](https://github.com/boyd-dev/account-abstraction/tree/main/scripts) 177 | [depolySimpleAccountV2](https://github.com/boyd-dev/account-abstraction/tree/main/scripts) 178 | * 테스트 케이스 179 | [SimpleAccount-test](https://github.com/boyd-dev/account-abstraction/blob/main/test/SimpleAccount-test.js) -------------------------------------------------------------------------------- /docs/2.md: -------------------------------------------------------------------------------- 1 | *원문: https://www.alchemy.com/blog/account-abstraction-paymasters* 2 | 3 | ## Sponsoring Transactions Using Paymasters 4 | 5 | 지난 글에서 (여러 역할을 분담하는 컨트랙트를 통해) EOA의 기능을 그대로 구현했고 사용자가 자신만의 유효성 검사 로직을 선택할 수 있도록 했습니다. 6 | 7 | 그런데 지갑은 여전히 가스비를 내야하고, 이것은 지갑 주인이 온체인에 어떤 작업을 수행하려면 이더를 가지고 있어야 한다는 것을 의미합니다. 8 | 9 | 만약 지갑 소유자 대신에 다른 누군가가 가스비를 내주는 경우가 필요하다면 어떻게 해야 할까요? 10 | 11 | **이것이 필요한 이유들이 있습니다:** 12 | 13 | - 지갑 소유자가 블록체인을 처음 사용하는 사람이라면, 온체인에서 무엇을 실행하기 전에 이더를 확보해야 할 필요가 있는데 이것이 어려운 일이 될 수 있습니다. 14 | - 잠재적인 고객들이 주저하지 않도록 애플리케이션이 가스비를 대신 내주는 것을 원할 수도 있습니다. 15 | - 스폰서가 가스비를 이더가 아닌, USDC 같은 다른 토큰으로 내는 것을 허용할 수도 있습니다(역주: 스폰서가 이더로 가스비를 내주고 사용자에게는 USDC를 청구한다는 의미). 16 | - 개인정보 보호 차원에서, 사용자가 믹서를 통해 새 주소로 자산을 인출하여 관련없는 계정에서 가스비를 내고 싶어할 지도 모릅니다. 17 | 18 | ### Introducing paymasters 19 | 20 | 내가 다른 사람의 가스비를 대신 내주는 dapp이라고 해봅시다. (그렇다고 해도) 아마 모든 사람들의 가스비를 전부 내주고 싶지는 않을텐데, 21 | 그래서 사용자 요청을 확인하고 그 요청의 가스비를 대신 내줄지 말지를 결정하는 어떤 로직을 체인에 적용할 필요가 있을 겁니다. 22 | 23 | 로직을 체인에 적용하려면 컨트랙트를 배포해야 하고 그것을 "paymaster"라고 하겠습니다. 24 | 25 | 이것은 사용자 요청을 검사하여 가스비 지불 여부를 결정하는 하나의 메소드만 가지게 될 것입니다: 26 | 27 | ```solidity 28 | contract Paymaster { 29 | function validatePaymasterOp(UserOperation op); 30 | } 31 | ``` 32 | 이렇게 되면, 지갑이 사용자 요청을 제출할 때 가스비를 부담해줄 paymaster가 지정되어 있는지(paymaster가 있다면) 확인할 필요가 있습니다. 33 | 34 | 사용자 요청에 새로운 항목을 추가하겠습니다. 또 paymaster가 지불 여부를 판단하는데 필요한 어떤 데이터를 전달하기 위한 35 | 항목도 추가합니다. 예를 들어 오프체인에서 paymaster 관리자가 서명한 어떤 데이터일 수 있습니다. 36 | 37 | ```solidity 38 | struct UserOperation { 39 | // ... 40 | address paymaster; 41 | bytes paymasterData; 42 | } 43 | ``` 44 | 다음에는, 새로 추가된 paymaster를 처리하기 위해 `entry point`의 `handleOps`를 수정합니다. 이것은 다음과 같이 동작할 것입니다. 45 | 46 | 각 요청에 대하여: 47 | - 전송자가 지정한 지갑의 `validateOp`을 호출합니다. 48 | - 요청에 paymaster 주소가 있다면 paymaster의 `validatePaymasterOp`을 호출합니다. 49 | - 유효성 검사에서 실패한 요청들은 폐기합니다. 50 | - 지갑의 `executeOp`을 실행하고 소모된 가스를 추적하여 그에 상응하는 이더를 실행자에게 지불합니다. paymaster 항목이 있다면 51 | paymaster가 그것을 지불합니다. 그렇지 않으면 이전과 동일하게 지갑이 부담합니다. 52 | 53 | 지갑에서와 마찬가지로, paymaster는 `entry point`의 `deposit` 메소드를 통해 이더를 미리 예치합니다. 54 | 55 | ![2-1.png](../img/2-1.png) 56 | 57 | 매우 간단하죠? 58 | 59 | 번들러도 시뮬레이션을 변경할 필요가 있습니다. 60 | 61 | ### Paymaster staking 62 | 63 | 이전 글에서 지갑이 번들러에게 가스비를 되돌려 주는 부분에서, 번들러는 유효성 검사의 실패를 피하기 위해서 시뮬레이션을 수행한다고 했습니다. 왜냐하면 유효성 검사가 실패하면 가스비를 환불받지 못하므로 64 | 가스비만 날리는 셈이기 때문입니다. 65 | 66 | **여기서도 같은 문제가 발생합니다.** 67 | 68 | paymaster에서 유효성을 통과하지 못하면 가스비를 돌려주지 않으므로 번들러는 paymaster 검사를 통과하지 못하게 될 사용자 요청을 처리하지 말아야 합니다. 69 | 70 | 우선적으로 `validateOp`에 적용했던 것과 동일한 제한을 `validatePaymasterOp`에도 할 것 같습니다(즉 지갑 자신과 지갑과 관련된 스토리지에만 접근 가능하며 금지된 opcode를 쓰지 말아야 한다는 등의). 그렇게 되면 71 | 번들러는 사용자 요청에 대해 `validatePaymasterOp`과 `validateOp`을 동시에 시뮬레이션할 수 있게 됩니다. 72 | 73 | 그러나 여기서 주의할 것이 있습니다. 74 | `validateOp`는 지갑과 연관된 스토리지에만 접근할 수 있다는 제약사항이기 때문에 번들링된 다수의 요청들이, 다른 지갑들이라면, 서로 간섭하지 않을 것이라는 사실을 75 | 알고 있습니다. 하지만 같은 paymaster를 사용하는 요청들은 그 paymaster의 스토리지를 공유하게 됩니다. 76 | 77 | 이것은 `validatePaymasterOp`이 한번 수행되고나면 동일한 paymaster를 사용하는 나머지 요청들이 유효성 검사에서 78 | 실패할 가능성이 있다는 것을 의미합니다. 79 | 80 | 나쁜 paymaster는 이것을 서비스 거부 공격(DoS)으로 사용할지도 모릅니다. 81 | 82 | 이것을 막기 위해 평판 시스템(reputation system)을 도입하겠습니다. 83 | 84 | 번들러는 각 paymaster들이 최근에 얼마나 자주 유효성 검사에 실패했는지 추적하고 그런 paymaster를 이용하는 요청을 85 | 금지하거나 유입을 차단하므로써 paymaster에게 페널티를 주는 것입니다. 86 | 87 | 이러한 평판 시스템은 나쁜 paymaster가 스스로 (역주: 자신에게 좋은 평판을 매기는 계정들을) 수없이 복제한다면(Sybil 공격) 제대로 동작하지 않을 것입니다. 그래서 paymaster에게 88 | 이더를 스테이킹하도록 할 것입니다. 다수의 계정을 만드는 것이 이익이 되지 않도록 하는 것입니다. 89 | 90 | **`entry point`에 스테이킹을 위한 새로운 메소드를 추가합니다:** 91 | 92 | ```solidity 93 | contract EntryPoint { 94 | // ... 95 | 96 | function addStake() payable; 97 | function unlockStake(); 98 | function withdrawStake(address payable destination); 99 | } 100 | ``` 101 | 한번 스테이킹이 되면 `unlockStake`을 호출한 후에 일정한 시간이 지날 때까지는 인출되지 않습니다. 새로운 메소드들은 102 | 지갑과 paymaster의 가스비 예치 메소드인 `deposit`과 즉시 인출이 이루어지는 `withdrawTo`와 구별됩니다. 103 | 104 | **스테이킹에는 예외가 하나 있습니다:** 105 | 106 | paymaster가 지갑과 연관된 스토리지에만 접근하고 자신의 스토리지에는 접근하지 않는다면 스테이킹을 할 필요가 없습니다. 왜냐하면, 이 경우에는 107 | 각 지갑의 `validateOp`와 같이 번들 안에 있는 다수의 요청에 의한 스토리지 접근이 겹치지 않을 것이기 때문입니다. 108 | 109 | 사실 평판 시스템의 자세한 규칙들을 이해하는 것이 그렇게 중요한 것 같지는 않습니다. [여기에](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-4337.md#specification-2) 더 상세한 내용이 있습니다. 110 | 하지만 가스비를 소진시켜버리는 paymaster의 요청을 가려내는 메커니즘을 번들러가 가지고 있다는 것을 아는 것만으로 충분할 것 같습니다. 111 | 112 | 또 각 번들러는 평판을 로컬에 추적하고 있어서, 작업에 도움이 되고 다른 번들러들과 문제를 일으키지 않는다면 113 | 평판을 산정하는 그들만의 로직을 자유로롭게 구현할 수 있습니다. 114 | 115 | >💡다른 스테이킹 시스템과는 다르게, 스테이커에게는 벌금(slashed)이 없습니다. 그들은 있을지도 모를 공격자에게 많은 자금이 116 | 필요하게 만들고, 락업시키는 하는 역할을 합니다(역주: 공격비용을 증가). 117 | 118 | ### Improvement: Paymaster `postOp` 119 | 120 | paymaster에 기능을 추가하여 약간 더 개선해보겠습니다. 현재 paymaster는 실제 실행 전에 유효성 검사 단계에서만 121 | 호출됩니다. 122 | 123 | 하지만 실행의 결과에 따라 어떤 다른 일을 수행할 필요가 있을지도 모릅니다. 124 | 125 | 예를 들면, 사용자가 가스비를 USDC로 지불할 수 있게 허용하는 paymaster는 USDC를 청구하기 위해서 요청에 소모된 가스량을 알 필요가 있습니다. 126 | 127 | 따라서 새로운 메소드 `postOp`를 추가하겠습니다. 이것은 `entry point`가 요청을 처리한 후에 얼마의 가스가 사용되었는지 128 | 전달할 때 호출될 것입니다. 129 | 130 | 또 paymaster가 어떤 정보를 다시 자신에게 전달해서 실행 후(post-op) 단계에서 유효성 검사 동안 처리된 데이터를 이용하는 경우도 있을 수 있습니다. 131 | 그래서 임의의 "컨텍스트" 데이터를 나중에 `postOp`으로 전달할 수 있도록 합니다. 132 | 133 | **`postOp`를 다음과 같이 만들어보겠습니다:** 134 | 135 | ```solidity 136 | contract Paymaster { 137 | function validatePaymasterOp(UserOperation op) returns (bytes context); 138 | function postOp(bytes context, uint256 actualGasCost); 139 | } 140 | ``` 141 | 하지만 paymaster가 나중에 USDC로 청구하는 방식은 까다로운 부분이 있습니다. 142 | 143 | 아마 paymaster는 실행하기 전(`validatePaymasterOp` 단계)에서 사용자가 충분한 USDC를 가지고 있는지 확인했을 것입니다. 144 | 하지만 실행하는 동안에 지갑의 USDC를 모두 인출해버리는 것이 가능하기 때문에, 이것은 paymaster가 마지막 단계에서 가스비를 받지 못한다는 것을 의미합니다. 145 | 146 | >💡paymaster가 시작 단계에서 최대 (최대 가스에 상당하는) USDC를 청구하고 남는 것은 나중에 되돌려주면 이런 상황을 147 | 피할 수 있지 않을까요? 가능한 이야기입니다. 하지만 그렇게 하면 두 번의 `transfer` 호출이 필요하고 가스가 더 들것입니다. 또 두 번의 148 | `Transfer` 이벤트가 발생할 것입니다. 다른 개선책이 있는지 살펴보겠습니다. 149 | 150 | (그래서) 요청이 실행이 된 후에도 paymaster가 요청을 무효화시킬 수 있는 방안이 필요합니다. 만약 그렇게 되면 어떤 경우라도 이미 `validatePaymasterOp` 단계에서 151 | 가스비를 청구하는 것에 동의했기 때문에 가스비를 인출할 수 있어야 합니다. 152 | 153 | 이 방법을 적용하려면 `entry point`가 `postOp`를 두 번 호출할 수 있도록 만들어야 합니다. 154 | 155 | `entry point`는 지갑의 `executeOp`가 실행되는 과정에서 `postOp`을 한번 실행할 것이고 만약 `postOp`이 156 | revert 된다면 `executeOp` 역시 revert 될 것입니다. 157 | 158 | 이런 상황이 발생하게 되면, `entry point`는 `postOp`를 한번 더 호출합니다. 하지만 이번에는 `executeOp`이 실행되기 전 상태, 즉 159 | `validatePaymasterOp`만 통과된 상태이므로 paymaster는 가스비를 인출할 수 있어야 합니다. 160 | 161 | `postOp`에 (이러한) 맥락을 반영하기 위해 파라미터 하나를 더 추가하겠습니다: 그것이 revert 후에 "두 번째 실행"인지를 식별할 수 있는 162 | 플래그를 두는 것입니다: 163 | 164 | ```solidity 165 | contract Paymaster { 166 | function validatePaymasterOp(UserOperation op) returns (bytes context); 167 | function postOp(bool hasAlreadyReverted, bytes context, uint256 actualGasCost); 168 | } 169 | ``` 170 | 171 | ### Recap: How paymasters enable sponsored transactions 172 | 지갑 소유자가 아닌 다른 사람이 가스비를 지불하도록 하기 위해서, 새로운 주체인 **paymaster**를 도입했습니다. paymaster는 아래와 같은 인터페이스를 가진 173 | 스마트 컨트랙트를 배포할 것입니다. 174 | 175 | ```solidity 176 | contract Paymaster { 177 | function validatePaymasterOp(UserOperation op) returns (bytes context); 178 | function postOp(bool hasAlreadyReverted, bytes context, uint256 actualGasCost); 179 | } 180 | ``` 181 | 182 | 사용자 요청에는 지정된 paymaster를 나타내는 새로운 항목이 추가됩니다. 183 | 184 | ```solidity 185 | struct UserOperation { 186 | // ... 187 | address paymaster; 188 | bytes paymasterData; 189 | } 190 | ``` 191 | 192 | paymaster는 지갑과 마찬가지 방식으로 `entry point`에 이더를 예치합니다. 193 | `entry point`는 `handleOps` 메소드를 수정해서 각 요청에 대하여, `validateOp`을 통한 지갑의 유효성 검사와 요청에 지정된 194 | paymaster의 `validatePaymasterOp`으로 paymaster를 검증합니다. 그리고 마지막에는 paymaster의 `postOp`을 195 | 호출합니다. 196 | 197 | paymaster의 유효성을 시뮬레이션하는 과정에서 발생할 수 있는 불상사를 막기 위해, 이더를 락업해주는 스테이킹 시스템을 도입할 198 | 필요도 있었습니다. 199 | 200 | 이를 위해 `entry point`에 몇 가지 메소드가 추가됩니다. 201 | 202 | ```solidity 203 | contract EntryPoint { 204 | // ... 205 | 206 | function addStake() payable; 207 | function unlockStake(); 208 | function withdrawStake(address payable destination); 209 | } 210 | ``` 211 | 212 | paymaster를 추가함으로써 대부분 사람들이 계정 추상화를 생각하면서 바라는 많은 기능들을 구현할 수 있습니다! 213 | 214 | 점점 ERC-4337에 가까워지고 있습니다. 그러나 같아지려면 아직도 몇 가지 기능이 더 필요합니다. 215 | 216 | Feels good to be here! 217 | 218 | [이전](./1.md) | [다음](./3.md) -------------------------------------------------------------------------------- /contracts/interfaces/IEntryPoint.sol: -------------------------------------------------------------------------------- 1 | /** 2 | ** Account-Abstraction (EIP-4337) singleton EntryPoint implementation. 3 | ** Only one instance required on each chain. 4 | **/ 5 | // SPDX-License-Identifier: GPL-3.0 6 | pragma solidity ^0.8.12; 7 | 8 | /* solhint-disable avoid-low-level-calls */ 9 | /* solhint-disable no-inline-assembly */ 10 | /* solhint-disable reason-string */ 11 | 12 | import "./UserOperation.sol"; 13 | import "./IStakeManager.sol"; 14 | import "./IAggregator.sol"; 15 | 16 | interface IEntryPoint is IStakeManager { 17 | 18 | /*** 19 | * An event emitted after each successful request 20 | * @param userOpHash - unique identifier for the request (hash its entire content, except signature). 21 | * @param sender - the account that generates this request. 22 | * @param paymaster - if non-null, the paymaster that pays for this request. 23 | * @param nonce - the nonce value from the request. 24 | * @param success - true if the sender transaction succeeded, false if reverted. 25 | * @param actualGasCost - actual amount paid (by account or paymaster) for this UserOperation. 26 | * @param actualGasUsed - total gas used by this UserOperation (including preVerification, creation, validation and execution). 27 | */ 28 | event UserOperationEvent(bytes32 indexed userOpHash, address indexed sender, address indexed paymaster, uint256 nonce, bool success, uint256 actualGasCost, uint256 actualGasUsed); 29 | 30 | /** 31 | * account "sender" was deployed. 32 | * @param userOpHash the userOp that deployed this account. UserOperationEvent will follow. 33 | * @param sender the account that is deployed 34 | * @param factory the factory used to deploy this account (in the initCode) 35 | * @param paymaster the paymaster used by this UserOp 36 | */ 37 | event AccountDeployed(bytes32 indexed userOpHash, address indexed sender, address factory, address paymaster); 38 | 39 | /** 40 | * An event emitted if the UserOperation "callData" reverted with non-zero length 41 | * @param userOpHash the request unique identifier. 42 | * @param sender the sender of this request 43 | * @param nonce the nonce used in the request 44 | * @param revertReason - the return bytes from the (reverted) call to "callData". 45 | */ 46 | event UserOperationRevertReason(bytes32 indexed userOpHash, address indexed sender, uint256 nonce, bytes revertReason); 47 | 48 | /** 49 | * signature aggregator used by the following UserOperationEvents within this bundle. 50 | */ 51 | event SignatureAggregatorChanged(address indexed aggregator); 52 | 53 | /** 54 | * a custom revert error of handleOps, to identify the offending op. 55 | * NOTE: if simulateValidation passes successfully, there should be no reason for handleOps to fail on it. 56 | * @param opIndex - index into the array of ops to the failed one (in simulateValidation, this is always zero) 57 | * @param reason - revert reason 58 | * The string starts with a unique code "AAmn", where "m" is "1" for factory, "2" for account and "3" for paymaster issues, 59 | * so a failure can be attributed to the correct entity. 60 | * Should be caught in off-chain handleOps simulation and not happen on-chain. 61 | * Useful for mitigating DoS attempts against batchers or for troubleshooting of factory/account/paymaster reverts. 62 | */ 63 | error FailedOp(uint256 opIndex, string reason); 64 | 65 | /** 66 | * error case when a signature aggregator fails to verify the aggregated signature it had created. 67 | */ 68 | error SignatureValidationFailed(address aggregator); 69 | 70 | /** 71 | * Successful result from simulateValidation. 72 | * @param returnInfo gas and time-range returned values 73 | * @param senderInfo stake information about the sender 74 | * @param factoryInfo stake information about the factory (if any) 75 | * @param paymasterInfo stake information about the paymaster (if any) 76 | */ 77 | error ValidationResult(ReturnInfo returnInfo, 78 | StakeInfo senderInfo, StakeInfo factoryInfo, StakeInfo paymasterInfo); 79 | 80 | /** 81 | * Successful result from simulateValidation, if the account returns a signature aggregator 82 | * @param returnInfo gas and time-range returned values 83 | * @param senderInfo stake information about the sender 84 | * @param factoryInfo stake information about the factory (if any) 85 | * @param paymasterInfo stake information about the paymaster (if any) 86 | * @param aggregatorInfo signature aggregation info (if the account requires signature aggregator) 87 | * bundler MUST use it to verify the signature, or reject the UserOperation 88 | */ 89 | error ValidationResultWithAggregation(ReturnInfo returnInfo, 90 | StakeInfo senderInfo, StakeInfo factoryInfo, StakeInfo paymasterInfo, 91 | AggregatorStakeInfo aggregatorInfo); 92 | 93 | /** 94 | * return value of getSenderAddress 95 | */ 96 | error SenderAddressResult(address sender); 97 | 98 | /** 99 | * return value of simulateHandleOp 100 | */ 101 | error ExecutionResult(uint256 preOpGas, uint256 paid, uint48 validAfter, uint48 validUntil, bool targetSuccess, bytes targetResult); 102 | 103 | //UserOps handled, per aggregator 104 | struct UserOpsPerAggregator { 105 | UserOperation[] userOps; 106 | 107 | // aggregator address 108 | IAggregator aggregator; 109 | // aggregated signature 110 | bytes signature; 111 | } 112 | 113 | /** 114 | * Execute a batch of UserOperation. 115 | * no signature aggregator is used. 116 | * if any account requires an aggregator (that is, it returned an aggregator when 117 | * performing simulateValidation), then handleAggregatedOps() must be used instead. 118 | * @param ops the operations to execute 119 | * @param beneficiary the address to receive the fees 120 | */ 121 | function handleOps(UserOperation[] calldata ops, address payable beneficiary) external; 122 | 123 | /** 124 | * Execute a batch of UserOperation with Aggregators 125 | * @param opsPerAggregator the operations to execute, grouped by aggregator (or address(0) for no-aggregator accounts) 126 | * @param beneficiary the address to receive the fees 127 | */ 128 | function handleAggregatedOps( 129 | UserOpsPerAggregator[] calldata opsPerAggregator, 130 | address payable beneficiary 131 | ) external; 132 | 133 | /** 134 | * generate a request Id - unique identifier for this request. 135 | * the request ID is a hash over the content of the userOp (except the signature), the entrypoint and the chainid. 136 | */ 137 | function getUserOpHash(UserOperation calldata userOp) external view returns (bytes32); 138 | 139 | /** 140 | * Simulate a call to account.validateUserOp and paymaster.validatePaymasterUserOp. 141 | * @dev this method always revert. Successful result is ValidationResult error. other errors are failures. 142 | * @dev The node must also verify it doesn't use banned opcodes, and that it doesn't reference storage outside the account's data. 143 | * @param userOp the user operation to validate. 144 | */ 145 | function simulateValidation(UserOperation calldata userOp) external; 146 | 147 | /** 148 | * gas and return values during simulation 149 | * @param preOpGas the gas used for validation (including preValidationGas) 150 | * @param prefund the required prefund for this operation 151 | * @param sigFailed validateUserOp's (or paymaster's) signature check failed 152 | * @param validAfter - first timestamp this UserOp is valid (merging account and paymaster time-range) 153 | * @param validUntil - last timestamp this UserOp is valid (merging account and paymaster time-range) 154 | * @param paymasterContext returned by validatePaymasterUserOp (to be passed into postOp) 155 | */ 156 | struct ReturnInfo { 157 | uint256 preOpGas; 158 | uint256 prefund; 159 | bool sigFailed; 160 | uint48 validAfter; 161 | uint48 validUntil; 162 | bytes paymasterContext; 163 | } 164 | 165 | /** 166 | * returned aggregated signature info. 167 | * the aggregator returned by the account, and its current stake. 168 | */ 169 | struct AggregatorStakeInfo { 170 | address aggregator; 171 | StakeInfo stakeInfo; 172 | } 173 | 174 | /** 175 | * Get counterfactual sender address. 176 | * Calculate the sender contract address that will be generated by the initCode and salt in the UserOperation. 177 | * this method always revert, and returns the address in SenderAddressResult error 178 | * @param initCode the constructor code to be passed into the UserOperation. 179 | */ 180 | function getSenderAddress(bytes memory initCode) external; 181 | 182 | 183 | /** 184 | * simulate full execution of a UserOperation (including both validation and target execution) 185 | * this method will always revert with "ExecutionResult". 186 | * it performs full validation of the UserOperation, but ignores signature error. 187 | * an optional target address is called after the userop succeeds, and its value is returned 188 | * (before the entire call is reverted) 189 | * Note that in order to collect the the success/failure of the target call, it must be executed 190 | * with trace enabled to track the emitted events. 191 | * @param op the UserOperation to simulate 192 | * @param target if nonzero, a target address to call after userop simulation. If called, the targetSuccess and targetResult 193 | * are set to the return from that call. 194 | * @param targetCallData callData to pass to target address 195 | */ 196 | function simulateHandleOp(UserOperation calldata op, address target, bytes calldata targetCallData) external; 197 | } 198 | -------------------------------------------------------------------------------- /docs/1.md: -------------------------------------------------------------------------------- 1 | *원문: https://www.alchemy.com/blog/account-abstraction* 2 | 3 | ## How simple choices lead down the complex path to ERC-4337 4 | **단순한 방식이 결국 복잡한 과정을 거쳐서 ERC-4337에 이르게 되기까지** 5 | 6 | 계정 추상화는 블록체인과 연결하는 방식을 완전히 바꾸게 될 것입니다. 하지만 [ERC-4337](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-4337.md)에서 제안된 계정 추상화의 표준은 읽기가 7 | 쉽지 않고, 왜 그렇게 많은 참여자들이 필요한지, 왜 그런 식으로 연계되는지 이해하기 힘들 것입니다. 8 | 9 | 좀 더 간단하게 할 수 없을까요? 10 | 11 | 이 글에서는 계정 추상화를 매우 단순하게 설계해보려고 합니다. 그리고 그 과정에서 발생하는 문제점들을 해결하고 더 많은 요구사항들을 추가시키고, 그래서 더 복잡해지고, 마침내 ERC-4337에 점점 가까워지는 것을 보게 될 것입니다. 12 | 13 | 이 글은 스마트 컨트랙트에 대한 지식은 있지만 계정 추상화에 대해서는 특별히 아는 것이 없는 사람들을 위해 작성되었습니다. 14 | 15 | 이 문서는 계정 추상화를 구현해가는 과정을 살펴보려하기 때문에 최종 버전인 ERC-4337과 일치하지 않는 API와 내용들이 있을 수 있습니다. 16 | 예를 들면, `User Operation`에 들어가는 항목들을 나열할 때 이것이 실제 표준의 항목들이라고 생각하지 마십시오. 그것은 `User Operation`을 정의하면서 17 | 최종 버전을 완성하기 전에 처음에 그냥 넣어보는 항목일 수 있습니다. 18 | 19 | 이제 시작해보겠습니다. 20 | 21 | ### Goal: Create a wallet that protects our assets 22 | 처음 시작은 귀중한 자산을 보호하는 방법을 고안하는 것입니다. 23 | 사람들은 보통 단일한 개인키(지금의 계정처럼)로 트랜잭션을 서명하기를 원합니다. 하지만 고가의 ["Carbonated Courage NFT"](https://carbonatedcourage.org/) 같은 것은 24 | 머리가 셋 달린 개(역주: 그리스 신화에 나오는 지하 세계를 지킨다는 개)가 지키는 금고 안에 보관된 두 번째 키까지 서명을 해야 25 | 전송이 가능하도록 해야 할 것 같습니다. 26 | 27 | 여기서 첫번째 질문이 나옵니다: 28 | 모든 이더리움 계정은 스마트 컨트랙트 계정 또는 EOA 계정입니다. 후자는 오프체인에서 개인키를 사용하여 만들어지고 관리됩니다. 자산을 보관하는 계정은 29 | 스마트 컨트랙트가 되어야 할까요, 아니면 EOA 계정이 되어야 할까요? 30 | 31 | 사실 자산을 보관하는 곳은 스마트 컨트랙트이어야 합니다. 만약 EOA가 자산을 가지고 있다면 그것은 언제든지 EOA의 개인키로 서명하기만 32 | 하면 전송될 수 있습니다. 33 | 34 | 현재 사람들의 생각과는 달리, (자산은) EOA가 아니라 스마트 컨트랙트에 의해 온체인 상에 있어야 하고 우리는 그것을 스마트 컨트랙트 지갑, 35 | 또는 그냥 "지갑(wallet)"이라고 하겠습니다. 36 | 37 | 그런데 우리가 원하는 기능을 수행할 수 있도록 지갑 컨트랙트에 명령을 내리는 방법이 필요합니다. EOA에서 했던 것처럼 38 | 전송이나 컨트랙트 호출을 할 수 있는 방법들이 필요한 것입니다. 39 | 40 | ### User operations 41 | 자산을 보관할 지갑 컨트랙트를 배포해보겠습니다. 이 컨트랙트는 내가 원하는 기능을 수행할 수 있는 하나의 메소드만 가지고 있습니다. 42 | 내 지갑에게 어떤 기능을 수행하도록 하는 액션을 나타내는 데이터를 `user operation` 또는 `user op` 이라고 하겠습니다(역주: 앞으로 "사용자 요청" 또는 "요청"이라고 하겠습니다). 43 | 지갑 컨트랙트는 아래와 같습니다. 44 | 45 | ```solidity 46 | contract Wallet { 47 | function executeOp(UserOperation op); 48 | } 49 | ``` 50 | 51 | `user operation`에는 어떤 것들이 들어가야 할까요? 우선 `eth_sendTransaction`에 전달되어야 하는 모든 파라미터들이 필요합니다: 52 | 53 | ```solidity 54 | struct UserOperation { 55 | address to; 56 | bytes data; 57 | uint256 value; // Amount of wei sent 58 | uint256 gas; 59 | // ... 60 | } 61 | ``` 62 | 여기에 (추가적으로) 이 요청을 허가할 수 있는 어떤 정보를 제공할 필요가 있습니다. 즉 지갑이 이 작업을 수행할지 여부를 결정할 수 있도록 63 | 해주는 데이터가 필요합니다. 64 | 65 | NFT를 보관하는 지갑에서, 대부분의 요청들은 메인 키로 서명한 전자서명을 전달하면 됩니다. 하지만 이 `user op`가 Carbonated Courage NFT를 66 | 전송하는 요청이라면 두 개의 키로 요청 데이터를 서명한 전자서명이 필요할 것입니다. 67 | 68 | 또 이전에 보낸 `user op`를 다시 보내는 리플레이 공격을 막기 위해 nonce를 보내야 합니다. 69 | 70 | ```solidity 71 | struct UserOperation { 72 | // ... 73 | bytes signature; 74 | uint256 nonce; 75 | } 76 | ``` 77 | 78 | 이렇게 하면 처음 세운 목표를 달성했습니다! 79 | 이 컨트랙트가 내 Carbonated Courage NFT를 가지고 있는 한, 두 개의 키로 서명된 전자서명이 없으면 절대로 다른 사람에게 전송될 수 없을 것입니다. 80 | 81 | ### Who calls the smart contract wallet? 82 | 83 | 여기서 그냥 넘어간 부분은 `executeOp(op)`를 호출하는 방법입니다. 내 개인키들 없이는 동작하지 않기 때문에 누가 그것을 호출하든 보안상 위험은 없을 것입니다. 하지만 그것을 실행해줄 누군가가 필요합니다. 84 | 85 | 이더리움에서는 모든 트랜잭션들이 EOA로부터 시작됩니다. 그리고 EOA는 가스비를 이더로 지불해야 합니다. 그렇게 하려면 86 | 이 지갑 컨트랙트를 호출하는 용도로만 사용하는 별도의 EOA 계정이 필요할 것 같습니다. 이 계정은 두 개의 키를 가질 필요는 없습니다. 가스비 정도만 지불할 수 있는 87 | 이더만 보유하게 하고, 반면에 귀중한 자산은 안전한 지갑 컨트랙트가 가지도록 하는 것입니다. 88 | 89 | 간단한 컨트랙트만으로 계정 추상화를 구현한 것입니다! Not bad! 90 | 91 | ![1-1.png](../img/1-1.png) 92 | 93 | ### Goal: No separate EOA 94 | 95 | 위 방법의 단점은 지갑을 호출하기 위해 별도의 EOA 계정을 하나 더 가지고 있어야 한다는 것입니다. 만약 그렇게 하고 싶지 않다면 어떻게 해야 할까요? 내가 가스비를 이더로 지불하는 것은 96 | 맞지만 두 개의 계정을 가지고 싶지는 않다면 말입니다. 97 | 98 | 앞에서 지갑 컨트랙트의 `executeOp` 메소드는 누구나 실행할 수 있을 것이라고 말했습니다. 그러니까 EOA 계정을 가진 누군가에게 99 | 부탁할 수도 있을 것입니다. 이런 역할을 하는 사람들을 "실행자"라고 부르겠습니다. 100 | 101 | 실행자는 가스비를 내야 하기 때문에 대부분 공짜로 그것을 해주는 사람들은 없을 것입니다. 그래서 지갑 컨트랙트가 102 | 약간의 이더를 가지도록 하고 실행자가 지갑을 호출하면 일정 금액의 이더를 실행자에게 보상해주도록 하는 것입니다. 103 | 104 | >💡"실행자(executor)”는 ERC-4337 에 나오는 용어는 아닙니다. 하지만 이런 역할을 수행하는 참여자를 잘 설명하는 용어입니다. 105 | 후에 이것을 ERC-4337에서 실제 사용되는 “bundler”라는 단어로 대체할 것입니다. 지금 단계에서는 "번들링"을 하지는 않기 때문에 그렇게 부르지는 않겠습니다. 다른 프로토콜에서는 이들을 “relayer”라고 하기도 합니다. 106 | 107 | ### First attempt: The wallet refunds the executor at the end 108 | 단순하게 시작해보겠습니다. 지갑의 인터페이스는 아래와 같았습니다: 109 | 110 | ```solidity 111 | contract Wallet { 112 | function executeOp(UserOperation op); 113 | } 114 | ``` 115 | `executeOp`를 수정합니다. 마지막 단계에서 어느 정도의 가스가 소모되었는지 계산하고 그에 맞는 금액의 이더를 실행자에게 되돌려주는 겁니다. 116 | 117 | ![1-2.png](../img/1-2.png) 118 | 119 | #### First brush with simulation 120 | 시뮬레이션해보기 121 | 122 | (실행자들이) 내 지갑 컨트랙트를 믿는다면 이런 방식은 잘 동작할 것입니다! 하지만 실행자들이 내 지갑이 가스비를 되돌려줄 것이라는 123 | 확신이 있어야 합니다. 만약 `executeOp`를 호출했는데 실제 가스비를 되돌려받지 못하면 실행자가 모든 비용을 내는 셈이 됩니다. 124 | 125 | 이러한 시나리오를 피하기 위해서는 `executeOp`를 로컬에서, 마치 `debug_traceCall`처럼 한번 시뮬레이션 해볼 필요가 있고 실제 가스비를 126 | 보전받을 수 있는지 확인하는 것입니다. 그 후에 진짜 트랜잭션을 보내면 됩니다. 127 | 128 | 여기서 문제는 실제에도 시뮬레이션처럼 동작할 것이라는 보장을 완전하게 할 수 없다는 점입니다. 시뮬레이션에서 129 | 지갑이 가스비를 되돌려주었다고 해도, 트랜잭션이 블록에 실제 저장되는 시점에는 실패할 수 있습니다. 나쁜 지갑은 130 | 의도적으로 그렇게 할 수 있고, 트랜잭션을 공짜로 실행하고 막대한 가스비를 실행자에게 물릴 수 있습니다. 131 | 132 | #### Simulation could differ from real execution for a couple of reasons: 133 | 시뮬레이션은 몇 가지 이유로 실제 트랜잭션과 차이가 날 수 있습니다: 134 | 135 | * 스토리지에서 값을 읽고 변경할 때 시뮬레이션 시점과 실제 트랜잭션이 발생하는 시점이 다릅니다. 136 | * `TIMESTAMP`, `BLOCKHASH`, `BASEFEE` 같은 opcode들이 사용되는 경우 블록마다 값이 다릅니다. 137 | 138 | 실행자가 취할 수 있는 한가지 방법은 이러한 동작을 제한하는 것입니다. 그러니까 "환경"과 관련된 opcode들을 사용하면 그것을 거부하는 것입니다. 139 | 하지만 이렇게 되면 너무 제한이 많아집니다. 140 | 141 | 지갑은 EOA처럼 모든 것을 실행할 수 있어야 합니다. 그래서 이러한 opcode들을 제한하는 것은 정당한 사용까지 막는 것입니다. 예를 들어, 이런 제한을 두면 142 | `TIMESTAMP`를 많이 사용하는 유니스왑과 연결할 수 없게 됩니다. 143 | 144 | 지갑의 `executeOp`는 임의의 코드를 포함할 수 있기 때문에 부정확한 시뮬레이션을 막기 위해 제약을 두는 것은 합리적이지 않습니다. 이 문제는 현재 인터페이스만으로 145 | 대처하기 어렵습니다. `executeOp`는 블랙박스처럼 보입니다. 146 | 147 | #### Better attempt: Introducing the entry point 148 | 149 | 문제는 실행자에게 신뢰할 수 없는 컨트랙트의 코드를 실행해줄 것을 요청한다는 데서 비롯됩니다. 150 | 실행자는 어떤 확실한 보장을 받기를 원합니다. 이것이 바로 스마트 컨트랙트가 필요한 이유이기도 한데, 새로운 신뢰 컨트랙트(즉 감사를 받고, 소스코드가 공개된)를 151 | 도입하는 것입니다. 이것을 `entry point` 라고 하고 다음과 같은 메소드를 실행자에게 제공하는 것입니다: 152 | 153 | ```solidity 154 | contract EntryPoint { 155 | function handleOp(UserOperation op); 156 | 157 | // ... 158 | } 159 | ``` 160 | 161 | `handleOp`은 다음과 같은 일을 수행합니다: 162 | 163 | * 지갑이 최대 가스에 대해 비용을 지불할 여력이 있는지 확인(`user op`에 있는 `gas` 항목에 기반). 만약 그렇지 못하다면 거부. 164 | 165 | * 지갑의 `executeOp` 메소드 호출(적당한 가스로), 그리고 실제 사용되는 가스를 추적 166 | 167 | * 실행자에게 가스비에 상응하는 지갑의 이더를 전송 168 | 169 | 세번째 항목의 작업을 수행하려면 이제는 지갑이 아니라 `entry point`가 이더를 가지고 있어야 합니다. 왜냐하면 앞서 본 것처럼 170 | 지갑 컨트랙트로부터 이더를 인출할 수 있을 것이라는 보장을 완전히 할 수 없기 때문입니다. 171 | 172 | 따라서 `entry point`는 지갑으로부터(또는 지갑을 대신한 다른 사람에게서) 가스비에 해당하는 이더를 받을 수 있는(역주: 예치) 메소드가 필요하고, 또 원하는 경우 173 | 지갑이 다시 그것을 되찾을 수 있는(역주: 인출) 메소드도 필요합니다: 174 | 175 | ```solidity 176 | contract EntryPoint { 177 | // ... 178 | 179 | function deposit(address wallet) payable; 180 | function withdrawTo(address payable destination); 181 | } 182 | ``` 183 | 이러한 구현을 통해 실행자는 어떤 경우라도 가스비를 되돌려 받을 수 있게 됩니다. 184 | 185 | ![1-3.png](../img/1-3.png) 186 | 187 | 이것은 실행자에게 확실히 좋은 결과입니다. 하지만 지갑에게 다른 큰 문제가 생기게 됩니다. 188 | 189 | >💡지갑이 `entry point`에 이더를 예치하는 것이 아니라 직접 가스비를 낼 수 있어야 하지 않을까요? 예, 그렇게 해야죠! 하지만 다음 섹션의 190 | 변경 사항을 적용하기 전까지는 그렇게 할 수 없고 또 여전히 예치/인출 기능은 필요합니다. 또 대납(paymaster) 기능을 지원하기 위해서라도 필요하게 됩니다. 191 | 192 | ### Splitting validation and execution 193 | 194 | 현재 지갑의 인터페이스는 아래와 같습니다. 195 | 196 | ```solidity 197 | contract Wallet { 198 | function executeOp(UserOperation op); 199 | } 200 | ``` 201 | 이 메소드는 두 가지 일을 수행합니다: 사용자 요청의 유효성을 검사하는 일, 그리고 그 요청을 실행하는 일입니다. 그런데 지갑의 소유자가 직접 가스비를 202 | 내는 경우라면 둘을 구분할 필요는 없지만 실행자에게 실행을 요청하는 경우에는 중요한 차이가 있습니다. 203 | 204 | 지금까지 구현은 어떤 경우라도 지갑이 실행자에게 가스비를 내도록 되어 있습니다. 하지만 유효성 검사가 실패한 경우에는 그렇게 하고 싶지 않습니다. 205 | 유효성 검사에 실패한다면 허가받지 않은 누군가가 지갑에 접근했다는 의미입니다. 206 | 207 | 이 경우에, `executeOp`에서 당연히 요청을 막게 되는데, 현재 구현에서는 가스비를 내도록 되어 있습니다. 문제가 심각해질 수 있는 것이, 208 | 지갑과 관련없는 누군가가 이러한 요청을 한꺼번에 계속 보낸다면 지갑의 가스비는 바닥나게 될 것입니다. 209 | 210 | 반면에, 유효성 검사는 통과했지만 트랜잭션이 실패한다면 지갑은 가스비를 내야하는 것이 맞습니다. 이것은 지갑 소유자가 트랜잭션이 성공하지 못해도 그 요청을 한 211 | 것이기 때문인데, 마치 EOA가 승인한 트랜잭션이 revert 되고 소모된 가스비를 부담해야 하는 것과 같은 상황입니다. 212 | 213 | 현재 지갑의 인터페이스에서는 하나의 메소드에서 처리되기 때문에 유효성 검사 실패와 트랜잭션 실행 실패를 구분할 방법이 없으므로 두 개로 나누어야 합니다. 214 | 215 | ```solidity 216 | contract Wallet { 217 | function validateOp(UserOperation op); 218 | function executeOp(UserOperation op); 219 | } 220 | ``` 221 | 222 | `entry point`에서 `handleOp`의 새로운 구현은 아래와 같습니다. 223 | 224 | * `validateOp`을 호출합니다. 실패하면 여기서 중단됩니다. 225 | 226 | * 사용될지도 모를 최대 가스에 대한 가스비를 지갑의 예치분에서 확보합니다(사용자 요청의 `gas` 항목에 기반). 지갑이 충분한 이더를 가지고 있지 않으면 거부합니다. 227 | 228 | * `executeOp`을 호출합니다. 그리고 소모되는 가스를 추적합니다. 호출이 성공하든 실패하든 미리 확보된 가스비를 실행자에게 되돌려주고 남은 이더는 예치분으로 남깁니다. 229 | 230 | 이렇게 하면 지갑에게도 좋은 일입니다! 이제 허가되지 않은 요청은 가스비가 청구되지 않습니다. 231 | 232 | ![1-4.png](../img/1-4.png) 233 | 234 | 그런데 상황이 다시 실행자에게 불리한 것처럼 느껴집니다... 235 | 236 | >💡나쁜 지갑에서 모든 실행을 `validateOp`에 넣으면 어떻게 될까요? 그래서 트랜잭션 실행이 실패하더라도 가스비가 청구되지 않도록 한다면? 잠시 후에 보게 되겠지만 237 | 트랜잭션을 처리하는 작업에는 `validateOp`가 적합하지 않도록 필요한 제약을 추가할 것입니다. 238 | 239 | ### Simulation redux 240 | **다시 시뮬레이션으로** 241 | 242 | 이제 허가되지 않은 사용자가 지갑을 요청을 보내면 그 요청은 `validateOp` 에서 막힐 것이고 지갑은 가스비를 내지 않을 것입니다. 하지만 실행자는 여전히 `validateOp`을 실행하는데 243 | 드는 비용을 내야 하고 그것은 보전받지 못합니다. 244 | 245 | 나쁜 지갑은 더 이상 공짜로 트랜잭션을 처리할 수 없지만 해커들은 계속 실패할 요청을 실행자에게 보내서 가스비를 소진시킬 수 있습니다. 246 | 247 | 앞서 살펴본 시뮬레이션에서, 실행자는 우선 로컬에서 그 요청이 제대로 실행될 수 있는지 확인할 것이고, 그 후에 온체인에서 `handleOp`를 호출하여 트랜잭션을 전송할 것입니다. 248 | 249 | 시뮬레이션에서 성공해도 실제 트랜잭션에서 성공이나 실패 여부를 보장할 수는 없기 때문에 실행자가 그것을 막을 방도는 없고 250 | 그래서 문제가 발생할 수 있었습니다. 251 | 252 | 그런데 이번에는 다른 부분이 있습니다. 253 | 254 | 실행자는 전체 요청, `validateOp`와 이어지는 `executeOp` 를 모두 시뮬레이션할 필요가 없습니다. 첫번째 부분 `validateOp`만 시뮬레이션해서 255 | 가스비를 낼 수 있는지 여부만 확인하면 됩니다. 임의의 코드를 실행하여 블록체인과 데이터를 주고받는 `executeOp`와는 다르게, `validateOp`에는 256 | 엄격한 제약을 적용할 수 있습니다. 257 | 258 | **좀더 구체적으로 말하면, 실행자는 `validateOp`가 다음 조건들을 만족하지 않으면 온체인의 어떤 것도 호출하지 않고 `user op`를 거부할 것입니다.** 259 | 260 | 1. 금지 목록에 있는, 이를테면 `TIMESTAMP`, `BLOCKHASH`, 등의 opcode들을 사용하지 않습니다. 261 | 2. 접근할 수 있는 스토리지는 지갑 컨트렉트와 관련된 스토리지(**associated storage**)이고, 다음과 같이 정합니다. 262 | 263 | - 지갑 컨트랙트의 스토리지 264 | - 다른 컨트랙트 스토리지 중에서 `mapping(address => value)`과 같이 지갑 주소에 해당하는 슬롯(역주: 다른 컨트랙트에서 지갑 주소를 `mapping` 타입의 키로 하는 스토리지가 있을 때 참조가 허용된다는 의미) 265 | - 지갑 주소와 동일한 스토리지 슬롯에 있는 다른 컨트랙트의 스토리지(솔리디티에서는 나타나지 않는, 일반적이지 않은 스토리지 구조). 266 | 267 | 이들 규칙의 목적은 시뮬레이션 `validateOp`은 성공이지만 실제 트랜잭션에서 실패할 가능성을 최소화하는데 있습니다. 268 | 269 | >💡스토리지 제한이 주는 장점은 서로 다른 지갑들의 `validateOp`를 호출할 때 서로 간섭하지 않도록 하는 것입니다. 이것은 270 | 번들링에서 중요한 문제입니다. 271 | 272 | ### Improvement: Paying for gas directly from the wallet 273 | 274 | 지금까지는 지갑이 가스비를 `entry point`에 미리 예치하고나서 요청을 보내는 방식이었습니다. 그러나 보통 EOA가 가스비를 지불합니다(역주: 현재 EOA 구조에서는). 지갑도 같은 식으로 동작할 수 있어야 하지 않을까요? 275 | 유효성 검사와 요청 실행을 분리한 상태이므로 가능합니다. `entry point`는 유효성 검사 과정에서 지갑에게 `entry point`로 276 | 이더를 보낼 것을 요구할 수 있고 그렇지 않으면 요청을 거절하게 할 수 있습니다. 277 | 이제 `entry point`가 지갑에게 가스비를 요청하고 요청한 금액이 들어오지 않으면 요청을 거절하도록 지갑의 `validateOp`를 수정해보겠습니다: 278 | 279 | ```solidity 280 | contract Wallet { 281 | function validateOp(UserOperation op, uint256 requiredPayment); 282 | function executeOp(UserOperation op); 283 | } 284 | ``` 285 | 유효성을 검사하는 시점에서는 실제로 실행될 때 정확히 얼만큼의 가스가 소모될지 모르기 때문에 사용자 요청의 `gas` 항목을 기준으로 286 | 최대 가스를 산정하여 지불할 수 있는 금액을 요청합니다. 실행이 완료되면 사용 되지 않은 이더는 지갑으로 환불할 수도 있을 것입니다. 287 | 288 | 그러나 여기서 주의할 점이 하나 있습니다. 스마트 컨트랙트를 작성할 때 임의의 컨트랙트에 이더를 보낸다는 것은 불안한 일입니다. 왜냐하면 그렇게 하는 과정에서 289 | 그 컨트랙트의 어떤 코드가 실행될 수 있는데, 그것을 실패하게 할 수도, 예상치 못한 가스를 소모시킬 수도, 또는 재진입 공격이 시도될 수 있습니다. 290 | 그래서 남은 가스비를 직접 지갑으로 환불하지는 않겠습니다. 291 | 292 | 대신 그것을 가지고 있다가 지갑이 인출 메소드를 통해서 나중에 가져갈 수 있도록 하겠습니다. 이것을 pull-payment pattern 이라고 합니다. 293 | 그래서 실제로는 `deposit` 되는 컨트랙트에 남은 가스비를 모아두기로 합니다. 나중에 지갑이 `withdrawTo`을 호출하여 찾아갈 수 있게 합니다. 294 | 295 | 결국 예치와 인출 구조가 필요하게 되는 것입니다(또는 적어도 인출은 필요). 296 | 297 | 이것은 지갑의 가스비 지불이 실제로 서로 다른 두 군데에서 발생할 수 있다는 것을 의미합니다: `entry point`가 보유한 이더, 그리고 지갑 자체가 가진 이더. 298 | `entry point`는 우선 예치된 이더에서 가스비를 내려고 할 것이고, 충분하지 않다면 지갑의 `validateOp`를 호출하여 필요한 수량을 요청할 것입니다. 299 | 300 | ### Executor incentives 301 | 302 | 지금까지는 실행자가 되는 것은 보상없는 임무였습니다. 그들은 많은 시뮬레이션을 해야 하지만 수익이 없습니다. 그리고 가끔 시뮬레이션을 303 | 잘못하면 가스비 손해를 볼 수도 있습니다. 304 | 305 | 실행자에게 보상을 주기 위해 지갑 소유자들이 요청과 함께 실행자에게 지급되는 팁을 전송할 수 있도록 하겠습니다. 306 | 307 | **요청에 다음과 같은 항목을 추가합니다.** 308 | 309 | ```solidity 310 | struct UserOperation { 311 | // ... 312 | uint256 maxPriorityFeePerGas; 313 | } 314 | ``` 315 | 일반 트랜잭션의 그것과 비슷한 이름처럼, `maxPriorityFeePerGas`는 트랜잭션을 보내는 사람이 자신의 요청을 먼저 처리해주기를 바라며 지불하는 316 | 수수료를 의미합니다. 317 | 318 | 실행자는 `entry point`의 `handleOp`를 호출할 때 더 낮은 `maxPriorityFeePerGas`를 선택해서 차액을 취할 수 있습니다. 319 | 320 | ### Entry point as a singleton 321 | 322 | 우리는 `entry point`가 어떻게 신뢰할 수 있는 컨트랙트가 될 수 있는지 그리고 무엇을 하는지 이야기 했습니다. 323 | 그런데 지갑이나 실행자에 어떤 `entry point`를 지정해야 하는지에 대해서는 언급하지 않았음을 알아차렸을 겁니다. 324 | 325 | `entry point`는 전체 시스템에서 싱글톤이 될 수 있습니다. 모든 지갑과 모든 실행자가 동일한 `entry point` 컨트랙트와 326 | 연결될 것입니다(역주: ERC-4337에는 다음과 같이 되어 있음: EntryPoint - a singleton contract to execute bundles of UserOperations. Bundlers/Clients whitelist the supported entrypoint). 327 | 328 | 이것은 사용자 요청 안에 특정 지갑을 지정해야 하고, 이 요청이 `entry point`의 `handleOp`에 전달되면 `entry point`가 유효성 검사와 실행을 어느 지갑에게 요청해야 하는지를 329 | 알 수 있어야 한다는 것을 의미합니다. 330 | 331 | 이렇게 수정해보겠습니다. 332 | 333 | ```solidity 334 | struct UserOperation { 335 | // ... 336 | address sender; 337 | } 338 | ``` 339 | 340 | ### No Separate EOA Recap 341 | 342 | 목표는 별도의 EOA 계정 없이 가스비를 내는 온체인 지갑을 만드는 일이었습니다. 그리고 그 목적을 달성했습니다! 343 | 344 | 지갑 인터페이스는 아래와 같습니다: 345 | 346 | ```solidity 347 | contract Wallet { 348 | function validateOp(UserOperation op, uint256 requiredPayment); 349 | function executeOp(UserOperation op); 350 | } 351 | ``` 352 | 353 | 그리고 전체 블록체인에 적용되는 싱글톤 entry point 인터페이스는: 354 | 355 | ```solidity 356 | contract EntryPoint { 357 | function handleOp(UserOperation op); 358 | function deposit(address wallet) payable; 359 | function withdrawTo(address destination); 360 | } 361 | ``` 362 | 지갑 주인이 뭔가를 보낼 때는 오프체인에서 요청을 만들고 실행자에게 넘겨주면 됩니다. 363 | 364 | 실행자는 지갑의 `validateOp` 메소드를 사용하여 이 요청을 시뮬레이션을 해보고 받아들일지 말지를 결정합니다. 365 | 366 | 받아들인다면 실행자는 트랜잭션을 `entry point`의 `handleOp`를 호출합니다. 367 | 368 | `entry point`는 유효성을 검사하고 온체인에서 해당 요청을 실행합니다. 그리고 실행자에게 지갑이 예치해놓은 이더에서 가스비를 되돌려줍니다. 369 | 370 | Whew! 371 | That was a lot, but we made it! 372 | 373 | ### Interlude: Bundling 374 | 375 | 다음으로 더 진행하기 전에 매우 간단한 최적화를 해보려고 합니다. 376 | 이제까지 구현에서는, 실행자가 하나의 요청을 받아서 하나의 트랜잭션을 전송하는 것이었습니다. 하지만 `entry point`는 하나의 지갑에만 377 | 국한된 것이 아닙니다. 다수의 사용자 요청들을 모아서 하나의 트랜잭션으로 처리하면 가스비를 줄일 수 있을 것 같습니다! 378 | 이러한 사용자 요청들을 번들링하는 것은 cold 스토리지에 접근하는데 드는 비용(같은 스토리지에 접근하는 것은 처음 접근할 때보다 비용이 낮아짐 )을 낮출 수 있을 뿐 아니라 379 | 트랜잭션 전송에 매번 21,000 가스가 소모되는 것을 줄일 수 있습니다. 380 | 381 | 이를 위해서는 약간의 수정이 필요합니다. 382 | 383 | 다음을 384 | 385 | ```solidity 386 | contract EntryPoint { 387 | function handleOp(UserOperation op); 388 | 389 | // ... 390 | } 391 | ``` 392 | 이렇게 변경합니다. 393 | 394 | ```solidity 395 | contract EntryPoint { 396 | function handleOps(UserOperation[] ops); 397 | 398 | // ... 399 | } 400 | ``` 401 | ![1-5.png](../img/1-5.png) 402 | 403 | 새로운 `handleOps` 메소드는 여러분이 이미 예상하는 것처럼 동작합니다. 404 | 405 | 각 요청에 대해서 `validateOp`를 호출합니다. 유효성에 실패하면 그 요청은 폐기합니다. 406 | 각 요청들을 그 요청에 연결된 지갑의 `executeOp`로 실행합니다. 가스 소모량을 추적하고 소모된 가스비만큼 실행자에게 이더를 보냅니다. 407 | 여기서 알아둘 것은 요청을 하나씩 검사하고 실행하는 것이 아니라 모든 요청에 대한 유효성을 전부 검사한 후에 전체 요청을 실행한다는 점입니다. 408 | 409 | 이렇게 시뮬레이션을 유지하는 것은 중요합니다. 410 | 411 | `handleOps`를 수행할 때, 다음 요청에 대한 유효성을 검사하기 전에 첫 번째 요청을 실행한다면 이 실행이 두 번째 요청의 유효성과 연관된 412 | 스토리지에 영향을 줄 수 있고 그것은 두 번째 요청의 유효성을 독립적으로 검사해서 문제가 없더라도 (실제 실행에서는) 실패할 수 있기 때문입니다. 413 | 414 | 비슷한 맥락으로, 번들링된 요청들 사이에서 하나의 검증이 후속 요청의 검증에 영향을 미치는 상황을 피하려는 것입니다. 415 | 416 | 번들 안에 한 지갑에 대한 다수의 요청이 포함되어 있지 않는 한, 이러한 방식에 얽매일 필요는 없습니다. 왜냐하면 위에서 말했던 스토리지 제한으로 417 | 두 개의 요청이 같은 스토리지에 동시에 접근하는 경우를 검사하기 때문에 서로 간섭할 수 없기 때문입니다. 418 | 이것을 잘 이용하면, 실행자는 번들 안에 해당 지갑에 대한 요청이 하나 뿐인지를 확인해 볼 수도 있을 것입니다. 419 | 420 | 실행자에게 좋은 일은 새로운 수익의 기회가 생긴다는 것입니다! 421 | 422 | 실행자는 사용자들이 보낸 요청을 번들링하면서 이익이 되는 방향으로 Maximal Extractable Value (MEV)을 가질 기회가 있습니다(자신들의 요청을 사이에 넣어서). 423 | 424 | 이제 번들링을 할 수 있으므로 "실행자"라 하는 대신 그들의 진짜 이름 "번들러"라고 부르겠습니다. 425 | 앞으로 이어질 글들에서 이들을 ERC-4337에서와 같이 번들러라고 하겠지만 EOA가 보낸 트랜잭션을 온체인에서 실행하는 역할을 하므로 426 | "실행자"라고 생각하는 것도 괜찮다는 생각이 듭니다. 427 | 428 | ### Bundlers as network participants 429 | 430 | 지갑 소유자는 자신들의 요청을 번들에 포함시켜주기를 기대하면서 번들러에게 보냅니다. 이것은 계정 소유자가 블록 생성자에게 트랜잭션을 전송하는 431 | 통상적인 상황과 유사합니다. 그래서 이런 네트워크 구조에서 얻을 수 있는 좋은 활용 방안을 찾을 수 있습니다. 432 | 433 | 노드가 일반적인 트랜잭션들을 멤풀에 저장하고 다른 노드들에게 전파하는 것처럼, 번들러는 유효한 사용자 요청들을 멤풀에 저장하고 다른 번들러에게 434 | 전파할 수 있습니다. 번들러들은 다른 번들러들과 공유하기 전에 사용자 요청을 검사하는데 이것은 전체 요청들의 유효성을 모두 검사하는 435 | 일을 경감시켜 줄 수 있습니다. 436 | 437 | 번들러는 블록 생성자가 되면 더 많은 이익을 볼 수 있습니다. 왜냐하면 자신들이 만든 번들을 블록에 포함시킬 수 있기 때문에, 시뮬레이션을 통과한 438 | 트랜잭션 실행이 실패할 가능성을 줄이거나 없앨 수 있기 때문입니다. 더구나 블록 생성자와 번들러는 MEV를 활용하는 유사한 방법으로 이득을 볼 수 있습니다. 439 | 440 | 시간이 지날수록 번들러와 블록 생성자는 하나가 될 것이라고 예상하고 있습니다. 441 | 442 | Whew! 443 | That was a lot. 444 | 445 | 지금까지 자산을 보관할 수 있는 스마트 컨트랙트 지갑을 어떻게 생성하고, 실행자들, 즉 번들러가 우리를 대신해서 446 | 어떻게 지갑 스마트 컨트랙트를 호출하며 연계되는지 알아보았습니다. 447 | 448 | ### Continue reading: 449 | 450 | 다음에 이어지는 글들에서는 대납 트랜잭션(sponsored transactions)과 지갑 생성 그리고 압축 서명(aggregating signatures)에 대해 451 | 알아보겠습니다! 452 | 453 | [다음](./2.md) -------------------------------------------------------------------------------- /docs/eip-4337.md: -------------------------------------------------------------------------------- 1 | *원문: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-4337.md* 2 | 3 | ERC-4337을 요약하고 정리합니다(원문과 주석 구분없이 작성). 이 표준은 현재(2023.03.10) draft 상태입니다. 4 | 5 | ### 개요 6 | 7 | 이 스펙은 합의 계층 프로토콜의 변경없이 구현하는 계정 추상화(account abstraction)에 관한 제안입니다. 8 | 하위 프로토콜 레벨에 새로운 기능이나 트랜잭션 타입을 추가하는 대신(하드포크 대신), 상위 계층에서 `UserOperation`이라는 9 | 트랜잭션과 유사한(pseudo-transaction) 객체를 도입합니다. 10 | 11 | 사용자가 `UserOperation` 객체를 분리된 맴풀(mempool)에 전송합니다. 번들러(bundler)라고 하는 역할을 수행하는 사람들이 12 | 이것을 모아서 `handleOps` 라는 하나의 트랜잭션으로 만들어 어떤 특별한 컨트랙트(a special contract)로 전송하고 13 | 그 트랜잭션이 처리되어 결과가 블록에 저장됩니다. 14 | 15 | 계정 추상화란 아주 단순하게 말하면 스마트 컨트랙트 지갑(SC wallet)을 의미하는 것으로 이해할 수 있습니다. 16 | 17 | "분리된 멤풀"의 의미는, 사용자 요청(`UserOperation`)은 이더리움 노드의 퍼블릭 멤풀에 직접 들어가는 것이 아니라 번들러를 거치게 되므로 번들러들의 멤풀을 18 | 가리키는 것입니다. 과거 작업증명 때에도 플래시봇 서비스를 이용하는 사용자의 트랜잭션은 퍼블릭 멤풀로 가지 않고 번들링 과정을 거쳐서 전송되었으므로 그것과 유사하다고 보면 될 것 같습니다. 지분증명으로 19 | 전환된 지금도 각 검증노드들은 mev-boost를 사용하여 외부의 블록 생성자들로부터 트랜잭션 처리를 아웃소싱하는 형태이므로 같은 맥락에 있습니다. 20 | 21 | 따라서 "분리된"의 의미는 이더리움 네트워크의 멤풀이 아닌 번들러 네트워크의 멤풀로 사용자 요청이 전송된다는 말입니다. 사용자 요청 멤풀은 중앙화된 22 | 특정 노드의 멤풀이 아니라 공개된 멤풀입니다. 현재 이더리움의 환경과 유사하다고 보면 되겠습니다(번들러 노드와 네트워크가 23 | 어떻게 구성될 것인지는 [다음 글](https://notes.ethereum.org/@yoav/unified-erc-4337-mempool)을 참조). 24 | 25 | 26 | ### 제안 동기 27 | 28 | * Achieve the key goal of account abstraction: 계정 추상화의 핵심 기능의 구현 29 | * Decentralization: 탈중앙화, 번들러는 누구나 될 수 있음 30 | * Do not require any Ethereum consensus changes: 이더리움 합의 프로토콜 레벨의 변경이 필요없음 31 | * Try to support other use cases: 여러 유즈케이스를 지원 32 | * Privacy-preserving applications 33 | * Atomic multi-operations 34 | * Sponsored transaction(대납 기능, ERC-20 토큰으로 수수료 납부 등) 35 | * Signature aggregation 36 | 37 | ### 상세 스펙 38 | 39 | - [UserOperation](../contracts/interfaces/UserOperation.sol) - 사용자 트랙잭션에 해당하는 객체. "트랜잭션"과 구별하기 위해 이렇게 명명함(역주: "사용자 요청"이라고 하겠음). 트랜잭션처럼 40 | "sender", "to", "calldata", "maxFeePerGas", "maxPriorityFee", "signature", "nonce" 등을 포함하고, 41 | 트랜잭션에는 없는 다른 항목들을 포함. "nonce"와 "signature"는 프로토콜에서 지정된 것이 아니라 계정(지갑) 구현에 따라 다를 수 있음. 42 | - Sender - 사용자 요청을 보내는 지갑 컨트랙트. 43 | - EntryPoint - 번들링된 사용자 요청들을 처리하는 싱글톤 컨트랙트. 번들러/클라이언트는 지원하는 entry point를 화이트 리스팅함. 44 | - Bundler - 다수의 사용자 요청을 모으는 노드(블록 생성자)로, `EntryPoint.handleOps` 트랜잭션을 생성. 하지만 모든 블록 생성자들이 번들러가 되어야 하는 것은 아님. 45 | - Aggregator - 계정(지갑)이 신뢰하는 헬퍼 컨트랙트로, 압축 서명의 유효성을 검증. 번들러/클라이언트는 지원하는 aggregator를 화이트 리스팅함. 46 | 47 | 알케미 블로그에서 이미 등장한 용어들이라 이해하는데 큰 어려움은 없을 것 같습니다. `UserOperation`이라는 것은 결국 48 | 사용자가 지갑 컨트랙트를 통해 수행하려는 작업들의 집합이라고 볼 수 있습니다. 49 | 50 | ERC-4337에서는 이더리움 프로토콜 변경을 하지않기 위해서, 새로운 계정 추상화용 트랜잭션 타입을 만들지 않았습니다. 대신에 `UserOperation`이라고 하는 51 | ABI-인코딩된 데이터를 지갑에 전달하는 사용자 요청을 도입했습니다. 사용자 요청의 항목들은 아래와 같습니다: 52 | 53 | | 항목 | 타입 | 설명 | 54 | |------------------------|-----------|-----------------------------------------------------| 55 | | `sender` | `address` | 사용자 요청을 보내는 지갑 계정 | 56 | | `nonce` | `uint256` | 리플레이를 막는 파라미터; 처음 지갑 생성을 위한 salt로 사용 | 57 | | `initCode` | `bytes` | 지갑의 initCode(아직 온체인에 없어서 경우 생성할 필요가 있다는 의미와 같음) | 58 | | `callData` | `bytes` | 메인 요청 실행 동안에 `sender`에게 전달되는 데이터 | 59 | | `callGasLimit` | `uint256` | 메인 요청(실제 실행) 실행에 할당된 가스량 | 60 | | `verificationGasLimit` | `uint256` | 확인 단계(유효성 검사)에 할당된 가스량 | 61 | | `preVerificationGas` | `uint256` | 콜데이터 및 사전 확인에 소요되는 가스 보정 값 | 62 | | `maxFeePerGas` | `uint256` | 가스당 최대(허용) 수수료, EIP-1559의 `max_fee_per_gas`에 해당 | 63 | | `maxPriorityFeePerGas` | `uint256` | 가스당 팁 수수료, EIP-1559의 `max_priority_fee_per_gas`에 해당 | 64 | | `paymasterAndData` | `bytes` | paymaster의 주소와 전달할 데이터(직접 납부하는 경우는 빈 값) | 65 | | `signature` | `bytes` | 유효성 검사 단계에서 지갑으로 전달될 nonce 포함 데이터 | 66 | 67 | 여기서 `sender`는 사용자 요청을 보내는 계정이 아니라 지갑 계정을 말합니다. 지금은 "EOA==지갑" 이므로 다소 어색할 수 있지만 68 | 앞으로는 서명과 지갑 기능을 분리하여 생각해야 합니다(ERC-4337 개발자 바이스에 의하면 EOA는 더이상 쓰지 않을 것이라고 합니다). 69 | 70 | 사용자는 사용자 요청을 전용 멤풀에 보냅니다. 번들러라고 부르는 참여자가(블록 생성자가 될 수도 있고, 블록 생성자에게 트랜잭션을 릴레이 해주는 사람들일 수도 있음, 71 | 예를 들어 플래시봇 같은 번들 마켓플레이스를 통해 릴레이해주는 사람들) 멤풀에서 트랜잭션들을 가져와서 번들링 합니다. 72 | 다수의 사용자 요청을 포함한 하나의 번들 트랜잭션은 사전에 배포된 전역 entry point 컨트랙트의 `handleOps`를 호출하게 됩니다. 73 | 74 | 번들러의 역할을 블록 생성자와 구분하기 보다는 동일한 주체가 같이 수행할 것으로 예상됩니다. 지금도 블록 생성자와 릴레이가 같은 경우가 많습니다. 75 | 76 | 리플레이 공격을 막기 위해 서명에 `chainId`와 [entry point](../contracts/interfaces/IEntryPoint.sol) 컨트랙트 주소가 포함됩니다. 77 | 78 | 스펙에 있는 [`IAccount`](../contracts/interfaces/IAccount.sol) 인터페이스는 지갑 컨트랙트의 인터페이스를 말합니다. 79 | 80 | ```solidity 81 | interface IAccount { 82 | function validateUserOp 83 | (UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) 84 | external returns (uint256 validationData); 85 | } 86 | ``` 87 | 지갑 컨트랙트는: 88 | 89 | - 자신을 호출하는 entry point가 검증된 컨트랙트 주소인지 확인해야 함. 90 | - `userOpHash`는 사용자 요청과 `chainId`, 그리고 entry point 주소를 해시한 것임. 91 | - 지갑이 서명 압축을 지원하지 않는 경우는, `userOpHash`의 서명이 유효한지를 반드시 확인해야 함. 만약 일치하지 않은 경우 92 | `SIG_VALIDATION_FAILED`로 정의된 값을(revert 하지 말고) 리턴해야 함(알케미 블로그에서 말한 "감시자 값"에 해당). 그 외에는 93 | revert 함. 94 | - 지갑은 모자란 가스비 `missingAccountFunds`를 반드시 호출자인 entry point에게 지급해야 함(이미 예치된 금액이 충분한 경우 이 값은 0이 될 수 있음). 95 | - 지갑은 가스비를 초과 지급할 수도 있음(남는 것은 `withdrawTo`를 호출하여 언제든지 인출 가능). 96 | - `aggregator`는 압축 서명을 사용하지 않는 지갑을 무시해야 함. 97 | - 리턴 값에는 다음 세 개의 정보가 들어있음. 98 | - `authorizer` - 서명 검증 실패하면 1을 리턴. 그렇지 않으면 authorizer(aggregator) 컨트랙트의 주소를 리턴 99 | - `validUntil` - 6 바이트 타임스탬프, `userOp`은 이 시간까지 유효함, 0인경우 무한대. 100 | - `validAfter` - 6 바이트 타임스탬프, `userOp`은 이 시간이후부터 유효함. 101 | 102 | 지갑이 압축 서명을 지원하는 경우는 `authorizer`를 통해 `aggregator`의 주소를 리턴해야 합니다. 103 | 104 | 다음으로 `IAggregator`는 아래와 같습니다: 105 | 106 | ```solidity 107 | interface IAggregator { 108 | function validateUserOpSignature(UserOperation calldata userOp) external view returns (bytes memory sigForUserOp); 109 | function aggregateSignatures(UserOperation[] calldata userOps) external view returns (bytes memory aggregatesSignature); 110 | function validateSignatures(UserOperation[] calldata userOps, bytes calldata signature) view external; 111 | } 112 | ``` 113 | - 지갑이 `aggregator`를 사용하는 경우, `EntryPoint.simulateValidation`에서 `aggregator` 주소를 받음. 114 | 압축 서명인 경우는 `ValidationResult` 대신 `ValidationResultWithAggregator`을 예외로 발생시켜 revert 함(시뮬레이션이므로). 115 | - 사용자 요청을 받을 때 번들러는 `validateUserOpSignature`을 호출(각 요청에 대한 서명 확인). 116 | - `aggregateSignatures`는 반드시 하나의 압축 서명 값을 생성해야 함. 117 | - `validateSignatures`는 반드시 (압축 서명을 구성하는 모든) 사용자 요청들에 대해 확인해야 함. 실패하면 revert. 이 메소드는 `EntryPoint.handleOps`가 호출함. 118 | - 번들러는 압축 서명을 만들고 검사하는 다른 네이티브 라이브러리를 `aggregator`를 대신에 사용할 수 있음. 119 | 120 | 번들러는 지갑이 지정한 `aggregator`를 사용할 때 그의 스테이킹 상태와 차단 여부를 확인해야 합니다. 만약 너무 많은 리소스를 121 | 사용하거나 서명을 압축하는데 실패하는 경우에는 해당 `aggregator`를 차단할 수 있습니다. 122 | 123 | **Required entry point contract functionality** 124 | 125 | [entry point](../contracts/interfaces/IEntryPoint.sol)에는 `handleOps`와 `handleAggregatedOps` 두 개의 메소드가 있습니다. 126 | 127 | `handleAggregatedOps`는 다수의 사용자 요청에 대해 다수의 `aggregator` 를 batch 처리하는 메소드입니다. 당연히 각 사용자 요청에 대해 `aggregator`를 넘겨 주어야 하므로 다음과 같은 128 | 구조체 타입 `UserOpsPerAggregator`의 배열을 파라미터로 받습니다. 129 | ```solidity 130 | struct UserOpsPerAggregator { 131 | UserOperation[] userOps; 132 | 133 | // aggregator address 134 | IAggregator aggregator; 135 | // aggregated signature 136 | bytes signature; 137 | } 138 | 139 | function handleAggregatedOps( 140 | UserOpsPerAggregator[] calldata opsPerAggregator, 141 | address payable beneficiary 142 | ) external; 143 | ``` 144 | `handleOps`는 각 사용자 요청에 대하여 다음과 같은 검증 작업을 수행해야 합니다: 145 | 146 | - 계정(지갑)이 존재하지 않는 경우 사용자 요청 항목에 있는 `initcode`를 사용하여 계정을 생성. 생성에 실패하는 경우는 중단. 147 | - 계정(지갑)의 `validateUserOp`을 호출. 에러가 발생하는 경우 해당 요청만을 건너뛰거나 전체를 revert. 148 | - 계정(지갑)이 entry point에 예치한 금액 확인(최대 사용 가스 기준). 149 | 150 | 위의 유효성 검사를 통과하면 실행 단계로 들어갑니다: 151 | 152 | - 사용자 요청에 있는 콜데이터를 계정(지갑)으로 전송(즉 지갑의 어떤 함수를 호출). 153 | 154 | 번들러가 사용자 요청을 받아들이기 전에 RPC 메소드를 통해서 `EntryPoint.simulateValidation`를 호출하여 서명과 가스비 지불 가능 여부를 155 | 검사합니다. `simulateValidation`은 결과를 커스텀 예외를 발생시켜서 리턴합니다. 그 외의 revert는 실패로 간주하여 멤풀에 넣지 않고 폐기 합니다. 156 | 예외는 `aggregator`를 사용하는 경우 스테이킹 정보를 넘겨야 하므로 `ValidationResultWithAggregation`을 사용합니다. 157 | 158 | ```solidity 159 | error ValidationResult(ReturnInfo returnInfo, 160 | StakeInfo senderInfo, StakeInfo factoryInfo, StakeInfo paymasterInfo); 161 | 162 | error ValidationResultWithAggregation(ReturnInfo returnInfo, 163 | StakeInfo senderInfo, StakeInfo factoryInfo, StakeInfo paymasterInfo, 164 | AggregatorStakeInfo aggregatorInfo); 165 | ``` 166 | 167 | **Extension: paymasters** 168 | 169 | entry point를 확장해서 paymaster를 지원할 수 있습니다. paymaster는 스폰서 트랜잭션, 이를테면 가스비 대납과 같이 기존 170 | 가스비 지불 흐름을 커스터마이징하는데 이용할 수 있는 방안입니다. 애플리케이션에서 특정 사용자들에게 가스비 할인 혜택을 준다든가 171 | 이더로 가스비를 내는 대신 다른 ERC-20 토큰으로 낸다든가 하는 유즈 케이스들이 있을 수 있습니다. paymaster가 지정되면 다음과 같은 흐름이 됩니다: 172 | 173 | entry point가 유효성을 검사하는 단계에서 `handleOps`는 먼저 paymaster가 요청에 대한 가스비 지불을 할 수 있는 충분한 자금이 174 | 예치되어 있는지 확인한 후 paymaster 컨트랙트의 `validatePaymasterUserOp`을 호출합니다. 이 함수에서 해당 요청이 175 | paymaster가 지불할 대상이 되는지 판단하게 됩니다. 가스비는 paymaster가 지불하므로 지갑의 `validateUserOp`이 호출될 때는 176 | `missingAccountFunds`를 0으로 전달합니다. 177 | 178 | `validatePaymasterUserOp`가 어떤 "컨텍스트" 데이터를 리턴하는 경우, 사용자 요청 처리 후에 entry point는 `postOp`을 호출해야 합니다. 179 | `postOp`는 메인 실행(즉 사용자 요청 처리)이 revert 되어도 호출을 보장해야 합니다. 이것은 `try-catch`의 `catch`절에서 한번 더 `postOp`이 180 | 실행된다는 것을 의미합니다. 알케미 블로그에서 설명한 것처럼, 메인 실행이 revert 되어도 그동안 소모된 가스비에 대한 청구가 181 | 가능해야 하기 때문입니다. 182 | 183 | 나쁜 paymaster들을 막기 위해 평판 시스템(reputation system)을 도입하고 스테이킹을 하도록 합니다. 스테이킹된 이더는 일정 시간을 기다려야 인출이 가능합니다. 184 | 185 | **Client behavior upon receiving a UserOperation** 186 | 187 | 여기서 클라이언트와 번들러를 구분했는데, 클라이언트는 사용자 요청을 제일 처음 받는 사람을 말하고 번들러는 그것을 번들링하는 사람을 가리키는 것으로, 동일한 주체가 수행할 것으로 188 | 예상됩니다. 클라이언트는 다음과 같은 기본 검사(sanity check)를 수행해야 합니다: 189 | 190 | - 지갑 컨트랙트인 `sender`가 존재하는지 아니면 배포를 위한 `initCode`가 있는지 확인(동시에 둘 다 있으면 안됨). 191 | - `initCode`의 첫 20 바이트는 팩토리 컨트랙트의 주소이고 다른 스토리지를 참조하는 경우 스테이킹이 되어 있는지 확인. 192 | - `verificationGasLimit`이 `MAX_VERIFICATION_GAS`보다 작은지 확인. 그리고 `preVerificationGas`이 충분한지 확인(콜데이터를 serializing 하는 가스+`PRE_VERIFICATION_OVERHEAD_GAS`). 193 | - `paymasterAndData`는 paymaster의 주소로 시작하는데, 온체인에 당연히 배포가 되어 있어야 하고, 가스비 예치가 되어 있어야 하며, 차단되지 않은 상태. 194 | 또 스테이킹이 필요할 수도 있음. 195 | - `callGasLimit`는 실제 지갑 컨트랙트로 전달된 콜데이터(함수 호출)를 실행할 때 드는 가스를 지정하므로 최소한 `CALL` opcode 비용보다 큰 값. 196 | - `maxFeePerGas`와 `maxPriorityFeePerGas`는 현재 `block.basefee`보다 큰 값. 197 | - 멤풀에는 같은 사용자 요청이 하나 이상 존재할 수 없고(`maxPriorityFeePerGas`가 더 높으면 기존 요청을 대체 가능) 배치 내에서도 `sender`당 하나만 가능. 198 | 199 | `verificationGasLimit`은 사용자 요청의 유효성을 검사할 때 사용할 가스를 말합니다. `MAX_VERIFICATION_GAS`는 번들러가 설정하는 가스의 상한으로 추측됩니다. `preVerificationGas`는 사용자 요청의 "오버헤드" 비용에 해당하는데, EVM이 연산을 200 | 수행할 때 추가적으로 소요될 지도 모르는 가스를 고려한 것이라고 합니다. 새로 추가될 RPC 호출인 `eth_estimateUserOperationGas`을 사용하면 기준 값을 얻을 수 있을 것 같습니다. 201 | 202 | 위와 같은 기본 검사 후에는 시뮬레이션을 수행합니다. 시뮬레이션은 로컬에서 RPC 호출로 `EntryPoint.simulateValidation`을 실행하는 것을 말합니다. 이것을 통과하면 비로소 203 | 사용자 요청을 멤풀에 넣게 됩니다. 204 | 205 | **Simulation** 206 | 207 | 시뮬레이션은 사용자 요청을 멤풀에 넣기 전에 유효한 요청인지, 가스비를 낼 수 있는지, 또 실제 메인 실행에서도 문제가 없는지 208 | 검사하고 시험삼아 실행보는 것을 말합니다. 이러한 이유로 사용자의 요청은 시뮬레이션과 메인 실행 사이에 변경될 수 있는 값을 참조해서는 안됩니다. 209 | 210 | 지갑과 연계되는 세 개의 컨트랙트, 즉 factory, paymaster, aggregator들도 마찬가지 규칙을 적용받기 때문에 스토리지 접근에 제한을 211 | 받을 수 있습니다. 212 | 213 | 시뮬레이션은 다음과 같이 수행됩니다: 214 | 215 | 번들러는 `EntryPoint.simulateValidation(userOp)`를 RPC 호출합니다. 이 메소드는 시뮬레이션이 정상적으로 실행되면 216 | `ValidationResult`라는 사용자 예외를 던집니다(오류를 의미하는 것이 아님). 만약 다른 에러가 발생하는 경우 사용자 요청은 거부됩니다. 217 | 218 | 1. `initCode`가 있으면 계정(지갑)을 생성합니다. 219 | 2. 지갑의 `validateUserOp`을 호출합니다. 220 | 3. paymaster가 지정되어 있으면 `paymaster.validatePaymasterUserOp`를 호출합니다. 221 | 222 | `validateUserOp`이나 `validatePaymasterUserOp`는 사용자 요청의 수명에 해당하는 `validAfter`와 `validUntil` 타임스탬프를 리턴할 수 있습니다. 223 | 주어진 시간 내에 만료될 것 같은 요청은 멤풀에서 제거할 수 있습니다(즉 다음 블록에 포함되기 전에 만료될 요청). 224 | 225 | 시뮬레이션에서 확인할 것들은: 226 | 227 | 1. 금지된 opcode들을 쓰지 말 것. 228 | 2. `GAS` opcode를 쓰지 말것(단 `CALL`, `DELEGATECALL`, `CALLCODE`, `STATICCALL`을 바로 다음에 쓰는 경우는 허용). 229 | 3. 제한된 스토리지에 접근하지 말 것. 230 | 1. 자신의 스토리지는 허용(factory, paymaster 자신들의 스토리지 허용). 231 | 2. 계정(지갑)의 스토리지. 232 | 3. 같은 번들에 속한 다른 계정(지갑)의 스토리지는 접근 불가(factory, paymaster의 스토리지에도 접근 불가). 233 | 4. "호출" 관련 opcode(`CALL`, `DELEGATECALL`, `CALLCODE`, `STATICCALL`) 제약 사항 234 | 1. `value`를 사용할 수 없음(지갑에서 entry point로 전송되는 경우 제외). 235 | 2. out-of-gas 로 revert 되지 말아야 함. 236 | 3. 목적지 주소는 코드를 가지고 있어야 함(`EXTCODESIZE`>0). 237 | 4. `depositFor`를 제외한 entry point의 메소드를 호출하면 안됨(recursion 방지). 238 | 5. 연계된 모든 주소의 `EXTCODEHASH`는 시뮬레이션 중에 변경되지 않아야 함. 239 | 6. 코드가 없는 주소에 `EXTCODEHASH`, `EXTCODELENGTH`, `EXTCODECOPY` 사용 불가 240 | 7. 사용자 요청의 `initCode` 크기가 0이 아닐 경우에만(op.initcode.length != 0), `CREATE2`가 허용됨. 그 외에는 `CREATE2` 사용 불가 241 | 242 | **Storage associated with an address** 243 | 244 | "주소와 연계된(associated) 스토리지"라는 것은 (솔리디티를 기준으로) 해당 컨트랙트 자신의 스토리지와, 그 컨트랙트 주소를 `mapping`의 키로 하는 245 | 다른 컨트랙트의 스토리지를 의미합니다. 246 | 247 | (계정)주소 A와 연계된 것들은: 248 | 249 | 1. 컨트랙트 A 자신의 슬롯들. 250 | 2. 다른 컨트랙트의 A번 슬롯(A 주소와 동일한 슬롯?). 251 | 3. `mapping(address => value)`에서 컨트랙트 A를 키로 하는 값이 저장된 슬롯(슬롯 번호는 keccak256(A || X) + n 으로 결정, n은 252 | `mapping(address => struct)`인 경우를 위한 옵셋). 253 | 254 | **Bundling** 255 | 256 | 번들링 과정(배치를 만드는 작업)은 다음과 같습니다: 257 | 258 | - 동일한 배치 내에서 다른 사용자 요청의 지갑에 접근하는 요청 제거. 259 | - 동일한 배치 내에서 다른 사용자 요청 유효성 검사 과정에서 나오는 주소에 접근하는 요청 제거(팩토리로 생성되는 다른 지갑 접근 금지). 260 | - 배치에서 사용되는 각 paymaster에 대해 잔액을 추적. paymaster가 그 요청들의 가스비를 모두 낼 수 있는지 확인. 261 | - aggrerator별 사용자 요청을 만들기 위해 aggregator로 사용자 요청을 정렬. 262 | - 각 aggregator에 대해, 압축 서명을 생성하기 위한 코드 실행. 263 | - 번들링된 요청들을 모두 실행할 수 있는 최대 가스를 추정하고 그 값으로 트랜잭션을 전송해야 함. 264 | - 시뮬레이션 중 에러가 발생하면 `EntryPoint.handleOps`에서 던지는 `FailedOp` 예외를 확인할 것. 예외를 발생시킨 사용자 요청을 폐기. 265 | 266 | 사용자 요청의 유효성 검사는 두 번을 합니다. 클라이언트가 시뮬레이션에서 한 번, 그리고 위의 번들링 과정 중에 번들러가 한 번 더 수행합니다. 번들러는 267 | `accessList`를 사용하여 가스비를 줄일 필요가 있습니다(이미 처음 시뮬레이션에서 한 번 접근했던 스토리지이므로). 268 | 269 | **Forbidden opcodes** 270 | 271 | `GASPRICE`, `GASLIMIT`, `DIFFICULTY`, `TIMESTAMP`, `BASEFEE`, `BLOCKHASH`, `NUMBER`, `SELFBALANCE`, 272 | `BALANCE`, `ORIGIN`, `GAS`, `CREATE`, `COINBASE`, `SELFDESTRUCT` 273 | 274 | ![flow1](../img/flow1.png) 275 | 276 | ![flow2](../img/flow2.png) 277 | 278 | ### 평판 시스템과 전역 주체들에 대한 제재 279 | 280 | 사용자 요청들은 서로 접근 제한을 두어 간섭하지 않도록 하는 것이 당연하지만 글로벌하게 동작하는 paymaster, factory, aggregator 등의 경우는 281 | 다수의 요청들이 공유하는 형태이므로 유효성 검사 단계에서 영향을 줄 수 있습니다. 282 | 283 | 이것을 방지하기 위해 멤풀의 다수 사용자 요청들이 유효성 검사에서 실패하게 되면 이를 유발한 주체를 제재합니다. 284 | "sybil-attack"을 막기 위해 스테이킹 시스템을 도입하여 DoS 공격에 드는 비용을 높이게 됩니다. 그러나 슬래싱은 없고 언제나 인출이 285 | 가능합니다(일정한 시간이 지난 후). 286 | 287 | 스테이킹을 하지 않아도 되는 경우: 288 | 289 | - 그 어떤 스토리지도 참조하지 않거나 `sender` 계정의 스토리지만 참조할 때. 290 | - 사용자 요청이 새로운 계정을 생성하지 않으면(`initCode`의 값이 없음) `sender` 계정과 연계된 스토리지 접근 가능. 291 | - `postOp()` 메소드를 가진 paymaster는 스테이킹을 해야 함. 292 | 293 | **평판 시스템의 스펙** 294 | _생략_ 😂 295 | 296 | ### 설계 사상(Rationale) 297 | 298 | 계정 추상화를 위한 스마트 지갑에서 중요하게 고려해야 할 문제는 DoS에 대한 대비입니다: 어떻게 하면 블록 생성자들이 299 | 전체 사용자 요청을 실행하지 않고 실제로 가스비가 지불될 것이라고 확신할 수 있을까요? 사용자 요청을 한번 전부 실행해볼 수 밖에 300 | 없는 상황은 DoS의 공격에 취약할 수 있는 부분입니다. 공격자는 손쉽게 수많은 요청을 전송할 수 있고 마지막에 가서 revert 해버릴 수 있기 301 | 때문입니다. 마찬가지로 멤풀이 막히는 것을 막기 위해 P2P 네트워크의 노드들은 그것을 처리하기 전에 요청들이 가스비를 낼 수 있는지 302 | 확인할 필요가 있습니다. 303 | 304 | 이 제안에서는 계정이 `validateUserOp`이라는 메소드를 가지도록 했습니다. 이 메소드는 사용자 요청 `UserOperation`을 받아서 서명을 확인하고 가스비 납부 305 | 가능 여부를 검사합니다. 이것은 "pure" 함수에 가까워야 합니다: 계정 자신의 스토리지에만 접근해야 하고 환경 opcode(`TIMESTAMP` 같은 것들)를 306 | 사용할 수 없습니다. 계정의 스토리지만을 수정할 수 있고 이더를 보낼 수만 있습니다(entry point에 가스비 지불). 이 메소드는 `verificationGasLimit`으로 307 | 가스가 제한되고 노드는 `verificationGasLimit`가 너무 높은 요청들의 수용 또는 거부 여부를 선택할 수 있습니다. 308 | 이러한 제한은 블록 생성자들과 네트워크가 로컬에서 요청들을 시뮬레이션 단계에서 수행하므로써, 실제 블록에서 실행되었을때 그 결과가 일치할 것이라는 것을 309 | 확신할 수 있습니다. 310 | 311 | entry point 기반의 접근 방식은 검증과 실행을 서로 분리할 수 있으며, 계정 로직을 간단하게 만들 수 있습니다. 312 | 계정이 어떤 템플릿을 준수하도록 하여 먼저 자체 호출하여 검증한 후에 다시 자체 호출로 실행되도록 하는 것이(실행이 샌드박스 처리되어 수수료 지불이 revert 되지 않도록 함) 313 | 다른 방안으로 제시되었습니다. 하지만 템플릿 기반 접근 방식은 기존 코드 컴파일 및 검증 도구가 템플릿 검증을 기반으로 설계되지 않아 구현하기 314 | 어렵다는 이유로 채택하지 않았습니다. 315 | 316 | #### Paymasters 317 | 318 | paymaster는 트랜잭션 "후원(sponsorship)"을 통해 다른 방식의 가스비 지불 메커니즘을 적용할 수 있도록 합니다. 319 | 이것은 paymaster가 자신들만의 로직으로 사용자 요청을 가공할 수 있도록 하지만, 설계상 중요한 제약 사항들이 있습니다: 320 | 321 | - "수동적인" 파라미터를 받을 수 없음(예를 들어 온체인 DEX로부터 ERC-20 토큰의 교환 비율). 322 | - 사용자가 가스비를 낼 것처럼 요청을 보내고 나중에 블록에서 실행될 시점에 변경할 수 있기 때문에 공격에 노출될 위험이 있음. 323 | 324 | paymaster 구조는 사용자를 대신에서 조건에 따라 가스비를 대납할 수 있는 컨트랙트를 구현할 수 있도록 합니다. 325 | 사용자가 ERC-20 토큰을 납부하는 경우에만 paymaster가 가스비를 부담하도록 만들 수도 있습니다. 그러니까 paymaster 컨트랙트가 326 | `validatePaymasterUserOp`에서 충분한 수량의 ERC-20 토큰이 승인(approve)되었는지 확인할 수 있고 `postOp`에서 `transferFrom`을 실행하여 327 | 토큰을 인출할 수 있게 되는 것입니다. 만약 사용자 요청이 토큰을 인출하거나 승인을 취소해서 revert가 된다고 해도 다시 밖에 있는 `postOp`가 328 | 실행되므로 (paymaster는) 토큰을 인출할 수 있습니다. 329 | 330 | #### First-time account creation 331 | 이 제안의 중요한 설계 목표 중 하나는 기존 EOA의 주요 기능을 그대로 구현하는 것이었습니다. 사용자가 어떤 절차를 거치지 않아도, 332 | 기존 사용자들이 대신 지갑을 생성해주지 않아도, 그냥 로컬에서 키페어를 생성하기만 하면 즉시 자산을 받을 수 있도록 하는 것입니다. 333 | 334 | 지갑 생성은 "factory"에 의해 이루어집니다. factory는 지갑 생성을 위해 `CREATE2`(`CREATE`가 아니라)를 사용할 것으로 예상합니다. 그래서 지갑 생성 순서는 335 | 생성된 주소의 간섭을 받지 않습니다(`CREATE`는 실행하는 계정의 nonce를 사용하여 주소를 만들게 되므로). 336 | 337 | 지갑 생성 코드인 `initCode` 항목의 첫 20 바이트는 주소이고 그 다음에 콜데이터가 이어집니다. 이 콜데이터는 지갑 컨트랙트를 생성하는 코드이고 생성된 주소를 338 | 리턴합니다. factory가 `CREATE2`를 사용하거나 다른 결정론적인 방법을 실행하면, 이미 지갑이 있더라도 지갑 주소를 리턴합니다. 339 | 이렇게 하면 클라이언트가 시뮬레이션에서 `EntryPoint.getSenderAddress`를 호출하여 그 지갑이 이미 배포되었는지 340 | 여부에 상관없이 지갑 주소를 조회할 수 있습니다. 341 | 342 | `initCode`가 있음에도 `sender`의 주소가 이미 존재하는 컨트랙트를 가리키고 있거나 또는 `initCode` 호출 후에 `sender` 주소가 존재하지 않으면 요청은 취소됩니다. 343 | entry point에서 `initCode`를 직접 실행해서는 안됩니다. factory에 의해 생성된 컨트랙트는 `validateUserOp`을 통해서 사용자 요청의 서명을 확인해야 합니다. 344 | 보안적인 이유로 생성된 컨트랙트의 주소가 서명에 의존성을 갖도록 하는 것이 중요합니다. 이렇게 해서 누군가 그 주소로 지갑을 배포하려고 해도 지갑을 345 | 컨트롤할 수 없습니다. factory가 글로벌 스토리지에 접근하는 경우 스테이킹을 해야 합니다. 346 | 347 | #### Entry point upgrading 348 | 계정(지갑)은 가스 효율성과 업그레이드 때문에 `DELEGATECALL`을 이용하는 것이 좋습니다. 계정 코드는 entry point 주소를 하드코딩해서 가스 효율을 높이게 됩니다. 349 | 기능 추가나 버그 패치 등으로 새로운 entry point가 도입되는 경우 새로운 entry point를 가리키는 새로운 지갑을 배포하여 기존 지갑을 대체할 수 있습니다. 350 | 351 | #### RPC methods (eth namespace) 352 | _생략_ 😂 -------------------------------------------------------------------------------- /contracts/core/EntryPoint.sol: -------------------------------------------------------------------------------- 1 | /** 2 | ** Account-Abstraction (EIP-4337) singleton EntryPoint implementation. 3 | ** Only one instance required on each chain. 4 | **/ 5 | // SPDX-License-Identifier: GPL-3.0 6 | pragma solidity =0.8.17; 7 | 8 | /* solhint-disable avoid-low-level-calls */ 9 | /* solhint-disable no-inline-assembly */ 10 | 11 | import "../interfaces/IAccount.sol"; 12 | import "../interfaces/IPaymaster.sol"; 13 | import "../interfaces/IEntryPoint.sol"; 14 | 15 | import "../utils/Exec.sol"; 16 | import "./StakeManager.sol"; 17 | import "./SenderCreator.sol"; 18 | import "./Helpers.sol"; 19 | 20 | 21 | contract EntryPoint is IEntryPoint, StakeManager { 22 | 23 | using UserOperationLib for UserOperation; 24 | 25 | SenderCreator private immutable senderCreator = new SenderCreator(); 26 | 27 | // internal value used during simulation: need to query aggregator. 28 | address private constant SIMULATE_FIND_AGGREGATOR = address(1); 29 | 30 | // marker for inner call revert on out of gas 31 | bytes32 private constant INNER_OUT_OF_GAS = hex'deaddead'; 32 | 33 | uint256 private constant REVERT_REASON_MAX_LEN = 2048; 34 | 35 | /** 36 | * for simulation purposes, validateUserOp (and validatePaymasterUserOp) must return this value 37 | * in case of signature failure, instead of revert. 38 | */ 39 | uint256 public constant SIG_VALIDATION_FAILED = 1; 40 | 41 | /** 42 | * compensate the caller's beneficiary address with the collected fees of all UserOperations. 43 | * @param beneficiary the address to receive the fees 44 | * @param amount amount to transfer. 45 | */ 46 | function _compensate(address payable beneficiary, uint256 amount) internal { 47 | require(beneficiary != address(0), "AA90 invalid beneficiary"); 48 | (bool success,) = beneficiary.call{value : amount}(""); 49 | require(success, "AA91 failed send to beneficiary"); 50 | } 51 | 52 | /** 53 | * execute a user op 54 | * @param opIndex index into the opInfo array 55 | * @param userOp the userOp to execute 56 | * @param opInfo the opInfo filled by validatePrepayment for this userOp. 57 | * @return collected the total amount this userOp paid. 58 | */ 59 | function _executeUserOp(uint256 opIndex, UserOperation calldata userOp, UserOpInfo memory opInfo) private returns (uint256 collected) { 60 | uint256 preGas = gasleft(); 61 | bytes memory context = getMemoryBytesFromOffset(opInfo.contextOffset); 62 | 63 | try this.innerHandleOp(userOp.callData, opInfo, context) returns (uint256 _actualGasCost) { 64 | collected = _actualGasCost; 65 | } catch { 66 | bytes32 innerRevertCode; 67 | assembly { 68 | returndatacopy(0, 0, 32) 69 | innerRevertCode := mload(0) 70 | } 71 | // handleOps was called with gas limit too low. abort entire bundle. 72 | if (innerRevertCode == INNER_OUT_OF_GAS) { 73 | //report paymaster, since if it is not deliberately caused by the bundler, 74 | // it must be a revert caused by paymaster. 75 | revert FailedOp(opIndex, "AA95 out of gas"); 76 | } 77 | 78 | uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; 79 | collected = _handlePostOp(opIndex, IPaymaster.PostOpMode.postOpReverted, opInfo, context, actualGas); 80 | } 81 | } 82 | 83 | /** 84 | * Execute a batch of UserOperations. 85 | * no signature aggregator is used. 86 | * if any account requires an aggregator (that is, it returned an aggregator when 87 | * performing simulateValidation), then handleAggregatedOps() must be used instead. 88 | * @param ops the operations to execute 89 | * @param beneficiary the address to receive the fees 90 | */ 91 | function handleOps(UserOperation[] calldata ops, address payable beneficiary) public { 92 | 93 | uint256 opslen = ops.length; 94 | UserOpInfo[] memory opInfos = new UserOpInfo[](opslen); 95 | 96 | unchecked { 97 | for (uint256 i = 0; i < opslen; i++) { 98 | UserOpInfo memory opInfo = opInfos[i]; 99 | (uint256 validationData, uint256 pmValidationData) = _validatePrepayment(i, ops[i], opInfo); 100 | _validateAccountAndPaymasterValidationData(i, validationData, pmValidationData, address(0)); 101 | } 102 | 103 | uint256 collected = 0; 104 | 105 | for (uint256 i = 0; i < opslen; i++) { 106 | collected += _executeUserOp(i, ops[i], opInfos[i]); 107 | } 108 | 109 | _compensate(beneficiary, collected); 110 | } //unchecked 111 | } 112 | 113 | /** 114 | * Execute a batch of UserOperation with Aggregators 115 | * @param opsPerAggregator the operations to execute, grouped by aggregator (or address(0) for no-aggregator accounts) 116 | * @param beneficiary the address to receive the fees 117 | */ 118 | function handleAggregatedOps( 119 | UserOpsPerAggregator[] calldata opsPerAggregator, 120 | address payable beneficiary 121 | ) public { 122 | 123 | uint256 opasLen = opsPerAggregator.length; 124 | uint256 totalOps = 0; 125 | for (uint256 i = 0; i < opasLen; i++) { 126 | UserOpsPerAggregator calldata opa = opsPerAggregator[i]; 127 | UserOperation[] calldata ops = opa.userOps; 128 | IAggregator aggregator = opa.aggregator; 129 | 130 | //address(1) is special marker of "signature error" 131 | require(address(aggregator) != address(1), "AA96 invalid aggregator"); 132 | 133 | if (address(aggregator) != address(0)) { 134 | // solhint-disable-next-line no-empty-blocks 135 | try aggregator.validateSignatures(ops, opa.signature) {} 136 | catch { 137 | revert SignatureValidationFailed(address(aggregator)); 138 | } 139 | } 140 | 141 | totalOps += ops.length; 142 | } 143 | 144 | UserOpInfo[] memory opInfos = new UserOpInfo[](totalOps); 145 | 146 | uint256 opIndex = 0; 147 | for (uint256 a = 0; a < opasLen; a++) { 148 | UserOpsPerAggregator calldata opa = opsPerAggregator[a]; 149 | UserOperation[] calldata ops = opa.userOps; 150 | IAggregator aggregator = opa.aggregator; 151 | 152 | uint256 opslen = ops.length; 153 | for (uint256 i = 0; i < opslen; i++) { 154 | UserOpInfo memory opInfo = opInfos[opIndex]; 155 | (uint256 validationData, uint256 paymasterValidationData) = _validatePrepayment(opIndex, ops[i], opInfo); 156 | _validateAccountAndPaymasterValidationData(i, validationData, paymasterValidationData, address(aggregator)); 157 | opIndex++; 158 | } 159 | } 160 | 161 | uint256 collected = 0; 162 | opIndex = 0; 163 | for (uint256 a = 0; a < opasLen; a++) { 164 | UserOpsPerAggregator calldata opa = opsPerAggregator[a]; 165 | emit SignatureAggregatorChanged(address(opa.aggregator)); 166 | UserOperation[] calldata ops = opa.userOps; 167 | uint256 opslen = ops.length; 168 | 169 | for (uint256 i = 0; i < opslen; i++) { 170 | collected += _executeUserOp(opIndex, ops[i], opInfos[opIndex]); 171 | opIndex++; 172 | } 173 | } 174 | emit SignatureAggregatorChanged(address(0)); 175 | 176 | _compensate(beneficiary, collected); 177 | } 178 | 179 | /// @inheritdoc IEntryPoint 180 | function simulateHandleOp(UserOperation calldata op, address target, bytes calldata targetCallData) external override { 181 | 182 | UserOpInfo memory opInfo; 183 | _simulationOnlyValidations(op); 184 | (uint256 validationData, uint256 paymasterValidationData) = _validatePrepayment(0, op, opInfo); 185 | ValidationData memory data = _intersectTimeRange(validationData, paymasterValidationData); 186 | 187 | numberMarker(); 188 | uint256 paid = _executeUserOp(0, op, opInfo); 189 | numberMarker(); 190 | bool targetSuccess; 191 | bytes memory targetResult; 192 | if (target != address(0)) { 193 | (targetSuccess, targetResult) = target.call(targetCallData); 194 | } 195 | revert ExecutionResult(opInfo.preOpGas, paid, data.validAfter, data.validUntil, targetSuccess, targetResult); 196 | } 197 | 198 | 199 | // A memory copy of UserOp static fields only. 200 | // Excluding: callData, initCode and signature. Replacing paymasterAndData with paymaster. 201 | struct MemoryUserOp { 202 | address sender; 203 | uint256 nonce; 204 | uint256 callGasLimit; 205 | uint256 verificationGasLimit; 206 | uint256 preVerificationGas; 207 | address paymaster; 208 | uint256 maxFeePerGas; 209 | uint256 maxPriorityFeePerGas; 210 | } 211 | 212 | struct UserOpInfo { 213 | MemoryUserOp mUserOp; 214 | bytes32 userOpHash; 215 | uint256 prefund; 216 | uint256 contextOffset; 217 | uint256 preOpGas; 218 | } 219 | 220 | /** 221 | * inner function to handle a UserOperation. 222 | * Must be declared "external" to open a call context, but it can only be called by handleOps. 223 | */ 224 | function innerHandleOp(bytes memory callData, UserOpInfo memory opInfo, bytes calldata context) external returns (uint256 actualGasCost) { 225 | uint256 preGas = gasleft(); 226 | require(msg.sender == address(this), "AA92 internal call only"); 227 | MemoryUserOp memory mUserOp = opInfo.mUserOp; 228 | 229 | uint callGasLimit = mUserOp.callGasLimit; 230 | unchecked { 231 | // handleOps was called with gas limit too low. abort entire bundle. 232 | if (gasleft() < callGasLimit + mUserOp.verificationGasLimit + 5000) { 233 | assembly { 234 | mstore(0, INNER_OUT_OF_GAS) 235 | revert(0, 32) 236 | } 237 | } 238 | } 239 | 240 | IPaymaster.PostOpMode mode = IPaymaster.PostOpMode.opSucceeded; 241 | if (callData.length > 0) { 242 | bool success = Exec.call(mUserOp.sender, 0, callData, callGasLimit); 243 | if (!success) { 244 | bytes memory result = Exec.getReturnData(REVERT_REASON_MAX_LEN); 245 | if (result.length > 0) { 246 | emit UserOperationRevertReason(opInfo.userOpHash, mUserOp.sender, mUserOp.nonce, result); 247 | } 248 | mode = IPaymaster.PostOpMode.opReverted; 249 | } 250 | } 251 | 252 | unchecked { 253 | uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; 254 | //note: opIndex is ignored (relevant only if mode==postOpReverted, which is only possible outside of innerHandleOp) 255 | return _handlePostOp(0, mode, opInfo, context, actualGas); 256 | } 257 | } 258 | 259 | /** 260 | * generate a request Id - unique identifier for this request. 261 | * the request ID is a hash over the content of the userOp (except the signature), the entrypoint and the chainid. 262 | */ 263 | function getUserOpHash(UserOperation calldata userOp) public view returns (bytes32) { 264 | return keccak256(abi.encode(userOp.hash(), address(this), block.chainid)); 265 | } 266 | 267 | /** 268 | * copy general fields from userOp into the memory opInfo structure. 269 | */ 270 | function _copyUserOpToMemory(UserOperation calldata userOp, MemoryUserOp memory mUserOp) internal pure { 271 | mUserOp.sender = userOp.sender; 272 | mUserOp.nonce = userOp.nonce; 273 | mUserOp.callGasLimit = userOp.callGasLimit; 274 | mUserOp.verificationGasLimit = userOp.verificationGasLimit; 275 | mUserOp.preVerificationGas = userOp.preVerificationGas; 276 | mUserOp.maxFeePerGas = userOp.maxFeePerGas; 277 | mUserOp.maxPriorityFeePerGas = userOp.maxPriorityFeePerGas; 278 | bytes calldata paymasterAndData = userOp.paymasterAndData; 279 | if (paymasterAndData.length > 0) { 280 | require(paymasterAndData.length >= 20, "AA93 invalid paymasterAndData"); 281 | mUserOp.paymaster = address(bytes20(paymasterAndData[: 20])); 282 | } else { 283 | mUserOp.paymaster = address(0); 284 | } 285 | } 286 | 287 | /** 288 | * Simulate a call to account.validateUserOp and paymaster.validatePaymasterUserOp. 289 | * @dev this method always revert. Successful result is ValidationResult error. other errors are failures. 290 | * @dev The node must also verify it doesn't use banned opcodes, and that it doesn't reference storage outside the account's data. 291 | * @param userOp the user operation to validate. 292 | */ 293 | function simulateValidation(UserOperation calldata userOp) external { 294 | UserOpInfo memory outOpInfo; 295 | 296 | _simulationOnlyValidations(userOp); 297 | (uint256 validationData, uint256 paymasterValidationData) = _validatePrepayment(0, userOp, outOpInfo); 298 | StakeInfo memory paymasterInfo = _getStakeInfo(outOpInfo.mUserOp.paymaster); 299 | StakeInfo memory senderInfo = _getStakeInfo(outOpInfo.mUserOp.sender); 300 | StakeInfo memory factoryInfo; 301 | { 302 | bytes calldata initCode = userOp.initCode; 303 | address factory = initCode.length >= 20 ? address(bytes20(initCode[0 : 20])) : address(0); 304 | factoryInfo = _getStakeInfo(factory); 305 | } 306 | 307 | ValidationData memory data = _intersectTimeRange(validationData, paymasterValidationData); 308 | address aggregator = data.aggregator; 309 | bool sigFailed = aggregator == address(1); 310 | ReturnInfo memory returnInfo = ReturnInfo(outOpInfo.preOpGas, outOpInfo.prefund, 311 | sigFailed, data.validAfter, data.validUntil, getMemoryBytesFromOffset(outOpInfo.contextOffset)); 312 | 313 | if (aggregator != address(0) && aggregator != address(1)) { 314 | AggregatorStakeInfo memory aggregatorInfo = AggregatorStakeInfo(aggregator, _getStakeInfo(aggregator)); 315 | revert ValidationResultWithAggregation(returnInfo, senderInfo, factoryInfo, paymasterInfo, aggregatorInfo); 316 | } 317 | revert ValidationResult(returnInfo, senderInfo, factoryInfo, paymasterInfo); 318 | 319 | } 320 | 321 | function _getRequiredPrefund(MemoryUserOp memory mUserOp) internal pure returns (uint256 requiredPrefund) { 322 | unchecked { 323 | //when using a Paymaster, the verificationGasLimit is used also to as a limit for the postOp call. 324 | // our security model might call postOp eventually twice 325 | uint256 mul = mUserOp.paymaster != address(0) ? 3 : 1; 326 | uint256 requiredGas = mUserOp.callGasLimit + mUserOp.verificationGasLimit * mul + mUserOp.preVerificationGas; 327 | 328 | requiredPrefund = requiredGas * mUserOp.maxFeePerGas; 329 | } 330 | } 331 | 332 | // create the sender's contract if needed. 333 | function _createSenderIfNeeded(uint256 opIndex, UserOpInfo memory opInfo, bytes calldata initCode) internal { 334 | if (initCode.length != 0) { 335 | address sender = opInfo.mUserOp.sender; 336 | if (sender.code.length != 0) revert FailedOp(opIndex, "AA10 sender already constructed"); 337 | address sender1 = senderCreator.createSender{gas : opInfo.mUserOp.verificationGasLimit}(initCode); 338 | if (sender1 == address(0)) revert FailedOp(opIndex, "AA13 initCode failed or OOG"); 339 | if (sender1 != sender) revert FailedOp(opIndex, "AA14 initCode must return sender"); 340 | if (sender1.code.length == 0) revert FailedOp(opIndex, "AA15 initCode must create sender"); 341 | address factory = address(bytes20(initCode[0 : 20])); 342 | emit AccountDeployed(opInfo.userOpHash, sender, factory, opInfo.mUserOp.paymaster); 343 | } 344 | } 345 | 346 | /** 347 | * Get counterfactual sender address. 348 | * Calculate the sender contract address that will be generated by the initCode and salt in the UserOperation. 349 | * this method always revert, and returns the address in SenderAddressResult error 350 | * @param initCode the constructor code to be passed into the UserOperation. 351 | */ 352 | function getSenderAddress(bytes calldata initCode) public { 353 | revert SenderAddressResult(senderCreator.createSender(initCode)); 354 | } 355 | 356 | function _simulationOnlyValidations(UserOperation calldata userOp) internal view { 357 | // solhint-disable-next-line no-empty-blocks 358 | try this._validateSenderAndPaymaster(userOp.initCode, userOp.sender, userOp.paymasterAndData) {} 359 | catch Error(string memory revertReason) { 360 | if (bytes(revertReason).length != 0) { 361 | revert FailedOp(0, revertReason); 362 | } 363 | } 364 | } 365 | 366 | /** 367 | * Called only during simulation. 368 | * This function always reverts to prevent warm/cold storage differentiation in simulation vs execution. 369 | */ 370 | function _validateSenderAndPaymaster(bytes calldata initCode, address sender, bytes calldata paymasterAndData) external view { 371 | if (initCode.length == 0 && sender.code.length == 0) { 372 | // it would revert anyway. but give a meaningful message 373 | revert("AA20 account not deployed"); 374 | } 375 | if (paymasterAndData.length >= 20) { 376 | address paymaster = address(bytes20(paymasterAndData[0 : 20])); 377 | if (paymaster.code.length == 0) { 378 | // it would revert anyway. but give a meaningful message 379 | revert("AA30 paymaster not deployed"); 380 | } 381 | } 382 | // always revert 383 | revert(""); 384 | } 385 | 386 | /** 387 | * call account.validateUserOp. 388 | * revert (with FailedOp) in case validateUserOp reverts, or account didn't send required prefund. 389 | * decrement account's deposit if needed 390 | */ 391 | function _validateAccountPrepayment(uint256 opIndex, UserOperation calldata op, UserOpInfo memory opInfo, uint256 requiredPrefund) 392 | internal returns (uint256 gasUsedByValidateAccountPrepayment, uint256 validationData) { 393 | unchecked { 394 | uint256 preGas = gasleft(); 395 | MemoryUserOp memory mUserOp = opInfo.mUserOp; 396 | address sender = mUserOp.sender; 397 | _createSenderIfNeeded(opIndex, opInfo, op.initCode); 398 | address paymaster = mUserOp.paymaster; 399 | numberMarker(); 400 | uint256 missingAccountFunds = 0; 401 | if (paymaster == address(0)) { 402 | uint256 bal = balanceOf(sender); 403 | missingAccountFunds = bal > requiredPrefund ? 0 : requiredPrefund - bal; 404 | } 405 | try IAccount(sender).validateUserOp{gas : mUserOp.verificationGasLimit}(op, opInfo.userOpHash, missingAccountFunds) 406 | returns (uint256 _validationData) { 407 | validationData = _validationData; 408 | } catch Error(string memory revertReason) { 409 | revert FailedOp(opIndex, string.concat("AA23 reverted: ", revertReason)); 410 | } catch { 411 | revert FailedOp(opIndex, "AA23 reverted (or OOG)"); 412 | } 413 | if (paymaster == address(0)) { 414 | DepositInfo storage senderInfo = deposits[sender]; 415 | uint256 deposit = senderInfo.deposit; 416 | if (requiredPrefund > deposit) { 417 | revert FailedOp(opIndex, "AA21 didn't pay prefund"); 418 | } 419 | senderInfo.deposit = uint112(deposit - requiredPrefund); 420 | } 421 | gasUsedByValidateAccountPrepayment = preGas - gasleft(); 422 | } 423 | } 424 | 425 | /** 426 | * In case the request has a paymaster: 427 | * Validate paymaster has enough deposit. 428 | * Call paymaster.validatePaymasterUserOp. 429 | * Revert with proper FailedOp in case paymaster reverts. 430 | * Decrement paymaster's deposit 431 | */ 432 | function _validatePaymasterPrepayment(uint256 opIndex, UserOperation calldata op, UserOpInfo memory opInfo, uint256 requiredPreFund, uint256 gasUsedByValidateAccountPrepayment) 433 | internal returns (bytes memory context, uint256 validationData) { 434 | unchecked { 435 | MemoryUserOp memory mUserOp = opInfo.mUserOp; 436 | uint256 verificationGasLimit = mUserOp.verificationGasLimit; 437 | require(verificationGasLimit > gasUsedByValidateAccountPrepayment, "AA41 too little verificationGas"); 438 | uint256 gas = verificationGasLimit - gasUsedByValidateAccountPrepayment; 439 | 440 | address paymaster = mUserOp.paymaster; 441 | DepositInfo storage paymasterInfo = deposits[paymaster]; 442 | uint256 deposit = paymasterInfo.deposit; 443 | if (deposit < requiredPreFund) { 444 | revert FailedOp(opIndex, "AA31 paymaster deposit too low"); 445 | } 446 | paymasterInfo.deposit = uint112(deposit - requiredPreFund); 447 | try IPaymaster(paymaster).validatePaymasterUserOp{gas : gas}(op, opInfo.userOpHash, requiredPreFund) returns (bytes memory _context, uint256 _validationData){ 448 | context = _context; 449 | validationData = _validationData; 450 | } catch Error(string memory revertReason) { 451 | revert FailedOp(opIndex, string.concat("AA33 reverted: ", revertReason)); 452 | } catch { 453 | revert FailedOp(opIndex, "AA33 reverted (or OOG)"); 454 | } 455 | } 456 | } 457 | 458 | /** 459 | * revert if either account validationData or paymaster validationData is expired 460 | */ 461 | function _validateAccountAndPaymasterValidationData(uint256 opIndex, uint256 validationData, uint256 paymasterValidationData, address expectedAggregator) internal view { 462 | 463 | (address aggregator, bool outOfTimeRange) = _getValidationData(validationData); 464 | 465 | if (expectedAggregator != aggregator) { 466 | revert FailedOp(opIndex, "AA24 signature error"); 467 | } 468 | if (outOfTimeRange) { 469 | revert FailedOp(opIndex, "AA22 expired or not due"); 470 | } 471 | //pmAggregator is not a real signature aggregator: we don't have logic to handle it as address. 472 | // non-zero address means that the paymaster fails due to some signature check (which is ok only during estimation) 473 | address pmAggregator; 474 | (pmAggregator, outOfTimeRange) = _getValidationData(paymasterValidationData); 475 | if (pmAggregator != address(0)) { 476 | revert FailedOp(opIndex, "AA34 signature error"); 477 | } 478 | if (outOfTimeRange) { 479 | revert FailedOp(opIndex, "AA32 paymaster expired or not due"); 480 | } 481 | } 482 | 483 | function _getValidationData(uint256 validationData) internal view returns (address aggregator, bool outOfTimeRange) { 484 | if (validationData == 0) { 485 | return (address(0), false); 486 | } 487 | ValidationData memory data = _parseValidationData(validationData); 488 | // solhint-disable-next-line not-rely-on-time 489 | outOfTimeRange = block.timestamp > data.validUntil || block.timestamp < data.validAfter; 490 | aggregator = data.aggregator; 491 | } 492 | 493 | /** 494 | * validate account and paymaster (if defined). 495 | * also make sure total validation doesn't exceed verificationGasLimit 496 | * this method is called off-chain (simulateValidation()) and on-chain (from handleOps) 497 | * @param opIndex the index of this userOp into the "opInfos" array 498 | * @param userOp the userOp to validate 499 | */ 500 | function _validatePrepayment(uint256 opIndex, UserOperation calldata userOp, UserOpInfo memory outOpInfo) 501 | private returns (uint256 validationData, uint256 paymasterValidationData) { 502 | 503 | uint256 preGas = gasleft(); 504 | MemoryUserOp memory mUserOp = outOpInfo.mUserOp; 505 | _copyUserOpToMemory(userOp, mUserOp); 506 | outOpInfo.userOpHash = getUserOpHash(userOp); 507 | 508 | 509 | // validate all numeric values in userOp are well below 128 bit, so they can safely be added 510 | // and multiplied without causing overflow 511 | uint256 maxGasValues = mUserOp.preVerificationGas | mUserOp.verificationGasLimit | mUserOp.callGasLimit | 512 | userOp.maxFeePerGas | userOp.maxPriorityFeePerGas; 513 | require(maxGasValues <= type(uint120).max, "AA94 gas values overflow"); 514 | 515 | uint256 gasUsedByValidateAccountPrepayment; 516 | (uint256 requiredPreFund) = _getRequiredPrefund(mUserOp); 517 | (gasUsedByValidateAccountPrepayment, validationData) = _validateAccountPrepayment(opIndex, userOp, outOpInfo, requiredPreFund); 518 | //a "marker" where account opcode validation is done and paymaster opcode validation is about to start 519 | // (used only by off-chain simulateValidation) 520 | numberMarker(); 521 | 522 | bytes memory context; 523 | if (mUserOp.paymaster != address(0)) { 524 | (context, paymasterValidationData) = _validatePaymasterPrepayment(opIndex, userOp, outOpInfo, requiredPreFund, gasUsedByValidateAccountPrepayment); 525 | } 526 | unchecked { 527 | uint256 gasUsed = preGas - gasleft(); 528 | 529 | if (userOp.verificationGasLimit < gasUsed) { 530 | revert FailedOp(opIndex, "AA40 over verificationGasLimit"); 531 | } 532 | outOpInfo.prefund = requiredPreFund; 533 | outOpInfo.contextOffset = getOffsetOfMemoryBytes(context); 534 | outOpInfo.preOpGas = preGas - gasleft() + userOp.preVerificationGas; 535 | } 536 | } 537 | 538 | /** 539 | * process post-operation. 540 | * called just after the callData is executed. 541 | * if a paymaster is defined and its validation returned a non-empty context, its postOp is called. 542 | * the excess amount is refunded to the account (or paymaster - if it was used in the request) 543 | * @param opIndex index in the batch 544 | * @param mode - whether is called from innerHandleOp, or outside (postOpReverted) 545 | * @param opInfo userOp fields and info collected during validation 546 | * @param context the context returned in validatePaymasterUserOp 547 | * @param actualGas the gas used so far by this user operation 548 | */ 549 | function _handlePostOp(uint256 opIndex, IPaymaster.PostOpMode mode, UserOpInfo memory opInfo, bytes memory context, uint256 actualGas) private returns (uint256 actualGasCost) { 550 | uint256 preGas = gasleft(); 551 | unchecked { 552 | address refundAddress; 553 | MemoryUserOp memory mUserOp = opInfo.mUserOp; 554 | uint256 gasPrice = getUserOpGasPrice(mUserOp); 555 | 556 | address paymaster = mUserOp.paymaster; 557 | if (paymaster == address(0)) { 558 | refundAddress = mUserOp.sender; 559 | } else { 560 | refundAddress = paymaster; 561 | if (context.length > 0) { 562 | actualGasCost = actualGas * gasPrice; 563 | if (mode != IPaymaster.PostOpMode.postOpReverted) { 564 | IPaymaster(paymaster).postOp{gas : mUserOp.verificationGasLimit}(mode, context, actualGasCost); 565 | } else { 566 | // solhint-disable-next-line no-empty-blocks 567 | try IPaymaster(paymaster).postOp{gas : mUserOp.verificationGasLimit}(mode, context, actualGasCost) {} 568 | catch Error(string memory reason) { 569 | revert FailedOp(opIndex, string.concat("AA50 postOp reverted: ", reason)); 570 | } 571 | catch { 572 | revert FailedOp(opIndex, "AA50 postOp revert"); 573 | } 574 | } 575 | } 576 | } 577 | actualGas += preGas - gasleft(); 578 | actualGasCost = actualGas * gasPrice; 579 | if (opInfo.prefund < actualGasCost) { 580 | revert FailedOp(opIndex, "AA51 prefund below actualGasCost"); 581 | } 582 | uint256 refund = opInfo.prefund - actualGasCost; 583 | _incrementDeposit(refundAddress, refund); 584 | bool success = mode == IPaymaster.PostOpMode.opSucceeded; 585 | emit UserOperationEvent(opInfo.userOpHash, mUserOp.sender, mUserOp.paymaster, mUserOp.nonce, success, actualGasCost, actualGas); 586 | } // unchecked 587 | } 588 | 589 | /** 590 | * the gas price this UserOp agrees to pay. 591 | * relayer/block builder might submit the TX with higher priorityFee, but the user should not 592 | */ 593 | function getUserOpGasPrice(MemoryUserOp memory mUserOp) internal view returns (uint256) { 594 | unchecked { 595 | uint256 maxFeePerGas = mUserOp.maxFeePerGas; 596 | uint256 maxPriorityFeePerGas = mUserOp.maxPriorityFeePerGas; 597 | if (maxFeePerGas == maxPriorityFeePerGas) { 598 | //legacy mode (for networks that don't support basefee opcode) 599 | return maxFeePerGas; 600 | } 601 | return min(maxFeePerGas, maxPriorityFeePerGas + block.basefee); 602 | } 603 | } 604 | 605 | function min(uint256 a, uint256 b) internal pure returns (uint256) { 606 | return a < b ? a : b; 607 | } 608 | 609 | function getOffsetOfMemoryBytes(bytes memory data) internal pure returns (uint256 offset) { 610 | assembly {offset := data} 611 | } 612 | 613 | function getMemoryBytesFromOffset(uint256 offset) internal pure returns (bytes memory data) { 614 | assembly {data := offset} 615 | } 616 | 617 | //place the NUMBER opcode in the code. 618 | // this is used as a marker during simulation, as this OP is completely banned from the simulated code of the 619 | // account and paymaster. 620 | function numberMarker() internal view { 621 | assembly {mstore(0, number())} 622 | } 623 | } 624 | --------------------------------------------------------------------------------