From 59d98db0b8c206f58b090b669a67f141771d96eb Mon Sep 17 00:00:00 2001 From: maxvia87 Date: Wed, 4 Oct 2023 09:36:01 +0200 Subject: [PATCH 01/15] chore: add @account-abstraction/contracts to package --- package-lock.json | 11 +++++++++++ package.json | 1 + 2 files changed, 12 insertions(+) diff --git a/package-lock.json b/package-lock.json index 8d1136bc0..5a2881312 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.11.1", "license": "Apache-2.0", "dependencies": { + "@account-abstraction/contracts": "^0.6.0", "@erc725/smart-contracts": "^5.2.0", "@openzeppelin/contracts": "^4.9.2", "@openzeppelin/contracts-upgradeable": "^4.9.2", @@ -57,6 +58,11 @@ "web3": "^1.5.2" } }, + "node_modules/@account-abstraction/contracts": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@account-abstraction/contracts/-/contracts-0.6.0.tgz", + "integrity": "sha512-8ooRJuR7XzohMDM4MV34I12Ci2bmxfE9+cixakRL7lA4BAwJKQ3ahvd8FbJa9kiwkUPCUNtj+/zxDQWYYalLMQ==" + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -22295,6 +22301,11 @@ } }, "dependencies": { + "@account-abstraction/contracts": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@account-abstraction/contracts/-/contracts-0.6.0.tgz", + "integrity": "sha512-8ooRJuR7XzohMDM4MV34I12Ci2bmxfE9+cixakRL7lA4BAwJKQ3ahvd8FbJa9kiwkUPCUNtj+/zxDQWYYalLMQ==" + }, "@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", diff --git a/package.json b/package.json index 47b2e7dd6..b398e32aa 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ }, "homepage": "https://github.com/lukso-network/lsp-smart-contracts#readme", "dependencies": { + "@account-abstraction/contracts": "^0.6.0", "@erc725/smart-contracts": "^5.2.0", "@openzeppelin/contracts": "^4.9.2", "@openzeppelin/contracts-upgradeable": "^4.9.2", From e3bb8ed71290e4f086c5a7853c01f3480c949a13 Mon Sep 17 00:00:00 2001 From: maxvia87 Date: Wed, 4 Oct 2023 11:06:51 +0200 Subject: [PATCH 02/15] feat: create Extension4337 --- contracts/LSP17Extensions/Extension4337.sol | 106 ++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 contracts/LSP17Extensions/Extension4337.sol diff --git a/contracts/LSP17Extensions/Extension4337.sol b/contracts/LSP17Extensions/Extension4337.sol new file mode 100644 index 000000000..c44268850 --- /dev/null +++ b/contracts/LSP17Extensions/Extension4337.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.9; + +import {_ERC1271_FAILVALUE} from "../LSP0ERC725Account/LSP0Constants.sol"; +import {LSP6Utils} from "../LSP6KeyManager/LSP6Utils.sol"; +import {ILSP14Ownable2Step} from "../LSP14Ownable2Step/ILSP14Ownable2Step.sol"; +import {LSP14Ownable2Step} from "../LSP14Ownable2Step/LSP14Ownable2Step.sol"; +import {LSP17Extension} from "../LSP17ContractExtension/LSP17Extension.sol"; +import { + ILSP20CallVerifier +} from "../LSP20CallVerification/ILSP20CallVerifier.sol"; + +import {IAccount} from "@account-abstraction/contracts/interfaces/IAccount.sol"; +import { + UserOperation +} from "@account-abstraction/contracts/interfaces/UserOperation.sol"; +import { + IEntryPoint +} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; +import { + IERC725X +} from "@erc725/smart-contracts/contracts/interfaces/IERC725X.sol"; +import { + IERC725Y +} from "@erc725/smart-contracts/contracts/interfaces/IERC725Y.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +contract Extension4337 is LSP17Extension, IAccount { + using ECDSA for bytes32; + + address public immutable entryPoint; + + // permission needed to be able to use this extension + bytes32 internal constant _4337_PERMISSION = + 0x0000000000000000000000000000000000000000000000000000000000800000; + + // error code returned when signature or permission validation fails + uint256 internal constant _SIG_VALIDATION_FAILED = 1; + + constructor(address entryPoint_) { + entryPoint = entryPoint_; + } + + function validateUserOp( + UserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) external returns (uint256) { + // only entryPoint can call this function + require( + _extendableMsgSender() == entryPoint, + "Extension4337: only entryPoint can call validateUserOp" + ); + + // recover initiator of the tx from the signature + bytes32 hash = userOpHash.toEthSignedMessageHash(); + address recovered = hash.recover(userOp.signature); + + // fetch address permissions + bytes32 permissionsRetrieved = LSP6Utils.getPermissionsFor( + IERC725Y(msg.sender), + recovered + ); + + // verify that the recovered address has the _4337_PERMISSION + if (!LSP6Utils.hasPermission(permissionsRetrieved, _4337_PERMISSION)) { + return _SIG_VALIDATION_FAILED; + } + + // retrieve owner from caller + address owner = LSP14Ownable2Step(msg.sender).owner(); + + // verify that the recovered address can execute the userOp.callData + bytes4 magicValue = ILSP20CallVerifier(owner).lsp20VerifyCall( + msg.sender, + recovered, + 0, + userOp.callData + ); + + // if the call verifier returns _ERC1271_FAILVALUE, the caller is not authorized to make this call + if (_ERC1271_FAILVALUE == magicValue) { + return _SIG_VALIDATION_FAILED; + } + + // if entryPoint is missing funds to pay for the tx, deposit funds + if (missingAccountFunds > 0) { + // deposit bytes to entryPoint + bytes memory depositToBytes = abi.encodeWithSignature( + "depositTo(uint256)", + missingAccountFunds + ); + + // send funds from Universal Profile to entryPoint + IERC725X(msg.sender).execute( + 0, + entryPoint, + missingAccountFunds, + depositToBytes + ); + } + + // if sig validation passed, return 0 + return 0; + } +} From 6670b14e8ee6ce78dd3c20d4e57006ce4c657454 Mon Sep 17 00:00:00 2001 From: maxvia87 Date: Wed, 4 Oct 2023 11:07:13 +0200 Subject: [PATCH 03/15] chore: add 4337 tests to package and ci --- .github/workflows/build-lint-test.yml | 1 + package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/build-lint-test.yml b/.github/workflows/build-lint-test.yml index 887607dfc..99b5a8e54 100644 --- a/.github/workflows/build-lint-test.yml +++ b/.github/workflows/build-lint-test.yml @@ -76,6 +76,7 @@ jobs: "lsp11", "lsp11init", "lsp17", + "lsp17extensions", "lsp20", "lsp20init", "lsp23", diff --git a/package.json b/package.json index b398e32aa..09af4d34d 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "test:lsp11": "hardhat test --no-compile tests/LSP11BasicSocialRecovery/LSP11BasicSocialRecovery.test.ts", "test:lsp11init": "hardhat test --no-compile tests/LSP11BasicSocialRecovery/LSP11BasicSocialRecoveryInit.test.ts", "test:lsp17": "hardhat test --no-compile tests/LSP17ContractExtension/LSP17Extendable.test.ts", + "test:lsp17extensions": "hardhat test --no-compile tests/LSP17Extentions/**/*.test.ts", "test:lsp20": "hardhat test --no-compile tests/LSP20CallVerification/LSP6/LSP20WithLSP6.test.ts", "test:lsp20init": "hardhat test --no-compile tests/LSP20CallVerification/LSP6/LSP20WithLSP6Init.test.ts", "test:lsp23": "hardhat test --no-compile tests/LSP23LinkedContractsDeployment/LSP23LinkedContractsDeployment.test.ts", From 84e57f554a68309e223748e2a061c3c5273f3a8f Mon Sep 17 00:00:00 2001 From: maxvia87 Date: Wed, 4 Oct 2023 15:48:23 +0200 Subject: [PATCH 04/15] test: create test helpers 4337 --- .../LSP17Extentions/helpers/Create2Factory.ts | 125 ++++++++ tests/LSP17Extentions/helpers/UserOp.ts | 271 ++++++++++++++++++ .../LSP17Extentions/helpers/UserOperation.ts | 15 + .../LSP17Extentions/helpers/solidityTypes.ts | 10 + tests/LSP17Extentions/helpers/utils.ts | 100 +++++++ 5 files changed, 521 insertions(+) create mode 100644 tests/LSP17Extentions/helpers/Create2Factory.ts create mode 100644 tests/LSP17Extentions/helpers/UserOp.ts create mode 100644 tests/LSP17Extentions/helpers/UserOperation.ts create mode 100644 tests/LSP17Extentions/helpers/solidityTypes.ts create mode 100644 tests/LSP17Extentions/helpers/utils.ts diff --git a/tests/LSP17Extentions/helpers/Create2Factory.ts b/tests/LSP17Extentions/helpers/Create2Factory.ts new file mode 100644 index 000000000..fa1bd5e1e --- /dev/null +++ b/tests/LSP17Extentions/helpers/Create2Factory.ts @@ -0,0 +1,125 @@ +// from: https://github.com/Arachnid/deterministic-deployment-proxy +import { BigNumber, BigNumberish, ethers, Signer } from 'ethers'; +import { arrayify, hexConcat, hexlify, hexZeroPad, keccak256 } from 'ethers/lib/utils'; +import { Provider } from '@ethersproject/providers'; +import { TransactionRequest } from '@ethersproject/abstract-provider'; + +export class Create2Factory { + factoryDeployed = false; + + // from: https://github.com/Arachnid/deterministic-deployment-proxy + static readonly contractAddress = '0x4e59b44847b379578588920ca78fbf26c0b4956c'; + static readonly factoryTx = + '0xf8a58085174876e800830186a08080b853604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222'; + static readonly factoryDeployer = '0x3fab184622dc19b6109349b94811493bf2a45362'; + static readonly deploymentGasPrice = 100e9; + static readonly deploymentGasLimit = 100000; + static readonly factoryDeploymentFee = ( + Create2Factory.deploymentGasPrice * Create2Factory.deploymentGasLimit + ).toString(); + + constructor( + readonly provider: Provider, + readonly signer = (provider as ethers.providers.JsonRpcProvider).getSigner(), + ) {} + + /** + * deploy a contract using our deterministic deployer. + * The deployer is deployed (unless it is already deployed) + * NOTE: this transaction will fail if already deployed. use getDeployedAddress to check it first. + * @param initCode delpoyment code. can be a hex string or factory.getDeploymentTransaction(..) + * @param salt specific salt for deployment + * @param gasLimit gas limit or 'estimate' to use estimateGas. by default, calculate gas based on data size. + */ + async deploy( + initCode: string | TransactionRequest, + salt: BigNumberish = 0, + gasLimit?: BigNumberish | 'estimate', + ): Promise { + await this.deployFactory(); + if (typeof initCode !== 'string') { + // eslint-disable-next-line @typescript-eslint/no-base-to-string + initCode = (initCode as TransactionRequest).data!.toString(); + } + + const addr = Create2Factory.getDeployedAddress(initCode, salt); + if ((await this.provider.getCode(addr).then((code) => code.length)) > 2) { + return addr; + } + + const deployTx = { + to: Create2Factory.contractAddress, + data: this.getDeployTransactionCallData(initCode, salt), + }; + if (gasLimit === 'estimate') { + gasLimit = await this.signer.estimateGas(deployTx); + } + + if (gasLimit === undefined) { + gasLimit = + arrayify(initCode) + .map((x) => (x === 0 ? 4 : 16)) + .reduce((sum, x) => sum + x) + + (200 * initCode.length) / 2 + // actual is usually somewhat smaller (only deposited code, not entire constructor) + 6 * Math.ceil(initCode.length / 64) + // hash price. very minor compared to deposit costs + 32000 + + 21000; + + // deployer requires some extra gas + gasLimit = Math.floor((gasLimit * 64) / 63); + } + + const ret = await this.signer.sendTransaction({ ...deployTx, gasLimit }); + await ret.wait(); + if ((await this.provider.getCode(addr).then((code) => code.length)) === 2) { + throw new Error('failed to deploy'); + } + return addr; + } + + getDeployTransactionCallData(initCode: string, salt: BigNumberish = 0): string { + const saltBytes32 = hexZeroPad(hexlify(salt), 32); + return hexConcat([saltBytes32, initCode]); + } + + /** + * return the deployed address of this code. + * (the deployed address to be used by deploy() + * @param initCode + * @param salt + */ + static getDeployedAddress(initCode: string, salt: BigNumberish): string { + const saltBytes32 = hexZeroPad(hexlify(salt), 32); + return ( + '0x' + + keccak256( + hexConcat(['0xff', Create2Factory.contractAddress, saltBytes32, keccak256(initCode)]), + ).slice(-40) + ); + } + + // deploy the factory, if not already deployed. + async deployFactory(signer?: Signer): Promise { + if (await this._isFactoryDeployed()) { + return; + } + await (signer ?? this.signer).sendTransaction({ + to: Create2Factory.factoryDeployer, + value: BigNumber.from(Create2Factory.factoryDeploymentFee), + }); + await this.provider.sendTransaction(Create2Factory.factoryTx); + if (!(await this._isFactoryDeployed())) { + throw new Error('fatal: failed to deploy deterministic deployer'); + } + } + + async _isFactoryDeployed(): Promise { + if (!this.factoryDeployed) { + const deployed = await this.provider.getCode(Create2Factory.contractAddress); + if (deployed.length > 2) { + this.factoryDeployed = true; + } + } + return this.factoryDeployed; + } +} diff --git a/tests/LSP17Extentions/helpers/UserOp.ts b/tests/LSP17Extentions/helpers/UserOp.ts new file mode 100644 index 000000000..fac137c9a --- /dev/null +++ b/tests/LSP17Extentions/helpers/UserOp.ts @@ -0,0 +1,271 @@ +import { arrayify, defaultAbiCoder, hexDataSlice, keccak256 } from 'ethers/lib/utils'; +import { BigNumber, Contract, Signer, Wallet } from 'ethers'; +import { AddressZero, callDataCost, rethrow } from './utils'; +import { ecsign, toRpcSig, keccak256 as keccak256_buffer } from 'ethereumjs-util'; +import { UserOperation } from './UserOperation'; +import { Create2Factory } from './Create2Factory'; +import { EntryPoint } from '@account-abstraction/contracts'; +import { ethers } from 'ethers'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { UniversalProfile } from '../../../types'; + +export function packUserOp(op: UserOperation, forSignature = true): string { + if (forSignature) { + return defaultAbiCoder.encode( + [ + 'address', + 'uint256', + 'bytes32', + 'bytes32', + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'bytes32', + ], + [ + op.sender, + op.nonce, + keccak256(op.initCode), + keccak256(op.callData), + op.callGasLimit, + op.verificationGasLimit, + op.preVerificationGas, + op.maxFeePerGas, + op.maxPriorityFeePerGas, + keccak256(op.paymasterAndData), + ], + ); + } else { + // for the purpose of calculating gas cost encode also signature (and no keccak of bytes) + return defaultAbiCoder.encode( + [ + 'address', + 'uint256', + 'bytes', + 'bytes', + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'bytes', + 'bytes', + ], + [ + op.sender, + op.nonce, + op.initCode, + op.callData, + op.callGasLimit, + op.verificationGasLimit, + op.preVerificationGas, + op.maxFeePerGas, + op.maxPriorityFeePerGas, + op.paymasterAndData, + op.signature, + ], + ); + } +} + +export function packUserOp1(op: UserOperation): string { + return defaultAbiCoder.encode( + [ + 'address', // sender + 'uint256', // nonce + 'bytes32', // initCode + 'bytes32', // callData + 'uint256', // callGasLimit + 'uint256', // verificationGasLimit + 'uint256', // preVerificationGas + 'uint256', // maxFeePerGas + 'uint256', // maxPriorityFeePerGas + 'bytes32', // paymasterAndData + ], + [ + op.sender, + op.nonce, + keccak256(op.initCode), + keccak256(op.callData), + op.callGasLimit, + op.verificationGasLimit, + op.preVerificationGas, + op.maxFeePerGas, + op.maxPriorityFeePerGas, + keccak256(op.paymasterAndData), + ], + ); +} + +export function getUserOpHash(op: UserOperation, entryPoint: string, chainId: number): string { + const userOpHash = keccak256(packUserOp(op, true)); + const enc = defaultAbiCoder.encode( + ['bytes32', 'address', 'uint256'], + [userOpHash, entryPoint, chainId], + ); + return keccak256(enc); +} + +export const DefaultsForUserOp: UserOperation = { + sender: AddressZero, + nonce: 0, + initCode: '0x', + callData: '0x', + callGasLimit: 0, + verificationGasLimit: 250000, // default verification gas + preVerificationGas: 21000, // should also cover calldata cost. + maxFeePerGas: 0, + maxPriorityFeePerGas: 1e9, + paymasterAndData: '0x', + signature: '0x', +}; + +export function signUserOp( + op: UserOperation, + signer: Wallet, + entryPoint: string, + chainId: number, +): UserOperation { + const message = getUserOpHash(op, entryPoint, chainId); + const msg1 = Buffer.concat([ + Buffer.from('\x19Ethereum Signed Message:\n32', 'ascii'), + Buffer.from(arrayify(message)), + ]); + + const sig = ecsign(keccak256_buffer(msg1), Buffer.from(arrayify(signer.privateKey))); + // that's equivalent of: await signer.signMessage(message); + // (but without "async" + const signedMessage1 = toRpcSig(sig.v, sig.r, sig.s); + return { + ...op, + signature: signedMessage1, + }; +} + +export function fillUserOpDefaults( + op: Partial, + defaults = DefaultsForUserOp, +): UserOperation { + const partial: any = { ...op }; + // we want "item:undefined" to be used from defaults, and not override defaults, so we must explicitly + // remove those so "merge" will succeed. + for (const key in partial) { + if (partial[key] == null) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete partial[key]; + } + } + const filled = { ...defaults, ...partial }; + return filled; +} + +// helper to fill structure: +// - default callGasLimit to estimate call from entryPoint to account +// if there is initCode: +// - calculate sender by eth_call the deployment code +// - default verificationGasLimit estimateGas of deployment code plus default 100000 +// no initCode: +// - update nonce from account.getNonce() +// entryPoint param is only required to fill in "sender address when specifying "initCode" +// nonce: assume contract as "getNonce()" function, and fill in. +// sender - only in case of construction: fill sender from initCode. +// callGasLimit: VERY crude estimation (by estimating call to account, and add rough entryPoint overhead +// verificationGasLimit: hard-code default at 100k. should add "create2" cost +export async function fillUserOp( + op: Partial, + signer: SignerWithAddress, + entryPoint?: EntryPoint, +): Promise { + const op1 = { ...op }; + const provider = entryPoint?.provider; + if (op.initCode != null) { + const initAddr = hexDataSlice(op1.initCode!, 0, 20); + const initCallData = hexDataSlice(op1.initCode!, 20); + if (op1.nonce == null) op1.nonce = 0; + if (op1.sender == null) { + // hack: if the init contract is our known deployer, then we know what the address would be, without a view call + if (initAddr.toLowerCase() === Create2Factory.contractAddress.toLowerCase()) { + const ctr = hexDataSlice(initCallData, 32); + const salt = hexDataSlice(initCallData, 0, 32); + op1.sender = Create2Factory.getDeployedAddress(ctr, salt); + } else { + // console.log('\t== not our deployer. our=', Create2Factory.contractAddress, 'got', initAddr) + if (provider == null) throw new Error('no entrypoint/provider'); + op1.sender = await entryPoint!.callStatic + .getSenderAddress(op1.initCode!) + .catch((e) => e.errorArgs.sender); + } + } + if (op1.verificationGasLimit == null) { + if (provider == null) throw new Error('no entrypoint/provider'); + const initEstimate = await provider.estimateGas({ + from: entryPoint?.address, + to: initAddr, + data: initCallData, + gasLimit: 10e6, + }); + op1.verificationGasLimit = BigNumber.from(DefaultsForUserOp.verificationGasLimit).add( + initEstimate, + ); + } + } + if (op1.nonce == null) { + if (provider == null) throw new Error('must have entryPoint to autofill nonce'); + + const signerKeyAsUint192 = ethers.BigNumber.from(signer.address).toHexString(); + + try { + op1.nonce = await entryPoint.getNonce(op1.sender, signerKeyAsUint192); + } catch { + rethrow(); + } + } + if (op1.callGasLimit == null && op.callData != null) { + if (provider == null) throw new Error('must have entryPoint for callGasLimit estimate'); + const gasEtimated = await provider.estimateGas({ + from: entryPoint?.address, + to: op1.sender, + data: op1.callData, + }); + + // estimateGas assumes direct call from entryPoint. add wrapper cost. + op1.callGasLimit = gasEtimated; // .add(55000) + } + if (op1.maxFeePerGas == null) { + if (provider == null) throw new Error('must have entryPoint to autofill maxFeePerGas'); + const block = await provider.getBlock('latest'); + op1.maxFeePerGas = block.baseFeePerGas!.add( + op1.maxPriorityFeePerGas ?? DefaultsForUserOp.maxPriorityFeePerGas, + ); + } + + if (op1.maxPriorityFeePerGas == null) { + op1.maxPriorityFeePerGas = DefaultsForUserOp.maxPriorityFeePerGas; + } + const op2 = fillUserOpDefaults(op1); + // eslint-disable-next-line @typescript-eslint/no-base-to-string + if (op2.preVerificationGas.toString() === '0') { + // TODO: we don't add overhead, which is ~21000 for a single TX, but much lower in a batch. + op2.preVerificationGas = callDataCost(packUserOp(op2, false)); + } + return op2; +} + +export async function fillAndSign( + op: Partial, + signer: SignerWithAddress, + entryPoint?: EntryPoint, +): Promise { + const provider = entryPoint?.provider; + const op2 = await fillUserOp(op, signer, entryPoint); + + const chainId = await provider!.getNetwork().then((net) => net.chainId); + const message = arrayify(getUserOpHash(op2, entryPoint!.address, chainId)); + + return { + ...op2, + signature: await signer.signMessage(message), + }; +} diff --git a/tests/LSP17Extentions/helpers/UserOperation.ts b/tests/LSP17Extentions/helpers/UserOperation.ts new file mode 100644 index 000000000..8754e4ed1 --- /dev/null +++ b/tests/LSP17Extentions/helpers/UserOperation.ts @@ -0,0 +1,15 @@ +import * as typ from './solidityTypes'; + +export interface UserOperation { + sender: typ.address; + nonce: typ.uint256; + initCode: typ.bytes; + callData: typ.bytes; + callGasLimit: typ.uint256; + verificationGasLimit: typ.uint256; + preVerificationGas: typ.uint256; + maxFeePerGas: typ.uint256; + maxPriorityFeePerGas: typ.uint256; + paymasterAndData: typ.bytes; + signature: typ.bytes; +} diff --git a/tests/LSP17Extentions/helpers/solidityTypes.ts b/tests/LSP17Extentions/helpers/solidityTypes.ts new file mode 100644 index 000000000..5026ef9e3 --- /dev/null +++ b/tests/LSP17Extentions/helpers/solidityTypes.ts @@ -0,0 +1,10 @@ +// define the same export types as used by export typechain/ethers +import { BigNumberish } from 'ethers' +import { BytesLike } from '@ethersproject/bytes' + +export type address = string +export type uint256 = BigNumberish +export type uint = BigNumberish +export type uint48 = BigNumberish +export type bytes = BytesLike +export type bytes32 = BytesLike diff --git a/tests/LSP17Extentions/helpers/utils.ts b/tests/LSP17Extentions/helpers/utils.ts new file mode 100644 index 000000000..af0e1a4b7 --- /dev/null +++ b/tests/LSP17Extentions/helpers/utils.ts @@ -0,0 +1,100 @@ +import { ethers } from 'hardhat'; +import { Create2Factory } from './Create2Factory'; +import { EntryPoint__factory, EntryPoint } from '@account-abstraction/contracts'; + +export const AddressZero = ethers.constants.AddressZero; + +export function callDataCost(data: string): number { + return ethers.utils + .arrayify(data) + .map((x) => (x === 0 ? 4 : 16)) + .reduce((sum, x) => sum + x); +} + +export async function deployEntryPoint(provider = ethers.provider): Promise { + const create2factory = new Create2Factory(provider); + const addr = await create2factory.deploy( + EntryPoint__factory.bytecode, + 0, + process.env.COVERAGE != null ? 20e6 : 8e6, + ); + return EntryPoint__factory.connect(addr, provider.getSigner()); +} + +export async function getBalance(address: string): Promise { + const balance = await ethers.provider.getBalance(address); + return parseInt(balance.toString()); +} + +export async function isDeployed(addr: string): Promise { + const code = await ethers.provider.getCode(addr); + return code.length > 2; +} + +const panicCodes: { [key: number]: string } = { + // from https://docs.soliditylang.org/en/v0.8.0/control-structures.html + 0x01: 'assert(false)', + 0x11: 'arithmetic overflow/underflow', + 0x12: 'divide by zero', + 0x21: 'invalid enum value', + 0x22: 'storage byte array that is incorrectly encoded', + 0x31: '.pop() on an empty array.', + 0x32: 'array sout-of-bounds or negative index', + 0x41: 'memory overflow', + 0x51: 'zero-initialized variable of internal function type', +}; + +// rethrow "cleaned up" exception. +// - stack trace goes back to method (or catch) line, not inner provider +// - attempt to parse revert data (needed for geth) +// use with ".catch(rethrow())", so that current source file/line is meaningful. +export function rethrow(): (e: Error) => void { + const callerStack = new Error() + .stack!.replace(/Error.*\n.*at.*\n/, '') + .replace(/.*at.* \(internal[\s\S]*/, ''); + + if (arguments[0] != null) { + throw new Error('must use .catch(rethrow()), and NOT .catch(rethrow)'); + } + return function (e: Error) { + const solstack = e.stack!.match(/((?:.* at .*\.sol.*\n)+)/); + const stack = (solstack != null ? solstack[1] : '') + callerStack; + // const regex = new RegExp('error=.*"data":"(.*?)"').compile() + const found = /error=.*?"data":"(.*?)"/.exec(e.message); + let message: string; + if (found != null) { + const data = found[1]; + message = decodeRevertReason(data) ?? e.message + ' - ' + data.slice(0, 100); + } else { + message = e.message; + } + const err = new Error(message); + err.stack = 'Error: ' + message + '\n' + stack; + throw err; + }; +} + +export function decodeRevertReason(data: string, nullIfNoMatch = true): string | null { + const methodSig = data.slice(0, 10); + const dataParams = '0x' + data.slice(10); + + if (methodSig === '0x08c379a0') { + const [err] = ethers.utils.defaultAbiCoder.decode(['string'], dataParams); + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `Error(${err})`; + } else if (methodSig === '0x00fa072b') { + const [opindex, paymaster, msg] = ethers.utils.defaultAbiCoder.decode( + ['uint256', 'address', 'string'], + dataParams, + ); + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `FailedOp(${opindex}, ${paymaster !== AddressZero ? paymaster : 'none'}, ${msg})`; + } else if (methodSig === '0x4e487b71') { + const [code] = ethers.utils.defaultAbiCoder.decode(['uint256'], dataParams); + return `Panic(${panicCodes[code] ?? code} + ')`; + } + if (!nullIfNoMatch) { + return data; + } + return null; +} From a5e908d7dfeb38190b7edd8b113363b09d354a03 Mon Sep 17 00:00:00 2001 From: maxvia87 Date: Wed, 4 Oct 2023 15:48:40 +0200 Subject: [PATCH 05/15] test: create tests for 4337 --- contracts/LSP17Extensions/Extension4337.sol | 3 +- .../Extension4337/4337.test.ts | 231 ++++++++++++++++++ 2 files changed, 232 insertions(+), 2 deletions(-) create mode 100644 tests/LSP17Extentions/Extension4337/4337.test.ts diff --git a/contracts/LSP17Extensions/Extension4337.sol b/contracts/LSP17Extensions/Extension4337.sol index c44268850..80313e8e3 100644 --- a/contracts/LSP17Extensions/Extension4337.sol +++ b/contracts/LSP17Extensions/Extension4337.sol @@ -46,10 +46,9 @@ contract Extension4337 is LSP17Extension, IAccount { bytes32 userOpHash, uint256 missingAccountFunds ) external returns (uint256) { - // only entryPoint can call this function require( _extendableMsgSender() == entryPoint, - "Extension4337: only entryPoint can call validateUserOp" + "Only EntryPoint contract can call this" ); // recover initiator of the tx from the signature diff --git a/tests/LSP17Extentions/Extension4337/4337.test.ts b/tests/LSP17Extentions/Extension4337/4337.test.ts new file mode 100644 index 000000000..900342491 --- /dev/null +++ b/tests/LSP17Extentions/Extension4337/4337.test.ts @@ -0,0 +1,231 @@ +import { ethers } from 'hardhat'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { Signer } from 'ethers'; +import { EntryPoint__factory, EntryPoint } from '@account-abstraction/contracts'; + +import { parseEther } from 'ethers/lib/utils'; +import { expect } from 'chai'; +import { + Extension4337, + Extension4337__factory, + LSP6KeyManager, + LSP6KeyManager__factory, + UniversalProfile, + UniversalProfile__factory, +} from '../../../types'; +import { deployEntryPoint, getBalance, isDeployed } from '../helpers/utils'; +import { ALL_PERMISSIONS, ERC725YDataKeys } from '../../../constants'; +import { combinePermissions } from '../../utils/helpers'; +import { fillAndSign } from '../helpers/UserOp'; + +describe('4337', function () { + let bundler: SignerWithAddress; + let deployer: Signer; + let universalProfile: UniversalProfile; + let universalProfileWithExtension: Extension4337; + let universalProfileAddress: string; + let keyManager: LSP6KeyManager; + let entryPoint: EntryPoint; + let controllerWith4337Permission: SignerWithAddress; + let controllerWithout4337Permission: SignerWithAddress; + let controllerWithOnly4337Permission: SignerWithAddress; + let transferCallData: string; + const Permission4337 = '0x0000000000000000000000000000000000000000000000000000000000800000'; + const amountToTransfer = 1; + + before('before', async function () { + const provider = ethers.provider; + deployer = provider.getSigner(); + + [ + bundler, + controllerWith4337Permission, + controllerWithout4337Permission, + controllerWithOnly4337Permission, + ] = await ethers.getSigners(); + + universalProfile = await new UniversalProfile__factory(deployer).deploy( + await deployer.getAddress(), + ); + universalProfileAddress = universalProfile.address; + + keyManager = await new LSP6KeyManager__factory(deployer).deploy(universalProfile.address); + + // transfer ownership to keyManager + await universalProfile.transferOwnership(keyManager.address); + const dataKey = + ERC725YDataKeys.LSP6['AddressPermissions:Permissions'] + + (await deployer.getAddress()).slice(2); + + await universalProfile.setData(dataKey, ALL_PERMISSIONS); + + const acceptOwnershipBytes = universalProfile.interface.encodeFunctionData( + 'acceptOwnership', + [], + ); + await keyManager.execute(acceptOwnershipBytes); + expect(await universalProfile.owner()).to.eq(keyManager.address); + + // deploy entrypoint + entryPoint = await deployEntryPoint(); + expect(await isDeployed(entryPoint.address)).to.eq(true); + + // give all permissions to entrypoint + const dataKeyEntryPointPermissions = + ERC725YDataKeys.LSP6['AddressPermissions:Permissions'] + entryPoint.address.slice(2); + await universalProfile.setData(dataKeyEntryPointPermissions, ALL_PERMISSIONS); + + // deploy extension and attach it to universalProfile + const extension4337 = await new Extension4337__factory(deployer).deploy(entryPoint.address); + const validateUserOpSigHash = extension4337.interface.getSighash('validateUserOp'); + + const extensionDataKey = + ERC725YDataKeys.LSP17.LSP17ExtensionPrefix + + validateUserOpSigHash.slice(2) + + '00000000000000000000000000000000'; + + await universalProfile.setData(extensionDataKey, extension4337.address); + universalProfileWithExtension = Extension4337__factory.connect( + universalProfile.address, + deployer, + ); + + // give permissions to controllers + const dataKeyWithPermission4337 = + ERC725YDataKeys.LSP6['AddressPermissions:Permissions'] + + controllerWith4337Permission.address.slice(2); + await universalProfile.setData( + dataKeyWithPermission4337, + combinePermissions(ALL_PERMISSIONS, Permission4337), + ); + + const dataKeyWithoutPermission4337 = + ERC725YDataKeys.LSP6['AddressPermissions:Permissions'] + + controllerWithout4337Permission.address.slice(2); + await universalProfile.setData(dataKeyWithoutPermission4337, ALL_PERMISSIONS); + + const dataKeyWithOnlyPermission4337 = + ERC725YDataKeys.LSP6['AddressPermissions:Permissions'] + + controllerWithOnly4337Permission.address.slice(2); + + await universalProfile.setData(dataKeyWithOnlyPermission4337, Permission4337); + + // execute call data + transferCallData = universalProfile.interface.encodeFunctionData('execute', [ + 0, + ethers.constants.AddressZero, + amountToTransfer, + '0x1234', + ]); + + // send 1 ethers to universalProfile + await deployer.sendTransaction({ + to: universalProfile.address, + value: parseEther('1'), + }); + + // stake on entrypoint + const stakeAmount = parseEther('1'); + await entryPoint.depositTo(universalProfileAddress, { value: stakeAmount }); + }); + + it('should pass', async function () { + const address0BalanceBefore = await getBalance(ethers.constants.AddressZero); + + const op = await fillAndSign( + { + sender: universalProfileAddress, + callData: transferCallData, + }, + controllerWith4337Permission, + entryPoint, + ); + + await entryPoint.handleOps([op], bundler.address); + + const address0BalanceAfter = await getBalance(ethers.constants.AddressZero); + + expect(address0BalanceAfter - address0BalanceBefore).to.eq(amountToTransfer); + }); + + it('should fail when calling from wrong entrypoint', async function () { + const anotherEntryPoint = await new EntryPoint__factory(deployer).deploy(); + + const op = await fillAndSign( + { + sender: universalProfileAddress, + callData: transferCallData, + }, + controllerWith4337Permission, + entryPoint, + ); + + await expect(anotherEntryPoint.handleOps([op], bundler.address)) + .to.be.revertedWithCustomError(entryPoint, 'FailedOp') + .withArgs(0, 'AA23 reverted: Only EntryPoint can call this'); + }); + + it('should fail when controller does not have 4337 permission', async function () { + const anotherEntryPoint = await new EntryPoint__factory(deployer).deploy(); + + const op = await fillAndSign( + { + sender: universalProfileAddress, + callData: transferCallData, + }, + controllerWithout4337Permission, + entryPoint, + ); + + await expect(anotherEntryPoint.handleOps([op], bundler.address)).to.be.revertedWithCustomError( + entryPoint, + 'FailedOp', + ); + }); + + it('should fail when controller only has 4337 permission', async function () { + const op = await fillAndSign( + { + sender: universalProfileAddress, + callData: transferCallData, + }, + controllerWithOnly4337Permission, + entryPoint, + ); + await expect(entryPoint.handleOps([op], bundler.address)).to.be.revertedWithCustomError( + entryPoint, + 'FailedOp', + ); + }); + + it('should fail on invalid userop', async function () { + let op = await fillAndSign( + { + sender: universalProfileAddress, + callData: transferCallData, + nonce: 1234, + }, + controllerWith4337Permission, + entryPoint, + ); + + await expect(entryPoint.handleOps([op], bundler.address)) + .to.revertedWithCustomError(entryPoint, 'FailedOp') + .withArgs(0, 'AA25 invalid account nonce'); + + op = await fillAndSign( + { + sender: universalProfileAddress, + callData: transferCallData, + }, + controllerWith4337Permission, + entryPoint, + ); + + // invalidate the signature + op.callGasLimit = 1; + await expect(entryPoint.handleOps([op], bundler.address)) + .to.revertedWithCustomError(entryPoint, 'FailedOp') + .withArgs(0, 'AA24 signature error'); + }); +}); From a492824197a4d169f1a0f97c611ea67d87e082f6 Mon Sep 17 00:00:00 2001 From: maxvia87 Date: Wed, 4 Oct 2023 15:54:57 +0200 Subject: [PATCH 06/15] fix: remove unused imports --- contracts/LSP17Extensions/Extension4337.sol | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/contracts/LSP17Extensions/Extension4337.sol b/contracts/LSP17Extensions/Extension4337.sol index 80313e8e3..4534ac958 100644 --- a/contracts/LSP17Extensions/Extension4337.sol +++ b/contracts/LSP17Extensions/Extension4337.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.9; import {_ERC1271_FAILVALUE} from "../LSP0ERC725Account/LSP0Constants.sol"; import {LSP6Utils} from "../LSP6KeyManager/LSP6Utils.sol"; -import {ILSP14Ownable2Step} from "../LSP14Ownable2Step/ILSP14Ownable2Step.sol"; import {LSP14Ownable2Step} from "../LSP14Ownable2Step/LSP14Ownable2Step.sol"; import {LSP17Extension} from "../LSP17ContractExtension/LSP17Extension.sol"; import { @@ -14,9 +13,6 @@ import {IAccount} from "@account-abstraction/contracts/interfaces/IAccount.sol"; import { UserOperation } from "@account-abstraction/contracts/interfaces/UserOperation.sol"; -import { - IEntryPoint -} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; import { IERC725X } from "@erc725/smart-contracts/contracts/interfaces/IERC725X.sol"; @@ -28,7 +24,7 @@ import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; contract Extension4337 is LSP17Extension, IAccount { using ECDSA for bytes32; - address public immutable entryPoint; + address public immutable ENTRY_POINT; // permission needed to be able to use this extension bytes32 internal constant _4337_PERMISSION = @@ -38,7 +34,7 @@ contract Extension4337 is LSP17Extension, IAccount { uint256 internal constant _SIG_VALIDATION_FAILED = 1; constructor(address entryPoint_) { - entryPoint = entryPoint_; + ENTRY_POINT = entryPoint_; } function validateUserOp( @@ -47,7 +43,7 @@ contract Extension4337 is LSP17Extension, IAccount { uint256 missingAccountFunds ) external returns (uint256) { require( - _extendableMsgSender() == entryPoint, + _extendableMsgSender() == ENTRY_POINT, "Only EntryPoint contract can call this" ); @@ -93,7 +89,7 @@ contract Extension4337 is LSP17Extension, IAccount { // send funds from Universal Profile to entryPoint IERC725X(msg.sender).execute( 0, - entryPoint, + ENTRY_POINT, missingAccountFunds, depositToBytes ); From 8f88f307b7b3688d276e725e7e0ac117c54bfb75 Mon Sep 17 00:00:00 2001 From: maxvia87 Date: Wed, 4 Oct 2023 16:08:36 +0200 Subject: [PATCH 07/15] fix: lint issues --- .../Extension4337/4337.test.ts | 18 +++------- .../LSP17Extentions/helpers/Create2Factory.ts | 3 +- tests/LSP17Extentions/helpers/UserOp.ts | 34 +++++++------------ .../LSP17Extentions/helpers/solidityTypes.ts | 16 ++++----- tests/LSP17Extentions/helpers/utils.ts | 11 +++--- 5 files changed, 32 insertions(+), 50 deletions(-) diff --git a/tests/LSP17Extentions/Extension4337/4337.test.ts b/tests/LSP17Extentions/Extension4337/4337.test.ts index 900342491..babf2e369 100644 --- a/tests/LSP17Extentions/Extension4337/4337.test.ts +++ b/tests/LSP17Extentions/Extension4337/4337.test.ts @@ -6,7 +6,6 @@ import { EntryPoint__factory, EntryPoint } from '@account-abstraction/contracts' import { parseEther } from 'ethers/lib/utils'; import { expect } from 'chai'; import { - Extension4337, Extension4337__factory, LSP6KeyManager, LSP6KeyManager__factory, @@ -22,7 +21,6 @@ describe('4337', function () { let bundler: SignerWithAddress; let deployer: Signer; let universalProfile: UniversalProfile; - let universalProfileWithExtension: Extension4337; let universalProfileAddress: string; let keyManager: LSP6KeyManager; let entryPoint: EntryPoint; @@ -36,6 +34,7 @@ describe('4337', function () { before('before', async function () { const provider = ethers.provider; deployer = provider.getSigner(); + const deployerAddress = await deployer.getAddress(); [ bundler, @@ -53,16 +52,13 @@ describe('4337', function () { // transfer ownership to keyManager await universalProfile.transferOwnership(keyManager.address); + const dataKey = - ERC725YDataKeys.LSP6['AddressPermissions:Permissions'] + - (await deployer.getAddress()).slice(2); + ERC725YDataKeys.LSP6['AddressPermissions:Permissions'] + deployerAddress.slice(2); await universalProfile.setData(dataKey, ALL_PERMISSIONS); - const acceptOwnershipBytes = universalProfile.interface.encodeFunctionData( - 'acceptOwnership', - [], - ); + const acceptOwnershipBytes = universalProfile.interface.encodeFunctionData('acceptOwnership'); await keyManager.execute(acceptOwnershipBytes); expect(await universalProfile.owner()).to.eq(keyManager.address); @@ -85,10 +81,6 @@ describe('4337', function () { '00000000000000000000000000000000'; await universalProfile.setData(extensionDataKey, extension4337.address); - universalProfileWithExtension = Extension4337__factory.connect( - universalProfile.address, - deployer, - ); // give permissions to controllers const dataKeyWithPermission4337 = @@ -162,7 +154,7 @@ describe('4337', function () { await expect(anotherEntryPoint.handleOps([op], bundler.address)) .to.be.revertedWithCustomError(entryPoint, 'FailedOp') - .withArgs(0, 'AA23 reverted: Only EntryPoint can call this'); + .withArgs(0, 'AA23 reverted: Only EntryPoint contract can call this'); }); it('should fail when controller does not have 4337 permission', async function () { diff --git a/tests/LSP17Extentions/helpers/Create2Factory.ts b/tests/LSP17Extentions/helpers/Create2Factory.ts index fa1bd5e1e..abf7a43bd 100644 --- a/tests/LSP17Extentions/helpers/Create2Factory.ts +++ b/tests/LSP17Extentions/helpers/Create2Factory.ts @@ -38,8 +38,7 @@ export class Create2Factory { ): Promise { await this.deployFactory(); if (typeof initCode !== 'string') { - // eslint-disable-next-line @typescript-eslint/no-base-to-string - initCode = (initCode as TransactionRequest).data!.toString(); + initCode = (initCode as TransactionRequest).data.toString(); } const addr = Create2Factory.getDeployedAddress(initCode, salt); diff --git a/tests/LSP17Extentions/helpers/UserOp.ts b/tests/LSP17Extentions/helpers/UserOp.ts index fac137c9a..8949af9e5 100644 --- a/tests/LSP17Extentions/helpers/UserOp.ts +++ b/tests/LSP17Extentions/helpers/UserOp.ts @@ -1,5 +1,5 @@ import { arrayify, defaultAbiCoder, hexDataSlice, keccak256 } from 'ethers/lib/utils'; -import { BigNumber, Contract, Signer, Wallet } from 'ethers'; +import { BigNumber, Wallet } from 'ethers'; import { AddressZero, callDataCost, rethrow } from './utils'; import { ecsign, toRpcSig, keccak256 as keccak256_buffer } from 'ethereumjs-util'; import { UserOperation } from './UserOperation'; @@ -7,7 +7,6 @@ import { Create2Factory } from './Create2Factory'; import { EntryPoint } from '@account-abstraction/contracts'; import { ethers } from 'ethers'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import { UniversalProfile } from '../../../types'; export function packUserOp(op: UserOperation, forSignature = true): string { if (forSignature) { @@ -38,7 +37,6 @@ export function packUserOp(op: UserOperation, forSignature = true): string { ], ); } else { - // for the purpose of calculating gas cost encode also signature (and no keccak of bytes) return defaultAbiCoder.encode( [ 'address', @@ -114,8 +112,8 @@ export const DefaultsForUserOp: UserOperation = { initCode: '0x', callData: '0x', callGasLimit: 0, - verificationGasLimit: 250000, // default verification gas - preVerificationGas: 21000, // should also cover calldata cost. + verificationGasLimit: 250000, + preVerificationGas: 21000, maxFeePerGas: 0, maxPriorityFeePerGas: 1e9, paymasterAndData: '0x', @@ -149,11 +147,9 @@ export function fillUserOpDefaults( defaults = DefaultsForUserOp, ): UserOperation { const partial: any = { ...op }; - // we want "item:undefined" to be used from defaults, and not override defaults, so we must explicitly - // remove those so "merge" will succeed. + for (const key in partial) { if (partial[key] == null) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete partial[key]; } } @@ -181,20 +177,18 @@ export async function fillUserOp( const op1 = { ...op }; const provider = entryPoint?.provider; if (op.initCode != null) { - const initAddr = hexDataSlice(op1.initCode!, 0, 20); - const initCallData = hexDataSlice(op1.initCode!, 20); + const initAddr = hexDataSlice(op1.initCode, 0, 20); + const initCallData = hexDataSlice(op1.initCode, 20); if (op1.nonce == null) op1.nonce = 0; if (op1.sender == null) { - // hack: if the init contract is our known deployer, then we know what the address would be, without a view call if (initAddr.toLowerCase() === Create2Factory.contractAddress.toLowerCase()) { const ctr = hexDataSlice(initCallData, 32); const salt = hexDataSlice(initCallData, 0, 32); op1.sender = Create2Factory.getDeployedAddress(ctr, salt); } else { - // console.log('\t== not our deployer. our=', Create2Factory.contractAddress, 'got', initAddr) if (provider == null) throw new Error('no entrypoint/provider'); - op1.sender = await entryPoint!.callStatic - .getSenderAddress(op1.initCode!) + op1.sender = await entryPoint.callStatic + .getSenderAddress(op1.initCode) .catch((e) => e.errorArgs.sender); } } @@ -230,13 +224,12 @@ export async function fillUserOp( data: op1.callData, }); - // estimateGas assumes direct call from entryPoint. add wrapper cost. - op1.callGasLimit = gasEtimated; // .add(55000) + op1.callGasLimit = gasEtimated; } if (op1.maxFeePerGas == null) { if (provider == null) throw new Error('must have entryPoint to autofill maxFeePerGas'); const block = await provider.getBlock('latest'); - op1.maxFeePerGas = block.baseFeePerGas!.add( + op1.maxFeePerGas = block.baseFeePerGas.add( op1.maxPriorityFeePerGas ?? DefaultsForUserOp.maxPriorityFeePerGas, ); } @@ -245,9 +238,8 @@ export async function fillUserOp( op1.maxPriorityFeePerGas = DefaultsForUserOp.maxPriorityFeePerGas; } const op2 = fillUserOpDefaults(op1); - // eslint-disable-next-line @typescript-eslint/no-base-to-string + if (op2.preVerificationGas.toString() === '0') { - // TODO: we don't add overhead, which is ~21000 for a single TX, but much lower in a batch. op2.preVerificationGas = callDataCost(packUserOp(op2, false)); } return op2; @@ -261,8 +253,8 @@ export async function fillAndSign( const provider = entryPoint?.provider; const op2 = await fillUserOp(op, signer, entryPoint); - const chainId = await provider!.getNetwork().then((net) => net.chainId); - const message = arrayify(getUserOpHash(op2, entryPoint!.address, chainId)); + const chainId = await provider.getNetwork().then((net) => net.chainId); + const message = arrayify(getUserOpHash(op2, entryPoint.address, chainId)); return { ...op2, diff --git a/tests/LSP17Extentions/helpers/solidityTypes.ts b/tests/LSP17Extentions/helpers/solidityTypes.ts index 5026ef9e3..5663a0166 100644 --- a/tests/LSP17Extentions/helpers/solidityTypes.ts +++ b/tests/LSP17Extentions/helpers/solidityTypes.ts @@ -1,10 +1,10 @@ // define the same export types as used by export typechain/ethers -import { BigNumberish } from 'ethers' -import { BytesLike } from '@ethersproject/bytes' +import { BigNumberish } from 'ethers'; +import { BytesLike } from '@ethersproject/bytes'; -export type address = string -export type uint256 = BigNumberish -export type uint = BigNumberish -export type uint48 = BigNumberish -export type bytes = BytesLike -export type bytes32 = BytesLike +export type address = string; +export type uint256 = BigNumberish; +export type uint = BigNumberish; +export type uint48 = BigNumberish; +export type bytes = BytesLike; +export type bytes32 = BytesLike; diff --git a/tests/LSP17Extentions/helpers/utils.ts b/tests/LSP17Extentions/helpers/utils.ts index af0e1a4b7..daf591c81 100644 --- a/tests/LSP17Extentions/helpers/utils.ts +++ b/tests/LSP17Extentions/helpers/utils.ts @@ -49,17 +49,18 @@ const panicCodes: { [key: number]: string } = { // - attempt to parse revert data (needed for geth) // use with ".catch(rethrow())", so that current source file/line is meaningful. export function rethrow(): (e: Error) => void { - const callerStack = new Error() - .stack!.replace(/Error.*\n.*at.*\n/, '') + const callerStack = new Error().stack + .replace(/Error.*\n.*at.*\n/, '') .replace(/.*at.* \(internal[\s\S]*/, ''); + // eslint-disable-next-line if (arguments[0] != null) { throw new Error('must use .catch(rethrow()), and NOT .catch(rethrow)'); } return function (e: Error) { - const solstack = e.stack!.match(/((?:.* at .*\.sol.*\n)+)/); + const solstack = e.stack.match(/((?:.* at .*\.sol.*\n)+)/); const stack = (solstack != null ? solstack[1] : '') + callerStack; - // const regex = new RegExp('error=.*"data":"(.*?)"').compile() + const found = /error=.*?"data":"(.*?)"/.exec(e.message); let message: string; if (found != null) { @@ -80,14 +81,12 @@ export function decodeRevertReason(data: string, nullIfNoMatch = true): string | if (methodSig === '0x08c379a0') { const [err] = ethers.utils.defaultAbiCoder.decode(['string'], dataParams); - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions return `Error(${err})`; } else if (methodSig === '0x00fa072b') { const [opindex, paymaster, msg] = ethers.utils.defaultAbiCoder.decode( ['uint256', 'address', 'string'], dataParams, ); - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions return `FailedOp(${opindex}, ${paymaster !== AddressZero ? paymaster : 'none'}, ${msg})`; } else if (methodSig === '0x4e487b71') { const [code] = ethers.utils.defaultAbiCoder.decode(['uint256'], dataParams); From 5832789cf02c8e9dc7877177e01d6cbe38b56468 Mon Sep 17 00:00:00 2001 From: maxvia87 Date: Fri, 6 Oct 2023 10:32:36 +0200 Subject: [PATCH 08/15] refactor: apply recommendations --- contracts/LSP17Extensions/Extension4337.sol | 73 ++++++++++++------- .../Extension4337/4337.test.ts | 0 .../helpers/Create2Factory.ts | 0 .../helpers/UserOp.ts | 0 .../helpers/UserOperation.ts | 0 .../helpers/solidityTypes.ts | 0 .../helpers/utils.ts | 0 7 files changed, 45 insertions(+), 28 deletions(-) rename tests/{LSP17Extentions => LSP17Extensions}/Extension4337/4337.test.ts (100%) rename tests/{LSP17Extentions => LSP17Extensions}/helpers/Create2Factory.ts (100%) rename tests/{LSP17Extentions => LSP17Extensions}/helpers/UserOp.ts (100%) rename tests/{LSP17Extentions => LSP17Extensions}/helpers/UserOperation.ts (100%) rename tests/{LSP17Extentions => LSP17Extensions}/helpers/solidityTypes.ts (100%) rename tests/{LSP17Extentions => LSP17Extensions}/helpers/utils.ts (100%) diff --git a/contracts/LSP17Extensions/Extension4337.sol b/contracts/LSP17Extensions/Extension4337.sol index 4534ac958..e88533ab4 100644 --- a/contracts/LSP17Extensions/Extension4337.sol +++ b/contracts/LSP17Extensions/Extension4337.sol @@ -1,30 +1,37 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.9; -import {_ERC1271_FAILVALUE} from "../LSP0ERC725Account/LSP0Constants.sol"; -import {LSP6Utils} from "../LSP6KeyManager/LSP6Utils.sol"; -import {LSP14Ownable2Step} from "../LSP14Ownable2Step/LSP14Ownable2Step.sol"; -import {LSP17Extension} from "../LSP17ContractExtension/LSP17Extension.sol"; -import { - ILSP20CallVerifier -} from "../LSP20CallVerification/ILSP20CallVerifier.sol"; - +// interfaces import {IAccount} from "@account-abstraction/contracts/interfaces/IAccount.sol"; -import { - UserOperation -} from "@account-abstraction/contracts/interfaces/UserOperation.sol"; import { IERC725X } from "@erc725/smart-contracts/contracts/interfaces/IERC725X.sol"; import { IERC725Y } from "@erc725/smart-contracts/contracts/interfaces/IERC725Y.sol"; +import { + ILSP20CallVerifier +} from "../LSP20CallVerification/ILSP20CallVerifier.sol"; + +// modules +import {LSP14Ownable2Step} from "../LSP14Ownable2Step/LSP14Ownable2Step.sol"; +import {LSP17Extension} from "../LSP17ContractExtension/LSP17Extension.sol"; + +// librairies import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {LSP6Utils} from "../LSP6KeyManager/LSP6Utils.sol"; + +// constants +import { + UserOperation +} from "@account-abstraction/contracts/interfaces/UserOperation.sol"; +import {_ERC1271_FAILVALUE} from "../LSP0ERC725Account/LSP0Constants.sol"; contract Extension4337 is LSP17Extension, IAccount { using ECDSA for bytes32; + using LSP6Utils for *; - address public immutable ENTRY_POINT; + address internal immutable _ENTRY_POINT; // permission needed to be able to use this extension bytes32 internal constant _4337_PERMISSION = @@ -34,16 +41,19 @@ contract Extension4337 is LSP17Extension, IAccount { uint256 internal constant _SIG_VALIDATION_FAILED = 1; constructor(address entryPoint_) { - ENTRY_POINT = entryPoint_; + _ENTRY_POINT = entryPoint_; } + /** + * @inheritdoc IAccount + */ function validateUserOp( UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds ) external returns (uint256) { require( - _extendableMsgSender() == ENTRY_POINT, + _extendableMsgSender() == _ENTRY_POINT, "Only EntryPoint contract can call this" ); @@ -51,14 +61,13 @@ contract Extension4337 is LSP17Extension, IAccount { bytes32 hash = userOpHash.toEthSignedMessageHash(); address recovered = hash.recover(userOp.signature); - // fetch address permissions - bytes32 permissionsRetrieved = LSP6Utils.getPermissionsFor( - IERC725Y(msg.sender), - recovered - ); - // verify that the recovered address has the _4337_PERMISSION - if (!LSP6Utils.hasPermission(permissionsRetrieved, _4337_PERMISSION)) { + if ( + !LSP6Utils.hasPermission( + IERC725Y(msg.sender).getPermissionsFor(recovered), + _4337_PERMISSION + ) + ) { return _SIG_VALIDATION_FAILED; } @@ -66,12 +75,12 @@ contract Extension4337 is LSP17Extension, IAccount { address owner = LSP14Ownable2Step(msg.sender).owner(); // verify that the recovered address can execute the userOp.callData - bytes4 magicValue = ILSP20CallVerifier(owner).lsp20VerifyCall( - msg.sender, - recovered, - 0, - userOp.callData - ); + bytes4 magicValue = ILSP20CallVerifier(owner).lsp20VerifyCall({ + callee: msg.sender, + caller: recovered, + value: 0, + receivedCalldata: userOp.callData + }); // if the call verifier returns _ERC1271_FAILVALUE, the caller is not authorized to make this call if (_ERC1271_FAILVALUE == magicValue) { @@ -89,7 +98,7 @@ contract Extension4337 is LSP17Extension, IAccount { // send funds from Universal Profile to entryPoint IERC725X(msg.sender).execute( 0, - ENTRY_POINT, + _ENTRY_POINT, missingAccountFunds, depositToBytes ); @@ -98,4 +107,12 @@ contract Extension4337 is LSP17Extension, IAccount { // if sig validation passed, return 0 return 0; } + + /** + * @dev Get The address of the EntryPoint contract. + * @return The address of the EntryPoint contract + */ + function entryPoint() public view returns (address) { + return _ENTRY_POINT; + } } diff --git a/tests/LSP17Extentions/Extension4337/4337.test.ts b/tests/LSP17Extensions/Extension4337/4337.test.ts similarity index 100% rename from tests/LSP17Extentions/Extension4337/4337.test.ts rename to tests/LSP17Extensions/Extension4337/4337.test.ts diff --git a/tests/LSP17Extentions/helpers/Create2Factory.ts b/tests/LSP17Extensions/helpers/Create2Factory.ts similarity index 100% rename from tests/LSP17Extentions/helpers/Create2Factory.ts rename to tests/LSP17Extensions/helpers/Create2Factory.ts diff --git a/tests/LSP17Extentions/helpers/UserOp.ts b/tests/LSP17Extensions/helpers/UserOp.ts similarity index 100% rename from tests/LSP17Extentions/helpers/UserOp.ts rename to tests/LSP17Extensions/helpers/UserOp.ts diff --git a/tests/LSP17Extentions/helpers/UserOperation.ts b/tests/LSP17Extensions/helpers/UserOperation.ts similarity index 100% rename from tests/LSP17Extentions/helpers/UserOperation.ts rename to tests/LSP17Extensions/helpers/UserOperation.ts diff --git a/tests/LSP17Extentions/helpers/solidityTypes.ts b/tests/LSP17Extensions/helpers/solidityTypes.ts similarity index 100% rename from tests/LSP17Extentions/helpers/solidityTypes.ts rename to tests/LSP17Extensions/helpers/solidityTypes.ts diff --git a/tests/LSP17Extentions/helpers/utils.ts b/tests/LSP17Extensions/helpers/utils.ts similarity index 100% rename from tests/LSP17Extentions/helpers/utils.ts rename to tests/LSP17Extensions/helpers/utils.ts From 29702b3cceeec408baa38572da67cfea19a0ec7d Mon Sep 17 00:00:00 2001 From: CJ42 Date: Fri, 6 Oct 2023 16:28:11 +0100 Subject: [PATCH 09/15] feat: move `OnERC721ReceivedExtension` from Mocks to LSP17 folder --- .../OnERC721ReceivedExtension.sol | 15 +++++++++++++++ .../OnERC721ReceivedExtension.sol | 16 ---------------- contracts/Mocks/Tokens/RequireCallbackToken.sol | 2 +- 3 files changed, 16 insertions(+), 17 deletions(-) create mode 100644 contracts/LSP17Extensions/OnERC721ReceivedExtension.sol delete mode 100644 contracts/Mocks/FallbackExtensions/OnERC721ReceivedExtension.sol diff --git a/contracts/LSP17Extensions/OnERC721ReceivedExtension.sol b/contracts/LSP17Extensions/OnERC721ReceivedExtension.sol new file mode 100644 index 000000000..436bab17a --- /dev/null +++ b/contracts/LSP17Extensions/OnERC721ReceivedExtension.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.4; + +import { + ERC721Holder +} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; + +/** + * @dev LSP17 Extension that can be attached to a LSP17Extendable contract + * to allow it to receive ERC721 tokens via `safeTransferFrom`. + */ +// solhint-disable-next-line no-empty-block +contract OnERC721ReceivedExtension is ERC721Holder { + +} diff --git a/contracts/Mocks/FallbackExtensions/OnERC721ReceivedExtension.sol b/contracts/Mocks/FallbackExtensions/OnERC721ReceivedExtension.sol deleted file mode 100644 index 6dfa3f4a9..000000000 --- a/contracts/Mocks/FallbackExtensions/OnERC721ReceivedExtension.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.4; - -/** - * @dev This contract is used only for testing purposes - */ -contract OnERC721ReceivedExtension { - function onERC721Received( - address /* operator */, - address /* from */, - uint256 /* tokenId */, - bytes calldata /* data */ - ) external pure returns (bytes4) { - return 0x150b7a02; - } -} diff --git a/contracts/Mocks/Tokens/RequireCallbackToken.sol b/contracts/Mocks/Tokens/RequireCallbackToken.sol index e7bacc99b..e2d7dfaaa 100644 --- a/contracts/Mocks/Tokens/RequireCallbackToken.sol +++ b/contracts/Mocks/Tokens/RequireCallbackToken.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.4; import { OnERC721ReceivedExtension -} from "../FallbackExtensions/OnERC721ReceivedExtension.sol"; +} from "../../LSP17Extensions/OnERC721ReceivedExtension.sol"; /** * @dev This contract is used only for testing purposes From e85519085cb7c7d63c157b284a656c7ab1cd0c06 Mon Sep 17 00:00:00 2001 From: CJ42 Date: Fri, 6 Oct 2023 16:32:01 +0100 Subject: [PATCH 10/15] docs: add auto-generated docs for LSP17 Extensions --- contracts/LSP2ERC725YJSONSchema/LSP2Utils.sol | 2 +- .../LSP17Extensions/Extension4337.md | 173 ++++++++++++++++++ .../OnERC721ReceivedExtension.md | 61 ++++++ dodoc/config.ts | 2 + 4 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 docs/contracts/LSP17Extensions/Extension4337.md create mode 100644 docs/contracts/LSP17Extensions/OnERC721ReceivedExtension.md diff --git a/contracts/LSP2ERC725YJSONSchema/LSP2Utils.sol b/contracts/LSP2ERC725YJSONSchema/LSP2Utils.sol index 06fd72f1d..1c1319b00 100644 --- a/contracts/LSP2ERC725YJSONSchema/LSP2Utils.sol +++ b/contracts/LSP2ERC725YJSONSchema/LSP2Utils.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0 +// SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.4; // interfaces diff --git a/docs/contracts/LSP17Extensions/Extension4337.md b/docs/contracts/LSP17Extensions/Extension4337.md new file mode 100644 index 000000000..f7642dcf0 --- /dev/null +++ b/docs/contracts/LSP17Extensions/Extension4337.md @@ -0,0 +1,173 @@ + + + +# Extension4337 + +:::info Standard Specifications + +[`LSP-17-Extensions`](https://github.com/lukso-network/lips/tree/main/LSPs/LSP-17-Extensions.md) + +::: +:::info Solidity implementation + +[`Extension4337.sol`](https://github.com/lukso-network/lsp-smart-contracts/blob/develop/contracts/LSP17Extensions/Extension4337.sol) + +::: + +## Public Methods + +Public methods are accessible externally from users, allowing interaction with this function from dApps or other smart contracts. +When marked as 'public', a method can be called both externally and internally, on the other hand, when marked as 'external', a method can only be called externally. + +### constructor + +:::note References + +- Specification details: [**LSP-17-Extensions**](https://github.com/lukso-network/lips/tree/main/LSPs/LSP-17-Extensions.md#constructor) +- Solidity implementation: [`Extension4337.sol`](https://github.com/lukso-network/lsp-smart-contracts/blob/develop/contracts/LSP17Extensions/Extension4337.sol) + +::: + +```solidity +constructor(address entryPoint_); +``` + +#### Parameters + +| Name | Type | Description | +| ------------- | :-------: | ----------- | +| `entryPoint_` | `address` | - | + +
+ +### entryPoint + +:::note References + +- Specification details: [**LSP-17-Extensions**](https://github.com/lukso-network/lips/tree/main/LSPs/LSP-17-Extensions.md#entrypoint) +- Solidity implementation: [`Extension4337.sol`](https://github.com/lukso-network/lsp-smart-contracts/blob/develop/contracts/LSP17Extensions/Extension4337.sol) +- Function signature: `entryPoint()` +- Function selector: `0xb0d691fe` + +::: + +```solidity +function entryPoint() external view returns (address); +``` + +Get The address of the EntryPoint contract. + +#### Returns + +| Name | Type | Description | +| ---- | :-------: | -------------------------------------- | +| `0` | `address` | The address of the EntryPoint contract | + +
+ +### supportsInterface + +:::note References + +- Specification details: [**LSP-17-Extensions**](https://github.com/lukso-network/lips/tree/main/LSPs/LSP-17-Extensions.md#supportsinterface) +- Solidity implementation: [`Extension4337.sol`](https://github.com/lukso-network/lsp-smart-contracts/blob/develop/contracts/LSP17Extensions/Extension4337.sol) +- Function signature: `supportsInterface(bytes4)` +- Function selector: `0x01ffc9a7` + +::: + +```solidity +function supportsInterface(bytes4 interfaceId) external view returns (bool); +``` + +See [`IERC165-supportsInterface`](#ierc165-supportsinterface). + +#### Parameters + +| Name | Type | Description | +| ------------- | :------: | ----------- | +| `interfaceId` | `bytes4` | - | + +#### Returns + +| Name | Type | Description | +| ---- | :----: | ----------- | +| `0` | `bool` | - | + +
+ +### validateUserOp + +:::note References + +- Specification details: [**LSP-17-Extensions**](https://github.com/lukso-network/lips/tree/main/LSPs/LSP-17-Extensions.md#validateuserop) +- Solidity implementation: [`Extension4337.sol`](https://github.com/lukso-network/lsp-smart-contracts/blob/develop/contracts/LSP17Extensions/Extension4337.sol) +- Function signature: `validateUserOp(UserOperation,bytes32,uint256)` +- Function selector: `0xe86fc51e` + +::: + +```solidity +function validateUserOp( + UserOperation userOp, + bytes32 userOpHash, + uint256 missingAccountFunds +) external nonpayable returns (uint256); +``` + +_Validate user's signature and nonce the entryPoint will make the call to the recipient only if this validation call returns successfully. signature failure should be reported by returning SIG_VALIDATION_FAILED (1). This allows making a "simulation call" without a valid signature Other failures (e.g. nonce mismatch, or invalid signature format) should still revert to signal failure._ + +Must validate caller is the entryPoint. Must validate the signature and nonce + +#### Parameters + +| Name | Type | Description | +| --------------------- | :-------------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `userOp` | `UserOperation` | the operation that is about to be executed. | +| `userOpHash` | `bytes32` | hash of the user's request data. can be used as the basis for signature. | +| `missingAccountFunds` | `uint256` | missing funds on the account's deposit in the entrypoint. This is the minimum amount to transfer to the sender(entryPoint) to be able to make the call. The excess is left as a deposit in the entrypoint, for future calls. can be withdrawn anytime using "entryPoint.withdrawTo()" In case there is a paymaster in the request (or the current deposit is high enough), this value will be zero. | + +#### Returns + +| Name | Type | Description | +| ---- | :-------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `0` | `uint256` | packaged ValidationData structure. use `_packValidationData` and `_unpackValidationData` to encode and decode <20-byte> sigAuthorizer - 0 for valid signature, 1 to mark signature failure, otherwise, an address of an "authorizer" contract. <6-byte> validUntil - last timestamp this operation is valid. 0 for "indefinite" <6-byte> validAfter - first timestamp this operation is valid If an account doesn't use time-range, it is enough to return SIG_VALIDATION_FAILED value (1) for signature failure. Note that the validation code cannot use block.timestamp (or block.number) directly. | + +
+ +## Internal Methods + +Any method labeled as `internal` serves as utility function within the contract. They can be used when writing solidity contracts that inherit from this contract. These methods can be extended or modified by overriding their internal behavior to suit specific needs. + +Internal functions cannot be called externally, whether from other smart contracts, dApp interfaces, or backend services. Their restricted accessibility ensures that they remain exclusively available within the context of the current contract, promoting controlled and encapsulated usage of these internal utilities. + +### \_extendableMsgData + +```solidity +function _extendableMsgData() internal view returns (bytes); +``` + +Returns the original `msg.data` passed to the extendable contract +without the appended `msg.sender` and `msg.value`. + +
+ +### \_extendableMsgSender + +```solidity +function _extendableMsgSender() internal view returns (address); +``` + +Returns the original `msg.sender` calling the extendable contract. + +
+ +### \_extendableMsgValue + +```solidity +function _extendableMsgValue() internal view returns (uint256); +``` + +Returns the original `msg.value` sent to the extendable contract. + +
diff --git a/docs/contracts/LSP17Extensions/OnERC721ReceivedExtension.md b/docs/contracts/LSP17Extensions/OnERC721ReceivedExtension.md new file mode 100644 index 000000000..d40d353e2 --- /dev/null +++ b/docs/contracts/LSP17Extensions/OnERC721ReceivedExtension.md @@ -0,0 +1,61 @@ + + + +# OnERC721ReceivedExtension + +:::info Standard Specifications + +[`LSP-17-Extensions`](https://github.com/lukso-network/lips/tree/main/LSPs/LSP-17-Extensions.md) + +::: +:::info Solidity implementation + +[`OnERC721ReceivedExtension.sol`](https://github.com/lukso-network/lsp-smart-contracts/blob/develop/contracts/LSP17Extensions/OnERC721ReceivedExtension.sol) + +::: + +LSP17 Extension that can be attached to a LSP17Extendable contract to allow it to receive ERC721 tokens via `safeTransferFrom`. + +## Public Methods + +Public methods are accessible externally from users, allowing interaction with this function from dApps or other smart contracts. +When marked as 'public', a method can be called both externally and internally, on the other hand, when marked as 'external', a method can only be called externally. + +### onERC721Received + +:::note References + +- Specification details: [**LSP-17-Extensions**](https://github.com/lukso-network/lips/tree/main/LSPs/LSP-17-Extensions.md#,,,)) +- Solidity implementation: [`OnERC721ReceivedExtension.sol`](https://github.com/lukso-network/lsp-smart-contracts/blob/develop/contracts/LSP17Extensions/OnERC721ReceivedExtension.sol) +- Function signature: `,,,)` +- Function selector: `0x940e0af1` + +::: + +```solidity +function onERC721Received( + address, + address, + uint256, + bytes +) external nonpayable returns (bytes4); +``` + +See [`IERC721Receiver-onERC721Received`](#ierc721receiver-onerc721received). Always returns `IERC721Receiver.onERC721Received.selector`. + +#### Parameters + +| Name | Type | Description | +| ---- | :-------: | ----------- | +| `_0` | `address` | - | +| `_1` | `address` | - | +| `_2` | `uint256` | - | +| `_3` | `bytes` | - | + +#### Returns + +| Name | Type | Description | +| ---- | :------: | ----------- | +| `0` | `bytes4` | - | + +
diff --git a/dodoc/config.ts b/dodoc/config.ts index d81ec045b..9ac28dc9a 100644 --- a/dodoc/config.ts +++ b/dodoc/config.ts @@ -15,6 +15,8 @@ export const dodocConfig = { 'contracts/LSP16UniversalFactory/LSP16UniversalFactory.sol', 'contracts/LSP17ContractExtension/LSP17Extendable.sol', 'contracts/LSP17ContractExtension/LSP17Extension.sol', + 'contracts/LSP17Extensions/Extension4337.sol', + 'contracts/LSP17Extensions/OnERC721ReceivedExtension.sol', 'contracts/LSP20CallVerification/LSP20CallVerification.sol', 'contracts/LSP23LinkedContractsFactory/LSP23LinkedContractsFactory.sol', 'contracts/LSP23LinkedContractsFactory/IPostDeploymentModule.sol', From 7ec5dce743a05b0dad26bdea5f84496c7c635271 Mon Sep 17 00:00:00 2001 From: maxvia87 Date: Sat, 7 Oct 2023 11:00:19 +0200 Subject: [PATCH 11/15] fix: depositTo entrypoint --- contracts/LSP17Extensions/Extension4337.sol | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/contracts/LSP17Extensions/Extension4337.sol b/contracts/LSP17Extensions/Extension4337.sol index bd4cf97fb..59263c4f0 100644 --- a/contracts/LSP17Extensions/Extension4337.sol +++ b/contracts/LSP17Extensions/Extension4337.sol @@ -3,6 +3,9 @@ pragma solidity ^0.8.9; // interfaces import {IAccount} from "@account-abstraction/contracts/interfaces/IAccount.sol"; +import { + IStakeManager +} from "@account-abstraction/contracts/interfaces/IStakeManager.sol"; import { IERC725X } from "@erc725/smart-contracts/contracts/interfaces/IERC725X.sol"; @@ -90,9 +93,9 @@ contract Extension4337 is LSP17Extension, IAccount { // if entryPoint is missing funds to pay for the tx, deposit funds if (missingAccountFunds > 0) { // deposit bytes to entryPoint - bytes memory depositToBytes = abi.encodeWithSignature( - "depositTo(uint256)", - missingAccountFunds + bytes memory depositToBytes = abi.encodeWithSelector( + IStakeManager.depositTo.selector, + msg.sender ); // send funds from Universal Profile to ENTRY_POINT From db28dc7af742a7b4354ea30c0c23cbe3b879fc6e Mon Sep 17 00:00:00 2001 From: maxvia87 Date: Mon, 9 Oct 2023 11:12:41 +0200 Subject: [PATCH 12/15] test: apply recommendations --- contracts/LSP17Extensions/Extension4337.sol | 2 +- .../OnERC721ReceivedExtension.sol | 2 +- .../Extension4337/4337.test.ts | 49 ++++++-------- tests/LSP17Extensions/helpers/UserOp.ts | 52 ++++++-------- .../LSP17Extensions/helpers/UserOperation.ts | 15 ----- tests/LSP17Extensions/helpers/utils.ts | 67 ------------------- 6 files changed, 44 insertions(+), 143 deletions(-) delete mode 100644 tests/LSP17Extensions/helpers/UserOperation.ts diff --git a/contracts/LSP17Extensions/Extension4337.sol b/contracts/LSP17Extensions/Extension4337.sol index 59263c4f0..73a9a1e87 100644 --- a/contracts/LSP17Extensions/Extension4337.sol +++ b/contracts/LSP17Extensions/Extension4337.sol @@ -112,7 +112,7 @@ contract Extension4337 is LSP17Extension, IAccount { } /** - * @dev Get The address of the EntryPoint contract. + * @dev Get the address of the Entry Point contract that will execute the user operation. * @return The address of the EntryPoint contract */ function entryPoint() public view returns (address) { diff --git a/contracts/LSP17Extensions/OnERC721ReceivedExtension.sol b/contracts/LSP17Extensions/OnERC721ReceivedExtension.sol index 436bab17a..47f6be65e 100644 --- a/contracts/LSP17Extensions/OnERC721ReceivedExtension.sol +++ b/contracts/LSP17Extensions/OnERC721ReceivedExtension.sol @@ -9,7 +9,7 @@ import { * @dev LSP17 Extension that can be attached to a LSP17Extendable contract * to allow it to receive ERC721 tokens via `safeTransferFrom`. */ -// solhint-disable-next-line no-empty-block +// solhint-disable-next-line no-empty-blocks contract OnERC721ReceivedExtension is ERC721Holder { } diff --git a/tests/LSP17Extensions/Extension4337/4337.test.ts b/tests/LSP17Extensions/Extension4337/4337.test.ts index 6539e5c03..3a9f9ff68 100644 --- a/tests/LSP17Extensions/Extension4337/4337.test.ts +++ b/tests/LSP17Extensions/Extension4337/4337.test.ts @@ -13,7 +13,7 @@ import { UniversalProfile__factory, } from '../../../types'; import { deployEntryPoint, getBalance, isDeployed } from '../helpers/utils'; -import { ALL_PERMISSIONS, ERC725YDataKeys } from '../../../constants'; +import { ALL_PERMISSIONS, ERC725YDataKeys, OPERATION_TYPES } from '../../../constants'; import { combinePermissions } from '../../utils/helpers'; import { fillAndSign } from '../helpers/UserOp'; @@ -46,6 +46,7 @@ describe('4337', function () { universalProfile = await new UniversalProfile__factory(deployer).deploy( await deployer.getAddress(), + { value: parseEther('1') }, ); universalProfileAddress = universalProfile.address; @@ -103,29 +104,23 @@ describe('4337', function () { await universalProfile.setData(dataKeyWithOnlyPermission4337, Permission4337); - // execute call data + // execute calldata transferCallData = universalProfile.interface.encodeFunctionData('execute', [ - 0, + OPERATION_TYPES.CALL, ethers.constants.AddressZero, amountToTransfer, '0x1234', ]); - // send 1 ethers to universalProfile - await deployer.sendTransaction({ - to: universalProfile.address, - value: parseEther('1'), - }); - // stake on entrypoint const stakeAmount = parseEther('1'); await entryPoint.depositTo(universalProfileAddress, { value: stakeAmount }); }); it('should pass', async function () { - const address0BalanceBefore = await getBalance(ethers.constants.AddressZero); + const addressZeroBalanceBefore = await getBalance(ethers.constants.AddressZero); - const op = await fillAndSign( + const userOperation = await fillAndSign( { sender: universalProfileAddress, callData: transferCallData, @@ -134,17 +129,17 @@ describe('4337', function () { entryPoint, ); - await entryPoint.handleOps([op], bundler.address); + await entryPoint.handleOps([userOperation], bundler.address); - const address0BalanceAfter = await getBalance(ethers.constants.AddressZero); + const addressZeroBalanceAfter = await getBalance(ethers.constants.AddressZero); - expect(address0BalanceAfter - address0BalanceBefore).to.eq(amountToTransfer); + expect(addressZeroBalanceAfter - addressZeroBalanceBefore).to.eq(amountToTransfer); }); it('should fail when calling from wrong entrypoint', async function () { const anotherEntryPoint = await new EntryPoint__factory(deployer).deploy(); - const op = await fillAndSign( + const userOperation = await fillAndSign( { sender: universalProfileAddress, callData: transferCallData, @@ -153,15 +148,17 @@ describe('4337', function () { entryPoint, ); - await expect(anotherEntryPoint.handleOps([op], bundler.address)) + const opIndex = 0; //index into the array of ops to the failed one (in simulateValidation, this is always zero). + + await expect(anotherEntryPoint.handleOps([userOperation], bundler.address)) .to.be.revertedWithCustomError(entryPoint, 'FailedOp') - .withArgs(0, 'AA23 reverted: Only EntryPoint contract can call this'); + .withArgs(opIndex, 'AA23 reverted: Only EntryPoint contract can call this'); }); it('should fail when controller does not have 4337 permission', async function () { const anotherEntryPoint = await new EntryPoint__factory(deployer).deploy(); - const op = await fillAndSign( + const userOperation = await fillAndSign( { sender: universalProfileAddress, callData: transferCallData, @@ -170,14 +167,13 @@ describe('4337', function () { entryPoint, ); - await expect(anotherEntryPoint.handleOps([op], bundler.address)).to.be.revertedWithCustomError( - entryPoint, - 'FailedOp', - ); + await expect( + anotherEntryPoint.handleOps([userOperation], bundler.address), + ).to.be.revertedWithCustomError(entryPoint, 'FailedOp'); }); it('should fail when controller only has 4337 permission', async function () { - const op = await fillAndSign( + const userOperation = await fillAndSign( { sender: universalProfileAddress, callData: transferCallData, @@ -185,10 +181,9 @@ describe('4337', function () { controllerWithOnly4337Permission, entryPoint, ); - await expect(entryPoint.handleOps([op], bundler.address)).to.be.revertedWithCustomError( - entryPoint, - 'FailedOp', - ); + await expect( + entryPoint.handleOps([userOperation], bundler.address), + ).to.be.revertedWithCustomError(entryPoint, 'FailedOp'); }); it('should fail on invalid userop', async function () { diff --git a/tests/LSP17Extensions/helpers/UserOp.ts b/tests/LSP17Extensions/helpers/UserOp.ts index 8949af9e5..cb8b3141e 100644 --- a/tests/LSP17Extensions/helpers/UserOp.ts +++ b/tests/LSP17Extensions/helpers/UserOp.ts @@ -1,15 +1,30 @@ import { arrayify, defaultAbiCoder, hexDataSlice, keccak256 } from 'ethers/lib/utils'; import { BigNumber, Wallet } from 'ethers'; -import { AddressZero, callDataCost, rethrow } from './utils'; +import { AddressZero, callDataCost } from './utils'; import { ecsign, toRpcSig, keccak256 as keccak256_buffer } from 'ethereumjs-util'; -import { UserOperation } from './UserOperation'; import { Create2Factory } from './Create2Factory'; import { EntryPoint } from '@account-abstraction/contracts'; import { ethers } from 'ethers'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import * as typ from './solidityTypes'; + +export interface UserOperation { + sender: typ.address; + nonce: typ.uint256; + initCode: typ.bytes; + callData: typ.bytes; + callGasLimit: typ.uint256; + verificationGasLimit: typ.uint256; + preVerificationGas: typ.uint256; + maxFeePerGas: typ.uint256; + maxPriorityFeePerGas: typ.uint256; + paymasterAndData: typ.bytes; + signature: typ.bytes; +} export function packUserOp(op: UserOperation, forSignature = true): string { if (forSignature) { + // Encoding the UserOperation object fields into a single string for signature return defaultAbiCoder.encode( [ 'address', @@ -37,6 +52,7 @@ export function packUserOp(op: UserOperation, forSignature = true): string { ], ); } else { + // Encoding the UserOperation object fields into a single string including the signature return defaultAbiCoder.encode( [ 'address', @@ -68,37 +84,9 @@ export function packUserOp(op: UserOperation, forSignature = true): string { } } -export function packUserOp1(op: UserOperation): string { - return defaultAbiCoder.encode( - [ - 'address', // sender - 'uint256', // nonce - 'bytes32', // initCode - 'bytes32', // callData - 'uint256', // callGasLimit - 'uint256', // verificationGasLimit - 'uint256', // preVerificationGas - 'uint256', // maxFeePerGas - 'uint256', // maxPriorityFeePerGas - 'bytes32', // paymasterAndData - ], - [ - op.sender, - op.nonce, - keccak256(op.initCode), - keccak256(op.callData), - op.callGasLimit, - op.verificationGasLimit, - op.preVerificationGas, - op.maxFeePerGas, - op.maxPriorityFeePerGas, - keccak256(op.paymasterAndData), - ], - ); -} - export function getUserOpHash(op: UserOperation, entryPoint: string, chainId: number): string { const userOpHash = keccak256(packUserOp(op, true)); + // Encoding the UserOperation hash, entryPoint address, and chainId for final hash computation const enc = defaultAbiCoder.encode( ['bytes32', 'address', 'uint256'], [userOpHash, entryPoint, chainId], @@ -213,7 +201,7 @@ export async function fillUserOp( try { op1.nonce = await entryPoint.getNonce(op1.sender, signerKeyAsUint192); } catch { - rethrow(); + op1.nonce = 0; } } if (op1.callGasLimit == null && op.callData != null) { diff --git a/tests/LSP17Extensions/helpers/UserOperation.ts b/tests/LSP17Extensions/helpers/UserOperation.ts deleted file mode 100644 index 8754e4ed1..000000000 --- a/tests/LSP17Extensions/helpers/UserOperation.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as typ from './solidityTypes'; - -export interface UserOperation { - sender: typ.address; - nonce: typ.uint256; - initCode: typ.bytes; - callData: typ.bytes; - callGasLimit: typ.uint256; - verificationGasLimit: typ.uint256; - preVerificationGas: typ.uint256; - maxFeePerGas: typ.uint256; - maxPriorityFeePerGas: typ.uint256; - paymasterAndData: typ.bytes; - signature: typ.bytes; -} diff --git a/tests/LSP17Extensions/helpers/utils.ts b/tests/LSP17Extensions/helpers/utils.ts index daf591c81..10c338d83 100644 --- a/tests/LSP17Extensions/helpers/utils.ts +++ b/tests/LSP17Extensions/helpers/utils.ts @@ -30,70 +30,3 @@ export async function isDeployed(addr: string): Promise { const code = await ethers.provider.getCode(addr); return code.length > 2; } - -const panicCodes: { [key: number]: string } = { - // from https://docs.soliditylang.org/en/v0.8.0/control-structures.html - 0x01: 'assert(false)', - 0x11: 'arithmetic overflow/underflow', - 0x12: 'divide by zero', - 0x21: 'invalid enum value', - 0x22: 'storage byte array that is incorrectly encoded', - 0x31: '.pop() on an empty array.', - 0x32: 'array sout-of-bounds or negative index', - 0x41: 'memory overflow', - 0x51: 'zero-initialized variable of internal function type', -}; - -// rethrow "cleaned up" exception. -// - stack trace goes back to method (or catch) line, not inner provider -// - attempt to parse revert data (needed for geth) -// use with ".catch(rethrow())", so that current source file/line is meaningful. -export function rethrow(): (e: Error) => void { - const callerStack = new Error().stack - .replace(/Error.*\n.*at.*\n/, '') - .replace(/.*at.* \(internal[\s\S]*/, ''); - - // eslint-disable-next-line - if (arguments[0] != null) { - throw new Error('must use .catch(rethrow()), and NOT .catch(rethrow)'); - } - return function (e: Error) { - const solstack = e.stack.match(/((?:.* at .*\.sol.*\n)+)/); - const stack = (solstack != null ? solstack[1] : '') + callerStack; - - const found = /error=.*?"data":"(.*?)"/.exec(e.message); - let message: string; - if (found != null) { - const data = found[1]; - message = decodeRevertReason(data) ?? e.message + ' - ' + data.slice(0, 100); - } else { - message = e.message; - } - const err = new Error(message); - err.stack = 'Error: ' + message + '\n' + stack; - throw err; - }; -} - -export function decodeRevertReason(data: string, nullIfNoMatch = true): string | null { - const methodSig = data.slice(0, 10); - const dataParams = '0x' + data.slice(10); - - if (methodSig === '0x08c379a0') { - const [err] = ethers.utils.defaultAbiCoder.decode(['string'], dataParams); - return `Error(${err})`; - } else if (methodSig === '0x00fa072b') { - const [opindex, paymaster, msg] = ethers.utils.defaultAbiCoder.decode( - ['uint256', 'address', 'string'], - dataParams, - ); - return `FailedOp(${opindex}, ${paymaster !== AddressZero ? paymaster : 'none'}, ${msg})`; - } else if (methodSig === '0x4e487b71') { - const [code] = ethers.utils.defaultAbiCoder.decode(['uint256'], dataParams); - return `Panic(${panicCodes[code] ?? code} + ')`; - } - if (!nullIfNoMatch) { - return data; - } - return null; -} From cfadcf8d88352ea74a9815959cd369e8dc215c0d Mon Sep 17 00:00:00 2001 From: maxvia87 Date: Mon, 9 Oct 2023 11:29:45 +0200 Subject: [PATCH 13/15] fix: check for _LSP20_VERIFY_CALL_RESULT_MAGIC_VALUE --- contracts/LSP17Extensions/Extension4337.sol | 8 +++++--- docs/contracts/LSP17Extensions/Extension4337.md | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/contracts/LSP17Extensions/Extension4337.sol b/contracts/LSP17Extensions/Extension4337.sol index 73a9a1e87..e37848131 100644 --- a/contracts/LSP17Extensions/Extension4337.sol +++ b/contracts/LSP17Extensions/Extension4337.sol @@ -28,7 +28,9 @@ import {LSP6Utils} from "../LSP6KeyManager/LSP6Utils.sol"; import { UserOperation } from "@account-abstraction/contracts/interfaces/UserOperation.sol"; -import {_ERC1271_FAILVALUE} from "../LSP0ERC725Account/LSP0Constants.sol"; +import { + _LSP20_VERIFY_CALL_RESULT_MAGIC_VALUE +} from "../LSP20CallVerification/LSP20Constants.sol"; contract Extension4337 is LSP17Extension, IAccount { using ECDSA for bytes32; @@ -85,8 +87,8 @@ contract Extension4337 is LSP17Extension, IAccount { receivedCalldata: userOp.callData }); - // if the call verifier returns _ERC1271_FAILVALUE, the caller is not authorized to make this call - if (_ERC1271_FAILVALUE == magicValue) { + // if the call verifier returns a different magic value than _LSP20_VERIFY_CALL_RESULT_MAGIC_VALUE, return signature validation failed + if (_LSP20_VERIFY_CALL_RESULT_MAGIC_VALUE != magicValue) { return _SIG_VALIDATION_FAILED; } diff --git a/docs/contracts/LSP17Extensions/Extension4337.md b/docs/contracts/LSP17Extensions/Extension4337.md index f7642dcf0..43c15b407 100644 --- a/docs/contracts/LSP17Extensions/Extension4337.md +++ b/docs/contracts/LSP17Extensions/Extension4337.md @@ -55,7 +55,7 @@ constructor(address entryPoint_); function entryPoint() external view returns (address); ``` -Get The address of the EntryPoint contract. +Get the address of the Entry Point contract that will execute the user operation. #### Returns From 2821ffa67da43afbcbc2bc459d666f077dd38078 Mon Sep 17 00:00:00 2001 From: maxvia87 Date: Mon, 9 Oct 2023 11:38:02 +0200 Subject: [PATCH 14/15] fix: signature verification in extension --- contracts/LSP17Extensions/Extension4337.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/LSP17Extensions/Extension4337.sol b/contracts/LSP17Extensions/Extension4337.sol index e37848131..6d2945f69 100644 --- a/contracts/LSP17Extensions/Extension4337.sol +++ b/contracts/LSP17Extensions/Extension4337.sol @@ -28,9 +28,6 @@ import {LSP6Utils} from "../LSP6KeyManager/LSP6Utils.sol"; import { UserOperation } from "@account-abstraction/contracts/interfaces/UserOperation.sol"; -import { - _LSP20_VERIFY_CALL_RESULT_MAGIC_VALUE -} from "../LSP20CallVerification/LSP20Constants.sol"; contract Extension4337 is LSP17Extension, IAccount { using ECDSA for bytes32; @@ -87,8 +84,11 @@ contract Extension4337 is LSP17Extension, IAccount { receivedCalldata: userOp.callData }); - // if the call verifier returns a different magic value than _LSP20_VERIFY_CALL_RESULT_MAGIC_VALUE, return signature validation failed - if (_LSP20_VERIFY_CALL_RESULT_MAGIC_VALUE != magicValue) { + // if the call verifier returns a different magic value, return signature validation failed + if ( + bytes3(magicValue) != + bytes3(ILSP20CallVerifier.lsp20VerifyCall.selector) + ) { return _SIG_VALIDATION_FAILED; } From a1a4cda00150cda31aeb996955781904d38a5ff9 Mon Sep 17 00:00:00 2001 From: maxvia87 Date: Mon, 9 Oct 2023 15:17:40 +0200 Subject: [PATCH 15/15] refactor: remove autorefund logic from 4337 extension --- contracts/LSP17Extensions/Extension4337.sol | 25 +------------------ .../LSP17Extensions/Extension4337.md | 16 ++++++------ 2 files changed, 9 insertions(+), 32 deletions(-) diff --git a/contracts/LSP17Extensions/Extension4337.sol b/contracts/LSP17Extensions/Extension4337.sol index 6d2945f69..81cf154d3 100644 --- a/contracts/LSP17Extensions/Extension4337.sol +++ b/contracts/LSP17Extensions/Extension4337.sol @@ -3,12 +3,6 @@ pragma solidity ^0.8.9; // interfaces import {IAccount} from "@account-abstraction/contracts/interfaces/IAccount.sol"; -import { - IStakeManager -} from "@account-abstraction/contracts/interfaces/IStakeManager.sol"; -import { - IERC725X -} from "@erc725/smart-contracts/contracts/interfaces/IERC725X.sol"; import { IERC725Y } from "@erc725/smart-contracts/contracts/interfaces/IERC725Y.sol"; @@ -52,7 +46,7 @@ contract Extension4337 is LSP17Extension, IAccount { function validateUserOp( UserOperation calldata userOp, bytes32 userOpHash, - uint256 missingAccountFunds + uint256 /* missingAccountFunds */ ) external returns (uint256) { require( _extendableMsgSender() == _ENTRY_POINT, @@ -92,23 +86,6 @@ contract Extension4337 is LSP17Extension, IAccount { return _SIG_VALIDATION_FAILED; } - // if entryPoint is missing funds to pay for the tx, deposit funds - if (missingAccountFunds > 0) { - // deposit bytes to entryPoint - bytes memory depositToBytes = abi.encodeWithSelector( - IStakeManager.depositTo.selector, - msg.sender - ); - - // send funds from Universal Profile to ENTRY_POINT - IERC725X(msg.sender).execute( - 0, - _ENTRY_POINT, - missingAccountFunds, - depositToBytes - ); - } - // if sig validation passed, return 0 return 0; } diff --git a/docs/contracts/LSP17Extensions/Extension4337.md b/docs/contracts/LSP17Extensions/Extension4337.md index 43c15b407..9c8b510c2 100644 --- a/docs/contracts/LSP17Extensions/Extension4337.md +++ b/docs/contracts/LSP17Extensions/Extension4337.md @@ -102,8 +102,8 @@ See [`IERC165-supportsInterface`](#ierc165-supportsinterface). - Specification details: [**LSP-17-Extensions**](https://github.com/lukso-network/lips/tree/main/LSPs/LSP-17-Extensions.md#validateuserop) - Solidity implementation: [`Extension4337.sol`](https://github.com/lukso-network/lsp-smart-contracts/blob/develop/contracts/LSP17Extensions/Extension4337.sol) -- Function signature: `validateUserOp(UserOperation,bytes32,uint256)` -- Function selector: `0xe86fc51e` +- Function signature: `validateUserOp(UserOperation,bytes32,)` +- Function selector: `0x68159319` ::: @@ -111,7 +111,7 @@ See [`IERC165-supportsInterface`](#ierc165-supportsinterface). function validateUserOp( UserOperation userOp, bytes32 userOpHash, - uint256 missingAccountFunds + uint256 ) external nonpayable returns (uint256); ``` @@ -121,11 +121,11 @@ Must validate caller is the entryPoint. Must validate the signature and nonce #### Parameters -| Name | Type | Description | -| --------------------- | :-------------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `userOp` | `UserOperation` | the operation that is about to be executed. | -| `userOpHash` | `bytes32` | hash of the user's request data. can be used as the basis for signature. | -| `missingAccountFunds` | `uint256` | missing funds on the account's deposit in the entrypoint. This is the minimum amount to transfer to the sender(entryPoint) to be able to make the call. The excess is left as a deposit in the entrypoint, for future calls. can be withdrawn anytime using "entryPoint.withdrawTo()" In case there is a paymaster in the request (or the current deposit is high enough), this value will be zero. | +| Name | Type | Description | +| ------------ | :-------------: | ------------------------------------------------------------------------ | +| `userOp` | `UserOperation` | the operation that is about to be executed. | +| `userOpHash` | `bytes32` | hash of the user's request data. can be used as the basis for signature. | +| `_2` | `uint256` | - | #### Returns