diff --git a/packages/bundler/src/BundlerConfig.ts b/packages/bundler/src/BundlerConfig.ts index 8feeb01a..aa11bc21 100644 --- a/packages/bundler/src/BundlerConfig.ts +++ b/packages/bundler/src/BundlerConfig.ts @@ -9,6 +9,7 @@ export interface BundlerConfig { chainId: number beneficiary: string entryPoint: string + senderCreator: string gasFactor: string minBalance: string mnemonic: string @@ -48,6 +49,7 @@ export const BundlerConfigShape = { chainId: ow.number, beneficiary: ow.string, entryPoint: ow.string, + senderCreator: ow.string, gasFactor: ow.string, minBalance: ow.string, mnemonic: ow.string, @@ -102,6 +104,7 @@ export const bundlerConfigDefault: Partial = { port: '3000', privateApiPort: '3001', entryPoint: '0x0000000071727De22E5E9d8BAf0edAc6f37da032', + senderCreator: '0xefc2c1444ebcc4db75e7613d20c6a62ff67a167c', unsafe: false, conditionalRpc: false, minStake: MIN_STAKE_VALUE, diff --git a/packages/bundler/src/modules/ExecutionManager.ts b/packages/bundler/src/modules/ExecutionManager.ts index 89a50e29..a10816fc 100644 --- a/packages/bundler/src/modules/ExecutionManager.ts +++ b/packages/bundler/src/modules/ExecutionManager.ts @@ -15,6 +15,7 @@ import { DepositManager } from './DepositManager' import { BigNumberish, Signer } from 'ethers' import { BundlerConfig } from '../BundlerConfig' import { PreVerificationGasCalculator } from '@account-abstraction/sdk' +import { ERC7562Parser } from '@account-abstraction/validation-manager/dist/src/ERC7562Parser' const debug = Debug('aa.exec') @@ -147,11 +148,14 @@ export class ExecutionManager { async _setConfiguration (configOverrides: Partial): Promise { const { configuration, entryPoint, unsafe } = this.validationManager._getDebugConfiguration() - const pvgc = new PreVerificationGasCalculator(Object.assign({}, configuration, configOverrides)) + const mergedConfiguration = Object.assign({}, configuration, configOverrides) + const pvgc = new PreVerificationGasCalculator(mergedConfiguration) + const erc7562Parser = new ERC7562Parser(entryPoint.address, mergedConfiguration.senderCreator ?? '') this.validationManager = new ValidationManager( entryPoint, unsafe, - pvgc + pvgc, + erc7562Parser ) return pvgc } diff --git a/packages/bundler/src/modules/initServer.ts b/packages/bundler/src/modules/initServer.ts index 63145e80..ad7d0942 100644 --- a/packages/bundler/src/modules/initServer.ts +++ b/packages/bundler/src/modules/initServer.ts @@ -9,9 +9,13 @@ import { BundlerReputationParams, ReputationManager } from './ReputationManager' import { MempoolManager } from './MempoolManager' import { BundleManager } from './BundleManager' import { + AA_ENTRY_POINT, + AA_NONCE_MANAGER, + AA_SENDER_CREATOR, + AA_STAKE_MANAGER, + IValidationManager, ValidationManager, - ValidationManagerRIP7560, - IValidationManager, AA_STAKE_MANAGER + ValidationManagerRIP7560 } from '@account-abstraction/validation-manager' import { BundlerConfig } from '../BundlerConfig' import { EventsManager } from './EventsManager' @@ -21,6 +25,7 @@ import { IBundleManager } from './IBundleManager' import { DepositManager } from './DepositManager' import { IRip7560StakeManager__factory } from '@account-abstraction/utils/dist/src/types' import { PreVerificationGasCalculator, ChainConfigs } from '@account-abstraction/sdk' +import { ERC7562Parser } from '@account-abstraction/validation-manager/dist/src/ERC7562Parser' /** * initialize server modules. @@ -38,13 +43,15 @@ export function initServer (config: BundlerConfig, signer: Signer): [ExecutionMa let validationManager: IValidationManager let bundleManager: IBundleManager if (!config.rip7560) { + const erc7562Parser = new ERC7562Parser(entryPoint.address, config.senderCreator) const tracerProvider = config.tracerRpcUrl == null ? undefined : getNetworkProvider(config.tracerRpcUrl) - validationManager = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator, tracerProvider) + validationManager = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator, erc7562Parser, tracerProvider) bundleManager = new BundleManager(entryPoint, entryPoint.provider as JsonRpcProvider, signer, eventsManager, mempoolManager, validationManager, reputationManager, config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, config.conditionalRpc) } else { + const erc7562Parser = new ERC7562Parser(AA_ENTRY_POINT, AA_SENDER_CREATOR, AA_NONCE_MANAGER) const stakeManager = IRip7560StakeManager__factory.connect(AA_STAKE_MANAGER, signer) - validationManager = new ValidationManagerRIP7560(stakeManager, entryPoint.provider as JsonRpcProvider, config.unsafe) + validationManager = new ValidationManagerRIP7560(stakeManager, entryPoint.provider as JsonRpcProvider, erc7562Parser, config.unsafe) bundleManager = new BundleManagerRIP7560(entryPoint.provider as JsonRpcProvider, signer, eventsManager, mempoolManager, validationManager, reputationManager, config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, config.conditionalRpc, false) } diff --git a/packages/bundler/test/BundlerManager.test.ts b/packages/bundler/test/BundlerManager.test.ts index a855f33c..9827b7df 100644 --- a/packages/bundler/test/BundlerManager.test.ts +++ b/packages/bundler/test/BundlerManager.test.ts @@ -23,6 +23,7 @@ import { ExecutionManager } from '../src/modules/ExecutionManager' import { EventsManager } from '../src/modules/EventsManager' import { createSigner } from './testUtils' import { DepositManager } from '../src/modules/DepositManager' +import { ERC7562Parser } from '@account-abstraction/validation-manager/dist/src/ERC7562Parser' describe('#BundlerManager', () => { let bm: BundleManager @@ -40,6 +41,7 @@ describe('#BundlerManager', () => { chainId: 1337, beneficiary: await signer.getAddress(), entryPoint: entryPoint.address, + senderCreator: '0xefc2c1444ebcc4db75e7613d20c6a62ff67a167c', gasFactor: '0.2', minBalance: '0', mnemonic: '', @@ -63,7 +65,8 @@ describe('#BundlerManager', () => { const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) const mempoolMgr = new MempoolManager(repMgr) const preVerificationGasCalculator = new PreVerificationGasCalculator(MainnetConfig) - const validMgr = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator) + const erc7562Parser = new ERC7562Parser(entryPoint.address, config.senderCreator) + const validMgr = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator, erc7562Parser) const evMgr = new EventsManager(entryPoint, mempoolMgr, repMgr) bm = new BundleManager(entryPoint, entryPoint.provider as JsonRpcProvider, entryPoint.signer, evMgr, mempoolMgr, validMgr, repMgr, config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, config.conditionalRpc) }) @@ -97,6 +100,7 @@ describe('#BundlerManager', () => { chainId: 1337, beneficiary: await bundlerSigner.getAddress(), entryPoint: _entryPoint.address, + senderCreator: '0xefc2c1444ebcc4db75e7613d20c6a62ff67a167c', gasFactor: '0.2', minBalance: '0', mnemonic: '', @@ -119,7 +123,8 @@ describe('#BundlerManager', () => { const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) const mempoolMgr = new MempoolManager(repMgr) const preVerificationGasCalculator = new PreVerificationGasCalculator(MainnetConfig) - const validMgr = new ValidationManager(_entryPoint, config.unsafe, preVerificationGasCalculator) + const erc7562Parser = new ERC7562Parser(entryPoint.address, config.senderCreator) + const validMgr = new ValidationManager(_entryPoint, config.unsafe, preVerificationGasCalculator, erc7562Parser) const evMgr = new EventsManager(_entryPoint, mempoolMgr, repMgr) bundleMgr = new BundleManager(_entryPoint, _entryPoint.provider as JsonRpcProvider, _entryPoint.signer, evMgr, mempoolMgr, validMgr, repMgr, config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, false) const depositManager = new DepositManager(entryPoint, mempoolMgr, bundleMgr) diff --git a/packages/bundler/test/BundlerServer.test.ts b/packages/bundler/test/BundlerServer.test.ts index 7b47c4af..cb7e30b4 100644 --- a/packages/bundler/test/BundlerServer.test.ts +++ b/packages/bundler/test/BundlerServer.test.ts @@ -23,6 +23,7 @@ import { ExecutionManager } from '../src/modules/ExecutionManager' import { MethodHandlerERC4337 } from '../src/MethodHandlerERC4337' import { BundlerConfig } from '../src/BundlerConfig' import { DepositManager } from '../src/modules/DepositManager' +import { ERC7562Parser } from '@account-abstraction/validation-manager/dist/src/ERC7562Parser' describe('BundleServer', function () { let entryPoint: IEntryPoint @@ -36,6 +37,7 @@ describe('BundleServer', function () { chainId: 1337, beneficiary: await signer.getAddress(), entryPoint: entryPoint.address, + senderCreator: '0xefc2c1444ebcc4db75e7613d20c6a62ff67a167c', gasFactor: '0.2', minBalance: '0', mnemonic: '', @@ -59,7 +61,8 @@ describe('BundleServer', function () { const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) const mempoolMgr = new MempoolManager(repMgr) const preVerificationGasCalculator = new PreVerificationGasCalculator(MainnetConfig) - const validMgr = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator) + const erc7562Parser = new ERC7562Parser(entryPoint.address, config.senderCreator) + const validMgr = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator, erc7562Parser) const evMgr = new EventsManager(entryPoint, mempoolMgr, repMgr) const bundleMgr = new BundleManager(entryPoint, entryPoint.provider as JsonRpcProvider, entryPoint.signer, evMgr, mempoolMgr, validMgr, repMgr, config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, false) const depositManager = new DepositManager(entryPoint, mempoolMgr, bundleMgr) diff --git a/packages/bundler/test/DebugMethodHandler.test.ts b/packages/bundler/test/DebugMethodHandler.test.ts index c5678d4c..761d0277 100644 --- a/packages/bundler/test/DebugMethodHandler.test.ts +++ b/packages/bundler/test/DebugMethodHandler.test.ts @@ -25,6 +25,7 @@ import { MethodHandlerERC4337 } from '../src/MethodHandlerERC4337' import { createSigner } from './testUtils' import { EventsManager } from '../src/modules/EventsManager' import { DepositManager } from '../src/modules/DepositManager' +import { ERC7562Parser } from '@account-abstraction/validation-manager/dist/src/ERC7562Parser' const provider = ethers.provider @@ -46,6 +47,7 @@ describe('#DebugMethodHandler', () => { chainId: 1337, beneficiary: await signer.getAddress(), entryPoint: entryPoint.address, + senderCreator: '0xefc2c1444ebcc4db75e7613d20c6a62ff67a167c', gasFactor: '0.2', minBalance: '0', mnemonic: '', @@ -69,7 +71,8 @@ describe('#DebugMethodHandler', () => { const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) const mempoolMgr = new MempoolManager(repMgr) const preVerificationGasCalculator = new PreVerificationGasCalculator(MainnetConfig) - const validMgr = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator) + const erc7562Parser = new ERC7562Parser(entryPoint.address, config.senderCreator) + const validMgr = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator, erc7562Parser) const eventsManager = new EventsManager(entryPoint, mempoolMgr, repMgr) const bundleMgr = new BundleManager(entryPoint, entryPoint.provider as JsonRpcProvider, entryPoint.signer, eventsManager, mempoolMgr, validMgr, repMgr, config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, false) diff --git a/packages/bundler/test/UserOpMethodHandler.test.ts b/packages/bundler/test/UserOpMethodHandler.test.ts index 84d44af8..c47dbe96 100644 --- a/packages/bundler/test/UserOpMethodHandler.test.ts +++ b/packages/bundler/test/UserOpMethodHandler.test.ts @@ -34,6 +34,7 @@ import { ethers } from 'hardhat' import { createSigner } from './testUtils' import { EventsManager } from '../src/modules/EventsManager' import { DepositManager } from '../src/modules/DepositManager' +import { ERC7562Parser } from '@account-abstraction/validation-manager/dist/src/ERC7562Parser' describe('UserOpMethodHandler', function () { const helloWorld = 'hello world' @@ -64,6 +65,7 @@ describe('UserOpMethodHandler', function () { chainId: 1337, beneficiary: await signer.getAddress(), entryPoint: entryPoint.address, + senderCreator: '0xefc2c1444ebcc4db75e7613d20c6a62ff67a167c', gasFactor: '0.2', minBalance: '0', mnemonic: '', @@ -87,7 +89,8 @@ describe('UserOpMethodHandler', function () { const repMgr = new ReputationManager(provider, BundlerReputationParams, parseEther(config.minStake), config.minUnstakeDelay) mempoolMgr = new MempoolManager(repMgr) const preVerificationGasCalculator = new PreVerificationGasCalculator(MainnetConfig) - const validMgr = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator) + const erc7562Parser = new ERC7562Parser(entryPoint.address, config.senderCreator) + const validMgr = new ValidationManager(entryPoint, config.unsafe, preVerificationGasCalculator, erc7562Parser) const evMgr = new EventsManager(entryPoint, mempoolMgr, repMgr) const bundleMgr = new BundleManager(entryPoint, entryPoint.provider as JsonRpcProvider, entryPoint.signer, evMgr, mempoolMgr, validMgr, repMgr, config.beneficiary, parseEther(config.minBalance), config.maxBundleGas, false) const depositManager = new DepositManager(entryPoint, mempoolMgr, bundleMgr) diff --git a/packages/bundler/test/ValidateManager.test.ts b/packages/bundler/test/ValidateManager.test.ts index 129869bf..c582b104 100644 --- a/packages/bundler/test/ValidateManager.test.ts +++ b/packages/bundler/test/ValidateManager.test.ts @@ -35,6 +35,7 @@ import { TestTimeRangeAccountFactory, TestTimeRangeAccountFactory__factory } from '../src/types' +import { ERC7562Parser } from '@account-abstraction/validation-manager/dist/src/ERC7562Parser' const cEmptyUserOp: UserOperation = { sender: AddressZero, @@ -154,7 +155,10 @@ describe('#ValidationManager', () => { const unsafe = !await supportsDebugTraceCall(provider, false) const preVerificationGasCalculator = new PreVerificationGasCalculator(MainnetConfig) - vm = new ValidationManager(entryPoint, unsafe, preVerificationGasCalculator) + + const senderCreator = '0xefc2c1444ebcc4db75e7613d20c6a62ff67a167c' + const erc7562Parser = new ERC7562Parser(entryPoint.address, senderCreator) + vm = new ValidationManager(entryPoint, unsafe, preVerificationGasCalculator, erc7562Parser) if (!await supportsDebugTraceCall(ethers.provider, false)) { console.log('WARNING: opcode banning tests can only run with geth') diff --git a/packages/utils/src/interfaces/OperationBase.ts b/packages/utils/src/interfaces/OperationBase.ts index 0523c30d..cddf756f 100644 --- a/packages/utils/src/interfaces/OperationBase.ts +++ b/packages/utils/src/interfaces/OperationBase.ts @@ -1,7 +1,5 @@ import { BigNumberish, BytesLike } from 'ethers' -import { EIP7702Authorization } from './EIP7702Authorization' - /** * The operation interface that is shared by ERC-4337 and RIP-7560 types. */ diff --git a/packages/validation-manager/package.json b/packages/validation-manager/package.json index 6a3d5067..a4458178 100644 --- a/packages/validation-manager/package.json +++ b/packages/validation-manager/package.json @@ -22,7 +22,8 @@ "@ethersproject/properties": "^5.7.0", "@ethersproject/providers": "^5.7.0", "debug": "^4.3.4", - "ethers": "^5.7.0" + "ethers": "^5.7.0", + "ow": "^0.28.1" }, "devDependencies": {} } diff --git a/packages/validation-manager/src/AccountAbstractionEntity.ts b/packages/validation-manager/src/AccountAbstractionEntity.ts new file mode 100644 index 00000000..9f705600 --- /dev/null +++ b/packages/validation-manager/src/AccountAbstractionEntity.ts @@ -0,0 +1,11 @@ +export enum AccountAbstractionEntity { + account = 'account', + paymaster = 'paymaster', + factory = 'factory', + aggregator = 'aggregator', + senderCreator = 'SenderCreator', + entryPoint = 'EntryPoint', + nativeEntryPoint = 'NativeEntryPoint', + nativeNonceManager = 'NativeNonceManager', + none = 'none' +} diff --git a/packages/validation-manager/src/BundlerCollectorTracer.ts b/packages/validation-manager/src/BundlerCollectorTracer.ts index ac93d1ae..60d7dc69 100644 --- a/packages/validation-manager/src/BundlerCollectorTracer.ts +++ b/packages/validation-manager/src/BundlerCollectorTracer.ts @@ -51,13 +51,17 @@ export interface ExitInfo { data: string } +export interface StorageAccessInfos { [address: string]: AccessInfo } +export interface OpcodeInfos { [address: string]: number } +export interface ContractSizes { [address: string]: ContractSizeInfo } +export interface ExtCodeAccessInfos { [address: string]: string } + export interface TopLevelCallInfo { - // topLevelMethodSig: string topLevelTargetAddress: string - opcodes: { [opcode: string]: number } - access: { [address: string]: AccessInfo } - contractSize: { [addr: string]: ContractSizeInfo } - extCodeAccessInfo: { [addr: string]: string } + opcodes: OpcodeInfos + access: StorageAccessInfos + contractSize: ContractSizes + extCodeAccessInfo: ExtCodeAccessInfos oog?: boolean calls?: [] type?: string diff --git a/packages/validation-manager/src/ERC7562BannedOpcodes.ts b/packages/validation-manager/src/ERC7562BannedOpcodes.ts new file mode 100644 index 00000000..eb0c4bf9 --- /dev/null +++ b/packages/validation-manager/src/ERC7562BannedOpcodes.ts @@ -0,0 +1,31 @@ +/** + * [OP-011] the opcodes banned for all entities. + */ +export const bannedOpCodes = new Set( + [ + 'BASEFEE', + 'BLOCKHASH', + 'COINBASE', + 'DIFFICULTY', + 'GAS', + 'GASLIMIT', + 'GASPRICE', + 'INVALID', + 'NUMBER', + 'ORIGIN', + 'PREVRANDAO', + 'RANDOM', + 'SELFDESTRUCT', + 'TIMESTAMP' + ] +) + +/** + * [OP-080] the opcodes allowed in staked entities. + */ +export const opcodesOnlyInStakedEntities = new Set( + [ + 'BALANCE', + 'SELFBALANCE' + ] +) diff --git a/packages/validation-manager/src/ERC7562Call.ts b/packages/validation-manager/src/ERC7562Call.ts new file mode 100644 index 00000000..21b73144 --- /dev/null +++ b/packages/validation-manager/src/ERC7562Call.ts @@ -0,0 +1,76 @@ +import ow from 'ow' + +export interface ContractSize { + contractSize: number + opcode: number +} + +export interface AccessedSlots { + reads?: Record + transientReads?: Record + transientWrites?: Record + writes?: Record +} + +export interface ERC7562Call { + accessedSlots: AccessedSlots + contractSize: Record + error?: string + extCodeAccessInfo: string[] + from: string + gas: string + gasUsed: string + input: string + outOfGas: boolean + output?: string + to: string + type: string + usedOpcodes: Record + value?: string + calls?: ERC7562Call[] + keccak?: string[] +} + +const contractSizeSchema = ow.object.exactShape({ + contractSize: ow.number, + opcode: ow.number +}) + +const accessedSlotsSchema = ow.object.exactShape({ + reads: ow.object.valuesOfType(ow.array.ofType(ow.string)), + transientReads: ow.object, + transientWrites: ow.object, + writes: ow.object.valuesOfType(ow.number) +}) + +const erc7562CallSchema = ow.object.exactShape({ + accessedSlots: accessedSlotsSchema, + contractSize: ow.object.valuesOfType(contractSizeSchema), + error: ow.optional.string, + extCodeAccessInfo: ow.array.ofType(ow.string), + from: ow.string, + gas: ow.string, + gasUsed: ow.string, + input: ow.string, + outOfGas: ow.boolean, + output: ow.optional.string, + to: ow.string, + type: ow.string, + usedOpcodes: ow.object.valuesOfType(ow.number), + value: ow.optional.string, + calls: ow.optional.array, + keccak: ow.optional.array.ofType(ow.string) +}) + +/** + * Recursively check the calls ow schema. + */ +export function validateERC7562Call (value: ERC7562Call): void { + ow(value, erc7562CallSchema) + + if (value.calls != null) { + for (const call of value.calls) { + validateERC7562Call(call) + } + } +} diff --git a/packages/validation-manager/src/ERC7562Parser.ts b/packages/validation-manager/src/ERC7562Parser.ts new file mode 100644 index 00000000..ccdd72ee --- /dev/null +++ b/packages/validation-manager/src/ERC7562Parser.ts @@ -0,0 +1,805 @@ +import { BigNumber } from 'ethers' +import { FunctionFragment, hexZeroPad, Interface, keccak256 } from 'ethers/lib/utils' + +import { + AddressZero, + IEntryPoint__factory, + IPaymaster__factory, + OperationBase, + RpcError, + SenderCreator__factory, + SlotMap, + StakeInfo, + StorageMap, + ValidationErrors, + toBytes32 +} from '@account-abstraction/utils' + +import { ERC7562Violation, toError } from './ERC7562Violation' +import { ERC7562Rule } from './enum/ERC7562Rule' +import { AccountAbstractionEntity } from './AccountAbstractionEntity' +import { bannedOpCodes, opcodesOnlyInStakedEntities } from './ERC7562BannedOpcodes' +import { ValidationResult } from './IValidationManager' +import { ERC7562Call } from './ERC7562Call' +import { getOpcodeName } from './enum/EVMOpcodes' + +// TODO: Use artifact from the submodule +const RIP7560EntryPointABI = [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address' + }, + { + indexed: true, + internalType: 'address', + name: 'paymaster', + type: 'address' + }, + { + indexed: true, + internalType: 'address', + name: 'deployer', + type: 'address' + } + ], + name: 'RIP7560AccountDeployed', + type: 'event' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address' + }, + { + indexed: true, + internalType: 'address', + name: 'paymaster', + type: 'address' + }, + { + indexed: false, + internalType: 'uint256', + name: 'nonceKey', + type: 'uint256' + }, + { + indexed: false, + internalType: 'uint256', + name: 'nonceSequence', + type: 'uint256' + }, + { + indexed: false, + internalType: 'uint256', + name: 'executionStatus', + type: 'uint256' + } + ], + name: 'RIP7560TransactionEvent', + type: 'event' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address' + }, + { + indexed: true, + internalType: 'address', + name: 'paymaster', + type: 'address' + }, + { + indexed: false, + internalType: 'uint256', + name: 'nonceKey', + type: 'uint256' + }, + { + indexed: false, + internalType: 'uint256', + name: 'nonceSequence', + type: 'uint256' + }, + { + indexed: false, + internalType: 'bytes', + name: 'revertReason', + type: 'bytes' + } + ], + name: 'RIP7560TransactionPostOpRevertReason', + type: 'event' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address' + }, + { + indexed: false, + internalType: 'uint256', + name: 'nonceKey', + type: 'uint256' + }, + { + indexed: false, + internalType: 'uint256', + name: 'nonceSequence', + type: 'uint256' + }, + { + indexed: false, + internalType: 'bytes', + name: 'revertReason', + type: 'bytes' + } + ], + name: 'RIP7560TransactionRevertReason', + type: 'event' + }, + { + inputs: [ + { + internalType: 'uint256', + name: 'validAfter', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'validUntil', + type: 'uint256' + } + ], + name: 'acceptAccount', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'uint256', + name: 'validAfter', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'validUntil', + type: 'uint256' + }, + { + internalType: 'bytes', + name: 'context', + type: 'bytes' + } + ], + name: 'acceptPaymaster', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'uint256', + name: 'validAfter', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'validUntil', + type: 'uint256' + } + ], + name: 'sigFailAccount', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'uint256', + name: 'validAfter', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'validUntil', + type: 'uint256' + }, + { + internalType: 'bytes', + name: 'context', + type: 'bytes' + } + ], + name: 'sigFailPaymaster', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + } +] + +export interface ERC7562ValidationResults { + storageMap: StorageMap + ruleViolations: ERC7562Violation[] + contractAddresses: string[] +} + +export class ERC7562Parser { + private keccak: string[] = [] + private ruleViolations: ERC7562Violation[] = [] + private currentEntity: AccountAbstractionEntity = AccountAbstractionEntity.none + private currentEntityAddress: string = '' + private stakeValidationResult!: ValidationResult + + private contractAddresses: string[] = [] + private storageMap: StorageMap = {} + + private bailOnViolation: boolean = false + + constructor ( + readonly entryPointAddress: string, + readonly senderCreatorAddress: string, + readonly nonceManagerAddress?: string + ) {} + + /** + * Analyzes the tracing results for the given UserOperation. + * Throws an exception in case canonical ERC-7562 rule violation was detected. + * + * In order to get a list of violated rules use {@link parseResults} directly. + * @returns {@link ERC7562ValidationResults} containing addresses and storage slots accessed by the UserOperation. + */ + requireCompliance ( + userOp: OperationBase, + erc7562Call: ERC7562Call, + validationResult: ValidationResult + ): ERC7562ValidationResults { + this.bailOnViolation = true + const results = this.parseResults(userOp, erc7562Call, validationResult) + this.bailOnViolation = false + return results + } + + /** + * Analyzes the tracing results for the given UserOperation. + * + * Unlike {@link requireCompliance}, does not throw an exception in case a rule violation was detected. + * + * @returns {@link ERC7562ValidationResults} containing addresses and storage slots accessed by the UserOperation, + * @returns an array of ERC-7562 rules that were violated by the UserOperation. + */ + parseResults ( + userOp: OperationBase, + erc7562Call: ERC7562Call, + validationResult: ValidationResult + ): ERC7562ValidationResults { + this._init(erc7562Call) + if (erc7562Call.calls == null || erc7562Call.calls.length < 1) { + throw new Error('Unexpected traceCall result: no calls from entrypoint.') + } + this.stakeValidationResult = validationResult + this._innerStepRecursive(userOp, erc7562Call, 0) + return { + contractAddresses: this.contractAddresses, + ruleViolations: this.ruleViolations, + storageMap: this.storageMap + } + } + + private _init (erc7562Call: ERC7562Call): void { + this.keccak = erc7562Call.keccak ?? [] + this.ruleViolations = [] + this.currentEntity = AccountAbstractionEntity.none + this.currentEntityAddress = '' + this.contractAddresses = [] + this.storageMap = {} + } + + private _isCallToEntryPoint (erc7562Call: ERC7562Call): boolean { + return erc7562Call.to?.toLowerCase() === this.entryPointAddress?.toLowerCase() && + erc7562Call.from?.toLowerCase() !== this.entryPointAddress?.toLowerCase() && + // skipping the top-level call from address(0) to 'simulateValidations()' + erc7562Call.from?.toLowerCase() !== AddressZero + } + + private _isEntityStaked (entity?: AccountAbstractionEntity): boolean { + let entStake: StakeInfo | undefined + switch (entity ?? this.currentEntity) { + case AccountAbstractionEntity.account: + entStake = this.stakeValidationResult.senderInfo + break + case AccountAbstractionEntity.factory: + entStake = this.stakeValidationResult.factoryInfo + break + case AccountAbstractionEntity.paymaster: + entStake = this.stakeValidationResult.paymasterInfo + break + default: + break + } + return entStake != null && BigNumber.from(1).lte(entStake.stake) && BigNumber.from(1).lte(entStake.unstakeDelaySec) + } + + private _associatedWith (slot: string, addr: string, entitySlots: { [addr: string]: Set }): boolean { + const addrPadded = hexZeroPad(addr, 32).toLowerCase() + if (slot === addrPadded) { + return true + } + const k = entitySlots[addr] + if (k == null) { + return false + } + const slotN = BigNumber.from(slot) + // scan all slot entries to check of the given slot is within a structure, starting at that offset. + // assume a maximum size on a (static) structure size. + for (const k1 of k.keys()) { + const kn = BigNumber.from(k1) + if (slotN.gte(kn) && slotN.lt(kn.add(128))) { + return true + } + } + return false + } + + private _tryDetectKnownMethod (erc7562Call: ERC7562Call): string { + const mergedAbi = Object.values([ + ...RIP7560EntryPointABI, + ...SenderCreator__factory.abi, + ...IEntryPoint__factory.abi, + ...IPaymaster__factory.abi + ]) + const AbiInterfaces = new Interface(mergedAbi) + const methodSig = erc7562Call.input.slice(0, 10) + try { + const abiFunction: FunctionFragment = AbiInterfaces.getFunction(methodSig) + return abiFunction.name + } catch (_) {} + return methodSig + } + + private _violationDetected (violation: ERC7562Violation): void { + this.ruleViolations.push(violation) + if (this.bailOnViolation) { + throw toError(violation) + } + } + + private _detectEntityChange (userOp: OperationBase, erc7562Call: ERC7562Call): void { + if ( + erc7562Call.from.toLowerCase() !== AddressZero && + erc7562Call.from.toLowerCase() !== this.entryPointAddress.toLowerCase() && + erc7562Call.from.toLowerCase() !== this.senderCreatorAddress.toLowerCase()) { + return + } + const nonceManagerAddress = this.nonceManagerAddress + if (userOp.sender.toLowerCase() === erc7562Call.to.toLowerCase()) { + this.currentEntity = AccountAbstractionEntity.account + this.currentEntityAddress = userOp.sender + } else if ( + erc7562Call.from.toLowerCase() === this.senderCreatorAddress.toLowerCase() && + userOp.factory?.toLowerCase() === erc7562Call.to.toLowerCase() + ) { + this.currentEntity = AccountAbstractionEntity.factory + this.currentEntityAddress = userOp.factory + } else if (userOp.paymaster?.toLowerCase() === erc7562Call.to.toLowerCase()) { + this.currentEntity = AccountAbstractionEntity.paymaster + this.currentEntityAddress = userOp.paymaster + } else if (this.entryPointAddress.toLowerCase() === erc7562Call.to.toLowerCase()) { + this.currentEntity = AccountAbstractionEntity.entryPoint + this.currentEntityAddress = this.entryPointAddress + } else if (this.senderCreatorAddress.toLowerCase() === erc7562Call.to.toLowerCase()) { + this.currentEntity = AccountAbstractionEntity.senderCreator + this.currentEntityAddress = this.senderCreatorAddress + } else if ( + nonceManagerAddress != null && + nonceManagerAddress.toLowerCase() === erc7562Call.to.toLowerCase() + ) { + this.currentEntity = AccountAbstractionEntity.nativeNonceManager + this.currentEntityAddress = nonceManagerAddress + } else { + throw new RpcError(`could not find entity name for address ${erc7562Call.to}. This should not happen. This is a bug.`, 0) + } + } + + private _tryGetAddressName (userOp: OperationBase, address: string): AccountAbstractionEntity | string { + const lowerAddress = address.toLowerCase() + if (lowerAddress === userOp.sender.toLowerCase()) { + return AccountAbstractionEntity.account + } else if (userOp.factory?.toLowerCase() === lowerAddress) { + return AccountAbstractionEntity.factory + } else if (userOp.paymaster?.toLowerCase() === lowerAddress) { + return AccountAbstractionEntity.paymaster + } else if (this.entryPointAddress.toLowerCase() === lowerAddress) { + return AccountAbstractionEntity.entryPoint + } else if (this.senderCreatorAddress.toLowerCase() === lowerAddress) { + return AccountAbstractionEntity.senderCreator + } + return address + } + + /** + * Calculate storage slots associated with each entity. + * keccak( A || ...) is associated with "A" + * + * @param userOp + */ + private _parseEntitySlots ( + userOp: OperationBase + ): { + [addr: string]: Set + } { + // for each entity (sender, factory, paymaster), hold the valid slot addresses + const entityAddresses = [userOp.sender.toLowerCase(), userOp.paymaster?.toLowerCase(), userOp.factory?.toLowerCase()] + const entitySlots: { [addr: string]: Set } = {} + + for (const keccakInput of this.keccak) { + for (const entityAddress of entityAddresses) { + if (entityAddress == null) { + continue + } + const addrPadded = toBytes32(entityAddress) + // valid slot: the slot was generated by keccak(entityAddr || ...) + if (keccakInput.startsWith(addrPadded)) { + if (entitySlots[entityAddress] == null) { + entitySlots[entityAddress] = new Set() + } + entitySlots[entityAddress].add(keccak256(keccakInput)) + } + } + } + return entitySlots + } + + private _innerStepRecursive ( + userOp: OperationBase, + erc7562Call: ERC7562Call, + recursionDepth: number + ): void { + const address = erc7562Call.to + this.contractAddresses.push(address) + this._detectEntityChange(userOp, erc7562Call) + this._checkOp011(erc7562Call, recursionDepth) + this._checkOp020(erc7562Call, recursionDepth) + this._checkOp031(userOp, erc7562Call, recursionDepth) + this._checkOp041(userOp, erc7562Call, recursionDepth) + this._checkOp054(erc7562Call, recursionDepth) + this._checkOp054ExtCode(erc7562Call, address, recursionDepth) + this._checkOp061(erc7562Call, recursionDepth) + this._checkOp080(erc7562Call, recursionDepth) + this._checkStorage(userOp, erc7562Call, recursionDepth) + for (const call of erc7562Call.calls ?? []) { + this._innerStepRecursive(userOp, call, recursionDepth + 1) + } + } + + /** + * OP-011: Blocked opcodes + * OP-080: `BALANCE` (0x31) and `SELFBALANCE` (0x47) are allowed only from a staked entity, else they are blocked + */ + private _checkOp011 (erc7562Call: ERC7562Call, recursionDepth: number): void { + if (erc7562Call.to.toLowerCase() === this.entryPointAddress.toLowerCase()) { + // Currently inside the EntryPoint deposit code, no access control applies here + return + } + const opcodes = erc7562Call.usedOpcodes + const bannedOpCodeUsed = + Object + .keys(opcodes) + .map((opcode: string) => { + return getOpcodeName(parseInt(opcode)) ?? '' + }) + .filter((opcode: string) => { + return bannedOpCodes.has(opcode) + }) + bannedOpCodeUsed.forEach( + (opcode: string): void => { + this._violationDetected({ + rule: ERC7562Rule.op011, + depth: recursionDepth, + entity: this.currentEntity, + address: erc7562Call.from, + opcode, + value: '0', + errorCode: ValidationErrors.OpcodeValidation, + description: `${this.currentEntity.toString()} uses banned opcode: ${opcode.toString()}` + }) + } + ) + } + + /** + * OP-020: Revert on "out of gas" is forbidden as it can "leak" the gas limit or the current call stack depth. + */ + private _checkOp020 (erc7562Call: ERC7562Call, recursionDepth: number): void { + if (erc7562Call.outOfGas) { + this._violationDetected({ + rule: ERC7562Rule.op020, + // TODO: fill in depth, entity + depth: recursionDepth, + entity: this.currentEntity, + address: erc7562Call.from, + opcode: erc7562Call.type, + value: '0', + errorCode: ValidationErrors.OpcodeValidation, + description: `${this.currentEntity.toString()} internally reverts on oog` + }) + } + } + + /** + * OP-031: CREATE2 is allowed exactly once in the deployment phase and must deploy code for the "sender" address + */ + private _checkOp031 ( + userOp: OperationBase, + erc7562Call: ERC7562Call, + recursionDepth: number + ): void { + if ( + erc7562Call.type !== 'CREATE' && + erc7562Call.type !== 'CREATE2' + ) { + return + } + const isFactoryStaked = this._isEntityStaked(AccountAbstractionEntity.factory) + const isAllowedCreateByOP032 = + userOp.factory != null && + erc7562Call.type === 'CREATE' && + this.currentEntity === AccountAbstractionEntity.account && + erc7562Call.from.toLowerCase() === userOp.sender.toLowerCase() + const isAllowedCreateByEREP060 = + ( + erc7562Call.from.toLowerCase() === userOp.sender?.toLowerCase() || + erc7562Call.from.toLowerCase() === userOp.factory?.toLowerCase() + ) && + isFactoryStaked + const isAllowedCreateSenderByFactory = + this.currentEntity === AccountAbstractionEntity.factory && + erc7562Call.to.toLowerCase() === userOp.sender.toLowerCase() + if (!(isAllowedCreateByOP032 || isAllowedCreateByEREP060 || isAllowedCreateSenderByFactory)) { + this._violationDetected({ + rule: ERC7562Rule.op011, + depth: recursionDepth, + entity: this.currentEntity, + address: erc7562Call.from ?? 'n/a', + opcode: 'CREATE2', + value: '0', + errorCode: ValidationErrors.OpcodeValidation, + description: `${this.currentEntity.toString()} uses banned opcode: CREATE2` + }) + } + } + + /** + * OP-041: Access to an address without a deployed code is forbidden for EXTCODE* and *CALL opcodes + */ + private _checkOp041 ( + userOp: OperationBase, + erc7562Call: ERC7562Call, + recursionDepth: number + ): void { + // the only contract we allow to access before its deployment is the "sender" itself, which gets created. + let illegalZeroCodeAccess: any + for (const addr of Object.keys(erc7562Call.contractSize)) { + // [OP-042] + if ( + addr.toLowerCase() !== userOp.sender.toLowerCase() && + // addr.toLowerCase() !== AA_ENTRY_POINT && + addr.toLowerCase() !== this.entryPointAddress.toLowerCase() && + erc7562Call.contractSize[addr].contractSize <= 2) { + illegalZeroCodeAccess = erc7562Call.contractSize[addr] + illegalZeroCodeAccess.address = addr + this._violationDetected({ + address: '', + depth: recursionDepth, + entity: this.currentEntity, + rule: ERC7562Rule.op041, + errorCode: ValidationErrors.OpcodeValidation, + description: `${this.currentEntity.toString()} accesses un-deployed contract address ${illegalZeroCodeAccess?.address as string} with opcode ${illegalZeroCodeAccess?.opcode as string}` + }) + } + } + } + + /** + * OP-052: May call `depositTo(sender)` with any value from either the `sender` or `factory`. + * OP-053: May call the fallback function from the `sender` with any value. + * OP-054: Any other access to the EntryPoint is forbidden. + */ + private _checkOp054 ( + erc7562Call: ERC7562Call, + recursionDepth: number + ): void { + const isCallToEntryPoint = this._isCallToEntryPoint(erc7562Call) + const knownMethod = this._tryDetectKnownMethod(erc7562Call) + const isEntryPointCallAllowedRIP7560 = knownMethod === 'acceptAccount' || + knownMethod === 'acceptPaymaster' || + knownMethod === 'sigFailAccount' || + knownMethod === 'sigFailPaymaster' + const isEntryPointCallAllowedOP052 = knownMethod === 'depositTo' + const isEntryPointCallAllowedOP053 = knownMethod === '0x' + const isEntryPointCallAllowed = isEntryPointCallAllowedOP052 || + isEntryPointCallAllowedOP053 || + isEntryPointCallAllowedRIP7560 + const isRuleViolated = isCallToEntryPoint && !isEntryPointCallAllowed + if (isRuleViolated) { + this._violationDetected({ + rule: ERC7562Rule.op054, + depth: recursionDepth, + entity: this.currentEntity, + address: erc7562Call.from, + opcode: erc7562Call.type, + value: erc7562Call.value, + errorCode: ValidationErrors.OpcodeValidation, + description: `illegal call into EntryPoint during validation ${knownMethod}` + }) + } + } + + private _checkOp054ExtCode ( + erc7562Call: ERC7562Call, + address: string, + recursionDepth: number + ): void { + for (const addr of erc7562Call.extCodeAccessInfo) { + if (addr.toLowerCase() === this.entryPointAddress.toLowerCase()) { + this._violationDetected({ + address, + depth: recursionDepth, + entity: this.currentEntity, + errorCode: ValidationErrors.OpcodeValidation, + rule: ERC7562Rule.op054, + description: `${this.currentEntity} accesses EntryPoint contract address ${this.entryPointAddress} with EXTCODE* opcode` + }) + } + } + } + + /** + * OP-061: CALL with value is forbidden. The only exception is a call to the EntryPoint. + */ + private _checkOp061 ( + erc7562Call: ERC7562Call, + recursionDepth: number + ): void { + const isIllegalNonZeroValueCall = + !this._isCallToEntryPoint(erc7562Call) && + !BigNumber.from(erc7562Call.value ?? 0).eq(0) + if (isIllegalNonZeroValueCall) { + this._violationDetected({ + rule: ERC7562Rule.op061, + depth: recursionDepth, + entity: this.currentEntity, + address: erc7562Call.from, + opcode: erc7562Call.type, + value: erc7562Call.value, + errorCode: ValidationErrors.OpcodeValidation, + description: 'May not may CALL with value' + }) + } + } + + /** + * OP-080: BALANCE (0x31) and SELFBALANCE (0x47) are allowed only from a staked entity, else they are blocked + */ + private _checkOp080 ( + erc7562Call: ERC7562Call, + recursionDepth: number + ): void { + const opcodes = erc7562Call.usedOpcodes + const isEntityStaked = this._isEntityStaked() + const onlyStakedOpCodeUsed = + Object + .keys(opcodes) + .map((opcode: string) => { + return getOpcodeName(parseInt(opcode)) ?? '' + }) + .filter((opcode: string) => { + return opcodesOnlyInStakedEntities.has(opcode) && !isEntityStaked + }) + onlyStakedOpCodeUsed + .forEach( + (opcode: string): void => { + this._violationDetected({ + rule: ERC7562Rule.op011, + // TODO: fill in depth, entity + depth: recursionDepth, + entity: this.currentEntity, + address: erc7562Call.from ?? 'n/a', + opcode, + value: '0', + errorCode: ValidationErrors.OpcodeValidation, + description: `unstaked ${this.currentEntity.toString()} uses banned opcode: ${opcode}` + }) + } + ) + } + + private _checkStorage ( + userOp: OperationBase, + erc7562Call: ERC7562Call, + recursionDepth: number + ): void { + if ( + erc7562Call.to.toLowerCase() === this.entryPointAddress.toLowerCase() || + erc7562Call.to.toLowerCase() === this.nonceManagerAddress?.toLowerCase() + ) { + // Currently inside system code, no access control applies here + return + } + const allSlots: string[] = [ + ...Object.keys(erc7562Call.accessedSlots.writes ?? {}), + ...Object.keys(erc7562Call.accessedSlots.reads ?? {}), + ...Object.keys(erc7562Call.accessedSlots.transientWrites ?? {}), + ...Object.keys(erc7562Call.accessedSlots.transientReads ?? {}) + ] + const address: string = erc7562Call.to + const entitySlots = this._parseEntitySlots(userOp) + const addressName = this._tryGetAddressName(userOp, address) + const isEntityStaked = this._isEntityStaked() + const isFactoryStaked = this._isEntityStaked(AccountAbstractionEntity.factory) + const isSenderCreation = userOp.factory != null + for (const slot of allSlots) { + if (this.storageMap[address] == null) { + this.storageMap[address] = {} + } + (this.storageMap[address] as SlotMap)[slot] = '' // TODO: not clear why were the values relevant + const isSenderInternalSTO010: boolean = address.toLowerCase() === userOp.sender.toLowerCase() + const isSenderAssociated: boolean = this._associatedWith(slot, userOp.sender.toLowerCase(), entitySlots) + const isEntityInternalSTO031: boolean = address.toLowerCase() === this.currentEntityAddress.toLowerCase() + const isEntityAssociatedSTO032: boolean = this._associatedWith(slot, this.currentEntityAddress.toLowerCase(), entitySlots) + const isReadOnlyAccessSTO033: boolean = erc7562Call.accessedSlots.writes?.[slot] == null && erc7562Call.accessedSlots.transientWrites?.[slot] == null + + const isAllowedIfEntityStaked = isEntityInternalSTO031 || isEntityAssociatedSTO032 || isReadOnlyAccessSTO033 + const isAllowedST031ST032ST033: boolean = isAllowedIfEntityStaked && isEntityStaked + + const isAllowedSTO021: boolean = isSenderAssociated && !isSenderCreation + const isAllowedIfFactoryStaked = isSenderAssociated && isSenderCreation + const isAllowedSTO022: boolean = isAllowedIfFactoryStaked && isFactoryStaked + const allowed = isSenderInternalSTO010 || isAllowedSTO021 || isAllowedSTO022 || isAllowedST031ST032ST033 + if (!allowed) { + let description: string + if ( + (isAllowedIfEntityStaked && !isEntityStaked) || + (isAllowedIfFactoryStaked && !isFactoryStaked) + ) { + description = `unstaked ${this.currentEntity.toString()} accessed ${addressName} slot ${slot}` + } else { + const isWrite = Object.keys(erc7562Call.accessedSlots.writes ?? {}).includes(slot) || Object.keys(erc7562Call.accessedSlots.transientWrites ?? {}).includes(slot) + const isTransient = Object.keys(erc7562Call.accessedSlots.transientReads ?? {}).includes(slot) || Object.keys(erc7562Call.accessedSlots.transientWrites ?? {}).includes(slot) + const readWrite = isWrite ? 'write to' : 'read from' + const transientStr = isTransient ? 'transient ' : '' + description = `${this.currentEntity.toString()} has forbidden ${readWrite} ${transientStr}${addressName} slot ${slot}` + } + this._violationDetected({ + address: '', + depth: recursionDepth, + entity: this.currentEntity, + errorCode: ValidationErrors.OpcodeValidation, + rule: ERC7562Rule.sto010, + description + }) + } + } + } +} diff --git a/packages/validation-manager/src/ERC7562Violation.ts b/packages/validation-manager/src/ERC7562Violation.ts new file mode 100644 index 00000000..fc78d22b --- /dev/null +++ b/packages/validation-manager/src/ERC7562Violation.ts @@ -0,0 +1,21 @@ +import { RpcError, ValidationErrors } from '@account-abstraction/utils' + +import { ERC7562Rule } from './enum/ERC7562Rule' +import { AccountAbstractionEntity } from './AccountAbstractionEntity' + +export interface ERC7562Violation { + rule: ERC7562Rule + depth: number + entity: AccountAbstractionEntity + address: string + errorCode: ValidationErrors + description: string + conflict?: string + opcode?: string + value?: string + slot?: string +} + +export function toError (violation: ERC7562Violation): Error { + return new RpcError(violation.description, violation.errorCode) +} diff --git a/packages/validation-manager/src/ValidationManager.ts b/packages/validation-manager/src/ValidationManager.ts index c6e92d8a..5a2a0438 100644 --- a/packages/validation-manager/src/ValidationManager.ts +++ b/packages/validation-manager/src/ValidationManager.ts @@ -23,21 +23,23 @@ import { decodeErrorReason, decodeRevertReason, getAddr, + getAuthorizationList, getEip7702AuthorizationSigner, mergeValidationDataValues, packUserOp, requireAddressAndFields, requireCond, - runContractScript, getAuthorizationList, SenderCreator__factory, IEntryPoint__factory, IPaymaster__factory + runContractScript } from '@account-abstraction/utils' -import { tracerResultParser } from './TracerResultParser' -import { bundlerCollectorTracer, BundlerTracerResult, ExitInfo } from './BundlerCollectorTracer' import { debug_traceCall } from './GethTracer' import EntryPointSimulationsJson from '@account-abstraction/contracts/artifacts/EntryPointSimulations.json' import { IValidationManager, ValidateUserOpResult, ValidationResult } from './IValidationManager' -import { Interface } from 'ethers/lib/utils' +import { ERC7562Parser } from './ERC7562Parser' +import { ERC7562Call } from './ERC7562Call' +import { bundlerCollectorTracer, BundlerTracerResult } from './BundlerCollectorTracer' +import { tracerResultParser } from './TracerResultParser' const debug = Debug('aa.mgr.validate') @@ -57,10 +59,12 @@ const entryPointSimulations = IEntryPointSimulations__factory.createInterface() */ export class ValidationManager implements IValidationManager { private readonly provider: JsonRpcProvider + constructor ( readonly entryPoint: IEntryPoint, readonly unsafe: boolean, readonly preVerificationGasCalculator: PreVerificationGasCalculator, + readonly erc7562Parser: ERC7562Parser, readonly providerForTracer?: JsonRpcProvider ) { this.provider = this.entryPoint.provider as JsonRpcProvider @@ -141,7 +145,7 @@ export class ValidationManager implements IValidationManager { async _geth_traceCall_SimulateValidation ( operation: OperationBase, stateOverride: { [address: string]: { code: string } } - ): Promise<[ValidationResult, BundlerTracerResult]> { + ): Promise<[ValidationResult, ERC7562Call | null, BundlerTracerResult | null]> { const userOp = operation as UserOperation const provider = this.entryPoint.provider as JsonRpcProvider const simulateCall = entryPointSimulations.encodeFunctionData('simulateValidation', [packUserOp(userOp)]) @@ -174,7 +178,7 @@ export class ValidationManager implements IValidationManager { if (!this.usingErc7562NativeTracer()) { // Using preState tracer + JS tracer const lastResult = tracerResult.calls.slice(-1)[0] - data = (lastResult as ExitInfo).data + data = (lastResult).data if (lastResult.type === 'REVERT') { throw new RpcError(decodeRevertReason(data, false) as string, ValidationErrors.SimulateValidation) } @@ -200,7 +204,11 @@ export class ValidationManager implements IValidationManager { ) // console.log('==debug=', ...tracerResult.numberLevels.forEach(x=>x.access), 'sender=', userOp.sender, 'paymaster=', hexlify(userOp.paymasterAndData)?.slice(0, 42)) // errorResult is "ValidationResult" - return [validationResult, tracerResult] + if (this.usingErc7562NativeTracer()) { + return [validationResult, tracerResult, null] + } else { + return [validationResult, null, tracerResult as BundlerTracerResult] + } } catch (e: any) { // if already parsed, throw as is if (e.code != null) { @@ -256,19 +264,21 @@ export class ValidationManager implements IValidationManager { const stateOverrideForEip7702 = await this.getAuthorizationsStateOverride(authorizationList) let storageMap: StorageMap = {} if (!this.unsafe) { - let tracerResult: BundlerTracerResult - [res, tracerResult] = await this._geth_traceCall_SimulateValidation(userOp, stateOverrideForEip7702).catch(e => { + let erc7562Call: ERC7562Call | null + let bundlerTracerResult: BundlerTracerResult | null + [res, erc7562Call, bundlerTracerResult] = await this._geth_traceCall_SimulateValidation(userOp, stateOverrideForEip7702).catch(e => { throw e }) - // console.log('validation res', res) - // todo fix - if (this.usingErc7562NativeTracer()) { - this.convertTracerResult(tracerResult, userOp) - } // console.log('tracer res') // console.dir(tracerResult, { depth: null }) let contractAddresses: string[] - [contractAddresses, storageMap] = tracerResultParser(userOp, tracerResult, res, this.entryPoint.address) + if (erc7562Call != null) { + ({ contractAddresses, storageMap } = this.erc7562Parser.requireCompliance(userOp, erc7562Call, res)) + } else if (bundlerTracerResult != null) { + [contractAddresses, storageMap] = tracerResultParser(userOp, bundlerTracerResult, res, this.entryPoint.address) + } else { + throw new Error('Tracer result is null for both legacy and modern parser') + } // if no previous contract hashes, then calculate hashes of contracts if (previousCodeHashes == null) { codeHashes = await this.getCodeHashes(contractAddresses) @@ -406,95 +416,6 @@ export class ValidationManager implements IValidationManager { return await this.entryPoint.getUserOpHash(packUserOp(userOp as UserOperation)) } - // todo fix rest of the code to work with the new tracer result instead of adjusting it here - convertTracerResult (tracerResult: any, userOp: UserOperation): BundlerTracerResult { - const SENDER_CREATOR = '0xefc2c1444ebcc4db75e7613d20c6a62ff67a167c'.toLowerCase() - // Before flattening we add top level addresses for calls from EntryPoint and from SENDER_CREATOR - tracerResult.calls.forEach((call: {calls: any, to: any, topLevelTargetAddress: any}) => { - call.topLevelTargetAddress = call.to - if (call.to.toLowerCase() === SENDER_CREATOR && call.calls != null) { - call.calls.forEach((subcall: any) => { - subcall.topLevelTargetAddress = subcall.to - }) - } - }) - tracerResult.calls = this.flattenCalls(tracerResult.calls) - tracerResult.calls.forEach((call: { topLevelTargetAddress: any, method: any, input: any, to: any, from: any, opcodes: any, usedOpcodes: any, access: any, accessedSlots: any, extCodeAccessInfo: any, outOfGas: any, oog: any }) => { - call.opcodes = {} - if (call.usedOpcodes != null) { - Object.keys(call.usedOpcodes).forEach((opcode: string) => { - call.opcodes[this.getOpcodeName(parseInt(opcode))] = call.usedOpcodes[opcode] - }) - } - - if (call.access == null) { - call.access = {} - } - if (call.accessedSlots != null) { - call.access[call.to] = { - reads: call.accessedSlots.reads ?? {}, - writes: call.accessedSlots.writes ?? {}, - transientReads: call.accessedSlots.transientReads ?? {}, - transientWrites: call.accessedSlots.transientWrites ?? {} - } - Object.keys(call.access[call.to].reads).forEach((slot) => { - if (call.access[call.to].reads[slot] != null && call.access[call.to].reads[slot].length > 0) { - call.access[call.to].reads[slot] = call.access[call.to].reads[slot][0] - } - }) - } - if (call.extCodeAccessInfo == null) { - call.extCodeAccessInfo = {} - } - const newExtCode: any = {} - if (Array.isArray(call.extCodeAccessInfo)) { - call.extCodeAccessInfo.forEach((addr: any) => { - newExtCode[addr] = 1 - }) - } - call.extCodeAccessInfo = newExtCode - call.oog = call.outOfGas - - // Adding method name - if (call.topLevelTargetAddress == null && call.to.toLowerCase() === this.entryPoint.address.toLowerCase()) { - if (call.input.length <= 2) { - call.method = '0x' - } else { - const mergedAbi = Object.values([ - ...SenderCreator__factory.abi, - ...IEntryPoint__factory.abi, - ...IPaymaster__factory.abi - ].reduce((set, entry) => { - const key = `${entry.name}(${entry.inputs.map(i => i.type).join(',')})` - // console.log('key=', key, keccak256(Buffer.from(key)).slice(0,10)) - return { - ...set, - [key]: entry - } - }, {})) as any - const AbiInterfaces = new Interface(mergedAbi) - - function callCatch (x: () => T, def: T1): T | T1 { - try { - return x() - } catch { - return def - } - } - const methodSig = call.input.slice(0, 10) - const method = callCatch(() => AbiInterfaces.getFunction(methodSig), methodSig) - call.method = method.name - } - } - }) - // TODO: This is a hardcoded address of SenderCreator immutable member in EntryPoint. Any change in EntryPoint's code - // requires a change of this address. - // TODO check why the filter fails test_ban_user_op_access_other_ops_sender_in_bundle - tracerResult.callsFromEntryPoint = tracerResult.calls // .filter((call: { from: string }) => call.from.toLowerCase() === this.entryPoint.address.toLowerCase() || call.from.toLowerCase() === SENDER_CREATOR) - - return tracerResult - } - flattenCalls (calls: any[]): any[] { return calls.reduce((acc: any, call: any) => { acc.push(call) // Add the current call to the accumulator diff --git a/packages/validation-manager/src/ValidationManagerRIP7560.ts b/packages/validation-manager/src/ValidationManagerRIP7560.ts index 4b1e3351..825fe59a 100644 --- a/packages/validation-manager/src/ValidationManagerRIP7560.ts +++ b/packages/validation-manager/src/ValidationManagerRIP7560.ts @@ -15,15 +15,18 @@ import { PreVerificationGasCalculatorConfig } from '@account-abstraction/sdk' import { IValidationManager, ValidateUserOpResult, ValidationResult } from './IValidationManager' import { eth_traceRip7560Validation } from './GethTracer' -import { tracerResultParser } from './TracerResultParser' +import { ERC7562Parser } from './ERC7562Parser' export const AA_ENTRY_POINT = '0x0000000000000000000000000000000000007560' +export const AA_SENDER_CREATOR = '0x00000000000000000000000000000000ffff7560' export const AA_STAKE_MANAGER = '0x570Aa568b6cf62ff08c6C3a3b3DB1a0438E871Fb' +export const AA_NONCE_MANAGER = '0x632fafb21910d6c8b4a3995063dd984f2b829c02' export class ValidationManagerRIP7560 implements IValidationManager { constructor ( readonly stakeManager: IRip7560StakeManager, readonly provider: JsonRpcProvider, + readonly erc7562Parser: ERC7562Parser, readonly unsafe: boolean ) { } @@ -101,7 +104,7 @@ export class ValidationManagerRIP7560 implements IValidationManager { // this.parseValidationTracingResult(traceResult) // let contractAddresses: string[] // [contractAddresses, storageMap] = - tracerResultParser(operation, traceResult, validationResult, AA_ENTRY_POINT) + this.erc7562Parser.requireCompliance(operation, traceResult, validationResult) // TODO alex shahaf handle codehashes // if no previous contract hashes, then calculate hashes of contracts if (previousCodeHashes == null) { diff --git a/packages/validation-manager/src/altmempool/AltMempoolConfig.ts b/packages/validation-manager/src/altmempool/AltMempoolConfig.ts new file mode 100644 index 00000000..1f422ea6 --- /dev/null +++ b/packages/validation-manager/src/altmempool/AltMempoolConfig.ts @@ -0,0 +1,49 @@ +import { ERC7562Rule } from '../enum/ERC7562Rule' + +type Role = 'sender' | 'paymaster' | 'factory' + +type EnterOpcode = 'CALL' | 'DELEGATECALL' | 'CALLCODE' | 'STATICCALL' | 'CREATE' | 'CREATE2' + +export interface AltMempoolRuleExceptionBase { + role?: Role + address?: string + depths?: number[] + enterOpcode?: EnterOpcode[] + enterMethodSelector?: `0x${string}` +} + +export interface AltMempoolRuleExceptionBannedOpcode extends AltMempoolRuleExceptionBase { + opcodes: string[] + slots: Array<`0x${string}`> +} + +type RuleException = `0x${string}` | Role | AltMempoolRuleExceptionBase | AltMempoolRuleExceptionBannedOpcode + +export interface BaseAltMempoolRule { + enabled?: boolean + exceptions?: RuleException[] +} + +export interface AltMempoolConfig { + [mempoolId: number]: { [rule in ERC7562Rule]?: BaseAltMempoolRule } +} + +const config: AltMempoolConfig = { + 1: { + [ERC7562Rule.erep010]: { + enabled: true, + exceptions: [ + 'sender', + '0xdeadbeef', + { + depths: [3], + enterOpcode: ['CALL'], + opcodes: ['SSTORE', 'SLOAD'], + slots: ['0xdeadbeef'] + } + ] + } + } +} + +console.log(config) diff --git a/packages/validation-manager/src/enum/ERC7562Rule.ts b/packages/validation-manager/src/enum/ERC7562Rule.ts new file mode 100644 index 00000000..d36a4301 --- /dev/null +++ b/packages/validation-manager/src/enum/ERC7562Rule.ts @@ -0,0 +1,37 @@ +export enum ERC7562Rule { + op011 = 'OP-011', + op012 = 'OP-012', + op013 = 'OP-013', + op020 = 'OP-020', + op031 = 'OP-031', + op041 = 'OP-041', + op042 = 'OP-042', + op051 = 'OP-051', + op052 = 'OP-052', + op053 = 'OP-053', + op054 = 'OP-054', + op061 = 'OP-061', + op062 = 'OP-062', + op070 = 'OP-070', + op080 = 'OP-080', + cod010 = 'COD-010', + sto010 = 'STO-010', + sto021 = 'STO-021', + sto022 = 'STO-022', + sto031 = 'STO-031', + sto032 = 'STO-032', + sto033 = 'STO-033', + sto040 = 'STO-040', + sto041 = 'STO-041', + grep010 = 'GREP-010', + grep020 = 'GREP-020', + grep040 = 'GREP-040', + grep050 = 'GREP-050', + srep010 = 'SREP-010', + srep040 = 'SREP-040', + erep010 = 'EREP-010', + erep015 = 'EREP-015', + erep020 = 'EREP-020', + erep030 = 'EREP-030', + erep040 = 'EREP-040' +} diff --git a/packages/validation-manager/src/enum/EVMOpcodes.ts b/packages/validation-manager/src/enum/EVMOpcodes.ts new file mode 100644 index 00000000..af38b960 --- /dev/null +++ b/packages/validation-manager/src/enum/EVMOpcodes.ts @@ -0,0 +1,44 @@ +export enum EVMOpcodes { + ADDRESS = 0x30, + BALANCE = 0x31, + ORIGIN = 0x32, + GASPRICE = 0x3A, + BLOCKHASH = 0x40, + COINBASE = 0x41, + TIMESTAMP = 0x42, + NUMBER = 0x43, + DIFFICULTY = 0x44, + GASLIMIT = 0x45, + SELFBALANCE = 0x47, + BASEFEE = 0x48, + BLOBHASH = 0x49, + BLOBBASEFEE = 0x4A, + GAS = 0x5A, + CREATE = 0xF0, + SELFDESTRUCT = 0xFF, + EXTCODESIZE = 0x3B, + EXTCODECOPY = 0x3C, + EXTCODEHASH = 0x3F, + CHAINID = 0x46, + MLOAD = 0x51, + MSTORE = 0x52, + MSTORE8 = 0x53, + SLOAD = 0x54, + SSTORE = 0x55, + JUMPDEST = 0x5B, + TLOAD = 0x5C, + TSTORE = 0x5D, + MCOPY = 0x5E, + PUSH0 = 0x5F, + CALL = 0xF1, + RETURN = 0xF3, + DELEGATECALL = 0xF4, + CREATE2 = 0xF5, + STATICCALL = 0xFA, + REVERT = 0xFD, + INVALID = 0xFE, +} + +export function getOpcodeName (value: number): string | undefined { + return Object.entries(EVMOpcodes).find(([_, v]) => v === value)?.[0] +} diff --git a/packages/validation-manager/src/index.ts b/packages/validation-manager/src/index.ts index 3698a76d..bea25cee 100644 --- a/packages/validation-manager/src/index.ts +++ b/packages/validation-manager/src/index.ts @@ -3,19 +3,23 @@ import { JsonRpcProvider } from '@ethersproject/providers' import { AddressZero, IEntryPoint__factory, - OperationRIP7560, UserOperation } from '@account-abstraction/utils' import { PreVerificationGasCalculator } from '@account-abstraction/sdk' -import { bundlerJSTracerName, debug_traceCall, eth_traceRip7560Validation } from './GethTracer' +import { bundlerJSTracerName, debug_traceCall } from './GethTracer' +// @ts-ignore import { bundlerCollectorTracer } from './BundlerCollectorTracer' import { ValidateUserOpResult } from './IValidationManager' import { ValidationManager } from './ValidationManager' +import { ERC7562Parser } from './ERC7562Parser' export * from './ValidationManager' export * from './ValidationManagerRIP7560' export * from './IValidationManager' +export * from './altmempool/AltMempoolConfig' +export * from './enum/ERC7562Rule' +export * from './enum/EVMOpcodes' export async function supportsNativeTracer (provider: JsonRpcProvider, nativeTracer = bundlerJSTracerName): Promise { try { @@ -33,37 +37,8 @@ export async function supportsDebugTraceCall (provider: JsonRpcProvider, rip7560 } if (rip7560) { - // TODO: remove - const defaultsForRip7560Tx: OperationRIP7560 = { - accessList: [], - builderFee: '0x0', - chainId: '0x539', - value: '0x0', - sender: AddressZero, - nonceKey: '0x0', - nonce: '0x0', - executionData: '0x', - callGasLimit: '0x0', - verificationGasLimit: '0x10000', - maxFeePerGas: '0x100000000', - maxPriorityFeePerGas: '0x100000000', - paymaster: AddressZero, - paymasterData: '0x', - factory: AddressZero, - factoryData: '0x', - paymasterVerificationGasLimit: '0x10000', - paymasterPostOpGasLimit: '0x0', - authorizationData: '0x', - authorizationList: [] - }; - - // TODO: align parameter names across 4337 and 7560 - (defaultsForRip7560Tx as any).deployer = defaultsForRip7560Tx.factory; - (defaultsForRip7560Tx as any).deployerData = defaultsForRip7560Tx.factoryData - // make sure we can trace a call. - const ret = await eth_traceRip7560Validation(provider, defaultsForRip7560Tx - ).catch(e => e) - return ret.traceResults != null + // no need to check for the internal RIP-7560 support + return true } // make sure we can trace a call. const ret = await debug_traceCall(provider, @@ -82,10 +57,13 @@ export async function checkRulesViolations ( throw new Error('This provider does not support stack tracing') } const entryPoint = IEntryPoint__factory.connect(entryPointAddress, provider) + const senderCreator = '0xefc2c1444ebcc4db75e7613d20c6a62ff67a167c' + const erc7562Parser = new ERC7562Parser(entryPointAddress, senderCreator) const validationManager = new ValidationManager( entryPoint, false, - Object.assign({}) as PreVerificationGasCalculator + Object.assign({}) as PreVerificationGasCalculator, + erc7562Parser ) return await validationManager.validateUserOp(userOperation) }