From e6d5769b3e897595a87679d928c5a66999018e10 Mon Sep 17 00:00:00 2001 From: livingrockrises <90545960+livingrockrises@users.noreply.github.com> Date: Sun, 7 Apr 2024 19:23:02 +0530 Subject: [PATCH 01/21] hardhat base utils i --- contracts/mocks/Imports.sol | 5 + contracts/mocks/MockValidator.sol | 3 + package.json | 12 +- remappings.txt | 5 +- test/hardhat/Lock.ts | 3 + .../biconomy-sponsorship-paymaster-specs.ts | 166 +++++++++ test/hardhat/utils/deployment.ts | 141 +++++++ test/hardhat/utils/general.ts | 60 +++ test/hardhat/utils/testUtils.ts | 229 ++++++++++++ test/hardhat/utils/types.ts | 34 ++ test/hardhat/utils/userOpHelpers.ts | 347 ++++++++++++++++++ 11 files changed, 1001 insertions(+), 4 deletions(-) create mode 100644 contracts/mocks/MockValidator.sol create mode 100644 test/hardhat/biconomy-sponsorship-paymaster-specs.ts create mode 100644 test/hardhat/utils/deployment.ts create mode 100644 test/hardhat/utils/general.ts create mode 100644 test/hardhat/utils/testUtils.ts create mode 100644 test/hardhat/utils/types.ts create mode 100644 test/hardhat/utils/userOpHelpers.ts diff --git a/contracts/mocks/Imports.sol b/contracts/mocks/Imports.sol index d2a4197..3eb785e 100644 --- a/contracts/mocks/Imports.sol +++ b/contracts/mocks/Imports.sol @@ -4,3 +4,8 @@ pragma solidity ^0.8.24; /* solhint-disable reason-string */ import "account-abstraction/contracts/core/EntryPoint.sol"; +import "account-abstraction/contracts/core/EntryPointSimulations.sol"; + +import "@biconomy-devx/erc7579-msa/contracts/SmartAccount.sol"; +import "@biconomy-devx/erc7579-msa/contracts/factory/AccountFactory.sol"; + diff --git a/contracts/mocks/MockValidator.sol b/contracts/mocks/MockValidator.sol new file mode 100644 index 0000000..5f7bdd9 --- /dev/null +++ b/contracts/mocks/MockValidator.sol @@ -0,0 +1,3 @@ +pragma solidity ^0.8.24; + +import "@biconomy-devx/erc7579-msa/test/foundry/mocks/MockValidator.sol"; \ No newline at end of file diff --git a/package.json b/package.json index eaefe02..1227124 100644 --- a/package.json +++ b/package.json @@ -7,34 +7,40 @@ "url": "https://github.com/bcnmy" }, "dependencies": { + "@biconomy-devx/erc7579-msa": "^0.0.4", "@openzeppelin/contracts": "^5.0.1", "hardhat": "^2.20.1" }, "devDependencies": { "@bonadocs/docgen": "^1.0.1-alpha.1", + "@ethersproject/abstract-provider": "^5.7.0", "@nomicfoundation/hardhat-chai-matchers": "^2.0.6", "@nomicfoundation/hardhat-ethers": "^3.0.5", "@nomicfoundation/hardhat-foundry": "^1.1.1", "@nomicfoundation/hardhat-network-helpers": "^1.0.10", "@nomicfoundation/hardhat-toolbox": "^4.0.0", "@nomicfoundation/hardhat-verify": "^2.0.4", + "@nomiclabs/hardhat-ethers": "^2.2.3", "@prb/test": "^0.6.4", "@typechain/ethers-v6": "^0.5.1", "@typechain/hardhat": "^9.1.0", "@types/chai": "^4.3.11", "@types/mocha": ">=10.0.6", "@types/node": ">=20.11.19", + "account-abstraction": "github:eth-infinitism/account-abstraction#develop", "chai": "^4.3.7", "codecov": "^3.8.3", "ethers": "^6.11.1", "forge-std": "github:foundry-rs/forge-std#v1.7.6", - "modulekit": "github:rhinestonewtf/modulekit", - "solady": "github:vectorized/solady", - "account-abstraction": "github:eth-infinitism/account-abstraction#develop", + "hardhat-deploy": "^0.11.45", + "hardhat-deploy-ethers": "^0.4.1", "hardhat-gas-reporter": "^1.0.10", "hardhat-storage-layout": "^0.1.7", + "modulekit": "github:rhinestonewtf/modulekit", "prettier": "^3.2.5", "prettier-plugin-solidity": "^1.3.1", + "sentinellist": "github:zeroknots/sentinellist", + "solady": "github:vectorized/solady", "solhint": "^4.1.1", "solhint-plugin-prettier": "^0.1.0", "solidity-coverage": "^0.8.7", diff --git a/remappings.txt b/remappings.txt index e91ca09..af5268a 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,4 +1,7 @@ @openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ @prb/test/=node_modules/@prb/test/ forge-std/=node_modules/forge-std/ -modulekit/=node_modules/modulekit/src/ \ No newline at end of file +account-abstraction=node_modules/account-abstraction/ +modulekit/=node_modules/modulekit/src/ +sentinellist/=node_modules/sentinellist/ +solady/=node_modules/solady \ No newline at end of file diff --git a/test/hardhat/Lock.ts b/test/hardhat/Lock.ts index 98693fe..8e49635 100644 --- a/test/hardhat/Lock.ts +++ b/test/hardhat/Lock.ts @@ -23,6 +23,9 @@ describe("Lock", function () { const Lock = await ethers.getContractFactory("Lock"); const lock = await Lock.deploy(unlockTime, { value: lockedAmount }); + const SmartAccount = await ethers.getContractFactory("SmartAccount"); + const smartAccount = await SmartAccount.deploy(); + return { lock, unlockTime, lockedAmount, owner, otherAccount }; } diff --git a/test/hardhat/biconomy-sponsorship-paymaster-specs.ts b/test/hardhat/biconomy-sponsorship-paymaster-specs.ts new file mode 100644 index 0000000..0db6c81 --- /dev/null +++ b/test/hardhat/biconomy-sponsorship-paymaster-specs.ts @@ -0,0 +1,166 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { AbiCoder, AddressLike, BytesLike, Signer, parseEther, toBeHex } from "ethers"; +import { + EntryPoint, + EntryPoint__factory, + MockValidator, + MockValidator__factory, + SmartAccount, + SmartAccount__factory, + AccountFactory, + AccountFactory__factory, + BiconomySponsorshipPaymaster, + BiconomySponsorshipPaymaster__factory +} from "../../typechain-types"; + +import { DefaultsForUserOp, fillAndSign, fillSignAndPack, packUserOp, simulateValidation } from './utils/userOpHelpers' +import { parseValidationData } from "./utils/testUtils"; + + +export const AddressZero = ethers.ZeroAddress; + +const MOCK_VALID_UNTIL = "0x00000000deadbeef"; +const MOCK_VALID_AFTER = "0x0000000000001234"; +const MARKUP = 1100000; +export const ENTRY_POINT_V7 = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"; + +const coder = AbiCoder.defaultAbiCoder() + +export async function deployEntryPoint( + provider = ethers.provider + ): Promise { + const epf = await (await ethers.getContractFactory("EntryPoint")).deploy(); + // Retrieve the deployed contract bytecode + const deployedCode = await ethers.provider.getCode( + await epf.getAddress(), + ); + + // Use hardhat_setCode to set the contract code at the specified address + await ethers.provider.send("hardhat_setCode", [ENTRY_POINT_V7, deployedCode]); + + return epf.attach(ENTRY_POINT_V7) as EntryPoint; +} + +describe("EntryPoint with Biconomy Sponsorship Paymaster", function () { + let entryPoint: EntryPoint; + let depositorSigner: Signer; + let walletOwner: Signer; + let walletAddress: string, paymasterAddress: string; + let paymasterDepositorId: string; + let ethersSigner: Signer[]; + let offchainSigner: Signer, deployer: Signer, feeCollector: Signer; + let paymaster: BiconomySponsorshipPaymaster; + let smartWalletImp: SmartAccount; + let ecdsaModule: MockValidator; + let walletFactory: AccountFactory; + + beforeEach(async function () { + ethersSigner = await ethers.getSigners(); + entryPoint = await deployEntryPoint(); + + deployer = ethersSigner[0]; + offchainSigner = ethersSigner[1]; + depositorSigner = ethersSigner[2]; + feeCollector = ethersSigner[3]; + walletOwner = deployer; + + paymasterDepositorId = await depositorSigner.getAddress(); + + const offchainSignerAddress = await offchainSigner.getAddress(); + const walletOwnerAddress = await walletOwner.getAddress(); + const feeCollectorAddess = await feeCollector.getAddress(); + + ecdsaModule = await new MockValidator__factory( + deployer + ).deploy(); + + paymaster = + await new BiconomySponsorshipPaymaster__factory(deployer).deploy( + await deployer.getAddress(), + await entryPoint.getAddress(), + offchainSignerAddress, + feeCollectorAddess + ); + + smartWalletImp = await new SmartAccount__factory( + deployer + ).deploy(); + + walletFactory = await new AccountFactory__factory(deployer).deploy( + await smartWalletImp.getAddress(), + ); + + await walletFactory + .connect(deployer) + .addStake( 86400, { value: parseEther("2") }); + + const smartAccountDeploymentIndex = 0; + + // Module initialization data, encoded + const moduleInstallData = ethers.solidityPacked(["address"], [walletOwnerAddress]); + + await walletFactory.createAccount( + await ecdsaModule.getAddress(), + moduleInstallData, + smartAccountDeploymentIndex + ); + + const expected = await walletFactory.getCounterFactualAddress( + await ecdsaModule.getAddress(), + moduleInstallData, + smartAccountDeploymentIndex + ); + + walletAddress = expected; + + paymasterAddress = await paymaster.getAddress(); + + await paymaster + .connect(deployer) + .addStake(86400, { value: parseEther("2") }); + + await paymaster.depositFor(paymasterDepositorId, { value: parseEther("1") }); + + await entryPoint.depositTo(paymasterAddress, { value: parseEther("1") }); + }); + + describe("#validatePaymasterUserOp and #sendSponsoredTx", () => { + it("succeed with valid signature", async () => { + const nonceKey = ethers.zeroPadBytes(await ecdsaModule.getAddress(), 24); + const userOp1 = await fillAndSign({ + sender: walletAddress, + paymaster: paymasterAddress, + paymasterData: ethers.concat([ + ethers.zeroPadValue(paymasterDepositorId, 20), + ethers.zeroPadValue(toBeHex(MOCK_VALID_UNTIL), 6), + ethers.zeroPadValue(toBeHex(MOCK_VALID_AFTER), 6), + ethers.zeroPadValue(toBeHex(MARKUP), 4), + '0x' + '00'.repeat(65) + ]) + }, walletOwner, entryPoint, 'getNonce', nonceKey) + const hash = await paymaster.getHash(packUserOp(userOp1), paymasterDepositorId, MOCK_VALID_UNTIL, MOCK_VALID_AFTER, MARKUP) + const sig = await offchainSigner.signMessage(ethers.getBytes(hash)) + const userOp = await fillSignAndPack({ + ...userOp1, + paymaster: paymasterAddress, + paymasterData: ethers.concat([ + ethers.zeroPadValue(paymasterDepositorId, 20), + ethers.zeroPadValue(toBeHex(MOCK_VALID_UNTIL), 6), + ethers.zeroPadValue(toBeHex(MOCK_VALID_AFTER), 6), + ethers.zeroPadValue(toBeHex(MARKUP), 4), + sig + ]) + }, walletOwner, entryPoint, 'getNonce', nonceKey) + console.log("userOp: ", userOp); + const res = await simulateValidation(userOp, await entryPoint.getAddress()) + const validationData = parseValidationData(res.returnInfo.paymasterValidationData) + expect(validationData).to.eql({ + aggregator: AddressZero, + validAfter: parseInt(MOCK_VALID_AFTER), + validUntil: parseInt(MOCK_VALID_UNTIL) + }) + }); + }); +}) + diff --git a/test/hardhat/utils/deployment.ts b/test/hardhat/utils/deployment.ts new file mode 100644 index 0000000..282831d --- /dev/null +++ b/test/hardhat/utils/deployment.ts @@ -0,0 +1,141 @@ +import { BytesLike, HDNodeWallet, Signer } from "ethers"; +import { deployments, ethers } from "hardhat"; +import { AccountFactory, BiconomySponsorshipPaymaster, EntryPoint, MockValidator, SmartAccount } from "../../../typechain-types"; +import { TASK_DEPLOY } from "hardhat-deploy"; +import { DeployResult } from "hardhat-deploy/dist/types"; + +export const ENTRY_POINT_V7 = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"; + +/** + * Generic function to deploy a contract using ethers.js. + * + * @param contractName The name of the contract to deploy. + * @param deployer The Signer object representing the deployer account. + * @returns A promise that resolves to the deployed contract instance. + */ +export async function deployContract( + contractName: string, + deployer: Signer, + ): Promise { + const ContractFactory = await ethers.getContractFactory( + contractName, + deployer, + ); + const contract = await ContractFactory.deploy(); + await contract.waitForDeployment(); + return contract as T; +} + +/** + * Deploys the EntryPoint contract with a deterministic deployment. + * @returns A promise that resolves to the deployed EntryPoint contract instance. + */ +export async function getDeployedEntrypoint() : Promise { + const [deployer] = await ethers.getSigners(); + + // Deploy the contract normally to get its bytecode + const EntryPoint = await ethers.getContractFactory("EntryPoint"); + const entryPoint = await EntryPoint.deploy(); + await entryPoint.waitForDeployment(); + + // Retrieve the deployed contract bytecode + const deployedCode = await ethers.provider.getCode( + await entryPoint.getAddress(), + ); + + // Use hardhat_setCode to set the contract code at the specified address + await ethers.provider.send("hardhat_setCode", [ENTRY_POINT_V7, deployedCode]); + + return EntryPoint.attach(ENTRY_POINT_V7) as EntryPoint; +} + +/** + * Deploys the (MSA) Smart Account implementation contract with a deterministic deployment. + * @returns A promise that resolves to the deployed SA implementation contract instance. + */ +export async function getDeployedMSAImplementation(): Promise { + const accounts: Signer[] = await ethers.getSigners(); + const addresses = await Promise.all( + accounts.map((account) => account.getAddress()), + ); + + const SmartAccount = await ethers.getContractFactory("SmartAccount"); + const deterministicMSAImpl = await deployments.deploy("SmartAccount", { + from: addresses[0], + deterministicDeployment: true, + }); + + return SmartAccount.attach(deterministicMSAImpl.address) as SmartAccount; +} + +/** + * Deploys the AccountFactory contract with a deterministic deployment. + * @returns A promise that resolves to the deployed EntryPoint contract instance. + */ +export async function getDeployedAccountFactory( + implementationAddress: string, + // Note: this could be converted to dto so that additional args can easily be passed + ): Promise { + const accounts: Signer[] = await ethers.getSigners(); + const addresses = await Promise.all( + accounts.map((account) => account.getAddress()), + ); + + const AccountFactory = await ethers.getContractFactory("AccountFactory"); + const deterministicAccountFactory = await deployments.deploy( + "AccountFactory", + { + from: addresses[0], + deterministicDeployment: true, + args: [implementationAddress], + }, + ); + + return AccountFactory.attach( + deterministicAccountFactory.address, + ) as AccountFactory; +} + +/** + * Deploys the MockValidator contract with a deterministic deployment. + * @returns A promise that resolves to the deployed MockValidator contract instance. + */ +export async function getDeployedMockValidator(): Promise { + const accounts: Signer[] = await ethers.getSigners(); + const addresses = await Promise.all( + accounts.map((account) => account.getAddress()), + ); + + const MockValidator = await ethers.getContractFactory("MockValidator"); + const deterministicMockValidator = await deployments.deploy("MockValidator", { + from: addresses[0], + deterministicDeployment: true, + }); + + return MockValidator.attach( + deterministicMockValidator.address, + ) as MockValidator; +} + +/** + * Deploys the MockValidator contract with a deterministic deployment. + * @returns A promise that resolves to the deployed MockValidator contract instance. + */ +export async function getDeployedSponsorshipPaymaster(owner: string, entryPoint: string, verifyingSigner: string, feeCollector: string): Promise { + const accounts: Signer[] = await ethers.getSigners(); + const addresses = await Promise.all( + accounts.map((account) => account.getAddress()), + ); + + const BiconomySponsorshipPaymaster = await ethers.getContractFactory("BiconomySponsorshipPaymaster"); + const deterministicSponsorshipPaymaster = await deployments.deploy("BiconomySponsorshipPaymaster", { + from: addresses[0], + deterministicDeployment: true, + args: [owner, entryPoint, verifyingSigner, feeCollector], + }); + + return BiconomySponsorshipPaymaster.attach( + deterministicSponsorshipPaymaster.address, + ) as BiconomySponsorshipPaymaster; +} + diff --git a/test/hardhat/utils/general.ts b/test/hardhat/utils/general.ts new file mode 100644 index 0000000..7e9e596 --- /dev/null +++ b/test/hardhat/utils/general.ts @@ -0,0 +1,60 @@ +import { BigNumberish } from "ethers"; +import { ethers } from "hardhat"; + +/** + * Encodes data using the defaultAbiCoder from ethers.AbiCoder. + * @param types The types of the values being encoded. + * @param values The values to encode. + * @returns The encoded data. + */ +export function encodeData(types: string[], values: any[]): string { + return ethers.AbiCoder.defaultAbiCoder().encode(types, values); +} + +/** + * Converts a regular string to a bytes32 string. + * + * @param text The regular string to convert. + * @returns The converted bytes32 string. + */ +export const toBytes32 = (text: string): string => { + return ethers.encodeBytes32String(text); +}; + +/** + * Converts a bytes32 string to a regular string. + * + * @param bytes32 The bytes32 string to convert. + * @returns The converted regular string. + */ +export const fromBytes32 = (bytes32: string): string => { + return ethers.decodeBytes32String(bytes32); +}; + +/** + * Converts a numeric value to its equivalent in 18 decimal places. + * @param value The numeric value to convert. + * @returns The equivalent value in 18 decimal places as a bigint. + */ +export const to18 = (value: BigNumberish): bigint => { + return ethers.parseUnits(value.toString(), 18); +}; + +/** + * Converts a value from 18 decimal places to a string representation. + * + * @param value The value to convert. + * @returns The string representation of the converted value. + */ +export const from18 = (value: bigint): string => { + return ethers.formatUnits(value, 18); +}; + +/** + * Converts the given amount to Gwei. + * @param amount - The amount to convert. + * @returns The converted amount in Gwei. + */ +export function toGwei(amount: BigNumberish): BigNumberish { + return ethers.parseUnits(amount.toString(), "gwei"); +} diff --git a/test/hardhat/utils/testUtils.ts b/test/hardhat/utils/testUtils.ts new file mode 100644 index 0000000..06c4218 --- /dev/null +++ b/test/hardhat/utils/testUtils.ts @@ -0,0 +1,229 @@ +import { AbiCoder, AddressLike, BigNumberish, Contract, Interface, dataSlice, parseEther, toBeHex } from 'ethers'; +import { ethers } from 'hardhat' +import { EntryPoint__factory, IERC20 } from '../../../typechain-types'; + +// define mode and exec type enums +export const CALLTYPE_SINGLE = "0x00"; // 1 byte +export const CALLTYPE_BATCH = "0x01"; // 1 byte +export const EXECTYPE_DEFAULT = "0x00"; // 1 byte +export const EXECTYPE_TRY = "0x01"; // 1 byte +export const EXECTYPE_DELEGATE = "0xFF"; // 1 byte +export const MODE_DEFAULT = "0x00000000"; // 4 bytes +export const UNUSED = "0x00000000"; // 4 bytes +export const MODE_PAYLOAD = "0x00000000000000000000000000000000000000000000"; // 22 bytes + +export const AddressZero = ethers.ZeroAddress; +export const HashZero = ethers.ZeroHash +export const ONE_ETH = parseEther('1') +export const TWO_ETH = parseEther('2') +export const FIVE_ETH = parseEther('5') +export const maxUint48 = (2 ** 48) - 1 + +export const tostr = (x: any): string => x != null ? x.toString() : 'null' + +const coder = AbiCoder.defaultAbiCoder() + +export interface ValidationData { + aggregator: string + validAfter: number + validUntil: number +} + +export 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' +} +export const Erc20 = [ + "function transfer(address _receiver, uint256 _value) public returns (bool success)", + "function transferFrom(address, address, uint256) public returns (bool)", + "function approve(address _spender, uint256 _value) public returns (bool success)", + "function allowance(address _owner, address _spender) public view returns (uint256 remaining)", + "function balanceOf(address _owner) public view returns (uint256 balance)", + "event Approval(address indexed _owner, address indexed _spender, uint256 _value)", + ]; + +export const Erc20Interface = new ethers.Interface(Erc20); + +export const encodeTransfer = ( + target: string, + amount: string | number + ): string => { + return Erc20Interface.encodeFunctionData("transfer", [target, amount]); +}; + +export const encodeTransferFrom = ( + from: string, + target: string, + amount: string | number + ): string => { + return Erc20Interface.encodeFunctionData("transferFrom", [ + from, + target, + amount, + ]); +}; + +// 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 + } +} + +const decodeRevertReasonContracts = new Interface([ + ...EntryPoint__factory.createInterface().fragments, + 'error ECDSAInvalidSignature()' +]) // .filter(f => f.type === 'error')) + +export function decodeRevertReason (data: string | Error, nullIfNoMatch = true): string | null { + if (typeof data !== 'string') { + const err = data as any + data = (err.data ?? err.error?.data) as string + if (typeof data !== 'string') throw err + } + + const methodSig = data.slice(0, 10) + const dataParams = '0x' + data.slice(10) + + // can't add Error(string) to xface... + if (methodSig === '0x08c379a0') { + const [err] = coder.decode(['string'], dataParams) + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `Error(${err})` + } else if (methodSig === '0x4e487b71') { + const [code] = coder.decode(['uint256'], dataParams) + return `Panic(${panicCodes[code] ?? code} + ')` + } + + try { + const err = decodeRevertReasonContracts.parseError(data) + // treat any error "bytes" argument as possible error to decode (e.g. FailedOpWithRevert, PostOpReverted) + const args = err!.args.map((arg: any, index) => { + switch (err?.fragment.inputs[index].type) { + case 'bytes' : return decodeRevertReason(arg) + case 'string': return `"${(arg as string)}"` + default: return arg + } + }) + return `${err!.name}(${args.join(',')})` + } catch (e) { + // throw new Error('unsupported errorSig ' + data) + if (!nullIfNoMatch) { + return data + } + return null + } +} + +export function tonumber (x: any): number { + try { + return parseFloat(x.toString()) + } catch (e: any) { + console.log('=== failed to parseFloat:', x, (e).message) + return NaN + } +} + +// just throw 1eth from account[0] to the given address (or contract instance) +export async function fund (contractOrAddress: string | Contract, amountEth = '1'): Promise { + let address: string + if (typeof contractOrAddress === 'string') { + address = contractOrAddress + } else { + address = await contractOrAddress.getAddress() + } + const [firstSigner] = await ethers.getSigners(); + await firstSigner.sendTransaction({ to: address, value: parseEther(amountEth) }) +} + +export async function getBalance (address: string): Promise { + const balance = await ethers.provider.getBalance(address) + return parseInt(balance.toString()) +} + +export async function getTokenBalance (token: IERC20, address: string): Promise { + const balance = await token.balanceOf(address) + return parseInt(balance.toString()) +} + +export async function isDeployed (addr: string): Promise { + const code = await ethers.provider.getCode(addr) + return code.length > 2 +} + +// Getting initcode for AccountFactory which accepts one validator (with ECDSA owner required for installation) +export async function getInitCode( + ownerAddress: AddressLike, + factoryAddress: AddressLike, + validatorAddress: AddressLike, + saDeploymentIndex: number = 0, +): Promise { + const AccountFactory = await ethers.getContractFactory("AccountFactory"); + const moduleInstallData = ethers.solidityPacked(["address"], [ownerAddress]); + + // Encode the createAccount function call with the provided parameters + const factoryDeploymentData = AccountFactory.interface + .encodeFunctionData("createAccount", [ + validatorAddress, + moduleInstallData, + saDeploymentIndex, + ]) + .slice(2); + + return factoryAddress + factoryDeploymentData; +} + +export function callDataCost (data: string): number { + return ethers.getBytes(data) + .map(x => x === 0 ? 4 : 16) + .reduce((sum, x) => sum + x) +} + +export function parseValidationData (validationData: BigNumberish): ValidationData { + const data = ethers.zeroPadValue(toBeHex(validationData), 32) + + // string offsets start from left (msb) + const aggregator = dataSlice(data, 32 - 20) + let validUntil = parseInt(dataSlice(data, 32 - 26, 32 - 20)) + if (validUntil === 0) { + validUntil = maxUint48 + } + const validAfter = parseInt(dataSlice(data, 0, 6)) + + return { + aggregator, + validAfter, + validUntil + } +} + + diff --git a/test/hardhat/utils/types.ts b/test/hardhat/utils/types.ts new file mode 100644 index 0000000..791fc10 --- /dev/null +++ b/test/hardhat/utils/types.ts @@ -0,0 +1,34 @@ +import { + AddressLike, + BigNumberish, + BytesLike, + } from "ethers"; + +export interface UserOperation { + sender: AddressLike; // Or string + nonce?: BigNumberish; + initCode?: BytesLike; + callData?: BytesLike; + callGasLimit?: BigNumberish; + verificationGasLimit?: BigNumberish; + preVerificationGas?: BigNumberish; + maxFeePerGas?: BigNumberish; + maxPriorityFeePerGas?: BigNumberish; + paymaster?: AddressLike; // Or string + paymasterVerificationGasLimit?: BigNumberish; + paymasterPostOpGasLimit?: BigNumberish; + paymasterData?: BytesLike; + signature?: BytesLike; + } + + export interface PackedUserOperation { + sender: AddressLike; // Or string + nonce: BigNumberish; + initCode: BytesLike; + callData: BytesLike; + accountGasLimits: BytesLike; + preVerificationGas: BigNumberish; + gasFees: BytesLike; + paymasterAndData: BytesLike; + signature: BytesLike; + } \ No newline at end of file diff --git a/test/hardhat/utils/userOpHelpers.ts b/test/hardhat/utils/userOpHelpers.ts new file mode 100644 index 0000000..8dc582c --- /dev/null +++ b/test/hardhat/utils/userOpHelpers.ts @@ -0,0 +1,347 @@ +import { ethers } from "hardhat"; +import { EntryPoint, EntryPointSimulations__factory, IEntryPointSimulations } from "../../../typechain-types"; +import { PackedUserOperation, UserOperation } from "./types"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { TransactionRequest } from '@ethersproject/abstract-provider' +import { AbiCoder, BigNumberish, BytesLike, Contract, Signer, dataSlice, keccak256, toBeHex } from "ethers"; +import { toGwei } from "./general"; +import { callDataCost, decodeRevertReason, rethrow } from "./testUtils"; +import EntryPointSimulationsJson from '../../../artifacts/account-abstraction/contracts/core/EntryPointSimulations.sol/EntryPointSimulations.json' + +const AddressZero = ethers.ZeroAddress; +const coder = AbiCoder.defaultAbiCoder() + +export function packUserOp (userOp: UserOperation): PackedUserOperation { + + const { + sender, + nonce, + initCode = "0x", + callData = "0x", + callGasLimit = 1_500_000, + verificationGasLimit = 1_500_000, + preVerificationGas = 2_000_000, + maxFeePerGas = toGwei("20"), + maxPriorityFeePerGas = toGwei("10"), + paymaster = ethers.ZeroAddress, + paymasterData = "0x", + paymasterVerificationGasLimit = 3_00_000, + paymasterPostOpGasLimit = 0, + signature = "0x", + } = userOp; + + const accountGasLimits = packAccountGasLimits(verificationGasLimit, callGasLimit) + const gasFees = packAccountGasLimits(maxPriorityFeePerGas, maxFeePerGas) + let paymasterAndData = '0x' + if (paymaster.toString().length >= 20 && paymaster !== ethers.ZeroAddress) { + paymasterAndData = packPaymasterData( + userOp.paymaster as string, + paymasterVerificationGasLimit, + paymasterPostOpGasLimit, + paymasterData as string, + ) as string; + } + return { + sender: userOp.sender, + nonce: userOp.nonce || 0, + callData: userOp.callData || '0x', + accountGasLimits, + initCode: userOp.initCode || '0x', + preVerificationGas: userOp.preVerificationGas || 50000, + gasFees, + paymasterAndData, + signature: userOp.signature || '0x' + } +} + +export function encodeUserOp (userOp: UserOperation, forSignature = true): string { + const packedUserOp = packUserOp(userOp) + if (forSignature) { + return coder.encode( + ['address', 'uint256', 'bytes32', 'bytes32', + 'bytes32', 'uint256', 'bytes32', + 'bytes32'], + [packedUserOp.sender, packedUserOp.nonce, keccak256(packedUserOp.initCode), keccak256(packedUserOp.callData), + packedUserOp.accountGasLimits, packedUserOp.preVerificationGas, packedUserOp.gasFees, + keccak256(packedUserOp.paymasterAndData)]) + } else { + // for the purpose of calculating gas cost encode also signature (and no keccak of bytes) + return coder.encode( + ['address', 'uint256', 'bytes', 'bytes', + 'bytes32', 'uint256', 'bytes32', + 'bytes', 'bytes'], + [packedUserOp.sender, packedUserOp.nonce, packedUserOp.initCode, packedUserOp.callData, + packedUserOp.accountGasLimits, packedUserOp.preVerificationGas, packedUserOp.gasFees, + packedUserOp.paymasterAndData, packedUserOp.signature]) + } +} + +// Can be moved to testUtils +export function packPaymasterData( + paymaster: string, + paymasterVerificationGasLimit: BigNumberish, + postOpGasLimit: BigNumberish, + paymasterData: BytesLike, + ): BytesLike { + return ethers.concat([ + paymaster, + ethers.zeroPadValue(toBeHex(Number(paymasterVerificationGasLimit)), 16), + ethers.zeroPadValue(toBeHex(Number(postOpGasLimit)), 16), + paymasterData, + ]); +} + +// Can be moved to testUtils +export function packAccountGasLimits (verificationGasLimit: BigNumberish, callGasLimit: BigNumberish): string { + return ethers.concat([ + ethers.zeroPadValue(toBeHex(Number(verificationGasLimit)), 16), ethers.zeroPadValue(toBeHex(Number(callGasLimit)), 16) + ]) +} + +// Can be moved to testUtils +export function unpackAccountGasLimits (accountGasLimits: string): { verificationGasLimit: number, callGasLimit: number } { + return { verificationGasLimit: parseInt(accountGasLimits.slice(2, 34), 16), callGasLimit: parseInt(accountGasLimits.slice(34), 16) } +} + +export function getUserOpHash (op: UserOperation, entryPoint: string, chainId: number): string { + const userOpHash = keccak256(encodeUserOp(op, true)) + const enc = coder.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: 150000, // default verification gas. will add create2 cost (3200+200*length) if initCode exists + preVerificationGas: 21000, // should also cover calldata cost. + maxFeePerGas: 0, + maxPriorityFeePerGas: 1e9, + paymaster: AddressZero, + paymasterData: '0x', + paymasterVerificationGasLimit: 3e5, + paymasterPostOpGasLimit: 0, + signature: '0x' +} + +// Different compared to infinitism utils +export async function signUserOp (op: UserOperation, signer: Signer, entryPoint: string, chainId: number): Promise { + const message = getUserOpHash(op, entryPoint, chainId) + + const signature = await signer.signMessage(ethers.getBytes(message)); + + return { + ...op, + signature: signature + } +} + +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 (TODO: add overhead) +// 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, entryPoint?: EntryPoint, getNonceFunction = 'getNonce', nonceKey = "0"): Promise { + const op1 = { ...op } + const provider = ethers.provider + if (op.initCode != null && op.initCode !== "0x" ) { + const initAddr = dataSlice(op1.initCode!, 0, 20) + const initCallData = dataSlice(op1.initCode!, 20) + if (op1.nonce == null) op1.nonce = 0 + if (op1.sender == null) { + if (provider == null) throw new Error('no entrypoint/provider') + op1.sender = await entryPoint!.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: await entryPoint?.getAddress(), + to: initAddr, + data: initCallData, + gasLimit: 10e6 + }) + op1.verificationGasLimit = Number(DefaultsForUserOp.verificationGasLimit!) + Number(initEstimate) + } + } + if (op1.nonce == null) { + // TODO: nonce should be fetched from entrypoint based on key + // if (provider == null) throw new Error('must have entryPoint to autofill nonce') + // const c = new Contract(op.sender! as string, [`function ${getNonceFunction}() view returns(uint256)`], provider) + // op1.nonce = await c[getNonceFunction]().catch(rethrow()) + const nonce = await entryPoint?.getNonce(op1.sender!, nonceKey); + op1.nonce = nonce ?? 0n; + } + 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: await entryPoint?.getAddress(), + to: op1.sender, + data: op1.callData as string + }) + + // console.log('estim', op1.sender,'len=', op1.callData!.length, 'res=', gasEtimated) + // estimateGas assumes direct call from entryPoint. add wrapper cost. + op1.callGasLimit = gasEtimated // .add(55000) + } + if (op1.paymaster != null) { + if (op1.paymasterVerificationGasLimit == null) { + op1.paymasterVerificationGasLimit = DefaultsForUserOp.paymasterVerificationGasLimit + } + if (op1.paymasterPostOpGasLimit == null) { + op1.paymasterPostOpGasLimit = DefaultsForUserOp.paymasterPostOpGasLimit + } + } + if (op1.maxFeePerGas == null) { + if (provider == null) throw new Error('must have entryPoint to autofill maxFeePerGas') + const block = await provider.getBlock('latest') + op1.maxFeePerGas = Number(block!.baseFeePerGas!) + Number(op1.maxPriorityFeePerGas ?? DefaultsForUserOp.maxPriorityFeePerGas) + } + // TODO: this is exactly what fillUserOp below should do - but it doesn't. + // adding this manually + if (op1.maxPriorityFeePerGas == null) { + op1.maxPriorityFeePerGas = DefaultsForUserOp.maxPriorityFeePerGas + } + const op2 = fillUserOpDefaults(op1) + // if(op2 === undefined || op2 === null) { + // throw new Error('op2 is undefined or null') + // } + // 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(encodeUserOp(op2, false)) + } + return op2; +} + +export async function fillAndPack (op: Partial, entryPoint?: EntryPoint, getNonceFunction = 'getNonce'): Promise { + const userOp = await fillUserOp(op, entryPoint, getNonceFunction); + if(userOp === undefined) { + throw new Error('userOp is undefined') + } + return packUserOp(userOp) +} + +export async function fillAndSign (op: Partial, signer: Signer | Signer, entryPoint?: EntryPoint, getNonceFunction = 'getNonce', nonceKey = "0"): Promise { + const provider = ethers.provider + const op2 = await fillUserOp(op, entryPoint, getNonceFunction, nonceKey) + if(op2 === undefined) { + throw new Error('op2 is undefined') + } + + const chainId = await provider!.getNetwork().then(net => net.chainId) + const message = ethers.getBytes(getUserOpHash(op2, await entryPoint!.getAddress(), Number(chainId))) + + let signature + try { + signature = await signer.signMessage(message) + } catch (err: any) { + // attempt to use 'eth_sign' instead of 'personal_sign' which is not supported by Foundry Anvil + signature = await (signer as any)._legacySignMessage(message) + } + return { + ...op2, + signature + } +} + + export async function fillSignAndPack (op: Partial, signer: Signer | Signer, entryPoint?: EntryPoint, getNonceFunction = 'getNonce', nonceKey = "0"): Promise { + const filledAndSignedOp = await fillAndSign(op, signer, entryPoint, getNonceFunction, nonceKey) + return packUserOp(filledAndSignedOp) +} + +/** + * This function relies on a "state override" functionality of the 'eth_call' RPC method + * in order to provide the details of a simulated validation call to the bundler + * @param userOp + * @param entryPointAddress + * @param txOverrides + */ +export async function simulateValidation ( + userOp: PackedUserOperation, + entryPointAddress: string, + txOverrides?: any): Promise { + const entryPointSimulations = EntryPointSimulations__factory.createInterface() + const data = entryPointSimulations.encodeFunctionData('simulateValidation', [userOp]) + const tx: TransactionRequest = { + to: entryPointAddress, + data, + ...txOverrides + } + const stateOverride = { + [entryPointAddress]: { + code: EntryPointSimulationsJson.deployedBytecode + } + } + try { + const simulationResult = await ethers.provider.send('eth_call', [tx, 'latest', stateOverride]) + const res = entryPointSimulations.decodeFunctionResult('simulateValidation', simulationResult) + // note: here collapsing the returned "tuple of one" into a single value - will break for returning actual tuples + return res[0] + } catch (error: any) { + const revertData = error?.data + if (revertData != null) { + // note: this line throws the revert reason instead of returning it + entryPointSimulations.decodeFunctionResult('simulateValidation', revertData) + } + throw error + } +} + +// TODO: this code is very much duplicated but "encodeFunctionData" is based on 20 overloads +// TypeScript is not able to resolve overloads with variables: https://github.com/microsoft/TypeScript/issues/14107 +export async function simulateHandleOp ( + userOp: PackedUserOperation, + target: string, + targetCallData: string, + entryPointAddress: string, + txOverrides?: any): Promise { + const entryPointSimulations = EntryPointSimulations__factory.createInterface() + const data = entryPointSimulations.encodeFunctionData('simulateHandleOp', [userOp, target, targetCallData]) + const tx: TransactionRequest = { + to: entryPointAddress, + data, + ...txOverrides + } + const stateOverride = { + [entryPointAddress]: { + code: EntryPointSimulationsJson.deployedBytecode + } + } + try { + const simulationResult = await ethers.provider.send('eth_call', [tx, 'latest', stateOverride]) + const res = entryPointSimulations.decodeFunctionResult('simulateHandleOp', simulationResult) + // note: here collapsing the returned "tuple of one" into a single value - will break for returning actual tuples + return res[0] + } catch (error: any) { + const err = decodeRevertReason(error) + if (err != null) { + throw new Error(err) + } + throw error + } + } From a921986aa4a3a19193f0db863164acdcfade6f7c Mon Sep 17 00:00:00 2001 From: livingrockrises <90545960+livingrockrises@users.noreply.github.com> Date: Mon, 8 Apr 2024 01:36:05 +0530 Subject: [PATCH 02/21] fix broken test and sig verification logic --- .../SponsorshipPaymasterWithPremium.sol | 2 +- .../biconomy-sponsorship-paymaster-specs.ts | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol b/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol index b4c90c0..4cdc960 100644 --- a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol +++ b/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol @@ -206,7 +206,7 @@ contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard, Biconom bool validSig = verifyingSigner.isValidSignatureNow( ECDSA_solady.toEthSignedMessageHash(getHash(userOp, paymasterId, validUntil, validAfter, priceMarkup)), - userOp.signature + signature ); //don't revert on signature failure: return SIG_VALIDATION_FAILED diff --git a/test/hardhat/biconomy-sponsorship-paymaster-specs.ts b/test/hardhat/biconomy-sponsorship-paymaster-specs.ts index 0db6c81..dbfabb1 100644 --- a/test/hardhat/biconomy-sponsorship-paymaster-specs.ts +++ b/test/hardhat/biconomy-sponsorship-paymaster-specs.ts @@ -123,9 +123,11 @@ describe("EntryPoint with Biconomy Sponsorship Paymaster", function () { await paymaster.depositFor(paymasterDepositorId, { value: parseEther("1") }); await entryPoint.depositTo(paymasterAddress, { value: parseEther("1") }); + + await deployer.sendTransaction({to: expected, value: parseEther("1"), data: '0x'}); }); - describe("#validatePaymasterUserOp and #sendSponsoredTx", () => { + describe("Deployed Account : #validatePaymasterUserOp and #sendEmptySponsoredTx", () => { it("succeed with valid signature", async () => { const nonceKey = ethers.zeroPadBytes(await ecdsaModule.getAddress(), 24); const userOp1 = await fillAndSign({ @@ -137,7 +139,8 @@ describe("EntryPoint with Biconomy Sponsorship Paymaster", function () { ethers.zeroPadValue(toBeHex(MOCK_VALID_AFTER), 6), ethers.zeroPadValue(toBeHex(MARKUP), 4), '0x' + '00'.repeat(65) - ]) + ]), + paymasterPostOpGasLimit: 40_000, }, walletOwner, entryPoint, 'getNonce', nonceKey) const hash = await paymaster.getHash(packUserOp(userOp1), paymasterDepositorId, MOCK_VALID_UNTIL, MOCK_VALID_AFTER, MARKUP) const sig = await offchainSigner.signMessage(ethers.getBytes(hash)) @@ -150,9 +153,10 @@ describe("EntryPoint with Biconomy Sponsorship Paymaster", function () { ethers.zeroPadValue(toBeHex(MOCK_VALID_AFTER), 6), ethers.zeroPadValue(toBeHex(MARKUP), 4), sig - ]) + ]), + paymasterPostOpGasLimit: 40_000, }, walletOwner, entryPoint, 'getNonce', nonceKey) - console.log("userOp: ", userOp); + // const parsedPnD = await paymaster.parsePaymasterAndData(userOp.paymasterAndData) const res = await simulateValidation(userOp, await entryPoint.getAddress()) const validationData = parseValidationData(res.returnInfo.paymasterValidationData) expect(validationData).to.eql({ @@ -160,6 +164,8 @@ describe("EntryPoint with Biconomy Sponsorship Paymaster", function () { validAfter: parseInt(MOCK_VALID_AFTER), validUntil: parseInt(MOCK_VALID_UNTIL) }) + + await entryPoint.handleOps([userOp], await deployer.getAddress()) }); }); }) From 55d967ddd387b50a2121537f85d3451ecb6aceef Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Wed, 26 Jun 2024 13:13:35 +0400 Subject: [PATCH 03/21] Setup nexus submodule and fix remappings for foundry test --- .gitmodules | 3 +++ lib/nexus | 1 + remappings.txt | 3 ++- test/foundry/SponsorshipPaymasterWithPremium.t.sol | 9 +++++++++ 4 files changed, 15 insertions(+), 1 deletion(-) create mode 160000 lib/nexus create mode 100644 test/foundry/SponsorshipPaymasterWithPremium.t.sol diff --git a/.gitmodules b/.gitmodules index e69de29..fa2b614 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/nexus"] + path = lib/nexus + url = https://github.com/bcnmy/nexus diff --git a/lib/nexus b/lib/nexus new file mode 160000 index 0000000..ab9616b --- /dev/null +++ b/lib/nexus @@ -0,0 +1 @@ +Subproject commit ab9616bd71fcd51048e834f87a7b60dccbfc0adb diff --git a/remappings.txt b/remappings.txt index af5268a..3272323 100644 --- a/remappings.txt +++ b/remappings.txt @@ -4,4 +4,5 @@ forge-std/=node_modules/forge-std/ account-abstraction=node_modules/account-abstraction/ modulekit/=node_modules/modulekit/src/ sentinellist/=node_modules/sentinellist/ -solady/=node_modules/solady \ No newline at end of file +solady/=node_modules/solady +ds-test/=node_modules/ds-test/src/ diff --git a/test/foundry/SponsorshipPaymasterWithPremium.t.sol b/test/foundry/SponsorshipPaymasterWithPremium.t.sol new file mode 100644 index 0000000..8c47d9d --- /dev/null +++ b/test/foundry/SponsorshipPaymasterWithPremium.t.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity ^0.8.24; +import { BiconomySponsorshipPaymaster } from "../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; +import { Test } from "forge-std/src/Test.sol"; +import { StdCheats } from "forge-std/src/StdCheats.sol"; + +contract SponsorshipPaymasterWithPremiumTest is Test { + +} \ No newline at end of file From 659b7e364c2f21cce3cdfeb22e9643a0c83c457f Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Thu, 27 Jun 2024 10:34:30 +0400 Subject: [PATCH 04/21] setup imports, accounts, and align versions --- .gitmodules | 6 +- .solhint.json | 2 +- contracts/base/BasePaymaster.sol | 2 +- contracts/common/Errors.sol | 2 +- .../IBiconomySponsorshipPaymaster.sol | 2 +- contracts/mocks/Imports.sol | 2 +- contracts/mocks/MockValidator.sol | 2 +- .../references/SampleVerifyingPaymaster.sol | 2 +- .../SponsorshipPaymasterWithPremium.sol | 2 +- contracts/test/Foo.sol | 2 +- contracts/test/Lock.sol | 2 +- contracts/utils/SoladyOwnable.sol | 2 +- foundry.toml | 2 +- hardhat.config.ts | 2 +- lib/nexus | 1 - lib/nexus.git | 1 + remappings.txt | 1 + test/foundry/Lock.t.sol | 2 +- .../SponsorshipPaymasterWithPremium.t.sol | 19 ++-- test/foundry/base/NexusTestBase.sol | 88 +++++++++++++++++++ test/foundry/mocks/Counter.sol | 2 +- 21 files changed, 121 insertions(+), 25 deletions(-) delete mode 160000 lib/nexus create mode 160000 lib/nexus.git create mode 100644 test/foundry/base/NexusTestBase.sol diff --git a/.gitmodules b/.gitmodules index fa2b614..b756d1f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "lib/nexus"] - path = lib/nexus - url = https://github.com/bcnmy/nexus +[submodule "lib/nexus.git"] + path = lib/nexus.git + url = https://github.com/bcnmy/nexus.git diff --git a/.solhint.json b/.solhint.json index a41dc0f..06fbb26 100644 --- a/.solhint.json +++ b/.solhint.json @@ -1,7 +1,7 @@ { "extends": "solhint:recommended", "rules": { - "compiler-version": ["error", "^0.8.24"], + "compiler-version": ["error", "^0.8.26"], "func-visibility": ["warn", { "ignoreConstructors": true }], "reentrancy": "error", "state-visibility": "error", diff --git a/contracts/base/BasePaymaster.sol b/contracts/base/BasePaymaster.sol index 25ca1a6..8b7e1e0 100644 --- a/contracts/base/BasePaymaster.sol +++ b/contracts/base/BasePaymaster.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.24; +pragma solidity ^0.8.26; /* solhint-disable reason-string */ diff --git a/contracts/common/Errors.sol b/contracts/common/Errors.sol index 045a5f1..4e42283 100644 --- a/contracts/common/Errors.sol +++ b/contracts/common/Errors.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-only -pragma solidity ^0.8.24; +pragma solidity ^0.8.26; contract BiconomySponsorshipPaymasterErrors { diff --git a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol index f90955c..ed4da78 100644 --- a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol +++ b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +pragma solidity ^0.8.26; interface IBiconomySponsorshipPaymaster { event PostopCostChanged(uint256 indexed _oldValue, uint256 indexed _newValue); diff --git a/contracts/mocks/Imports.sol b/contracts/mocks/Imports.sol index 3eb785e..7b0976a 100644 --- a/contracts/mocks/Imports.sol +++ b/contracts/mocks/Imports.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.24; +pragma solidity ^0.8.26; /* solhint-disable reason-string */ diff --git a/contracts/mocks/MockValidator.sol b/contracts/mocks/MockValidator.sol index 5f7bdd9..2c3d359 100644 --- a/contracts/mocks/MockValidator.sol +++ b/contracts/mocks/MockValidator.sol @@ -1,3 +1,3 @@ -pragma solidity ^0.8.24; +pragma solidity ^0.8.26; import "@biconomy-devx/erc7579-msa/test/foundry/mocks/MockValidator.sol"; \ No newline at end of file diff --git a/contracts/references/SampleVerifyingPaymaster.sol b/contracts/references/SampleVerifyingPaymaster.sol index 3fdce99..46f12bf 100644 --- a/contracts/references/SampleVerifyingPaymaster.sol +++ b/contracts/references/SampleVerifyingPaymaster.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.24; +pragma solidity ^0.8.26; /* solhint-disable reason-string */ /* solhint-disable no-inline-assembly */ diff --git a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol b/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol index 4cdc960..91d9988 100644 --- a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol +++ b/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.24; +pragma solidity ^0.8.26; /* solhint-disable reason-string */ diff --git a/contracts/test/Foo.sol b/contracts/test/Foo.sol index f419123..8302d06 100644 --- a/contracts/test/Foo.sol +++ b/contracts/test/Foo.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.24; +pragma solidity >=0.8.26; /** * @title Foo diff --git a/contracts/test/Lock.sol b/contracts/test/Lock.sol index d11302f..522be01 100644 --- a/contracts/test/Lock.sol +++ b/contracts/test/Lock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; +pragma solidity ^0.8.26; /** * @title Lock diff --git a/contracts/utils/SoladyOwnable.sol b/contracts/utils/SoladyOwnable.sol index 9589b3d..0cd57c4 100644 --- a/contracts/utils/SoladyOwnable.sol +++ b/contracts/utils/SoladyOwnable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +pragma solidity ^0.8.26; import {Ownable} from "solady/src/auth/Ownable.sol"; diff --git a/foundry.toml b/foundry.toml index e3480d4..04c3656 100644 --- a/foundry.toml +++ b/foundry.toml @@ -11,7 +11,7 @@ optimizer_runs = 1_000_000 out = "out" script = "scripts" - solc = "0.8.24" + solc = "0.8.26" src = "contracts" test = "test" cache_path = "cache_forge" diff --git a/hardhat.config.ts b/hardhat.config.ts index 3e7fdf2..e139ab6 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -5,7 +5,7 @@ import "@bonadocs/docgen"; const config: HardhatUserConfig = { solidity: { - version: "0.8.24", + version: "0.8.26", settings: { optimizer: { enabled: true, diff --git a/lib/nexus b/lib/nexus deleted file mode 160000 index ab9616b..0000000 --- a/lib/nexus +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ab9616bd71fcd51048e834f87a7b60dccbfc0adb diff --git a/lib/nexus.git b/lib/nexus.git new file mode 160000 index 0000000..5d81e53 --- /dev/null +++ b/lib/nexus.git @@ -0,0 +1 @@ +Subproject commit 5d81e533941b49194fbc469b09b182c6c5d0e9d9 diff --git a/remappings.txt b/remappings.txt index 3272323..b197025 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,5 +1,6 @@ @openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ @prb/test/=node_modules/@prb/test/ +@nexus/=lib/nexus.git/ forge-std/=node_modules/forge-std/ account-abstraction=node_modules/account-abstraction/ modulekit/=node_modules/modulekit/src/ diff --git a/test/foundry/Lock.t.sol b/test/foundry/Lock.t.sol index 5782e2d..a6f245b 100644 --- a/test/foundry/Lock.t.sol +++ b/test/foundry/Lock.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.24 <0.9.0; +pragma solidity >=0.8.26 <0.9.0; import { PRBTest } from "@prb/test/src/PRBTest.sol"; import { Lock } from "../../contracts/test/Lock.sol"; diff --git a/test/foundry/SponsorshipPaymasterWithPremium.t.sol b/test/foundry/SponsorshipPaymasterWithPremium.t.sol index 8c47d9d..d1191a6 100644 --- a/test/foundry/SponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/SponsorshipPaymasterWithPremium.t.sol @@ -1,9 +1,16 @@ // SPDX-License-Identifier: Unlicensed -pragma solidity ^0.8.24; -import { BiconomySponsorshipPaymaster } from "../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; +pragma solidity ^0.8.26; + import { Test } from "forge-std/src/Test.sol"; -import { StdCheats } from "forge-std/src/StdCheats.sol"; +import { Vm } from "forge-std/src/Vm.sol"; + +import { BiconomySponsorshipPaymaster } from "../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; +import { NexusTestBase } from "./base/NexusTestBase.sol"; + +contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { + function setUp() public virtual override { + super.setUp(); -contract SponsorshipPaymasterWithPremiumTest is Test { - -} \ No newline at end of file + + } +} diff --git a/test/foundry/base/NexusTestBase.sol b/test/foundry/base/NexusTestBase.sol new file mode 100644 index 0000000..a287985 --- /dev/null +++ b/test/foundry/base/NexusTestBase.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import { Test } from "forge-std/src/Test.sol"; + +import { EntryPoint } from "account-abstraction/contracts/core/EntryPoint.sol"; +import { Nexus } from "@nexus/contracts/Nexus.sol"; +import { NexusAccountFactory } from "@nexus/contracts/factory/NexusAccountFactory.sol"; + +abstract contract NexusTestBase is Test { + // Test Environment Configuration + string constant mnemonic = "test test test test test test test test test test test junk"; + uint256 constant testAccountCount = 10; + uint256 constant initialMainAccountFunds = 100_000 ether; + uint256 constant defaultPreVerificationGas = 21_000; + + uint32 nextKeyIndex; + + // Test Accounts + struct TestAccount { + address payable addr; + uint256 privateKey; + } + + TestAccount[] testAccounts; + TestAccount alice; + TestAccount bob; + TestAccount charlie; + TestAccount dan; + TestAccount emma; + TestAccount frank; + TestAccount george; + TestAccount henry; + TestAccount ida; + + TestAccount owner; + + // ERC7579 Contracts + EntryPoint entryPoint; + Nexus saImplementation; + NexusAccountFactory factory; + + function getNextPrivateKey() internal returns (uint256) { + return vm.deriveKey(mnemonic, ++nextKeyIndex); + } + + function setUp() public virtual { + // Generate Test Addresses + for (uint256 i = 0; i < testAccountCount; i++) { + uint256 privateKey = getNextPrivateKey(); + testAccounts.push(TestAccount(payable(vm.addr(privateKey)), privateKey)); + + deal(testAccounts[i].addr, initialMainAccountFunds); + } + + // Name Test Addresses + alice = testAccounts[0]; + vm.label(alice.addr, string.concat("Alice", vm.toString(uint256(0)))); + + bob = testAccounts[1]; + vm.label(bob.addr, string.concat("Bob", vm.toString(uint256(1)))); + + charlie = testAccounts[2]; + vm.label(charlie.addr, string.concat("Charlie", vm.toString(uint256(2)))); + + dan = testAccounts[3]; + vm.label(dan.addr, string.concat("Dan", vm.toString(uint256(3)))); + + emma = testAccounts[4]; + vm.label(emma.addr, string.concat("Emma", vm.toString(uint256(4)))); + + frank = testAccounts[5]; + vm.label(frank.addr, string.concat("Frank", vm.toString(uint256(5)))); + + george = testAccounts[6]; + vm.label(george.addr, string.concat("George", vm.toString(uint256(6)))); + + henry = testAccounts[7]; + vm.label(henry.addr, string.concat("Henry", vm.toString(uint256(7)))); + + ida = testAccounts[7]; + vm.label(ida.addr, string.concat("Ida", vm.toString(uint256(8)))); + + // Name Owner + owner = testAccounts[8]; + vm.label(owner.addr, string.concat("Owner", vm.toString(uint256(9)))); + } +} diff --git a/test/foundry/mocks/Counter.sol b/test/foundry/mocks/Counter.sol index 5807161..c4ec3d6 100644 --- a/test/foundry/mocks/Counter.sol +++ b/test/foundry/mocks/Counter.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; +pragma solidity ^0.8.26; contract Counter { uint256 private _number; From 967379cdcfe25fcd89124939d014620704f2c5e8 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Thu, 27 Jun 2024 15:04:29 +0400 Subject: [PATCH 05/21] Deployment test for BiconomySponsorshipPaymaster --- .../SponsorshipPaymasterWithPremium.t.sol | 19 +- test/foundry/base/NexusTestBase.sol | 674 ++++++++++++++++-- 2 files changed, 631 insertions(+), 62 deletions(-) diff --git a/test/foundry/SponsorshipPaymasterWithPremium.t.sol b/test/foundry/SponsorshipPaymasterWithPremium.t.sol index d1191a6..db3a515 100644 --- a/test/foundry/SponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/SponsorshipPaymasterWithPremium.t.sol @@ -1,16 +1,23 @@ // SPDX-License-Identifier: Unlicensed pragma solidity ^0.8.26; -import { Test } from "forge-std/src/Test.sol"; -import { Vm } from "forge-std/src/Vm.sol"; +import { console2 } from "forge-std/src/Console2.sol"; -import { BiconomySponsorshipPaymaster } from "../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; import { NexusTestBase } from "./base/NexusTestBase.sol"; +import { BiconomySponsorshipPaymaster } from "../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; + + contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { - function setUp() public virtual override { - super.setUp(); + function setUp() public { + setupTestEnvironment(); + } - + function testDeploy() external { + BiconomySponsorshipPaymaster testArtifact = new BiconomySponsorshipPaymaster(BOB_ADDRESS, ENTRYPOINT, ALICE_ADDRESS, CHARLIE_ADDRESS); + assertEq(address(testArtifact.owner()), BOB_ADDRESS); + assertEq(address(testArtifact.entryPoint()), ENTRYPOINT_ADDRESS); + assertEq(address(testArtifact.verifyingSigner()), ALICE_ADDRESS); + assertEq(address(testArtifact.feeCollector()), CHARLIE_ADDRESS); } } diff --git a/test/foundry/base/NexusTestBase.sol b/test/foundry/base/NexusTestBase.sol index a287985..778f1a2 100644 --- a/test/foundry/base/NexusTestBase.sol +++ b/test/foundry/base/NexusTestBase.sol @@ -2,87 +2,649 @@ pragma solidity ^0.8.26; import { Test } from "forge-std/src/Test.sol"; +import { Vm } from "forge-std/src/Vm.sol"; + +import "solady/src/utils/ECDSA.sol"; + +import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import { EntryPoint } from "account-abstraction/contracts/core/EntryPoint.sol"; +import { IEntryPoint } from "account-abstraction/contracts/interfaces/IEntryPoint.sol"; +import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol"; + import { Nexus } from "@nexus/contracts/Nexus.sol"; import { NexusAccountFactory } from "@nexus/contracts/factory/NexusAccountFactory.sol"; +import { BiconomyMetaFactory } from "@nexus/contracts/factory/BiconomyMetaFactory.sol"; +import { MockValidator } from "@nexus/contracts/mocks/MockValidator.sol"; +import { MockHook } from "@nexus/contracts/mocks/MockHook.sol"; +// import { MockExecutor } from "@nexus/contracts/mocks/MockExecutor.sol"; +import { MockHandler } from "@nexus/contracts/mocks/MockHandler.sol"; +import { BootstrapLib } from "@nexus/contracts/lib/BootstrapLib.sol"; +import { ModeLib, ExecutionMode, ExecType, CallType, CALLTYPE_BATCH, CALLTYPE_SINGLE, EXECTYPE_DEFAULT, EXECTYPE_TRY } from "@nexus/contracts/lib/ModeLib.sol"; +// import { ExecLib, Execution } from "@nexus/contracts/lib/ExecLib.sol"; +import { Bootstrap, BootstrapConfig } from "@nexus/contracts/utils/Bootstrap.sol"; +import { CheatCodes } from "@nexus/test/foundry/utils/CheatCodes.sol"; +import { EventsAndErrors } from "@nexus/test/foundry/utils/EventsAndErrors.sol"; + +import { BiconomySponsorshipPaymaster } from "../../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; + + + +abstract contract NexusTestBase is CheatCodes, EventsAndErrors { + // ----------------------------------------- + // State Variables + // ----------------------------------------- + + Vm.Wallet internal DEPLOYER; + Vm.Wallet internal BOB; + Vm.Wallet internal ALICE; + Vm.Wallet internal CHARLIE; + Vm.Wallet internal BUNDLER; + Vm.Wallet internal FACTORY_OWNER; + + address constant ENTRYPOINT_ADDRESS = address(0x0000000071727De22E5E9d8BAf0edAc6f37da032); + + address internal BOB_ADDRESS; + address internal ALICE_ADDRESS; + address internal CHARLIE_ADDRESS; + + Nexus internal BOB_ACCOUNT; + Nexus internal ALICE_ACCOUNT; + Nexus internal CHARLIE_ACCOUNT; + + IEntryPoint internal ENTRYPOINT; + NexusAccountFactory internal FACTORY; + BiconomyMetaFactory internal META_FACTORY; + MockHook internal HOOK_MODULE; + MockHandler internal HANDLER_MODULE; + // MockExecutor internal EXECUTOR_MODULE; + MockValidator internal VALIDATOR_MODULE; + Nexus internal ACCOUNT_IMPLEMENTATION; + + Bootstrap internal BOOTSTRAPPER; + + // ----------------------------------------- + // Setup Functions + // ----------------------------------------- + /// @notice Initializes the testing environment with wallets, contracts, and accounts + function setupTestEnvironment() internal virtual { + /// Initializes the testing environment + setupPredefinedWallets(); + deployTestContracts(); + deployNexusForPredefinedWallets(); + } + + function createAndFundWallet(string memory name, uint256 amount) internal returns (Vm.Wallet memory) { + Vm.Wallet memory wallet = newWallet(name); + vm.deal(wallet.addr, amount); + return wallet; + } + + function setupPredefinedWallets() internal { + DEPLOYER = createAndFundWallet("DEPLOYER", 1000 ether); + + BOB = createAndFundWallet("BOB", 1000 ether); + BOB_ADDRESS = BOB.addr; + + ALICE = createAndFundWallet("ALICE", 1000 ether); + CHARLIE = createAndFundWallet("CHARLIE", 1000 ether); + + ALICE_ADDRESS = ALICE.addr; + CHARLIE_ADDRESS = CHARLIE.addr; + + FACTORY_OWNER = createAndFundWallet("FACTORY_OWNER", 1000 ether); + } + + function deployTestContracts() internal { + ENTRYPOINT = new EntryPoint(); + vm.etch(ENTRYPOINT_ADDRESS, address(ENTRYPOINT).code); + ENTRYPOINT = IEntryPoint(ENTRYPOINT_ADDRESS); + ACCOUNT_IMPLEMENTATION = new Nexus(address(ENTRYPOINT)); + FACTORY = new NexusAccountFactory(address(ACCOUNT_IMPLEMENTATION), address(FACTORY_OWNER.addr)); + META_FACTORY = new BiconomyMetaFactory(address(FACTORY_OWNER.addr)); + vm.prank(FACTORY_OWNER.addr); + META_FACTORY.addFactoryToWhitelist(address(FACTORY)); + HOOK_MODULE = new MockHook(); + HANDLER_MODULE = new MockHandler(); + // EXECUTOR_MODULE = new MockExecutor(); + VALIDATOR_MODULE = new MockValidator(); + BOOTSTRAPPER = new Bootstrap(); + } + + // ----------------------------------------- + // Account Deployment Functions + // ----------------------------------------- + /// @notice Deploys an account with a specified wallet, deposit amount, and optional custom validator + /// @param wallet The wallet to deploy the account for + /// @param deposit The deposit amount + /// @param validator The custom validator address, if not provided uses default + /// @return The deployed Nexus account + function deployNexus(Vm.Wallet memory wallet, uint256 deposit, address validator) internal returns (Nexus) { + address payable accountAddress = calculateAccountAddress(wallet.addr, validator); + bytes memory initCode = buildInitCode(wallet.addr, validator); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = buildUserOpWithInitAndCalldata(wallet, initCode, "", validator); + + ENTRYPOINT.depositTo{ value: deposit }(address(accountAddress)); + ENTRYPOINT.handleOps(userOps, payable(wallet.addr)); + assertTrue(MockValidator(validator).isOwner(accountAddress, wallet.addr)); + return Nexus(accountAddress); + } + + /// @notice Deploys Nexus accounts for predefined wallets + function deployNexusForPredefinedWallets() internal { + BOB_ACCOUNT = deployNexus(BOB, 100 ether, address(VALIDATOR_MODULE)); + vm.label(address(BOB_ACCOUNT), "BOB_ACCOUNT"); + ALICE_ACCOUNT = deployNexus(ALICE, 100 ether, address(VALIDATOR_MODULE)); + vm.label(address(ALICE_ACCOUNT), "ALICE_ACCOUNT"); + CHARLIE_ACCOUNT = deployNexus(CHARLIE, 100 ether, address(VALIDATOR_MODULE)); + vm.label(address(CHARLIE_ACCOUNT), "CHARLIE_ACCOUNT"); + } + // ----------------------------------------- + // Utility Functions + // ----------------------------------------- + + /// @notice Calculates the address of a new account + /// @param owner The address of the owner + /// @param validator The address of the validator + /// @return account The calculated account address + function calculateAccountAddress( + address owner, + address validator + ) + internal + view + returns (address payable account) + { + bytes memory moduleInstallData = abi.encodePacked(owner); + + BootstrapConfig[] memory validators = BootstrapLib.createArrayConfig(validator, moduleInstallData); + BootstrapConfig memory hook = BootstrapLib.createSingleConfig(address(0), ""); + bytes memory saDeploymentIndex = "0"; + + // Create initcode and salt to be sent to Factory + bytes memory _initData = BOOTSTRAPPER.getInitNexusScopedCalldata(validators, hook); + bytes32 salt = keccak256(saDeploymentIndex); + + account = FACTORY.computeAccountAddress(_initData, salt); + return account; + } + + /// @notice Prepares the init code for account creation with a validator + /// @param ownerAddress The address of the owner + /// @param validator The address of the validator + /// @return initCode The prepared init code + function buildInitCode(address ownerAddress, address validator) internal view returns (bytes memory initCode) { + bytes memory moduleInitData = abi.encodePacked(ownerAddress); + + BootstrapConfig[] memory validators = BootstrapLib.createArrayConfig(validator, moduleInitData); + BootstrapConfig memory hook = BootstrapLib.createSingleConfig(address(0), ""); + + bytes memory saDeploymentIndex = "0"; + + // Create initcode and salt to be sent to Factory + bytes memory _initData = BOOTSTRAPPER.getInitNexusScopedCalldata(validators, hook); + + bytes32 salt = keccak256(saDeploymentIndex); + + bytes memory factoryData = abi.encodeWithSelector(FACTORY.createAccount.selector, _initData, salt); + + // Prepend the factory address to the encoded function call to form the initCode + initCode = abi.encodePacked( + address(META_FACTORY), + abi.encodeWithSelector(META_FACTORY.deployWithFactory.selector, address(FACTORY), factoryData) + ); + } + + /// @notice Prepares a user operation with init code and call data + /// @param wallet The wallet for which the user operation is prepared + /// @param initCode The init code + /// @param callData The call data + /// @param validator The validator address + /// @return userOp The prepared user operation + function buildUserOpWithInitAndCalldata( + Vm.Wallet memory wallet, + bytes memory initCode, + bytes memory callData, + address validator + ) + internal + view + returns (PackedUserOperation memory userOp) + { + userOp = buildUserOpWithCalldata(wallet, callData, validator); + userOp.initCode = initCode; -abstract contract NexusTestBase is Test { - // Test Environment Configuration - string constant mnemonic = "test test test test test test test test test test test junk"; - uint256 constant testAccountCount = 10; - uint256 constant initialMainAccountFunds = 100_000 ether; - uint256 constant defaultPreVerificationGas = 21_000; + bytes memory signature = signUserOp(wallet, userOp); + userOp.signature = signature; + } - uint32 nextKeyIndex; + /// @notice Prepares a user operation with call data and a validator + /// @param wallet The wallet for which the user operation is prepared + /// @param callData The call data + /// @param validator The validator address + /// @return userOp The prepared user operation + function buildUserOpWithCalldata( + Vm.Wallet memory wallet, + bytes memory callData, + address validator + ) + internal + view + returns (PackedUserOperation memory userOp) + { + address payable account = calculateAccountAddress(wallet.addr, validator); + uint256 nonce = getNonce(account, validator); + userOp = buildPackedUserOp(account, nonce); + userOp.callData = callData; - // Test Accounts - struct TestAccount { - address payable addr; - uint256 privateKey; + bytes memory signature = signUserOp(wallet, userOp); + userOp.signature = signature; } + /// @notice Retrieves the nonce for a given account and validator + /// @param account The account address + /// @param validator The validator address + /// @return nonce The retrieved nonce - TestAccount[] testAccounts; - TestAccount alice; - TestAccount bob; - TestAccount charlie; - TestAccount dan; - TestAccount emma; - TestAccount frank; - TestAccount george; - TestAccount henry; - TestAccount ida; + function getNonce(address account, address validator) internal view returns (uint256 nonce) { + uint192 key = uint192(bytes24(bytes20(address(validator)))); + nonce = ENTRYPOINT.getNonce(address(account), key); + } - TestAccount owner; + /// @notice Signs a user operation + /// @param wallet The wallet to sign the operation + /// @param userOp The user operation to sign + /// @return The signed user operation + function signUserOp( + Vm.Wallet memory wallet, + PackedUserOperation memory userOp + ) + internal + view + returns (bytes memory) + { + bytes32 opHash = ENTRYPOINT.getUserOpHash(userOp); + return signMessage(wallet, opHash); + } - // ERC7579 Contracts - EntryPoint entryPoint; - Nexus saImplementation; - NexusAccountFactory factory; + // ----------------------------------------- + // Utility Functions + // ----------------------------------------- - function getNextPrivateKey() internal returns (uint256) { - return vm.deriveKey(mnemonic, ++nextKeyIndex); + /// @notice Modifies the address of a deployed contract in a test environment + /// @param originalAddress The original address of the contract + /// @param newAddress The new address to replace the original + function changeContractAddress(address originalAddress, address newAddress) internal { + vm.etch(newAddress, originalAddress.code); } - function setUp() public virtual { - // Generate Test Addresses - for (uint256 i = 0; i < testAccountCount; i++) { - uint256 privateKey = getNextPrivateKey(); - testAccounts.push(TestAccount(payable(vm.addr(privateKey)), privateKey)); + /// @notice Builds a user operation struct for account abstraction tests + /// @param sender The sender address + /// @param nonce The nonce + /// @return userOp The built user operation + function buildPackedUserOp(address sender, uint256 nonce) internal pure returns (PackedUserOperation memory) { + return PackedUserOperation({ + sender: sender, + nonce: nonce, + initCode: "", + callData: "", + accountGasLimits: bytes32(abi.encodePacked(uint128(3e6), uint128(3e6))), // verification and call gas limit + preVerificationGas: 3e5, // Adjusted preVerificationGas + gasFees: bytes32(abi.encodePacked(uint128(3e6), uint128(3e6))), // maxFeePerGas and maxPriorityFeePerGas + paymasterAndData: "", + signature: "" + }); + } + + /// @notice Signs a message and packs r, s, v into bytes + /// @param wallet The wallet to sign the message + /// @param messageHash The hash of the message to sign + /// @return signature The packed signature + function signMessage(Vm.Wallet memory wallet, bytes32 messageHash) internal pure returns (bytes memory signature) { + bytes32 userOpHash = ECDSA.toEthSignedMessageHash(messageHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet.privateKey, userOpHash); + signature = abi.encodePacked(r, s, v); + } + + // /// @notice Prepares a packed user operation with specified parameters + // /// @param signer The wallet to sign the operation + // /// @param account The Nexus account + // /// @param execType The execution type + // /// @param executions The executions to include + // /// @param validator The validator address + // /// @return userOps The prepared packed user operations + // function buildPackedUserOperation( + // Vm.Wallet memory signer, + // Nexus account, + // ExecType execType, + // Execution[] memory executions, + // address validator + // ) + // internal + // view + // returns (PackedUserOperation[] memory userOps) + // { + // // Validate execType + // require(execType == EXECTYPE_DEFAULT || execType == EXECTYPE_TRY, "Invalid ExecType"); + + // // Determine mode and calldata based on callType and executions length + // ExecutionMode mode; + // bytes memory executionCalldata; + // uint256 length = executions.length; + + // if (length == 1) { + // mode = (execType == EXECTYPE_DEFAULT) ? ModeLib.encodeSimpleSingle() : ModeLib.encodeTrySingle(); + // executionCalldata = abi.encodeCall( + // Nexus.execute, + // (mode, ExecLib.encodeSingle(executions[0].target, executions[0].value, executions[0].callData)) + // ); + // } else if (length > 1) { + // mode = (execType == EXECTYPE_DEFAULT) ? ModeLib.encodeSimpleBatch() : ModeLib.encodeTryBatch(); + // executionCalldata = abi.encodeCall(Nexus.execute, (mode, ExecLib.encodeBatch(executions))); + // } else { + // revert("Executions array cannot be empty"); + // } - deal(testAccounts[i].addr, initialMainAccountFunds); + // // Initialize the userOps array with one operation + // userOps = new PackedUserOperation[](1); + + // // Build the UserOperation + // userOps[0] = buildPackedUserOp(address(account), getNonce(address(account), validator)); + // userOps[0].callData = executionCalldata; + + // // Sign the operation + // bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]); + // userOps[0].signature = signMessage(signer, userOpHash); + + // return userOps; + // } + + /// @dev Returns a random non-zero address. + /// @notice Returns a random non-zero address + /// @return result A random non-zero address + function randomNonZeroAddress() internal returns (address result) { + do { + result = address(uint160(random())); + } while (result == address(0)); + } + + /// @notice Checks if an address is a contract + /// @param account The address to check + /// @return True if the address is a contract, false otherwise + function isContract(address account) internal view returns (bool) { + uint256 size; + assembly { + size := extcodesize(account) } + return size > 0; + } - // Name Test Addresses - alice = testAccounts[0]; - vm.label(alice.addr, string.concat("Alice", vm.toString(uint256(0)))); + /// @dev credits: vectorized || solady + /// @dev Returns a pseudorandom random number from [0 .. 2**256 - 1] (inclusive). + /// For usage in fuzz tests, please ensure that the function has an unnamed uint256 argument. + /// e.g. `testSomething(uint256) public`. + function random() internal returns (uint256 r) { + /// @solidity memory-safe-assembly + assembly { + // This is the keccak256 of a very long string I randomly mashed on my keyboard. + let sSlot := 0xd715531fe383f818c5f158c342925dcf01b954d24678ada4d07c36af0f20e1ee + let sValue := sload(sSlot) - bob = testAccounts[1]; - vm.label(bob.addr, string.concat("Bob", vm.toString(uint256(1)))); + mstore(0x20, sValue) + r := keccak256(0x20, 0x40) - charlie = testAccounts[2]; - vm.label(charlie.addr, string.concat("Charlie", vm.toString(uint256(2)))); + // If the storage is uninitialized, initialize it to the keccak256 of the calldata. + if iszero(sValue) { + sValue := sSlot + let m := mload(0x40) + calldatacopy(m, 0, calldatasize()) + r := keccak256(m, calldatasize()) + } + sstore(sSlot, add(r, 1)) - dan = testAccounts[3]; - vm.label(dan.addr, string.concat("Dan", vm.toString(uint256(3)))); + // Do some biased sampling for more robust tests. + // prettier-ignore + for { } 1 { } { + let d := byte(0, r) + // With a 1/256 chance, randomly set `r` to any of 0,1,2. + if iszero(d) { + r := and(r, 3) + break + } + // With a 1/2 chance, set `r` to near a random power of 2. + if iszero(and(2, d)) { + // Set `t` either `not(0)` or `xor(sValue, r)`. + let t := xor(not(0), mul(iszero(and(4, d)), not(xor(sValue, r)))) + // Set `r` to `t` shifted left or right by a random multiple of 8. + switch and(8, d) + case 0 { + if iszero(and(16, d)) { t := 1 } + r := add(shl(shl(3, and(byte(3, r), 0x1f)), t), sub(and(r, 7), 3)) + } + default { + if iszero(and(16, d)) { t := shl(255, 1) } + r := add(shr(shl(3, and(byte(3, r), 0x1f)), t), sub(and(r, 7), 3)) + } + // With a 1/2 chance, negate `r`. + if iszero(and(0x20, d)) { r := not(r) } + break + } + // Otherwise, just set `r` to `xor(sValue, r)`. + r := xor(sValue, r) + break + } + } + } + + /// @notice Pre-funds a smart account and asserts success + /// @param sa The smart account address + /// @param prefundAmount The amount to pre-fund + function prefundSmartAccountAndAssertSuccess(address sa, uint256 prefundAmount) internal { + (bool res,) = sa.call{ value: prefundAmount }(""); // Pre-funding the account contract + assertTrue(res, "Pre-funding account should succeed"); + } - emma = testAccounts[4]; - vm.label(emma.addr, string.concat("Emma", vm.toString(uint256(4)))); + // /// @notice Prepares a single execution + // /// @param to The target address + // /// @param value The value to send + // /// @param data The call data + // /// @return execution The prepared execution array + // function prepareSingleExecution( + // address to, + // uint256 value, + // bytes memory data + // ) + // internal + // pure + // returns (Execution[] memory execution) + // { + // execution = new Execution[](1); + // execution[0] = Execution(to, value, data); + // } - frank = testAccounts[5]; - vm.label(frank.addr, string.concat("Frank", vm.toString(uint256(5)))); + // /// @notice Prepares several identical executions + // /// @param execution The execution to duplicate + // /// @param executionsNumber The number of executions to prepare + // /// @return executions The prepared executions array + // function prepareSeveralIdenticalExecutions( + // Execution memory execution, + // uint256 executionsNumber + // ) + // internal + // pure + // returns (Execution[] memory) + // { + // Execution[] memory executions = new Execution[](executionsNumber); + // for (uint256 i = 0; i < executionsNumber; i++) { + // executions[i] = execution; + // } + // return executions; + // } - george = testAccounts[6]; - vm.label(george.addr, string.concat("George", vm.toString(uint256(6)))); + // /// @notice Helper function to execute a single operation. + // function executeSingle( + // Vm.Wallet memory user, + // Nexus userAccount, + // address target, + // uint256 value, + // bytes memory callData, + // ExecType execType + // ) + // internal + // { + // Execution[] memory executions = new Execution[](1); + // executions[0] = Execution({ target: target, value: value, callData: callData }); - henry = testAccounts[7]; - vm.label(henry.addr, string.concat("Henry", vm.toString(uint256(7)))); + // PackedUserOperation[] memory userOps = + // buildPackedUserOperation(user, userAccount, execType, executions, address(VALIDATOR_MODULE)); + // ENTRYPOINT.handleOps(userOps, payable(user.addr)); + // } - ida = testAccounts[7]; - vm.label(ida.addr, string.concat("Ida", vm.toString(uint256(8)))); + // /// @notice Helper function to execute a batch of operations. + // function executeBatch( + // Vm.Wallet memory user, + // Nexus userAccount, + // Execution[] memory executions, + // ExecType execType + // ) + // internal + // { + // PackedUserOperation[] memory userOps = + // buildPackedUserOperation(user, userAccount, execType, executions, address(VALIDATOR_MODULE)); + // ENTRYPOINT.handleOps(userOps, payable(user.addr)); + // } - // Name Owner - owner = testAccounts[8]; - vm.label(owner.addr, string.concat("Owner", vm.toString(uint256(9)))); + /// @notice Calculates the gas cost of the calldata + /// @param data The calldata + /// @return calldataGas The gas cost of the calldata + function calculateCalldataCost(bytes memory data) internal pure returns (uint256 calldataGas) { + for (uint256 i = 0; i < data.length; i++) { + if (uint8(data[i]) == 0) { + calldataGas += 4; + } else { + calldataGas += 16; + } + } + } + + /// @notice Helper function to measure and log gas for simple EOA calls + /// @param description The description for the log + /// @param target The target contract address + /// @param value The value to be sent with the call + /// @param callData The calldata for the call + function measureAndLogGasEOA( + string memory description, + address target, + uint256 value, + bytes memory callData + ) + internal + { + uint256 calldataCost = 0; + for (uint256 i = 0; i < callData.length; i++) { + if (uint8(callData[i]) == 0) { + calldataCost += 4; + } else { + calldataCost += 16; + } + } + + uint256 baseGas = 21_000; + + uint256 initialGas = gasleft(); + (bool res,) = target.call{ value: value }(callData); + uint256 gasUsed = initialGas - gasleft() + baseGas + calldataCost; + assertTrue(res); + emit log_named_uint(description, gasUsed); } + + /// @notice Helper function to calculate calldata cost and log gas usage + /// @param description The description for the log + /// @param userOps The user operations to be executed + function measureAndLogGas(string memory description, PackedUserOperation[] memory userOps) internal { + bytes memory callData = abi.encodeWithSelector(ENTRYPOINT.handleOps.selector, userOps, payable(BUNDLER.addr)); + + uint256 calldataCost = 0; + for (uint256 i = 0; i < callData.length; i++) { + if (uint8(callData[i]) == 0) { + calldataCost += 4; + } else { + calldataCost += 16; + } + } + + uint256 baseGas = 21_000; + + uint256 initialGas = gasleft(); + ENTRYPOINT.handleOps(userOps, payable(BUNDLER.addr)); + uint256 gasUsed = initialGas - gasleft() + baseGas + calldataCost; + emit log_named_uint(description, gasUsed); + } + + /// @notice Handles a user operation and measures gas usage + /// @param userOps The user operations to handle + /// @param refundReceiver The address to receive the gas refund + /// @return gasUsed The amount of gas used + function handleUserOpAndMeasureGas( + PackedUserOperation[] memory userOps, + address refundReceiver + ) + internal + returns (uint256 gasUsed) + { + uint256 gasStart = gasleft(); + ENTRYPOINT.handleOps(userOps, payable(refundReceiver)); + gasUsed = gasStart - gasleft(); + } + + // /// @notice Generates and signs the paymaster data for a user operation. + // /// @dev This function prepares the `paymasterAndData` field for a `PackedUserOperation` with the correct signature. + // /// @param userOp The user operation to be signed. + // /// @param signer The wallet that will sign the paymaster hash. + // /// @param paymaster The paymaster contract. + // /// @return Updated `PackedUserOperation` with `paymasterAndData` field correctly set. + // function generateAndSignPaymasterData( + // PackedUserOperation memory userOp, + // Vm.Wallet memory signer, + // BiconomySponsorshipPaymaster paymaster + // ) + // internal + // view + // returns (bytes memory) + // { + // // Validity timestamps + // uint48 validUntil = uint48(block.timestamp + 1 days); + // uint48 validAfter = uint48(block.timestamp); + + // // Initial paymaster data with zero signature + // bytes memory initialPmData = abi.encodePacked( + // address(paymaster), + // uint128(3e6), // Verification gas limit + // uint128(0), // Post-operation gas limit + // abi.encode(validUntil, validAfter), + // new bytes(65) // Zero signature + // ); + + // // Update user operation with initial paymaster data + // userOp.paymasterAndData = initialPmData; + + // // Generate hash to be signed + // bytes32 paymasterHash = paymaster.getHash(userOp, validUntil, validAfter); + + // // Sign the hash + // bytes memory paymasterSignature = signMessage(signer, paymasterHash); + // require(paymasterSignature.length == 65, "Invalid Paymaster Signature length"); + + // // Final paymaster data with the actual signature + // bytes memory finalPmData = abi.encodePacked( + // address(paymaster), + // uint128(3e6), // Verification gas limit + // uint128(0), // Post-operation gas limit + // abi.encode(validUntil, validAfter), + // paymasterSignature + // ); + + // return finalPmData; + // } } From 2e32ee29f63b6d54c7dd15f6bfe2706a120238fc Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Fri, 28 Jun 2024 13:19:07 +0400 Subject: [PATCH 06/21] basic state tests --- .../SponsorshipPaymasterWithPremium.t.sol | 92 ++++++++++++++++++- test/foundry/base/NexusTestBase.sol | 41 ++++++--- 2 files changed, 117 insertions(+), 16 deletions(-) diff --git a/test/foundry/SponsorshipPaymasterWithPremium.t.sol b/test/foundry/SponsorshipPaymasterWithPremium.t.sol index db3a515..14f8bf9 100644 --- a/test/foundry/SponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/SponsorshipPaymasterWithPremium.t.sol @@ -7,17 +7,99 @@ import { NexusTestBase } from "./base/NexusTestBase.sol"; import { BiconomySponsorshipPaymaster } from "../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; - contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { + BiconomySponsorshipPaymaster public bicoPaymaster; + function setUp() public { setupTestEnvironment(); + // Deploy Sponsorship Paymaster + bicoPaymaster = new BiconomySponsorshipPaymaster(ALICE_ADDRESS, ENTRYPOINT, BOB_ADDRESS, CHARLIE_ADDRESS); + + // Deposit funds for paymaster id + vm.startPrank(CHARLIE_ADDRESS); + ENTRYPOINT.depositTo{ value: 10 ether }(address(bicoPaymaster)); + vm.stopPrank(); } function testDeploy() external { - BiconomySponsorshipPaymaster testArtifact = new BiconomySponsorshipPaymaster(BOB_ADDRESS, ENTRYPOINT, ALICE_ADDRESS, CHARLIE_ADDRESS); - assertEq(address(testArtifact.owner()), BOB_ADDRESS); + BiconomySponsorshipPaymaster testArtifact = + new BiconomySponsorshipPaymaster(ALICE_ADDRESS, ENTRYPOINT, BOB_ADDRESS, CHARLIE_ADDRESS); + assertEq(testArtifact.owner(), ALICE_ADDRESS); assertEq(address(testArtifact.entryPoint()), ENTRYPOINT_ADDRESS); - assertEq(address(testArtifact.verifyingSigner()), ALICE_ADDRESS); - assertEq(address(testArtifact.feeCollector()), CHARLIE_ADDRESS); + assertEq(testArtifact.verifyingSigner(), BOB_ADDRESS); + assertEq(testArtifact.feeCollector(), CHARLIE_ADDRESS); + } + + function testCheckStates() external { + assertEq(bicoPaymaster.owner(), ALICE_ADDRESS); + assertEq(address(bicoPaymaster.entryPoint()), ENTRYPOINT_ADDRESS); + assertEq(bicoPaymaster.verifyingSigner(), BOB_ADDRESS); + assertEq(bicoPaymaster.feeCollector(), CHARLIE_ADDRESS); + } + + function testOwnershipTransfer() external { + vm.startPrank(ALICE_ADDRESS); + assertEq(bicoPaymaster.owner(), ALICE_ADDRESS); + bicoPaymaster.transferOwnership(DAN_ADDRESS); + assertEq(bicoPaymaster.owner(), DAN_ADDRESS); + vm.stopPrank(); + } + + function testInvalidOwnershipTransfer() external { + vm.startPrank(ALICE_ADDRESS); + assertEq(bicoPaymaster.owner(), ALICE_ADDRESS); + vm.expectRevert(abi.encodeWithSignature("NewOwnerIsZeroAddress()")); + bicoPaymaster.transferOwnership(address(0)); + vm.stopPrank(); + + vm.startPrank(DAN_ADDRESS); + assertEq(bicoPaymaster.owner(), ALICE_ADDRESS); + vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); + bicoPaymaster.transferOwnership(DAN_ADDRESS); + vm.stopPrank(); + } + + function testChangingVerifyingSigner() external { + vm.startPrank(ALICE_ADDRESS); + assertEq(bicoPaymaster.verifyingSigner(), BOB_ADDRESS); + bicoPaymaster.setSigner(DAN_ADDRESS); + assertEq(bicoPaymaster.verifyingSigner(), DAN_ADDRESS); + vm.stopPrank(); + } + + function testInvalidChangingVerifyingSigner() external { + vm.startPrank(ALICE_ADDRESS); + assertEq(bicoPaymaster.verifyingSigner(), BOB_ADDRESS); + vm.expectRevert(abi.encodeWithSignature("VerifyingSignerCannotBeZero()")); + bicoPaymaster.setSigner(address(0)); + vm.stopPrank(); + + vm.startPrank(DAN_ADDRESS); + assertEq(bicoPaymaster.verifyingSigner(), BOB_ADDRESS); + vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); + bicoPaymaster.setSigner(DAN_ADDRESS); + vm.stopPrank(); + } + + function testChangingFeeCollector() external { + vm.startPrank(ALICE_ADDRESS); + assertEq(bicoPaymaster.feeCollector(), CHARLIE_ADDRESS); + bicoPaymaster.setFeeCollector(DAN_ADDRESS); + assertEq(bicoPaymaster.feeCollector(), DAN_ADDRESS); + vm.stopPrank(); + } + + function testInvalidChangingFeeCollector() external { + vm.startPrank(ALICE_ADDRESS); + assertEq(bicoPaymaster.feeCollector(), CHARLIE_ADDRESS); + vm.expectRevert(abi.encodeWithSignature("FeeCollectorCannotBeZero()")); + bicoPaymaster.setFeeCollector(DAN_ADDRESS); + vm.stopPrank(); + + vm.startPrank(DAN_ADDRESS); + assertEq(bicoPaymaster.feeCollector(), CHARLIE_ADDRESS); + vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); + bicoPaymaster.setFeeCollector(DAN_ADDRESS); + vm.stopPrank(); } } diff --git a/test/foundry/base/NexusTestBase.sol b/test/foundry/base/NexusTestBase.sol index 778f1a2..1bfef51 100644 --- a/test/foundry/base/NexusTestBase.sol +++ b/test/foundry/base/NexusTestBase.sol @@ -20,7 +20,16 @@ import { MockHook } from "@nexus/contracts/mocks/MockHook.sol"; // import { MockExecutor } from "@nexus/contracts/mocks/MockExecutor.sol"; import { MockHandler } from "@nexus/contracts/mocks/MockHandler.sol"; import { BootstrapLib } from "@nexus/contracts/lib/BootstrapLib.sol"; -import { ModeLib, ExecutionMode, ExecType, CallType, CALLTYPE_BATCH, CALLTYPE_SINGLE, EXECTYPE_DEFAULT, EXECTYPE_TRY } from "@nexus/contracts/lib/ModeLib.sol"; +import { + ModeLib, + ExecutionMode, + ExecType, + CallType, + CALLTYPE_BATCH, + CALLTYPE_SINGLE, + EXECTYPE_DEFAULT, + EXECTYPE_TRY +} from "@nexus/contracts/lib/ModeLib.sol"; // import { ExecLib, Execution } from "@nexus/contracts/lib/ExecLib.sol"; import { Bootstrap, BootstrapConfig } from "@nexus/contracts/utils/Bootstrap.sol"; import { CheatCodes } from "@nexus/test/foundry/utils/CheatCodes.sol"; @@ -28,8 +37,6 @@ import { EventsAndErrors } from "@nexus/test/foundry/utils/EventsAndErrors.sol"; import { BiconomySponsorshipPaymaster } from "../../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; - - abstract contract NexusTestBase is CheatCodes, EventsAndErrors { // ----------------------------------------- // State Variables @@ -39,23 +46,28 @@ abstract contract NexusTestBase is CheatCodes, EventsAndErrors { Vm.Wallet internal BOB; Vm.Wallet internal ALICE; Vm.Wallet internal CHARLIE; + Vm.Wallet internal DAN; + Vm.Wallet internal EMMA; Vm.Wallet internal BUNDLER; Vm.Wallet internal FACTORY_OWNER; - address constant ENTRYPOINT_ADDRESS = address(0x0000000071727De22E5E9d8BAf0edAc6f37da032); - address internal BOB_ADDRESS; address internal ALICE_ADDRESS; address internal CHARLIE_ADDRESS; + address internal DAN_ADDRESS; + address internal EMMA_ADDRESS; Nexus internal BOB_ACCOUNT; Nexus internal ALICE_ACCOUNT; Nexus internal CHARLIE_ACCOUNT; + Nexus internal DAN_ACCOUNT; + Nexus internal EMMA_ACCOUNT; + address constant ENTRYPOINT_ADDRESS = address(0x0000000071727De22E5E9d8BAf0edAc6f37da032); IEntryPoint internal ENTRYPOINT; + NexusAccountFactory internal FACTORY; BiconomyMetaFactory internal META_FACTORY; - MockHook internal HOOK_MODULE; MockHandler internal HANDLER_MODULE; // MockExecutor internal EXECUTOR_MODULE; MockValidator internal VALIDATOR_MODULE; @@ -83,14 +95,17 @@ abstract contract NexusTestBase is CheatCodes, EventsAndErrors { function setupPredefinedWallets() internal { DEPLOYER = createAndFundWallet("DEPLOYER", 1000 ether); - BOB = createAndFundWallet("BOB", 1000 ether); - BOB_ADDRESS = BOB.addr; - ALICE = createAndFundWallet("ALICE", 1000 ether); + BOB = createAndFundWallet("BOB", 1000 ether); CHARLIE = createAndFundWallet("CHARLIE", 1000 ether); + DAN = createAndFundWallet("DAN", 1000 ether); + EMMA = createAndFundWallet("EMMA", 1000 ether); ALICE_ADDRESS = ALICE.addr; + BOB_ADDRESS = BOB.addr; CHARLIE_ADDRESS = CHARLIE.addr; + DAN_ADDRESS = DAN.addr; + EMMA_ADDRESS = EMMA.addr; FACTORY_OWNER = createAndFundWallet("FACTORY_OWNER", 1000 ether); } @@ -104,7 +119,6 @@ abstract contract NexusTestBase is CheatCodes, EventsAndErrors { META_FACTORY = new BiconomyMetaFactory(address(FACTORY_OWNER.addr)); vm.prank(FACTORY_OWNER.addr); META_FACTORY.addFactoryToWhitelist(address(FACTORY)); - HOOK_MODULE = new MockHook(); HANDLER_MODULE = new MockHandler(); // EXECUTOR_MODULE = new MockExecutor(); VALIDATOR_MODULE = new MockValidator(); @@ -140,6 +154,10 @@ abstract contract NexusTestBase is CheatCodes, EventsAndErrors { vm.label(address(ALICE_ACCOUNT), "ALICE_ACCOUNT"); CHARLIE_ACCOUNT = deployNexus(CHARLIE, 100 ether, address(VALIDATOR_MODULE)); vm.label(address(CHARLIE_ACCOUNT), "CHARLIE_ACCOUNT"); + DAN_ACCOUNT = deployNexus(DAN, 100 ether, address(VALIDATOR_MODULE)); + vm.label(address(DAN_ACCOUNT), "DAN_ACCOUNT"); + EMMA_ACCOUNT = deployNexus(EMMA, 100 ether, address(VALIDATOR_MODULE)); + vm.label(address(EMMA_ACCOUNT), "EMMA_ACCOUNT"); } // ----------------------------------------- // Utility Functions @@ -599,7 +617,8 @@ abstract contract NexusTestBase is CheatCodes, EventsAndErrors { } // /// @notice Generates and signs the paymaster data for a user operation. - // /// @dev This function prepares the `paymasterAndData` field for a `PackedUserOperation` with the correct signature. + // /// @dev This function prepares the `paymasterAndData` field for a `PackedUserOperation` with the correct + // signature. // /// @param userOp The user operation to be signed. // /// @param signer The wallet that will sign the paymaster hash. // /// @param paymaster The paymaster contract. From 46d8c8902f2c0d26953e8da4e57ff87113dda3e0 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Fri, 28 Jun 2024 13:19:28 +0400 Subject: [PATCH 07/21] quick fix --- test/foundry/SponsorshipPaymasterWithPremium.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/foundry/SponsorshipPaymasterWithPremium.t.sol b/test/foundry/SponsorshipPaymasterWithPremium.t.sol index 14f8bf9..58050b6 100644 --- a/test/foundry/SponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/SponsorshipPaymasterWithPremium.t.sol @@ -93,7 +93,7 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { vm.startPrank(ALICE_ADDRESS); assertEq(bicoPaymaster.feeCollector(), CHARLIE_ADDRESS); vm.expectRevert(abi.encodeWithSignature("FeeCollectorCannotBeZero()")); - bicoPaymaster.setFeeCollector(DAN_ADDRESS); + bicoPaymaster.setFeeCollector(address(0)); vm.stopPrank(); vm.startPrank(DAN_ADDRESS); From 87c8c69fc1f9b504374d3dc4b23c4dd6c4df95d1 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Fri, 28 Jun 2024 13:53:10 +0400 Subject: [PATCH 08/21] deposit tests --- .../SponsorshipPaymasterWithPremium.t.sol | 27 +++++++++++++++---- test/foundry/base/NexusTestBase.sol | 2 ++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/test/foundry/SponsorshipPaymasterWithPremium.t.sol b/test/foundry/SponsorshipPaymasterWithPremium.t.sol index 58050b6..3e63cc6 100644 --- a/test/foundry/SponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/SponsorshipPaymasterWithPremium.t.sol @@ -14,11 +14,6 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { setupTestEnvironment(); // Deploy Sponsorship Paymaster bicoPaymaster = new BiconomySponsorshipPaymaster(ALICE_ADDRESS, ENTRYPOINT, BOB_ADDRESS, CHARLIE_ADDRESS); - - // Deposit funds for paymaster id - vm.startPrank(CHARLIE_ADDRESS); - ENTRYPOINT.depositTo{ value: 10 ether }(address(bicoPaymaster)); - vm.stopPrank(); } function testDeploy() external { @@ -102,4 +97,26 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { bicoPaymaster.setFeeCollector(DAN_ADDRESS); vm.stopPrank(); } + + function testDepositFor() external { + uint256 dappBalance = bicoPaymaster.getBalance(DAPP_PAYMASTER.addr); + uint256 depositAmount = 10 ether; + assertEq(dappBalance, 0 ether); + bicoPaymaster.depositFor{ value: depositAmount }(DAPP_PAYMASTER.addr); + dappBalance = bicoPaymaster.getBalance(DAPP_PAYMASTER.addr); + assertEq(dappBalance, depositAmount); + } + + function testInvalidDepositFor() external { + vm.expectRevert(abi.encodeWithSignature("PaymasterIdCannotBeZero()")); + bicoPaymaster.depositFor{ value: 1 ether }(address(0)); + + vm.expectRevert(abi.encodeWithSignature("DepositCanNotBeZero()")); + bicoPaymaster.depositFor{ value: 0 ether }(DAPP_PAYMASTER.addr); + } + + function testInvalidDeposit() external { + vm.expectRevert("Use depositFor() instead"); + bicoPaymaster.deposit{ value: 1 ether }(); + } } diff --git a/test/foundry/base/NexusTestBase.sol b/test/foundry/base/NexusTestBase.sol index 1bfef51..33a5392 100644 --- a/test/foundry/base/NexusTestBase.sol +++ b/test/foundry/base/NexusTestBase.sol @@ -49,6 +49,7 @@ abstract contract NexusTestBase is CheatCodes, EventsAndErrors { Vm.Wallet internal DAN; Vm.Wallet internal EMMA; Vm.Wallet internal BUNDLER; + Vm.Wallet internal DAPP_PAYMASTER; Vm.Wallet internal FACTORY_OWNER; address internal BOB_ADDRESS; @@ -107,6 +108,7 @@ abstract contract NexusTestBase is CheatCodes, EventsAndErrors { DAN_ADDRESS = DAN.addr; EMMA_ADDRESS = EMMA.addr; + DAPP_PAYMASTER = createAndFundWallet("DAPP_PAYMASTER", 1000 ether); FACTORY_OWNER = createAndFundWallet("FACTORY_OWNER", 1000 ether); } From 6a85282f7eac6f5fec6211dd2329c6d58db73569 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Fri, 28 Jun 2024 14:21:56 +0400 Subject: [PATCH 09/21] rename tests according to naming conventions --- .../SponsorshipPaymasterWithPremium.t.sol | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/test/foundry/SponsorshipPaymasterWithPremium.t.sol b/test/foundry/SponsorshipPaymasterWithPremium.t.sol index 3e63cc6..5258ea1 100644 --- a/test/foundry/SponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/SponsorshipPaymasterWithPremium.t.sol @@ -16,7 +16,7 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { bicoPaymaster = new BiconomySponsorshipPaymaster(ALICE_ADDRESS, ENTRYPOINT, BOB_ADDRESS, CHARLIE_ADDRESS); } - function testDeploy() external { + function test_Deploy() external { BiconomySponsorshipPaymaster testArtifact = new BiconomySponsorshipPaymaster(ALICE_ADDRESS, ENTRYPOINT, BOB_ADDRESS, CHARLIE_ADDRESS); assertEq(testArtifact.owner(), ALICE_ADDRESS); @@ -25,14 +25,14 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { assertEq(testArtifact.feeCollector(), CHARLIE_ADDRESS); } - function testCheckStates() external { + function test_CheckStates() external { assertEq(bicoPaymaster.owner(), ALICE_ADDRESS); assertEq(address(bicoPaymaster.entryPoint()), ENTRYPOINT_ADDRESS); assertEq(bicoPaymaster.verifyingSigner(), BOB_ADDRESS); assertEq(bicoPaymaster.feeCollector(), CHARLIE_ADDRESS); } - function testOwnershipTransfer() external { + function test_OwnershipTransfer() external { vm.startPrank(ALICE_ADDRESS); assertEq(bicoPaymaster.owner(), ALICE_ADDRESS); bicoPaymaster.transferOwnership(DAN_ADDRESS); @@ -40,13 +40,15 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { vm.stopPrank(); } - function testInvalidOwnershipTransfer() external { + function test_RevertIf_OwnershipTransferToZeroAddress() external { vm.startPrank(ALICE_ADDRESS); assertEq(bicoPaymaster.owner(), ALICE_ADDRESS); vm.expectRevert(abi.encodeWithSignature("NewOwnerIsZeroAddress()")); bicoPaymaster.transferOwnership(address(0)); vm.stopPrank(); + } + function test_RevertIf_UnauthorizedOwnershipTransfer() external { vm.startPrank(DAN_ADDRESS); assertEq(bicoPaymaster.owner(), ALICE_ADDRESS); vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); @@ -54,7 +56,7 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { vm.stopPrank(); } - function testChangingVerifyingSigner() external { + function test_SetVerifyingSigner() external { vm.startPrank(ALICE_ADDRESS); assertEq(bicoPaymaster.verifyingSigner(), BOB_ADDRESS); bicoPaymaster.setSigner(DAN_ADDRESS); @@ -62,13 +64,15 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { vm.stopPrank(); } - function testInvalidChangingVerifyingSigner() external { + function test_RevertIf_SetVerifyingSignerToZeroAddress() external { vm.startPrank(ALICE_ADDRESS); assertEq(bicoPaymaster.verifyingSigner(), BOB_ADDRESS); vm.expectRevert(abi.encodeWithSignature("VerifyingSignerCannotBeZero()")); bicoPaymaster.setSigner(address(0)); vm.stopPrank(); + } + function test_RevertIf_UnauthorizedSetVerifyingSigner() external { vm.startPrank(DAN_ADDRESS); assertEq(bicoPaymaster.verifyingSigner(), BOB_ADDRESS); vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); @@ -76,7 +80,7 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { vm.stopPrank(); } - function testChangingFeeCollector() external { + function test_SetFeeCollector() external { vm.startPrank(ALICE_ADDRESS); assertEq(bicoPaymaster.feeCollector(), CHARLIE_ADDRESS); bicoPaymaster.setFeeCollector(DAN_ADDRESS); @@ -84,13 +88,15 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { vm.stopPrank(); } - function testInvalidChangingFeeCollector() external { + function test_RevertIf_SetFeeCollectorToZeroAddress() external { vm.startPrank(ALICE_ADDRESS); assertEq(bicoPaymaster.feeCollector(), CHARLIE_ADDRESS); vm.expectRevert(abi.encodeWithSignature("FeeCollectorCannotBeZero()")); bicoPaymaster.setFeeCollector(address(0)); vm.stopPrank(); + } + function test_RevertIf_UnauthorizedSetFeeCollector() external { vm.startPrank(DAN_ADDRESS); assertEq(bicoPaymaster.feeCollector(), CHARLIE_ADDRESS); vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); @@ -98,7 +104,7 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { vm.stopPrank(); } - function testDepositFor() external { + function test_DepositFor() external { uint256 dappBalance = bicoPaymaster.getBalance(DAPP_PAYMASTER.addr); uint256 depositAmount = 10 ether; assertEq(dappBalance, 0 ether); @@ -107,15 +113,17 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { assertEq(dappBalance, depositAmount); } - function testInvalidDepositFor() external { + function test_RevertIf_DepositForZeroAddress() external { vm.expectRevert(abi.encodeWithSignature("PaymasterIdCannotBeZero()")); bicoPaymaster.depositFor{ value: 1 ether }(address(0)); + } + function test_RevertIf_DepositForZeroValue() external { vm.expectRevert(abi.encodeWithSignature("DepositCanNotBeZero()")); bicoPaymaster.depositFor{ value: 0 ether }(DAPP_PAYMASTER.addr); } - function testInvalidDeposit() external { + function test_RevertIf_DepositCalled() external { vm.expectRevert("Use depositFor() instead"); bicoPaymaster.deposit{ value: 1 ether }(); } From 0eb2f815c52df3092250346ae57d80cdd782182c Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Fri, 28 Jun 2024 16:30:04 +0400 Subject: [PATCH 10/21] Check events being emitted --- test/foundry/Lock.t.sol | 49 ------------------- .../SponsorshipPaymasterWithPremium.t.sol | 9 ++++ test/foundry/base/NexusTestBase.sol | 3 ++ test/foundry/mocks/Counter.sol | 26 ---------- 4 files changed, 12 insertions(+), 75 deletions(-) delete mode 100644 test/foundry/Lock.t.sol delete mode 100644 test/foundry/mocks/Counter.sol diff --git a/test/foundry/Lock.t.sol b/test/foundry/Lock.t.sol deleted file mode 100644 index a6f245b..0000000 --- a/test/foundry/Lock.t.sol +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.26 <0.9.0; - -import { PRBTest } from "@prb/test/src/PRBTest.sol"; -import { Lock } from "../../contracts/test/Lock.sol"; -import { StdCheats } from "forge-std/src/StdCheats.sol"; - -contract LockTest is PRBTest, StdCheats { - Lock public lock; - address payable owner; - - receive() external payable { } - - function setUp() public { - owner = payable(address(this)); - uint256 unlockTime = block.timestamp + 1 days; // Set unlock time to 1 day from now - lock = new Lock{ value: 1 ether }(unlockTime); - } - - function testInitialOwner() public { - assertEq(lock.owner(), owner); - } - - function testWithdrawal() public { - // Fast forward time to surpass the unlockTime - vm.warp(block.timestamp + 2 days); - - uint256 initialBalance = address(this).balance; - lock.withdraw(); - uint256 finalBalance = address(this).balance; - - // Check if the contract's balance was transferred to the owner - assertGt(finalBalance, initialBalance); - } - - function testWithdrawTooEarly() public { - // This test is expected to fail as the withdrawal is too early - vm.expectRevert(bytes("You can't withdraw yet")); - lock.withdraw(); - } - - function testWithdrawByNonOwner() public { - // Change the sender to someone other than the owner - vm.warp(block.timestamp + 2 days); - vm.prank(address(0x123)); - vm.expectRevert(bytes("You aren't the owner")); - lock.withdraw(); - } -} diff --git a/test/foundry/SponsorshipPaymasterWithPremium.t.sol b/test/foundry/SponsorshipPaymasterWithPremium.t.sol index 5258ea1..c7335bd 100644 --- a/test/foundry/SponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/SponsorshipPaymasterWithPremium.t.sol @@ -5,6 +5,7 @@ import { console2 } from "forge-std/src/Console2.sol"; import { NexusTestBase } from "./base/NexusTestBase.sol"; +import { IBiconomySponsorshipPaymaster } from "./../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; import { BiconomySponsorshipPaymaster } from "../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { @@ -35,6 +36,8 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { function test_OwnershipTransfer() external { vm.startPrank(ALICE_ADDRESS); assertEq(bicoPaymaster.owner(), ALICE_ADDRESS); + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); + emit OwnershipTransferred(ALICE_ADDRESS, DAN_ADDRESS); bicoPaymaster.transferOwnership(DAN_ADDRESS); assertEq(bicoPaymaster.owner(), DAN_ADDRESS); vm.stopPrank(); @@ -59,6 +62,8 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { function test_SetVerifyingSigner() external { vm.startPrank(ALICE_ADDRESS); assertEq(bicoPaymaster.verifyingSigner(), BOB_ADDRESS); + vm.expectEmit(true, true, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.VerifyingSignerChanged(BOB_ADDRESS, DAN_ADDRESS, ALICE_ADDRESS); bicoPaymaster.setSigner(DAN_ADDRESS); assertEq(bicoPaymaster.verifyingSigner(), DAN_ADDRESS); vm.stopPrank(); @@ -83,6 +88,8 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { function test_SetFeeCollector() external { vm.startPrank(ALICE_ADDRESS); assertEq(bicoPaymaster.feeCollector(), CHARLIE_ADDRESS); + vm.expectEmit(true, true, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.FeeCollectorChanged(CHARLIE_ADDRESS, DAN_ADDRESS, ALICE_ADDRESS); bicoPaymaster.setFeeCollector(DAN_ADDRESS); assertEq(bicoPaymaster.feeCollector(), DAN_ADDRESS); vm.stopPrank(); @@ -108,6 +115,8 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { uint256 dappBalance = bicoPaymaster.getBalance(DAPP_PAYMASTER.addr); uint256 depositAmount = 10 ether; assertEq(dappBalance, 0 ether); + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.GasDeposited(DAPP_PAYMASTER.addr, depositAmount); bicoPaymaster.depositFor{ value: depositAmount }(DAPP_PAYMASTER.addr); dappBalance = bicoPaymaster.getBalance(DAPP_PAYMASTER.addr); assertEq(dappBalance, depositAmount); diff --git a/test/foundry/base/NexusTestBase.sol b/test/foundry/base/NexusTestBase.sol index 33a5392..2a664ba 100644 --- a/test/foundry/base/NexusTestBase.sol +++ b/test/foundry/base/NexusTestBase.sol @@ -38,6 +38,9 @@ import { EventsAndErrors } from "@nexus/test/foundry/utils/EventsAndErrors.sol"; import { BiconomySponsorshipPaymaster } from "../../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; abstract contract NexusTestBase is CheatCodes, EventsAndErrors { + // Events + event OwnershipTransferred(address indexed oldOwner, address indexed newOwner); + // ----------------------------------------- // State Variables // ----------------------------------------- diff --git a/test/foundry/mocks/Counter.sol b/test/foundry/mocks/Counter.sol deleted file mode 100644 index c4ec3d6..0000000 --- a/test/foundry/mocks/Counter.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.26; - -contract Counter { - uint256 private _number; - - function incrementNumber() public { - _number++; - } - - function decrementNumber() public { - _number--; - } - - function getNumber() public view returns (uint256) { - return _number; - } - - function revertOperation() public pure { - revert("Counter: Revert operation"); - } - - function test_() public pure { - // This function is used to ignore file in coverage report - } -} From 502ee33df63accd5e1fa50114779eb4c3b71873c Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Fri, 28 Jun 2024 17:07:29 +0400 Subject: [PATCH 11/21] tests for withdrawing from paymaster --- .../SponsorshipPaymasterWithPremium.t.sol | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/test/foundry/SponsorshipPaymasterWithPremium.t.sol b/test/foundry/SponsorshipPaymasterWithPremium.t.sol index c7335bd..e0fa8a2 100644 --- a/test/foundry/SponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/SponsorshipPaymasterWithPremium.t.sol @@ -112,14 +112,14 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { } function test_DepositFor() external { - uint256 dappBalance = bicoPaymaster.getBalance(DAPP_PAYMASTER.addr); + uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_PAYMASTER.addr); uint256 depositAmount = 10 ether; - assertEq(dappBalance, 0 ether); + assertEq(dappPaymasterBalance, 0 ether); vm.expectEmit(true, true, false, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.GasDeposited(DAPP_PAYMASTER.addr, depositAmount); bicoPaymaster.depositFor{ value: depositAmount }(DAPP_PAYMASTER.addr); - dappBalance = bicoPaymaster.getBalance(DAPP_PAYMASTER.addr); - assertEq(dappBalance, depositAmount); + dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_PAYMASTER.addr); + assertEq(dappPaymasterBalance, depositAmount); } function test_RevertIf_DepositForZeroAddress() external { @@ -136,4 +136,34 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { vm.expectRevert("Use depositFor() instead"); bicoPaymaster.deposit{ value: 1 ether }(); } + + function test_WithdrawTo() external { + uint256 depositAmount = 10 ether; + bicoPaymaster.depositFor{ value: depositAmount }(DAPP_PAYMASTER.addr); + uint256 danInitialBalance = DAN_ADDRESS.balance; + + vm.startPrank(DAPP_PAYMASTER.addr); + vm.expectEmit(true, true, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.GasWithdrawn(DAPP_PAYMASTER.addr, DAN_ADDRESS, depositAmount); + bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), depositAmount); + uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_PAYMASTER.addr); + assertEq(dappPaymasterBalance, 0 ether); + uint256 expectedDanBalance = danInitialBalance + depositAmount; + assertEq(DAN_ADDRESS.balance, expectedDanBalance); + vm.stopPrank(); + } + + function test_RevertIf_WithdrawToZeroAddress() external { + vm.startPrank(DAPP_PAYMASTER.addr); + vm.expectRevert(abi.encodeWithSignature("CanNotWithdrawToZeroAddress()")); + bicoPaymaster.withdrawTo(payable(address(0)), 0 ether); + vm.stopPrank(); + } + + function test_RevertIf_WithdrawToExceedsBalance() external { + vm.startPrank(DAPP_PAYMASTER.addr); + vm.expectRevert("Sponsorship Paymaster: Insufficient funds to withdraw from gas tank"); + bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), 1 ether); + vm.stopPrank(); + } } From d7efb30931a9e11814dcf63ff54f26bee5e08a37 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Mon, 1 Jul 2024 13:29:18 +0400 Subject: [PATCH 12/21] tests for validating paymaster and postop --- .../SponsorshipPaymasterWithPremium.t.sol | 178 +++++++--- test/foundry/base/NexusTestBase.sol | 332 ++++-------------- 2 files changed, 214 insertions(+), 296 deletions(-) diff --git a/test/foundry/SponsorshipPaymasterWithPremium.t.sol b/test/foundry/SponsorshipPaymasterWithPremium.t.sol index e0fa8a2..7ed12a2 100644 --- a/test/foundry/SponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/SponsorshipPaymasterWithPremium.t.sol @@ -2,11 +2,10 @@ pragma solidity ^0.8.26; import { console2 } from "forge-std/src/Console2.sol"; - import { NexusTestBase } from "./base/NexusTestBase.sol"; - import { IBiconomySponsorshipPaymaster } from "./../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; import { BiconomySponsorshipPaymaster } from "../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; +import "account-abstraction/contracts/core/UserOperationLib.sol"; contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { BiconomySponsorshipPaymaster public bicoPaymaster; @@ -14,38 +13,39 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { function setUp() public { setupTestEnvironment(); // Deploy Sponsorship Paymaster - bicoPaymaster = new BiconomySponsorshipPaymaster(ALICE_ADDRESS, ENTRYPOINT, BOB_ADDRESS, CHARLIE_ADDRESS); + bicoPaymaster = new BiconomySponsorshipPaymaster( + PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr + ); } function test_Deploy() external { - BiconomySponsorshipPaymaster testArtifact = - new BiconomySponsorshipPaymaster(ALICE_ADDRESS, ENTRYPOINT, BOB_ADDRESS, CHARLIE_ADDRESS); - assertEq(testArtifact.owner(), ALICE_ADDRESS); + BiconomySponsorshipPaymaster testArtifact = new BiconomySponsorshipPaymaster( + PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr + ); + assertEq(testArtifact.owner(), PAYMASTER_OWNER.addr); assertEq(address(testArtifact.entryPoint()), ENTRYPOINT_ADDRESS); - assertEq(testArtifact.verifyingSigner(), BOB_ADDRESS); - assertEq(testArtifact.feeCollector(), CHARLIE_ADDRESS); + assertEq(testArtifact.verifyingSigner(), PAYMASTER_SIGNER.addr); + assertEq(testArtifact.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); } - function test_CheckStates() external { - assertEq(bicoPaymaster.owner(), ALICE_ADDRESS); + function test_CheckInitialPaymasterState() external { + assertEq(bicoPaymaster.owner(), PAYMASTER_OWNER.addr); assertEq(address(bicoPaymaster.entryPoint()), ENTRYPOINT_ADDRESS); - assertEq(bicoPaymaster.verifyingSigner(), BOB_ADDRESS); - assertEq(bicoPaymaster.feeCollector(), CHARLIE_ADDRESS); + assertEq(bicoPaymaster.verifyingSigner(), PAYMASTER_SIGNER.addr); + assertEq(bicoPaymaster.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); } function test_OwnershipTransfer() external { - vm.startPrank(ALICE_ADDRESS); - assertEq(bicoPaymaster.owner(), ALICE_ADDRESS); + vm.startPrank(PAYMASTER_OWNER.addr); vm.expectEmit(true, true, false, true, address(bicoPaymaster)); - emit OwnershipTransferred(ALICE_ADDRESS, DAN_ADDRESS); + emit OwnershipTransferred(PAYMASTER_OWNER.addr, DAN_ADDRESS); bicoPaymaster.transferOwnership(DAN_ADDRESS); assertEq(bicoPaymaster.owner(), DAN_ADDRESS); vm.stopPrank(); } function test_RevertIf_OwnershipTransferToZeroAddress() external { - vm.startPrank(ALICE_ADDRESS); - assertEq(bicoPaymaster.owner(), ALICE_ADDRESS); + vm.startPrank(PAYMASTER_OWNER.addr); vm.expectRevert(abi.encodeWithSignature("NewOwnerIsZeroAddress()")); bicoPaymaster.transferOwnership(address(0)); vm.stopPrank(); @@ -53,25 +53,31 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { function test_RevertIf_UnauthorizedOwnershipTransfer() external { vm.startPrank(DAN_ADDRESS); - assertEq(bicoPaymaster.owner(), ALICE_ADDRESS); vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); bicoPaymaster.transferOwnership(DAN_ADDRESS); vm.stopPrank(); } function test_SetVerifyingSigner() external { - vm.startPrank(ALICE_ADDRESS); - assertEq(bicoPaymaster.verifyingSigner(), BOB_ADDRESS); + vm.startPrank(PAYMASTER_OWNER.addr); vm.expectEmit(true, true, true, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.VerifyingSignerChanged(BOB_ADDRESS, DAN_ADDRESS, ALICE_ADDRESS); + emit IBiconomySponsorshipPaymaster.VerifyingSignerChanged( + PAYMASTER_SIGNER.addr, DAN_ADDRESS, PAYMASTER_OWNER.addr + ); bicoPaymaster.setSigner(DAN_ADDRESS); assertEq(bicoPaymaster.verifyingSigner(), DAN_ADDRESS); vm.stopPrank(); } + function test_RevertIf_SetVerifyingSignerToContract() external { + vm.startPrank(PAYMASTER_OWNER.addr); + vm.expectRevert(abi.encodeWithSignature("VerifyingSignerCannotBeContract()")); + bicoPaymaster.setSigner(ENTRYPOINT_ADDRESS); + vm.stopPrank(); + } + function test_RevertIf_SetVerifyingSignerToZeroAddress() external { - vm.startPrank(ALICE_ADDRESS); - assertEq(bicoPaymaster.verifyingSigner(), BOB_ADDRESS); + vm.startPrank(PAYMASTER_OWNER.addr); vm.expectRevert(abi.encodeWithSignature("VerifyingSignerCannotBeZero()")); bicoPaymaster.setSigner(address(0)); vm.stopPrank(); @@ -79,25 +85,24 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { function test_RevertIf_UnauthorizedSetVerifyingSigner() external { vm.startPrank(DAN_ADDRESS); - assertEq(bicoPaymaster.verifyingSigner(), BOB_ADDRESS); vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); bicoPaymaster.setSigner(DAN_ADDRESS); vm.stopPrank(); } function test_SetFeeCollector() external { - vm.startPrank(ALICE_ADDRESS); - assertEq(bicoPaymaster.feeCollector(), CHARLIE_ADDRESS); + vm.startPrank(PAYMASTER_OWNER.addr); vm.expectEmit(true, true, true, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.FeeCollectorChanged(CHARLIE_ADDRESS, DAN_ADDRESS, ALICE_ADDRESS); + emit IBiconomySponsorshipPaymaster.FeeCollectorChanged( + PAYMASTER_FEE_COLLECTOR.addr, DAN_ADDRESS, PAYMASTER_OWNER.addr + ); bicoPaymaster.setFeeCollector(DAN_ADDRESS); assertEq(bicoPaymaster.feeCollector(), DAN_ADDRESS); vm.stopPrank(); } function test_RevertIf_SetFeeCollectorToZeroAddress() external { - vm.startPrank(ALICE_ADDRESS); - assertEq(bicoPaymaster.feeCollector(), CHARLIE_ADDRESS); + vm.startPrank(PAYMASTER_OWNER.addr); vm.expectRevert(abi.encodeWithSignature("FeeCollectorCannotBeZero()")); bicoPaymaster.setFeeCollector(address(0)); vm.stopPrank(); @@ -105,20 +110,19 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { function test_RevertIf_UnauthorizedSetFeeCollector() external { vm.startPrank(DAN_ADDRESS); - assertEq(bicoPaymaster.feeCollector(), CHARLIE_ADDRESS); vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); bicoPaymaster.setFeeCollector(DAN_ADDRESS); vm.stopPrank(); } function test_DepositFor() external { - uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_PAYMASTER.addr); + uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); uint256 depositAmount = 10 ether; assertEq(dappPaymasterBalance, 0 ether); vm.expectEmit(true, true, false, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.GasDeposited(DAPP_PAYMASTER.addr, depositAmount); - bicoPaymaster.depositFor{ value: depositAmount }(DAPP_PAYMASTER.addr); - dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_PAYMASTER.addr); + emit IBiconomySponsorshipPaymaster.GasDeposited(DAPP_ACCOUNT.addr, depositAmount); + bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); + dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); assertEq(dappPaymasterBalance, depositAmount); } @@ -129,7 +133,7 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { function test_RevertIf_DepositForZeroValue() external { vm.expectRevert(abi.encodeWithSignature("DepositCanNotBeZero()")); - bicoPaymaster.depositFor{ value: 0 ether }(DAPP_PAYMASTER.addr); + bicoPaymaster.depositFor{ value: 0 ether }(DAPP_ACCOUNT.addr); } function test_RevertIf_DepositCalled() external { @@ -139,14 +143,14 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { function test_WithdrawTo() external { uint256 depositAmount = 10 ether; - bicoPaymaster.depositFor{ value: depositAmount }(DAPP_PAYMASTER.addr); + bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); uint256 danInitialBalance = DAN_ADDRESS.balance; - vm.startPrank(DAPP_PAYMASTER.addr); + vm.startPrank(DAPP_ACCOUNT.addr); vm.expectEmit(true, true, true, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.GasWithdrawn(DAPP_PAYMASTER.addr, DAN_ADDRESS, depositAmount); + emit IBiconomySponsorshipPaymaster.GasWithdrawn(DAPP_ACCOUNT.addr, DAN_ADDRESS, depositAmount); bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), depositAmount); - uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_PAYMASTER.addr); + uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); assertEq(dappPaymasterBalance, 0 ether); uint256 expectedDanBalance = danInitialBalance + depositAmount; assertEq(DAN_ADDRESS.balance, expectedDanBalance); @@ -154,16 +158,108 @@ contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { } function test_RevertIf_WithdrawToZeroAddress() external { - vm.startPrank(DAPP_PAYMASTER.addr); + vm.startPrank(DAPP_ACCOUNT.addr); vm.expectRevert(abi.encodeWithSignature("CanNotWithdrawToZeroAddress()")); bicoPaymaster.withdrawTo(payable(address(0)), 0 ether); vm.stopPrank(); } function test_RevertIf_WithdrawToExceedsBalance() external { - vm.startPrank(DAPP_PAYMASTER.addr); + vm.startPrank(DAPP_ACCOUNT.addr); vm.expectRevert("Sponsorship Paymaster: Insufficient funds to withdraw from gas tank"); bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), 1 ether); vm.stopPrank(); } + + function test_ValidatePaymasterAndPostOp() external { + uint256 initialDappPaymasterBalance = 10 ether; + bicoPaymaster.depositFor{ value: initialDappPaymasterBalance }(DAPP_ACCOUNT.addr); + + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + + uint48 validUntil = uint48(block.timestamp + 1 days); + uint48 validAfter = uint48(block.timestamp); + + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + userOp.paymasterAndData = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 + ); + userOp.signature = signUserOp(ALICE, userOp); + + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + + ops[0] = userOp; + + vm.startPrank(BUNDLER.addr); + vm.expectEmit(true, false, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); + vm.expectEmit(true, false, false, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.PremiumCollected(DAPP_ACCOUNT.addr, 0); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + vm.stopPrank(); + + uint256 resultingDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + assertNotEq(initialDappPaymasterBalance, resultingDappPaymasterBalance); + } + + function test_RevertIf_ValidatePaymasterUserOpWithIncorrectSignatureLength() external { + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + + uint48 validUntil = uint48(block.timestamp + 1 days); + uint48 validAfter = uint48(block.timestamp); + + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + userOp.paymasterAndData = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 + ); + userOp.paymasterAndData = excludeLastNBytes(userOp.paymasterAndData, 2); + userOp.signature = signUserOp(ALICE, userOp); + + ops[0] = userOp; + + vm.startPrank(BUNDLER.addr); + vm.expectRevert(); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + vm.stopPrank(); + } + + function test_RevertIf_ValidatePaymasterUserOpWithInvalidPriceMarkUp() external { + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + + uint48 validUntil = uint48(block.timestamp + 1 days); + uint48 validAfter = uint48(block.timestamp); + + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + userOp.paymasterAndData = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, (2e6 + 1) + ); + userOp.signature = signUserOp(ALICE, userOp); + + ops[0] = userOp; + + vm.startPrank(BUNDLER.addr); + vm.expectRevert(); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + vm.stopPrank(); + } + + function test_RevertIf_ValidatePaymasterUserOpWithInsufficientDeposit() external { + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + + uint48 validUntil = uint48(block.timestamp + 1 days); + uint48 validAfter = uint48(block.timestamp); + + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + userOp.paymasterAndData = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 + ); + userOp.signature = signUserOp(ALICE, userOp); + + ops[0] = userOp; + + vm.startPrank(BUNDLER.addr); + vm.expectRevert(); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + vm.stopPrank(); + } } diff --git a/test/foundry/base/NexusTestBase.sol b/test/foundry/base/NexusTestBase.sol index 2a664ba..14728d3 100644 --- a/test/foundry/base/NexusTestBase.sol +++ b/test/foundry/base/NexusTestBase.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.26; import { Test } from "forge-std/src/Test.sol"; import { Vm } from "forge-std/src/Vm.sol"; +import { console2 } from "forge-std/src/console2.sol"; import "solady/src/utils/ECDSA.sol"; @@ -46,23 +47,26 @@ abstract contract NexusTestBase is CheatCodes, EventsAndErrors { // ----------------------------------------- Vm.Wallet internal DEPLOYER; - Vm.Wallet internal BOB; Vm.Wallet internal ALICE; + Vm.Wallet internal BOB; Vm.Wallet internal CHARLIE; Vm.Wallet internal DAN; Vm.Wallet internal EMMA; Vm.Wallet internal BUNDLER; - Vm.Wallet internal DAPP_PAYMASTER; + Vm.Wallet internal PAYMASTER_OWNER; + Vm.Wallet internal PAYMASTER_SIGNER; + Vm.Wallet internal PAYMASTER_FEE_COLLECTOR; + Vm.Wallet internal DAPP_ACCOUNT; Vm.Wallet internal FACTORY_OWNER; - address internal BOB_ADDRESS; address internal ALICE_ADDRESS; + address internal BOB_ADDRESS; address internal CHARLIE_ADDRESS; address internal DAN_ADDRESS; address internal EMMA_ADDRESS; - Nexus internal BOB_ACCOUNT; Nexus internal ALICE_ACCOUNT; + Nexus internal BOB_ACCOUNT; Nexus internal CHARLIE_ACCOUNT; Nexus internal DAN_ACCOUNT; Nexus internal EMMA_ACCOUNT; @@ -98,6 +102,7 @@ abstract contract NexusTestBase is CheatCodes, EventsAndErrors { function setupPredefinedWallets() internal { DEPLOYER = createAndFundWallet("DEPLOYER", 1000 ether); + BUNDLER = createAndFundWallet("BUNDLER", 1000 ether); ALICE = createAndFundWallet("ALICE", 1000 ether); BOB = createAndFundWallet("BOB", 1000 ether); @@ -111,7 +116,10 @@ abstract contract NexusTestBase is CheatCodes, EventsAndErrors { DAN_ADDRESS = DAN.addr; EMMA_ADDRESS = EMMA.addr; - DAPP_PAYMASTER = createAndFundWallet("DAPP_PAYMASTER", 1000 ether); + PAYMASTER_OWNER = createAndFundWallet("PAYMASTER_OWNER", 1000 ether); + PAYMASTER_SIGNER = createAndFundWallet("PAYMASTER_SIGNER", 1000 ether); + PAYMASTER_FEE_COLLECTOR = createAndFundWallet("PAYMASTER_FEE_COLLECTOR", 1000 ether); + DAPP_ACCOUNT = createAndFundWallet("DAPP_ACCOUNT", 1000 ether); FACTORY_OWNER = createAndFundWallet("FACTORY_OWNER", 1000 ether); } @@ -330,136 +338,6 @@ abstract contract NexusTestBase is CheatCodes, EventsAndErrors { signature = abi.encodePacked(r, s, v); } - // /// @notice Prepares a packed user operation with specified parameters - // /// @param signer The wallet to sign the operation - // /// @param account The Nexus account - // /// @param execType The execution type - // /// @param executions The executions to include - // /// @param validator The validator address - // /// @return userOps The prepared packed user operations - // function buildPackedUserOperation( - // Vm.Wallet memory signer, - // Nexus account, - // ExecType execType, - // Execution[] memory executions, - // address validator - // ) - // internal - // view - // returns (PackedUserOperation[] memory userOps) - // { - // // Validate execType - // require(execType == EXECTYPE_DEFAULT || execType == EXECTYPE_TRY, "Invalid ExecType"); - - // // Determine mode and calldata based on callType and executions length - // ExecutionMode mode; - // bytes memory executionCalldata; - // uint256 length = executions.length; - - // if (length == 1) { - // mode = (execType == EXECTYPE_DEFAULT) ? ModeLib.encodeSimpleSingle() : ModeLib.encodeTrySingle(); - // executionCalldata = abi.encodeCall( - // Nexus.execute, - // (mode, ExecLib.encodeSingle(executions[0].target, executions[0].value, executions[0].callData)) - // ); - // } else if (length > 1) { - // mode = (execType == EXECTYPE_DEFAULT) ? ModeLib.encodeSimpleBatch() : ModeLib.encodeTryBatch(); - // executionCalldata = abi.encodeCall(Nexus.execute, (mode, ExecLib.encodeBatch(executions))); - // } else { - // revert("Executions array cannot be empty"); - // } - - // // Initialize the userOps array with one operation - // userOps = new PackedUserOperation[](1); - - // // Build the UserOperation - // userOps[0] = buildPackedUserOp(address(account), getNonce(address(account), validator)); - // userOps[0].callData = executionCalldata; - - // // Sign the operation - // bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]); - // userOps[0].signature = signMessage(signer, userOpHash); - - // return userOps; - // } - - /// @dev Returns a random non-zero address. - /// @notice Returns a random non-zero address - /// @return result A random non-zero address - function randomNonZeroAddress() internal returns (address result) { - do { - result = address(uint160(random())); - } while (result == address(0)); - } - - /// @notice Checks if an address is a contract - /// @param account The address to check - /// @return True if the address is a contract, false otherwise - function isContract(address account) internal view returns (bool) { - uint256 size; - assembly { - size := extcodesize(account) - } - return size > 0; - } - - /// @dev credits: vectorized || solady - /// @dev Returns a pseudorandom random number from [0 .. 2**256 - 1] (inclusive). - /// For usage in fuzz tests, please ensure that the function has an unnamed uint256 argument. - /// e.g. `testSomething(uint256) public`. - function random() internal returns (uint256 r) { - /// @solidity memory-safe-assembly - assembly { - // This is the keccak256 of a very long string I randomly mashed on my keyboard. - let sSlot := 0xd715531fe383f818c5f158c342925dcf01b954d24678ada4d07c36af0f20e1ee - let sValue := sload(sSlot) - - mstore(0x20, sValue) - r := keccak256(0x20, 0x40) - - // If the storage is uninitialized, initialize it to the keccak256 of the calldata. - if iszero(sValue) { - sValue := sSlot - let m := mload(0x40) - calldatacopy(m, 0, calldatasize()) - r := keccak256(m, calldatasize()) - } - sstore(sSlot, add(r, 1)) - - // Do some biased sampling for more robust tests. - // prettier-ignore - for { } 1 { } { - let d := byte(0, r) - // With a 1/256 chance, randomly set `r` to any of 0,1,2. - if iszero(d) { - r := and(r, 3) - break - } - // With a 1/2 chance, set `r` to near a random power of 2. - if iszero(and(2, d)) { - // Set `t` either `not(0)` or `xor(sValue, r)`. - let t := xor(not(0), mul(iszero(and(4, d)), not(xor(sValue, r)))) - // Set `r` to `t` shifted left or right by a random multiple of 8. - switch and(8, d) - case 0 { - if iszero(and(16, d)) { t := 1 } - r := add(shl(shl(3, and(byte(3, r), 0x1f)), t), sub(and(r, 7), 3)) - } - default { - if iszero(and(16, d)) { t := shl(255, 1) } - r := add(shr(shl(3, and(byte(3, r), 0x1f)), t), sub(and(r, 7), 3)) - } - // With a 1/2 chance, negate `r`. - if iszero(and(0x20, d)) { r := not(r) } - break - } - // Otherwise, just set `r` to `xor(sValue, r)`. - r := xor(sValue, r) - break - } - } - } - /// @notice Pre-funds a smart account and asserts success /// @param sa The smart account address /// @param prefundAmount The amount to pre-fund @@ -468,76 +346,6 @@ abstract contract NexusTestBase is CheatCodes, EventsAndErrors { assertTrue(res, "Pre-funding account should succeed"); } - // /// @notice Prepares a single execution - // /// @param to The target address - // /// @param value The value to send - // /// @param data The call data - // /// @return execution The prepared execution array - // function prepareSingleExecution( - // address to, - // uint256 value, - // bytes memory data - // ) - // internal - // pure - // returns (Execution[] memory execution) - // { - // execution = new Execution[](1); - // execution[0] = Execution(to, value, data); - // } - - // /// @notice Prepares several identical executions - // /// @param execution The execution to duplicate - // /// @param executionsNumber The number of executions to prepare - // /// @return executions The prepared executions array - // function prepareSeveralIdenticalExecutions( - // Execution memory execution, - // uint256 executionsNumber - // ) - // internal - // pure - // returns (Execution[] memory) - // { - // Execution[] memory executions = new Execution[](executionsNumber); - // for (uint256 i = 0; i < executionsNumber; i++) { - // executions[i] = execution; - // } - // return executions; - // } - - // /// @notice Helper function to execute a single operation. - // function executeSingle( - // Vm.Wallet memory user, - // Nexus userAccount, - // address target, - // uint256 value, - // bytes memory callData, - // ExecType execType - // ) - // internal - // { - // Execution[] memory executions = new Execution[](1); - // executions[0] = Execution({ target: target, value: value, callData: callData }); - - // PackedUserOperation[] memory userOps = - // buildPackedUserOperation(user, userAccount, execType, executions, address(VALIDATOR_MODULE)); - // ENTRYPOINT.handleOps(userOps, payable(user.addr)); - // } - - // /// @notice Helper function to execute a batch of operations. - // function executeBatch( - // Vm.Wallet memory user, - // Nexus userAccount, - // Execution[] memory executions, - // ExecType execType - // ) - // internal - // { - // PackedUserOperation[] memory userOps = - // buildPackedUserOperation(user, userAccount, execType, executions, address(VALIDATOR_MODULE)); - // ENTRYPOINT.handleOps(userOps, payable(user.addr)); - // } - /// @notice Calculates the gas cost of the calldata /// @param data The calldata /// @return calldataGas The gas cost of the calldata @@ -621,54 +429,68 @@ abstract contract NexusTestBase is CheatCodes, EventsAndErrors { gasUsed = gasStart - gasleft(); } - // /// @notice Generates and signs the paymaster data for a user operation. - // /// @dev This function prepares the `paymasterAndData` field for a `PackedUserOperation` with the correct - // signature. - // /// @param userOp The user operation to be signed. - // /// @param signer The wallet that will sign the paymaster hash. - // /// @param paymaster The paymaster contract. - // /// @return Updated `PackedUserOperation` with `paymasterAndData` field correctly set. - // function generateAndSignPaymasterData( - // PackedUserOperation memory userOp, - // Vm.Wallet memory signer, - // BiconomySponsorshipPaymaster paymaster - // ) - // internal - // view - // returns (bytes memory) - // { - // // Validity timestamps - // uint48 validUntil = uint48(block.timestamp + 1 days); - // uint48 validAfter = uint48(block.timestamp); - - // // Initial paymaster data with zero signature - // bytes memory initialPmData = abi.encodePacked( - // address(paymaster), - // uint128(3e6), // Verification gas limit - // uint128(0), // Post-operation gas limit - // abi.encode(validUntil, validAfter), - // new bytes(65) // Zero signature - // ); - - // // Update user operation with initial paymaster data - // userOp.paymasterAndData = initialPmData; - - // // Generate hash to be signed - // bytes32 paymasterHash = paymaster.getHash(userOp, validUntil, validAfter); - - // // Sign the hash - // bytes memory paymasterSignature = signMessage(signer, paymasterHash); - // require(paymasterSignature.length == 65, "Invalid Paymaster Signature length"); - - // // Final paymaster data with the actual signature - // bytes memory finalPmData = abi.encodePacked( - // address(paymaster), - // uint128(3e6), // Verification gas limit - // uint128(0), // Post-operation gas limit - // abi.encode(validUntil, validAfter), - // paymasterSignature - // ); - - // return finalPmData; - // } + /// @notice Generates and signs the paymaster data for a user operation. + /// @dev This function prepares the `paymasterAndData` field for a `PackedUserOperation` with the correct signature. + /// @param userOp The user operation to be signed. + /// @param signer The wallet that will sign the paymaster hash. + /// @param paymaster The paymaster contract. + /// @return Updated `PackedUserOperation` with `paymasterAndData` field correctly set. + function generateAndSignPaymasterData( + PackedUserOperation memory userOp, + Vm.Wallet memory signer, + BiconomySponsorshipPaymaster paymaster, + address paymasterId, + uint48 validUntil, + uint48 validAfter, + uint32 priceMarkup + ) + internal + view + returns (bytes memory) + { + // Initial paymaster data with zero signature + bytes memory initialPmData = abi.encodePacked( + address(paymaster), + uint128(3e6), + uint128(3e6), + paymasterId, + validUntil, + validAfter, + priceMarkup, + new bytes(65) // Zero signature + ); + + // Update user operation with initial paymaster data + userOp.paymasterAndData = initialPmData; + + // Generate hash to be signed + bytes32 paymasterHash = paymaster.getHash(userOp, paymasterId, validUntil, validAfter, priceMarkup); + + // Sign the hash + bytes memory paymasterSignature = signMessage(signer, paymasterHash); + require(paymasterSignature.length == 65, "Invalid Paymaster Signature length"); + + // Final paymaster data with the actual signature + bytes memory finalPmData = abi.encodePacked( + address(paymaster), + uint128(3e6), + uint128(3e6), + paymasterId, + validUntil, + validAfter, + priceMarkup, + paymasterSignature + ); + + return finalPmData; + } + + function excludeLastNBytes(bytes memory data, uint256 n) internal pure returns (bytes memory) { + require(data.length > n, "Input data is too short"); + bytes memory result = new bytes(data.length - n); + for (uint256 i = 0; i < data.length - n; i++) { + result[i] = data[i]; + } + return result; + } } From 03ff89074570d84b79a737a113cc53cd09d4bdf2 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Mon, 1 Jul 2024 14:23:40 +0400 Subject: [PATCH 13/21] tests for receiving and withdrawing ether to/from paymaster --- .../SponsorshipPaymasterWithPremium.t.sol | 265 ------------------ 1 file changed, 265 deletions(-) delete mode 100644 test/foundry/SponsorshipPaymasterWithPremium.t.sol diff --git a/test/foundry/SponsorshipPaymasterWithPremium.t.sol b/test/foundry/SponsorshipPaymasterWithPremium.t.sol deleted file mode 100644 index 7ed12a2..0000000 --- a/test/foundry/SponsorshipPaymasterWithPremium.t.sol +++ /dev/null @@ -1,265 +0,0 @@ -// SPDX-License-Identifier: Unlicensed -pragma solidity ^0.8.26; - -import { console2 } from "forge-std/src/Console2.sol"; -import { NexusTestBase } from "./base/NexusTestBase.sol"; -import { IBiconomySponsorshipPaymaster } from "./../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; -import { BiconomySponsorshipPaymaster } from "../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; -import "account-abstraction/contracts/core/UserOperationLib.sol"; - -contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { - BiconomySponsorshipPaymaster public bicoPaymaster; - - function setUp() public { - setupTestEnvironment(); - // Deploy Sponsorship Paymaster - bicoPaymaster = new BiconomySponsorshipPaymaster( - PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr - ); - } - - function test_Deploy() external { - BiconomySponsorshipPaymaster testArtifact = new BiconomySponsorshipPaymaster( - PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr - ); - assertEq(testArtifact.owner(), PAYMASTER_OWNER.addr); - assertEq(address(testArtifact.entryPoint()), ENTRYPOINT_ADDRESS); - assertEq(testArtifact.verifyingSigner(), PAYMASTER_SIGNER.addr); - assertEq(testArtifact.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); - } - - function test_CheckInitialPaymasterState() external { - assertEq(bicoPaymaster.owner(), PAYMASTER_OWNER.addr); - assertEq(address(bicoPaymaster.entryPoint()), ENTRYPOINT_ADDRESS); - assertEq(bicoPaymaster.verifyingSigner(), PAYMASTER_SIGNER.addr); - assertEq(bicoPaymaster.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); - } - - function test_OwnershipTransfer() external { - vm.startPrank(PAYMASTER_OWNER.addr); - vm.expectEmit(true, true, false, true, address(bicoPaymaster)); - emit OwnershipTransferred(PAYMASTER_OWNER.addr, DAN_ADDRESS); - bicoPaymaster.transferOwnership(DAN_ADDRESS); - assertEq(bicoPaymaster.owner(), DAN_ADDRESS); - vm.stopPrank(); - } - - function test_RevertIf_OwnershipTransferToZeroAddress() external { - vm.startPrank(PAYMASTER_OWNER.addr); - vm.expectRevert(abi.encodeWithSignature("NewOwnerIsZeroAddress()")); - bicoPaymaster.transferOwnership(address(0)); - vm.stopPrank(); - } - - function test_RevertIf_UnauthorizedOwnershipTransfer() external { - vm.startPrank(DAN_ADDRESS); - vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); - bicoPaymaster.transferOwnership(DAN_ADDRESS); - vm.stopPrank(); - } - - function test_SetVerifyingSigner() external { - vm.startPrank(PAYMASTER_OWNER.addr); - vm.expectEmit(true, true, true, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.VerifyingSignerChanged( - PAYMASTER_SIGNER.addr, DAN_ADDRESS, PAYMASTER_OWNER.addr - ); - bicoPaymaster.setSigner(DAN_ADDRESS); - assertEq(bicoPaymaster.verifyingSigner(), DAN_ADDRESS); - vm.stopPrank(); - } - - function test_RevertIf_SetVerifyingSignerToContract() external { - vm.startPrank(PAYMASTER_OWNER.addr); - vm.expectRevert(abi.encodeWithSignature("VerifyingSignerCannotBeContract()")); - bicoPaymaster.setSigner(ENTRYPOINT_ADDRESS); - vm.stopPrank(); - } - - function test_RevertIf_SetVerifyingSignerToZeroAddress() external { - vm.startPrank(PAYMASTER_OWNER.addr); - vm.expectRevert(abi.encodeWithSignature("VerifyingSignerCannotBeZero()")); - bicoPaymaster.setSigner(address(0)); - vm.stopPrank(); - } - - function test_RevertIf_UnauthorizedSetVerifyingSigner() external { - vm.startPrank(DAN_ADDRESS); - vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); - bicoPaymaster.setSigner(DAN_ADDRESS); - vm.stopPrank(); - } - - function test_SetFeeCollector() external { - vm.startPrank(PAYMASTER_OWNER.addr); - vm.expectEmit(true, true, true, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.FeeCollectorChanged( - PAYMASTER_FEE_COLLECTOR.addr, DAN_ADDRESS, PAYMASTER_OWNER.addr - ); - bicoPaymaster.setFeeCollector(DAN_ADDRESS); - assertEq(bicoPaymaster.feeCollector(), DAN_ADDRESS); - vm.stopPrank(); - } - - function test_RevertIf_SetFeeCollectorToZeroAddress() external { - vm.startPrank(PAYMASTER_OWNER.addr); - vm.expectRevert(abi.encodeWithSignature("FeeCollectorCannotBeZero()")); - bicoPaymaster.setFeeCollector(address(0)); - vm.stopPrank(); - } - - function test_RevertIf_UnauthorizedSetFeeCollector() external { - vm.startPrank(DAN_ADDRESS); - vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); - bicoPaymaster.setFeeCollector(DAN_ADDRESS); - vm.stopPrank(); - } - - function test_DepositFor() external { - uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); - uint256 depositAmount = 10 ether; - assertEq(dappPaymasterBalance, 0 ether); - vm.expectEmit(true, true, false, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.GasDeposited(DAPP_ACCOUNT.addr, depositAmount); - bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); - dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); - assertEq(dappPaymasterBalance, depositAmount); - } - - function test_RevertIf_DepositForZeroAddress() external { - vm.expectRevert(abi.encodeWithSignature("PaymasterIdCannotBeZero()")); - bicoPaymaster.depositFor{ value: 1 ether }(address(0)); - } - - function test_RevertIf_DepositForZeroValue() external { - vm.expectRevert(abi.encodeWithSignature("DepositCanNotBeZero()")); - bicoPaymaster.depositFor{ value: 0 ether }(DAPP_ACCOUNT.addr); - } - - function test_RevertIf_DepositCalled() external { - vm.expectRevert("Use depositFor() instead"); - bicoPaymaster.deposit{ value: 1 ether }(); - } - - function test_WithdrawTo() external { - uint256 depositAmount = 10 ether; - bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); - uint256 danInitialBalance = DAN_ADDRESS.balance; - - vm.startPrank(DAPP_ACCOUNT.addr); - vm.expectEmit(true, true, true, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.GasWithdrawn(DAPP_ACCOUNT.addr, DAN_ADDRESS, depositAmount); - bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), depositAmount); - uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); - assertEq(dappPaymasterBalance, 0 ether); - uint256 expectedDanBalance = danInitialBalance + depositAmount; - assertEq(DAN_ADDRESS.balance, expectedDanBalance); - vm.stopPrank(); - } - - function test_RevertIf_WithdrawToZeroAddress() external { - vm.startPrank(DAPP_ACCOUNT.addr); - vm.expectRevert(abi.encodeWithSignature("CanNotWithdrawToZeroAddress()")); - bicoPaymaster.withdrawTo(payable(address(0)), 0 ether); - vm.stopPrank(); - } - - function test_RevertIf_WithdrawToExceedsBalance() external { - vm.startPrank(DAPP_ACCOUNT.addr); - vm.expectRevert("Sponsorship Paymaster: Insufficient funds to withdraw from gas tank"); - bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), 1 ether); - vm.stopPrank(); - } - - function test_ValidatePaymasterAndPostOp() external { - uint256 initialDappPaymasterBalance = 10 ether; - bicoPaymaster.depositFor{ value: initialDappPaymasterBalance }(DAPP_ACCOUNT.addr); - - PackedUserOperation[] memory ops = new PackedUserOperation[](1); - - uint48 validUntil = uint48(block.timestamp + 1 days); - uint48 validAfter = uint48(block.timestamp); - - PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); - userOp.paymasterAndData = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 - ); - userOp.signature = signUserOp(ALICE, userOp); - - bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); - - ops[0] = userOp; - - vm.startPrank(BUNDLER.addr); - vm.expectEmit(true, false, true, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); - vm.expectEmit(true, false, false, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.PremiumCollected(DAPP_ACCOUNT.addr, 0); - ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - vm.stopPrank(); - - uint256 resultingDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); - assertNotEq(initialDappPaymasterBalance, resultingDappPaymasterBalance); - } - - function test_RevertIf_ValidatePaymasterUserOpWithIncorrectSignatureLength() external { - PackedUserOperation[] memory ops = new PackedUserOperation[](1); - - uint48 validUntil = uint48(block.timestamp + 1 days); - uint48 validAfter = uint48(block.timestamp); - - PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); - userOp.paymasterAndData = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 - ); - userOp.paymasterAndData = excludeLastNBytes(userOp.paymasterAndData, 2); - userOp.signature = signUserOp(ALICE, userOp); - - ops[0] = userOp; - - vm.startPrank(BUNDLER.addr); - vm.expectRevert(); - ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - vm.stopPrank(); - } - - function test_RevertIf_ValidatePaymasterUserOpWithInvalidPriceMarkUp() external { - PackedUserOperation[] memory ops = new PackedUserOperation[](1); - - uint48 validUntil = uint48(block.timestamp + 1 days); - uint48 validAfter = uint48(block.timestamp); - - PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); - userOp.paymasterAndData = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, (2e6 + 1) - ); - userOp.signature = signUserOp(ALICE, userOp); - - ops[0] = userOp; - - vm.startPrank(BUNDLER.addr); - vm.expectRevert(); - ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - vm.stopPrank(); - } - - function test_RevertIf_ValidatePaymasterUserOpWithInsufficientDeposit() external { - PackedUserOperation[] memory ops = new PackedUserOperation[](1); - - uint48 validUntil = uint48(block.timestamp + 1 days); - uint48 validAfter = uint48(block.timestamp); - - PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); - userOp.paymasterAndData = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 - ); - userOp.signature = signUserOp(ALICE, userOp); - - ops[0] = userOp; - - vm.startPrank(BUNDLER.addr); - vm.expectRevert(); - ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - vm.stopPrank(); - } -} From 404b493be9916247b57b7cd748cf7d6844a6f8ad Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Mon, 1 Jul 2024 15:55:49 +0400 Subject: [PATCH 14/21] commit unit tests --- .../SponsorshipPaymasterWithPremium.t.sol | 297 ++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 test/foundry/unit/concrete/SponsorshipPaymasterWithPremium.t.sol diff --git a/test/foundry/unit/concrete/SponsorshipPaymasterWithPremium.t.sol b/test/foundry/unit/concrete/SponsorshipPaymasterWithPremium.t.sol new file mode 100644 index 0000000..69632ed --- /dev/null +++ b/test/foundry/unit/concrete/SponsorshipPaymasterWithPremium.t.sol @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity ^0.8.26; + +import { console2 } from "forge-std/src/Console2.sol"; +import { NexusTestBase } from "../../base/NexusTestBase.sol"; +import { IBiconomySponsorshipPaymaster } from "../../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; +import { BiconomySponsorshipPaymaster } from "../../../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; +import "account-abstraction/contracts/core/UserOperationLib.sol"; + +contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { + BiconomySponsorshipPaymaster public bicoPaymaster; + + function setUp() public { + setupTestEnvironment(); + // Deploy Sponsorship Paymaster + bicoPaymaster = new BiconomySponsorshipPaymaster( + PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr + ); + } + + function test_Deploy() external { + BiconomySponsorshipPaymaster testArtifact = new BiconomySponsorshipPaymaster( + PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr + ); + assertEq(testArtifact.owner(), PAYMASTER_OWNER.addr); + assertEq(address(testArtifact.entryPoint()), ENTRYPOINT_ADDRESS); + assertEq(testArtifact.verifyingSigner(), PAYMASTER_SIGNER.addr); + assertEq(testArtifact.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); + } + + function test_CheckInitialPaymasterState() external { + assertEq(bicoPaymaster.owner(), PAYMASTER_OWNER.addr); + assertEq(address(bicoPaymaster.entryPoint()), ENTRYPOINT_ADDRESS); + assertEq(bicoPaymaster.verifyingSigner(), PAYMASTER_SIGNER.addr); + assertEq(bicoPaymaster.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); + } + + function test_OwnershipTransfer() external { + vm.startPrank(PAYMASTER_OWNER.addr); + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); + emit OwnershipTransferred(PAYMASTER_OWNER.addr, DAN_ADDRESS); + bicoPaymaster.transferOwnership(DAN_ADDRESS); + assertEq(bicoPaymaster.owner(), DAN_ADDRESS); + vm.stopPrank(); + } + + function test_RevertIf_OwnershipTransferToZeroAddress() external { + vm.startPrank(PAYMASTER_OWNER.addr); + vm.expectRevert(abi.encodeWithSignature("NewOwnerIsZeroAddress()")); + bicoPaymaster.transferOwnership(address(0)); + vm.stopPrank(); + } + + function test_RevertIf_UnauthorizedOwnershipTransfer() external { + vm.startPrank(DAN_ADDRESS); + vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); + bicoPaymaster.transferOwnership(DAN_ADDRESS); + vm.stopPrank(); + } + + function test_SetVerifyingSigner() external { + vm.startPrank(PAYMASTER_OWNER.addr); + vm.expectEmit(true, true, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.VerifyingSignerChanged( + PAYMASTER_SIGNER.addr, DAN_ADDRESS, PAYMASTER_OWNER.addr + ); + bicoPaymaster.setSigner(DAN_ADDRESS); + assertEq(bicoPaymaster.verifyingSigner(), DAN_ADDRESS); + vm.stopPrank(); + } + + function test_RevertIf_SetVerifyingSignerToContract() external { + vm.startPrank(PAYMASTER_OWNER.addr); + vm.expectRevert(abi.encodeWithSignature("VerifyingSignerCannotBeContract()")); + bicoPaymaster.setSigner(ENTRYPOINT_ADDRESS); + vm.stopPrank(); + } + + function test_RevertIf_SetVerifyingSignerToZeroAddress() external { + vm.startPrank(PAYMASTER_OWNER.addr); + vm.expectRevert(abi.encodeWithSignature("VerifyingSignerCannotBeZero()")); + bicoPaymaster.setSigner(address(0)); + vm.stopPrank(); + } + + function test_RevertIf_UnauthorizedSetVerifyingSigner() external { + vm.startPrank(DAN_ADDRESS); + vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); + bicoPaymaster.setSigner(DAN_ADDRESS); + vm.stopPrank(); + } + + function test_SetFeeCollector() external { + vm.startPrank(PAYMASTER_OWNER.addr); + vm.expectEmit(true, true, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.FeeCollectorChanged( + PAYMASTER_FEE_COLLECTOR.addr, DAN_ADDRESS, PAYMASTER_OWNER.addr + ); + bicoPaymaster.setFeeCollector(DAN_ADDRESS); + assertEq(bicoPaymaster.feeCollector(), DAN_ADDRESS); + vm.stopPrank(); + } + + function test_RevertIf_SetFeeCollectorToZeroAddress() external { + vm.startPrank(PAYMASTER_OWNER.addr); + vm.expectRevert(abi.encodeWithSignature("FeeCollectorCannotBeZero()")); + bicoPaymaster.setFeeCollector(address(0)); + vm.stopPrank(); + } + + function test_RevertIf_UnauthorizedSetFeeCollector() external { + vm.startPrank(DAN_ADDRESS); + vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); + bicoPaymaster.setFeeCollector(DAN_ADDRESS); + vm.stopPrank(); + } + + function test_DepositFor() external { + uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + uint256 depositAmount = 10 ether; + assertEq(dappPaymasterBalance, 0 ether); + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.GasDeposited(DAPP_ACCOUNT.addr, depositAmount); + bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); + dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + assertEq(dappPaymasterBalance, depositAmount); + } + + function test_RevertIf_DepositForZeroAddress() external { + vm.expectRevert(abi.encodeWithSignature("PaymasterIdCannotBeZero()")); + bicoPaymaster.depositFor{ value: 1 ether }(address(0)); + } + + function test_RevertIf_DepositForZeroValue() external { + vm.expectRevert(abi.encodeWithSignature("DepositCanNotBeZero()")); + bicoPaymaster.depositFor{ value: 0 ether }(DAPP_ACCOUNT.addr); + } + + function test_RevertIf_DepositCalled() external { + vm.expectRevert("Use depositFor() instead"); + bicoPaymaster.deposit{ value: 1 ether }(); + } + + function test_WithdrawTo() external { + uint256 depositAmount = 10 ether; + bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); + uint256 danInitialBalance = DAN_ADDRESS.balance; + + vm.startPrank(DAPP_ACCOUNT.addr); + vm.expectEmit(true, true, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.GasWithdrawn(DAPP_ACCOUNT.addr, DAN_ADDRESS, depositAmount); + bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), depositAmount); + uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + assertEq(dappPaymasterBalance, 0 ether); + uint256 expectedDanBalance = danInitialBalance + depositAmount; + assertEq(DAN_ADDRESS.balance, expectedDanBalance); + vm.stopPrank(); + } + + function test_RevertIf_WithdrawToZeroAddress() external { + vm.startPrank(DAPP_ACCOUNT.addr); + vm.expectRevert(abi.encodeWithSignature("CanNotWithdrawToZeroAddress()")); + bicoPaymaster.withdrawTo(payable(address(0)), 0 ether); + vm.stopPrank(); + } + + function test_RevertIf_WithdrawToExceedsBalance() external { + vm.startPrank(DAPP_ACCOUNT.addr); + vm.expectRevert("Sponsorship Paymaster: Insufficient funds to withdraw from gas tank"); + bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), 1 ether); + vm.stopPrank(); + } + + function test_ValidatePaymasterAndPostOp() external { + uint256 initialDappPaymasterBalance = 10 ether; + bicoPaymaster.depositFor{ value: initialDappPaymasterBalance }(DAPP_ACCOUNT.addr); + + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + + uint48 validUntil = uint48(block.timestamp + 1 days); + uint48 validAfter = uint48(block.timestamp); + + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + userOp.paymasterAndData = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 + ); + userOp.signature = signUserOp(ALICE, userOp); + + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + + ops[0] = userOp; + + vm.startPrank(BUNDLER.addr); + vm.expectEmit(true, false, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); + vm.expectEmit(true, false, false, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.PremiumCollected(DAPP_ACCOUNT.addr, 0); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + vm.stopPrank(); + + uint256 resultingDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + assertNotEq(initialDappPaymasterBalance, resultingDappPaymasterBalance); + } + + function test_RevertIf_ValidatePaymasterUserOpWithIncorrectSignatureLength() external { + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + + uint48 validUntil = uint48(block.timestamp + 1 days); + uint48 validAfter = uint48(block.timestamp); + + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + userOp.paymasterAndData = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 + ); + userOp.paymasterAndData = excludeLastNBytes(userOp.paymasterAndData, 2); + userOp.signature = signUserOp(ALICE, userOp); + + ops[0] = userOp; + + vm.startPrank(BUNDLER.addr); + vm.expectRevert(); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + vm.stopPrank(); + } + + function test_RevertIf_ValidatePaymasterUserOpWithInvalidPriceMarkUp() external { + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + + uint48 validUntil = uint48(block.timestamp + 1 days); + uint48 validAfter = uint48(block.timestamp); + + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + userOp.paymasterAndData = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, (2e6 + 1) + ); + userOp.signature = signUserOp(ALICE, userOp); + + ops[0] = userOp; + + vm.startPrank(BUNDLER.addr); + vm.expectRevert(); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + vm.stopPrank(); + } + + function test_RevertIf_ValidatePaymasterUserOpWithInsufficientDeposit() external { + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + + uint48 validUntil = uint48(block.timestamp + 1 days); + uint48 validAfter = uint48(block.timestamp); + + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + userOp.paymasterAndData = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 + ); + userOp.signature = signUserOp(ALICE, userOp); + + ops[0] = userOp; + + vm.startPrank(BUNDLER.addr); + vm.expectRevert(); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + vm.stopPrank(); + } + + function test_Receive() external { + uint256 initialPaymasterBalance = address(bicoPaymaster).balance; + uint256 sendAmount = 10 ether; + vm.startPrank(ALICE_ADDRESS); + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.Received(ALICE_ADDRESS, sendAmount); + (bool success,) = address(bicoPaymaster).call{ value: sendAmount }(""); + vm.stopPrank(); + assert(success); + uint256 resultingPaymasterBalance = address(bicoPaymaster).balance; + assertEq(resultingPaymasterBalance, initialPaymasterBalance + sendAmount); + } + + function test_WithdrawEth() external { + uint256 initialAliceBalance = ALICE_ADDRESS.balance; + uint256 ethAmount = 10 ether; + vm.deal(address(bicoPaymaster), ethAmount); + vm.startPrank(PAYMASTER_OWNER.addr); + bicoPaymaster.withdrawEth(payable(ALICE_ADDRESS), ethAmount); + vm.stopPrank(); + assertEq(ALICE_ADDRESS.balance, initialAliceBalance + ethAmount); + assertEq(address(bicoPaymaster).balance, 0 ether); + } + + function test_RevertIf_WithdrawEthExceedsBalance() external { + uint256 ethAmount = 10 ether; + vm.startPrank(PAYMASTER_OWNER.addr); + vm.expectRevert("withdraw failed"); + bicoPaymaster.withdrawEth(payable(ALICE_ADDRESS), ethAmount); + vm.stopPrank(); + } +} From 4ba40d4b77ca537467363afbe896dd0ca45161b0 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Mon, 1 Jul 2024 16:13:20 +0400 Subject: [PATCH 15/21] fuzz tests --- .../SponsorshipPaymasterWithPremium.t.sol | 297 ------------------ 1 file changed, 297 deletions(-) delete mode 100644 test/foundry/unit/concrete/SponsorshipPaymasterWithPremium.t.sol diff --git a/test/foundry/unit/concrete/SponsorshipPaymasterWithPremium.t.sol b/test/foundry/unit/concrete/SponsorshipPaymasterWithPremium.t.sol deleted file mode 100644 index 69632ed..0000000 --- a/test/foundry/unit/concrete/SponsorshipPaymasterWithPremium.t.sol +++ /dev/null @@ -1,297 +0,0 @@ -// SPDX-License-Identifier: Unlicensed -pragma solidity ^0.8.26; - -import { console2 } from "forge-std/src/Console2.sol"; -import { NexusTestBase } from "../../base/NexusTestBase.sol"; -import { IBiconomySponsorshipPaymaster } from "../../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; -import { BiconomySponsorshipPaymaster } from "../../../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; -import "account-abstraction/contracts/core/UserOperationLib.sol"; - -contract SponsorshipPaymasterWithPremiumTest is NexusTestBase { - BiconomySponsorshipPaymaster public bicoPaymaster; - - function setUp() public { - setupTestEnvironment(); - // Deploy Sponsorship Paymaster - bicoPaymaster = new BiconomySponsorshipPaymaster( - PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr - ); - } - - function test_Deploy() external { - BiconomySponsorshipPaymaster testArtifact = new BiconomySponsorshipPaymaster( - PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr - ); - assertEq(testArtifact.owner(), PAYMASTER_OWNER.addr); - assertEq(address(testArtifact.entryPoint()), ENTRYPOINT_ADDRESS); - assertEq(testArtifact.verifyingSigner(), PAYMASTER_SIGNER.addr); - assertEq(testArtifact.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); - } - - function test_CheckInitialPaymasterState() external { - assertEq(bicoPaymaster.owner(), PAYMASTER_OWNER.addr); - assertEq(address(bicoPaymaster.entryPoint()), ENTRYPOINT_ADDRESS); - assertEq(bicoPaymaster.verifyingSigner(), PAYMASTER_SIGNER.addr); - assertEq(bicoPaymaster.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); - } - - function test_OwnershipTransfer() external { - vm.startPrank(PAYMASTER_OWNER.addr); - vm.expectEmit(true, true, false, true, address(bicoPaymaster)); - emit OwnershipTransferred(PAYMASTER_OWNER.addr, DAN_ADDRESS); - bicoPaymaster.transferOwnership(DAN_ADDRESS); - assertEq(bicoPaymaster.owner(), DAN_ADDRESS); - vm.stopPrank(); - } - - function test_RevertIf_OwnershipTransferToZeroAddress() external { - vm.startPrank(PAYMASTER_OWNER.addr); - vm.expectRevert(abi.encodeWithSignature("NewOwnerIsZeroAddress()")); - bicoPaymaster.transferOwnership(address(0)); - vm.stopPrank(); - } - - function test_RevertIf_UnauthorizedOwnershipTransfer() external { - vm.startPrank(DAN_ADDRESS); - vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); - bicoPaymaster.transferOwnership(DAN_ADDRESS); - vm.stopPrank(); - } - - function test_SetVerifyingSigner() external { - vm.startPrank(PAYMASTER_OWNER.addr); - vm.expectEmit(true, true, true, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.VerifyingSignerChanged( - PAYMASTER_SIGNER.addr, DAN_ADDRESS, PAYMASTER_OWNER.addr - ); - bicoPaymaster.setSigner(DAN_ADDRESS); - assertEq(bicoPaymaster.verifyingSigner(), DAN_ADDRESS); - vm.stopPrank(); - } - - function test_RevertIf_SetVerifyingSignerToContract() external { - vm.startPrank(PAYMASTER_OWNER.addr); - vm.expectRevert(abi.encodeWithSignature("VerifyingSignerCannotBeContract()")); - bicoPaymaster.setSigner(ENTRYPOINT_ADDRESS); - vm.stopPrank(); - } - - function test_RevertIf_SetVerifyingSignerToZeroAddress() external { - vm.startPrank(PAYMASTER_OWNER.addr); - vm.expectRevert(abi.encodeWithSignature("VerifyingSignerCannotBeZero()")); - bicoPaymaster.setSigner(address(0)); - vm.stopPrank(); - } - - function test_RevertIf_UnauthorizedSetVerifyingSigner() external { - vm.startPrank(DAN_ADDRESS); - vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); - bicoPaymaster.setSigner(DAN_ADDRESS); - vm.stopPrank(); - } - - function test_SetFeeCollector() external { - vm.startPrank(PAYMASTER_OWNER.addr); - vm.expectEmit(true, true, true, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.FeeCollectorChanged( - PAYMASTER_FEE_COLLECTOR.addr, DAN_ADDRESS, PAYMASTER_OWNER.addr - ); - bicoPaymaster.setFeeCollector(DAN_ADDRESS); - assertEq(bicoPaymaster.feeCollector(), DAN_ADDRESS); - vm.stopPrank(); - } - - function test_RevertIf_SetFeeCollectorToZeroAddress() external { - vm.startPrank(PAYMASTER_OWNER.addr); - vm.expectRevert(abi.encodeWithSignature("FeeCollectorCannotBeZero()")); - bicoPaymaster.setFeeCollector(address(0)); - vm.stopPrank(); - } - - function test_RevertIf_UnauthorizedSetFeeCollector() external { - vm.startPrank(DAN_ADDRESS); - vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); - bicoPaymaster.setFeeCollector(DAN_ADDRESS); - vm.stopPrank(); - } - - function test_DepositFor() external { - uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); - uint256 depositAmount = 10 ether; - assertEq(dappPaymasterBalance, 0 ether); - vm.expectEmit(true, true, false, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.GasDeposited(DAPP_ACCOUNT.addr, depositAmount); - bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); - dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); - assertEq(dappPaymasterBalance, depositAmount); - } - - function test_RevertIf_DepositForZeroAddress() external { - vm.expectRevert(abi.encodeWithSignature("PaymasterIdCannotBeZero()")); - bicoPaymaster.depositFor{ value: 1 ether }(address(0)); - } - - function test_RevertIf_DepositForZeroValue() external { - vm.expectRevert(abi.encodeWithSignature("DepositCanNotBeZero()")); - bicoPaymaster.depositFor{ value: 0 ether }(DAPP_ACCOUNT.addr); - } - - function test_RevertIf_DepositCalled() external { - vm.expectRevert("Use depositFor() instead"); - bicoPaymaster.deposit{ value: 1 ether }(); - } - - function test_WithdrawTo() external { - uint256 depositAmount = 10 ether; - bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); - uint256 danInitialBalance = DAN_ADDRESS.balance; - - vm.startPrank(DAPP_ACCOUNT.addr); - vm.expectEmit(true, true, true, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.GasWithdrawn(DAPP_ACCOUNT.addr, DAN_ADDRESS, depositAmount); - bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), depositAmount); - uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); - assertEq(dappPaymasterBalance, 0 ether); - uint256 expectedDanBalance = danInitialBalance + depositAmount; - assertEq(DAN_ADDRESS.balance, expectedDanBalance); - vm.stopPrank(); - } - - function test_RevertIf_WithdrawToZeroAddress() external { - vm.startPrank(DAPP_ACCOUNT.addr); - vm.expectRevert(abi.encodeWithSignature("CanNotWithdrawToZeroAddress()")); - bicoPaymaster.withdrawTo(payable(address(0)), 0 ether); - vm.stopPrank(); - } - - function test_RevertIf_WithdrawToExceedsBalance() external { - vm.startPrank(DAPP_ACCOUNT.addr); - vm.expectRevert("Sponsorship Paymaster: Insufficient funds to withdraw from gas tank"); - bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), 1 ether); - vm.stopPrank(); - } - - function test_ValidatePaymasterAndPostOp() external { - uint256 initialDappPaymasterBalance = 10 ether; - bicoPaymaster.depositFor{ value: initialDappPaymasterBalance }(DAPP_ACCOUNT.addr); - - PackedUserOperation[] memory ops = new PackedUserOperation[](1); - - uint48 validUntil = uint48(block.timestamp + 1 days); - uint48 validAfter = uint48(block.timestamp); - - PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); - userOp.paymasterAndData = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 - ); - userOp.signature = signUserOp(ALICE, userOp); - - bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); - - ops[0] = userOp; - - vm.startPrank(BUNDLER.addr); - vm.expectEmit(true, false, true, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); - vm.expectEmit(true, false, false, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.PremiumCollected(DAPP_ACCOUNT.addr, 0); - ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - vm.stopPrank(); - - uint256 resultingDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); - assertNotEq(initialDappPaymasterBalance, resultingDappPaymasterBalance); - } - - function test_RevertIf_ValidatePaymasterUserOpWithIncorrectSignatureLength() external { - PackedUserOperation[] memory ops = new PackedUserOperation[](1); - - uint48 validUntil = uint48(block.timestamp + 1 days); - uint48 validAfter = uint48(block.timestamp); - - PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); - userOp.paymasterAndData = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 - ); - userOp.paymasterAndData = excludeLastNBytes(userOp.paymasterAndData, 2); - userOp.signature = signUserOp(ALICE, userOp); - - ops[0] = userOp; - - vm.startPrank(BUNDLER.addr); - vm.expectRevert(); - ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - vm.stopPrank(); - } - - function test_RevertIf_ValidatePaymasterUserOpWithInvalidPriceMarkUp() external { - PackedUserOperation[] memory ops = new PackedUserOperation[](1); - - uint48 validUntil = uint48(block.timestamp + 1 days); - uint48 validAfter = uint48(block.timestamp); - - PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); - userOp.paymasterAndData = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, (2e6 + 1) - ); - userOp.signature = signUserOp(ALICE, userOp); - - ops[0] = userOp; - - vm.startPrank(BUNDLER.addr); - vm.expectRevert(); - ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - vm.stopPrank(); - } - - function test_RevertIf_ValidatePaymasterUserOpWithInsufficientDeposit() external { - PackedUserOperation[] memory ops = new PackedUserOperation[](1); - - uint48 validUntil = uint48(block.timestamp + 1 days); - uint48 validAfter = uint48(block.timestamp); - - PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); - userOp.paymasterAndData = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 - ); - userOp.signature = signUserOp(ALICE, userOp); - - ops[0] = userOp; - - vm.startPrank(BUNDLER.addr); - vm.expectRevert(); - ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - vm.stopPrank(); - } - - function test_Receive() external { - uint256 initialPaymasterBalance = address(bicoPaymaster).balance; - uint256 sendAmount = 10 ether; - vm.startPrank(ALICE_ADDRESS); - vm.expectEmit(true, true, false, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.Received(ALICE_ADDRESS, sendAmount); - (bool success,) = address(bicoPaymaster).call{ value: sendAmount }(""); - vm.stopPrank(); - assert(success); - uint256 resultingPaymasterBalance = address(bicoPaymaster).balance; - assertEq(resultingPaymasterBalance, initialPaymasterBalance + sendAmount); - } - - function test_WithdrawEth() external { - uint256 initialAliceBalance = ALICE_ADDRESS.balance; - uint256 ethAmount = 10 ether; - vm.deal(address(bicoPaymaster), ethAmount); - vm.startPrank(PAYMASTER_OWNER.addr); - bicoPaymaster.withdrawEth(payable(ALICE_ADDRESS), ethAmount); - vm.stopPrank(); - assertEq(ALICE_ADDRESS.balance, initialAliceBalance + ethAmount); - assertEq(address(bicoPaymaster).balance, 0 ether); - } - - function test_RevertIf_WithdrawEthExceedsBalance() external { - uint256 ethAmount = 10 ether; - vm.startPrank(PAYMASTER_OWNER.addr); - vm.expectRevert("withdraw failed"); - bicoPaymaster.withdrawEth(payable(ALICE_ADDRESS), ethAmount); - vm.stopPrank(); - } -} From 5868a5cd72f976b205952bc21466945b001e6ee9 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Mon, 1 Jul 2024 16:13:45 +0400 Subject: [PATCH 16/21] all fuzz tests --- ...tSponsorshipPaymasterWithPremiumTest.t.sol | 297 ++++++++++++++++++ ..._TestSponsorshipPaymasterWithPremium.t.sol | 83 +++++ 2 files changed, 380 insertions(+) create mode 100644 test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol create mode 100644 test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol diff --git a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol new file mode 100644 index 0000000..28716cf --- /dev/null +++ b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity ^0.8.26; + +import { console2 } from "forge-std/src/Console2.sol"; +import { NexusTestBase } from "../../base/NexusTestBase.sol"; +import { IBiconomySponsorshipPaymaster } from "../../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; +import { BiconomySponsorshipPaymaster } from "../../../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; +import "account-abstraction/contracts/core/UserOperationLib.sol"; + +contract TestSponsorshipPaymasterWithPremium is NexusTestBase { + BiconomySponsorshipPaymaster public bicoPaymaster; + + function setUp() public { + setupTestEnvironment(); + // Deploy Sponsorship Paymaster + bicoPaymaster = new BiconomySponsorshipPaymaster( + PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr + ); + } + + function test_Deploy() external { + BiconomySponsorshipPaymaster testArtifact = new BiconomySponsorshipPaymaster( + PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr + ); + assertEq(testArtifact.owner(), PAYMASTER_OWNER.addr); + assertEq(address(testArtifact.entryPoint()), ENTRYPOINT_ADDRESS); + assertEq(testArtifact.verifyingSigner(), PAYMASTER_SIGNER.addr); + assertEq(testArtifact.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); + } + + function test_CheckInitialPaymasterState() external { + assertEq(bicoPaymaster.owner(), PAYMASTER_OWNER.addr); + assertEq(address(bicoPaymaster.entryPoint()), ENTRYPOINT_ADDRESS); + assertEq(bicoPaymaster.verifyingSigner(), PAYMASTER_SIGNER.addr); + assertEq(bicoPaymaster.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); + } + + function test_OwnershipTransfer() external { + vm.startPrank(PAYMASTER_OWNER.addr); + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); + emit OwnershipTransferred(PAYMASTER_OWNER.addr, DAN_ADDRESS); + bicoPaymaster.transferOwnership(DAN_ADDRESS); + assertEq(bicoPaymaster.owner(), DAN_ADDRESS); + vm.stopPrank(); + } + + function test_RevertIf_OwnershipTransferToZeroAddress() external { + vm.startPrank(PAYMASTER_OWNER.addr); + vm.expectRevert(abi.encodeWithSignature("NewOwnerIsZeroAddress()")); + bicoPaymaster.transferOwnership(address(0)); + vm.stopPrank(); + } + + function test_RevertIf_UnauthorizedOwnershipTransfer() external { + vm.startPrank(DAN_ADDRESS); + vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); + bicoPaymaster.transferOwnership(DAN_ADDRESS); + vm.stopPrank(); + } + + function test_SetVerifyingSigner() external { + vm.startPrank(PAYMASTER_OWNER.addr); + vm.expectEmit(true, true, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.VerifyingSignerChanged( + PAYMASTER_SIGNER.addr, DAN_ADDRESS, PAYMASTER_OWNER.addr + ); + bicoPaymaster.setSigner(DAN_ADDRESS); + assertEq(bicoPaymaster.verifyingSigner(), DAN_ADDRESS); + vm.stopPrank(); + } + + function test_RevertIf_SetVerifyingSignerToContract() external { + vm.startPrank(PAYMASTER_OWNER.addr); + vm.expectRevert(abi.encodeWithSignature("VerifyingSignerCannotBeContract()")); + bicoPaymaster.setSigner(ENTRYPOINT_ADDRESS); + vm.stopPrank(); + } + + function test_RevertIf_SetVerifyingSignerToZeroAddress() external { + vm.startPrank(PAYMASTER_OWNER.addr); + vm.expectRevert(abi.encodeWithSignature("VerifyingSignerCannotBeZero()")); + bicoPaymaster.setSigner(address(0)); + vm.stopPrank(); + } + + function test_RevertIf_UnauthorizedSetVerifyingSigner() external { + vm.startPrank(DAN_ADDRESS); + vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); + bicoPaymaster.setSigner(DAN_ADDRESS); + vm.stopPrank(); + } + + function test_SetFeeCollector() external { + vm.startPrank(PAYMASTER_OWNER.addr); + vm.expectEmit(true, true, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.FeeCollectorChanged( + PAYMASTER_FEE_COLLECTOR.addr, DAN_ADDRESS, PAYMASTER_OWNER.addr + ); + bicoPaymaster.setFeeCollector(DAN_ADDRESS); + assertEq(bicoPaymaster.feeCollector(), DAN_ADDRESS); + vm.stopPrank(); + } + + function test_RevertIf_SetFeeCollectorToZeroAddress() external { + vm.startPrank(PAYMASTER_OWNER.addr); + vm.expectRevert(abi.encodeWithSignature("FeeCollectorCannotBeZero()")); + bicoPaymaster.setFeeCollector(address(0)); + vm.stopPrank(); + } + + function test_RevertIf_UnauthorizedSetFeeCollector() external { + vm.startPrank(DAN_ADDRESS); + vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); + bicoPaymaster.setFeeCollector(DAN_ADDRESS); + vm.stopPrank(); + } + + function test_DepositFor() external { + uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + uint256 depositAmount = 10 ether; + assertEq(dappPaymasterBalance, 0 ether); + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.GasDeposited(DAPP_ACCOUNT.addr, depositAmount); + bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); + dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + assertEq(dappPaymasterBalance, depositAmount); + } + + function test_RevertIf_DepositForZeroAddress() external { + vm.expectRevert(abi.encodeWithSignature("PaymasterIdCannotBeZero()")); + bicoPaymaster.depositFor{ value: 1 ether }(address(0)); + } + + function test_RevertIf_DepositForZeroValue() external { + vm.expectRevert(abi.encodeWithSignature("DepositCanNotBeZero()")); + bicoPaymaster.depositFor{ value: 0 ether }(DAPP_ACCOUNT.addr); + } + + function test_RevertIf_DepositCalled() external { + vm.expectRevert("Use depositFor() instead"); + bicoPaymaster.deposit{ value: 1 ether }(); + } + + function test_WithdrawTo() external { + uint256 depositAmount = 10 ether; + bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); + uint256 danInitialBalance = DAN_ADDRESS.balance; + + vm.startPrank(DAPP_ACCOUNT.addr); + vm.expectEmit(true, true, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.GasWithdrawn(DAPP_ACCOUNT.addr, DAN_ADDRESS, depositAmount); + bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), depositAmount); + uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + assertEq(dappPaymasterBalance, 0 ether); + uint256 expectedDanBalance = danInitialBalance + depositAmount; + assertEq(DAN_ADDRESS.balance, expectedDanBalance); + vm.stopPrank(); + } + + function test_RevertIf_WithdrawToZeroAddress() external { + vm.startPrank(DAPP_ACCOUNT.addr); + vm.expectRevert(abi.encodeWithSignature("CanNotWithdrawToZeroAddress()")); + bicoPaymaster.withdrawTo(payable(address(0)), 0 ether); + vm.stopPrank(); + } + + function test_RevertIf_WithdrawToExceedsBalance() external { + vm.startPrank(DAPP_ACCOUNT.addr); + vm.expectRevert("Sponsorship Paymaster: Insufficient funds to withdraw from gas tank"); + bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), 1 ether); + vm.stopPrank(); + } + + function test_ValidatePaymasterAndPostOp() external { + uint256 initialDappPaymasterBalance = 10 ether; + bicoPaymaster.depositFor{ value: initialDappPaymasterBalance }(DAPP_ACCOUNT.addr); + + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + + uint48 validUntil = uint48(block.timestamp + 1 days); + uint48 validAfter = uint48(block.timestamp); + + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + userOp.paymasterAndData = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 + ); + userOp.signature = signUserOp(ALICE, userOp); + + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + + ops[0] = userOp; + + vm.startPrank(BUNDLER.addr); + vm.expectEmit(true, false, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); + vm.expectEmit(true, false, false, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.PremiumCollected(DAPP_ACCOUNT.addr, 0); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + vm.stopPrank(); + + uint256 resultingDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + assertNotEq(initialDappPaymasterBalance, resultingDappPaymasterBalance); + } + + function test_RevertIf_ValidatePaymasterUserOpWithIncorrectSignatureLength() external { + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + + uint48 validUntil = uint48(block.timestamp + 1 days); + uint48 validAfter = uint48(block.timestamp); + + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + userOp.paymasterAndData = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 + ); + userOp.paymasterAndData = excludeLastNBytes(userOp.paymasterAndData, 2); + userOp.signature = signUserOp(ALICE, userOp); + + ops[0] = userOp; + + vm.startPrank(BUNDLER.addr); + vm.expectRevert(); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + vm.stopPrank(); + } + + function test_RevertIf_ValidatePaymasterUserOpWithInvalidPriceMarkUp() external { + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + + uint48 validUntil = uint48(block.timestamp + 1 days); + uint48 validAfter = uint48(block.timestamp); + + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + userOp.paymasterAndData = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, (2e6 + 1) + ); + userOp.signature = signUserOp(ALICE, userOp); + + ops[0] = userOp; + + vm.startPrank(BUNDLER.addr); + vm.expectRevert(); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + vm.stopPrank(); + } + + function test_RevertIf_ValidatePaymasterUserOpWithInsufficientDeposit() external { + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + + uint48 validUntil = uint48(block.timestamp + 1 days); + uint48 validAfter = uint48(block.timestamp); + + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + userOp.paymasterAndData = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 + ); + userOp.signature = signUserOp(ALICE, userOp); + + ops[0] = userOp; + + vm.startPrank(BUNDLER.addr); + vm.expectRevert(); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + vm.stopPrank(); + } + + function test_Receive() external { + uint256 initialPaymasterBalance = address(bicoPaymaster).balance; + uint256 sendAmount = 10 ether; + vm.startPrank(ALICE_ADDRESS); + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.Received(ALICE_ADDRESS, sendAmount); + (bool success,) = address(bicoPaymaster).call{ value: sendAmount }(""); + vm.stopPrank(); + assert(success); + uint256 resultingPaymasterBalance = address(bicoPaymaster).balance; + assertEq(resultingPaymasterBalance, initialPaymasterBalance + sendAmount); + } + + function test_WithdrawEth() external { + uint256 initialAliceBalance = ALICE_ADDRESS.balance; + uint256 ethAmount = 10 ether; + vm.deal(address(bicoPaymaster), ethAmount); + vm.startPrank(PAYMASTER_OWNER.addr); + bicoPaymaster.withdrawEth(payable(ALICE_ADDRESS), ethAmount); + vm.stopPrank(); + assertEq(ALICE_ADDRESS.balance, initialAliceBalance + ethAmount); + assertEq(address(bicoPaymaster).balance, 0 ether); + } + + function test_RevertIf_WithdrawEthExceedsBalance() external { + uint256 ethAmount = 10 ether; + vm.startPrank(PAYMASTER_OWNER.addr); + vm.expectRevert("withdraw failed"); + bicoPaymaster.withdrawEth(payable(ALICE_ADDRESS), ethAmount); + vm.stopPrank(); + } +} diff --git a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol new file mode 100644 index 0000000..5fbf8fb --- /dev/null +++ b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity ^0.8.26; + +import { console2 } from "forge-std/src/Console2.sol"; +import { NexusTestBase } from "../../base/NexusTestBase.sol"; +import { IBiconomySponsorshipPaymaster } from "../../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; +import { BiconomySponsorshipPaymaster } from "../../../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; + +contract TestFuzz_SponsorshipPaymasterWithPremium is NexusTestBase { + BiconomySponsorshipPaymaster public bicoPaymaster; + + function setUp() public { + setupTestEnvironment(); + // Deploy Sponsorship Paymaster + bicoPaymaster = new BiconomySponsorshipPaymaster( + PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr + ); + } + + function test_CheckInitialPaymasterState() external { + assertEq(bicoPaymaster.owner(), PAYMASTER_OWNER.addr); + assertEq(address(bicoPaymaster.entryPoint()), ENTRYPOINT_ADDRESS); + assertEq(bicoPaymaster.verifyingSigner(), PAYMASTER_SIGNER.addr); + assertEq(bicoPaymaster.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); + } + + function testFuzz_DepositFor(uint256 depositAmount) external { + vm.assume(depositAmount <= 1000 ether); + vm.assume(depositAmount > 0 ether); + vm.deal(DAPP_ACCOUNT.addr, depositAmount); + uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + assertEq(dappPaymasterBalance, 0 ether); + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.GasDeposited(DAPP_ACCOUNT.addr, depositAmount); + bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); + dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + assertEq(dappPaymasterBalance, depositAmount); + } + + function testFuzz_WithdrawTo(uint256 withdrawAmount) external { + vm.assume(withdrawAmount <= 1000 ether); + vm.assume(withdrawAmount > 0 ether); + vm.deal(DAPP_ACCOUNT.addr, withdrawAmount); + bicoPaymaster.depositFor{ value: withdrawAmount }(DAPP_ACCOUNT.addr); + uint256 danInitialBalance = DAN_ADDRESS.balance; + + vm.startPrank(DAPP_ACCOUNT.addr); + vm.expectEmit(true, true, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.GasWithdrawn(DAPP_ACCOUNT.addr, DAN_ADDRESS, withdrawAmount); + bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), withdrawAmount); + uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + assertEq(dappPaymasterBalance, 0 ether); + uint256 expectedDanBalance = danInitialBalance + withdrawAmount; + assertEq(DAN_ADDRESS.balance, expectedDanBalance); + vm.stopPrank(); + } + + function testFuzz_Receive(uint256 ethAmount) external { + vm.assume(ethAmount <= 1000 ether); + vm.assume(ethAmount > 0 ether); + uint256 initialPaymasterBalance = address(bicoPaymaster).balance; + vm.startPrank(ALICE_ADDRESS); + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.Received(ALICE_ADDRESS, ethAmount); + (bool success,) = address(bicoPaymaster).call{ value: ethAmount }(""); + vm.stopPrank(); + assert(success); + uint256 resultingPaymasterBalance = address(bicoPaymaster).balance; + assertEq(resultingPaymasterBalance, initialPaymasterBalance + ethAmount); + } + + function testFuzz_WithdrawEth(uint256 ethAmount) external { + vm.assume(ethAmount <= 1000 ether); + vm.assume(ethAmount > 0 ether); + uint256 initialAliceBalance = ALICE_ADDRESS.balance; + vm.deal(address(bicoPaymaster), ethAmount); + vm.startPrank(PAYMASTER_OWNER.addr); + bicoPaymaster.withdrawEth(payable(ALICE_ADDRESS), ethAmount); + vm.stopPrank(); + assertEq(ALICE_ADDRESS.balance, initialAliceBalance + ethAmount); + assertEq(address(bicoPaymaster).balance, 0 ether); + } +} From 2fd121c28b8144e7adf3d688963b408226833feb Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Mon, 1 Jul 2024 16:22:47 +0400 Subject: [PATCH 17/21] get rid of nonfuzz test in fuzz file --- .../TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol | 7 ------- 1 file changed, 7 deletions(-) diff --git a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol index 5fbf8fb..930a790 100644 --- a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol @@ -17,13 +17,6 @@ contract TestFuzz_SponsorshipPaymasterWithPremium is NexusTestBase { ); } - function test_CheckInitialPaymasterState() external { - assertEq(bicoPaymaster.owner(), PAYMASTER_OWNER.addr); - assertEq(address(bicoPaymaster.entryPoint()), ENTRYPOINT_ADDRESS); - assertEq(bicoPaymaster.verifyingSigner(), PAYMASTER_SIGNER.addr); - assertEq(bicoPaymaster.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); - } - function testFuzz_DepositFor(uint256 depositAmount) external { vm.assume(depositAmount <= 1000 ether); vm.assume(depositAmount > 0 ether); From 45c20eb5481a27211dd6c23923e8eb611ea5e94e Mon Sep 17 00:00:00 2001 From: livingrockrises <90545960+livingrockrises@users.noreply.github.com> Date: Mon, 1 Jul 2024 19:50:57 +0530 Subject: [PATCH 18/21] forge install: nexus --- .gitmodules | 3 +++ lib/nexus | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/nexus diff --git a/.gitmodules b/.gitmodules index b756d1f..d96cd29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "lib/nexus.git"] path = lib/nexus.git url = https://github.com/bcnmy/nexus.git +[submodule "lib/nexus"] + path = lib/nexus + url = https://github.com/bcnmy/nexus diff --git a/lib/nexus b/lib/nexus new file mode 160000 index 0000000..ab9616b --- /dev/null +++ b/lib/nexus @@ -0,0 +1 @@ +Subproject commit ab9616bd71fcd51048e834f87a7b60dccbfc0adb From d01ace0c6aa9307257f76495d4ff67465a68bfca Mon Sep 17 00:00:00 2001 From: livingrockrises <90545960+livingrockrises@users.noreply.github.com> Date: Tue, 2 Jul 2024 11:50:52 +0530 Subject: [PATCH 19/21] build dep fixes --- .gitmodules | 6 ++--- lib/forge-std | 1 + lib/nexus.git | 1 - package.json | 1 - remappings.txt | 5 ++-- test/foundry/base/NexusTestBase.sol | 26 +++++++++---------- ...tSponsorshipPaymasterWithPremiumTest.t.sol | 2 +- 7 files changed, 20 insertions(+), 22 deletions(-) create mode 160000 lib/forge-std delete mode 160000 lib/nexus.git diff --git a/.gitmodules b/.gitmodules index d96cd29..f008824 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ -[submodule "lib/nexus.git"] - path = lib/nexus.git - url = https://github.com/bcnmy/nexus.git [submodule "lib/nexus"] path = lib/nexus url = https://github.com/bcnmy/nexus +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..8948d45 --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 8948d45d3d9022c508b83eb5d26fd3a7a93f2f32 diff --git a/lib/nexus.git b/lib/nexus.git deleted file mode 160000 index 5d81e53..0000000 --- a/lib/nexus.git +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5d81e533941b49194fbc469b09b182c6c5d0e9d9 diff --git a/package.json b/package.json index 1227124..a59cac8 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ "chai": "^4.3.7", "codecov": "^3.8.3", "ethers": "^6.11.1", - "forge-std": "github:foundry-rs/forge-std#v1.7.6", "hardhat-deploy": "^0.11.45", "hardhat-deploy-ethers": "^0.4.1", "hardhat-gas-reporter": "^1.0.10", diff --git a/remappings.txt b/remappings.txt index b197025..6710ed5 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,9 +1,8 @@ @openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ @prb/test/=node_modules/@prb/test/ -@nexus/=lib/nexus.git/ -forge-std/=node_modules/forge-std/ +nexus/=lib/nexus/ +forge-std/=lib/forge-std/ account-abstraction=node_modules/account-abstraction/ modulekit/=node_modules/modulekit/src/ sentinellist/=node_modules/sentinellist/ solady/=node_modules/solady -ds-test/=node_modules/ds-test/src/ diff --git a/test/foundry/base/NexusTestBase.sol b/test/foundry/base/NexusTestBase.sol index 14728d3..f1b9d11 100644 --- a/test/foundry/base/NexusTestBase.sol +++ b/test/foundry/base/NexusTestBase.sol @@ -13,14 +13,14 @@ import { EntryPoint } from "account-abstraction/contracts/core/EntryPoint.sol"; import { IEntryPoint } from "account-abstraction/contracts/interfaces/IEntryPoint.sol"; import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol"; -import { Nexus } from "@nexus/contracts/Nexus.sol"; -import { NexusAccountFactory } from "@nexus/contracts/factory/NexusAccountFactory.sol"; -import { BiconomyMetaFactory } from "@nexus/contracts/factory/BiconomyMetaFactory.sol"; -import { MockValidator } from "@nexus/contracts/mocks/MockValidator.sol"; -import { MockHook } from "@nexus/contracts/mocks/MockHook.sol"; -// import { MockExecutor } from "@nexus/contracts/mocks/MockExecutor.sol"; -import { MockHandler } from "@nexus/contracts/mocks/MockHandler.sol"; -import { BootstrapLib } from "@nexus/contracts/lib/BootstrapLib.sol"; +import { Nexus } from "nexus/contracts/Nexus.sol"; +import { NexusAccountFactory } from "nexus/contracts/factory/NexusAccountFactory.sol"; +import { BiconomyMetaFactory } from "nexus/contracts/factory/BiconomyMetaFactory.sol"; +import { MockValidator } from "nexus/contracts/mocks/MockValidator.sol"; +import { MockHook } from "nexus/contracts/mocks/MockHook.sol"; +// import { MockExecutor } from "nexus/contracts/mocks/MockExecutor.sol"; +import { MockHandler } from "nexus/contracts/mocks/MockHandler.sol"; +import { BootstrapLib } from "nexus/contracts/lib/BootstrapLib.sol"; import { ModeLib, ExecutionMode, @@ -30,11 +30,11 @@ import { CALLTYPE_SINGLE, EXECTYPE_DEFAULT, EXECTYPE_TRY -} from "@nexus/contracts/lib/ModeLib.sol"; -// import { ExecLib, Execution } from "@nexus/contracts/lib/ExecLib.sol"; -import { Bootstrap, BootstrapConfig } from "@nexus/contracts/utils/Bootstrap.sol"; -import { CheatCodes } from "@nexus/test/foundry/utils/CheatCodes.sol"; -import { EventsAndErrors } from "@nexus/test/foundry/utils/EventsAndErrors.sol"; +} from "nexus/contracts/lib/ModeLib.sol"; +// import { ExecLib, Execution } from "nexus/contracts/lib/ExecLib.sol"; +import { Bootstrap, BootstrapConfig } from "nexus/contracts/utils/Bootstrap.sol"; +import { CheatCodes } from "nexus/test/foundry/utils/CheatCodes.sol"; +import { EventsAndErrors } from "nexus/test/foundry/utils/EventsAndErrors.sol"; import { BiconomySponsorshipPaymaster } from "../../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; diff --git a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol index 28716cf..81e32de 100644 --- a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol +++ b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol @@ -28,7 +28,7 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { assertEq(testArtifact.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); } - function test_CheckInitialPaymasterState() external { + function test_CheckInitialPaymasterState() external view { assertEq(bicoPaymaster.owner(), PAYMASTER_OWNER.addr); assertEq(address(bicoPaymaster.entryPoint()), ENTRYPOINT_ADDRESS); assertEq(bicoPaymaster.verifyingSigner(), PAYMASTER_SIGNER.addr); From 5da6fbedc52581cc690c8ef506382d92134ce8f6 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Wed, 3 Jul 2024 15:48:05 +0400 Subject: [PATCH 20/21] add adam's requested changes --- contracts/common/Errors.sol | 14 +- .../SponsorshipPaymasterWithPremium.sol | 8 +- lib/nexus.git | 1 + test/foundry/base/BaseEventsAndErrors.sol | 17 +++ test/foundry/base/NexusTestBase.sol | 49 +++---- ...tSponsorshipPaymasterWithPremiumTest.t.sol | 103 +++++--------- ..._TestSponsorshipPaymasterWithPremium.t.sol | 23 ++-- test/hardhat/Lock.ts | 130 ------------------ 8 files changed, 94 insertions(+), 251 deletions(-) create mode 160000 lib/nexus.git create mode 100644 test/foundry/base/BaseEventsAndErrors.sol delete mode 100644 test/hardhat/Lock.ts diff --git a/contracts/common/Errors.sol b/contracts/common/Errors.sol index 4e42283..998492a 100644 --- a/contracts/common/Errors.sol +++ b/contracts/common/Errors.sol @@ -2,11 +2,10 @@ pragma solidity ^0.8.26; contract BiconomySponsorshipPaymasterErrors { - /** * @notice Throws when the paymaster address provided is address(0) */ - error PaymasterIdCannotBeZero(); + error PaymasterIdCanNotBeZero(); /** * @notice Throws when the 0 has been provided as deposit @@ -16,26 +15,25 @@ contract BiconomySponsorshipPaymasterErrors { /** * @notice Throws when the verifiying signer address provided is address(0) */ - error VerifyingSignerCannotBeZero(); + error VerifyingSignerCanNotBeZero(); /** * @notice Throws when the fee collector address provided is address(0) */ - error FeeCollectorCannotBeZero(); + error FeeCollectorCanNotBeZero(); /** * @notice Throws when the fee collector address provided is a deployed contract */ - error FeeCollectorCannotBeContract(); + error FeeCollectorCanNotBeContract(); /** * @notice Throws when the fee collector address provided is a deployed contract */ - error VerifyingSignerCannotBeContract(); + error VerifyingSignerCanNotBeContract(); /** * @notice Throws when trying to withdraw to address(0) */ error CanNotWithdrawToZeroAddress(); - -} \ No newline at end of file +} diff --git a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol b/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol index 91d9988..47f7c65 100644 --- a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol +++ b/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol @@ -54,7 +54,7 @@ contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard, Biconom * @param paymasterId dapp identifier for which deposit is being made */ function depositFor(address paymasterId) external payable nonReentrant { - if (paymasterId == address(0)) revert PaymasterIdCannotBeZero(); + if (paymasterId == address(0)) revert PaymasterIdCanNotBeZero(); if (msg.value == 0) revert DepositCanNotBeZero(); paymasterIdBalances[paymasterId] += msg.value; entryPoint.depositTo{value: msg.value}(address(this)); @@ -73,9 +73,9 @@ contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard, Biconom ) external payable onlyOwner { uint256 size; assembly { size := extcodesize(_newVerifyingSigner) } - if(size > 0) revert VerifyingSignerCannotBeContract(); + if(size > 0) revert VerifyingSignerCanNotBeContract(); if (_newVerifyingSigner == address(0)) - revert VerifyingSignerCannotBeZero(); + revert VerifyingSignerCanNotBeZero(); address oldSigner = verifyingSigner; assembly { sstore(verifyingSigner.slot, _newVerifyingSigner) @@ -93,7 +93,7 @@ contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard, Biconom function setFeeCollector( address _newFeeCollector ) external payable onlyOwner { - if (_newFeeCollector == address(0)) revert FeeCollectorCannotBeZero(); + if (_newFeeCollector == address(0)) revert FeeCollectorCanNotBeZero(); address oldFeeCollector = feeCollector; assembly { sstore(feeCollector.slot, _newFeeCollector) diff --git a/lib/nexus.git b/lib/nexus.git new file mode 160000 index 0000000..5d81e53 --- /dev/null +++ b/lib/nexus.git @@ -0,0 +1 @@ +Subproject commit 5d81e533941b49194fbc469b09b182c6c5d0e9d9 diff --git a/test/foundry/base/BaseEventsAndErrors.sol b/test/foundry/base/BaseEventsAndErrors.sol new file mode 100644 index 0000000..497366e --- /dev/null +++ b/test/foundry/base/BaseEventsAndErrors.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity ^0.8.26; + +import { EventsAndErrors } from "nexus/test/foundry/utils/EventsAndErrors.sol"; +import { BiconomySponsorshipPaymasterErrors } from "./../../../contracts/common/Errors.sol"; + +contract BaseEventsAndErrors is EventsAndErrors, BiconomySponsorshipPaymasterErrors { + // ========================== + // Events + // ========================== + event OwnershipTransferred(address indexed oldOwner, address indexed newOwner); + + // ========================== + // Errors + // ========================== + error NewOwnerIsZeroAddress(); +} diff --git a/test/foundry/base/NexusTestBase.sol b/test/foundry/base/NexusTestBase.sol index f1b9d11..6bc0df7 100644 --- a/test/foundry/base/NexusTestBase.sol +++ b/test/foundry/base/NexusTestBase.sol @@ -3,12 +3,9 @@ pragma solidity ^0.8.26; import { Test } from "forge-std/src/Test.sol"; import { Vm } from "forge-std/src/Vm.sol"; -import { console2 } from "forge-std/src/console2.sol"; import "solady/src/utils/ECDSA.sol"; -import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; - import { EntryPoint } from "account-abstraction/contracts/core/EntryPoint.sol"; import { IEntryPoint } from "account-abstraction/contracts/interfaces/IEntryPoint.sol"; import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol"; @@ -17,31 +14,14 @@ import { Nexus } from "nexus/contracts/Nexus.sol"; import { NexusAccountFactory } from "nexus/contracts/factory/NexusAccountFactory.sol"; import { BiconomyMetaFactory } from "nexus/contracts/factory/BiconomyMetaFactory.sol"; import { MockValidator } from "nexus/contracts/mocks/MockValidator.sol"; -import { MockHook } from "nexus/contracts/mocks/MockHook.sol"; -// import { MockExecutor } from "nexus/contracts/mocks/MockExecutor.sol"; -import { MockHandler } from "nexus/contracts/mocks/MockHandler.sol"; import { BootstrapLib } from "nexus/contracts/lib/BootstrapLib.sol"; -import { - ModeLib, - ExecutionMode, - ExecType, - CallType, - CALLTYPE_BATCH, - CALLTYPE_SINGLE, - EXECTYPE_DEFAULT, - EXECTYPE_TRY -} from "nexus/contracts/lib/ModeLib.sol"; -// import { ExecLib, Execution } from "nexus/contracts/lib/ExecLib.sol"; import { Bootstrap, BootstrapConfig } from "nexus/contracts/utils/Bootstrap.sol"; import { CheatCodes } from "nexus/test/foundry/utils/CheatCodes.sol"; -import { EventsAndErrors } from "nexus/test/foundry/utils/EventsAndErrors.sol"; +import { BaseEventsAndErrors } from "./BaseEventsAndErrors.sol"; import { BiconomySponsorshipPaymaster } from "../../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; -abstract contract NexusTestBase is CheatCodes, EventsAndErrors { - // Events - event OwnershipTransferred(address indexed oldOwner, address indexed newOwner); - +abstract contract NexusTestBase is CheatCodes, BaseEventsAndErrors { // ----------------------------------------- // State Variables // ----------------------------------------- @@ -76,13 +56,20 @@ abstract contract NexusTestBase is CheatCodes, EventsAndErrors { NexusAccountFactory internal FACTORY; BiconomyMetaFactory internal META_FACTORY; - MockHandler internal HANDLER_MODULE; - // MockExecutor internal EXECUTOR_MODULE; MockValidator internal VALIDATOR_MODULE; Nexus internal ACCOUNT_IMPLEMENTATION; Bootstrap internal BOOTSTRAPPER; + // ----------------------------------------- + // Modifiers + // ----------------------------------------- + modifier prankModifier(address pranker) { + startPrank(pranker); + _; + stopPrank(); + } + // ----------------------------------------- // Setup Functions // ----------------------------------------- @@ -132,8 +119,6 @@ abstract contract NexusTestBase is CheatCodes, EventsAndErrors { META_FACTORY = new BiconomyMetaFactory(address(FACTORY_OWNER.addr)); vm.prank(FACTORY_OWNER.addr); META_FACTORY.addFactoryToWhitelist(address(FACTORY)); - HANDLER_MODULE = new MockHandler(); - // EXECUTOR_MODULE = new MockExecutor(); VALIDATOR_MODULE = new MockValidator(); BOOTSTRAPPER = new Bootstrap(); } @@ -273,11 +258,11 @@ abstract contract NexusTestBase is CheatCodes, EventsAndErrors { bytes memory signature = signUserOp(wallet, userOp); userOp.signature = signature; } + /// @notice Retrieves the nonce for a given account and validator /// @param account The account address /// @param validator The validator address /// @return nonce The retrieved nonce - function getNonce(address account, address validator) internal view returns (uint256 nonce) { uint192 key = uint192(bytes24(bytes20(address(validator)))); nonce = ENTRYPOINT.getNonce(address(account), key); @@ -439,6 +424,8 @@ abstract contract NexusTestBase is CheatCodes, EventsAndErrors { PackedUserOperation memory userOp, Vm.Wallet memory signer, BiconomySponsorshipPaymaster paymaster, + uint128 paymasterValGasLimit, + uint128 paymasterPostOpGasLimit, address paymasterId, uint48 validUntil, uint48 validAfter, @@ -451,8 +438,8 @@ abstract contract NexusTestBase is CheatCodes, EventsAndErrors { // Initial paymaster data with zero signature bytes memory initialPmData = abi.encodePacked( address(paymaster), - uint128(3e6), - uint128(3e6), + paymasterValGasLimit, + paymasterPostOpGasLimit, paymasterId, validUntil, validAfter, @@ -473,8 +460,8 @@ abstract contract NexusTestBase is CheatCodes, EventsAndErrors { // Final paymaster data with the actual signature bytes memory finalPmData = abi.encodePacked( address(paymaster), - uint128(3e6), - uint128(3e6), + paymasterValGasLimit, + paymasterPostOpGasLimit, paymasterId, validUntil, validAfter, diff --git a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol index 81e32de..caf3dc9 100644 --- a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol +++ b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol @@ -1,11 +1,10 @@ // SPDX-License-Identifier: Unlicensed pragma solidity ^0.8.26; -import { console2 } from "forge-std/src/Console2.sol"; import { NexusTestBase } from "../../base/NexusTestBase.sol"; import { IBiconomySponsorshipPaymaster } from "../../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; import { BiconomySponsorshipPaymaster } from "../../../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; -import "account-abstraction/contracts/core/UserOperationLib.sol"; +import { PackedUserOperation } from "account-abstraction/contracts/core/UserOperationLib.sol"; contract TestSponsorshipPaymasterWithPremium is NexusTestBase { BiconomySponsorshipPaymaster public bicoPaymaster; @@ -35,104 +34,86 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { assertEq(bicoPaymaster.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); } - function test_OwnershipTransfer() external { - vm.startPrank(PAYMASTER_OWNER.addr); + function test_OwnershipTransfer() external prankModifier(PAYMASTER_OWNER.addr) { vm.expectEmit(true, true, false, true, address(bicoPaymaster)); emit OwnershipTransferred(PAYMASTER_OWNER.addr, DAN_ADDRESS); bicoPaymaster.transferOwnership(DAN_ADDRESS); assertEq(bicoPaymaster.owner(), DAN_ADDRESS); - vm.stopPrank(); } - function test_RevertIf_OwnershipTransferToZeroAddress() external { - vm.startPrank(PAYMASTER_OWNER.addr); - vm.expectRevert(abi.encodeWithSignature("NewOwnerIsZeroAddress()")); + function test_RevertIf_OwnershipTransferToZeroAddress() external prankModifier(PAYMASTER_OWNER.addr) { + vm.expectRevert(abi.encodeWithSelector(NewOwnerIsZeroAddress.selector)); bicoPaymaster.transferOwnership(address(0)); - vm.stopPrank(); } function test_RevertIf_UnauthorizedOwnershipTransfer() external { - vm.startPrank(DAN_ADDRESS); - vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); + vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); bicoPaymaster.transferOwnership(DAN_ADDRESS); - vm.stopPrank(); } - function test_SetVerifyingSigner() external { - vm.startPrank(PAYMASTER_OWNER.addr); + function test_SetVerifyingSigner() external prankModifier(PAYMASTER_OWNER.addr) { vm.expectEmit(true, true, true, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.VerifyingSignerChanged( PAYMASTER_SIGNER.addr, DAN_ADDRESS, PAYMASTER_OWNER.addr ); bicoPaymaster.setSigner(DAN_ADDRESS); assertEq(bicoPaymaster.verifyingSigner(), DAN_ADDRESS); - vm.stopPrank(); } - function test_RevertIf_SetVerifyingSignerToContract() external { - vm.startPrank(PAYMASTER_OWNER.addr); - vm.expectRevert(abi.encodeWithSignature("VerifyingSignerCannotBeContract()")); + function test_RevertIf_SetVerifyingSignerToContract() external prankModifier(PAYMASTER_OWNER.addr) { + vm.expectRevert(abi.encodeWithSelector(VerifyingSignerCanNotBeContract.selector)); bicoPaymaster.setSigner(ENTRYPOINT_ADDRESS); - vm.stopPrank(); } - function test_RevertIf_SetVerifyingSignerToZeroAddress() external { - vm.startPrank(PAYMASTER_OWNER.addr); - vm.expectRevert(abi.encodeWithSignature("VerifyingSignerCannotBeZero()")); + function test_RevertIf_SetVerifyingSignerToZeroAddress() external prankModifier(PAYMASTER_OWNER.addr) { + vm.expectRevert(abi.encodeWithSelector(VerifyingSignerCanNotBeZero.selector)); bicoPaymaster.setSigner(address(0)); - vm.stopPrank(); } function test_RevertIf_UnauthorizedSetVerifyingSigner() external { - vm.startPrank(DAN_ADDRESS); - vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); + vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); bicoPaymaster.setSigner(DAN_ADDRESS); - vm.stopPrank(); } - function test_SetFeeCollector() external { - vm.startPrank(PAYMASTER_OWNER.addr); + function test_SetFeeCollector() external prankModifier(PAYMASTER_OWNER.addr) { vm.expectEmit(true, true, true, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.FeeCollectorChanged( PAYMASTER_FEE_COLLECTOR.addr, DAN_ADDRESS, PAYMASTER_OWNER.addr ); bicoPaymaster.setFeeCollector(DAN_ADDRESS); assertEq(bicoPaymaster.feeCollector(), DAN_ADDRESS); - vm.stopPrank(); } - function test_RevertIf_SetFeeCollectorToZeroAddress() external { - vm.startPrank(PAYMASTER_OWNER.addr); - vm.expectRevert(abi.encodeWithSignature("FeeCollectorCannotBeZero()")); + function test_RevertIf_SetFeeCollectorToZeroAddress() external prankModifier(PAYMASTER_OWNER.addr) { + vm.expectRevert(abi.encodeWithSelector(FeeCollectorCanNotBeZero.selector)); bicoPaymaster.setFeeCollector(address(0)); - vm.stopPrank(); } function test_RevertIf_UnauthorizedSetFeeCollector() external { - vm.startPrank(DAN_ADDRESS); - vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); + vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); bicoPaymaster.setFeeCollector(DAN_ADDRESS); - vm.stopPrank(); } function test_DepositFor() external { uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); uint256 depositAmount = 10 ether; assertEq(dappPaymasterBalance, 0 ether); + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.GasDeposited(DAPP_ACCOUNT.addr, depositAmount); bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); + dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); assertEq(dappPaymasterBalance, depositAmount); } function test_RevertIf_DepositForZeroAddress() external { - vm.expectRevert(abi.encodeWithSignature("PaymasterIdCannotBeZero()")); + vm.expectRevert(abi.encodeWithSelector(PaymasterIdCanNotBeZero.selector)); bicoPaymaster.depositFor{ value: 1 ether }(address(0)); } function test_RevertIf_DepositForZeroValue() external { - vm.expectRevert(abi.encodeWithSignature("DepositCanNotBeZero()")); + vm.expectRevert(abi.encodeWithSelector(DepositCanNotBeZero.selector)); bicoPaymaster.depositFor{ value: 0 ether }(DAPP_ACCOUNT.addr); } @@ -141,34 +122,29 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { bicoPaymaster.deposit{ value: 1 ether }(); } - function test_WithdrawTo() external { + function test_WithdrawTo() external prankModifier(DAPP_ACCOUNT.addr) { uint256 depositAmount = 10 ether; bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); uint256 danInitialBalance = DAN_ADDRESS.balance; - vm.startPrank(DAPP_ACCOUNT.addr); vm.expectEmit(true, true, true, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.GasWithdrawn(DAPP_ACCOUNT.addr, DAN_ADDRESS, depositAmount); bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), depositAmount); + uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); assertEq(dappPaymasterBalance, 0 ether); uint256 expectedDanBalance = danInitialBalance + depositAmount; assertEq(DAN_ADDRESS.balance, expectedDanBalance); - vm.stopPrank(); } - function test_RevertIf_WithdrawToZeroAddress() external { - vm.startPrank(DAPP_ACCOUNT.addr); - vm.expectRevert(abi.encodeWithSignature("CanNotWithdrawToZeroAddress()")); + function test_RevertIf_WithdrawToZeroAddress() external prankModifier(DAPP_ACCOUNT.addr) { + vm.expectRevert(abi.encodeWithSelector(CanNotWithdrawToZeroAddress.selector)); bicoPaymaster.withdrawTo(payable(address(0)), 0 ether); - vm.stopPrank(); } - function test_RevertIf_WithdrawToExceedsBalance() external { - vm.startPrank(DAPP_ACCOUNT.addr); + function test_RevertIf_WithdrawToExceedsBalance() external prankModifier(DAPP_ACCOUNT.addr) { vm.expectRevert("Sponsorship Paymaster: Insufficient funds to withdraw from gas tank"); bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), 1 ether); - vm.stopPrank(); } function test_ValidatePaymasterAndPostOp() external { @@ -182,7 +158,7 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); userOp.paymasterAndData = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 + userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 ); userOp.signature = signUserOp(ALICE, userOp); @@ -190,13 +166,11 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { ops[0] = userOp; - vm.startPrank(BUNDLER.addr); vm.expectEmit(true, false, true, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); vm.expectEmit(true, false, false, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.PremiumCollected(DAPP_ACCOUNT.addr, 0); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - vm.stopPrank(); uint256 resultingDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); assertNotEq(initialDappPaymasterBalance, resultingDappPaymasterBalance); @@ -210,17 +184,15 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); userOp.paymasterAndData = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 + userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 ); userOp.paymasterAndData = excludeLastNBytes(userOp.paymasterAndData, 2); userOp.signature = signUserOp(ALICE, userOp); ops[0] = userOp; - vm.startPrank(BUNDLER.addr); vm.expectRevert(); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - vm.stopPrank(); } function test_RevertIf_ValidatePaymasterUserOpWithInvalidPriceMarkUp() external { @@ -231,16 +203,14 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); userOp.paymasterAndData = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, (2e6 + 1) + userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 ); userOp.signature = signUserOp(ALICE, userOp); ops[0] = userOp; - vm.startPrank(BUNDLER.addr); vm.expectRevert(); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - vm.stopPrank(); } function test_RevertIf_ValidatePaymasterUserOpWithInsufficientDeposit() external { @@ -251,47 +221,44 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); userOp.paymasterAndData = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, bicoPaymaster, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 + userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 ); userOp.signature = signUserOp(ALICE, userOp); ops[0] = userOp; - vm.startPrank(BUNDLER.addr); vm.expectRevert(); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - vm.stopPrank(); } - function test_Receive() external { + function test_Receive() external prankModifier(ALICE_ADDRESS) { uint256 initialPaymasterBalance = address(bicoPaymaster).balance; uint256 sendAmount = 10 ether; - vm.startPrank(ALICE_ADDRESS); + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.Received(ALICE_ADDRESS, sendAmount); (bool success,) = address(bicoPaymaster).call{ value: sendAmount }(""); - vm.stopPrank(); + assert(success); uint256 resultingPaymasterBalance = address(bicoPaymaster).balance; assertEq(resultingPaymasterBalance, initialPaymasterBalance + sendAmount); } - function test_WithdrawEth() external { + function test_WithdrawEth() external prankModifier(PAYMASTER_OWNER.addr) { uint256 initialAliceBalance = ALICE_ADDRESS.balance; uint256 ethAmount = 10 ether; vm.deal(address(bicoPaymaster), ethAmount); - vm.startPrank(PAYMASTER_OWNER.addr); + bicoPaymaster.withdrawEth(payable(ALICE_ADDRESS), ethAmount); vm.stopPrank(); + assertEq(ALICE_ADDRESS.balance, initialAliceBalance + ethAmount); assertEq(address(bicoPaymaster).balance, 0 ether); } - function test_RevertIf_WithdrawEthExceedsBalance() external { + function test_RevertIf_WithdrawEthExceedsBalance() external prankModifier(PAYMASTER_OWNER.addr) { uint256 ethAmount = 10 ether; - vm.startPrank(PAYMASTER_OWNER.addr); vm.expectRevert("withdraw failed"); bicoPaymaster.withdrawEth(payable(ALICE_ADDRESS), ethAmount); - vm.stopPrank(); } } diff --git a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol index 930a790..2eb44e7 100644 --- a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol @@ -21,55 +21,58 @@ contract TestFuzz_SponsorshipPaymasterWithPremium is NexusTestBase { vm.assume(depositAmount <= 1000 ether); vm.assume(depositAmount > 0 ether); vm.deal(DAPP_ACCOUNT.addr, depositAmount); + uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); assertEq(dappPaymasterBalance, 0 ether); + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.GasDeposited(DAPP_ACCOUNT.addr, depositAmount); bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); + dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); assertEq(dappPaymasterBalance, depositAmount); } - function testFuzz_WithdrawTo(uint256 withdrawAmount) external { + function testFuzz_WithdrawTo(uint256 withdrawAmount) external prankModifier(DAPP_ACCOUNT.addr) { vm.assume(withdrawAmount <= 1000 ether); vm.assume(withdrawAmount > 0 ether); vm.deal(DAPP_ACCOUNT.addr, withdrawAmount); + bicoPaymaster.depositFor{ value: withdrawAmount }(DAPP_ACCOUNT.addr); uint256 danInitialBalance = DAN_ADDRESS.balance; - vm.startPrank(DAPP_ACCOUNT.addr); vm.expectEmit(true, true, true, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.GasWithdrawn(DAPP_ACCOUNT.addr, DAN_ADDRESS, withdrawAmount); bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), withdrawAmount); + uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); assertEq(dappPaymasterBalance, 0 ether); uint256 expectedDanBalance = danInitialBalance + withdrawAmount; assertEq(DAN_ADDRESS.balance, expectedDanBalance); - vm.stopPrank(); } - function testFuzz_Receive(uint256 ethAmount) external { + function testFuzz_Receive(uint256 ethAmount) external prankModifier(ALICE_ADDRESS) { vm.assume(ethAmount <= 1000 ether); vm.assume(ethAmount > 0 ether); uint256 initialPaymasterBalance = address(bicoPaymaster).balance; - vm.startPrank(ALICE_ADDRESS); + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.Received(ALICE_ADDRESS, ethAmount); (bool success,) = address(bicoPaymaster).call{ value: ethAmount }(""); - vm.stopPrank(); + assert(success); uint256 resultingPaymasterBalance = address(bicoPaymaster).balance; assertEq(resultingPaymasterBalance, initialPaymasterBalance + ethAmount); } - function testFuzz_WithdrawEth(uint256 ethAmount) external { + function testFuzz_WithdrawEth(uint256 ethAmount) external prankModifier(PAYMASTER_OWNER.addr) { vm.assume(ethAmount <= 1000 ether); vm.assume(ethAmount > 0 ether); - uint256 initialAliceBalance = ALICE_ADDRESS.balance; vm.deal(address(bicoPaymaster), ethAmount); - vm.startPrank(PAYMASTER_OWNER.addr); + uint256 initialAliceBalance = ALICE_ADDRESS.balance; + bicoPaymaster.withdrawEth(payable(ALICE_ADDRESS), ethAmount); - vm.stopPrank(); + assertEq(ALICE_ADDRESS.balance, initialAliceBalance + ethAmount); assertEq(address(bicoPaymaster).balance, 0 ether); } diff --git a/test/hardhat/Lock.ts b/test/hardhat/Lock.ts deleted file mode 100644 index 8e49635..0000000 --- a/test/hardhat/Lock.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { - time, - loadFixture, -} from "@nomicfoundation/hardhat-toolbox/network-helpers"; -import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; -import { expect } from "chai"; -import { ethers } from "hardhat"; - -describe("Lock", function () { - // We define a fixture to reuse the same setup in every test. - // We use loadFixture to run this setup once, snapshot that state, - // and reset Hardhat Network to that snapshot in every test. - async function deployOneYearLockFixture() { - const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60; - const ONE_GWEI = 1_000_000_000; - - const lockedAmount = ONE_GWEI; - const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS; - - // Contracts are deployed using the first signer/account by default - const [owner, otherAccount] = await ethers.getSigners(); - - const Lock = await ethers.getContractFactory("Lock"); - const lock = await Lock.deploy(unlockTime, { value: lockedAmount }); - - const SmartAccount = await ethers.getContractFactory("SmartAccount"); - const smartAccount = await SmartAccount.deploy(); - - return { lock, unlockTime, lockedAmount, owner, otherAccount }; - } - - describe("Deployment", function () { - it("Should set the right unlockTime", async function () { - const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture); - - expect(await lock.unlockTime()).to.equal(unlockTime); - }); - - it("Should set the right owner", async function () { - const { lock, owner } = await loadFixture(deployOneYearLockFixture); - - expect(await lock.owner()).to.equal(owner.address); - }); - - it("Should receive and store the funds to lock", async function () { - const { lock, lockedAmount } = await loadFixture( - deployOneYearLockFixture, - ); - - expect(await ethers.provider.getBalance(lock.target)).to.equal( - lockedAmount, - ); - }); - - it("Should fail if the unlockTime is not in the future", async function () { - // We don't use the fixture here because we want a different deployment - const latestTime = await time.latest(); - const Lock = await ethers.getContractFactory("Lock"); - await expect(Lock.deploy(latestTime, { value: 1 })).to.be.revertedWith( - "Wrong Unlock time", - ); - }); - }); - - describe("Withdrawals", function () { - describe("Validations", function () { - it("Should revert with the right error if called too soon", async function () { - const { lock } = await loadFixture(deployOneYearLockFixture); - - await expect(lock.withdraw()).to.be.revertedWith( - "You can't withdraw yet", - ); - }); - - it("Should revert with the right error if called from another account", async function () { - const { lock, unlockTime, otherAccount } = await loadFixture( - deployOneYearLockFixture, - ); - - // We can increase the time in Hardhat Network - await time.increaseTo(unlockTime); - - // We use lock.connect() to send a transaction from another account - await expect(lock.connect(otherAccount).withdraw()).to.be.revertedWith( - "You aren't the owner", - ); - }); - - it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () { - const { lock, unlockTime } = await loadFixture( - deployOneYearLockFixture, - ); - - // Transactions are sent using the first signer by default - await time.increaseTo(unlockTime); - - await expect(lock.withdraw()).not.to.be.reverted; - }); - }); - - describe("Events", function () { - it("Should emit an event on withdrawals", async function () { - const { lock, unlockTime, lockedAmount } = await loadFixture( - deployOneYearLockFixture, - ); - - await time.increaseTo(unlockTime); - - await expect(lock.withdraw()) - .to.emit(lock, "Withdrawal") - .withArgs(lockedAmount, anyValue); // We accept any value as `when` arg - }); - }); - - describe("Transfers", function () { - it("Should transfer the funds to the owner", async function () { - const { lock, unlockTime, lockedAmount, owner } = await loadFixture( - deployOneYearLockFixture, - ); - - await time.increaseTo(unlockTime); - - await expect(lock.withdraw()).to.changeEtherBalances( - [owner, lock], - [lockedAmount, -lockedAmount], - ); - }); - }); - }); -}); From b4276e4213152309150da979c1ba64671d935ee9 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Wed, 3 Jul 2024 17:41:19 +0400 Subject: [PATCH 21/21] fixed linting and changed visibility where applicable --- contracts/base/BasePaymaster.sol | 156 +++++----- .../references/SampleVerifyingPaymaster.sol | 56 ++-- .../SponsorshipPaymasterWithPremium.sol | 285 ++++++++++-------- 3 files changed, 276 insertions(+), 221 deletions(-) diff --git a/contracts/base/BasePaymaster.sol b/contracts/base/BasePaymaster.sol index 8b7e1e0..b3d487a 100644 --- a/contracts/base/BasePaymaster.sol +++ b/contracts/base/BasePaymaster.sol @@ -13,6 +13,7 @@ import "account-abstraction/contracts/core/UserOperationLib.sol"; * provides helper methods for staking. * Validates that the postOp is called only by the entryPoint. */ + abstract contract BasePaymaster is IPaymaster, SoladyOwnable { IEntryPoint public immutable entryPoint; @@ -25,10 +26,44 @@ abstract contract BasePaymaster is IPaymaster, SoladyOwnable { entryPoint = _entryPoint; } - //sanity check: make sure this EntryPoint was compiled against the same - // IEntryPoint of this paymaster - function _validateEntryPointInterface(IEntryPoint _entryPoint) internal virtual { - require(IERC165(address(_entryPoint)).supportsInterface(type(IEntryPoint).interfaceId), "IEntryPoint interface mismatch"); + /** + * Add stake for this paymaster. + * This method can also carry eth value to add to the current stake. + * @param unstakeDelaySec - The unstake delay for this paymaster. Can only be increased. + */ + function addStake(uint32 unstakeDelaySec) external payable onlyOwner { + entryPoint.addStake{ value: msg.value }(unstakeDelaySec); + } + + /** + * Unlock the stake, in order to withdraw it. + * The paymaster can't serve requests once unlocked, until it calls addStake again + */ + function unlockStake() external onlyOwner { + entryPoint.unlockStake(); + } + + /** + * Withdraw the entire paymaster's stake. + * stake must be unlocked first (and then wait for the unstakeDelay to be over) + * @param withdrawAddress - The address to send withdrawn value. + */ + function withdrawStake(address payable withdrawAddress) external onlyOwner { + entryPoint.withdrawStake(withdrawAddress); + } + + /// @inheritdoc IPaymaster + function postOp( + PostOpMode mode, + bytes calldata context, + uint256 actualGasCost, + uint256 actualUserOpFeePerGas + ) + external + override + { + _requireFromEntryPoint(); + _postOp(mode, context, actualGasCost, actualUserOpFeePerGas); } /// @inheritdoc IPaymaster @@ -36,11 +71,47 @@ abstract contract BasePaymaster is IPaymaster, SoladyOwnable { PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost - ) external override returns (bytes memory context, uint256 validationData) { + ) + external + override + returns (bytes memory context, uint256 validationData) + { _requireFromEntryPoint(); return _validatePaymasterUserOp(userOp, userOpHash, maxCost); } + /** + * Add a deposit for this paymaster, used for paying for transaction fees. + */ + function deposit() external payable virtual { + entryPoint.depositTo{ value: msg.value }(address(this)); + } + + /** + * Withdraw value from the deposit. + * @param withdrawAddress - Target to send to. + * @param amount - Amount to withdraw. + */ + function withdrawTo(address payable withdrawAddress, uint256 amount) external virtual onlyOwner { + entryPoint.withdrawTo(withdrawAddress, amount); + } + + /** + * Return current paymaster's deposit on the entryPoint. + */ + function getDeposit() public view returns (uint256) { + return entryPoint.balanceOf(address(this)); + } + + //sanity check: make sure this EntryPoint was compiled against the same + // IEntryPoint of this paymaster + function _validateEntryPointInterface(IEntryPoint _entryPoint) internal virtual { + require( + IERC165(address(_entryPoint)).supportsInterface(type(IEntryPoint).interfaceId), + "IEntryPoint interface mismatch" + ); + } + /** * Validate a user operation. * @param userOp - The user operation. @@ -51,18 +122,10 @@ abstract contract BasePaymaster is IPaymaster, SoladyOwnable { PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost - ) internal virtual returns (bytes memory context, uint256 validationData); - - /// @inheritdoc IPaymaster - function postOp( - PostOpMode mode, - bytes calldata context, - uint256 actualGasCost, - uint256 actualUserOpFeePerGas - ) external override { - _requireFromEntryPoint(); - _postOp(mode, context, actualGasCost, actualUserOpFeePerGas); - } + ) + internal + virtual + returns (bytes memory context, uint256 validationData); /** * Post-operation handler. @@ -84,68 +147,19 @@ abstract contract BasePaymaster is IPaymaster, SoladyOwnable { bytes calldata context, uint256 actualGasCost, uint256 actualUserOpFeePerGas - ) internal virtual { + ) + internal + virtual + { (mode, context, actualGasCost, actualUserOpFeePerGas); // unused params // subclass must override this method if validatePaymasterUserOp returns a context revert("must override"); } - /** - * Add a deposit for this paymaster, used for paying for transaction fees. - */ - function deposit() public virtual payable { - entryPoint.depositTo{value: msg.value}(address(this)); - } - - /** - * Withdraw value from the deposit. - * @param withdrawAddress - Target to send to. - * @param amount - Amount to withdraw. - */ - function withdrawTo( - address payable withdrawAddress, - uint256 amount - ) public virtual onlyOwner { - entryPoint.withdrawTo(withdrawAddress, amount); - } - - /** - * Add stake for this paymaster. - * This method can also carry eth value to add to the current stake. - * @param unstakeDelaySec - The unstake delay for this paymaster. Can only be increased. - */ - function addStake(uint32 unstakeDelaySec) external payable onlyOwner { - entryPoint.addStake{value: msg.value}(unstakeDelaySec); - } - - /** - * Return current paymaster's deposit on the entryPoint. - */ - function getDeposit() public view returns (uint256) { - return entryPoint.balanceOf(address(this)); - } - - /** - * Unlock the stake, in order to withdraw it. - * The paymaster can't serve requests once unlocked, until it calls addStake again - */ - function unlockStake() external onlyOwner { - entryPoint.unlockStake(); - } - - /** - * Withdraw the entire paymaster's stake. - * stake must be unlocked first (and then wait for the unstakeDelay to be over) - * @param withdrawAddress - The address to send withdrawn value. - */ - function withdrawStake(address payable withdrawAddress) external onlyOwner { - entryPoint.withdrawStake(withdrawAddress); - } - /** * Validate the call is made from a valid entrypoint */ function _requireFromEntryPoint() internal virtual { require(msg.sender == address(entryPoint), "Sender not EntryPoint"); } -} \ No newline at end of file +} diff --git a/contracts/references/SampleVerifyingPaymaster.sol b/contracts/references/SampleVerifyingPaymaster.sol index 46f12bf..1522c6e 100644 --- a/contracts/references/SampleVerifyingPaymaster.sol +++ b/contracts/references/SampleVerifyingPaymaster.sol @@ -20,7 +20,6 @@ import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; * - the account checks a signature to prove identity and account ownership. */ contract VerifyingPaymaster is BasePaymaster { - using UserOperationLib for PackedUserOperation; address public immutable verifyingSigner; @@ -40,19 +39,25 @@ contract VerifyingPaymaster is BasePaymaster { * note that this signature covers all fields of the UserOperation, except the "paymasterAndData", * which will carry the signature itself. */ - function getHash(PackedUserOperation calldata userOp, uint48 validUntil, uint48 validAfter) - public view returns (bytes32) { + function getHash( + PackedUserOperation calldata userOp, + uint48 validUntil, + uint48 validAfter + ) + public + view + returns (bytes32) + { //can't use userOp.hash(), since it contains also the paymasterAndData itself. address sender = userOp.getSender(); - return - keccak256( + return keccak256( abi.encode( sender, userOp.nonce, keccak256(userOp.initCode), keccak256(userOp.callData), userOp.accountGasLimits, - uint256(bytes32(userOp.paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET : PAYMASTER_DATA_OFFSET])), + uint256(bytes32(userOp.paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET:PAYMASTER_DATA_OFFSET])), userOp.preVerificationGas, userOp.gasFees, block.chainid, @@ -63,6 +68,15 @@ contract VerifyingPaymaster is BasePaymaster { ); } + function parsePaymasterAndData(bytes calldata paymasterAndData) + public + pure + returns (uint48 validUntil, uint48 validAfter, bytes calldata signature) + { + (validUntil, validAfter) = abi.decode(paymasterAndData[VALID_TIMESTAMP_OFFSET:], (uint48, uint48)); + signature = paymasterAndData[SIGNATURE_OFFSET:]; + } + /** * verify our external signer signed this request. * the "paymasterAndData" is expected to be the paymaster and a signature over the entire request params @@ -70,14 +84,27 @@ contract VerifyingPaymaster is BasePaymaster { * paymasterAndData[20:84] : abi.encode(validUntil, validAfter) * paymasterAndData[84:] : signature */ - function _validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32 /*userOpHash*/, uint256 requiredPreFund) - internal view override returns (bytes memory context, uint256 validationData) { + function _validatePaymasterUserOp( + PackedUserOperation calldata userOp, + bytes32, /*userOpHash*/ + uint256 requiredPreFund + ) + internal + view + override + returns (bytes memory context, uint256 validationData) + { (requiredPreFund); - (uint48 validUntil, uint48 validAfter, bytes calldata signature) = parsePaymasterAndData(userOp.paymasterAndData); + (uint48 validUntil, uint48 validAfter, bytes calldata signature) = + parsePaymasterAndData(userOp.paymasterAndData); //ECDSA library supports both 64 and 65-byte long signatures. - // we only "require" it here so that the revert reason on invalid signature will be of "VerifyingPaymaster", and not "ECDSA" - require(signature.length == 64 || signature.length == 65, "VerifyingPaymaster: invalid signature length in paymasterAndData"); + // we only "require" it here so that the revert reason on invalid signature will be of "VerifyingPaymaster", and + // not "ECDSA" + require( + signature.length == 64 || signature.length == 65, + "VerifyingPaymaster: invalid signature length in paymasterAndData" + ); bytes32 hash = MessageHashUtils.toEthSignedMessageHash(getHash(userOp, validUntil, validAfter)); //don't revert on signature failure: return SIG_VALIDATION_FAILED @@ -89,9 +116,4 @@ contract VerifyingPaymaster is BasePaymaster { // by the external service prior to signing it. return ("", _packValidationData(false, validUntil, validAfter)); } - - function parsePaymasterAndData(bytes calldata paymasterAndData) public pure returns (uint48 validUntil, uint48 validAfter, bytes calldata signature) { - (validUntil, validAfter) = abi.decode(paymasterAndData[VALID_TIMESTAMP_OFFSET :], (uint48, uint48)); - signature = paymasterAndData[SIGNATURE_OFFSET :]; - } -} \ No newline at end of file +} diff --git a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol b/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol index 47f7c65..2e3abf4 100644 --- a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol +++ b/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol @@ -19,16 +19,21 @@ import { IBiconomySponsorshipPaymaster } from "../interfaces/IBiconomySponsorshi * @author livingrockrises * @notice Based on Infinitism 'VerifyingPaymaster' contract * @dev This contract is used to sponsor the transaction fees of the user operations - * Uses a verifying signer to provide the signature if predetermined conditions are met - * regarding the user operation calldata. Also this paymaster is Singleton in nature which + * Uses a verifying signer to provide the signature if predetermined conditions are met + * regarding the user operation calldata. Also this paymaster is Singleton in nature which * means multiple Dapps/Wallet clients willing to sponsor the transactions can share this paymaster. - * Maintains it's own accounting of the gas balance for each Dapp/Wallet client + * Maintains it's own accounting of the gas balance for each Dapp/Wallet client * and Manages it's own deposit on the EntryPoint. */ // @Todo: Add more methods in interface -contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard, BiconomySponsorshipPaymasterErrors, IBiconomySponsorshipPaymaster { +contract BiconomySponsorshipPaymaster is + BasePaymaster, + ReentrancyGuard, + BiconomySponsorshipPaymasterErrors, + IBiconomySponsorshipPaymaster +{ using UserOperationLib for PackedUserOperation; using SignatureCheckerLib for address; @@ -42,22 +47,34 @@ contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard, Biconom mapping(address => uint256) public paymasterIdBalances; - constructor(address _owner, IEntryPoint _entryPoint, address _verifyingSigner, address _feeCollector) BasePaymaster(_owner, _entryPoint) { + constructor( + address _owner, + IEntryPoint _entryPoint, + address _verifyingSigner, + address _feeCollector + ) + BasePaymaster(_owner, _entryPoint) + { // TODO // Check for zero address verifyingSigner = _verifyingSigner; feeCollector = _feeCollector; } + receive() external payable { + emit Received(msg.sender, msg.value); + } + /** - * @dev Add a deposit for this paymaster and given paymasterId (Dapp Depositor address), used for paying for transaction fees + * @dev Add a deposit for this paymaster and given paymasterId (Dapp Depositor address), used for paying for + * transaction fees * @param paymasterId dapp identifier for which deposit is being made */ function depositFor(address paymasterId) external payable nonReentrant { if (paymasterId == address(0)) revert PaymasterIdCanNotBeZero(); if (msg.value == 0) revert DepositCanNotBeZero(); paymasterIdBalances[paymasterId] += msg.value; - entryPoint.depositTo{value: msg.value}(address(this)); + entryPoint.depositTo{ value: msg.value }(address(this)); emit GasDeposited(paymasterId, msg.value); } @@ -68,14 +85,15 @@ contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard, Biconom * @notice If _newVerifyingSigner is set to zero address, it will revert with an error. * After setting the new signer address, it will emit an event VerifyingSignerChanged. */ - function setSigner( - address _newVerifyingSigner - ) external payable onlyOwner { + function setSigner(address _newVerifyingSigner) external payable onlyOwner { uint256 size; - assembly { size := extcodesize(_newVerifyingSigner) } - if(size > 0) revert VerifyingSignerCanNotBeContract(); - if (_newVerifyingSigner == address(0)) + assembly { + size := extcodesize(_newVerifyingSigner) + } + if (size > 0) revert VerifyingSignerCanNotBeContract(); + if (_newVerifyingSigner == address(0)) { revert VerifyingSignerCanNotBeZero(); + } address oldSigner = verifyingSigner; assembly { sstore(verifyingSigner.slot, _newVerifyingSigner) @@ -90,9 +108,7 @@ contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard, Biconom * @notice If _newFeeCollector is set to zero address, it will revert with an error. * After setting the new fee collector address, it will emit an event FeeCollectorChanged. */ - function setFeeCollector( - address _newFeeCollector - ) external payable onlyOwner { + function setFeeCollector(address _newFeeCollector) external payable onlyOwner { if (_newFeeCollector == address(0)) revert FeeCollectorCanNotBeZero(); address oldFeeCollector = feeCollector; assembly { @@ -106,41 +122,37 @@ contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard, Biconom * @param value The new value to be set as the unaccountedEPGasOverhead. * @notice only to be called by the owner of the contract. */ - function setPostopCost( - uint48 value - ) external payable onlyOwner { - require(value <= 200000, "Gas overhead too high"); + function setPostopCost(uint48 value) external payable onlyOwner { + require(value <= 200_000, "Gas overhead too high"); uint256 oldValue = postopCost; postopCost = value; emit PostopCostChanged(oldValue, value); } /** - * @dev get the current deposit for paymasterId (Dapp Depositor address) - * @param paymasterId dapp identifier + * @dev Override the default implementation. */ - function getBalance( - address paymasterId - ) external view returns (uint256 balance) { - balance = paymasterIdBalances[paymasterId]; + function deposit() external payable virtual override { + revert("Use depositFor() instead"); } /** - @dev Override the default implementation. + * @dev pull tokens out of paymaster in case they were sent to the paymaster at any point. + * @param token the token deposit to withdraw + * @param target address to send to + * @param amount amount to withdraw */ - function deposit() public payable virtual override { - revert("Use depositFor() instead"); + function withdrawERC20(IERC20 token, address target, uint256 amount) external payable onlyOwner nonReentrant { + _withdrawERC20(token, target, amount); } /** - * @dev Withdraws the specified amount of gas tokens from the paymaster's balance and transfers them to the specified address. + * @dev Withdraws the specified amount of gas tokens from the paymaster's balance and transfers them to the + * specified address. * @param withdrawAddress The address to which the gas tokens should be transferred. * @param amount The amount of gas tokens to withdraw. */ - function withdrawTo( - address payable withdrawAddress, - uint256 amount - ) public override nonReentrant { + function withdrawTo(address payable withdrawAddress, uint256 amount) external override nonReentrant { if (withdrawAddress == address(0)) revert CanNotWithdrawToZeroAddress(); uint256 currentBalance = paymasterIdBalances[msg.sender]; require(amount <= currentBalance, "Sponsorship Paymaster: Insufficient funds to withdraw from gas tank"); @@ -149,6 +161,19 @@ contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard, Biconom emit GasWithdrawn(msg.sender, withdrawAddress, amount); } + function withdrawEth(address payable recipient, uint256 amount) external onlyOwner { + (bool success,) = recipient.call{ value: amount }(""); + require(success, "withdraw failed"); + } + + /** + * @dev get the current deposit for paymasterId (Dapp Depositor address) + * @param paymasterId dapp identifier + */ + function getBalance(address paymasterId) external view returns (uint256 balance) { + balance = paymasterIdBalances[paymasterId]; + } + /** * return the hash we're going to sign off-chain (and validate on-chain) * this method is called by the off-chain service, to sign the request. @@ -156,19 +181,27 @@ contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard, Biconom * note that this signature covers all fields of the UserOperation, except the "paymasterAndData", * which will carry the signature itself. */ - function getHash(PackedUserOperation calldata userOp, address paymasterId, uint48 validUntil, uint48 validAfter, uint32 priceMarkup) - public view returns (bytes32) { + function getHash( + PackedUserOperation calldata userOp, + address paymasterId, + uint48 validUntil, + uint48 validAfter, + uint32 priceMarkup + ) + public + view + returns (bytes32) + { //can't use userOp.hash(), since it contains also the paymasterAndData itself. address sender = userOp.getSender(); - return - keccak256( + return keccak256( abi.encode( sender, userOp.nonce, keccak256(userOp.initCode), keccak256(userOp.callData), userOp.accountGasLimits, - uint256(bytes32(userOp.paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET : PAYMASTER_DATA_OFFSET])), + uint256(bytes32(userOp.paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET:PAYMASTER_DATA_OFFSET])), userOp.preVerificationGas, userOp.gasFees, block.chainid, @@ -181,6 +214,61 @@ contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard, Biconom ); } + function parsePaymasterAndData(bytes calldata paymasterAndData) + public + pure + returns ( + address paymasterId, + uint48 validUntil, + uint48 validAfter, + uint32 priceMarkup, + bytes calldata signature + ) + { + paymasterId = address(bytes20(paymasterAndData[VALID_PND_OFFSET:VALID_PND_OFFSET + 20])); + validUntil = uint48(bytes6(paymasterAndData[VALID_PND_OFFSET + 20:VALID_PND_OFFSET + 26])); + validAfter = uint48(bytes6(paymasterAndData[VALID_PND_OFFSET + 26:VALID_PND_OFFSET + 32])); + priceMarkup = uint32(bytes4(paymasterAndData[VALID_PND_OFFSET + 32:VALID_PND_OFFSET + 36])); + signature = paymasterAndData[VALID_PND_OFFSET + 36:]; + } + + /// @notice Performs post-operation tasks, such as deducting the sponsored gas cost from the paymasterId's balance + /// @dev This function is called after a user operation has been executed or reverted. + /// @param context The context containing the token amount and user sender address. + /// @param actualGasCost The actual gas cost of the transaction. + /// @param actualUserOpFeePerGas - the gas price this UserOp pays. This value is based on the UserOp's maxFeePerGas + // and maxPriorityFee (and basefee) + // It is not the same as tx.gasprice, which is what the bundler pays. + function _postOp( + PostOpMode, + bytes calldata context, + uint256 actualGasCost, + uint256 actualUserOpFeePerGas + ) + internal + override + { + unchecked { + (address paymasterId, uint32 dynamicMarkup, bytes32 userOpHash) = + abi.decode(context, (address, uint32, bytes32)); + + uint256 balToDeduct = actualGasCost + postopCost * actualUserOpFeePerGas; + + uint256 costIncludingPremium = (balToDeduct * dynamicMarkup) / PRICE_DENOMINATOR; + + // deduct with premium + paymasterIdBalances[paymasterId] -= costIncludingPremium; + + uint256 actualPremium = costIncludingPremium - balToDeduct; + // "collect" premium + paymasterIdBalances[feeCollector] += actualPremium; + + emit GasBalanceDeducted(paymasterId, costIncludingPremium, userOpHash); + // Review if we should emit balToDeduct as well + emit PremiumCollected(paymasterId, actualPremium); + } + } + /** * verify our external signer signed this request. * the "paymasterAndData" is expected to be the paymaster and a signature over the entire request params @@ -191,18 +279,25 @@ contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard, Biconom * paymasterAndData[84:88] : priceMarkup * paymasterAndData[88:] : signature */ - function _validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 requiredPreFund) - internal view override returns (bytes memory context, uint256 validationData) { - ( - address paymasterId, - uint48 validUntil, - uint48 validAfter, - uint32 priceMarkup, - bytes calldata signature - ) = parsePaymasterAndData(userOp.paymasterAndData); + function _validatePaymasterUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 requiredPreFund + ) + internal + view + override + returns (bytes memory context, uint256 validationData) + { + (address paymasterId, uint48 validUntil, uint48 validAfter, uint32 priceMarkup, bytes calldata signature) = + parsePaymasterAndData(userOp.paymasterAndData); //ECDSA library supports both 64 and 65-byte long signatures. - // we only "require" it here so that the revert reason on invalid signature will be of "VerifyingPaymaster", and not "ECDSA" - require(signature.length == 64 || signature.length == 65, "VerifyingPaymaster: invalid signature length in paymasterAndData"); + // we only "require" it here so that the revert reason on invalid signature will be of "VerifyingPaymaster", and + // not "ECDSA" + require( + signature.length == 64 || signature.length == 65, + "VerifyingPaymaster: invalid signature length in paymasterAndData" + ); bool validSig = verifyingSigner.isValidSignatureNow( ECDSA_solady.toEthSignedMessageHash(getHash(userOp, paymasterId, validUntil, validAfter, priceMarkup)), @@ -220,99 +315,23 @@ contract BiconomySponsorshipPaymaster is BasePaymaster, ReentrancyGuard, Biconom // Send 1e6 for No markup // Send between 0 and 1e6 for discount - uint256 effectiveCost = ((requiredPreFund + (postopCost * maxFeePerGas)) * priceMarkup) / - PRICE_DENOMINATOR; - - require(effectiveCost <= paymasterIdBalances[paymasterId], "Sponsorship Paymaster: paymasterId does not have enough deposit"); + uint256 effectiveCost = ((requiredPreFund + (postopCost * maxFeePerGas)) * priceMarkup) / PRICE_DENOMINATOR; - context = abi.encode( - paymasterId, - priceMarkup, - userOpHash + require( + effectiveCost <= paymasterIdBalances[paymasterId], + "Sponsorship Paymaster: paymasterId does not have enough deposit" ); + context = abi.encode(paymasterId, priceMarkup, userOpHash); + //no need for other on-chain validation: entire UserOp should have been checked // by the external service prior to signing it. return (context, _packValidationData(false, validUntil, validAfter)); } - /// @notice Performs post-operation tasks, such as deducting the sponsored gas cost from the paymasterId's balance - /// @dev This function is called after a user operation has been executed or reverted. - /// @param context The context containing the token amount and user sender address. - /// @param actualGasCost The actual gas cost of the transaction. - /// @param actualUserOpFeePerGas - the gas price this UserOp pays. This value is based on the UserOp's maxFeePerGas - // and maxPriorityFee (and basefee) - // It is not the same as tx.gasprice, which is what the bundler pays. - function _postOp(PostOpMode, bytes calldata context, uint256 actualGasCost, uint256 actualUserOpFeePerGas) internal override { - unchecked { - ( - address paymasterId, - uint32 dynamicMarkup, - bytes32 userOpHash - ) = abi.decode(context, (address, uint32, bytes32)); - - uint256 balToDeduct = actualGasCost + - postopCost * - actualUserOpFeePerGas; - - uint256 costIncludingPremium = (balToDeduct * dynamicMarkup) / - PRICE_DENOMINATOR; - - // deduct with premium - paymasterIdBalances[paymasterId] -= costIncludingPremium; - - uint256 actualPremium = costIncludingPremium - balToDeduct; - // "collect" premium - paymasterIdBalances[feeCollector] += actualPremium; - - emit GasBalanceDeducted(paymasterId, costIncludingPremium, userOpHash); - // Review if we should emit balToDeduct as well - emit PremiumCollected(paymasterId, actualPremium); - } - } - - function parsePaymasterAndData( - bytes calldata paymasterAndData - ) - public - pure - returns ( - address paymasterId, - uint48 validUntil, - uint48 validAfter, - uint32 priceMarkup, - bytes calldata signature - ) - { - paymasterId = address(bytes20(paymasterAndData[VALID_PND_OFFSET:VALID_PND_OFFSET+20])); - validUntil = uint48(bytes6(paymasterAndData[VALID_PND_OFFSET+20:VALID_PND_OFFSET+26])); - validAfter = uint48(bytes6(paymasterAndData[VALID_PND_OFFSET+26:VALID_PND_OFFSET+32])); - priceMarkup = uint32(bytes4(paymasterAndData[VALID_PND_OFFSET+32:VALID_PND_OFFSET+36])); - signature = paymasterAndData[VALID_PND_OFFSET+36:]; - } - - receive() external payable { - emit Received(msg.sender, msg.value); - } - - function withdrawEth(address payable recipient, uint256 amount) external onlyOwner { - (bool success,) = recipient.call{value: amount}(""); - require(success, "withdraw failed"); - } - - /** - * @dev pull tokens out of paymaster in case they were sent to the paymaster at any point. - * @param token the token deposit to withdraw - * @param target address to send to - * @param amount amount to withdraw - */ - function withdrawERC20(IERC20 token, address target, uint256 amount) public payable onlyOwner nonReentrant { - _withdrawERC20(token, target, amount); - } - function _withdrawERC20(IERC20 token, address target, uint256 amount) private { if (target == address(0)) revert CanNotWithdrawToZeroAddress(); SafeTransferLib.safeTransfer(address(token), target, amount); emit TokensWithdrawn(address(token), target, amount, msg.sender); } -} \ No newline at end of file +}