Skip to content

Commit

Permalink
Add EntryPointSimulations contract
Browse files Browse the repository at this point in the history
  • Loading branch information
mmv08 committed Jun 21, 2024
1 parent 4305341 commit 65352d4
Show file tree
Hide file tree
Showing 8 changed files with 1,539 additions and 952 deletions.
1 change: 1 addition & 0 deletions modules/4337/contracts/test/Imports.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "@account-abstraction/contracts/core/EntryPointSimulations.sol";
12 changes: 6 additions & 6 deletions modules/4337/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,27 +50,27 @@
"@account-abstraction/contracts": "^0.7.0",
"@noble/curves": "^1.4.0",
"@nomicfoundation/hardhat-ethers": "^3.0.6",
"@nomicfoundation/hardhat-network-helpers": "^1.0.10",
"@nomicfoundation/hardhat-network-helpers": "^1.0.11",
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
"@openzeppelin/contracts": "^5.0.2",
"@safe-global/safe-4337-local-bundler": "workspace:^0.0.0",
"@safe-global/safe-4337-provider": "workspace:^0.0.0",
"@simplewebauthn/server": "10.0.0",
"@types/chai": "^4.3.16",
"@types/mocha": "^10.0.6",
"@types/node": "^20.14.0",
"@types/node": "^20.14.7",
"@types/yargs": "^17.0.32",
"cbor": "^9.0.2",
"debug": "^4.3.4",
"debug": "^4.3.5",
"dotenv": "^16.4.5",
"ethers": "^6.12.1",
"hardhat": "^2.22.3",
"ethers": "^6.13.1",
"hardhat": "^2.22.5",
"hardhat-deploy": "^0.12.4",
"husky": "^9.0.11",
"solc": "^0.8.25",
"solhint": "^5.0.1",
"ts-node": "^10.9.2",
"typescript": "^5.4.5",
"typescript": "^5.5.2",
"yargs": "^17.7.2"
},
"dependencies": {
Expand Down
19 changes: 19 additions & 0 deletions modules/4337/src/deploy/entrypointHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { DeployFunction } from 'hardhat-deploy/types'

const deploy: DeployFunction = async ({ deployments, getNamedAccounts, network }) => {
if (!network.tags.test) {
return
}

const { deployer } = await getNamedAccounts()
const { deploy } = deployments

await deploy('EntryPointSimulations', {
from: deployer,
args: [],
log: true,
deterministicDeployment: true,
})
}

export default deploy
3 changes: 3 additions & 0 deletions modules/4337/src/utils/userOp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ export { PackedUserOperation, UserOperation }
type OptionalExceptFor<T, TRequired extends keyof T = keyof T> = Partial<Pick<T, Exclude<keyof T, TRequired>>> &
Required<Pick<T, TRequired>>

export const PLACEHOLDER_SIGNATURE =
'0x9c8ecb7ad80d2dd4411c8827079cda17095236ee3cba1c9b81153d52af17bc9d0701228dc95a75136a3e3a0130988ba4053cc15d3805db49e2cc08d9c99562191b'

export type SafeUserOperation = {
safe: string
entryPoint: string
Expand Down
20 changes: 16 additions & 4 deletions modules/4337/test/gas/Gas.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { expect } from 'chai'
import { deployments, ethers } from 'hardhat'
import { getSafe4337Module, getEntryPoint, getFactory, getSafeModuleSetup, getSafeL2Singleton } from '../utils/setup'
import {
getSafe4337Module,
getEntryPoint,
getFactory,
getSafeModuleSetup,
getSafeL2Singleton,
getEntryPointSimulations,
} from '../utils/setup'
import { buildSignatureBytes, logUserOperationGas } from '../../src/utils/execution'
import { buildPackedUserOperationFromSafeUserOperation, buildSafeUserOpTransaction, signSafeOp } from '../../src/utils/userOp'
import { chainId } from '../utils/encoding'
import { Safe4337 } from '../../src/utils/safe'
import { estimateUserOperationGas } from '../utils/simulations'

describe('Gas Metering', () => {
const setupTests = deployments.createFixture(async ({ deployments }) => {
Expand All @@ -13,6 +21,7 @@ describe('Gas Metering', () => {

const [user] = await ethers.getSigners()
const entryPoint = await getEntryPoint()
const entryPointSimulations = await getEntryPointSimulations()
const module = await getSafe4337Module()
const proxyFactory = await getFactory()
const proxyCreationCode = await proxyFactory.proxyCreationCode()
Expand All @@ -33,6 +42,7 @@ describe('Gas Metering', () => {
return {
user,
entryPoint,
entryPointSimulations,
validator: module,
safe,
erc20Token,
Expand All @@ -41,8 +51,9 @@ describe('Gas Metering', () => {
})

describe('Safe Deployment + Enabling 4337 Module', () => {
it('Safe with 4337 Module Deployment', async () => {
const { user, entryPoint, validator, safe } = await setupTests()
it.only('Safe with 4337 Module Deployment', async () => {
const { user, entryPoint, entryPointSimulations, validator, safe } = await setupTests()
const entryPointAddress = await entryPoint.getAddress()

// cover the prefund
await user.sendTransaction({ to: safe.address, value: ethers.parseEther('1.0') })
Expand All @@ -61,9 +72,10 @@ describe('Gas Metering', () => {
initCode: safe.getInitCode(),
},
)
const gasEstimation = await estimateUserOperationGas(ethers.provider, entryPointSimulations, safeOp, entryPointAddress)
console.log('Gas Estimation:', gasEstimation)

const signature = buildSignatureBytes([await signSafeOp(user, await validator.getAddress(), safeOp, await chainId())])

const userOp = buildPackedUserOperationFromSafeUserOperation({
safeOp,
signature,
Expand Down
5 changes: 5 additions & 0 deletions modules/4337/test/utils/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ export const getEntryPoint = async () => {
return await ethers.getContractAt('IEntryPoint', EntryPointDeployment.address)
}

export const getEntryPointSimulations = async () => {
const EntryPointDeployment = await deployments.get('EntryPointSimulations')
return await ethers.getContractAt('EntryPointSimulations', EntryPointDeployment.address)
}

export const getSafeAtAddress = async (address: string) => {
return await ethers.getContractAt('SafeMock', address)
}
Expand Down
184 changes: 184 additions & 0 deletions modules/4337/test/utils/simulations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { ethers } from 'ethers'
import { HardhatEthersProvider } from '@nomicfoundation/hardhat-ethers/internal/hardhat-ethers-provider'
import { buildPackedUserOperationFromSafeUserOperation, PLACEHOLDER_SIGNATURE, SafeUserOperation } from '../../src/utils/userOp'
import { EntryPointSimulations } from '../../typechain-types'

export interface GasOverheads {
/**
* fixed overhead for entire handleOp bundle.
*/
fixed: number

/**
* per userOp overhead, added on top of the above fixed per-bundle.
*/
perUserOp: number

/**
* overhead for userOp word (32 bytes) block
*/
perUserOpWord: number

// perCallDataWord: number

/**
* zero byte cost, for calldata gas cost calculations
*/
zeroByte: number

/**
* non-zero byte cost, for calldata gas cost calculations
*/
nonZeroByte: number

/**
* expected bundle size, to split per-bundle overhead between all ops.
*/
bundleSize: number

/**
* expected length of the userOp signature.
*/
sigSize: number
}

/**
* calculate the preVerificationGas of the given UserOperation
* preVerificationGas (by definition) is the cost overhead that can't be calculated on-chain.
* it is based on parameters that are defined by the Ethereum protocol for external transactions.
* @param userOp filled userOp to calculate. The only possible missing fields can be the signature and preVerificationGas itself
* @param overheads gas overheads to use, to override the default values
*/
export const calcPreVerificationGas = (userOp: SafeUserOperation): number => {
const gasOverheads: GasOverheads = {
fixed: 21000,
perUserOp: 18300,
perUserOpWord: 4,
zeroByte: 4,
nonZeroByte: 16,
bundleSize: 1,
sigSize: 65,
}
const op: SafeUserOperation = {
// dummy values, in case the UserOp is incomplete.
...userOp,
preVerificationGas: 21000, // dummy value, just for calldata cost
}

const packed = buildPackedUserOperationFromSafeUserOperation({ safeOp: op, signature: PLACEHOLDER_SIGNATURE })
const encoded = ethers.toBeArray(
ethers.AbiCoder.defaultAbiCoder().encode(
['address', 'uint256', 'bytes', 'bytes', 'bytes32', 'uint256', 'bytes32', 'bytes', 'bytes'],
[
packed.sender,
packed.nonce,
packed.initCode,
packed.callData,
packed.accountGasLimits,
packed.preVerificationGas,
packed.gasFees,
packed.paymasterAndData,
packed.signature,
],
),
)
const lengthInWord = (encoded.length + 31) / 32
const callDataCost = encoded.map((x) => (x === 0 ? gasOverheads.zeroByte : gasOverheads.nonZeroByte)).reduce((sum, x) => sum + x)
const ret = Math.round(
callDataCost + gasOverheads.fixed / gasOverheads.bundleSize + gasOverheads.perUserOp + gasOverheads.perUserOpWord * lengthInWord,
)
return ret
}

export interface EstimateUserOpGasResult {
/**
* the preVerification gas used by this UserOperation.
*/
preVerificationGas: bigint
/**
* gas used for validation of this UserOperation, including account creation
*/
verificationGasLimit: bigint

/**
* (possibly future timestamp) after which this UserOperation is valid
*/
validAfter?: bigint

/**
* the deadline after which this UserOperation is invalid (not a gas estimation parameter, but returned by validation
*/
validUntil?: bigint
/**
* estimated cost of calling the account with the given callData
*/
callGasLimit: bigint
}

export type ExecutionResultStructOutput = [
preOpGas: ethers.BigNumberish,
paid: ethers.BigNumberish,
accountValidationData: ethers.BigNumberish,
paymasterValidationData: ethers.BigNumberish,
targetSuccess: boolean,
targetResult: string,
] & {
preOpGas: ethers.BigNumberish
paid: ethers.BigNumberish
accountValidationData: ethers.BigNumberish
paymasterValidationData: ethers.BigNumberish
targetSuccess: boolean
targetResult: string
}

export const calcVerificationGasAndCallGasLimit = (userOperation: SafeUserOperation, executionResult: ExecutionResultStructOutput) => {
const verificationGasLimit = ((BigInt(executionResult.preOpGas) - BigInt(userOperation.preVerificationGas)) * 3n) / 2n

const gasPrice = BigInt(userOperation.maxFeePerGas)

const calculatedCallGasLimit = BigInt(executionResult.paid) / gasPrice - BigInt(executionResult.preOpGas)

const callGasLimit = (calculatedCallGasLimit > 9000n ? calculatedCallGasLimit : 9000n) + 21000n + 50000n

return { verificationGasLimit, callGasLimit }
}

export const estimateUserOperationGas = async (
provider: HardhatEthersProvider,
entryPointSimulations: EntryPointSimulations,
safeOp: SafeUserOperation,
entryPointAddress: string,
// ): Promise<EstimateUserOpGasResult> => {
) => {
const packedUserOp = buildPackedUserOperationFromSafeUserOperation({ safeOp, signature: PLACEHOLDER_SIGNATURE })
const encodedSimulateHandleOp = entryPointSimulations.interface.encodeFunctionData('simulateHandleOp', [
packedUserOp,
ethers.ZeroAddress,
'0x',
])

const simulationData = await provider.send('eth_call', [
{
to: entryPointAddress,
data: encodedSimulateHandleOp,
},
'latest',
{
[entryPointAddress]: {
code: await entryPointSimulations.getDeployedCode(),
},
},
])
const executionResultStruct = entryPointSimulations.interface.decodeFunctionResult(
'simulateHandleOp',
simulationData,
)[0] as unknown as ExecutionResultStructOutput

console.log({ executionResultStruct })
const { verificationGasLimit, callGasLimit } = calcVerificationGasAndCallGasLimit(safeOp, executionResultStruct)

return {
callGasLimit,
verificationGasLimit,
}
}
Loading

0 comments on commit 65352d4

Please sign in to comment.