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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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 +} From 9e72f5c698d256180f6bff1e17e3fef9feb2d07f Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Wed, 3 Jul 2024 17:52:26 +0400 Subject: [PATCH 22/69] yarn lint runs successfully --- .../IBiconomySponsorshipPaymaster.sol | 22 +- contracts/mocks/Imports.sol | 1 - contracts/mocks/MockValidator.sol | 2 +- contracts/utils/SoladyOwnable.sol | 4 +- .../biconomy-sponsorship-paymaster-specs.ts | 342 +++++---- test/hardhat/utils/deployment.ts | 191 ++--- test/hardhat/utils/testUtils.ts | 266 ++++--- test/hardhat/utils/types.ts | 58 +- test/hardhat/utils/userOpHelpers.ts | 681 +++++++++++------- 9 files changed, 890 insertions(+), 677 deletions(-) diff --git a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol index ed4da78..5f47d1a 100644 --- a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol +++ b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol @@ -2,18 +2,16 @@ pragma solidity ^0.8.26; interface IBiconomySponsorshipPaymaster { - event PostopCostChanged(uint256 indexed _oldValue, uint256 indexed _newValue); - event FixedPriceMarkupChanged(uint32 indexed _oldValue, uint32 indexed _newValue); + event PostopCostChanged(uint256 indexed oldValue, uint256 indexed newValue); + event FixedPriceMarkupChanged(uint32 indexed oldValue, uint32 indexed newValue); - event VerifyingSignerChanged(address indexed _oldSigner, address indexed _newSigner, address indexed _actor); + event VerifyingSignerChanged(address indexed oldSigner, address indexed newSigner, address indexed actor); - event FeeCollectorChanged( - address indexed _oldFeeCollector, address indexed _newFeeCollector, address indexed _actor - ); - event GasDeposited(address indexed _paymasterId, uint256 indexed _value); - event GasWithdrawn(address indexed _paymasterId, address indexed _to, uint256 indexed _value); - event GasBalanceDeducted(address indexed _paymasterId, uint256 indexed _charge, bytes32 indexed userOpHash); - event PremiumCollected(address indexed _paymasterId, uint256 indexed _premium); + event FeeCollectorChanged(address indexed oldFeeCollector, address indexed newFeeCollector, address indexed actor); + event GasDeposited(address indexed paymasterId, uint256 indexed value); + event GasWithdrawn(address indexed paymasterId, address indexed to, uint256 indexed value); + event GasBalanceDeducted(address indexed paymasterId, uint256 indexed charge, bytes32 indexed userOpHash); + event PremiumCollected(address indexed paymasterId, uint256 indexed premium); event Received(address indexed sender, uint256 value); - event TokensWithdrawn(address indexed _token, address indexed _to, uint256 indexed _amount, address actor); -} \ No newline at end of file + event TokensWithdrawn(address indexed token, address indexed to, uint256 indexed amount, address actor); +} diff --git a/contracts/mocks/Imports.sol b/contracts/mocks/Imports.sol index 7b0976a..131ae80 100644 --- a/contracts/mocks/Imports.sol +++ b/contracts/mocks/Imports.sol @@ -8,4 +8,3 @@ 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 index 2c3d359..7682958 100644 --- a/contracts/mocks/MockValidator.sol +++ b/contracts/mocks/MockValidator.sol @@ -1,3 +1,3 @@ pragma solidity ^0.8.26; -import "@biconomy-devx/erc7579-msa/test/foundry/mocks/MockValidator.sol"; \ No newline at end of file +import "@biconomy-devx/erc7579-msa/test/foundry/mocks/MockValidator.sol"; diff --git a/contracts/utils/SoladyOwnable.sol b/contracts/utils/SoladyOwnable.sol index 0cd57c4..8b680d3 100644 --- a/contracts/utils/SoladyOwnable.sol +++ b/contracts/utils/SoladyOwnable.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import {Ownable} from "solady/src/auth/Ownable.sol"; +import { Ownable } from "solady/src/auth/Ownable.sol"; contract SoladyOwnable is Ownable { constructor(address _owner) Ownable() { _initializeOwner(_owner); } -} \ No newline at end of file +} diff --git a/test/hardhat/biconomy-sponsorship-paymaster-specs.ts b/test/hardhat/biconomy-sponsorship-paymaster-specs.ts index dbfabb1..c3a48d4 100644 --- a/test/hardhat/biconomy-sponsorship-paymaster-specs.ts +++ b/test/hardhat/biconomy-sponsorship-paymaster-specs.ts @@ -1,23 +1,35 @@ 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 +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 { + DefaultsForUserOp, + fillAndSign, + fillSignAndPack, + packUserOp, + simulateValidation, +} from "./utils/userOpHelpers"; import { parseValidationData } from "./utils/testUtils"; - export const AddressZero = ethers.ZeroAddress; const MOCK_VALID_UNTIL = "0x00000000deadbeef"; @@ -25,148 +37,174 @@ const MOCK_VALID_AFTER = "0x0000000000001234"; const MARKUP = 1100000; export const ENTRY_POINT_V7 = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"; -const coder = AbiCoder.defaultAbiCoder() +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; + 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") }); - - await deployer.sendTransaction({to: expected, value: parseEther("1"), data: '0x'}); - }); - - describe("Deployed Account : #validatePaymasterUserOp and #sendEmptySponsoredTx", () => { - 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) - ]), - 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)) - 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 - ]), - paymasterPostOpGasLimit: 40_000, - }, walletOwner, entryPoint, 'getNonce', nonceKey) - // 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({ - aggregator: AddressZero, - validAfter: parseInt(MOCK_VALID_AFTER), - validUntil: parseInt(MOCK_VALID_UNTIL) - }) - - await entryPoint.handleOps([userOp], await deployer.getAddress()) - }); + 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") }); + + await deployer.sendTransaction({ + to: expected, + value: parseEther("1"), + data: "0x", + }); + }); + + describe("Deployed Account : #validatePaymasterUserOp and #sendEmptySponsoredTx", () => { + 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), + ]), + 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)); + 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, + ]), + paymasterPostOpGasLimit: 40_000, + }, + walletOwner, + entryPoint, + "getNonce", + nonceKey, + ); + // 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({ + aggregator: AddressZero, + validAfter: parseInt(MOCK_VALID_AFTER), + validUntil: parseInt(MOCK_VALID_UNTIL), + }); + + await entryPoint.handleOps([userOp], await deployer.getAddress()); + }); + }); +}); diff --git a/test/hardhat/utils/deployment.ts b/test/hardhat/utils/deployment.ts index 282831d..18ebef0 100644 --- a/test/hardhat/utils/deployment.ts +++ b/test/hardhat/utils/deployment.ts @@ -1,6 +1,12 @@ import { BytesLike, HDNodeWallet, Signer } from "ethers"; import { deployments, ethers } from "hardhat"; -import { AccountFactory, BiconomySponsorshipPaymaster, EntryPoint, MockValidator, SmartAccount } from "../../../typechain-types"; +import { + AccountFactory, + BiconomySponsorshipPaymaster, + EntryPoint, + MockValidator, + SmartAccount, +} from "../../../typechain-types"; import { TASK_DEPLOY } from "hardhat-deploy"; import { DeployResult } from "hardhat-deploy/dist/types"; @@ -14,39 +20,39 @@ export const ENTRY_POINT_V7 = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"; * @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; + 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; +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; } /** @@ -54,18 +60,18 @@ export async function getDeployedEntrypoint() : Promise { * @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; + 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; } /** @@ -73,27 +79,27 @@ export async function getDeployedMSAImplementation(): Promise { * @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; + 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; } /** @@ -101,41 +107,50 @@ export async function getDeployedAccountFactory( * @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; + 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", { +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( + }, + ); + + return BiconomySponsorshipPaymaster.attach( deterministicSponsorshipPaymaster.address, - ) as BiconomySponsorshipPaymaster; + ) as BiconomySponsorshipPaymaster; } - diff --git a/test/hardhat/utils/testUtils.ts b/test/hardhat/utils/testUtils.ts index 06c4218..abe1776 100644 --- a/test/hardhat/utils/testUtils.ts +++ b/test/hardhat/utils/testUtils.ts @@ -1,6 +1,15 @@ -import { AbiCoder, AddressLike, BigNumberish, Contract, Interface, dataSlice, parseEther, toBeHex } from 'ethers'; -import { ethers } from 'hardhat' -import { EntryPoint__factory, IERC20 } from '../../../typechain-types'; +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 @@ -13,171 +22,189 @@ 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 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' +export const tostr = (x: any): string => (x != null ? x.toString() : "null"); -const coder = AbiCoder.defaultAbiCoder() +const coder = AbiCoder.defaultAbiCoder(); export interface ValidationData { - aggregator: string - validAfter: number - validUntil: number + 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' -} + // 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)", - ]; - + "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]); + 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, - ]); + 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]*/, '') +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)') + 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 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 + 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) + const data = found[1]; + message = + decodeRevertReason(data) ?? e.message + " - " + data.slice(0, 100); } else { - message = e.message + message = e.message; } - const err = new Error(message) - err.stack = 'Error: ' + message + '\n' + stack - throw err - } + 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 + "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) + 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) + 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} + ')` + return `Error(${err})`; + } else if (methodSig === "0x4e487b71") { + const [code] = coder.decode(["uint256"], dataParams); + return `Panic(${panicCodes[code] ?? code} + ')`; } try { - const err = decodeRevertReasonContracts.parseError(data) + 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 + case "bytes": + return decodeRevertReason(arg); + case "string": + return `"${arg as string}"`; + default: + return arg; } - }) - return `${err!.name}(${args.join(',')})` + }); + return `${err!.name}(${args.join(",")})`; } catch (e) { // throw new Error('unsupported errorSig ' + data) if (!nullIfNoMatch) { - return data + return data; } - return null + return null; } } -export function tonumber (x: any): number { +export function tonumber(x: any): number { try { - return parseFloat(x.toString()) + return parseFloat(x.toString()); } catch (e: any) { - console.log('=== failed to parseFloat:', x, (e).message) - return NaN + 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 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 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 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 +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) @@ -202,28 +229,29 @@ export async function getInitCode( 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 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) +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)) + const aggregator = dataSlice(data, 32 - 20); + let validUntil = parseInt(dataSlice(data, 32 - 26, 32 - 20)); if (validUntil === 0) { - validUntil = maxUint48 + validUntil = maxUint48; } - const validAfter = parseInt(dataSlice(data, 0, 6)) + const validAfter = parseInt(dataSlice(data, 0, 6)); return { aggregator, validAfter, - validUntil - } + validUntil, + }; } - - diff --git a/test/hardhat/utils/types.ts b/test/hardhat/utils/types.ts index 791fc10..7dd52fa 100644 --- a/test/hardhat/utils/types.ts +++ b/test/hardhat/utils/types.ts @@ -1,34 +1,30 @@ -import { - AddressLike, - BigNumberish, - BytesLike, - } from "ethers"; +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; - } + 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 +export interface PackedUserOperation { + sender: AddressLike; // Or string + nonce: BigNumberish; + initCode: BytesLike; + callData: BytesLike; + accountGasLimits: BytesLike; + preVerificationGas: BigNumberish; + gasFees: BytesLike; + paymasterAndData: BytesLike; + signature: BytesLike; +} diff --git a/test/hardhat/utils/userOpHelpers.ts b/test/hardhat/utils/userOpHelpers.ts index 8dc582c..50fccd5 100644 --- a/test/hardhat/utils/userOpHelpers.ts +++ b/test/hardhat/utils/userOpHelpers.ts @@ -1,157 +1,230 @@ import { ethers } from "hardhat"; -import { EntryPoint, EntryPointSimulations__factory, IEntryPointSimulations } from "../../../typechain-types"; +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 { 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' +import EntryPointSimulationsJson from "../../../artifacts/account-abstraction/contracts/core/EntryPointSimulations.sol/EntryPointSimulations.json"; const AddressZero = ethers.ZeroAddress; -const coder = AbiCoder.defaultAbiCoder() +const coder = AbiCoder.defaultAbiCoder(); -export function packUserOp (userOp: UserOperation): PackedUserOperation { +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 { - 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' - } + 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]) - } +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, - ]); + 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) - ]) +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 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 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' -} + 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) +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 - } + 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] - } +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 + } + const filled = { ...defaults, ...partial }; + return filled; } // helper to fill structure: @@ -166,112 +239,151 @@ export function fillUserOpDefaults (op: Partial, defaults = Defau // 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) +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 (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.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)) + if (op1.paymasterPostOpGasLimit == null) { + op1.paymasterPostOpGasLimit = DefaultsForUserOp.paymasterPostOpGasLimit; } - return op2; + } + 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 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 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) + +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); } /** @@ -281,67 +393,94 @@ export async function fillAndSign (op: Partial, signer: Signer | * @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 +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 +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 199d312b29f4000cb72152e60dee6821ea923b07 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Thu, 4 Jul 2024 11:42:31 +0400 Subject: [PATCH 23/69] setPostOpCost tests --- contracts/test/Foo.sol | 18 -------- contracts/test/Lock.sol | 46 ------------------- scripts/foundry/Deploy.s.sol | 17 ------- ...tSponsorshipPaymasterWithPremiumTest.t.sol | 25 ++++++++++ ..._TestSponsorshipPaymasterWithPremium.t.sol | 14 ++++++ 5 files changed, 39 insertions(+), 81 deletions(-) delete mode 100644 contracts/test/Foo.sol delete mode 100644 contracts/test/Lock.sol delete mode 100644 scripts/foundry/Deploy.s.sol diff --git a/contracts/test/Foo.sol b/contracts/test/Foo.sol deleted file mode 100644 index 8302d06..0000000 --- a/contracts/test/Foo.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.26; - -/** - * @title Foo - * @dev A simple contract demonstrating a pure function in Solidity. - */ -contract Foo { - /** - * @notice Returns the input value unchanged. - * @dev A pure function that does not alter or interact with contract state. - * @param value The uint256 value to be returned. - * @return uint256 The same value that was input. - */ - function id(uint256 value) external pure returns (uint256) { - return value; - } -} diff --git a/contracts/test/Lock.sol b/contracts/test/Lock.sol deleted file mode 100644 index 522be01..0000000 --- a/contracts/test/Lock.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.26; - -/** - * @title Lock - * @dev Implements a time-locked wallet that only allows withdrawals after a certain date. - */ -contract Lock { - uint256 public unlockTime; - address payable public owner; - - /** - * @dev Emitted when funds are withdrawn from the contract. - * @param amount The amount of Ether withdrawn. - * @param when The timestamp of the withdrawal. - */ - event Withdrawal(uint256 amount, uint256 when); - - /** - * @notice Creates a locked wallet. - * @param unlockTime_ The timestamp after which withdrawals can be made. - */ - constructor(uint256 unlockTime_) payable { - require(block.timestamp < unlockTime_, "Wrong Unlock time"); - - unlockTime = unlockTime_; - owner = payable(msg.sender); - } - - /** - * @notice Allows funds to be received via direct transfers. - */ - receive() external payable { } - - /** - * @notice Withdraws all funds if the unlock time has passed and the caller is the owner. - */ - function withdraw() public { - require(block.timestamp > unlockTime, "You can't withdraw yet"); - require(msg.sender == owner, "You aren't the owner"); - - emit Withdrawal(address(this).balance, block.timestamp); - - owner.transfer(address(this).balance); - } -} diff --git a/scripts/foundry/Deploy.s.sol b/scripts/foundry/Deploy.s.sol deleted file mode 100644 index 5a55826..0000000 --- a/scripts/foundry/Deploy.s.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.23 <0.9.0; - -import { Foo } from "../../contracts/test/Foo.sol"; - -import { BaseScript } from "./Base.s.sol"; - -/// @dev See the Solidity Scripting tutorial: https://book.getfoundry.sh/tutorials/solidity-scripting -contract Deploy is BaseScript { - function run() public broadcast returns (Foo foo) { - foo = new Foo(); - } - - function test() public pure returns (uint256) { - return 0; - } -} diff --git a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol index caf3dc9..646d2e6 100644 --- a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol +++ b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol @@ -1,6 +1,7 @@ // 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"; @@ -25,6 +26,7 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { assertEq(address(testArtifact.entryPoint()), ENTRYPOINT_ADDRESS); assertEq(testArtifact.verifyingSigner(), PAYMASTER_SIGNER.addr); assertEq(testArtifact.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); + assertEq(testArtifact.postopCost(), 0 wei); } function test_CheckInitialPaymasterState() external view { @@ -32,6 +34,7 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { assertEq(address(bicoPaymaster.entryPoint()), ENTRYPOINT_ADDRESS); assertEq(bicoPaymaster.verifyingSigner(), PAYMASTER_SIGNER.addr); assertEq(bicoPaymaster.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); + assertEq(bicoPaymaster.postopCost(), 0 wei); } function test_OwnershipTransfer() external prankModifier(PAYMASTER_OWNER.addr) { @@ -261,4 +264,26 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { vm.expectRevert("withdraw failed"); bicoPaymaster.withdrawEth(payable(ALICE_ADDRESS), ethAmount); } + + function test_SetPostopCost() external prankModifier(PAYMASTER_OWNER.addr) { + uint48 initialPostopCost = bicoPaymaster.postopCost(); + assertEq(initialPostopCost, 0 wei); + uint48 newPostopCost = initialPostopCost + 1 wei; + + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.PostopCostChanged(initialPostopCost, newPostopCost); + bicoPaymaster.setPostopCost(newPostopCost); + + uint48 resultingPostopCost = bicoPaymaster.postopCost(); + assertEq(resultingPostopCost, newPostopCost); + } + + function test_RevertIf_SetPostopCostToHigh() external prankModifier(PAYMASTER_OWNER.addr) { + uint48 initialPostopCost = bicoPaymaster.postopCost(); + assertEq(initialPostopCost, 0 wei); + uint48 newPostopCost = initialPostopCost + 200_001 wei; + + vm.expectRevert("Gas overhead too high"); + bicoPaymaster.setPostopCost(newPostopCost); + } } diff --git a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol index 2eb44e7..0b6e7ff 100644 --- a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol @@ -76,4 +76,18 @@ contract TestFuzz_SponsorshipPaymasterWithPremium is NexusTestBase { assertEq(ALICE_ADDRESS.balance, initialAliceBalance + ethAmount); assertEq(address(bicoPaymaster).balance, 0 ether); } + + function testFuzz_SetPostopCost(uint48 value) external prankModifier(PAYMASTER_OWNER.addr) { + vm.assume(value <= 200_000 wei); + uint48 initialPostopCost = bicoPaymaster.postopCost(); + assertEq(initialPostopCost, 0 wei); + uint48 newPostopCost = value; + + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.PostopCostChanged(initialPostopCost, newPostopCost); + bicoPaymaster.setPostopCost(newPostopCost); + + uint48 resultingPostopCost = bicoPaymaster.postopCost(); + assertEq(resultingPostopCost, newPostopCost); + } } From de84d9971dcf2299afef72afd76e33a039e2ccaf Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Thu, 4 Jul 2024 13:26:46 +0400 Subject: [PATCH 24/69] tests for withdrawErc20 --- ...tSponsorshipPaymasterWithPremiumTest.t.sol | 29 ++++++++++++++++++- ..._TestSponsorshipPaymasterWithPremium.t.sol | 22 ++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol index 646d2e6..3575d6b 100644 --- a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol +++ b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol @@ -1,11 +1,11 @@ // 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 { PackedUserOperation } from "account-abstraction/contracts/core/UserOperationLib.sol"; +import { MockToken } from "./../../../../lib/nexus.git/contracts/mocks/MockToken.sol"; contract TestSponsorshipPaymasterWithPremium is NexusTestBase { BiconomySponsorshipPaymaster public bicoPaymaster; @@ -286,4 +286,31 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { vm.expectRevert("Gas overhead too high"); bicoPaymaster.setPostopCost(newPostopCost); } + + function test_WithdrawErc20() external prankModifier(PAYMASTER_OWNER.addr) { + MockToken token = new MockToken("Token", "TKN"); + uint256 mintAmount = 10 * (10 ** token.decimals()); + token.mint(address(bicoPaymaster), mintAmount); + + assertEq(token.balanceOf(address(bicoPaymaster)), mintAmount); + assertEq(token.balanceOf(ALICE_ADDRESS), 0); + + vm.expectEmit(true, true, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.TokensWithdrawn( + address(token), ALICE_ADDRESS, mintAmount, PAYMASTER_OWNER.addr + ); + bicoPaymaster.withdrawERC20(token, ALICE_ADDRESS, mintAmount); + + assertEq(token.balanceOf(address(bicoPaymaster)), 0); + assertEq(token.balanceOf(ALICE_ADDRESS), mintAmount); + } + + function test_RevertIf_WithdrawErc20ToZeroAddress() external prankModifier(PAYMASTER_OWNER.addr) { + MockToken token = new MockToken("Token", "TKN"); + uint256 mintAmount = 10 * (10 ** token.decimals()); + token.mint(address(bicoPaymaster), mintAmount); + + vm.expectRevert(abi.encodeWithSelector(CanNotWithdrawToZeroAddress.selector)); + bicoPaymaster.withdrawERC20(token, address(0), mintAmount); + } } diff --git a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol index 0b6e7ff..0f07398 100644 --- a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol @@ -5,6 +5,8 @@ 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 { MockToken } from "./../../../../lib/nexus.git/contracts/mocks/MockToken.sol"; + contract TestFuzz_SponsorshipPaymasterWithPremium is NexusTestBase { BiconomySponsorshipPaymaster public bicoPaymaster; @@ -90,4 +92,24 @@ contract TestFuzz_SponsorshipPaymasterWithPremium is NexusTestBase { uint48 resultingPostopCost = bicoPaymaster.postopCost(); assertEq(resultingPostopCost, newPostopCost); } + + function testFuzz_WithdrawErc20(address target, uint256 amount) external prankModifier(PAYMASTER_OWNER.addr) { + vm.assume(target != address(0)); + vm.assume(amount <= 1_000_000 * (10 ** 18)); + MockToken token = new MockToken("Token", "TKN"); + uint256 mintAmount = amount; + token.mint(address(bicoPaymaster), mintAmount); + + assertEq(token.balanceOf(address(bicoPaymaster)), mintAmount); + assertEq(token.balanceOf(ALICE_ADDRESS), 0); + + vm.expectEmit(true, true, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.TokensWithdrawn( + address(token), ALICE_ADDRESS, mintAmount, PAYMASTER_OWNER.addr + ); + bicoPaymaster.withdrawERC20(token, ALICE_ADDRESS, mintAmount); + + assertEq(token.balanceOf(address(bicoPaymaster)), 0); + assertEq(token.balanceOf(ALICE_ADDRESS), mintAmount); + } } From 38f9891cb581e9a20261f99716fea12c15c1d269 Mon Sep 17 00:00:00 2001 From: livingrockrises <90545960+livingrockrises@users.noreply.github.com> Date: Thu, 4 Jul 2024 16:25:02 +0530 Subject: [PATCH 25/69] fix forge build --- .../unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol | 2 +- .../fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol index 3575d6b..058e340 100644 --- a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol +++ b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol @@ -5,7 +5,7 @@ import { NexusTestBase } from "../../base/NexusTestBase.sol"; import { IBiconomySponsorshipPaymaster } from "../../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; import { BiconomySponsorshipPaymaster } from "../../../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; import { PackedUserOperation } from "account-abstraction/contracts/core/UserOperationLib.sol"; -import { MockToken } from "./../../../../lib/nexus.git/contracts/mocks/MockToken.sol"; +import { MockToken } from "./../../../../lib/nexus/contracts/mocks/MockToken.sol"; contract TestSponsorshipPaymasterWithPremium is NexusTestBase { BiconomySponsorshipPaymaster public bicoPaymaster; diff --git a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol index 0f07398..17e4676 100644 --- a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol @@ -5,7 +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"; -import { MockToken } from "./../../../../lib/nexus.git/contracts/mocks/MockToken.sol"; +import { MockToken } from "./../../../../lib/nexus/contracts/mocks/MockToken.sol"; contract TestFuzz_SponsorshipPaymasterWithPremium is NexusTestBase { From 3ac056871ad77d1689c075c4401cb5aff9e48850 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Wed, 3 Jul 2024 15:48:05 +0400 Subject: [PATCH 26/69] 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 de22654ebd0400bd4838ee622f991c617b9761ef Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Thu, 4 Jul 2024 11:42:31 +0400 Subject: [PATCH 27/69] setPostOpCost tests --- contracts/test/Foo.sol | 18 -------- contracts/test/Lock.sol | 46 ------------------- scripts/foundry/Deploy.s.sol | 17 ------- ...tSponsorshipPaymasterWithPremiumTest.t.sol | 25 ++++++++++ ..._TestSponsorshipPaymasterWithPremium.t.sol | 14 ++++++ 5 files changed, 39 insertions(+), 81 deletions(-) delete mode 100644 contracts/test/Foo.sol delete mode 100644 contracts/test/Lock.sol delete mode 100644 scripts/foundry/Deploy.s.sol diff --git a/contracts/test/Foo.sol b/contracts/test/Foo.sol deleted file mode 100644 index 8302d06..0000000 --- a/contracts/test/Foo.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.26; - -/** - * @title Foo - * @dev A simple contract demonstrating a pure function in Solidity. - */ -contract Foo { - /** - * @notice Returns the input value unchanged. - * @dev A pure function that does not alter or interact with contract state. - * @param value The uint256 value to be returned. - * @return uint256 The same value that was input. - */ - function id(uint256 value) external pure returns (uint256) { - return value; - } -} diff --git a/contracts/test/Lock.sol b/contracts/test/Lock.sol deleted file mode 100644 index 522be01..0000000 --- a/contracts/test/Lock.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.26; - -/** - * @title Lock - * @dev Implements a time-locked wallet that only allows withdrawals after a certain date. - */ -contract Lock { - uint256 public unlockTime; - address payable public owner; - - /** - * @dev Emitted when funds are withdrawn from the contract. - * @param amount The amount of Ether withdrawn. - * @param when The timestamp of the withdrawal. - */ - event Withdrawal(uint256 amount, uint256 when); - - /** - * @notice Creates a locked wallet. - * @param unlockTime_ The timestamp after which withdrawals can be made. - */ - constructor(uint256 unlockTime_) payable { - require(block.timestamp < unlockTime_, "Wrong Unlock time"); - - unlockTime = unlockTime_; - owner = payable(msg.sender); - } - - /** - * @notice Allows funds to be received via direct transfers. - */ - receive() external payable { } - - /** - * @notice Withdraws all funds if the unlock time has passed and the caller is the owner. - */ - function withdraw() public { - require(block.timestamp > unlockTime, "You can't withdraw yet"); - require(msg.sender == owner, "You aren't the owner"); - - emit Withdrawal(address(this).balance, block.timestamp); - - owner.transfer(address(this).balance); - } -} diff --git a/scripts/foundry/Deploy.s.sol b/scripts/foundry/Deploy.s.sol deleted file mode 100644 index 5a55826..0000000 --- a/scripts/foundry/Deploy.s.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.23 <0.9.0; - -import { Foo } from "../../contracts/test/Foo.sol"; - -import { BaseScript } from "./Base.s.sol"; - -/// @dev See the Solidity Scripting tutorial: https://book.getfoundry.sh/tutorials/solidity-scripting -contract Deploy is BaseScript { - function run() public broadcast returns (Foo foo) { - foo = new Foo(); - } - - function test() public pure returns (uint256) { - return 0; - } -} diff --git a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol index caf3dc9..646d2e6 100644 --- a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol +++ b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol @@ -1,6 +1,7 @@ // 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"; @@ -25,6 +26,7 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { assertEq(address(testArtifact.entryPoint()), ENTRYPOINT_ADDRESS); assertEq(testArtifact.verifyingSigner(), PAYMASTER_SIGNER.addr); assertEq(testArtifact.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); + assertEq(testArtifact.postopCost(), 0 wei); } function test_CheckInitialPaymasterState() external view { @@ -32,6 +34,7 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { assertEq(address(bicoPaymaster.entryPoint()), ENTRYPOINT_ADDRESS); assertEq(bicoPaymaster.verifyingSigner(), PAYMASTER_SIGNER.addr); assertEq(bicoPaymaster.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); + assertEq(bicoPaymaster.postopCost(), 0 wei); } function test_OwnershipTransfer() external prankModifier(PAYMASTER_OWNER.addr) { @@ -261,4 +264,26 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { vm.expectRevert("withdraw failed"); bicoPaymaster.withdrawEth(payable(ALICE_ADDRESS), ethAmount); } + + function test_SetPostopCost() external prankModifier(PAYMASTER_OWNER.addr) { + uint48 initialPostopCost = bicoPaymaster.postopCost(); + assertEq(initialPostopCost, 0 wei); + uint48 newPostopCost = initialPostopCost + 1 wei; + + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.PostopCostChanged(initialPostopCost, newPostopCost); + bicoPaymaster.setPostopCost(newPostopCost); + + uint48 resultingPostopCost = bicoPaymaster.postopCost(); + assertEq(resultingPostopCost, newPostopCost); + } + + function test_RevertIf_SetPostopCostToHigh() external prankModifier(PAYMASTER_OWNER.addr) { + uint48 initialPostopCost = bicoPaymaster.postopCost(); + assertEq(initialPostopCost, 0 wei); + uint48 newPostopCost = initialPostopCost + 200_001 wei; + + vm.expectRevert("Gas overhead too high"); + bicoPaymaster.setPostopCost(newPostopCost); + } } diff --git a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol index 2eb44e7..0b6e7ff 100644 --- a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol @@ -76,4 +76,18 @@ contract TestFuzz_SponsorshipPaymasterWithPremium is NexusTestBase { assertEq(ALICE_ADDRESS.balance, initialAliceBalance + ethAmount); assertEq(address(bicoPaymaster).balance, 0 ether); } + + function testFuzz_SetPostopCost(uint48 value) external prankModifier(PAYMASTER_OWNER.addr) { + vm.assume(value <= 200_000 wei); + uint48 initialPostopCost = bicoPaymaster.postopCost(); + assertEq(initialPostopCost, 0 wei); + uint48 newPostopCost = value; + + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.PostopCostChanged(initialPostopCost, newPostopCost); + bicoPaymaster.setPostopCost(newPostopCost); + + uint48 resultingPostopCost = bicoPaymaster.postopCost(); + assertEq(resultingPostopCost, newPostopCost); + } } From 3972d142490c5ac31b1b94b893b5b155a15b6a56 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Wed, 3 Jul 2024 17:41:19 +0400 Subject: [PATCH 28/69] 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 +} From f4f341c715a43bca97d5eeb217cd7e80933df1a1 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Wed, 3 Jul 2024 17:52:26 +0400 Subject: [PATCH 29/69] yarn lint runs successfully --- .../IBiconomySponsorshipPaymaster.sol | 22 +- contracts/mocks/Imports.sol | 1 - contracts/mocks/MockValidator.sol | 2 +- contracts/utils/SoladyOwnable.sol | 4 +- .../biconomy-sponsorship-paymaster-specs.ts | 342 +++++---- test/hardhat/utils/deployment.ts | 191 ++--- test/hardhat/utils/testUtils.ts | 266 ++++--- test/hardhat/utils/types.ts | 58 +- test/hardhat/utils/userOpHelpers.ts | 681 +++++++++++------- 9 files changed, 890 insertions(+), 677 deletions(-) diff --git a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol index ed4da78..5f47d1a 100644 --- a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol +++ b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol @@ -2,18 +2,16 @@ pragma solidity ^0.8.26; interface IBiconomySponsorshipPaymaster { - event PostopCostChanged(uint256 indexed _oldValue, uint256 indexed _newValue); - event FixedPriceMarkupChanged(uint32 indexed _oldValue, uint32 indexed _newValue); + event PostopCostChanged(uint256 indexed oldValue, uint256 indexed newValue); + event FixedPriceMarkupChanged(uint32 indexed oldValue, uint32 indexed newValue); - event VerifyingSignerChanged(address indexed _oldSigner, address indexed _newSigner, address indexed _actor); + event VerifyingSignerChanged(address indexed oldSigner, address indexed newSigner, address indexed actor); - event FeeCollectorChanged( - address indexed _oldFeeCollector, address indexed _newFeeCollector, address indexed _actor - ); - event GasDeposited(address indexed _paymasterId, uint256 indexed _value); - event GasWithdrawn(address indexed _paymasterId, address indexed _to, uint256 indexed _value); - event GasBalanceDeducted(address indexed _paymasterId, uint256 indexed _charge, bytes32 indexed userOpHash); - event PremiumCollected(address indexed _paymasterId, uint256 indexed _premium); + event FeeCollectorChanged(address indexed oldFeeCollector, address indexed newFeeCollector, address indexed actor); + event GasDeposited(address indexed paymasterId, uint256 indexed value); + event GasWithdrawn(address indexed paymasterId, address indexed to, uint256 indexed value); + event GasBalanceDeducted(address indexed paymasterId, uint256 indexed charge, bytes32 indexed userOpHash); + event PremiumCollected(address indexed paymasterId, uint256 indexed premium); event Received(address indexed sender, uint256 value); - event TokensWithdrawn(address indexed _token, address indexed _to, uint256 indexed _amount, address actor); -} \ No newline at end of file + event TokensWithdrawn(address indexed token, address indexed to, uint256 indexed amount, address actor); +} diff --git a/contracts/mocks/Imports.sol b/contracts/mocks/Imports.sol index 7b0976a..131ae80 100644 --- a/contracts/mocks/Imports.sol +++ b/contracts/mocks/Imports.sol @@ -8,4 +8,3 @@ 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 index 2c3d359..7682958 100644 --- a/contracts/mocks/MockValidator.sol +++ b/contracts/mocks/MockValidator.sol @@ -1,3 +1,3 @@ pragma solidity ^0.8.26; -import "@biconomy-devx/erc7579-msa/test/foundry/mocks/MockValidator.sol"; \ No newline at end of file +import "@biconomy-devx/erc7579-msa/test/foundry/mocks/MockValidator.sol"; diff --git a/contracts/utils/SoladyOwnable.sol b/contracts/utils/SoladyOwnable.sol index 0cd57c4..8b680d3 100644 --- a/contracts/utils/SoladyOwnable.sol +++ b/contracts/utils/SoladyOwnable.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import {Ownable} from "solady/src/auth/Ownable.sol"; +import { Ownable } from "solady/src/auth/Ownable.sol"; contract SoladyOwnable is Ownable { constructor(address _owner) Ownable() { _initializeOwner(_owner); } -} \ No newline at end of file +} diff --git a/test/hardhat/biconomy-sponsorship-paymaster-specs.ts b/test/hardhat/biconomy-sponsorship-paymaster-specs.ts index dbfabb1..c3a48d4 100644 --- a/test/hardhat/biconomy-sponsorship-paymaster-specs.ts +++ b/test/hardhat/biconomy-sponsorship-paymaster-specs.ts @@ -1,23 +1,35 @@ 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 +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 { + DefaultsForUserOp, + fillAndSign, + fillSignAndPack, + packUserOp, + simulateValidation, +} from "./utils/userOpHelpers"; import { parseValidationData } from "./utils/testUtils"; - export const AddressZero = ethers.ZeroAddress; const MOCK_VALID_UNTIL = "0x00000000deadbeef"; @@ -25,148 +37,174 @@ const MOCK_VALID_AFTER = "0x0000000000001234"; const MARKUP = 1100000; export const ENTRY_POINT_V7 = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"; -const coder = AbiCoder.defaultAbiCoder() +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; + 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") }); - - await deployer.sendTransaction({to: expected, value: parseEther("1"), data: '0x'}); - }); - - describe("Deployed Account : #validatePaymasterUserOp and #sendEmptySponsoredTx", () => { - 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) - ]), - 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)) - 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 - ]), - paymasterPostOpGasLimit: 40_000, - }, walletOwner, entryPoint, 'getNonce', nonceKey) - // 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({ - aggregator: AddressZero, - validAfter: parseInt(MOCK_VALID_AFTER), - validUntil: parseInt(MOCK_VALID_UNTIL) - }) - - await entryPoint.handleOps([userOp], await deployer.getAddress()) - }); + 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") }); + + await deployer.sendTransaction({ + to: expected, + value: parseEther("1"), + data: "0x", + }); + }); + + describe("Deployed Account : #validatePaymasterUserOp and #sendEmptySponsoredTx", () => { + 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), + ]), + 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)); + 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, + ]), + paymasterPostOpGasLimit: 40_000, + }, + walletOwner, + entryPoint, + "getNonce", + nonceKey, + ); + // 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({ + aggregator: AddressZero, + validAfter: parseInt(MOCK_VALID_AFTER), + validUntil: parseInt(MOCK_VALID_UNTIL), + }); + + await entryPoint.handleOps([userOp], await deployer.getAddress()); + }); + }); +}); diff --git a/test/hardhat/utils/deployment.ts b/test/hardhat/utils/deployment.ts index 282831d..18ebef0 100644 --- a/test/hardhat/utils/deployment.ts +++ b/test/hardhat/utils/deployment.ts @@ -1,6 +1,12 @@ import { BytesLike, HDNodeWallet, Signer } from "ethers"; import { deployments, ethers } from "hardhat"; -import { AccountFactory, BiconomySponsorshipPaymaster, EntryPoint, MockValidator, SmartAccount } from "../../../typechain-types"; +import { + AccountFactory, + BiconomySponsorshipPaymaster, + EntryPoint, + MockValidator, + SmartAccount, +} from "../../../typechain-types"; import { TASK_DEPLOY } from "hardhat-deploy"; import { DeployResult } from "hardhat-deploy/dist/types"; @@ -14,39 +20,39 @@ export const ENTRY_POINT_V7 = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"; * @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; + 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; +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; } /** @@ -54,18 +60,18 @@ export async function getDeployedEntrypoint() : Promise { * @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; + 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; } /** @@ -73,27 +79,27 @@ export async function getDeployedMSAImplementation(): Promise { * @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; + 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; } /** @@ -101,41 +107,50 @@ export async function getDeployedAccountFactory( * @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; + 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", { +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( + }, + ); + + return BiconomySponsorshipPaymaster.attach( deterministicSponsorshipPaymaster.address, - ) as BiconomySponsorshipPaymaster; + ) as BiconomySponsorshipPaymaster; } - diff --git a/test/hardhat/utils/testUtils.ts b/test/hardhat/utils/testUtils.ts index 06c4218..abe1776 100644 --- a/test/hardhat/utils/testUtils.ts +++ b/test/hardhat/utils/testUtils.ts @@ -1,6 +1,15 @@ -import { AbiCoder, AddressLike, BigNumberish, Contract, Interface, dataSlice, parseEther, toBeHex } from 'ethers'; -import { ethers } from 'hardhat' -import { EntryPoint__factory, IERC20 } from '../../../typechain-types'; +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 @@ -13,171 +22,189 @@ 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 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' +export const tostr = (x: any): string => (x != null ? x.toString() : "null"); -const coder = AbiCoder.defaultAbiCoder() +const coder = AbiCoder.defaultAbiCoder(); export interface ValidationData { - aggregator: string - validAfter: number - validUntil: number + 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' -} + // 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)", - ]; - + "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]); + 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, - ]); + 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]*/, '') +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)') + 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 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 + 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) + const data = found[1]; + message = + decodeRevertReason(data) ?? e.message + " - " + data.slice(0, 100); } else { - message = e.message + message = e.message; } - const err = new Error(message) - err.stack = 'Error: ' + message + '\n' + stack - throw err - } + 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 + "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) + 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) + 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} + ')` + return `Error(${err})`; + } else if (methodSig === "0x4e487b71") { + const [code] = coder.decode(["uint256"], dataParams); + return `Panic(${panicCodes[code] ?? code} + ')`; } try { - const err = decodeRevertReasonContracts.parseError(data) + 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 + case "bytes": + return decodeRevertReason(arg); + case "string": + return `"${arg as string}"`; + default: + return arg; } - }) - return `${err!.name}(${args.join(',')})` + }); + return `${err!.name}(${args.join(",")})`; } catch (e) { // throw new Error('unsupported errorSig ' + data) if (!nullIfNoMatch) { - return data + return data; } - return null + return null; } } -export function tonumber (x: any): number { +export function tonumber(x: any): number { try { - return parseFloat(x.toString()) + return parseFloat(x.toString()); } catch (e: any) { - console.log('=== failed to parseFloat:', x, (e).message) - return NaN + 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 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 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 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 +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) @@ -202,28 +229,29 @@ export async function getInitCode( 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 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) +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)) + const aggregator = dataSlice(data, 32 - 20); + let validUntil = parseInt(dataSlice(data, 32 - 26, 32 - 20)); if (validUntil === 0) { - validUntil = maxUint48 + validUntil = maxUint48; } - const validAfter = parseInt(dataSlice(data, 0, 6)) + const validAfter = parseInt(dataSlice(data, 0, 6)); return { aggregator, validAfter, - validUntil - } + validUntil, + }; } - - diff --git a/test/hardhat/utils/types.ts b/test/hardhat/utils/types.ts index 791fc10..7dd52fa 100644 --- a/test/hardhat/utils/types.ts +++ b/test/hardhat/utils/types.ts @@ -1,34 +1,30 @@ -import { - AddressLike, - BigNumberish, - BytesLike, - } from "ethers"; +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; - } + 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 +export interface PackedUserOperation { + sender: AddressLike; // Or string + nonce: BigNumberish; + initCode: BytesLike; + callData: BytesLike; + accountGasLimits: BytesLike; + preVerificationGas: BigNumberish; + gasFees: BytesLike; + paymasterAndData: BytesLike; + signature: BytesLike; +} diff --git a/test/hardhat/utils/userOpHelpers.ts b/test/hardhat/utils/userOpHelpers.ts index 8dc582c..50fccd5 100644 --- a/test/hardhat/utils/userOpHelpers.ts +++ b/test/hardhat/utils/userOpHelpers.ts @@ -1,157 +1,230 @@ import { ethers } from "hardhat"; -import { EntryPoint, EntryPointSimulations__factory, IEntryPointSimulations } from "../../../typechain-types"; +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 { 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' +import EntryPointSimulationsJson from "../../../artifacts/account-abstraction/contracts/core/EntryPointSimulations.sol/EntryPointSimulations.json"; const AddressZero = ethers.ZeroAddress; -const coder = AbiCoder.defaultAbiCoder() +const coder = AbiCoder.defaultAbiCoder(); -export function packUserOp (userOp: UserOperation): PackedUserOperation { +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 { - 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' - } + 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]) - } +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, - ]); + 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) - ]) +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 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 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' -} + 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) +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 - } + 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] - } +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 + } + const filled = { ...defaults, ...partial }; + return filled; } // helper to fill structure: @@ -166,112 +239,151 @@ export function fillUserOpDefaults (op: Partial, defaults = Defau // 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) +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 (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.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)) + if (op1.paymasterPostOpGasLimit == null) { + op1.paymasterPostOpGasLimit = DefaultsForUserOp.paymasterPostOpGasLimit; } - return op2; + } + 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 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 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) + +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); } /** @@ -281,67 +393,94 @@ export async function fillAndSign (op: Partial, signer: Signer | * @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 +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 +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 8326d900d35f17b6881694c4479953256558fd08 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Thu, 4 Jul 2024 13:26:46 +0400 Subject: [PATCH 30/69] tests for withdrawErc20 --- ...tSponsorshipPaymasterWithPremiumTest.t.sol | 29 ++++++++++++++++++- ..._TestSponsorshipPaymasterWithPremium.t.sol | 22 ++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol index 646d2e6..3575d6b 100644 --- a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol +++ b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol @@ -1,11 +1,11 @@ // 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 { PackedUserOperation } from "account-abstraction/contracts/core/UserOperationLib.sol"; +import { MockToken } from "./../../../../lib/nexus.git/contracts/mocks/MockToken.sol"; contract TestSponsorshipPaymasterWithPremium is NexusTestBase { BiconomySponsorshipPaymaster public bicoPaymaster; @@ -286,4 +286,31 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { vm.expectRevert("Gas overhead too high"); bicoPaymaster.setPostopCost(newPostopCost); } + + function test_WithdrawErc20() external prankModifier(PAYMASTER_OWNER.addr) { + MockToken token = new MockToken("Token", "TKN"); + uint256 mintAmount = 10 * (10 ** token.decimals()); + token.mint(address(bicoPaymaster), mintAmount); + + assertEq(token.balanceOf(address(bicoPaymaster)), mintAmount); + assertEq(token.balanceOf(ALICE_ADDRESS), 0); + + vm.expectEmit(true, true, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.TokensWithdrawn( + address(token), ALICE_ADDRESS, mintAmount, PAYMASTER_OWNER.addr + ); + bicoPaymaster.withdrawERC20(token, ALICE_ADDRESS, mintAmount); + + assertEq(token.balanceOf(address(bicoPaymaster)), 0); + assertEq(token.balanceOf(ALICE_ADDRESS), mintAmount); + } + + function test_RevertIf_WithdrawErc20ToZeroAddress() external prankModifier(PAYMASTER_OWNER.addr) { + MockToken token = new MockToken("Token", "TKN"); + uint256 mintAmount = 10 * (10 ** token.decimals()); + token.mint(address(bicoPaymaster), mintAmount); + + vm.expectRevert(abi.encodeWithSelector(CanNotWithdrawToZeroAddress.selector)); + bicoPaymaster.withdrawERC20(token, address(0), mintAmount); + } } diff --git a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol index 0b6e7ff..0f07398 100644 --- a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol @@ -5,6 +5,8 @@ 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 { MockToken } from "./../../../../lib/nexus.git/contracts/mocks/MockToken.sol"; + contract TestFuzz_SponsorshipPaymasterWithPremium is NexusTestBase { BiconomySponsorshipPaymaster public bicoPaymaster; @@ -90,4 +92,24 @@ contract TestFuzz_SponsorshipPaymasterWithPremium is NexusTestBase { uint48 resultingPostopCost = bicoPaymaster.postopCost(); assertEq(resultingPostopCost, newPostopCost); } + + function testFuzz_WithdrawErc20(address target, uint256 amount) external prankModifier(PAYMASTER_OWNER.addr) { + vm.assume(target != address(0)); + vm.assume(amount <= 1_000_000 * (10 ** 18)); + MockToken token = new MockToken("Token", "TKN"); + uint256 mintAmount = amount; + token.mint(address(bicoPaymaster), mintAmount); + + assertEq(token.balanceOf(address(bicoPaymaster)), mintAmount); + assertEq(token.balanceOf(ALICE_ADDRESS), 0); + + vm.expectEmit(true, true, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.TokensWithdrawn( + address(token), ALICE_ADDRESS, mintAmount, PAYMASTER_OWNER.addr + ); + bicoPaymaster.withdrawERC20(token, ALICE_ADDRESS, mintAmount); + + assertEq(token.balanceOf(address(bicoPaymaster)), 0); + assertEq(token.balanceOf(ALICE_ADDRESS), mintAmount); + } } From 1ee053e6df01a5d2b92a41ed2a59a7df2abd29fa Mon Sep 17 00:00:00 2001 From: livingrockrises <90545960+livingrockrises@users.noreply.github.com> Date: Thu, 4 Jul 2024 16:25:02 +0530 Subject: [PATCH 31/69] fix forge build --- .../unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol | 2 +- .../fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol index 3575d6b..058e340 100644 --- a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol +++ b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol @@ -5,7 +5,7 @@ import { NexusTestBase } from "../../base/NexusTestBase.sol"; import { IBiconomySponsorshipPaymaster } from "../../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; import { BiconomySponsorshipPaymaster } from "../../../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; import { PackedUserOperation } from "account-abstraction/contracts/core/UserOperationLib.sol"; -import { MockToken } from "./../../../../lib/nexus.git/contracts/mocks/MockToken.sol"; +import { MockToken } from "./../../../../lib/nexus/contracts/mocks/MockToken.sol"; contract TestSponsorshipPaymasterWithPremium is NexusTestBase { BiconomySponsorshipPaymaster public bicoPaymaster; diff --git a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol index 0f07398..17e4676 100644 --- a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol @@ -5,7 +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"; -import { MockToken } from "./../../../../lib/nexus.git/contracts/mocks/MockToken.sol"; +import { MockToken } from "./../../../../lib/nexus/contracts/mocks/MockToken.sol"; contract TestFuzz_SponsorshipPaymasterWithPremium is NexusTestBase { From e1ba22eccd06bd55df0252e57ae1967804ce5d65 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Thu, 4 Jul 2024 18:17:31 +0400 Subject: [PATCH 32/69] switched to custom errors and fixed postop gas calc --- contracts/common/Errors.sol | 30 ++++++++ .../SponsorshipPaymasterWithPremium.sol | 70 +++++++++---------- ...tSponsorshipPaymasterWithPremiumTest.t.sol | 30 +------- ..._TestSponsorshipPaymasterWithPremium.t.sol | 15 ---- 4 files changed, 65 insertions(+), 80 deletions(-) diff --git a/contracts/common/Errors.sol b/contracts/common/Errors.sol index 998492a..58bf40f 100644 --- a/contracts/common/Errors.sol +++ b/contracts/common/Errors.sol @@ -32,6 +32,36 @@ contract BiconomySponsorshipPaymasterErrors { */ error VerifyingSignerCanNotBeContract(); + /** + * @notice Throws when ETH withdrawal fails + */ + error WithdrawalFailed(); + + /** + * @notice Throws when insufficient funds to withdraw + */ + error InsufficientFundsInGasTank(); + + /** + * @notice Throws when invalid signature length in paymasterAndData + */ + error InvalidSignatureLength(); + + /** + * @notice Throws when invalid signature length in paymasterAndData + */ + error InvalidPriceMarkup(); + + /** + * @notice Throws when insufficient funds for paymasterid + */ + error InsufficientFundsForPaymasterId(); + + /** + * @notice Throws when calling deposit() + */ + error UseDepositForInstead(); + /** * @notice Throws when trying to withdraw to address(0) */ diff --git a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol b/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol index 2e3abf4..3011264 100644 --- a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol +++ b/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol @@ -39,7 +39,6 @@ contract BiconomySponsorshipPaymaster is address public verifyingSigner; address public feeCollector; - uint48 public postopCost; uint32 private constant PRICE_DENOMINATOR = 1e6; // note: could rename to PAYMASTER_ID_OFFSET @@ -55,8 +54,11 @@ contract BiconomySponsorshipPaymaster is ) BasePaymaster(_owner, _entryPoint) { - // TODO - // Check for zero address + if (_verifyingSigner == address(0)) { + revert VerifyingSignerCanNotBeZero(); + } else if (_feeCollector == address(0)) { + revert FeeCollectorCanNotBeZero(); + } verifyingSigner = _verifyingSigner; feeCollector = _feeCollector; } @@ -117,23 +119,11 @@ contract BiconomySponsorshipPaymaster is emit FeeCollectorChanged(oldFeeCollector, _newFeeCollector, msg.sender); } - /** - * @dev Set a new unaccountedEPGasOverhead value. - * @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 <= 200_000, "Gas overhead too high"); - uint256 oldValue = postopCost; - postopCost = value; - emit PostopCostChanged(oldValue, value); - } - /** * @dev Override the default implementation. */ function deposit() external payable virtual override { - revert("Use depositFor() instead"); + revert UseDepositForInstead(); } /** @@ -155,15 +145,19 @@ contract BiconomySponsorshipPaymaster is 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"); + if (amount > currentBalance) { + revert InsufficientFundsInGasTank(); + } paymasterIdBalances[msg.sender] = currentBalance - amount; entryPoint.withdrawTo(withdrawAddress, amount); emit GasWithdrawn(msg.sender, withdrawAddress, amount); } - function withdrawEth(address payable recipient, uint256 amount) external onlyOwner { + function withdrawEth(address payable recipient, uint256 amount) external onlyOwner nonReentrant { (bool success,) = recipient.call{ value: amount }(""); - require(success, "withdraw failed"); + if (!success) { + revert WithdrawalFailed(); + } } /** @@ -225,11 +219,13 @@ contract BiconomySponsorshipPaymaster is 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:]; + unchecked { + 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 @@ -252,14 +248,12 @@ contract BiconomySponsorshipPaymaster is (address paymasterId, uint32 dynamicMarkup, bytes32 userOpHash) = abi.decode(context, (address, uint32, bytes32)); - uint256 balToDeduct = actualGasCost + postopCost * actualUserOpFeePerGas; - - uint256 costIncludingPremium = (balToDeduct * dynamicMarkup) / PRICE_DENOMINATOR; + uint256 costIncludingPremium = (actualGasCost * dynamicMarkup) / PRICE_DENOMINATOR; // deduct with premium paymasterIdBalances[paymasterId] -= costIncludingPremium; - uint256 actualPremium = costIncludingPremium - balToDeduct; + uint256 actualPremium = costIncludingPremium - actualGasCost; // "collect" premium paymasterIdBalances[feeCollector] += actualPremium; @@ -294,10 +288,9 @@ contract BiconomySponsorshipPaymaster is //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" - ); + if(signature.length != 64 && signature.length != 65){ + revert InvalidSignatureLength(); + } bool validSig = verifyingSigner.isValidSignatureNow( ECDSA_solady.toEthSignedMessageHash(getHash(userOp, paymasterId, validUntil, validAfter, priceMarkup)), @@ -309,18 +302,19 @@ contract BiconomySponsorshipPaymaster is return ("", _packValidationData(true, validUntil, validAfter)); } - require(priceMarkup <= 2e6 && priceMarkup > 0, "Sponsorship Paymaster: Invalid markup %"); + if (priceMarkup > 2e6 || priceMarkup == 0) { + revert InvalidPriceMarkup(); + } uint256 maxFeePerGas = userOp.unpackMaxFeePerGas(); // Send 1e6 for No markup // Send between 0 and 1e6 for discount - uint256 effectiveCost = ((requiredPreFund + (postopCost * maxFeePerGas)) * priceMarkup) / PRICE_DENOMINATOR; + uint256 effectiveCost = (requiredPreFund * priceMarkup) / PRICE_DENOMINATOR; - require( - effectiveCost <= paymasterIdBalances[paymasterId], - "Sponsorship Paymaster: paymasterId does not have enough deposit" - ); + if (effectiveCost > paymasterIdBalances[paymasterId]) { + revert InsufficientFundsForPaymasterId(); + } context = abi.encode(paymasterId, priceMarkup, userOpHash); diff --git a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol index 058e340..4f58bbe 100644 --- a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol +++ b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol @@ -26,7 +26,6 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { assertEq(address(testArtifact.entryPoint()), ENTRYPOINT_ADDRESS); assertEq(testArtifact.verifyingSigner(), PAYMASTER_SIGNER.addr); assertEq(testArtifact.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); - assertEq(testArtifact.postopCost(), 0 wei); } function test_CheckInitialPaymasterState() external view { @@ -34,7 +33,6 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { assertEq(address(bicoPaymaster.entryPoint()), ENTRYPOINT_ADDRESS); assertEq(bicoPaymaster.verifyingSigner(), PAYMASTER_SIGNER.addr); assertEq(bicoPaymaster.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); - assertEq(bicoPaymaster.postopCost(), 0 wei); } function test_OwnershipTransfer() external prankModifier(PAYMASTER_OWNER.addr) { @@ -121,7 +119,7 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { } function test_RevertIf_DepositCalled() external { - vm.expectRevert("Use depositFor() instead"); + vm.expectRevert(abi.encodeWithSelector(UseDepositForInstead.selector)); bicoPaymaster.deposit{ value: 1 ether }(); } @@ -146,7 +144,7 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { } function test_RevertIf_WithdrawToExceedsBalance() external prankModifier(DAPP_ACCOUNT.addr) { - vm.expectRevert("Sponsorship Paymaster: Insufficient funds to withdraw from gas tank"); + vm.expectRevert(abi.encodeWithSelector(InsufficientFundsInGasTank.selector)); bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), 1 ether); } @@ -261,32 +259,10 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { function test_RevertIf_WithdrawEthExceedsBalance() external prankModifier(PAYMASTER_OWNER.addr) { uint256 ethAmount = 10 ether; - vm.expectRevert("withdraw failed"); + vm.expectRevert(abi.encodeWithSelector(WithdrawalFailed.selector)); bicoPaymaster.withdrawEth(payable(ALICE_ADDRESS), ethAmount); } - function test_SetPostopCost() external prankModifier(PAYMASTER_OWNER.addr) { - uint48 initialPostopCost = bicoPaymaster.postopCost(); - assertEq(initialPostopCost, 0 wei); - uint48 newPostopCost = initialPostopCost + 1 wei; - - vm.expectEmit(true, true, false, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.PostopCostChanged(initialPostopCost, newPostopCost); - bicoPaymaster.setPostopCost(newPostopCost); - - uint48 resultingPostopCost = bicoPaymaster.postopCost(); - assertEq(resultingPostopCost, newPostopCost); - } - - function test_RevertIf_SetPostopCostToHigh() external prankModifier(PAYMASTER_OWNER.addr) { - uint48 initialPostopCost = bicoPaymaster.postopCost(); - assertEq(initialPostopCost, 0 wei); - uint48 newPostopCost = initialPostopCost + 200_001 wei; - - vm.expectRevert("Gas overhead too high"); - bicoPaymaster.setPostopCost(newPostopCost); - } - function test_WithdrawErc20() external prankModifier(PAYMASTER_OWNER.addr) { MockToken token = new MockToken("Token", "TKN"); uint256 mintAmount = 10 * (10 ** token.decimals()); diff --git a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol index 17e4676..5fb19db 100644 --- a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol @@ -7,7 +7,6 @@ import { IBiconomySponsorshipPaymaster } from "../../../../contracts/interfaces/ import { BiconomySponsorshipPaymaster } from "../../../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; import { MockToken } from "./../../../../lib/nexus/contracts/mocks/MockToken.sol"; - contract TestFuzz_SponsorshipPaymasterWithPremium is NexusTestBase { BiconomySponsorshipPaymaster public bicoPaymaster; @@ -79,20 +78,6 @@ contract TestFuzz_SponsorshipPaymasterWithPremium is NexusTestBase { assertEq(address(bicoPaymaster).balance, 0 ether); } - function testFuzz_SetPostopCost(uint48 value) external prankModifier(PAYMASTER_OWNER.addr) { - vm.assume(value <= 200_000 wei); - uint48 initialPostopCost = bicoPaymaster.postopCost(); - assertEq(initialPostopCost, 0 wei); - uint48 newPostopCost = value; - - vm.expectEmit(true, true, false, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.PostopCostChanged(initialPostopCost, newPostopCost); - bicoPaymaster.setPostopCost(newPostopCost); - - uint48 resultingPostopCost = bicoPaymaster.postopCost(); - assertEq(resultingPostopCost, newPostopCost); - } - function testFuzz_WithdrawErc20(address target, uint256 amount) external prankModifier(PAYMASTER_OWNER.addr) { vm.assume(target != address(0)); vm.assume(amount <= 1_000_000 * (10 ** 18)); From 10b389270d43088eb8d7818e79a11fb96d714bac Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Sat, 6 Jul 2024 18:21:11 +0400 Subject: [PATCH 33/69] fixed a few things with gas calc and wrote tests --- .../SponsorshipPaymasterWithPremium.sol | 19 ++-- test/foundry/base/NexusTestBase.sol | 91 +++---------------- ...tSponsorshipPaymasterWithPremiumTest.t.sol | 74 ++++++++++++++- 3 files changed, 95 insertions(+), 89 deletions(-) diff --git a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol b/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol index 3011264..5c78e91 100644 --- a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol +++ b/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol @@ -232,14 +232,11 @@ contract BiconomySponsorshipPaymaster is /// @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 + uint256 ) internal override @@ -253,13 +250,15 @@ contract BiconomySponsorshipPaymaster is // deduct with premium paymasterIdBalances[paymasterId] -= costIncludingPremium; - uint256 actualPremium = costIncludingPremium - actualGasCost; - // "collect" premium - paymasterIdBalances[feeCollector] += actualPremium; + if (actualGasCost < costIncludingPremium) { + // "collect" premium + uint256 actualPremium = costIncludingPremium - actualGasCost; + paymasterIdBalances[feeCollector] += actualPremium; + // Review if we should emit balToDeduct as well + emit PremiumCollected(paymasterId, actualPremium); + } emit GasBalanceDeducted(paymasterId, costIncludingPremium, userOpHash); - // Review if we should emit balToDeduct as well - emit PremiumCollected(paymasterId, actualPremium); } } @@ -288,7 +287,7 @@ contract BiconomySponsorshipPaymaster is //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" - if(signature.length != 64 && signature.length != 65){ + if (signature.length != 64 && signature.length != 65) { revert InvalidSignatureLength(); } diff --git a/test/foundry/base/NexusTestBase.sol b/test/foundry/base/NexusTestBase.sol index 6bc0df7..308bf26 100644 --- a/test/foundry/base/NexusTestBase.sol +++ b/test/foundry/base/NexusTestBase.sol @@ -8,6 +8,7 @@ import "solady/src/utils/ECDSA.sol"; import { EntryPoint } from "account-abstraction/contracts/core/EntryPoint.sol"; import { IEntryPoint } from "account-abstraction/contracts/interfaces/IEntryPoint.sol"; +import { IPaymaster } from "account-abstraction/contracts/interfaces/IPaymaster.sol"; import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol"; import { Nexus } from "nexus/contracts/Nexus.sol"; @@ -331,87 +332,23 @@ abstract contract NexusTestBase is CheatCodes, BaseEventsAndErrors { assertTrue(res, "Pre-funding account should succeed"); } - /// @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 + function estimatePaymasterGasCosts( + BiconomySponsorshipPaymaster paymaster, + PackedUserOperation memory userOp, + bytes32 userOpHash, + uint256 requiredPreFund ) internal + prankModifier(ENTRYPOINT_ADDRESS) + returns (uint256 validationGasLimit, uint256 postopGasLimit) { - 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); - } + validationGasLimit = gasleft(); + (bytes memory context,) = paymaster.validatePaymasterUserOp(userOp, userOpHash, requiredPreFund); + validationGasLimit = validationGasLimit - gasleft(); - /// @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(); + postopGasLimit = gasleft(); + paymaster.postOp(IPaymaster.PostOpMode.opSucceeded, context, 3e4, 2e9); + postopGasLimit = postopGasLimit - gasleft(); } /// @notice Generates and signs the paymaster data for a user operation. diff --git a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol index 4f58bbe..c4b5113 100644 --- a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol +++ b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol @@ -1,6 +1,7 @@ // 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"; @@ -28,6 +29,16 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { assertEq(testArtifact.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); } + function test_RevertIf_DeployWithSignerSetToZero() external { + vm.expectRevert(abi.encodeWithSelector(VerifyingSignerCanNotBeZero.selector)); + new BiconomySponsorshipPaymaster(PAYMASTER_OWNER.addr, ENTRYPOINT, address(0), PAYMASTER_FEE_COLLECTOR.addr); + } + + function test_RevertIf_DeployWithFeeCollectorSetToZero() external { + vm.expectRevert(abi.encodeWithSelector(FeeCollectorCanNotBeZero.selector)); + new BiconomySponsorshipPaymaster(PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, address(0)); + } + function test_CheckInitialPaymasterState() external view { assertEq(bicoPaymaster.owner(), PAYMASTER_OWNER.addr); assertEq(address(bicoPaymaster.entryPoint()), ENTRYPOINT_ADDRESS); @@ -148,7 +159,7 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), 1 ether); } - function test_ValidatePaymasterAndPostOp() external { + function test_ValidatePaymasterAndPostOpWithoutPremium() external { uint256 initialDappPaymasterBalance = 10 ether; bicoPaymaster.depositFor{ value: initialDappPaymasterBalance }(DAPP_ACCOUNT.addr); @@ -169,12 +180,71 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { vm.expectEmit(true, false, true, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + + uint256 resultingDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + assertNotEq(initialDappPaymasterBalance, resultingDappPaymasterBalance); + } + + function test_ValidatePaymasterAndPostOpWithPremium() external { + uint256 initialDappPaymasterBalance = 10 ether; + bicoPaymaster.depositFor{ value: initialDappPaymasterBalance }(DAPP_ACCOUNT.addr); + uint256 initialBundlerBalance = BUNDLER.addr.balance; + + 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)); + + // Charge a 10% premium + uint32 premium = 1e6 + 1e5; + userOp.paymasterAndData = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, premium + ); + userOp.signature = signUserOp(ALICE, userOp); + + // Estimate paymaster gas limits + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + (uint256 validationGasLimit, uint256 postopGasLimit) = + estimatePaymasterGasCosts(bicoPaymaster, userOp, userOpHash, 5e4); + + // Ammend the userop to have new gas limits and signature + userOp.paymasterAndData = generateAndSignPaymasterData( + userOp, + PAYMASTER_SIGNER, + bicoPaymaster, + uint128(validationGasLimit), + uint128(postopGasLimit), + DAPP_ACCOUNT.addr, + validUntil, + validAfter, + premium + ); + userOp.signature = signUserOp(ALICE, userOp); + ops[0] = userOp; + userOpHash = ENTRYPOINT.getUserOpHash(userOp); + + // submit userops vm.expectEmit(true, false, false, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.PremiumCollected(DAPP_ACCOUNT.addr, 0); + vm.expectEmit(true, false, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + // Check that gas fees ended up in the right wallets uint256 resultingDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); - assertNotEq(initialDappPaymasterBalance, resultingDappPaymasterBalance); + uint256 resultingFeeCollectorPaymasterBalance = bicoPaymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); + uint256 resultingBundlerBalance = BUNDLER.addr.balance - initialBundlerBalance; + uint256 totalGasFeesCharged = initialDappPaymasterBalance - resultingDappPaymasterBalance; + console2.log(resultingDappPaymasterBalance); + console2.log(resultingFeeCollectorPaymasterBalance); + console2.log(resultingBundlerBalance); + console2.log(totalGasFeesCharged); + + // resultingDappPaymasterBalance + assertEq(resultingFeeCollectorPaymasterBalance, (totalGasFeesCharged * 1e5) / premium); } function test_RevertIf_ValidatePaymasterUserOpWithIncorrectSignatureLength() external { From 2817e4443aff35de3a88812da25fed6738173016 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Sun, 7 Jul 2024 22:06:39 +0400 Subject: [PATCH 34/69] more tests for accounting for premiums --- .../SponsorshipPaymasterWithPremium.sol | 14 +--- foundry.toml | 5 +- test/foundry/base/NexusTestBase.sol | 2 +- ...tSponsorshipPaymasterWithPremiumTest.t.sol | 66 +++++++++--------- ..._TestSponsorshipPaymasterWithPremium.t.sol | 67 ++++++++++++++++++- 5 files changed, 103 insertions(+), 51 deletions(-) diff --git a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol b/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol index 5c78e91..5b06711 100644 --- a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol +++ b/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol @@ -232,15 +232,7 @@ contract BiconomySponsorshipPaymaster is /// @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. - function _postOp( - PostOpMode, - bytes calldata context, - uint256 actualGasCost, - uint256 - ) - internal - override - { + function _postOp(PostOpMode, bytes calldata context, uint256 actualGasCost, uint256) internal override { unchecked { (address paymasterId, uint32 dynamicMarkup, bytes32 userOpHash) = abi.decode(context, (address, uint32, bytes32)); @@ -250,7 +242,7 @@ contract BiconomySponsorshipPaymaster is // deduct with premium paymasterIdBalances[paymasterId] -= costIncludingPremium; - if (actualGasCost < costIncludingPremium) { + if (costIncludingPremium > actualGasCost) { // "collect" premium uint256 actualPremium = costIncludingPremium - actualGasCost; paymasterIdBalances[feeCollector] += actualPremium; @@ -305,8 +297,6 @@ contract BiconomySponsorshipPaymaster is revert InvalidPriceMarkup(); } - uint256 maxFeePerGas = userOp.unpackMaxFeePerGas(); - // Send 1e6 for No markup // Send between 0 and 1e6 for discount uint256 effectiveCost = (requiredPreFund * priceMarkup) / PRICE_DENOMINATOR; diff --git a/foundry.toml b/foundry.toml index 04c3656..80fe03b 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,7 +5,6 @@ block_timestamp = 1_680_220_800 # March 31, 2023 at 00:00 GMT bytecode_hash = "none" evm_version = "paris" # See https://www.evmdiff.com/features?name=PUSH0&kind=opcode - fuzz = { runs = 1_000 } gas_reports = ["*"] optimizer = true optimizer_runs = 1_000_000 @@ -19,6 +18,10 @@ gas_reports_ignore = ["LockTest"] via_ir = true +[fuzz] + runs = 1_000 + max_test_rejects = 1_000_000 + [profile.ci] fuzz = { runs = 10_000 } verbosity = 4 diff --git a/test/foundry/base/NexusTestBase.sol b/test/foundry/base/NexusTestBase.sol index 308bf26..cd7dbaf 100644 --- a/test/foundry/base/NexusTestBase.sol +++ b/test/foundry/base/NexusTestBase.sol @@ -347,7 +347,7 @@ abstract contract NexusTestBase is CheatCodes, BaseEventsAndErrors { validationGasLimit = validationGasLimit - gasleft(); postopGasLimit = gasleft(); - paymaster.postOp(IPaymaster.PostOpMode.opSucceeded, context, 3e4, 2e9); + paymaster.postOp(IPaymaster.PostOpMode.opSucceeded, context, 1e12, 3e6); postopGasLimit = postopGasLimit - gasleft(); } diff --git a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol index c4b5113..0f6ed46 100644 --- a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol +++ b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol @@ -135,31 +135,6 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { } 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.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); - } - - function test_RevertIf_WithdrawToZeroAddress() external prankModifier(DAPP_ACCOUNT.addr) { - vm.expectRevert(abi.encodeWithSelector(CanNotWithdrawToZeroAddress.selector)); - bicoPaymaster.withdrawTo(payable(address(0)), 0 ether); - } - - function test_RevertIf_WithdrawToExceedsBalance() external prankModifier(DAPP_ACCOUNT.addr) { - vm.expectRevert(abi.encodeWithSelector(InsufficientFundsInGasTank.selector)); - bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), 1 ether); - } - - function test_ValidatePaymasterAndPostOpWithoutPremium() external { uint256 initialDappPaymasterBalance = 10 ether; bicoPaymaster.depositFor{ value: initialDappPaymasterBalance }(DAPP_ACCOUNT.addr); @@ -169,14 +144,34 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { uint48 validAfter = uint48(block.timestamp); PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + + // No premium + uint32 premium = 1e6; userOp.paymasterAndData = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 + userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, premium ); userOp.signature = signUserOp(ALICE, userOp); + // Estimate paymaster gas limits bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + (uint256 validationGasLimit, uint256 postopGasLimit) = + estimatePaymasterGasCosts(bicoPaymaster, userOp, userOpHash, 5e4); + // Ammend the userop to have new gas limits and signature + userOp.paymasterAndData = generateAndSignPaymasterData( + userOp, + PAYMASTER_SIGNER, + bicoPaymaster, + uint128(validationGasLimit), + uint128(postopGasLimit), + DAPP_ACCOUNT.addr, + validUntil, + validAfter, + premium + ); + userOp.signature = signUserOp(ALICE, userOp); ops[0] = userOp; + userOpHash = ENTRYPOINT.getUserOpHash(userOp); vm.expectEmit(true, false, true, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); @@ -189,7 +184,6 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { function test_ValidatePaymasterAndPostOpWithPremium() external { uint256 initialDappPaymasterBalance = 10 ether; bicoPaymaster.depositFor{ value: initialDappPaymasterBalance }(DAPP_ACCOUNT.addr); - uint256 initialBundlerBalance = BUNDLER.addr.balance; PackedUserOperation[] memory ops = new PackedUserOperation[](1); @@ -226,6 +220,8 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { ops[0] = userOp; userOpHash = ENTRYPOINT.getUserOpHash(userOp); + uint256 initialFeeCollectorBalance = bicoPaymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); + initialDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); // submit userops vm.expectEmit(true, false, false, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.PremiumCollected(DAPP_ACCOUNT.addr, 0); @@ -233,18 +229,16 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - // Check that gas fees ended up in the right wallets uint256 resultingDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); uint256 resultingFeeCollectorPaymasterBalance = bicoPaymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); - uint256 resultingBundlerBalance = BUNDLER.addr.balance - initialBundlerBalance; + uint256 totalGasFeesCharged = initialDappPaymasterBalance - resultingDappPaymasterBalance; - console2.log(resultingDappPaymasterBalance); - console2.log(resultingFeeCollectorPaymasterBalance); - console2.log(resultingBundlerBalance); - console2.log(totalGasFeesCharged); - - // resultingDappPaymasterBalance - assertEq(resultingFeeCollectorPaymasterBalance, (totalGasFeesCharged * 1e5) / premium); + uint256 premiumCollected = resultingFeeCollectorPaymasterBalance - initialFeeCollectorBalance; + + uint256 expectedGasPayment = totalGasFeesCharged - premiumCollected; + uint256 expectedPremium = expectedGasPayment / 10; + + assertEq(premiumCollected, expectedPremium); } function test_RevertIf_ValidatePaymasterUserOpWithIncorrectSignatureLength() external { diff --git a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol index 5fb19db..98fe11d 100644 --- a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol @@ -1,11 +1,13 @@ // SPDX-License-Identifier: Unlicensed pragma solidity ^0.8.26; -import { console2 } from "forge-std/src/Console2.sol"; +import { console2 } from "forge-std/src/console2.sol"; +import { stdMath } from "forge-std/src/Test.sol"; import { NexusTestBase } from "../../base/NexusTestBase.sol"; import { IBiconomySponsorshipPaymaster } from "../../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; import { BiconomySponsorshipPaymaster } from "../../../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; import { MockToken } from "./../../../../lib/nexus/contracts/mocks/MockToken.sol"; +import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol"; contract TestFuzz_SponsorshipPaymasterWithPremium is NexusTestBase { BiconomySponsorshipPaymaster public bicoPaymaster; @@ -97,4 +99,67 @@ contract TestFuzz_SponsorshipPaymasterWithPremium is NexusTestBase { assertEq(token.balanceOf(address(bicoPaymaster)), 0); assertEq(token.balanceOf(ALICE_ADDRESS), mintAmount); } + + function testFuzz_ValidatePaymasterAndPostOpWithPremium(uint32 premium) external { + vm.assume(premium <= 2e6); + vm.assume(premium > 1e6); + + 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, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, premium + ); + userOp.signature = signUserOp(ALICE, userOp); + + // Estimate paymaster gas limits + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + (uint256 validationGasLimit, uint256 postopGasLimit) = + estimatePaymasterGasCosts(bicoPaymaster, userOp, userOpHash, 5e4); + + // Ammend the userop to have new gas limits and signature + userOp.paymasterAndData = generateAndSignPaymasterData( + userOp, + PAYMASTER_SIGNER, + bicoPaymaster, + uint128(validationGasLimit), + uint128(postopGasLimit), + DAPP_ACCOUNT.addr, + validUntil, + validAfter, + premium + ); + userOp.signature = signUserOp(ALICE, userOp); + ops[0] = userOp; + userOpHash = ENTRYPOINT.getUserOpHash(userOp); + + uint256 initialFeeCollectorBalance = bicoPaymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); + initialDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + vm.expectEmit(true, false, false, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.PremiumCollected(DAPP_ACCOUNT.addr, 0); + vm.expectEmit(true, false, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + + uint256 resultingDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + uint256 resultingFeeCollectorPaymasterBalance = bicoPaymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); + + uint256 totalGasFeesCharged = initialDappPaymasterBalance - resultingDappPaymasterBalance; + uint256 premiumCollected = resultingFeeCollectorPaymasterBalance - initialFeeCollectorBalance; + + uint256 expectedPremium = totalGasFeesCharged - (totalGasFeesCharged * 1e6) / premium; + + console2.log(premiumCollected); + console2.log(expectedPremium); + console2.log(stdMath.percentDelta(premiumCollected, expectedPremium)); + // less than 0.01% difference between actual and expected values + assert(stdMath.percentDelta(premiumCollected, expectedPremium) < 1e14); + } } From 1d9605317bad9ffb75b36750e83c09cfce1f590c Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Sun, 7 Jul 2024 22:16:29 +0400 Subject: [PATCH 35/69] some cleanup --- ...estFuzz_TestSponsorshipPaymasterWithPremium.t.sol | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol index 98fe11d..25fe3d1 100644 --- a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: Unlicensed pragma solidity ^0.8.26; -import { console2 } from "forge-std/src/console2.sol"; -import { stdMath } from "forge-std/src/Test.sol"; import { NexusTestBase } from "../../base/NexusTestBase.sol"; import { IBiconomySponsorshipPaymaster } from "../../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; import { BiconomySponsorshipPaymaster } from "../../../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; @@ -152,14 +150,10 @@ contract TestFuzz_SponsorshipPaymasterWithPremium is NexusTestBase { uint256 resultingFeeCollectorPaymasterBalance = bicoPaymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); uint256 totalGasFeesCharged = initialDappPaymasterBalance - resultingDappPaymasterBalance; - uint256 premiumCollected = resultingFeeCollectorPaymasterBalance - initialFeeCollectorBalance; - uint256 expectedPremium = totalGasFeesCharged - (totalGasFeesCharged * 1e6) / premium; + uint256 premiumCollected = resultingFeeCollectorPaymasterBalance - initialFeeCollectorBalance; + uint256 expectedPremium = totalGasFeesCharged - ((totalGasFeesCharged * 1e6) / premium); - console2.log(premiumCollected); - console2.log(expectedPremium); - console2.log(stdMath.percentDelta(premiumCollected, expectedPremium)); - // less than 0.01% difference between actual and expected values - assert(stdMath.percentDelta(premiumCollected, expectedPremium) < 1e14); + assertEq(premiumCollected, expectedPremium); } } From 40aed7e03b9504162655dae91533549712cc08b1 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Mon, 8 Jul 2024 11:46:24 +0400 Subject: [PATCH 36/69] make tests more modular --- .../SponsorshipPaymasterWithPremium.sol | 22 ++-- test/foundry/base/NexusTestBase.sol | 62 +++++++++++ ...tSponsorshipPaymasterWithPremiumTest.t.sol | 102 ++++-------------- ..._TestSponsorshipPaymasterWithPremium.t.sol | 49 ++------- 4 files changed, 101 insertions(+), 134 deletions(-) diff --git a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol b/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol index 5b06711..e63b539 100644 --- a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol +++ b/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol @@ -234,23 +234,23 @@ contract BiconomySponsorshipPaymaster is /// @param actualGasCost The actual gas cost of the transaction. function _postOp(PostOpMode, bytes calldata context, uint256 actualGasCost, uint256) internal override { unchecked { - (address paymasterId, uint32 dynamicMarkup, bytes32 userOpHash) = + (address paymasterId, uint32 dynamicAdjustment, bytes32 userOpHash) = abi.decode(context, (address, uint32, bytes32)); - uint256 costIncludingPremium = (actualGasCost * dynamicMarkup) / PRICE_DENOMINATOR; + uint256 adjustedGasCost = (actualGasCost * dynamicAdjustment) / PRICE_DENOMINATOR; - // deduct with premium - paymasterIdBalances[paymasterId] -= costIncludingPremium; + // Deduct the adjusted cost + paymasterIdBalances[paymasterId] -= adjustedGasCost; - if (costIncludingPremium > actualGasCost) { - // "collect" premium - uint256 actualPremium = costIncludingPremium - actualGasCost; - paymasterIdBalances[feeCollector] += actualPremium; - // Review if we should emit balToDeduct as well - emit PremiumCollected(paymasterId, actualPremium); + if (adjustedGasCost > actualGasCost) { + // Add premium to fee + uint256 premium = adjustedGasCost - actualGasCost; + paymasterIdBalances[feeCollector] += premium; + // Review if we should emit adjustedGasCost as well + emit PremiumCollected(paymasterId, premium); } - emit GasBalanceDeducted(paymasterId, costIncludingPremium, userOpHash); + emit GasBalanceDeducted(paymasterId, adjustedGasCost, userOpHash); } } diff --git a/test/foundry/base/NexusTestBase.sol b/test/foundry/base/NexusTestBase.sol index cd7dbaf..dbe176c 100644 --- a/test/foundry/base/NexusTestBase.sol +++ b/test/foundry/base/NexusTestBase.sol @@ -351,6 +351,46 @@ abstract contract NexusTestBase is CheatCodes, BaseEventsAndErrors { postopGasLimit = postopGasLimit - gasleft(); } + function createUserOp( + Vm.Wallet memory sender, + BiconomySponsorshipPaymaster paymaster, + uint32 premium + ) + internal + returns (PackedUserOperation memory userOp, bytes32 userOpHash) + { + // Create userOp with no paymaster gas estimates + uint48 validUntil = uint48(block.timestamp + 1 days); + uint48 validAfter = uint48(block.timestamp); + + userOp = buildUserOpWithCalldata(sender, "", address(VALIDATOR_MODULE)); + + userOp.paymasterAndData = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, paymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, premium + ); + userOp.signature = signUserOp(sender, userOp); + + // Estimate paymaster gas limits + userOpHash = ENTRYPOINT.getUserOpHash(userOp); + (uint256 validationGasLimit, uint256 postopGasLimit) = + estimatePaymasterGasCosts(paymaster, userOp, userOpHash, 5e4); + + // Ammend the userop to have new gas limits and signature + userOp.paymasterAndData = generateAndSignPaymasterData( + userOp, + PAYMASTER_SIGNER, + paymaster, + uint128(validationGasLimit), + uint128(postopGasLimit), + DAPP_ACCOUNT.addr, + validUntil, + validAfter, + premium + ); + userOp.signature = signUserOp(sender, userOp); + userOpHash = ENTRYPOINT.getUserOpHash(userOp); + } + /// @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. @@ -417,4 +457,26 @@ abstract contract NexusTestBase is CheatCodes, BaseEventsAndErrors { } return result; } + + function getPremiums( + BiconomySponsorshipPaymaster paymaster, + uint256 initialDappPaymasterBalance, + uint256 initialFeeCollectorBalance, + uint32 premium + ) + internal + view + returns (uint256 expectedPremium, uint256 actualPremium) + { + uint256 resultingDappPaymasterBalance = paymaster.getBalance(DAPP_ACCOUNT.addr); + uint256 resultingFeeCollectorPaymasterBalance = paymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); + + uint256 totalGasFeesCharged = initialDappPaymasterBalance - resultingDappPaymasterBalance; + + if (premium >= 1e6) { + //premium + expectedPremium = totalGasFeesCharged - ((totalGasFeesCharged * 1e6) / premium); + actualPremium = resultingFeeCollectorPaymasterBalance - initialFeeCollectorBalance; + } else revert("Premium must be more than 1e6"); + } } diff --git a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol index 0f6ed46..a1df8d2 100644 --- a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol +++ b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol @@ -134,94 +134,40 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { bicoPaymaster.deposit{ value: 1 ether }(); } - function test_WithdrawTo() external prankModifier(DAPP_ACCOUNT.addr) { - 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)); - - // No premium + function test_ValidatePaymasterAndPostOpWithoutPremium() external prankModifier(DAPP_ACCOUNT.addr) { + bicoPaymaster.depositFor{ value: 10 ether }(DAPP_ACCOUNT.addr); + // No premoium uint32 premium = 1e6; - userOp.paymasterAndData = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, premium - ); - userOp.signature = signUserOp(ALICE, userOp); - - // Estimate paymaster gas limits - bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); - (uint256 validationGasLimit, uint256 postopGasLimit) = - estimatePaymasterGasCosts(bicoPaymaster, userOp, userOpHash, 5e4); - // Ammend the userop to have new gas limits and signature - userOp.paymasterAndData = generateAndSignPaymasterData( - userOp, - PAYMASTER_SIGNER, - bicoPaymaster, - uint128(validationGasLimit), - uint128(postopGasLimit), - DAPP_ACCOUNT.addr, - validUntil, - validAfter, - premium - ); - userOp.signature = signUserOp(ALICE, userOp); + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, premium); ops[0] = userOp; - userOpHash = ENTRYPOINT.getUserOpHash(userOp); + + uint256 initialDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + uint256 initialFeeCollectorBalance = bicoPaymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); vm.expectEmit(true, false, true, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - uint256 resultingDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); - assertNotEq(initialDappPaymasterBalance, resultingDappPaymasterBalance); + (uint256 expectedPremium, uint256 actualPremium) = + getPremiums(bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, premium); + + assertEq(expectedPremium, actualPremium); } function test_ValidatePaymasterAndPostOpWithPremium() 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)); - - // Charge a 10% premium + bicoPaymaster.depositFor{ value: 10 ether }(DAPP_ACCOUNT.addr); + // 10% premium on gas cost uint32 premium = 1e6 + 1e5; - userOp.paymasterAndData = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, premium - ); - userOp.signature = signUserOp(ALICE, userOp); - - // Estimate paymaster gas limits - bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); - (uint256 validationGasLimit, uint256 postopGasLimit) = - estimatePaymasterGasCosts(bicoPaymaster, userOp, userOpHash, 5e4); - // Ammend the userop to have new gas limits and signature - userOp.paymasterAndData = generateAndSignPaymasterData( - userOp, - PAYMASTER_SIGNER, - bicoPaymaster, - uint128(validationGasLimit), - uint128(postopGasLimit), - DAPP_ACCOUNT.addr, - validUntil, - validAfter, - premium - ); - userOp.signature = signUserOp(ALICE, userOp); + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, premium); ops[0] = userOp; - userOpHash = ENTRYPOINT.getUserOpHash(userOp); + uint256 initialDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); uint256 initialFeeCollectorBalance = bicoPaymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); - initialDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + // submit userops vm.expectEmit(true, false, false, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.PremiumCollected(DAPP_ACCOUNT.addr, 0); @@ -229,16 +175,10 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - uint256 resultingDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); - uint256 resultingFeeCollectorPaymasterBalance = bicoPaymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); - - uint256 totalGasFeesCharged = initialDappPaymasterBalance - resultingDappPaymasterBalance; - uint256 premiumCollected = resultingFeeCollectorPaymasterBalance - initialFeeCollectorBalance; - - uint256 expectedGasPayment = totalGasFeesCharged - premiumCollected; - uint256 expectedPremium = expectedGasPayment / 10; + (uint256 expectedPremium, uint256 actualPremium) = + getPremiums(bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, premium); - assertEq(premiumCollected, expectedPremium); + assertEq(expectedPremium, actualPremium); } function test_RevertIf_ValidatePaymasterUserOpWithIncorrectSignatureLength() external { diff --git a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol index 25fe3d1..11b43e4 100644 --- a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol @@ -101,59 +101,24 @@ contract TestFuzz_SponsorshipPaymasterWithPremium is NexusTestBase { function testFuzz_ValidatePaymasterAndPostOpWithPremium(uint32 premium) external { vm.assume(premium <= 2e6); vm.assume(premium > 1e6); - - uint256 initialDappPaymasterBalance = 10 ether; - bicoPaymaster.depositFor{ value: initialDappPaymasterBalance }(DAPP_ACCOUNT.addr); + bicoPaymaster.depositFor{ value: 10 ether }(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, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, premium - ); - userOp.signature = signUserOp(ALICE, userOp); - - // Estimate paymaster gas limits - bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); - (uint256 validationGasLimit, uint256 postopGasLimit) = - estimatePaymasterGasCosts(bicoPaymaster, userOp, userOpHash, 5e4); - - // Ammend the userop to have new gas limits and signature - userOp.paymasterAndData = generateAndSignPaymasterData( - userOp, - PAYMASTER_SIGNER, - bicoPaymaster, - uint128(validationGasLimit), - uint128(postopGasLimit), - DAPP_ACCOUNT.addr, - validUntil, - validAfter, - premium - ); - userOp.signature = signUserOp(ALICE, userOp); + (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, premium); ops[0] = userOp; - userOpHash = ENTRYPOINT.getUserOpHash(userOp); uint256 initialFeeCollectorBalance = bicoPaymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); - initialDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + uint256 initialDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); vm.expectEmit(true, false, false, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.PremiumCollected(DAPP_ACCOUNT.addr, 0); vm.expectEmit(true, false, true, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - uint256 resultingDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); - uint256 resultingFeeCollectorPaymasterBalance = bicoPaymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); - - uint256 totalGasFeesCharged = initialDappPaymasterBalance - resultingDappPaymasterBalance; + (uint256 expectedPremium, uint256 actualPremium) = + getPremiums(bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, premium); - uint256 premiumCollected = resultingFeeCollectorPaymasterBalance - initialFeeCollectorBalance; - uint256 expectedPremium = totalGasFeesCharged - ((totalGasFeesCharged * 1e6) / premium); - - assertEq(premiumCollected, expectedPremium); + assertEq(expectedPremium, actualPremium); } + } From 81abba53782efbd1111888b0fc1deeb0df85aebf Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Tue, 9 Jul 2024 16:48:47 +0400 Subject: [PATCH 37/69] postop cost added back and more tests added --- contracts/common/Errors.sol | 5 +++ .../SponsorshipPaymasterWithPremium.sol | 28 +++++++++++- test/foundry/base/NexusTestBase.sol | 4 +- ...tSponsorshipPaymasterWithPremiumTest.t.sol | 43 +++++++++++++++++++ 4 files changed, 77 insertions(+), 3 deletions(-) diff --git a/contracts/common/Errors.sol b/contracts/common/Errors.sol index 58bf40f..c72ea2e 100644 --- a/contracts/common/Errors.sol +++ b/contracts/common/Errors.sol @@ -66,4 +66,9 @@ contract BiconomySponsorshipPaymasterErrors { * @notice Throws when trying to withdraw to address(0) */ error CanNotWithdrawToZeroAddress(); + + /** + * @notice Throws when trying postOpCost is too high + */ + error PostOpCostTooHigh(); } diff --git a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol b/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol index e63b539..be63de2 100644 --- a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol +++ b/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol @@ -39,6 +39,7 @@ contract BiconomySponsorshipPaymaster is address public verifyingSigner; address public feeCollector; + uint48 public postOpCost; uint32 private constant PRICE_DENOMINATOR = 1e6; // note: could rename to PAYMASTER_ID_OFFSET @@ -119,6 +120,20 @@ contract BiconomySponsorshipPaymaster is emit FeeCollectorChanged(oldFeeCollector, _newFeeCollector, msg.sender); } + /** + * @dev Set a new unaccountedEPGasOverhead value. + * @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 { + if (value > 200_000) { + revert PostOpCostTooHigh(); + } + uint256 oldValue = postOpCost; + postOpCost = value; + emit PostopCostChanged(oldValue, value); + } + /** * @dev Override the default implementation. */ @@ -232,12 +247,21 @@ contract BiconomySponsorshipPaymaster is /// @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. - function _postOp(PostOpMode, bytes calldata context, uint256 actualGasCost, uint256) internal override { + function _postOp( + PostOpMode, + bytes calldata context, + uint256 actualGasCost, + uint256 actualUserOpFeePerGas + ) + internal + override + { unchecked { (address paymasterId, uint32 dynamicAdjustment, bytes32 userOpHash) = abi.decode(context, (address, uint32, bytes32)); - uint256 adjustedGasCost = (actualGasCost * dynamicAdjustment) / PRICE_DENOMINATOR; + uint256 totalGasCost = actualGasCost + (postOpCost * actualUserOpFeePerGas); + uint256 adjustedGasCost = (totalGasCost * dynamicAdjustment) / PRICE_DENOMINATOR; // Deduct the adjusted cost paymasterIdBalances[paymasterId] -= adjustedGasCost; diff --git a/test/foundry/base/NexusTestBase.sol b/test/foundry/base/NexusTestBase.sol index dbe176c..f94712f 100644 --- a/test/foundry/base/NexusTestBase.sol +++ b/test/foundry/base/NexusTestBase.sol @@ -477,6 +477,8 @@ abstract contract NexusTestBase is CheatCodes, BaseEventsAndErrors { //premium expectedPremium = totalGasFeesCharged - ((totalGasFeesCharged * 1e6) / premium); actualPremium = resultingFeeCollectorPaymasterBalance - initialFeeCollectorBalance; - } else revert("Premium must be more than 1e6"); + } else { + revert("Premium must be more than 1e6"); + } } } diff --git a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol index a1df8d2..18853c9 100644 --- a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol +++ b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol @@ -106,6 +106,24 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { bicoPaymaster.setFeeCollector(DAN_ADDRESS); } + function test_SetPostopCost() external prankModifier(PAYMASTER_OWNER.addr) { + uint48 initialPostopCost = bicoPaymaster.postOpCost(); + uint48 newPostopCost = 5_000; + + vm.expectEmit(true, true, false, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.PostopCostChanged(initialPostopCost, newPostopCost); + bicoPaymaster.setPostopCost(newPostopCost); + + uint48 resultingPostopCost = bicoPaymaster.postOpCost(); + assertEq(resultingPostopCost, newPostopCost); + } + + function test_RevertIf_SetPostopCostToHigh() external prankModifier(PAYMASTER_OWNER.addr) { + uint48 newPostopCost = 200_001; + vm.expectRevert(abi.encodeWithSelector(PostOpCostTooHigh.selector)); + bicoPaymaster.setPostopCost(newPostopCost); + } + function test_DepositFor() external { uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); uint256 depositAmount = 10 ether; @@ -134,6 +152,31 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { bicoPaymaster.deposit{ value: 1 ether }(); } + 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.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); + } + + function test_RevertIf_WithdrawToZeroAddress() external prankModifier(DAPP_ACCOUNT.addr) { + vm.expectRevert(abi.encodeWithSelector(CanNotWithdrawToZeroAddress.selector)); + bicoPaymaster.withdrawTo(payable(address(0)), 0 ether); + } + + function test_RevertIf_WithdrawToExceedsBalance() external prankModifier(DAPP_ACCOUNT.addr) { + vm.expectRevert(abi.encodeWithSelector(InsufficientFundsInGasTank.selector)); + bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), 1 ether); + } + function test_ValidatePaymasterAndPostOpWithoutPremium() external prankModifier(DAPP_ACCOUNT.addr) { bicoPaymaster.depositFor{ value: 10 ether }(DAPP_ACCOUNT.addr); // No premoium From abff5d2336b493457b04657cef4c66fe7f5cecf2 Mon Sep 17 00:00:00 2001 From: livingrockrises <90545960+livingrockrises@users.noreply.github.com> Date: Tue, 9 Jul 2024 19:02:21 +0530 Subject: [PATCH 38/69] fix postOpCost --- .../concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol index a66c921..df0f9df 100644 --- a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol +++ b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol @@ -27,7 +27,7 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { assertEq(address(testArtifact.entryPoint()), ENTRYPOINT_ADDRESS); assertEq(testArtifact.verifyingSigner(), PAYMASTER_SIGNER.addr); assertEq(testArtifact.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); - assertEq(testArtifact.postopCost(), 0 wei); + assertEq(testArtifact.postOpCost(), 0 wei); } function test_RevertIf_DeployWithSignerSetToZero() external { @@ -45,7 +45,7 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { assertEq(address(bicoPaymaster.entryPoint()), ENTRYPOINT_ADDRESS); assertEq(bicoPaymaster.verifyingSigner(), PAYMASTER_SIGNER.addr); assertEq(bicoPaymaster.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); - assertEq(bicoPaymaster.postopCost(), 0 wei); + assertEq(bicoPaymaster.postOpCost(), 0 wei); } function test_OwnershipTransfer() external prankModifier(PAYMASTER_OWNER.addr) { From f5acd1adb4eff20fb3f86ad2ae6393a12b35dc90 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Wed, 10 Jul 2024 12:21:32 +0400 Subject: [PATCH 39/69] parse paymaster data test --- contracts/base/BasePaymaster.sol | 8 ++ contracts/common/Errors.sol | 6 +- .../IBiconomySponsorshipPaymaster.sol | 4 +- ...sorshipPaymasterWithDynamicAdjustment.sol} | 54 ++++++------- .../base/{NexusTestBase.sol => TestBase.sol} | 53 ++++++------ ...tSponsorshipPaymasterWithPremiumTest.t.sol | 80 +++++++++++++------ ..._TestSponsorshipPaymasterWithPremium.t.sol | 44 +++++++--- 7 files changed, 150 insertions(+), 99 deletions(-) rename contracts/sponsorship/{SponsorshipPaymasterWithPremium.sol => SponsorshipPaymasterWithDynamicAdjustment.sol} (89%) rename test/foundry/base/{NexusTestBase.sol => TestBase.sol} (93%) diff --git a/contracts/base/BasePaymaster.sol b/contracts/base/BasePaymaster.sol index b3d487a..fad82e2 100644 --- a/contracts/base/BasePaymaster.sol +++ b/contracts/base/BasePaymaster.sol @@ -103,6 +103,14 @@ abstract contract BasePaymaster is IPaymaster, SoladyOwnable { return entryPoint.balanceOf(address(this)); } + function isContract(address _addr) internal view returns (bool) { + uint256 size; + assembly ("memory-safe") { + size := extcodesize(_addr) + } + return size > 0; + } + //sanity check: make sure this EntryPoint was compiled against the same // IEntryPoint of this paymaster function _validateEntryPointInterface(IEntryPoint _entryPoint) internal virtual { diff --git a/contracts/common/Errors.sol b/contracts/common/Errors.sol index c72ea2e..fdaddb8 100644 --- a/contracts/common/Errors.sol +++ b/contracts/common/Errors.sol @@ -50,7 +50,7 @@ contract BiconomySponsorshipPaymasterErrors { /** * @notice Throws when invalid signature length in paymasterAndData */ - error InvalidPriceMarkup(); + error InvalidDynamicAdjustment(); /** * @notice Throws when insufficient funds for paymasterid @@ -68,7 +68,7 @@ contract BiconomySponsorshipPaymasterErrors { error CanNotWithdrawToZeroAddress(); /** - * @notice Throws when trying postOpCost is too high + * @notice Throws when trying unaccountedGas is too high */ - error PostOpCostTooHigh(); + error UnaccountedGasTooHigh(); } diff --git a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol index 5f47d1a..0f2cfac 100644 --- a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol +++ b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.26; interface IBiconomySponsorshipPaymaster { event PostopCostChanged(uint256 indexed oldValue, uint256 indexed newValue); - event FixedPriceMarkupChanged(uint32 indexed oldValue, uint32 indexed newValue); + event FixedDynamicAdjustmentChanged(uint32 indexed oldValue, uint32 indexed newValue); event VerifyingSignerChanged(address indexed oldSigner, address indexed newSigner, address indexed actor); @@ -11,7 +11,7 @@ interface IBiconomySponsorshipPaymaster { event GasDeposited(address indexed paymasterId, uint256 indexed value); event GasWithdrawn(address indexed paymasterId, address indexed to, uint256 indexed value); event GasBalanceDeducted(address indexed paymasterId, uint256 indexed charge, bytes32 indexed userOpHash); - event PremiumCollected(address indexed paymasterId, uint256 indexed premium); + event DynamicAdjustmentCollected(address indexed paymasterId, uint256 indexed dynamicAdjustment); event Received(address indexed sender, uint256 value); event TokensWithdrawn(address indexed token, address indexed to, uint256 indexed amount, address actor); } diff --git a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol b/contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol similarity index 89% rename from contracts/sponsorship/SponsorshipPaymasterWithPremium.sol rename to contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol index 10aa259..a85bcc2 100644 --- a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol +++ b/contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol @@ -39,7 +39,7 @@ contract BiconomySponsorshipPaymaster is address public verifyingSigner; address public feeCollector; - uint48 public postOpCost; + uint48 public unaccountedGas; uint32 private constant PRICE_DENOMINATOR = 1e6; // note: could rename to PAYMASTER_ID_OFFSET @@ -89,16 +89,12 @@ contract BiconomySponsorshipPaymaster is * After setting the new signer address, it will emit an event VerifyingSignerChanged. */ function setSigner(address _newVerifyingSigner) external payable onlyOwner { - uint256 size; - assembly { - size := extcodesize(_newVerifyingSigner) - } - if (size > 0) revert VerifyingSignerCanNotBeContract(); + if (isContract(_newVerifyingSigner)) revert VerifyingSignerCanNotBeContract(); if (_newVerifyingSigner == address(0)) { revert VerifyingSignerCanNotBeZero(); } address oldSigner = verifyingSigner; - assembly { + assembly ("memory-safe") { sstore(verifyingSigner.slot, _newVerifyingSigner) } emit VerifyingSignerChanged(oldSigner, _newVerifyingSigner, msg.sender); @@ -114,9 +110,7 @@ contract BiconomySponsorshipPaymaster is function setFeeCollector(address _newFeeCollector) external payable onlyOwner { if (_newFeeCollector == address(0)) revert FeeCollectorCanNotBeZero(); address oldFeeCollector = feeCollector; - assembly { - sstore(feeCollector.slot, _newFeeCollector) - } + feeCollector = _newFeeCollector; emit FeeCollectorChanged(oldFeeCollector, _newFeeCollector, msg.sender); } @@ -127,10 +121,10 @@ contract BiconomySponsorshipPaymaster is */ function setPostopCost(uint48 value) external payable onlyOwner { if (value > 200_000) { - revert PostOpCostTooHigh(); + revert UnaccountedGasTooHigh(); } - uint256 oldValue = postOpCost; - postOpCost = value; + uint256 oldValue = unaccountedGas; + unaccountedGas = value; emit PostopCostChanged(oldValue, value); } @@ -195,7 +189,7 @@ contract BiconomySponsorshipPaymaster is address paymasterId, uint48 validUntil, uint48 validAfter, - uint32 priceMarkup + uint32 dynamicAdjustment ) public view @@ -218,7 +212,7 @@ contract BiconomySponsorshipPaymaster is paymasterId, validUntil, validAfter, - priceMarkup + dynamicAdjustment ) ); } @@ -230,7 +224,7 @@ contract BiconomySponsorshipPaymaster is address paymasterId, uint48 validUntil, uint48 validAfter, - uint32 priceMarkup, + uint32 dynamicAdjustment, bytes calldata signature ) { @@ -238,7 +232,7 @@ contract BiconomySponsorshipPaymaster is 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])); + dynamicAdjustment = uint32(bytes4(paymasterAndData[VALID_PND_OFFSET + 32:VALID_PND_OFFSET + 36])); signature = paymasterAndData[VALID_PND_OFFSET + 36:]; } } @@ -260,18 +254,18 @@ contract BiconomySponsorshipPaymaster is (address paymasterId, uint32 dynamicAdjustment, bytes32 userOpHash) = abi.decode(context, (address, uint32, bytes32)); - uint256 totalGasCost = actualGasCost + (postOpCost * actualUserOpFeePerGas); + uint256 totalGasCost = actualGasCost + (unaccountedGas * actualUserOpFeePerGas); uint256 adjustedGasCost = (totalGasCost * dynamicAdjustment) / PRICE_DENOMINATOR; // Deduct the adjusted cost paymasterIdBalances[paymasterId] -= adjustedGasCost; if (adjustedGasCost > actualGasCost) { - // Add premium to fee - uint256 premium = adjustedGasCost - actualGasCost; - paymasterIdBalances[feeCollector] += premium; + // Add dynamicAdjustment to fee + uint256 dynamicAdjustment = adjustedGasCost - actualGasCost; + paymasterIdBalances[feeCollector] += dynamicAdjustment; // Review if we should emit adjustedGasCost as well - emit PremiumCollected(paymasterId, premium); + emit DynamicAdjustmentCollected(paymasterId, dynamicAdjustment); } emit GasBalanceDeducted(paymasterId, adjustedGasCost, userOpHash); @@ -285,7 +279,7 @@ contract BiconomySponsorshipPaymaster is * paymasterAndData[52:72] : paymasterId (dappDepositor) * paymasterAndData[72:78] : validUntil * paymasterAndData[78:84] : validAfter - * paymasterAndData[84:88] : priceMarkup + * paymasterAndData[84:88] : dynamicAdjustment * paymasterAndData[88:] : signature */ function _validatePaymasterUserOp( @@ -298,7 +292,7 @@ contract BiconomySponsorshipPaymaster is override returns (bytes memory context, uint256 validationData) { - (address paymasterId, uint48 validUntil, uint48 validAfter, uint32 priceMarkup, bytes calldata signature) = + (address paymasterId, uint48 validUntil, uint48 validAfter, uint32 dynamicAdjustment, 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 @@ -308,7 +302,7 @@ contract BiconomySponsorshipPaymaster is } bool validSig = verifyingSigner.isValidSignatureNow( - ECDSA_solady.toEthSignedMessageHash(getHash(userOp, paymasterId, validUntil, validAfter, priceMarkup)), + ECDSA_solady.toEthSignedMessageHash(getHash(userOp, paymasterId, validUntil, validAfter, dynamicAdjustment)), signature ); @@ -317,21 +311,21 @@ contract BiconomySponsorshipPaymaster is return ("", _packValidationData(true, validUntil, validAfter)); } - if (priceMarkup > 2e6 || priceMarkup == 0) { - revert InvalidPriceMarkup(); + if (dynamicAdjustment > 2e6 || dynamicAdjustment == 0) { + revert InvalidDynamicAdjustment(); } // Send 1e6 for No markup // Send between 0 and 1e6 for discount - uint256 effectiveCost = (requiredPreFund * priceMarkup) / PRICE_DENOMINATOR; + uint256 effectiveCost = (requiredPreFund * dynamicAdjustment) / PRICE_DENOMINATOR; if (effectiveCost > paymasterIdBalances[paymasterId]) { revert InsufficientFundsForPaymasterId(); } - context = abi.encode(paymasterId, priceMarkup, userOpHash); + context = abi.encode(paymasterId, dynamicAdjustment, userOpHash); - context = abi.encode(paymasterId, priceMarkup, userOpHash); + context = abi.encode(paymasterId, dynamicAdjustment, userOpHash); //no need for other on-chain validation: entire UserOp should have been checked // by the external service prior to signing it. diff --git a/test/foundry/base/NexusTestBase.sol b/test/foundry/base/TestBase.sol similarity index 93% rename from test/foundry/base/NexusTestBase.sol rename to test/foundry/base/TestBase.sol index f94712f..82b0f14 100644 --- a/test/foundry/base/NexusTestBase.sol +++ b/test/foundry/base/TestBase.sol @@ -20,9 +20,9 @@ import { Bootstrap, BootstrapConfig } from "nexus/contracts/utils/Bootstrap.sol" import { CheatCodes } from "nexus/test/foundry/utils/CheatCodes.sol"; import { BaseEventsAndErrors } from "./BaseEventsAndErrors.sol"; -import { BiconomySponsorshipPaymaster } from "../../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; +import { BiconomySponsorshipPaymaster } from "../../../contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol"; -abstract contract NexusTestBase is CheatCodes, BaseEventsAndErrors { +abstract contract TestBase is CheatCodes, BaseEventsAndErrors { // ----------------------------------------- // State Variables // ----------------------------------------- @@ -354,7 +354,7 @@ abstract contract NexusTestBase is CheatCodes, BaseEventsAndErrors { function createUserOp( Vm.Wallet memory sender, BiconomySponsorshipPaymaster paymaster, - uint32 premium + uint32 dynamicAdjustment ) internal returns (PackedUserOperation memory userOp, bytes32 userOpHash) @@ -365,8 +365,8 @@ abstract contract NexusTestBase is CheatCodes, BaseEventsAndErrors { userOp = buildUserOpWithCalldata(sender, "", address(VALIDATOR_MODULE)); - userOp.paymasterAndData = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, paymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, premium + (userOp.paymasterAndData, ) = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, paymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, dynamicAdjustment ); userOp.signature = signUserOp(sender, userOp); @@ -376,7 +376,7 @@ abstract contract NexusTestBase is CheatCodes, BaseEventsAndErrors { estimatePaymasterGasCosts(paymaster, userOp, userOpHash, 5e4); // Ammend the userop to have new gas limits and signature - userOp.paymasterAndData = generateAndSignPaymasterData( + (userOp.paymasterAndData, ) = generateAndSignPaymasterData( userOp, PAYMASTER_SIGNER, paymaster, @@ -385,7 +385,7 @@ abstract contract NexusTestBase is CheatCodes, BaseEventsAndErrors { DAPP_ACCOUNT.addr, validUntil, validAfter, - premium + dynamicAdjustment ); userOp.signature = signUserOp(sender, userOp); userOpHash = ENTRYPOINT.getUserOpHash(userOp); @@ -396,7 +396,8 @@ abstract contract NexusTestBase is CheatCodes, BaseEventsAndErrors { /// @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. + /// @return finalPmData Full Pm Data. + /// @return signature Pm Signature on Data. function generateAndSignPaymasterData( PackedUserOperation memory userOp, Vm.Wallet memory signer, @@ -406,11 +407,11 @@ abstract contract NexusTestBase is CheatCodes, BaseEventsAndErrors { address paymasterId, uint48 validUntil, uint48 validAfter, - uint32 priceMarkup + uint32 dynamicAdjustment ) internal view - returns (bytes memory) + returns (bytes memory finalPmData, bytes memory signature) { // Initial paymaster data with zero signature bytes memory initialPmData = abi.encodePacked( @@ -420,7 +421,7 @@ abstract contract NexusTestBase is CheatCodes, BaseEventsAndErrors { paymasterId, validUntil, validAfter, - priceMarkup, + dynamicAdjustment, new bytes(65) // Zero signature ); @@ -428,25 +429,23 @@ abstract contract NexusTestBase is CheatCodes, BaseEventsAndErrors { userOp.paymasterAndData = initialPmData; // Generate hash to be signed - bytes32 paymasterHash = paymaster.getHash(userOp, paymasterId, validUntil, validAfter, priceMarkup); + bytes32 paymasterHash = paymaster.getHash(userOp, paymasterId, validUntil, validAfter, dynamicAdjustment); // Sign the hash - bytes memory paymasterSignature = signMessage(signer, paymasterHash); - require(paymasterSignature.length == 65, "Invalid Paymaster Signature length"); + signature = signMessage(signer, paymasterHash); + require(signature.length == 65, "Invalid Paymaster Signature length"); // Final paymaster data with the actual signature - bytes memory finalPmData = abi.encodePacked( + finalPmData = abi.encodePacked( address(paymaster), paymasterValGasLimit, paymasterPostOpGasLimit, paymasterId, validUntil, validAfter, - priceMarkup, - paymasterSignature + dynamicAdjustment, + signature ); - - return finalPmData; } function excludeLastNBytes(bytes memory data, uint256 n) internal pure returns (bytes memory) { @@ -458,27 +457,27 @@ abstract contract NexusTestBase is CheatCodes, BaseEventsAndErrors { return result; } - function getPremiums( + function getDynamicAdjustments( BiconomySponsorshipPaymaster paymaster, uint256 initialDappPaymasterBalance, uint256 initialFeeCollectorBalance, - uint32 premium + uint32 dynamicAdjustment ) internal view - returns (uint256 expectedPremium, uint256 actualPremium) + returns (uint256 expectedDynamicAdjustment, uint256 actualDynamicAdjustment) { uint256 resultingDappPaymasterBalance = paymaster.getBalance(DAPP_ACCOUNT.addr); uint256 resultingFeeCollectorPaymasterBalance = paymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); uint256 totalGasFeesCharged = initialDappPaymasterBalance - resultingDappPaymasterBalance; - if (premium >= 1e6) { - //premium - expectedPremium = totalGasFeesCharged - ((totalGasFeesCharged * 1e6) / premium); - actualPremium = resultingFeeCollectorPaymasterBalance - initialFeeCollectorBalance; + if (dynamicAdjustment >= 1e6) { + //dynamicAdjustment + expectedDynamicAdjustment = totalGasFeesCharged - ((totalGasFeesCharged * 1e6) / dynamicAdjustment); + actualDynamicAdjustment = resultingFeeCollectorPaymasterBalance - initialFeeCollectorBalance; } else { - revert("Premium must be more than 1e6"); + revert("DynamicAdjustment must be more than 1e6"); } } } diff --git a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol index df0f9df..5733cbe 100644 --- a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol +++ b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol @@ -2,13 +2,14 @@ pragma solidity ^0.8.26; import { console2 } from "forge-std/src/console2.sol"; -import { NexusTestBase } from "../../base/NexusTestBase.sol"; +import { TestBase } from "../../base/TestBase.sol"; import { IBiconomySponsorshipPaymaster } from "../../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; -import { BiconomySponsorshipPaymaster } from "../../../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; +import { BiconomySponsorshipPaymaster } from + "../../../../contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol"; import { PackedUserOperation } from "account-abstraction/contracts/core/UserOperationLib.sol"; import { MockToken } from "./../../../../lib/nexus/contracts/mocks/MockToken.sol"; -contract TestSponsorshipPaymasterWithPremium is NexusTestBase { +contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { BiconomySponsorshipPaymaster public bicoPaymaster; function setUp() public { @@ -27,7 +28,7 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { assertEq(address(testArtifact.entryPoint()), ENTRYPOINT_ADDRESS); assertEq(testArtifact.verifyingSigner(), PAYMASTER_SIGNER.addr); assertEq(testArtifact.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); - assertEq(testArtifact.postOpCost(), 0 wei); + assertEq(testArtifact.unaccountedGas(), 0 wei); } function test_RevertIf_DeployWithSignerSetToZero() external { @@ -45,7 +46,7 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { assertEq(address(bicoPaymaster.entryPoint()), ENTRYPOINT_ADDRESS); assertEq(bicoPaymaster.verifyingSigner(), PAYMASTER_SIGNER.addr); assertEq(bicoPaymaster.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); - assertEq(bicoPaymaster.postOpCost(), 0 wei); + assertEq(bicoPaymaster.unaccountedGas(), 0 wei); } function test_OwnershipTransfer() external prankModifier(PAYMASTER_OWNER.addr) { @@ -109,20 +110,20 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { } function test_SetPostopCost() external prankModifier(PAYMASTER_OWNER.addr) { - uint48 initialPostopCost = bicoPaymaster.postOpCost(); - uint48 newPostopCost = 5_000; + uint48 initialPostopCost = bicoPaymaster.unaccountedGas(); + uint48 newPostopCost = 5000; vm.expectEmit(true, true, false, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.PostopCostChanged(initialPostopCost, newPostopCost); bicoPaymaster.setPostopCost(newPostopCost); - uint48 resultingPostopCost = bicoPaymaster.postOpCost(); + uint48 resultingPostopCost = bicoPaymaster.unaccountedGas(); assertEq(resultingPostopCost, newPostopCost); } function test_RevertIf_SetPostopCostToHigh() external prankModifier(PAYMASTER_OWNER.addr) { uint48 newPostopCost = 200_001; - vm.expectRevert(abi.encodeWithSelector(PostOpCostTooHigh.selector)); + vm.expectRevert(abi.encodeWithSelector(UnaccountedGasTooHigh.selector)); bicoPaymaster.setPostopCost(newPostopCost); } @@ -179,13 +180,13 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), 1 ether); } - function test_ValidatePaymasterAndPostOpWithoutPremium() external prankModifier(DAPP_ACCOUNT.addr) { + function test_ValidatePaymasterAndPostOpWithoutDynamicAdjustment() external prankModifier(DAPP_ACCOUNT.addr) { bicoPaymaster.depositFor{ value: 10 ether }(DAPP_ACCOUNT.addr); // No premoium - uint32 premium = 1e6; + uint32 dynamicAdjustment = 1e6; PackedUserOperation[] memory ops = new PackedUserOperation[](1); - (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, premium); + (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, dynamicAdjustment); ops[0] = userOp; uint256 initialDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); @@ -195,19 +196,20 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - (uint256 expectedPremium, uint256 actualPremium) = - getPremiums(bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, premium); + (uint256 expectedDynamicAdjustment, uint256 actualDynamicAdjustment) = getDynamicAdjustments( + bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, dynamicAdjustment + ); - assertEq(expectedPremium, actualPremium); + assertEq(expectedDynamicAdjustment, actualDynamicAdjustment); } - function test_ValidatePaymasterAndPostOpWithPremium() external { + function test_ValidatePaymasterAndPostOpWithDynamicAdjustment() external { bicoPaymaster.depositFor{ value: 10 ether }(DAPP_ACCOUNT.addr); - // 10% premium on gas cost - uint32 premium = 1e6 + 1e5; + // 10% dynamicAdjustment on gas cost + uint32 dynamicAdjustment = 1e6 + 1e5; PackedUserOperation[] memory ops = new PackedUserOperation[](1); - (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, premium); + (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, dynamicAdjustment); ops[0] = userOp; uint256 initialDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); @@ -215,15 +217,16 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { // submit userops vm.expectEmit(true, false, false, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.PremiumCollected(DAPP_ACCOUNT.addr, 0); + emit IBiconomySponsorshipPaymaster.DynamicAdjustmentCollected(DAPP_ACCOUNT.addr, 0); vm.expectEmit(true, false, true, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - (uint256 expectedPremium, uint256 actualPremium) = - getPremiums(bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, premium); + (uint256 expectedDynamicAdjustment, uint256 actualDynamicAdjustment) = getDynamicAdjustments( + bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, dynamicAdjustment + ); - assertEq(expectedPremium, actualPremium); + assertEq(expectedDynamicAdjustment, actualDynamicAdjustment); } function test_RevertIf_ValidatePaymasterUserOpWithIncorrectSignatureLength() external { @@ -233,7 +236,7 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { uint48 validAfter = uint48(block.timestamp); PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); - userOp.paymasterAndData = generateAndSignPaymasterData( + (userOp.paymasterAndData, ) = generateAndSignPaymasterData( userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 ); userOp.paymasterAndData = excludeLastNBytes(userOp.paymasterAndData, 2); @@ -252,7 +255,7 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { uint48 validAfter = uint48(block.timestamp); PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); - userOp.paymasterAndData = generateAndSignPaymasterData( + (userOp.paymasterAndData, ) = generateAndSignPaymasterData( userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 ); userOp.signature = signUserOp(ALICE, userOp); @@ -270,7 +273,7 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { uint48 validAfter = uint48(block.timestamp); PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); - userOp.paymasterAndData = generateAndSignPaymasterData( + (userOp.paymasterAndData, ) = generateAndSignPaymasterData( userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 ); userOp.signature = signUserOp(ALICE, userOp); @@ -338,4 +341,29 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { vm.expectRevert(abi.encodeWithSelector(CanNotWithdrawToZeroAddress.selector)); bicoPaymaster.withdrawERC20(token, address(0), mintAmount); } + + function test_ParsePaymasterAndData() external view { + address paymasterId = DAPP_ACCOUNT.addr; + uint48 validUntil = uint48(block.timestamp + 1 days); + uint48 validAfter = uint48(block.timestamp); + uint32 dynamicAdjustment = 1e6; + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + (bytes memory paymasterAndData, bytes memory signature) = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, paymasterId, validUntil, validAfter, dynamicAdjustment + ); + + ( + address parsedPaymasterId, + uint48 parsedValidUntil, + uint48 parsedValidAfter, + uint32 parsedDynamicAdjustment, + bytes memory parsedSignature + ) = bicoPaymaster.parsePaymasterAndData(paymasterAndData); + + assertEq(paymasterId, parsedPaymasterId); + assertEq(validUntil, parsedValidUntil); + assertEq(validAfter, parsedValidAfter); + assertEq(dynamicAdjustment, parsedDynamicAdjustment); + assertEq(signature, parsedSignature); + } } diff --git a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol index 11b43e4..1567693 100644 --- a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol @@ -1,13 +1,14 @@ // SPDX-License-Identifier: Unlicensed pragma solidity ^0.8.26; -import { NexusTestBase } from "../../base/NexusTestBase.sol"; +import { TestBase } from "../../base/TestBase.sol"; import { IBiconomySponsorshipPaymaster } from "../../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; -import { BiconomySponsorshipPaymaster } from "../../../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; +import { BiconomySponsorshipPaymaster } from + "../../../../contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol"; import { MockToken } from "./../../../../lib/nexus/contracts/mocks/MockToken.sol"; import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol"; -contract TestFuzz_SponsorshipPaymasterWithPremium is NexusTestBase { +contract TestFuzz_SponsorshipPaymasterWithDynamicAdjustment is TestBase { BiconomySponsorshipPaymaster public bicoPaymaster; function setUp() public { @@ -98,27 +99,48 @@ contract TestFuzz_SponsorshipPaymasterWithPremium is NexusTestBase { assertEq(token.balanceOf(ALICE_ADDRESS), mintAmount); } - function testFuzz_ValidatePaymasterAndPostOpWithPremium(uint32 premium) external { - vm.assume(premium <= 2e6); - vm.assume(premium > 1e6); + function testFuzz_ValidatePaymasterAndPostOpWithDynamicAdjustment(uint32 dynamicAdjustment) external { + vm.assume(dynamicAdjustment <= 2e6); + vm.assume(dynamicAdjustment > 1e6); bicoPaymaster.depositFor{ value: 10 ether }(DAPP_ACCOUNT.addr); PackedUserOperation[] memory ops = new PackedUserOperation[](1); - (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, premium); + (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, dynamicAdjustment); ops[0] = userOp; uint256 initialFeeCollectorBalance = bicoPaymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); uint256 initialDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); vm.expectEmit(true, false, false, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.PremiumCollected(DAPP_ACCOUNT.addr, 0); + emit IBiconomySponsorshipPaymaster.DynamicAdjustmentCollected(DAPP_ACCOUNT.addr, 0); vm.expectEmit(true, false, true, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - (uint256 expectedPremium, uint256 actualPremium) = - getPremiums(bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, premium); + (uint256 expectedDynamicAdjustment, uint256 actualDynamicAdjustment) = getDynamicAdjustments( + bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, dynamicAdjustment + ); - assertEq(expectedPremium, actualPremium); + assertEq(expectedDynamicAdjustment, actualDynamicAdjustment); } + function testFuzz_ParsePaymasterAndData(address paymasterId, uint48 validUntil, uint48 validAfter, uint32 dynamicAdjustment) external view { + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + (bytes memory paymasterAndData, bytes memory signature) = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, paymasterId, validUntil, validAfter, dynamicAdjustment + ); + + ( + address parsedPaymasterId, + uint48 parsedValidUntil, + uint48 parsedValidAfter, + uint32 parsedDynamicAdjustment, + bytes memory parsedSignature + ) = bicoPaymaster.parsePaymasterAndData(paymasterAndData); + + assertEq(paymasterId, parsedPaymasterId); + assertEq(validUntil, parsedValidUntil); + assertEq(validAfter, parsedValidAfter); + assertEq(dynamicAdjustment, parsedDynamicAdjustment); + assertEq(signature, parsedSignature); + } } From ebf05c1f48005d45efd51f7213cd435815d39af7 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Wed, 10 Jul 2024 17:44:32 +0400 Subject: [PATCH 40/69] comprehensive accounting tests --- .../IBiconomySponsorshipPaymaster.sol | 2 +- ...nsorshipPaymasterWithDynamicAdjustment.sol | 31 ++++---- test/foundry/base/TestBase.sol | 67 +++++++++++++---- ...pPaymasterWithDynamicAdjustmentTest.t.sol} | 75 +++++++++++++------ ..._TestSponsorshipPaymasterWithPremium.t.sol | 26 ++++++- 5 files changed, 150 insertions(+), 51 deletions(-) rename test/foundry/unit/concrete/{TestSponsorshipPaymasterWithPremiumTest.t.sol => TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol} (82%) diff --git a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol index 0f2cfac..00a4c39 100644 --- a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol +++ b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.26; interface IBiconomySponsorshipPaymaster { - event PostopCostChanged(uint256 indexed oldValue, uint256 indexed newValue); + event UnaccountedGasChanged(uint256 indexed oldValue, uint256 indexed newValue); event FixedDynamicAdjustmentChanged(uint32 indexed oldValue, uint32 indexed newValue); event VerifyingSignerChanged(address indexed oldSigner, address indexed newSigner, address indexed actor); diff --git a/contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol b/contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol index a85bcc2..ce689cd 100644 --- a/contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol +++ b/contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol @@ -51,7 +51,8 @@ contract BiconomySponsorshipPaymaster is address _owner, IEntryPoint _entryPoint, address _verifyingSigner, - address _feeCollector + address _feeCollector, + uint48 _unaccountedGas ) BasePaymaster(_owner, _entryPoint) { @@ -59,9 +60,12 @@ contract BiconomySponsorshipPaymaster is revert VerifyingSignerCanNotBeZero(); } else if (_feeCollector == address(0)) { revert FeeCollectorCanNotBeZero(); + } else if (_unaccountedGas > 200_000) { + revert UnaccountedGasTooHigh(); } verifyingSigner = _verifyingSigner; feeCollector = _feeCollector; + unaccountedGas = _unaccountedGas; } receive() external payable { @@ -119,13 +123,13 @@ contract BiconomySponsorshipPaymaster is * @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 { + function setUnaccountedGas(uint48 value) external payable onlyOwner { if (value > 200_000) { revert UnaccountedGasTooHigh(); } uint256 oldValue = unaccountedGas; unaccountedGas = value; - emit PostopCostChanged(oldValue, value); + emit UnaccountedGasChanged(oldValue, value); } /** @@ -254,18 +258,21 @@ contract BiconomySponsorshipPaymaster is (address paymasterId, uint32 dynamicAdjustment, bytes32 userOpHash) = abi.decode(context, (address, uint32, bytes32)); - uint256 totalGasCost = actualGasCost + (unaccountedGas * actualUserOpFeePerGas); - uint256 adjustedGasCost = (totalGasCost * dynamicAdjustment) / PRICE_DENOMINATOR; + // Include unaccountedGas since EP doesn't include this in actualGasCost + // unaccountedGas = postOpGas + EP overhead gas + estimated penalty + actualGasCost = actualGasCost + (unaccountedGas * actualUserOpFeePerGas); + // Apply the dynamic adjustment + uint256 adjustedGasCost = (actualGasCost * dynamicAdjustment) / PRICE_DENOMINATOR; // Deduct the adjusted cost paymasterIdBalances[paymasterId] -= adjustedGasCost; if (adjustedGasCost > actualGasCost) { - // Add dynamicAdjustment to fee - uint256 dynamicAdjustment = adjustedGasCost - actualGasCost; - paymasterIdBalances[feeCollector] += dynamicAdjustment; + // Apply dynamicAdjustment to fee collector balance + uint256 premium = adjustedGasCost - actualGasCost; + paymasterIdBalances[feeCollector] += premium; // Review if we should emit adjustedGasCost as well - emit DynamicAdjustmentCollected(paymasterId, dynamicAdjustment); + emit DynamicAdjustmentCollected(paymasterId, premium); } emit GasBalanceDeducted(paymasterId, adjustedGasCost, userOpHash); @@ -292,8 +299,8 @@ contract BiconomySponsorshipPaymaster is override returns (bytes memory context, uint256 validationData) { - (address paymasterId, uint48 validUntil, uint48 validAfter, uint32 dynamicAdjustment, bytes calldata signature) = - parsePaymasterAndData(userOp.paymasterAndData); + (address paymasterId, uint48 validUntil, uint48 validAfter, uint32 dynamicAdjustment, 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" @@ -325,8 +332,6 @@ contract BiconomySponsorshipPaymaster is context = abi.encode(paymasterId, dynamicAdjustment, userOpHash); - context = abi.encode(paymasterId, dynamicAdjustment, 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)); diff --git a/test/foundry/base/TestBase.sol b/test/foundry/base/TestBase.sol index 82b0f14..1cfdf3c 100644 --- a/test/foundry/base/TestBase.sol +++ b/test/foundry/base/TestBase.sol @@ -8,6 +8,8 @@ import "solady/src/utils/ECDSA.sol"; import { EntryPoint } from "account-abstraction/contracts/core/EntryPoint.sol"; import { IEntryPoint } from "account-abstraction/contracts/interfaces/IEntryPoint.sol"; +import { IAccount } from "account-abstraction/contracts/interfaces/IAccount.sol"; +import { Exec } from "account-abstraction/contracts/utils/Exec.sol"; import { IPaymaster } from "account-abstraction/contracts/interfaces/IPaymaster.sol"; import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol"; @@ -20,7 +22,8 @@ import { Bootstrap, BootstrapConfig } from "nexus/contracts/utils/Bootstrap.sol" import { CheatCodes } from "nexus/test/foundry/utils/CheatCodes.sol"; import { BaseEventsAndErrors } from "./BaseEventsAndErrors.sol"; -import { BiconomySponsorshipPaymaster } from "../../../contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol"; +import { BiconomySponsorshipPaymaster } from + "../../../contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol"; abstract contract TestBase is CheatCodes, BaseEventsAndErrors { // ----------------------------------------- @@ -59,9 +62,12 @@ abstract contract TestBase is CheatCodes, BaseEventsAndErrors { BiconomyMetaFactory internal META_FACTORY; MockValidator internal VALIDATOR_MODULE; Nexus internal ACCOUNT_IMPLEMENTATION; - Bootstrap internal BOOTSTRAPPER; + // Used to buffer user op gas limits + // GAS_LIMIT = (ESTIMATED_GAS * GAS_BUFFER_RATIO) / 100 + uint8 private constant GAS_BUFFER_RATIO = 110; + // ----------------------------------------- // Modifiers // ----------------------------------------- @@ -332,23 +338,50 @@ abstract contract TestBase is CheatCodes, BaseEventsAndErrors { assertTrue(res, "Pre-funding account should succeed"); } + function estimateUserOpGasCosts(PackedUserOperation memory userOp) + internal + prankModifier(ENTRYPOINT_ADDRESS) + returns (uint256 verificationGasUsed, uint256 callGasUsed, uint256 verificationGasLimit, uint256 callGasLimit) + { + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + verificationGasUsed = gasleft(); + IAccount(userOp.sender).validateUserOp(userOp, userOpHash, 0); + verificationGasUsed = verificationGasUsed - gasleft(); + + callGasUsed = gasleft(); + bool success = Exec.call(userOp.sender, 0, userOp.callData, 3e6); + callGasUsed = callGasUsed - gasleft(); + assert(success); + + verificationGasLimit = (verificationGasUsed * GAS_BUFFER_RATIO) / 100; + callGasLimit = (callGasUsed * GAS_BUFFER_RATIO) / 100; + } + function estimatePaymasterGasCosts( BiconomySponsorshipPaymaster paymaster, PackedUserOperation memory userOp, - bytes32 userOpHash, uint256 requiredPreFund ) internal prankModifier(ENTRYPOINT_ADDRESS) - returns (uint256 validationGasLimit, uint256 postopGasLimit) + returns (uint256 validationGasUsed, uint256 postopGasUsed, uint256 validationGasLimit, uint256 postopGasLimit) { - validationGasLimit = gasleft(); + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + // Warm up accounts to get more accurate gas estimations (bytes memory context,) = paymaster.validatePaymasterUserOp(userOp, userOpHash, requiredPreFund); - validationGasLimit = validationGasLimit - gasleft(); + paymaster.postOp(IPaymaster.PostOpMode.opSucceeded, context, 1e12, 3e6); + + // Estimate gas used + validationGasUsed = gasleft(); + (context,) = paymaster.validatePaymasterUserOp(userOp, userOpHash, requiredPreFund); + validationGasUsed = validationGasUsed - gasleft(); - postopGasLimit = gasleft(); + postopGasUsed = gasleft(); paymaster.postOp(IPaymaster.PostOpMode.opSucceeded, context, 1e12, 3e6); - postopGasLimit = postopGasLimit - gasleft(); + postopGasUsed = (postopGasUsed - gasleft()); + + validationGasLimit = (validationGasUsed * GAS_BUFFER_RATIO) / 100; + postopGasLimit = (postopGasUsed * GAS_BUFFER_RATIO) / 100; } function createUserOp( @@ -359,24 +392,30 @@ abstract contract TestBase is CheatCodes, BaseEventsAndErrors { internal returns (PackedUserOperation memory userOp, bytes32 userOpHash) { - // Create userOp with no paymaster gas estimates + // Create userOp with no gas estimates uint48 validUntil = uint48(block.timestamp + 1 days); uint48 validAfter = uint48(block.timestamp); userOp = buildUserOpWithCalldata(sender, "", address(VALIDATOR_MODULE)); - (userOp.paymasterAndData, ) = generateAndSignPaymasterData( + (userOp.paymasterAndData,) = generateAndSignPaymasterData( userOp, PAYMASTER_SIGNER, paymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, dynamicAdjustment ); userOp.signature = signUserOp(sender, userOp); + (,, uint256 verificationGasLimit, uint256 callGasLimit) = estimateUserOpGasCosts(userOp); // Estimate paymaster gas limits - userOpHash = ENTRYPOINT.getUserOpHash(userOp); - (uint256 validationGasLimit, uint256 postopGasLimit) = - estimatePaymasterGasCosts(paymaster, userOp, userOpHash, 5e4); + (, uint256 postopGasUsed, uint256 validationGasLimit, uint256 postopGasLimit) = + estimatePaymasterGasCosts(paymaster, userOp, 5e4); + + vm.startPrank(paymaster.owner()); + // Set unaccounted gas to be gas used in postop + 1000 for EP overhead and penalty + paymaster.setUnaccountedGas(uint48(postopGasUsed + 1000)); + vm.stopPrank(); // Ammend the userop to have new gas limits and signature - (userOp.paymasterAndData, ) = generateAndSignPaymasterData( + userOp.accountGasLimits = bytes32(abi.encodePacked(uint128(verificationGasLimit), uint128(callGasLimit))); + (userOp.paymasterAndData,) = generateAndSignPaymasterData( userOp, PAYMASTER_SIGNER, paymaster, diff --git a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol similarity index 82% rename from test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol rename to test/foundry/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol index 5733cbe..dc7bf64 100644 --- a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol +++ b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Unlicensed pragma solidity ^0.8.26; -import { console2 } from "forge-std/src/console2.sol"; import { TestBase } from "../../base/TestBase.sol"; import { IBiconomySponsorshipPaymaster } from "../../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; import { BiconomySponsorshipPaymaster } from @@ -16,29 +15,38 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { setupTestEnvironment(); // Deploy Sponsorship Paymaster bicoPaymaster = new BiconomySponsorshipPaymaster( - PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr + PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr, 7e3 ); } function test_Deploy() external { BiconomySponsorshipPaymaster testArtifact = new BiconomySponsorshipPaymaster( - PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr + PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr, 7e3 ); 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); - assertEq(testArtifact.unaccountedGas(), 0 wei); + assertEq(testArtifact.unaccountedGas(), 7e3); } function test_RevertIf_DeployWithSignerSetToZero() external { vm.expectRevert(abi.encodeWithSelector(VerifyingSignerCanNotBeZero.selector)); - new BiconomySponsorshipPaymaster(PAYMASTER_OWNER.addr, ENTRYPOINT, address(0), PAYMASTER_FEE_COLLECTOR.addr); + new BiconomySponsorshipPaymaster( + PAYMASTER_OWNER.addr, ENTRYPOINT, address(0), PAYMASTER_FEE_COLLECTOR.addr, 7e3 + ); } function test_RevertIf_DeployWithFeeCollectorSetToZero() external { vm.expectRevert(abi.encodeWithSelector(FeeCollectorCanNotBeZero.selector)); - new BiconomySponsorshipPaymaster(PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, address(0)); + new BiconomySponsorshipPaymaster(PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, address(0), 7e3); + } + + function test_RevertIf_DeployWithUnaccountedGasCostTooHigh() external { + vm.expectRevert(abi.encodeWithSelector(UnaccountedGasTooHigh.selector)); + new BiconomySponsorshipPaymaster( + PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr, 200_001 + ); } function test_CheckInitialPaymasterState() external view { @@ -46,7 +54,7 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { assertEq(address(bicoPaymaster.entryPoint()), ENTRYPOINT_ADDRESS); assertEq(bicoPaymaster.verifyingSigner(), PAYMASTER_SIGNER.addr); assertEq(bicoPaymaster.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); - assertEq(bicoPaymaster.unaccountedGas(), 0 wei); + assertEq(bicoPaymaster.unaccountedGas(), 7e3); } function test_OwnershipTransfer() external prankModifier(PAYMASTER_OWNER.addr) { @@ -109,22 +117,22 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { bicoPaymaster.setFeeCollector(DAN_ADDRESS); } - function test_SetPostopCost() external prankModifier(PAYMASTER_OWNER.addr) { - uint48 initialPostopCost = bicoPaymaster.unaccountedGas(); - uint48 newPostopCost = 5000; + function test_SetUnaccountedGas() external prankModifier(PAYMASTER_OWNER.addr) { + uint48 initialUnaccountedGas = bicoPaymaster.unaccountedGas(); + uint48 newUnaccountedGas = 5000; vm.expectEmit(true, true, false, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.PostopCostChanged(initialPostopCost, newPostopCost); - bicoPaymaster.setPostopCost(newPostopCost); + emit IBiconomySponsorshipPaymaster.UnaccountedGasChanged(initialUnaccountedGas, newUnaccountedGas); + bicoPaymaster.setUnaccountedGas(newUnaccountedGas); - uint48 resultingPostopCost = bicoPaymaster.unaccountedGas(); - assertEq(resultingPostopCost, newPostopCost); + uint48 resultingUnaccountedGas = bicoPaymaster.unaccountedGas(); + assertEq(resultingUnaccountedGas, newUnaccountedGas); } - function test_RevertIf_SetPostopCostToHigh() external prankModifier(PAYMASTER_OWNER.addr) { - uint48 newPostopCost = 200_001; + function test_RevertIf_SetUnaccountedGasToHigh() external prankModifier(PAYMASTER_OWNER.addr) { + uint48 newUnaccountedGas = 200_001; vm.expectRevert(abi.encodeWithSelector(UnaccountedGasTooHigh.selector)); - bicoPaymaster.setPostopCost(newPostopCost); + bicoPaymaster.setUnaccountedGas(newUnaccountedGas); } function test_DepositFor() external { @@ -182,13 +190,14 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { function test_ValidatePaymasterAndPostOpWithoutDynamicAdjustment() external prankModifier(DAPP_ACCOUNT.addr) { bicoPaymaster.depositFor{ value: 10 ether }(DAPP_ACCOUNT.addr); - // No premoium + // No adjustment uint32 dynamicAdjustment = 1e6; PackedUserOperation[] memory ops = new PackedUserOperation[](1); (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, dynamicAdjustment); ops[0] = userOp; + uint256 initialBundlerBalance = BUNDLER.addr.balance; uint256 initialDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); uint256 initialFeeCollectorBalance = bicoPaymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); @@ -196,11 +205,22 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + uint256 resultingBundlerBalance = BUNDLER.addr.balance; + uint256 resultingDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + + uint256 gasPaidByDapp = initialDappPaymasterBalance - resultingDappPaymasterBalance; + uint256 totalGasFeePaid = resultingBundlerBalance - initialBundlerBalance; (uint256 expectedDynamicAdjustment, uint256 actualDynamicAdjustment) = getDynamicAdjustments( bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, dynamicAdjustment ); + // Assert that adjustment collected (if any) is correct assertEq(expectedDynamicAdjustment, actualDynamicAdjustment); + // Gas paid by dapp is higher than paymaster + // Guarantees that EP always has sufficient deposit to pay back dapps + assertGt(gasPaidByDapp, totalGasFeePaid); + // Ensure that max 1% difference between total gas paid and gas paid by dapp (from paymaster) + assertApproxEqRel(totalGasFeePaid + actualDynamicAdjustment, gasPaidByDapp, 0.01e18); } function test_ValidatePaymasterAndPostOpWithDynamicAdjustment() external { @@ -212,6 +232,7 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, dynamicAdjustment); ops[0] = userOp; + uint256 initialBundlerBalance = BUNDLER.addr.balance; uint256 initialDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); uint256 initialFeeCollectorBalance = bicoPaymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); @@ -222,11 +243,23 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + uint256 resultingBundlerBalance = BUNDLER.addr.balance; + uint256 resultingDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + + uint256 gasPaidByDapp = initialDappPaymasterBalance - resultingDappPaymasterBalance; + uint256 totalGasFeePaid = resultingBundlerBalance - initialBundlerBalance; (uint256 expectedDynamicAdjustment, uint256 actualDynamicAdjustment) = getDynamicAdjustments( bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, dynamicAdjustment ); + // Assert that adjustment collected (if any) is correct assertEq(expectedDynamicAdjustment, actualDynamicAdjustment); + // Gas paid by dapp is higher than paymaster + // Guarantees that EP always has sufficient deposit to pay back dapps + assertGt(gasPaidByDapp, totalGasFeePaid); + // Ensure that max 1% difference between total gas paid + the adjustment premium and gas paid by dapp (from + // paymaster) + assertApproxEqRel(totalGasFeePaid + actualDynamicAdjustment, gasPaidByDapp, 0.01e18); } function test_RevertIf_ValidatePaymasterUserOpWithIncorrectSignatureLength() external { @@ -236,7 +269,7 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { uint48 validAfter = uint48(block.timestamp); PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); - (userOp.paymasterAndData, ) = generateAndSignPaymasterData( + (userOp.paymasterAndData,) = generateAndSignPaymasterData( userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 ); userOp.paymasterAndData = excludeLastNBytes(userOp.paymasterAndData, 2); @@ -255,7 +288,7 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { uint48 validAfter = uint48(block.timestamp); PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); - (userOp.paymasterAndData, ) = generateAndSignPaymasterData( + (userOp.paymasterAndData,) = generateAndSignPaymasterData( userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 ); userOp.signature = signUserOp(ALICE, userOp); @@ -273,7 +306,7 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { uint48 validAfter = uint48(block.timestamp); PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); - (userOp.paymasterAndData, ) = generateAndSignPaymasterData( + (userOp.paymasterAndData,) = generateAndSignPaymasterData( userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 ); userOp.signature = signUserOp(ALICE, userOp); diff --git a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol index 1567693..a71fccd 100644 --- a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol @@ -15,7 +15,7 @@ contract TestFuzz_SponsorshipPaymasterWithDynamicAdjustment is TestBase { setupTestEnvironment(); // Deploy Sponsorship Paymaster bicoPaymaster = new BiconomySponsorshipPaymaster( - PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr + PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr, 7e3 ); } @@ -108,22 +108,44 @@ contract TestFuzz_SponsorshipPaymasterWithDynamicAdjustment is TestBase { (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, dynamicAdjustment); ops[0] = userOp; + uint256 initialBundlerBalance = BUNDLER.addr.balance; uint256 initialFeeCollectorBalance = bicoPaymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); uint256 initialDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + vm.expectEmit(true, false, false, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.DynamicAdjustmentCollected(DAPP_ACCOUNT.addr, 0); vm.expectEmit(true, false, true, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + uint256 resultingBundlerBalance = BUNDLER.addr.balance; + uint256 resultingDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + + uint256 gasPaidByDapp = initialDappPaymasterBalance - resultingDappPaymasterBalance; + uint256 totalGasFeePaid = resultingBundlerBalance - initialBundlerBalance; (uint256 expectedDynamicAdjustment, uint256 actualDynamicAdjustment) = getDynamicAdjustments( bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, dynamicAdjustment ); + // Assert that adjustment collected (if any) is correct assertEq(expectedDynamicAdjustment, actualDynamicAdjustment); + // Gas paid by dapp is higher than paymaster + // Guarantees that EP always has sufficient deposit to pay back dapps + assertGt(gasPaidByDapp, totalGasFeePaid); + // Ensure that max 1% difference between total gas paid + the adjustment premium and gas paid by dapp (from + // paymaster) + assertApproxEqRel(totalGasFeePaid + actualDynamicAdjustment, gasPaidByDapp, 0.01e18); } - function testFuzz_ParsePaymasterAndData(address paymasterId, uint48 validUntil, uint48 validAfter, uint32 dynamicAdjustment) external view { + function testFuzz_ParsePaymasterAndData( + address paymasterId, + uint48 validUntil, + uint48 validAfter, + uint32 dynamicAdjustment + ) + external + view + { PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); (bytes memory paymasterAndData, bytes memory signature) = generateAndSignPaymasterData( userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, paymasterId, validUntil, validAfter, dynamicAdjustment From 36d854bdd1cb8b640b2e9f2701a5d30d992b89e4 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Wed, 10 Jul 2024 17:46:20 +0400 Subject: [PATCH 41/69] fix-lint --- contracts/base/BasePaymaster.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/base/BasePaymaster.sol b/contracts/base/BasePaymaster.sol index fad82e2..8a6f1a0 100644 --- a/contracts/base/BasePaymaster.sol +++ b/contracts/base/BasePaymaster.sol @@ -103,14 +103,6 @@ abstract contract BasePaymaster is IPaymaster, SoladyOwnable { return entryPoint.balanceOf(address(this)); } - function isContract(address _addr) internal view returns (bool) { - uint256 size; - assembly ("memory-safe") { - size := extcodesize(_addr) - } - return size > 0; - } - //sanity check: make sure this EntryPoint was compiled against the same // IEntryPoint of this paymaster function _validateEntryPointInterface(IEntryPoint _entryPoint) internal virtual { @@ -170,4 +162,12 @@ abstract contract BasePaymaster is IPaymaster, SoladyOwnable { function _requireFromEntryPoint() internal virtual { require(msg.sender == address(entryPoint), "Sender not EntryPoint"); } + + function isContract(address _addr) internal view returns (bool) { + uint256 size; + assembly ("memory-safe") { + size := extcodesize(_addr) + } + return size > 0; + } } From d59cfd5083626331316154d046c2782f85ca92bc Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Wed, 10 Jul 2024 17:50:10 +0400 Subject: [PATCH 42/69] make UNACCOUNTED_GAS_LIMIT a constant --- .../SponsorshipPaymasterWithDynamicAdjustment.sol | 7 +++++-- ...TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol b/contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol index ce689cd..906e9d2 100644 --- a/contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol +++ b/contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol @@ -45,6 +45,9 @@ contract BiconomySponsorshipPaymaster is // note: could rename to PAYMASTER_ID_OFFSET uint256 private constant VALID_PND_OFFSET = PAYMASTER_DATA_OFFSET; + // Limit for unaccounted gas cost + uint16 private constant UNACCOUNTED_GAS_LIMIT = 10_000; + mapping(address => uint256) public paymasterIdBalances; constructor( @@ -60,7 +63,7 @@ contract BiconomySponsorshipPaymaster is revert VerifyingSignerCanNotBeZero(); } else if (_feeCollector == address(0)) { revert FeeCollectorCanNotBeZero(); - } else if (_unaccountedGas > 200_000) { + } else if (_unaccountedGas > UNACCOUNTED_GAS_LIMIT) { revert UnaccountedGasTooHigh(); } verifyingSigner = _verifyingSigner; @@ -124,7 +127,7 @@ contract BiconomySponsorshipPaymaster is * @notice only to be called by the owner of the contract. */ function setUnaccountedGas(uint48 value) external payable onlyOwner { - if (value > 200_000) { + if (value > UNACCOUNTED_GAS_LIMIT) { revert UnaccountedGasTooHigh(); } uint256 oldValue = unaccountedGas; diff --git a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol index dc7bf64..cec6c95 100644 --- a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol +++ b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol @@ -45,7 +45,7 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { function test_RevertIf_DeployWithUnaccountedGasCostTooHigh() external { vm.expectRevert(abi.encodeWithSelector(UnaccountedGasTooHigh.selector)); new BiconomySponsorshipPaymaster( - PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr, 200_001 + PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr, 10_001 ); } @@ -130,7 +130,7 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { } function test_RevertIf_SetUnaccountedGasToHigh() external prankModifier(PAYMASTER_OWNER.addr) { - uint48 newUnaccountedGas = 200_001; + uint48 newUnaccountedGas = 10_001; vm.expectRevert(abi.encodeWithSelector(UnaccountedGasTooHigh.selector)); bicoPaymaster.setUnaccountedGas(newUnaccountedGas); } From 358aeb174945caf8b011776b6d6c2a269c5d1997 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Thu, 11 Jul 2024 19:52:27 +0400 Subject: [PATCH 43/69] added check for bundler balance change equal to paymaster EP balance change --- test/foundry/base/TestBase.sol | 28 +++++++++++ ...ipPaymasterWithDynamicAdjustmentTest.t.sol | 50 +++++++------------ ..._TestSponsorshipPaymasterWithPremium.t.sol | 46 +++++++---------- 3 files changed, 64 insertions(+), 60 deletions(-) diff --git a/test/foundry/base/TestBase.sol b/test/foundry/base/TestBase.sol index 1cfdf3c..69eaceb 100644 --- a/test/foundry/base/TestBase.sol +++ b/test/foundry/base/TestBase.sol @@ -519,4 +519,32 @@ abstract contract TestBase is CheatCodes, BaseEventsAndErrors { revert("DynamicAdjustment must be more than 1e6"); } } + + function calculateAndAssertAdjustments( + BiconomySponsorshipPaymaster bicoPaymaster, + uint256 initialDappPaymasterBalance, + uint256 initialFeeCollectorBalance, + uint256 initialBundlerBalance, + uint256 initialPaymasterEpBalance, + uint32 dynamicAdjustment + ) + internal + { + (uint256 expectedDynamicAdjustment, uint256 actualDynamicAdjustment) = getDynamicAdjustments( + bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, dynamicAdjustment + ); + uint256 totalGasFeePaid = BUNDLER.addr.balance - initialBundlerBalance; + uint256 gasPaidByDapp = initialDappPaymasterBalance - bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + + // Assert that what paymaster paid is the same as what the bundler received + assertEq(totalGasFeePaid, initialPaymasterEpBalance - bicoPaymaster.getDeposit()); + // Assert that adjustment collected (if any) is correct + assertEq(expectedDynamicAdjustment, actualDynamicAdjustment); + // Gas paid by dapp is higher than paymaster + // Guarantees that EP always has sufficient deposit to pay back dapps + assertGt(gasPaidByDapp, BUNDLER.addr.balance - initialBundlerBalance); + // Ensure that max 1% difference between total gas paid + the adjustment premium and gas paid by dapp (from + // paymaster) + assertApproxEqRel(totalGasFeePaid + actualDynamicAdjustment, gasPaidByDapp, 0.01e18); + } } diff --git a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol index cec6c95..a77dbb1 100644 --- a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol +++ b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol @@ -198,29 +198,24 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { ops[0] = userOp; uint256 initialBundlerBalance = BUNDLER.addr.balance; + uint256 initialPaymasterEpBalance = bicoPaymaster.getDeposit(); uint256 initialDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); uint256 initialFeeCollectorBalance = bicoPaymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); + // submit userops vm.expectEmit(true, false, true, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - uint256 resultingBundlerBalance = BUNDLER.addr.balance; - uint256 resultingDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); - - uint256 gasPaidByDapp = initialDappPaymasterBalance - resultingDappPaymasterBalance; - uint256 totalGasFeePaid = resultingBundlerBalance - initialBundlerBalance; - (uint256 expectedDynamicAdjustment, uint256 actualDynamicAdjustment) = getDynamicAdjustments( - bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, dynamicAdjustment + // Calculate and assert dynamic adjustments and gas payments + calculateAndAssertAdjustments( + bicoPaymaster, + initialDappPaymasterBalance, + initialFeeCollectorBalance, + initialBundlerBalance, + initialPaymasterEpBalance, + dynamicAdjustment ); - - // Assert that adjustment collected (if any) is correct - assertEq(expectedDynamicAdjustment, actualDynamicAdjustment); - // Gas paid by dapp is higher than paymaster - // Guarantees that EP always has sufficient deposit to pay back dapps - assertGt(gasPaidByDapp, totalGasFeePaid); - // Ensure that max 1% difference between total gas paid and gas paid by dapp (from paymaster) - assertApproxEqRel(totalGasFeePaid + actualDynamicAdjustment, gasPaidByDapp, 0.01e18); } function test_ValidatePaymasterAndPostOpWithDynamicAdjustment() external { @@ -233,6 +228,7 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { ops[0] = userOp; uint256 initialBundlerBalance = BUNDLER.addr.balance; + uint256 initialPaymasterEpBalance = bicoPaymaster.getDeposit(); uint256 initialDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); uint256 initialFeeCollectorBalance = bicoPaymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); @@ -243,23 +239,15 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - uint256 resultingBundlerBalance = BUNDLER.addr.balance; - uint256 resultingDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); - - uint256 gasPaidByDapp = initialDappPaymasterBalance - resultingDappPaymasterBalance; - uint256 totalGasFeePaid = resultingBundlerBalance - initialBundlerBalance; - (uint256 expectedDynamicAdjustment, uint256 actualDynamicAdjustment) = getDynamicAdjustments( - bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, dynamicAdjustment + // Calculate and assert dynamic adjustments and gas payments + calculateAndAssertAdjustments( + bicoPaymaster, + initialDappPaymasterBalance, + initialFeeCollectorBalance, + initialBundlerBalance, + initialPaymasterEpBalance, + dynamicAdjustment ); - - // Assert that adjustment collected (if any) is correct - assertEq(expectedDynamicAdjustment, actualDynamicAdjustment); - // Gas paid by dapp is higher than paymaster - // Guarantees that EP always has sufficient deposit to pay back dapps - assertGt(gasPaidByDapp, totalGasFeePaid); - // Ensure that max 1% difference between total gas paid + the adjustment premium and gas paid by dapp (from - // paymaster) - assertApproxEqRel(totalGasFeePaid + actualDynamicAdjustment, gasPaidByDapp, 0.01e18); } function test_RevertIf_ValidatePaymasterUserOpWithIncorrectSignatureLength() external { diff --git a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol index a71fccd..5fb3416 100644 --- a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol @@ -20,8 +20,7 @@ contract TestFuzz_SponsorshipPaymasterWithDynamicAdjustment is TestBase { } function testFuzz_DepositFor(uint256 depositAmount) external { - vm.assume(depositAmount <= 1000 ether); - vm.assume(depositAmount > 0 ether); + vm.assume(depositAmount <= 1000 ether && depositAmount > 0 ether); vm.deal(DAPP_ACCOUNT.addr, depositAmount); uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); @@ -36,8 +35,7 @@ contract TestFuzz_SponsorshipPaymasterWithDynamicAdjustment is TestBase { } function testFuzz_WithdrawTo(uint256 withdrawAmount) external prankModifier(DAPP_ACCOUNT.addr) { - vm.assume(withdrawAmount <= 1000 ether); - vm.assume(withdrawAmount > 0 ether); + vm.assume(withdrawAmount <= 1000 ether && withdrawAmount > 0 ether); vm.deal(DAPP_ACCOUNT.addr, withdrawAmount); bicoPaymaster.depositFor{ value: withdrawAmount }(DAPP_ACCOUNT.addr); @@ -54,8 +52,7 @@ contract TestFuzz_SponsorshipPaymasterWithDynamicAdjustment is TestBase { } function testFuzz_Receive(uint256 ethAmount) external prankModifier(ALICE_ADDRESS) { - vm.assume(ethAmount <= 1000 ether); - vm.assume(ethAmount > 0 ether); + vm.assume(ethAmount <= 1000 ether && ethAmount > 0 ether); uint256 initialPaymasterBalance = address(bicoPaymaster).balance; vm.expectEmit(true, true, false, true, address(bicoPaymaster)); @@ -68,8 +65,7 @@ contract TestFuzz_SponsorshipPaymasterWithDynamicAdjustment is TestBase { } function testFuzz_WithdrawEth(uint256 ethAmount) external prankModifier(PAYMASTER_OWNER.addr) { - vm.assume(ethAmount <= 1000 ether); - vm.assume(ethAmount > 0 ether); + vm.assume(ethAmount <= 1000 ether && ethAmount > 0 ether); vm.deal(address(bicoPaymaster), ethAmount); uint256 initialAliceBalance = ALICE_ADDRESS.balance; @@ -80,8 +76,7 @@ contract TestFuzz_SponsorshipPaymasterWithDynamicAdjustment is TestBase { } function testFuzz_WithdrawErc20(address target, uint256 amount) external prankModifier(PAYMASTER_OWNER.addr) { - vm.assume(target != address(0)); - vm.assume(amount <= 1_000_000 * (10 ** 18)); + vm.assume(target != address(0) && amount <= 1_000_000 * (10 ** 18)); MockToken token = new MockToken("Token", "TKN"); uint256 mintAmount = amount; token.mint(address(bicoPaymaster), mintAmount); @@ -100,8 +95,7 @@ contract TestFuzz_SponsorshipPaymasterWithDynamicAdjustment is TestBase { } function testFuzz_ValidatePaymasterAndPostOpWithDynamicAdjustment(uint32 dynamicAdjustment) external { - vm.assume(dynamicAdjustment <= 2e6); - vm.assume(dynamicAdjustment > 1e6); + vm.assume(dynamicAdjustment <= 2e6 && dynamicAdjustment > 1e6); bicoPaymaster.depositFor{ value: 10 ether }(DAPP_ACCOUNT.addr); PackedUserOperation[] memory ops = new PackedUserOperation[](1); @@ -109,32 +103,26 @@ contract TestFuzz_SponsorshipPaymasterWithDynamicAdjustment is TestBase { ops[0] = userOp; uint256 initialBundlerBalance = BUNDLER.addr.balance; - uint256 initialFeeCollectorBalance = bicoPaymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); + uint256 initialPaymasterEpBalance = bicoPaymaster.getDeposit(); uint256 initialDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + uint256 initialFeeCollectorBalance = bicoPaymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); + // submit userops vm.expectEmit(true, false, false, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.DynamicAdjustmentCollected(DAPP_ACCOUNT.addr, 0); vm.expectEmit(true, false, true, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - uint256 resultingBundlerBalance = BUNDLER.addr.balance; - uint256 resultingDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); - - uint256 gasPaidByDapp = initialDappPaymasterBalance - resultingDappPaymasterBalance; - uint256 totalGasFeePaid = resultingBundlerBalance - initialBundlerBalance; - (uint256 expectedDynamicAdjustment, uint256 actualDynamicAdjustment) = getDynamicAdjustments( - bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, dynamicAdjustment + // Calculate and assert dynamic adjustments and gas payments + calculateAndAssertAdjustments( + bicoPaymaster, + initialDappPaymasterBalance, + initialFeeCollectorBalance, + initialBundlerBalance, + initialPaymasterEpBalance, + dynamicAdjustment ); - - // Assert that adjustment collected (if any) is correct - assertEq(expectedDynamicAdjustment, actualDynamicAdjustment); - // Gas paid by dapp is higher than paymaster - // Guarantees that EP always has sufficient deposit to pay back dapps - assertGt(gasPaidByDapp, totalGasFeePaid); - // Ensure that max 1% difference between total gas paid + the adjustment premium and gas paid by dapp (from - // paymaster) - assertApproxEqRel(totalGasFeePaid + actualDynamicAdjustment, gasPaidByDapp, 0.01e18); } function testFuzz_ParsePaymasterAndData( From fa787dcbacc3bff4bee271363932d392a0ce2f7c Mon Sep 17 00:00:00 2001 From: livingrockrises <90545960+livingrockrises@users.noreply.github.com> Date: Fri, 12 Jul 2024 01:14:16 +0530 Subject: [PATCH 44/69] remove hardhat stuff --- .github/workflows/ci.yml | 2 +- .github/workflows/coverage.yml | 8 +- .gitignore | 3 - CHANGELOG.md | 8 +- README.md | 18 +- hardhat.config.ts | 26 - package.json | 36 +- .../biconomy-sponsorship-paymaster-specs.ts | 210 -------- test/hardhat/utils/deployment.ts | 156 ------ test/hardhat/utils/general.ts | 60 --- test/hardhat/utils/testUtils.ts | 257 --------- test/hardhat/utils/types.ts | 30 -- test/hardhat/utils/userOpHelpers.ts | 486 ------------------ 13 files changed, 22 insertions(+), 1278 deletions(-) delete mode 100644 hardhat.config.ts delete mode 100644 test/hardhat/biconomy-sponsorship-paymaster-specs.ts delete mode 100644 test/hardhat/utils/deployment.ts delete mode 100644 test/hardhat/utils/general.ts delete mode 100644 test/hardhat/utils/testUtils.ts delete mode 100644 test/hardhat/utils/types.ts delete mode 100644 test/hardhat/utils/userOpHelpers.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d52cb6..9d1dccb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,5 +46,5 @@ jobs: - name: Build Typechain and Foundry run: yarn build - - name: Run Forge and Hardhat Tests + - name: Run Forge Tests run: yarn test diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index d5f509d..d3846a4 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -26,7 +26,7 @@ jobs: - name: Install Foundry Dependencies run: forge install - - name: Generate Hardhat & Foundry Coverage Report + - name: Generate Foundry Coverage Report run: yarn coverage:report - name: Upload Foundry Coverage Report to Codecov @@ -36,9 +36,3 @@ jobs: file: ./coverage/foundry/lcov.info flags: foundry - - name: Upload Hardhat Coverage Report to Codecov - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} - file: ./coverage/lcov.info - flags: hardhat diff --git a/.gitignore b/.gitignore index 9691a6f..52b85d9 100644 --- a/.gitignore +++ b/.gitignore @@ -24,9 +24,6 @@ broadcast/*/31337/ node_modules .env -# Hardhat files -/cache -/artifacts /docs # TypeChain files diff --git a/CHANGELOG.md b/CHANGELOG.md index ab54d1a..e026822 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] -- Initial setup and configurations for both Foundry and Hardhat environments. +- Initial setup and configurations for Foundry. - Integration of GitHub Actions for CI/CD pipelines. - Addition of linter configurations for Solidity and TypeScript. @@ -15,8 +15,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added - `Foo.sol` and `Lock.sol` smart contracts under the `contracts` directory. -- Foundry and Hardhat configurations for building and testing smart contracts. -- Comprehensive testing scripts for Foundry and Hardhat environments in `test/foundry` and `test/hardhat`. +- Foundry configuration for building and testing smart contracts. +- Comprehensive testing scripts for Foundry environment in `test/foundry`. - GitHub Actions workflows for automated testing, linting, and security checks. - Documentation for getting started, usage, and contribution guidelines. @@ -39,4 +39,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added - Initial commit with basic project structure. -- Setup of development environments for Solidity smart contract development using Foundry and Hardhat. +- Setup of development environments for Solidity smart contract development using Foundry. diff --git a/README.md b/README.md index 3bf95a3..0014a1f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -[![Biconomy](https://img.shields.io/badge/Made_with_%F0%9F%8D%8A_by-Biconomy-ff4e17?style=flat)](https://biconomy.io) [![License MIT](https://img.shields.io/badge/License-MIT-blue?&style=flat)](./LICENSE) [![Hardhat](https://img.shields.io/badge/Built%20with-Hardhat-FFDB1C.svg)](https://hardhat.org/) [![Foundry](https://img.shields.io/badge/Built%20with-Foundry-FFBD10.svg)](https://getfoundry.sh/) +[![Biconomy](https://img.shields.io/badge/Made_with_%F0%9F%8D%8A_by-Biconomy-ff4e17?style=flat)](https://biconomy.io) [![License MIT](https://img.shields.io/badge/License-MIT-blue?&style=flat)](./LICENSE) [![Foundry](https://img.shields.io/badge/Built%20with-Foundry-FFBD10.svg)](https://getfoundry.sh/) + -![Codecov Hardhat Coverage](https://img.shields.io/codecov/c/gh/bcnmy/sc-template?token=2BYDIFQ56W&flag=hardhat&label=Hardhat-coverage&logo=codecov) ![Codecov Foundry Coverage](https://img.shields.io/codecov/c/gh/bcnmy/sc-template?token=2BYDIFQ56W&flag=foundry&label=Foundry-coverage&logo=codecov) # Smart Contract Template Base 🚀 @@ -23,14 +23,14 @@ This repository serves as a comprehensive foundation for smart contract projects ## Features - **Smart Contract Template Base**: A robust foundation for future smart contract projects. -- **Hardhat & Foundry Support**: Equipped with both Hardhat and Foundry tools and an adapted folder structure for seamless development. +- **Foundry Support**: Equipped with Foundry tools and an adapted folder structure for seamless development. - **Best Practices**: Adheres to industry best practices in smart contract programming to ensure code quality and security. - **Continuous Integration & Deployment**: Utilizes GitHub Actions for automated testing and deployment, ensuring code reliability. - **Strict Linting**: Implements Solhint based on the Solidity style guide, enhancing code quality and consistency. -- **Comprehensive Testing**: Includes a wide range of tests (unit, fuzz, fork) for both Foundry and Hardhat environments. +- **Comprehensive Testing**: Includes a wide range of tests (unit, fuzz, fork) for both Foundry environment. - **Environment Configuration**: Comes with `.env.example` for easy setup of API keys and environmental variables. - **Code Formatting**: Uses Prettier to maintain a consistent code style across the project. -- **Configurations for Foundry & Hardhat**: Provides essential settings and scripts for building, testing, and deployment, tailored for both development environments. +- **Configurations for Foundry**: Provides essential settings and scripts for building, testing, and deployment, tailored for both development environments. ## Getting Started @@ -63,7 +63,7 @@ Copy `.env.example` to `.env` and fill in your details. ## 🛠️ Essential Scripts -Execute key operations for Foundry and Hardhat with these scripts. Append `:forge` or `:hardhat` to run them in the respective environment. +Execute key operations for Foundry with these scripts. Append `:forge` to run them in the respective environment. ### 🏗️ Build Contracts @@ -71,7 +71,7 @@ Execute key operations for Foundry and Hardhat with these scripts. Append `:forg yarn build ``` -Compiles contracts for both Foundry and Hardhat. +Compiles contracts for both Foundry. ### 🧪 Run Tests @@ -135,9 +135,9 @@ Automatically fixes linting problems found. yarn check ``` -To generate reports of the storage layout for potential upgrades safety using `hardhat-storage-layout`. -🔄 Add `:forge` or `:hardhat` to any script above to target only Foundry or Hardhat environment, respectively. + +🔄 Add `:forge` to any script above to target only Foundry ## 🔒 Security Audits diff --git a/hardhat.config.ts b/hardhat.config.ts deleted file mode 100644 index e139ab6..0000000 --- a/hardhat.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { HardhatUserConfig } from "hardhat/config"; -import "@nomicfoundation/hardhat-toolbox"; -import "hardhat-storage-layout"; -import "@bonadocs/docgen"; - -const config: HardhatUserConfig = { - solidity: { - version: "0.8.26", - settings: { - optimizer: { - enabled: true, - runs: 1000000, - details: { - yul: true, - }, - }, - viaIR: true, - }, - }, - docgen: { - projectName: "Biconomy Paymasters", - projectDescription: "Account Abstraction (v0.7.0) Paymasters", - }, -}; - -export default config; diff --git a/package.json b/package.json index a59cac8..5d8622f 100644 --- a/package.json +++ b/package.json @@ -8,22 +8,13 @@ }, "dependencies": { "@biconomy-devx/erc7579-msa": "^0.0.4", - "@openzeppelin/contracts": "^5.0.1", - "hardhat": "^2.20.1" + "@openzeppelin/contracts": "^5.0.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", @@ -31,10 +22,6 @@ "chai": "^4.3.7", "codecov": "^3.8.3", "ethers": "^6.11.1", - "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", @@ -52,31 +39,22 @@ "ethereum", "forge", "foundry", - "hardhat", "smart-contracts", "solidity" ], "private": true, "scripts": { "clean:forge": "forge clean", - "clean:hardhat": "yarn hardhat clean", - "clean": "yarn run clean:forge && yarn run clean:hardhat && rm -rf cache docs coverage storageLayout coverage.json", + "clean": "yarn run clean:forge && rm -rf cache docs coverage storageLayout coverage.json", "build:forge": "forge build", - "build:hardhat": "yarn hardhat compile", - "build": "yarn run build:forge && yarn run build:hardhat", + "build": "yarn run build:forge", "test:forge": "forge test", - "test:hardhat": "yarn hardhat test", - "test": "yarn run test:hardhat && yarn run test:forge", + "test": "yarn run test:forge", "test:gas:forge": "forge test --gas-report", - "test:gas:hardhat": "REPORT_GAS=true hardhat test", - "test:gas": "yarn test:gas:hardhat && yarn test:gas:forge", + "test:gas": "yarn test:gas:forge", "coverage:forge": "forge coverage", - "coverage:hardhat": "yarn hardhat coverage", - "coverage": "yarn run coverage:forge && yarn run coverage:hardhat", - "coverage:report": "forge coverage --report lcov && genhtml lcov.info --branch-coverage --output-dir coverage/foundry && mv lcov.info coverage/foundry && yarn run coverage:hardhat", - "docs": "yarn hardhat docgen", - "check-storage": "yarn hardhat check", - "deploy:hardhat": "yarn hardhat run --network localhost scripts/typescript/deploy.ts", + "coverage": "yarn run coverage:forge", + "coverage:report": "forge coverage --report lcov && genhtml lcov.info --branch-coverage --output-dir coverage/foundry && mv lcov.info coverage/foundry", "deploy:forge": "forge script scripts/solidity/Deploy.s.sol --broadcast --rpc-url http://localhost:8545", "lint:sol": "yarn solhint 'contracts/**/*.sol' && forge fmt --check", "lint:sol-fix": "yarn prettier --write 'contracts/**/*.sol' && yarn solhint 'contracts/**/*.sol' --fix --noPrompt && forge fmt", diff --git a/test/hardhat/biconomy-sponsorship-paymaster-specs.ts b/test/hardhat/biconomy-sponsorship-paymaster-specs.ts deleted file mode 100644 index c3a48d4..0000000 --- a/test/hardhat/biconomy-sponsorship-paymaster-specs.ts +++ /dev/null @@ -1,210 +0,0 @@ -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") }); - - await deployer.sendTransaction({ - to: expected, - value: parseEther("1"), - data: "0x", - }); - }); - - describe("Deployed Account : #validatePaymasterUserOp and #sendEmptySponsoredTx", () => { - 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), - ]), - 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)); - 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, - ]), - paymasterPostOpGasLimit: 40_000, - }, - walletOwner, - entryPoint, - "getNonce", - nonceKey, - ); - // 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({ - aggregator: AddressZero, - validAfter: parseInt(MOCK_VALID_AFTER), - validUntil: parseInt(MOCK_VALID_UNTIL), - }); - - await entryPoint.handleOps([userOp], await deployer.getAddress()); - }); - }); -}); diff --git a/test/hardhat/utils/deployment.ts b/test/hardhat/utils/deployment.ts deleted file mode 100644 index 18ebef0..0000000 --- a/test/hardhat/utils/deployment.ts +++ /dev/null @@ -1,156 +0,0 @@ -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 deleted file mode 100644 index 7e9e596..0000000 --- a/test/hardhat/utils/general.ts +++ /dev/null @@ -1,60 +0,0 @@ -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 deleted file mode 100644 index abe1776..0000000 --- a/test/hardhat/utils/testUtils.ts +++ /dev/null @@ -1,257 +0,0 @@ -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 deleted file mode 100644 index 7dd52fa..0000000 --- a/test/hardhat/utils/types.ts +++ /dev/null @@ -1,30 +0,0 @@ -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; -} diff --git a/test/hardhat/utils/userOpHelpers.ts b/test/hardhat/utils/userOpHelpers.ts deleted file mode 100644 index 50fccd5..0000000 --- a/test/hardhat/utils/userOpHelpers.ts +++ /dev/null @@ -1,486 +0,0 @@ -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 72fe3ead5e652aa100017be31f67e16842e4017f Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Fri, 30 Aug 2024 11:57:04 +0400 Subject: [PATCH 45/69] temp --- .gitignore | 3 + contracts/base/BasePaymaster.sol | 6 +- .../IBiconomySponsorshipPaymaster.sol | 45 +++- .../interfaces/IBiconomyTokenPaymaster.sol | 0 contracts/references/SampleTokenPaymaster.sol | 217 ++++++++++++++++++ ...t.sol => BiconomySponsorshipPaymaster.sol} | 37 +-- contracts/token/BiconomyTokenPaymaster.sol | 17 ++ remappings.txt | 12 +- scripts/hardhat/deploy.ts | 31 --- .../sample/deploy-verifying-paymaster.ts | 46 ---- test/foundry/base/TestBase.sol | 3 +- ...ipPaymasterWithDynamicAdjustmentTest.t.sol | 3 +- ..._TestSponsorshipPaymasterWithPremium.t.sol | 3 +- 13 files changed, 309 insertions(+), 114 deletions(-) create mode 100644 contracts/interfaces/IBiconomyTokenPaymaster.sol create mode 100644 contracts/references/SampleTokenPaymaster.sol rename contracts/sponsorship/{SponsorshipPaymasterWithDynamicAdjustment.sol => BiconomySponsorshipPaymaster.sol} (90%) create mode 100644 contracts/token/BiconomyTokenPaymaster.sol delete mode 100644 scripts/hardhat/deploy.ts delete mode 100644 scripts/hardhat/sample/deploy-verifying-paymaster.ts diff --git a/.gitignore b/.gitignore index 52b85d9..36412ab 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,9 @@ node_modules /coverage /coverage.json +/deploy.txt +deploy.txt + node_modules .env diff --git a/contracts/base/BasePaymaster.sol b/contracts/base/BasePaymaster.sol index 8a6f1a0..bc1042e 100644 --- a/contracts/base/BasePaymaster.sol +++ b/contracts/base/BasePaymaster.sol @@ -5,9 +5,9 @@ pragma solidity ^0.8.26; import { SoladyOwnable } from "../utils/SoladyOwnable.sol"; import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; -import { IPaymaster } from "account-abstraction/contracts/interfaces/IPaymaster.sol"; -import { IEntryPoint } from "account-abstraction/contracts/interfaces/IEntryPoint.sol"; -import "account-abstraction/contracts/core/UserOperationLib.sol"; +import { IPaymaster } from "@account-abstraction/contracts/interfaces/IPaymaster.sol"; +import { IEntryPoint } from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; +import "@account-abstraction/contracts/core/UserOperationLib.sol"; /** * Helper class for creating a paymaster. * provides helper methods for staking. diff --git a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol index 00a4c39..a2dcf17 100644 --- a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol +++ b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol @@ -1,12 +1,13 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.26; -interface IBiconomySponsorshipPaymaster { +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { PackedUserOperation } from "@account-abstraction/contracts/core/UserOperationLib.sol"; + +interface IBiconomySponsorshipPaymaster{ event UnaccountedGasChanged(uint256 indexed oldValue, uint256 indexed newValue); event FixedDynamicAdjustmentChanged(uint32 indexed oldValue, uint32 indexed newValue); - event VerifyingSignerChanged(address indexed oldSigner, address indexed newSigner, address indexed actor); - event FeeCollectorChanged(address indexed oldFeeCollector, address indexed newFeeCollector, address indexed actor); event GasDeposited(address indexed paymasterId, uint256 indexed value); event GasWithdrawn(address indexed paymasterId, address indexed to, uint256 indexed value); @@ -14,4 +15,40 @@ interface IBiconomySponsorshipPaymaster { event DynamicAdjustmentCollected(address indexed paymasterId, uint256 indexed dynamicAdjustment); event Received(address indexed sender, uint256 value); event TokensWithdrawn(address indexed token, address indexed to, uint256 indexed amount, address actor); + + function depositFor(address paymasterId) external payable; + + function setSigner(address _newVerifyingSigner) external payable; + + function setFeeCollector(address _newFeeCollector) external payable; + + function setUnaccountedGas(uint48 value) external payable; + + function withdrawERC20(IERC20 token, address target, uint256 amount) external; + + function withdrawEth(address payable recipient, uint256 amount) external payable; + + function getBalance(address paymasterId) external view returns (uint256 balance); + + function getHash( + PackedUserOperation calldata userOp, + address paymasterId, + uint48 validUntil, + uint48 validAfter, + uint32 dynamicAdjustment + ) + external + view + returns (bytes32); + + function parsePaymasterAndData(bytes calldata paymasterAndData) + external + pure + returns ( + address paymasterId, + uint48 validUntil, + uint48 validAfter, + uint32 dynamicAdjustment, + bytes calldata signature + ); } diff --git a/contracts/interfaces/IBiconomyTokenPaymaster.sol b/contracts/interfaces/IBiconomyTokenPaymaster.sol new file mode 100644 index 0000000..e69de29 diff --git a/contracts/references/SampleTokenPaymaster.sol b/contracts/references/SampleTokenPaymaster.sol new file mode 100644 index 0000000..7da5e64 --- /dev/null +++ b/contracts/references/SampleTokenPaymaster.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +// Import the required libraries and contracts +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; + +import "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; +import "@account-abstraction/contracts/core/BasePaymaster.sol"; +import "@account-abstraction/contracts/core/Helpers.sol"; +import "@account-abstraction/contracts/samples/utils/UniswapHelper.sol"; +import "@account-abstraction/contracts/samples/utils/OracleHelper.sol"; + +/// @title Sample ERC-20 Token Paymaster for ERC-4337 +/// This Paymaster covers gas fees in exchange for ERC20 tokens charged using allowance pre-issued by ERC-4337 accounts. +/// The contract refunds excess tokens if the actual gas cost is lower than the initially provided amount. +/// The token price cannot be queried in the validation code due to storage access restrictions of ERC-4337. +/// The price is cached inside the contract and is updated in the 'postOp' stage if the change is >10%. +/// It is theoretically possible the token has depreciated so much since the last 'postOp' the refund becomes negative. +/// The contract reverts the inner user transaction in that case but keeps the charge. +/// The contract also allows honest clients to prepay tokens at a higher price to avoid getting reverted. +/// It also allows updating price configuration and withdrawing tokens by the contract owner. +/// The contract uses an Oracle to fetch the latest token prices. +/// @dev Inherits from BasePaymaster. +contract TokenPaymaster is BasePaymaster, UniswapHelper, OracleHelper { + + using UserOperationLib for PackedUserOperation; + + struct TokenPaymasterConfig { + /// @notice The price markup percentage applied to the token price (1e26 = 100%). Ranges from 1e26 to 2e26 + uint256 priceMarkup; + + /// @notice Exchange tokens to native currency if the EntryPoint balance of this Paymaster falls below this value + uint128 minEntryPointBalance; + + /// @notice Estimated gas cost for refunding tokens after the transaction is completed + uint48 refundPostopCost; + + /// @notice Transactions are only valid as long as the cached price is not older than this value + uint48 priceMaxAge; + } + + event ConfigUpdated(TokenPaymasterConfig tokenPaymasterConfig); + + event UserOperationSponsored(address indexed user, uint256 actualTokenCharge, uint256 actualGasCost, uint256 actualTokenPriceWithMarkup); + + event Received(address indexed sender, uint256 value); + + /// @notice All 'price' variables are multiplied by this value to avoid rounding up + uint256 private constant PRICE_DENOMINATOR = 1e26; + + TokenPaymasterConfig public tokenPaymasterConfig; + + /// @notice Initializes the TokenPaymaster contract with the given parameters. + /// @param _token The ERC20 token used for transaction fee payments. + /// @param _entryPoint The EntryPoint contract used in the Account Abstraction infrastructure. + /// @param _wrappedNative The ERC-20 token that wraps the native asset for current chain. + /// @param _uniswap The Uniswap V3 SwapRouter contract. + /// @param _tokenPaymasterConfig The configuration for the Token Paymaster. + /// @param _oracleHelperConfig The configuration for the Oracle Helper. + /// @param _uniswapHelperConfig The configuration for the Uniswap Helper. + /// @param _owner The address that will be set as the owner of the contract. + constructor( + IERC20Metadata _token, + IEntryPoint _entryPoint, + IERC20 _wrappedNative, + ISwapRouter _uniswap, + TokenPaymasterConfig memory _tokenPaymasterConfig, + OracleHelperConfig memory _oracleHelperConfig, + UniswapHelperConfig memory _uniswapHelperConfig, + address _owner + ) + BasePaymaster( + _entryPoint + ) + OracleHelper( + _oracleHelperConfig + ) + UniswapHelper( + _token, + _wrappedNative, + _uniswap, + _uniswapHelperConfig + ) + { + setTokenPaymasterConfig(_tokenPaymasterConfig); + transferOwnership(_owner); + } + + /// @notice Updates the configuration for the Token Paymaster. + /// @param _tokenPaymasterConfig The new configuration struct. + function setTokenPaymasterConfig( + TokenPaymasterConfig memory _tokenPaymasterConfig + ) public onlyOwner { + require(_tokenPaymasterConfig.priceMarkup <= 2 * PRICE_DENOMINATOR, "TPM: price markup too high"); + require(_tokenPaymasterConfig.priceMarkup >= PRICE_DENOMINATOR, "TPM: price markup too low"); + tokenPaymasterConfig = _tokenPaymasterConfig; + emit ConfigUpdated(_tokenPaymasterConfig); + } + + function setUniswapConfiguration( + UniswapHelperConfig memory _uniswapHelperConfig + ) external onlyOwner { + _setUniswapHelperConfiguration(_uniswapHelperConfig); + } + + /// @notice Allows the contract owner to withdraw a specified amount of tokens from the contract. + /// @param to The address to transfer the tokens to. + /// @param amount The amount of tokens to transfer. + function withdrawToken(address to, uint256 amount) external onlyOwner { + SafeERC20.safeTransfer(token, to, amount); + } + + /// @notice Validates a paymaster user operation and calculates the required token amount for the transaction. + /// @param userOp The user operation data. + /// @param requiredPreFund The maximum cost (in native token) the paymaster has to prefund. + /// @return context The context containing the token amount and user sender address (if applicable). + /// @return validationResult A uint256 value indicating the result of the validation (always 0 in this implementation). + function _validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32, uint256 requiredPreFund) + internal + override + returns (bytes memory context, uint256 validationResult) {unchecked { + uint256 priceMarkup = tokenPaymasterConfig.priceMarkup; + uint256 dataLength = userOp.paymasterAndData.length - PAYMASTER_DATA_OFFSET; + require(dataLength == 0 || dataLength == 32, + "TPM: invalid data length" + ); + uint256 maxFeePerGas = userOp.unpackMaxFeePerGas(); + uint256 refundPostopCost = tokenPaymasterConfig.refundPostopCost; + require(refundPostopCost < userOp.unpackPostOpGasLimit(), "TPM: postOpGasLimit too low"); + uint256 preChargeNative = requiredPreFund + (refundPostopCost * maxFeePerGas); + // note: as price is in native-asset-per-token and we want more tokens increasing it means dividing it by markup + uint256 cachedPriceWithMarkup = cachedPrice * PRICE_DENOMINATOR / priceMarkup; + if (dataLength == 32) { + uint256 clientSuppliedPrice = uint256(bytes32(userOp.paymasterAndData[PAYMASTER_DATA_OFFSET : PAYMASTER_DATA_OFFSET + 32])); + if (clientSuppliedPrice < cachedPriceWithMarkup) { + // note: smaller number means 'more native asset per token' + cachedPriceWithMarkup = clientSuppliedPrice; + } + } + uint256 tokenAmount = weiToToken(preChargeNative, cachedPriceWithMarkup); + SafeERC20.safeTransferFrom(token, userOp.sender, address(this), tokenAmount); + context = abi.encode(tokenAmount, userOp.sender); + validationResult = _packValidationData( + false, + uint48(cachedPriceTimestamp + tokenPaymasterConfig.priceMaxAge), + 0 + ); + } + } + + /// @notice Performs post-operation tasks, such as updating the token price and refunding excess tokens. + /// @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 { + uint256 priceMarkup = tokenPaymasterConfig.priceMarkup; + ( + uint256 preCharge, + address userOpSender + ) = abi.decode(context, (uint256, address)); + uint256 _cachedPrice = updateCachedPrice(false); + // note: as price is in native-asset-per-token and we want more tokens increasing it means dividing it by markup + uint256 cachedPriceWithMarkup = _cachedPrice * PRICE_DENOMINATOR / priceMarkup; + // Refund tokens based on actual gas cost + uint256 actualChargeNative = actualGasCost + tokenPaymasterConfig.refundPostopCost * actualUserOpFeePerGas; + uint256 actualTokenNeeded = weiToToken(actualChargeNative, cachedPriceWithMarkup); + if (preCharge > actualTokenNeeded) { + // If the initially provided token amount is greater than the actual amount needed, refund the difference + SafeERC20.safeTransfer( + token, + userOpSender, + preCharge - actualTokenNeeded + ); + } else if (preCharge < actualTokenNeeded) { + // Attempt to cover Paymaster's gas expenses by withdrawing the 'overdraft' from the client + // If the transfer reverts also revert the 'postOp' to remove the incentive to cheat + SafeERC20.safeTransferFrom( + token, + userOpSender, + address(this), + actualTokenNeeded - preCharge + ); + } + + emit UserOperationSponsored(userOpSender, actualTokenNeeded, actualGasCost, cachedPriceWithMarkup); + refillEntryPointDeposit(_cachedPrice); + } + } + + /// @notice If necessary this function uses this Paymaster's token balance to refill the deposit on EntryPoint + /// @param _cachedPrice the token price that will be used to calculate the swap amount. + function refillEntryPointDeposit(uint256 _cachedPrice) private { + uint256 currentEntryPointBalance = entryPoint.balanceOf(address(this)); + if ( + currentEntryPointBalance < tokenPaymasterConfig.minEntryPointBalance + ) { + uint256 swappedWeth = _maybeSwapTokenToWeth(token, _cachedPrice); + unwrapWeth(swappedWeth); + entryPoint.depositTo{value: address(this).balance}(address(this)); + } + } + + 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"); + } +} \ No newline at end of file diff --git a/contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol b/contracts/sponsorship/BiconomySponsorshipPaymaster.sol similarity index 90% rename from contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol rename to contracts/sponsorship/BiconomySponsorshipPaymaster.sol index 906e9d2..41c7c90 100644 --- a/contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol +++ b/contracts/sponsorship/BiconomySponsorshipPaymaster.sol @@ -4,20 +4,21 @@ pragma solidity ^0.8.26; /* solhint-disable reason-string */ import "../base/BasePaymaster.sol"; -import "account-abstraction/contracts/core/UserOperationLib.sol"; -import "account-abstraction/contracts/core/Helpers.sol"; -import { SignatureCheckerLib } from "solady/src/utils/SignatureCheckerLib.sol"; -import { ECDSA as ECDSA_solady } from "solady/src/utils/ECDSA.sol"; +import "@account-abstraction/contracts/core/UserOperationLib.sol"; +import "@account-abstraction/contracts/core/Helpers.sol"; +import { SignatureCheckerLib } from "@solady/src/utils/SignatureCheckerLib.sol"; +import { ECDSA as ECDSA_solady } from "@solady/src/utils/ECDSA.sol"; import { BiconomySponsorshipPaymasterErrors } from "../common/Errors.sol"; import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { SafeTransferLib } from "solady/src/utils/SafeTransferLib.sol"; +import { SafeTransferLib } from "@solady/src/utils/SafeTransferLib.sol"; import { IBiconomySponsorshipPaymaster } from "../interfaces/IBiconomySponsorshipPaymaster.sol"; /** * @title BiconomySponsorshipPaymaster * @author livingrockrises - * @notice Based on Infinitism 'VerifyingPaymaster' contract + * @author ShivaanshK + * @notice Based on Infinitism's '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 @@ -43,7 +44,7 @@ contract BiconomySponsorshipPaymaster is uint32 private constant PRICE_DENOMINATOR = 1e6; // note: could rename to PAYMASTER_ID_OFFSET - uint256 private constant VALID_PND_OFFSET = PAYMASTER_DATA_OFFSET; + uint256 private constant PAYMASTER_ID_OFFSET = PAYMASTER_DATA_OFFSET; // Limit for unaccounted gas cost uint16 private constant UNACCOUNTED_GAS_LIMIT = 10_000; @@ -95,7 +96,7 @@ contract BiconomySponsorshipPaymaster is * @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 override onlyOwner { if (isContract(_newVerifyingSigner)) revert VerifyingSignerCanNotBeContract(); if (_newVerifyingSigner == address(0)) { revert VerifyingSignerCanNotBeZero(); @@ -114,7 +115,7 @@ contract BiconomySponsorshipPaymaster is * @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 override onlyOwner { if (_newFeeCollector == address(0)) revert FeeCollectorCanNotBeZero(); address oldFeeCollector = feeCollector; feeCollector = _newFeeCollector; @@ -126,7 +127,7 @@ contract BiconomySponsorshipPaymaster is * @param value The new value to be set as the unaccountedEPGasOverhead. * @notice only to be called by the owner of the contract. */ - function setUnaccountedGas(uint48 value) external payable onlyOwner { + function setUnaccountedGas(uint48 value) external payable override onlyOwner { if (value > UNACCOUNTED_GAS_LIMIT) { revert UnaccountedGasTooHigh(); } @@ -138,7 +139,7 @@ contract BiconomySponsorshipPaymaster is /** * @dev Override the default implementation. */ - function deposit() external payable virtual override { + function deposit() external payable override virtual { revert UseDepositForInstead(); } @@ -148,7 +149,7 @@ contract BiconomySponsorshipPaymaster is * @param target address to send to * @param amount amount to withdraw */ - function withdrawERC20(IERC20 token, address target, uint256 amount) external payable onlyOwner nonReentrant { + function withdrawERC20(IERC20 token, address target, uint256 amount) external onlyOwner nonReentrant { _withdrawERC20(token, target, amount); } @@ -169,7 +170,7 @@ contract BiconomySponsorshipPaymaster is emit GasWithdrawn(msg.sender, withdrawAddress, amount); } - function withdrawEth(address payable recipient, uint256 amount) external onlyOwner nonReentrant { + function withdrawEth(address payable recipient, uint256 amount) external payable onlyOwner nonReentrant { (bool success,) = recipient.call{ value: amount }(""); if (!success) { revert WithdrawalFailed(); @@ -236,11 +237,11 @@ contract BiconomySponsorshipPaymaster is ) { unchecked { - 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])); - dynamicAdjustment = uint32(bytes4(paymasterAndData[VALID_PND_OFFSET + 32:VALID_PND_OFFSET + 36])); - signature = paymasterAndData[VALID_PND_OFFSET + 36:]; + paymasterId = address(bytes20(paymasterAndData[PAYMASTER_ID_OFFSET:PAYMASTER_ID_OFFSET + 20])); + validUntil = uint48(bytes6(paymasterAndData[PAYMASTER_ID_OFFSET + 20:PAYMASTER_ID_OFFSET + 26])); + validAfter = uint48(bytes6(paymasterAndData[PAYMASTER_ID_OFFSET + 26:PAYMASTER_ID_OFFSET + 32])); + dynamicAdjustment = uint32(bytes4(paymasterAndData[PAYMASTER_ID_OFFSET + 32:PAYMASTER_ID_OFFSET + 36])); + signature = paymasterAndData[PAYMASTER_ID_OFFSET + 36:]; } } diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol new file mode 100644 index 0000000..9658bc0 --- /dev/null +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.26; + +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; +import {UserOperationLib} from "@account-abstraction/contracts/core/UserOperationLib.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {BasePaymaster} from "../base/BasePaymaster.sol"; +import "@account-abstraction/contracts/core/Helpers.sol" as Helpers; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; + + +contract BiconomyTokenPaymaster { + +} \ No newline at end of file diff --git a/remappings.txt b/remappings.txt index 6710ed5..f34af2e 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,8 +1,8 @@ @openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ @prb/test/=node_modules/@prb/test/ -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 +@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 diff --git a/scripts/hardhat/deploy.ts b/scripts/hardhat/deploy.ts deleted file mode 100644 index f1ac8e7..0000000 --- a/scripts/hardhat/deploy.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ethers } from "hardhat"; - -async function main() { - const currentTimestampInSeconds = Math.round(Date.now() / 1000); - const unlockTime = currentTimestampInSeconds + 60; - - const lockedAmount = ethers.parseEther("0.001"); - - const lock = await ethers.deployContract("Lock", [unlockTime], { - value: lockedAmount, - }); - - await lock.waitForDeployment(); - - console.log( - `Lock with ${ethers.formatEther( - lockedAmount, - )}ETH and unlock timestamp ${unlockTime} deployed to ${lock.target}`, - ); -} - -// We recommend this pattern to be able to use async/await everywhere -// and properly handle errors. -main() - .then(() => { - process.exit(0); - }) - .catch((error) => { - console.error(error); - process.exitCode = 1; - }); diff --git a/scripts/hardhat/sample/deploy-verifying-paymaster.ts b/scripts/hardhat/sample/deploy-verifying-paymaster.ts deleted file mode 100644 index db310e7..0000000 --- a/scripts/hardhat/sample/deploy-verifying-paymaster.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { ethers } from "hardhat"; - -const entryPointAddress = - process.env.ENTRY_POINT_ADDRESS || - "0x0000000071727De22E5E9d8BAf0edAc6f37da032"; - -const verifyingSigner = - process.env.PAYMASTER_SIGNER_ADDRESS_PROD || - "0x2cf491602ad22944D9047282aBC00D3e52F56B37"; - -const deployEntryPoint = process.env.DEPLOY_ENTRY_POINT || true; - -async function main() { - let targetEntryPoint = entryPointAddress; - - if (deployEntryPoint) { - // Note: unless the network is actual chain where entrypoint is deployed, we have to deploy for hardhat node tests - const entryPoint = await ethers.deployContract("EntryPoint"); - - await entryPoint.waitForDeployment(); - - targetEntryPoint = entryPoint.target as string; - - console.log(`EntryPoint updated to ${entryPoint.target}`); - } - - const verifyingPaymaster = await ethers.deployContract("VerifyingPaymaster", [ - targetEntryPoint, - verifyingSigner, - ]); - - await verifyingPaymaster.waitForDeployment(); - - console.log(`VerifyingPaymaster deployed to ${verifyingPaymaster.target}`); -} - -// We recommend this pattern to be able to use async/await everywhere -// and properly handle errors. -main() - .then(() => { - process.exit(0); - }) - .catch((error) => { - console.error(error); - process.exitCode = 1; - }); diff --git a/test/foundry/base/TestBase.sol b/test/foundry/base/TestBase.sol index 69eaceb..669fd19 100644 --- a/test/foundry/base/TestBase.sol +++ b/test/foundry/base/TestBase.sol @@ -22,8 +22,7 @@ import { Bootstrap, BootstrapConfig } from "nexus/contracts/utils/Bootstrap.sol" import { CheatCodes } from "nexus/test/foundry/utils/CheatCodes.sol"; import { BaseEventsAndErrors } from "./BaseEventsAndErrors.sol"; -import { BiconomySponsorshipPaymaster } from - "../../../contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol"; +import { BiconomySponsorshipPaymaster } from "../../../contracts/sponsorship/BiconomySponsorshipPaymaster.sol"; abstract contract TestBase is CheatCodes, BaseEventsAndErrors { // ----------------------------------------- diff --git a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol index a77dbb1..05b9362 100644 --- a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol +++ b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol @@ -3,8 +3,7 @@ pragma solidity ^0.8.26; import { TestBase } from "../../base/TestBase.sol"; import { IBiconomySponsorshipPaymaster } from "../../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; -import { BiconomySponsorshipPaymaster } from - "../../../../contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol"; +import { BiconomySponsorshipPaymaster } from "../../../../contracts/sponsorship/BiconomySponsorshipPaymaster.sol"; import { PackedUserOperation } from "account-abstraction/contracts/core/UserOperationLib.sol"; import { MockToken } from "./../../../../lib/nexus/contracts/mocks/MockToken.sol"; diff --git a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol index 5fb3416..bac7265 100644 --- a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol +++ b/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol @@ -3,8 +3,7 @@ pragma solidity ^0.8.26; import { TestBase } from "../../base/TestBase.sol"; import { IBiconomySponsorshipPaymaster } from "../../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; -import { BiconomySponsorshipPaymaster } from - "../../../../contracts/sponsorship/SponsorshipPaymasterWithDynamicAdjustment.sol"; +import { BiconomySponsorshipPaymaster } from "../../../../contracts/sponsorship/BiconomySponsorshipPaymaster.sol"; import { MockToken } from "./../../../../lib/nexus/contracts/mocks/MockToken.sol"; import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol"; From 5b48dfb1b81b8ecfc515f74dc176c3313153c606 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Fri, 30 Aug 2024 12:25:22 +0400 Subject: [PATCH 46/69] setup for the token paymaster --- .gitmodules | 6 ++ ...=> BiconomySponsorshipPaymasterErrors.sol} | 0 .../common/BiconomyTokenPaymasterErrors.sol | 6 ++ .../interfaces/IBiconomyTokenPaymaster.sol | 6 ++ contracts/mocks/Imports.sol | 7 +- .../BiconomySponsorshipPaymaster.sol | 2 +- contracts/token/BiconomyTokenPaymaster.sol | 72 ++++++++++++++++--- contracts/utils/SoladyOwnable.sol | 2 +- lib/v3-core | 1 + lib/v3-periphery | 1 + remappings.txt | 2 + .../base/BaseEventsAndErrors.sol | 2 +- test/{foundry => }/base/TestBase.sol | 1 + ...ipPaymasterWithDynamicAdjustmentTest.t.sol | 0 ..._TestSponsorshipPaymasterWithPremium.t.sol | 0 15 files changed, 89 insertions(+), 19 deletions(-) rename contracts/common/{Errors.sol => BiconomySponsorshipPaymasterErrors.sol} (100%) create mode 100644 contracts/common/BiconomyTokenPaymasterErrors.sol create mode 160000 lib/v3-core create mode 160000 lib/v3-periphery rename test/{foundry => }/base/BaseEventsAndErrors.sol (91%) rename test/{foundry => }/base/TestBase.sol (99%) rename test/{foundry => }/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol (100%) rename test/{foundry => }/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol (100%) diff --git a/.gitmodules b/.gitmodules index f008824..a2db065 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,9 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lib/v3-periphery"] + path = lib/v3-periphery + url = https://github.com/Uniswap/v3-periphery +[submodule "lib/v3-core"] + path = lib/v3-core + url = https://github.com/Uniswap/v3-core diff --git a/contracts/common/Errors.sol b/contracts/common/BiconomySponsorshipPaymasterErrors.sol similarity index 100% rename from contracts/common/Errors.sol rename to contracts/common/BiconomySponsorshipPaymasterErrors.sol diff --git a/contracts/common/BiconomyTokenPaymasterErrors.sol b/contracts/common/BiconomyTokenPaymasterErrors.sol new file mode 100644 index 0000000..9293b21 --- /dev/null +++ b/contracts/common/BiconomyTokenPaymasterErrors.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.26; + +contract BiconomyTokenPaymasterErrors { + +} diff --git a/contracts/interfaces/IBiconomyTokenPaymaster.sol b/contracts/interfaces/IBiconomyTokenPaymaster.sol index e69de29..2347f38 100644 --- a/contracts/interfaces/IBiconomyTokenPaymaster.sol +++ b/contracts/interfaces/IBiconomyTokenPaymaster.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.26; + +interface IBiconomyTokenPaymaster { + +} \ No newline at end of file diff --git a/contracts/mocks/Imports.sol b/contracts/mocks/Imports.sol index 131ae80..03ad730 100644 --- a/contracts/mocks/Imports.sol +++ b/contracts/mocks/Imports.sol @@ -3,8 +3,5 @@ pragma solidity ^0.8.26; /* 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"; +import "@account-abstraction/contracts/core/EntryPoint.sol"; +import "@account-abstraction/contracts/core/EntryPointSimulations.sol"; diff --git a/contracts/sponsorship/BiconomySponsorshipPaymaster.sol b/contracts/sponsorship/BiconomySponsorshipPaymaster.sol index 41c7c90..7faa2d7 100644 --- a/contracts/sponsorship/BiconomySponsorshipPaymaster.sol +++ b/contracts/sponsorship/BiconomySponsorshipPaymaster.sol @@ -8,7 +8,7 @@ import "@account-abstraction/contracts/core/UserOperationLib.sol"; import "@account-abstraction/contracts/core/Helpers.sol"; import { SignatureCheckerLib } from "@solady/src/utils/SignatureCheckerLib.sol"; import { ECDSA as ECDSA_solady } from "@solady/src/utils/ECDSA.sol"; -import { BiconomySponsorshipPaymasterErrors } from "../common/Errors.sol"; +import { BiconomySponsorshipPaymasterErrors } from "../common/BiconomySponsorshipPaymasterErrors.sol"; import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeTransferLib } from "@solady/src/utils/SafeTransferLib.sol"; diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index 9658bc0..d8cd1d0 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -1,17 +1,67 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.26; -import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { ECDSA as ECDSA_solady } from "@solady/src/utils/ECDSA.sol"; import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; -import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; -import {UserOperationLib} from "@account-abstraction/contracts/core/UserOperationLib.sol"; -import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import {BasePaymaster} from "../base/BasePaymaster.sol"; -import "@account-abstraction/contracts/core/Helpers.sol" as Helpers; -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import "@openzeppelin/contracts/utils/Address.sol"; +import { IEntryPoint } from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; +import { SignatureCheckerLib } from "@solady/src/utils/SignatureCheckerLib.sol"; +import { PackedUserOperation, UserOperationLib } from "@account-abstraction/contracts/core/UserOperationLib.sol"; +import { BasePaymaster } from "../base/BasePaymaster.sol"; +import { BiconomyTokenPaymasterErrors } from "../common/BiconomyTokenPaymasterErrors.sol"; +import { IBiconomyTokenPaymaster } from "../interfaces/IBiconomyTokenPaymaster.sol"; +contract BiconomyTokenPaymaster is + BasePaymaster, + ReentrancyGuard, + BiconomyTokenPaymasterErrors, + IBiconomyTokenPaymaster +{ + using UserOperationLib for PackedUserOperation; + using SignatureCheckerLib for address; -contract BiconomyTokenPaymaster { - -} \ No newline at end of file + constructor( + address _owner, + IEntryPoint _entryPoint + ) + BasePaymaster(_owner, _entryPoint) + { } + + /** + * @dev Validate a user operation. + * This method is abstract in BasePaymaster and must be implemented in derived contracts. + * @param userOp The user operation. + * @param userOpHash The hash of the user operation. + * @param maxCost The maximum cost of the user operation. + */ + function _validatePaymasterUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 maxCost + ) + internal + override + returns (bytes memory context, uint256 validationData) + { + // Implementation of user operation validation logic + } + + /** + * @dev Post-operation handler. + * This method is abstract in BasePaymaster and must be implemented in derived contracts. + * @param mode The mode of the post operation (opSucceeded, opReverted, or postOpReverted). + * @param context The context value returned by validatePaymasterUserOp. + * @param actualGasCost Actual gas used so far (excluding this postOp call). + * @param actualUserOpFeePerGas The gas price this UserOp pays. + */ + function _postOp( + PostOpMode mode, + bytes calldata context, + uint256 actualGasCost, + uint256 actualUserOpFeePerGas + ) + internal + override + { + // Implementation of post-operation logic + } +} diff --git a/contracts/utils/SoladyOwnable.sol b/contracts/utils/SoladyOwnable.sol index 8b680d3..5db5e71 100644 --- a/contracts/utils/SoladyOwnable.sol +++ b/contracts/utils/SoladyOwnable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import { Ownable } from "solady/src/auth/Ownable.sol"; +import { Ownable } from "@solady/src/auth/Ownable.sol"; contract SoladyOwnable is Ownable { constructor(address _owner) Ownable() { diff --git a/lib/v3-core b/lib/v3-core new file mode 160000 index 0000000..e3589b1 --- /dev/null +++ b/lib/v3-core @@ -0,0 +1 @@ +Subproject commit e3589b192d0be27e100cd0daaf6c97204fdb1899 diff --git a/lib/v3-periphery b/lib/v3-periphery new file mode 160000 index 0000000..80f26c8 --- /dev/null +++ b/lib/v3-periphery @@ -0,0 +1 @@ +Subproject commit 80f26c86c57b8a5e4b913f42844d4c8bd274d058 diff --git a/remappings.txt b/remappings.txt index f34af2e..9622f6b 100644 --- a/remappings.txt +++ b/remappings.txt @@ -2,6 +2,8 @@ @prb/test/=node_modules/@prb/test/ @nexus/=lib/nexus/ @forge-std/=lib/forge-std/ +@uniswap/v3-periphery/=lib/v3-periphery +@uniswap/v3-core/=lib/v3-core @account-abstraction=node_modules/account-abstraction/ @modulekit/=node_modules/modulekit/src/ @sentinellist/=node_modules/sentinellist/ diff --git a/test/foundry/base/BaseEventsAndErrors.sol b/test/base/BaseEventsAndErrors.sol similarity index 91% rename from test/foundry/base/BaseEventsAndErrors.sol rename to test/base/BaseEventsAndErrors.sol index 497366e..187a880 100644 --- a/test/foundry/base/BaseEventsAndErrors.sol +++ b/test/base/BaseEventsAndErrors.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.26; import { EventsAndErrors } from "nexus/test/foundry/utils/EventsAndErrors.sol"; -import { BiconomySponsorshipPaymasterErrors } from "./../../../contracts/common/Errors.sol"; +import { BiconomySponsorshipPaymasterErrors } from "./../../../contracts/common/BiconomySponsorshipPaymasterErrors.sol"; contract BaseEventsAndErrors is EventsAndErrors, BiconomySponsorshipPaymasterErrors { // ========================== diff --git a/test/foundry/base/TestBase.sol b/test/base/TestBase.sol similarity index 99% rename from test/foundry/base/TestBase.sol rename to test/base/TestBase.sol index 669fd19..427bfe8 100644 --- a/test/foundry/base/TestBase.sol +++ b/test/base/TestBase.sol @@ -528,6 +528,7 @@ abstract contract TestBase is CheatCodes, BaseEventsAndErrors { uint32 dynamicAdjustment ) internal + view { (uint256 expectedDynamicAdjustment, uint256 actualDynamicAdjustment) = getDynamicAdjustments( bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, dynamicAdjustment diff --git a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol b/test/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol similarity index 100% rename from test/foundry/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol rename to test/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol diff --git a/test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol b/test/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol similarity index 100% rename from test/foundry/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol rename to test/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol From a73ccb83508f337a5a40fe996eb257512600629b Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Fri, 30 Aug 2024 14:32:43 +0400 Subject: [PATCH 47/69] refactoring --- contracts/base/BasePaymaster.sol | 5 +- .../common/BiconomyTokenPaymasterErrors.sol | 22 ++++ .../IBiconomySponsorshipPaymaster.sol | 2 +- .../interfaces/IBiconomyTokenPaymaster.sol | 26 ++++- .../BiconomySponsorshipPaymaster.sol | 42 ++++--- contracts/token/BiconomyTokenPaymaster.sol | 109 +++++++++++++++++- test/base/BaseEventsAndErrors.sol | 4 +- test/base/TestBase.sol | 36 +++--- ...t.t.sol => TestSponsorshipPaymaster.t.sol} | 29 +++-- ...> TestFuzz_TestSponsorshipPaymaster.t.sol} | 8 +- 10 files changed, 229 insertions(+), 54 deletions(-) rename test/unit/concrete/{TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol => TestSponsorshipPaymaster.t.sol} (93%) rename test/unit/fuzz/{TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol => TestFuzz_TestSponsorshipPaymaster.t.sol} (94%) diff --git a/contracts/base/BasePaymaster.sol b/contracts/base/BasePaymaster.sol index bc1042e..f3394c1 100644 --- a/contracts/base/BasePaymaster.sol +++ b/contracts/base/BasePaymaster.sol @@ -163,7 +163,10 @@ abstract contract BasePaymaster is IPaymaster, SoladyOwnable { require(msg.sender == address(entryPoint), "Sender not EntryPoint"); } - function isContract(address _addr) internal view returns (bool) { + /** + * Check if address is a contract + */ + function _isContract(address _addr) internal view returns (bool) { uint256 size; assembly ("memory-safe") { size := extcodesize(_addr) diff --git a/contracts/common/BiconomyTokenPaymasterErrors.sol b/contracts/common/BiconomyTokenPaymasterErrors.sol index 9293b21..1aa0cbd 100644 --- a/contracts/common/BiconomyTokenPaymasterErrors.sol +++ b/contracts/common/BiconomyTokenPaymasterErrors.sol @@ -2,5 +2,27 @@ pragma solidity ^0.8.26; contract BiconomyTokenPaymasterErrors { + /** + * @notice Throws when the verifiying signer address provided is address(0) + */ + error VerifyingSignerCanNotBeZero(); + /** + * @notice Throws when the fee collector address provided is address(0) + */ + error FeeCollectorCanNotBeZero(); + + /** + * @notice Throws when the fee collector address provided is a deployed contract + */ + error FeeCollectorCanNotBeContract(); + + /** + * @notice Throws when the fee collector address provided is a deployed contract + */ + error VerifyingSignerCanNotBeContract(); + /** + * @notice Throws when trying unaccountedGas is too high + */ + error UnaccountedGasTooHigh(); } diff --git a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol index a2dcf17..7ad2651 100644 --- a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol +++ b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol @@ -22,7 +22,7 @@ interface IBiconomySponsorshipPaymaster{ function setFeeCollector(address _newFeeCollector) external payable; - function setUnaccountedGas(uint48 value) external payable; + function setUnaccountedGas(uint16 value) external payable; function withdrawERC20(IERC20 token, address target, uint256 amount) external; diff --git a/contracts/interfaces/IBiconomyTokenPaymaster.sol b/contracts/interfaces/IBiconomyTokenPaymaster.sol index 2347f38..59a7afa 100644 --- a/contracts/interfaces/IBiconomyTokenPaymaster.sol +++ b/contracts/interfaces/IBiconomyTokenPaymaster.sol @@ -2,5 +2,27 @@ pragma solidity ^0.8.26; interface IBiconomyTokenPaymaster { - -} \ No newline at end of file + enum ExchangeRateSource { + EXTERNAL_EXCHANGE_RATE, + ORACLE_BASED, + TWAP_BASED + } + + event UnaccountedGasChanged(uint256 indexed oldValue, uint256 indexed newValue); + event FixedDynamicAdjustmentChanged(uint32 indexed oldValue, uint32 indexed newValue); + event VerifyingSignerChanged(address indexed oldSigner, address indexed newSigner, address indexed actor); + event FeeCollectorChanged(address indexed oldFeeCollector, address indexed newFeeCollector, address indexed actor); + event GasDeposited(address indexed paymasterId, uint256 indexed value); + event GasWithdrawn(address indexed paymasterId, address indexed to, uint256 indexed value); + event GasBalanceDeducted(address indexed paymasterId, uint256 indexed charge, bytes32 indexed userOpHash); + event DynamicAdjustmentCollected(address indexed paymasterId, uint256 indexed dynamicAdjustment); + event Received(address indexed sender, uint256 value); + event TokensWithdrawn(address indexed token, address indexed to, uint256 indexed amount, address actor); + + + function setSigner(address _newVerifyingSigner) external payable; + + function setFeeCollector(address _newFeeCollector) external payable; + + function setUnaccountedGas(uint16 value) external payable; +} diff --git a/contracts/sponsorship/BiconomySponsorshipPaymaster.sol b/contracts/sponsorship/BiconomySponsorshipPaymaster.sol index 7faa2d7..9c4c9b8 100644 --- a/contracts/sponsorship/BiconomySponsorshipPaymaster.sol +++ b/contracts/sponsorship/BiconomySponsorshipPaymaster.sol @@ -40,14 +40,13 @@ contract BiconomySponsorshipPaymaster is address public verifyingSigner; address public feeCollector; - uint48 public unaccountedGas; + uint16 public unaccountedGas; uint32 private constant PRICE_DENOMINATOR = 1e6; - // note: could rename to PAYMASTER_ID_OFFSET + // Offset in PaymasterAndData to get to PAYMASTER_ID_OFFSET uint256 private constant PAYMASTER_ID_OFFSET = PAYMASTER_DATA_OFFSET; - // Limit for unaccounted gas cost - uint16 private constant UNACCOUNTED_GAS_LIMIT = 10_000; + uint16 private constant UNACCOUNTED_GAS_LIMIT = 50_000; mapping(address => uint256) public paymasterIdBalances; @@ -56,18 +55,14 @@ contract BiconomySponsorshipPaymaster is IEntryPoint _entryPoint, address _verifyingSigner, address _feeCollector, - uint48 _unaccountedGas + uint16 _unaccountedGas ) BasePaymaster(_owner, _entryPoint) { - if (_verifyingSigner == address(0)) { - revert VerifyingSignerCanNotBeZero(); - } else if (_feeCollector == address(0)) { - revert FeeCollectorCanNotBeZero(); - } else if (_unaccountedGas > UNACCOUNTED_GAS_LIMIT) { - revert UnaccountedGasTooHigh(); + _checkConstructorArgs(_verifyingSigner, _feeCollector, _unaccountedGas); + assembly ("memory-safe") { + sstore(verifyingSigner.slot, _verifyingSigner) } - verifyingSigner = _verifyingSigner; feeCollector = _feeCollector; unaccountedGas = _unaccountedGas; } @@ -97,7 +92,7 @@ contract BiconomySponsorshipPaymaster is * After setting the new signer address, it will emit an event VerifyingSignerChanged. */ function setSigner(address _newVerifyingSigner) external payable override onlyOwner { - if (isContract(_newVerifyingSigner)) revert VerifyingSignerCanNotBeContract(); + if (_isContract(_newVerifyingSigner)) revert VerifyingSignerCanNotBeContract(); if (_newVerifyingSigner == address(0)) { revert VerifyingSignerCanNotBeZero(); } @@ -116,6 +111,7 @@ contract BiconomySponsorshipPaymaster is * After setting the new fee collector address, it will emit an event FeeCollectorChanged. */ function setFeeCollector(address _newFeeCollector) external payable override onlyOwner { + if (_isContract(_newFeeCollector)) revert FeeCollectorCanNotBeContract(); if (_newFeeCollector == address(0)) revert FeeCollectorCanNotBeZero(); address oldFeeCollector = feeCollector; feeCollector = _newFeeCollector; @@ -127,11 +123,11 @@ contract BiconomySponsorshipPaymaster is * @param value The new value to be set as the unaccountedEPGasOverhead. * @notice only to be called by the owner of the contract. */ - function setUnaccountedGas(uint48 value) external payable override onlyOwner { + function setUnaccountedGas(uint16 value) external payable override onlyOwner { if (value > UNACCOUNTED_GAS_LIMIT) { revert UnaccountedGasTooHigh(); } - uint256 oldValue = unaccountedGas; + uint16 oldValue = unaccountedGas; unaccountedGas = value; emit UnaccountedGasChanged(oldValue, value); } @@ -139,7 +135,7 @@ contract BiconomySponsorshipPaymaster is /** * @dev Override the default implementation. */ - function deposit() external payable override virtual { + function deposit() external payable virtual override { revert UseDepositForInstead(); } @@ -346,4 +342,18 @@ contract BiconomySponsorshipPaymaster is SafeTransferLib.safeTransfer(address(token), target, amount); emit TokensWithdrawn(address(token), target, amount, msg.sender); } + + function _checkConstructorArgs(address _verifyingSigner, address _feeCollector, uint16 _unaccountedGas) internal view { + if (_verifyingSigner == address(0)) { + revert VerifyingSignerCanNotBeZero(); + } else if (_isContract(_verifyingSigner)) { + revert VerifyingSignerCanNotBeContract(); + } else if (_feeCollector == address(0)) { + revert FeeCollectorCanNotBeZero(); + } else if (_isContract(_feeCollector)) { + revert FeeCollectorCanNotBeContract(); + } else if (_unaccountedGas > UNACCOUNTED_GAS_LIMIT) { + revert UnaccountedGasTooHigh(); + } + } } diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index d8cd1d0..20263f4 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -10,6 +10,16 @@ import { BasePaymaster } from "../base/BasePaymaster.sol"; import { BiconomyTokenPaymasterErrors } from "../common/BiconomyTokenPaymasterErrors.sol"; import { IBiconomyTokenPaymaster } from "../interfaces/IBiconomyTokenPaymaster.sol"; +/** + * @title BiconomyTokenPaymaster + * @author ShivaanshK + * @author livingrockrises + * @notice Token Paymaster for v0.7 Entry Point + * @dev A paymaster that allows user to pay gas fee in ERC20 tokens. The paymaster owner chooses which tokens to + * accept. The payment manager (usually the owner) first deposits native gas into the EntryPoint. Then, for each + * transaction, it takes the gas fee from the user's ERC20 token balance. The exchange rate between ETH and the token is + * calculated using 1 of three methods: external price source, off-chain oracle, or a TWAP oracle. + */ contract BiconomyTokenPaymaster is BasePaymaster, ReentrancyGuard, @@ -19,12 +29,86 @@ contract BiconomyTokenPaymaster is using UserOperationLib for PackedUserOperation; using SignatureCheckerLib for address; + address public verifyingSigner; + address public feeCollector; + uint16 public unaccountedGas; + + // Limit for unaccounted gas cost + uint16 private constant UNACCOUNTED_GAS_LIMIT = 50_000; + constructor( address _owner, - IEntryPoint _entryPoint + IEntryPoint _entryPoint, + address _verifyingSigner, + address _feeCollector, + uint16 _unaccountedGas ) BasePaymaster(_owner, _entryPoint) - { } + { + _checkConstructorArgs(_verifyingSigner, _feeCollector, _unaccountedGas); + assembly ("memory-safe") { + sstore(verifyingSigner.slot, _verifyingSigner) + } + verifyingSigner = _verifyingSigner; + feeCollector = _feeCollector; + unaccountedGas = _unaccountedGas; + } + + /** + * @dev Set a new verifying signer address. + * Can only be called by the owner of the contract. + * @param _newVerifyingSigner The new address to be set as the verifying signer. + * @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 override onlyOwner { + if (_isContract(_newVerifyingSigner)) revert VerifyingSignerCanNotBeContract(); + if (_newVerifyingSigner == address(0)) { + revert VerifyingSignerCanNotBeZero(); + } + address oldSigner = verifyingSigner; + assembly ("memory-safe") { + sstore(verifyingSigner.slot, _newVerifyingSigner) + } + emit VerifyingSignerChanged(oldSigner, _newVerifyingSigner, msg.sender); + } + + /** + * @dev Set a new fee collector address. + * Can only be called by the owner of the contract. + * @param _newFeeCollector The new address to be set as the fee collector. + * @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 override onlyOwner { + if (_isContract(_newFeeCollector)) revert FeeCollectorCanNotBeContract(); + if (_newFeeCollector == address(0)) revert FeeCollectorCanNotBeZero(); + address oldFeeCollector = feeCollector; + feeCollector = _newFeeCollector; + emit FeeCollectorChanged(oldFeeCollector, _newFeeCollector, msg.sender); + } + + /** + * @dev Set a new unaccountedEPGasOverhead value. + * @param value The new value to be set as the unaccountedEPGasOverhead. + * @notice only to be called by the owner of the contract. + */ + function setUnaccountedGas(uint16 value) external payable override onlyOwner { + if (value > UNACCOUNTED_GAS_LIMIT) { + revert UnaccountedGasTooHigh(); + } + uint16 oldValue = unaccountedGas; + unaccountedGas = value; + emit UnaccountedGasChanged(oldValue, value); + } + + /** + * Add a deposit in native currency for this paymaster, used for paying for transaction fees. + * This is ideally done by the entity who is managing the received ERC20 gas tokens. + */ + function deposit() public payable virtual override nonReentrant { + entryPoint.depositTo{ value: msg.value }(address(this)); + } /** * @dev Validate a user operation. @@ -64,4 +148,25 @@ contract BiconomyTokenPaymaster is { // Implementation of post-operation logic } + + function _checkConstructorArgs( + address _verifyingSigner, + address _feeCollector, + uint16 _unaccountedGas + ) + internal + view + { + if (_verifyingSigner == address(0)) { + revert VerifyingSignerCanNotBeZero(); + } else if (_isContract(_verifyingSigner)) { + revert VerifyingSignerCanNotBeContract(); + } else if (_feeCollector == address(0)) { + revert FeeCollectorCanNotBeZero(); + } else if (_isContract(_feeCollector)) { + revert FeeCollectorCanNotBeContract(); + } else if (_unaccountedGas > UNACCOUNTED_GAS_LIMIT) { + revert UnaccountedGasTooHigh(); + } + } } diff --git a/test/base/BaseEventsAndErrors.sol b/test/base/BaseEventsAndErrors.sol index 187a880..021e399 100644 --- a/test/base/BaseEventsAndErrors.sol +++ b/test/base/BaseEventsAndErrors.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Unlicensed pragma solidity ^0.8.26; -import { EventsAndErrors } from "nexus/test/foundry/utils/EventsAndErrors.sol"; -import { BiconomySponsorshipPaymasterErrors } from "./../../../contracts/common/BiconomySponsorshipPaymasterErrors.sol"; +import { EventsAndErrors } from "@nexus/test/foundry/utils/EventsAndErrors.sol"; +import { BiconomySponsorshipPaymasterErrors } from "../../contracts/common/BiconomySponsorshipPaymasterErrors.sol"; contract BaseEventsAndErrors is EventsAndErrors, BiconomySponsorshipPaymasterErrors { // ========================== diff --git a/test/base/TestBase.sol b/test/base/TestBase.sol index 427bfe8..cd8c222 100644 --- a/test/base/TestBase.sol +++ b/test/base/TestBase.sol @@ -4,25 +4,25 @@ 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 { EntryPoint } from "account-abstraction/contracts/core/EntryPoint.sol"; -import { IEntryPoint } from "account-abstraction/contracts/interfaces/IEntryPoint.sol"; -import { IAccount } from "account-abstraction/contracts/interfaces/IAccount.sol"; -import { Exec } from "account-abstraction/contracts/utils/Exec.sol"; -import { IPaymaster } from "account-abstraction/contracts/interfaces/IPaymaster.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 { BootstrapLib } from "nexus/contracts/lib/BootstrapLib.sol"; -import { Bootstrap, BootstrapConfig } from "nexus/contracts/utils/Bootstrap.sol"; -import { CheatCodes } from "nexus/test/foundry/utils/CheatCodes.sol"; +import "@solady/src/utils/ECDSA.sol"; + +import { EntryPoint } from "@account-abstraction/contracts/core/EntryPoint.sol"; +import { IEntryPoint } from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; +import { IAccount } from "@account-abstraction/contracts/interfaces/IAccount.sol"; +import { Exec } from "@account-abstraction/contracts/utils/Exec.sol"; +import { IPaymaster } from "@account-abstraction/contracts/interfaces/IPaymaster.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 { BootstrapLib } from "@nexus/contracts/lib/BootstrapLib.sol"; +import { Bootstrap, BootstrapConfig } from "@nexus/contracts/utils/Bootstrap.sol"; +import { CheatCodes } from "@nexus/test/foundry/utils/CheatCodes.sol"; import { BaseEventsAndErrors } from "./BaseEventsAndErrors.sol"; -import { BiconomySponsorshipPaymaster } from "../../../contracts/sponsorship/BiconomySponsorshipPaymaster.sol"; +import { BiconomySponsorshipPaymaster } from "../../contracts/sponsorship/BiconomySponsorshipPaymaster.sol"; abstract contract TestBase is CheatCodes, BaseEventsAndErrors { // ----------------------------------------- @@ -409,7 +409,7 @@ abstract contract TestBase is CheatCodes, BaseEventsAndErrors { vm.startPrank(paymaster.owner()); // Set unaccounted gas to be gas used in postop + 1000 for EP overhead and penalty - paymaster.setUnaccountedGas(uint48(postopGasUsed + 1000)); + paymaster.setUnaccountedGas(uint16(postopGasUsed + 1000)); vm.stopPrank(); // Ammend the userop to have new gas limits and signature diff --git a/test/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol b/test/unit/concrete/TestSponsorshipPaymaster.t.sol similarity index 93% rename from test/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol rename to test/unit/concrete/TestSponsorshipPaymaster.t.sol index 05b9362..3457c50 100644 --- a/test/unit/concrete/TestSponsorshipPaymasterWithDynamicAdjustmentTest.t.sol +++ b/test/unit/concrete/TestSponsorshipPaymaster.t.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.26; import { TestBase } from "../../base/TestBase.sol"; -import { IBiconomySponsorshipPaymaster } from "../../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; -import { BiconomySponsorshipPaymaster } from "../../../../contracts/sponsorship/BiconomySponsorshipPaymaster.sol"; -import { PackedUserOperation } from "account-abstraction/contracts/core/UserOperationLib.sol"; -import { MockToken } from "./../../../../lib/nexus/contracts/mocks/MockToken.sol"; +import { IBiconomySponsorshipPaymaster } from "../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; +import { BiconomySponsorshipPaymaster } from "../../../contracts/sponsorship/BiconomySponsorshipPaymaster.sol"; +import { PackedUserOperation } from "@account-abstraction/contracts/core/UserOperationLib.sol"; +import { MockToken } from "@nexus/contracts/mocks/MockToken.sol"; contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { BiconomySponsorshipPaymaster public bicoPaymaster; @@ -36,15 +36,28 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { ); } + function test_RevertIf_DeployWithSignerAsContract() external { + vm.expectRevert(abi.encodeWithSelector(VerifyingSignerCanNotBeContract.selector)); + new BiconomySponsorshipPaymaster( + PAYMASTER_OWNER.addr, ENTRYPOINT, address(ENTRYPOINT), PAYMASTER_FEE_COLLECTOR.addr, 7e3 + ); + } + + function test_RevertIf_DeployWithFeeCollectorSetToZero() external { vm.expectRevert(abi.encodeWithSelector(FeeCollectorCanNotBeZero.selector)); new BiconomySponsorshipPaymaster(PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, address(0), 7e3); } + function test_RevertIf_DeployWithFeeCollectorAsContract() external { + vm.expectRevert(abi.encodeWithSelector(FeeCollectorCanNotBeContract.selector)); + new BiconomySponsorshipPaymaster(PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, address(ENTRYPOINT), 7e3); + } + function test_RevertIf_DeployWithUnaccountedGasCostTooHigh() external { vm.expectRevert(abi.encodeWithSelector(UnaccountedGasTooHigh.selector)); new BiconomySponsorshipPaymaster( - PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr, 10_001 + PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr, 50_001 ); } @@ -117,8 +130,8 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { } function test_SetUnaccountedGas() external prankModifier(PAYMASTER_OWNER.addr) { - uint48 initialUnaccountedGas = bicoPaymaster.unaccountedGas(); - uint48 newUnaccountedGas = 5000; + uint16 initialUnaccountedGas = bicoPaymaster.unaccountedGas(); + uint16 newUnaccountedGas = 5000; vm.expectEmit(true, true, false, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.UnaccountedGasChanged(initialUnaccountedGas, newUnaccountedGas); @@ -129,7 +142,7 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { } function test_RevertIf_SetUnaccountedGasToHigh() external prankModifier(PAYMASTER_OWNER.addr) { - uint48 newUnaccountedGas = 10_001; + uint16 newUnaccountedGas = 50_001; vm.expectRevert(abi.encodeWithSelector(UnaccountedGasTooHigh.selector)); bicoPaymaster.setUnaccountedGas(newUnaccountedGas); } diff --git a/test/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol b/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol similarity index 94% rename from test/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol rename to test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol index bac7265..1cf605a 100644 --- a/test/unit/fuzz/TestFuzz_TestSponsorshipPaymasterWithPremium.t.sol +++ b/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.26; import { TestBase } from "../../base/TestBase.sol"; -import { IBiconomySponsorshipPaymaster } from "../../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; -import { BiconomySponsorshipPaymaster } from "../../../../contracts/sponsorship/BiconomySponsorshipPaymaster.sol"; -import { MockToken } from "./../../../../lib/nexus/contracts/mocks/MockToken.sol"; -import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol"; +import { IBiconomySponsorshipPaymaster } from "../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; +import { BiconomySponsorshipPaymaster } from "../../../contracts/sponsorship/BiconomySponsorshipPaymaster.sol"; +import { MockToken } from "@nexus/contracts/mocks/MockToken.sol"; +import { PackedUserOperation } from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol"; contract TestFuzz_SponsorshipPaymasterWithDynamicAdjustment is TestBase { BiconomySponsorshipPaymaster public bicoPaymaster; From b944e24dd24e2caba8f32f970078ea14af7eb820 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Fri, 30 Aug 2024 16:15:44 +0400 Subject: [PATCH 48/69] deposit and withdraw functions --- .../common/BiconomyTokenPaymasterErrors.sol | 16 ++- .../BiconomySponsorshipPaymaster.sol | 11 +- contracts/token/BiconomyTokenPaymaster.sol | 127 ++++++++++++++---- 3 files changed, 121 insertions(+), 33 deletions(-) diff --git a/contracts/common/BiconomyTokenPaymasterErrors.sol b/contracts/common/BiconomyTokenPaymasterErrors.sol index 1aa0cbd..dc31037 100644 --- a/contracts/common/BiconomyTokenPaymasterErrors.sol +++ b/contracts/common/BiconomyTokenPaymasterErrors.sol @@ -12,17 +12,23 @@ contract BiconomyTokenPaymasterErrors { */ error FeeCollectorCanNotBeZero(); - /** - * @notice Throws when the fee collector address provided is a deployed contract - */ - error FeeCollectorCanNotBeContract(); - /** * @notice Throws when the fee collector address provided is a deployed contract */ error VerifyingSignerCanNotBeContract(); + /** * @notice Throws when trying unaccountedGas is too high */ error UnaccountedGasTooHigh(); + + /** + * @notice Throws when trying to withdraw to address(0) + */ + error CanNotWithdrawToZeroAddress(); + + /** + * @notice Throws when trying to withdraw multiple tokens, but each token doesn't have a corresponding amount + */ + error TokensAndAmountsLengthMismatch(); } diff --git a/contracts/sponsorship/BiconomySponsorshipPaymaster.sol b/contracts/sponsorship/BiconomySponsorshipPaymaster.sol index 9c4c9b8..f447cda 100644 --- a/contracts/sponsorship/BiconomySponsorshipPaymaster.sol +++ b/contracts/sponsorship/BiconomySponsorshipPaymaster.sol @@ -10,7 +10,7 @@ import { SignatureCheckerLib } from "@solady/src/utils/SignatureCheckerLib.sol"; import { ECDSA as ECDSA_solady } from "@solady/src/utils/ECDSA.sol"; import { BiconomySponsorshipPaymasterErrors } from "../common/BiconomySponsorshipPaymasterErrors.sol"; import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeTransferLib } from "@solady/src/utils/SafeTransferLib.sol"; import { IBiconomySponsorshipPaymaster } from "../interfaces/IBiconomySponsorshipPaymaster.sol"; @@ -343,7 +343,14 @@ contract BiconomySponsorshipPaymaster is emit TokensWithdrawn(address(token), target, amount, msg.sender); } - function _checkConstructorArgs(address _verifyingSigner, address _feeCollector, uint16 _unaccountedGas) internal view { + function _checkConstructorArgs( + address _verifyingSigner, + address _feeCollector, + uint16 _unaccountedGas + ) + internal + view + { if (_verifyingSigner == address(0)) { revert VerifyingSignerCanNotBeZero(); } else if (_isContract(_verifyingSigner)) { diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index 20263f4..4ec2662 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -6,6 +6,8 @@ import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.s import { IEntryPoint } from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; import { SignatureCheckerLib } from "@solady/src/utils/SignatureCheckerLib.sol"; import { PackedUserOperation, UserOperationLib } from "@account-abstraction/contracts/core/UserOperationLib.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeTransferLib } from "@solady/src/utils/SafeTransferLib.sol"; import { BasePaymaster } from "../base/BasePaymaster.sol"; import { BiconomyTokenPaymasterErrors } from "../common/BiconomyTokenPaymasterErrors.sol"; import { IBiconomyTokenPaymaster } from "../interfaces/IBiconomyTokenPaymaster.sol"; @@ -14,7 +16,7 @@ import { IBiconomyTokenPaymaster } from "../interfaces/IBiconomyTokenPaymaster.s * @title BiconomyTokenPaymaster * @author ShivaanshK * @author livingrockrises - * @notice Token Paymaster for v0.7 Entry Point + * @notice Token Paymaster for Entry Point v0.7 * @dev A paymaster that allows user to pay gas fee in ERC20 tokens. The paymaster owner chooses which tokens to * accept. The payment manager (usually the owner) first deposits native gas into the EntryPoint. Then, for each * transaction, it takes the gas fee from the user's ERC20 token balance. The exchange rate between ETH and the token is @@ -40,21 +42,109 @@ contract BiconomyTokenPaymaster is address _owner, IEntryPoint _entryPoint, address _verifyingSigner, - address _feeCollector, uint16 _unaccountedGas ) BasePaymaster(_owner, _entryPoint) { - _checkConstructorArgs(_verifyingSigner, _feeCollector, _unaccountedGas); + _checkConstructorArgs(_verifyingSigner, _unaccountedGas); assembly ("memory-safe") { sstore(verifyingSigner.slot, _verifyingSigner) } verifyingSigner = _verifyingSigner; - feeCollector = _feeCollector; + feeCollector = address(this); // initialize fee collector to this contract unaccountedGas = _unaccountedGas; } - /** + /** + * Add a deposit in native currency for this paymaster, used for paying for transaction fees. + * This is ideally done by the entity who is managing the received ERC20 gas tokens. + */ + function deposit() public payable virtual override nonReentrant { + entryPoint.depositTo{ value: msg.value }(address(this)); + } + + /** + * @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 onlyOwner nonReentrant { + if (withdrawAddress == address(0)) revert CanNotWithdrawToZeroAddress(); + entryPoint.withdrawTo(withdrawAddress, amount); + } + + /** + * @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) external payable onlyOwner nonReentrant { + _withdrawERC20(token, target, amount); + } + + /** + * @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 + */ + function withdrawERC20Full(IERC20 token, address target) external payable onlyOwner nonReentrant { + uint256 amount = token.balanceOf(address(this)); + _withdrawERC20(token, target, amount); + } + + /** + * @dev pull multiple tokens out of paymaster in case they were sent to the paymaster at any point. + * @param token the tokens deposit to withdraw + * @param target address to send to + * @param amount amounts to withdraw + */ + function withdrawMultipleERC20( + IERC20[] calldata token, + address target, + uint256[] calldata amount + ) + external + payable + onlyOwner + nonReentrant + { + if (token.length != amount.length) { + revert TokensAndAmountsLengthMismatch(); + } + unchecked { + for (uint256 i; i < token.length;) { + _withdrawERC20(token[i], target, amount[i]); + ++i; + } + } + } + + /** + * @dev pull multiple tokens out of paymaster in case they were sent to the paymaster at any point. + * @param token the tokens deposit to withdraw + * @param target address to send to + */ + function withdrawMultipleERC20Full( + IERC20[] calldata token, + address target + ) + external + payable + onlyOwner + nonReentrant + { + unchecked { + for (uint256 i; i < token.length;) { + uint256 amount = token[i].balanceOf(address(this)); + _withdrawERC20(token[i], target, amount); + ++i; + } + } + } + + /** * @dev Set a new verifying signer address. * Can only be called by the owner of the contract. * @param _newVerifyingSigner The new address to be set as the verifying signer. @@ -81,7 +171,6 @@ contract BiconomyTokenPaymaster is * After setting the new fee collector address, it will emit an event FeeCollectorChanged. */ function setFeeCollector(address _newFeeCollector) external payable override onlyOwner { - if (_isContract(_newFeeCollector)) revert FeeCollectorCanNotBeContract(); if (_newFeeCollector == address(0)) revert FeeCollectorCanNotBeZero(); address oldFeeCollector = feeCollector; feeCollector = _newFeeCollector; @@ -102,14 +191,6 @@ contract BiconomyTokenPaymaster is emit UnaccountedGasChanged(oldValue, value); } - /** - * Add a deposit in native currency for this paymaster, used for paying for transaction fees. - * This is ideally done by the entity who is managing the received ERC20 gas tokens. - */ - function deposit() public payable virtual override nonReentrant { - entryPoint.depositTo{ value: msg.value }(address(this)); - } - /** * @dev Validate a user operation. * This method is abstract in BasePaymaster and must be implemented in derived contracts. @@ -149,24 +230,18 @@ contract BiconomyTokenPaymaster is // Implementation of post-operation logic } - function _checkConstructorArgs( - address _verifyingSigner, - address _feeCollector, - uint16 _unaccountedGas - ) - internal - view - { + function _checkConstructorArgs(address _verifyingSigner, uint16 _unaccountedGas) internal view { if (_verifyingSigner == address(0)) { revert VerifyingSignerCanNotBeZero(); } else if (_isContract(_verifyingSigner)) { revert VerifyingSignerCanNotBeContract(); - } else if (_feeCollector == address(0)) { - revert FeeCollectorCanNotBeZero(); - } else if (_isContract(_feeCollector)) { - revert FeeCollectorCanNotBeContract(); } else if (_unaccountedGas > UNACCOUNTED_GAS_LIMIT) { revert UnaccountedGasTooHigh(); } } + + function _withdrawERC20(IERC20 token, address target, uint256 amount) private { + if (target == address(0)) revert CanNotWithdrawToZeroAddress(); + SafeTransferLib.safeTransfer(address(token), target, amount); + } } From 7d47e18f1c9ba18e10bcf6293f35affb72825953 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Mon, 2 Sep 2024 12:58:59 +0400 Subject: [PATCH 49/69] hashing and parsing PND --- .../common/BiconomyTokenPaymasterErrors.sol | 5 + .../interfaces/IBiconomyTokenPaymaster.sol | 7 +- contracts/token/BiconomyTokenPaymaster.sol | 103 +++++++++++++++++- 3 files changed, 110 insertions(+), 5 deletions(-) diff --git a/contracts/common/BiconomyTokenPaymasterErrors.sol b/contracts/common/BiconomyTokenPaymasterErrors.sol index dc31037..e034e77 100644 --- a/contracts/common/BiconomyTokenPaymasterErrors.sol +++ b/contracts/common/BiconomyTokenPaymasterErrors.sol @@ -31,4 +31,9 @@ contract BiconomyTokenPaymasterErrors { * @notice Throws when trying to withdraw multiple tokens, but each token doesn't have a corresponding amount */ error TokensAndAmountsLengthMismatch(); + + /** + * @notice Throws when invalid signature length in paymasterAndData + */ + error InvalidSignatureLength(); } diff --git a/contracts/interfaces/IBiconomyTokenPaymaster.sol b/contracts/interfaces/IBiconomyTokenPaymaster.sol index 59a7afa..97ef79c 100644 --- a/contracts/interfaces/IBiconomyTokenPaymaster.sol +++ b/contracts/interfaces/IBiconomyTokenPaymaster.sol @@ -2,10 +2,9 @@ pragma solidity ^0.8.26; interface IBiconomyTokenPaymaster { - enum ExchangeRateSource { - EXTERNAL_EXCHANGE_RATE, - ORACLE_BASED, - TWAP_BASED + enum PriceSource { + EXTERNAL, + ORACLE } event UnaccountedGasChanged(uint256 indexed oldValue, uint256 indexed newValue); diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index 4ec2662..837cc7d 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -11,6 +11,8 @@ import { SafeTransferLib } from "@solady/src/utils/SafeTransferLib.sol"; import { BasePaymaster } from "../base/BasePaymaster.sol"; import { BiconomyTokenPaymasterErrors } from "../common/BiconomyTokenPaymasterErrors.sol"; import { IBiconomyTokenPaymaster } from "../interfaces/IBiconomyTokenPaymaster.sol"; +import "@account-abstraction/contracts/core/Helpers.sol"; + /** * @title BiconomyTokenPaymaster @@ -191,6 +193,78 @@ contract BiconomyTokenPaymaster is emit UnaccountedGasChanged(oldValue, value); } + /** + * 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. + * it is called on-chain from the validatePaymasterUserOp, to validate the signature. + * note that this signature covers all fields of the UserOperation, except the "paymasterAndData", + * which will carry the signature itself. + */ + function getHash( + PackedUserOperation calldata userOp, + PriceSource priceSource, + uint48 validUntil, + uint48 validAfter, + address feeToken, + address oracleAggregator, + uint256 exchangeRate, + uint32 priceMarkup + ) + public + view + returns (bytes32) + { + //can't use userOp.hash(), since it contains also the paymasterAndData itself. + address sender = userOp.getSender(); + 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])), + userOp.preVerificationGas, + userOp.gasFees, + block.chainid, + address(this), + priceSource, // treated as a uint8 + validUntil, + validAfter, + feeToken, + oracleAggregator, + exchangeRate, + priceMarkup + ) + ); + } + + function parsePaymasterAndData(bytes calldata paymasterAndData) + public + pure + returns ( + PriceSource priceSource, + uint48 validUntil, + uint48 validAfter, + address feeToken, + address oracleAggregator, + uint256 exchangeRate, + uint32 priceMarkup, + bytes calldata signature + ) + { + unchecked { + priceSource = PriceSource(uint8(bytes1(paymasterAndData[PAYMASTER_DATA_OFFSET]))); + validUntil = uint48(bytes6(paymasterAndData[PAYMASTER_DATA_OFFSET + 1:PAYMASTER_DATA_OFFSET + 7])); + validAfter = uint48(bytes6(paymasterAndData[PAYMASTER_DATA_OFFSET + 7:PAYMASTER_DATA_OFFSET + 13])); + feeToken = address(bytes20(paymasterAndData[PAYMASTER_DATA_OFFSET + 13:PAYMASTER_DATA_OFFSET + 33])); + oracleAggregator = address(bytes20(paymasterAndData[PAYMASTER_DATA_OFFSET + 33:PAYMASTER_DATA_OFFSET + 53])); + exchangeRate = uint256(bytes32(paymasterAndData[PAYMASTER_DATA_OFFSET + 53:PAYMASTER_DATA_OFFSET + 85])); + priceMarkup = uint32(bytes4(paymasterAndData[PAYMASTER_DATA_OFFSET + 85:PAYMASTER_DATA_OFFSET + 89])); + signature = paymasterAndData[PAYMASTER_DATA_OFFSET + 89:]; + } + } + /** * @dev Validate a user operation. * This method is abstract in BasePaymaster and must be implemented in derived contracts. @@ -207,7 +281,34 @@ contract BiconomyTokenPaymaster is override returns (bytes memory context, uint256 validationData) { - // Implementation of user operation validation logic + // review: in this method try to resolve stack too deep (though via-ir is good enough) + ( + PriceSource priceSource, + uint48 validUntil, + uint48 validAfter, + address feeToken, + address oracleAggregator, + uint256 exchangeRate, + uint32 priceMarkup, + bytes calldata signature + ) = parsePaymasterAndData(userOp.paymasterAndData); + + if (signature.length != 64 && signature.length != 65) { + revert InvalidSignatureLength(); + } + + bool validSig = verifyingSigner.isValidSignatureNow( + ECDSA_solady.toEthSignedMessageHash( + getHash( + userOp, priceSource, validUntil, validAfter, feeToken, oracleAggregator, exchangeRate, priceMarkup + ) + ), + signature + ); + + if (!validSig) { + return ("", _packValidationData(true, validUntil, validAfter)); + } } /** From c2d52d05e95cb1ff60041b78b62cd92a99d30a56 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Wed, 4 Sep 2024 18:22:15 +0400 Subject: [PATCH 50/69] debugging rn --- .gitmodules | 11 +- .../references/SampleVerifyingPaymaster.sol | 119 -------- .../BiconomySponsorshipPaymaster.sol | 4 +- contracts/token/BiconomyTokenPaymaster.sol | 24 +- foundry.toml | 1 - lib/account-abstraction | 1 + lib/forge-std | 2 +- lib/nexus | 2 +- lib/nexus.git | 1 - lib/openzeppelin-contracts | 1 + lib/v3-core | 2 +- lib/v3-periphery | 2 +- remappings.txt | 11 +- test/base/TestBase.sol | 280 +----------------- .../concrete/TestSponsorshipPaymaster.t.sol | 34 +-- .../TestFuzz_TestSponsorshipPaymaster.t.sol | 8 +- 16 files changed, 63 insertions(+), 440 deletions(-) delete mode 100644 contracts/references/SampleVerifyingPaymaster.sol create mode 160000 lib/account-abstraction delete mode 160000 lib/nexus.git create mode 160000 lib/openzeppelin-contracts diff --git a/.gitmodules b/.gitmodules index a2db065..72d8123 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,7 @@ [submodule "lib/nexus"] path = lib/nexus - url = https://github.com/bcnmy/nexus + url = https://github.com/lib/nexus + branch = dev [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std @@ -10,3 +11,11 @@ [submodule "lib/v3-core"] path = lib/v3-core url = https://github.com/Uniswap/v3-core +[submodule "lib/account-abstraction"] + path = lib/account-abstraction + url = https://github.com/eth-infinitism/account-abstraction + branch = releases/v0.7 +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts + branch = master diff --git a/contracts/references/SampleVerifyingPaymaster.sol b/contracts/references/SampleVerifyingPaymaster.sol deleted file mode 100644 index 1522c6e..0000000 --- a/contracts/references/SampleVerifyingPaymaster.sol +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.26; - -/* solhint-disable reason-string */ -/* solhint-disable no-inline-assembly */ - -import "account-abstraction/contracts/core/BasePaymaster.sol"; -import "account-abstraction/contracts/core/UserOperationLib.sol"; -import "account-abstraction/contracts/core/Helpers.sol"; -import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; - -/** - * A sample paymaster that uses external service to decide whether to pay for the UserOp. - * The paymaster trusts an external signer to sign the transaction. - * The calling user must pass the UserOp to that external signer first, which performs - * whatever off-chain verification before signing the UserOp. - * Note that this signature is NOT a replacement for the account-specific signature: - * - the paymaster checks a signature to agree to PAY for GAS. - * - the account checks a signature to prove identity and account ownership. - */ -contract VerifyingPaymaster is BasePaymaster { - using UserOperationLib for PackedUserOperation; - - address public immutable verifyingSigner; - - uint256 private constant VALID_TIMESTAMP_OFFSET = PAYMASTER_DATA_OFFSET; - - uint256 private constant SIGNATURE_OFFSET = VALID_TIMESTAMP_OFFSET + 64; - - constructor(IEntryPoint _entryPoint, address _verifyingSigner) BasePaymaster(_entryPoint) { - verifyingSigner = _verifyingSigner; - } - - /** - * 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. - * it is called on-chain from the validatePaymasterUserOp, to validate the signature. - * 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) - { - //can't use userOp.hash(), since it contains also the paymasterAndData itself. - address sender = userOp.getSender(); - 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])), - userOp.preVerificationGas, - userOp.gasFees, - block.chainid, - address(this), - 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:]; - } - - /** - * verify our external signer signed this request. - * the "paymasterAndData" is expected to be the paymaster and a signature over the entire request params - * paymasterAndData[:20] : address(this) - * 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) - { - (requiredPreFund); - - (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" - ); - bytes32 hash = MessageHashUtils.toEthSignedMessageHash(getHash(userOp, validUntil, validAfter)); - - //don't revert on signature failure: return SIG_VALIDATION_FAILED - if (verifyingSigner != ECDSA.recover(hash, signature)) { - return ("", _packValidationData(true, validUntil, validAfter)); - } - - //no need for other on-chain validation: entire UserOp should have been checked - // by the external service prior to signing it. - return ("", _packValidationData(false, validUntil, validAfter)); - } -} diff --git a/contracts/sponsorship/BiconomySponsorshipPaymaster.sol b/contracts/sponsorship/BiconomySponsorshipPaymaster.sol index f447cda..76460de 100644 --- a/contracts/sponsorship/BiconomySponsorshipPaymaster.sol +++ b/contracts/sponsorship/BiconomySponsorshipPaymaster.sol @@ -9,7 +9,7 @@ import "@account-abstraction/contracts/core/Helpers.sol"; import { SignatureCheckerLib } from "@solady/src/utils/SignatureCheckerLib.sol"; import { ECDSA as ECDSA_solady } from "@solady/src/utils/ECDSA.sol"; import { BiconomySponsorshipPaymasterErrors } from "../common/BiconomySponsorshipPaymasterErrors.sol"; -import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import { ReentrancyGuardTransient } from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeTransferLib } from "@solady/src/utils/SafeTransferLib.sol"; import { IBiconomySponsorshipPaymaster } from "../interfaces/IBiconomySponsorshipPaymaster.sol"; @@ -31,7 +31,7 @@ import { IBiconomySponsorshipPaymaster } from "../interfaces/IBiconomySponsorshi contract BiconomySponsorshipPaymaster is BasePaymaster, - ReentrancyGuard, + ReentrancyGuardTransient, BiconomySponsorshipPaymasterErrors, IBiconomySponsorshipPaymaster { diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index 837cc7d..a28a398 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.26; import { ECDSA as ECDSA_solady } from "@solady/src/utils/ECDSA.sol"; -import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import { ReentrancyGuardTransient } from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; import { IEntryPoint } from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; import { SignatureCheckerLib } from "@solady/src/utils/SignatureCheckerLib.sol"; import { PackedUserOperation, UserOperationLib } from "@account-abstraction/contracts/core/UserOperationLib.sol"; @@ -19,14 +19,14 @@ import "@account-abstraction/contracts/core/Helpers.sol"; * @author ShivaanshK * @author livingrockrises * @notice Token Paymaster for Entry Point v0.7 - * @dev A paymaster that allows user to pay gas fee in ERC20 tokens. The paymaster owner chooses which tokens to + * @dev A paymaster that allows user to pay gas fees in ERC20 tokens. The paymaster owner chooses which tokens to * accept. The payment manager (usually the owner) first deposits native gas into the EntryPoint. Then, for each * transaction, it takes the gas fee from the user's ERC20 token balance. The exchange rate between ETH and the token is * calculated using 1 of three methods: external price source, off-chain oracle, or a TWAP oracle. */ contract BiconomyTokenPaymaster is BasePaymaster, - ReentrancyGuard, + ReentrancyGuardTransient, BiconomyTokenPaymasterErrors, IBiconomyTokenPaymaster { @@ -208,7 +208,7 @@ contract BiconomyTokenPaymaster is address feeToken, address oracleAggregator, uint256 exchangeRate, - uint32 priceMarkup + uint32 dynamicAdjustment ) public view @@ -234,7 +234,7 @@ contract BiconomyTokenPaymaster is feeToken, oracleAggregator, exchangeRate, - priceMarkup + dynamicAdjustment ) ); } @@ -249,7 +249,7 @@ contract BiconomyTokenPaymaster is address feeToken, address oracleAggregator, uint256 exchangeRate, - uint32 priceMarkup, + uint32 dynamicAdjustment, bytes calldata signature ) { @@ -260,7 +260,7 @@ contract BiconomyTokenPaymaster is feeToken = address(bytes20(paymasterAndData[PAYMASTER_DATA_OFFSET + 13:PAYMASTER_DATA_OFFSET + 33])); oracleAggregator = address(bytes20(paymasterAndData[PAYMASTER_DATA_OFFSET + 33:PAYMASTER_DATA_OFFSET + 53])); exchangeRate = uint256(bytes32(paymasterAndData[PAYMASTER_DATA_OFFSET + 53:PAYMASTER_DATA_OFFSET + 85])); - priceMarkup = uint32(bytes4(paymasterAndData[PAYMASTER_DATA_OFFSET + 85:PAYMASTER_DATA_OFFSET + 89])); + dynamicAdjustment = uint32(bytes4(paymasterAndData[PAYMASTER_DATA_OFFSET + 85:PAYMASTER_DATA_OFFSET + 89])); signature = paymasterAndData[PAYMASTER_DATA_OFFSET + 89:]; } } @@ -289,7 +289,7 @@ contract BiconomyTokenPaymaster is address feeToken, address oracleAggregator, uint256 exchangeRate, - uint32 priceMarkup, + uint32 dynamicAdjustment, bytes calldata signature ) = parsePaymasterAndData(userOp.paymasterAndData); @@ -300,27 +300,29 @@ contract BiconomyTokenPaymaster is bool validSig = verifyingSigner.isValidSignatureNow( ECDSA_solady.toEthSignedMessageHash( getHash( - userOp, priceSource, validUntil, validAfter, feeToken, oracleAggregator, exchangeRate, priceMarkup + userOp, priceSource, validUntil, validAfter, feeToken, oracleAggregator, exchangeRate, dynamicAdjustment ) ), signature ); + // Return with SIG_VALIDATION_FAILED instead of reverting if (!validSig) { return ("", _packValidationData(true, validUntil, validAfter)); } + + } /** * @dev Post-operation handler. * This method is abstract in BasePaymaster and must be implemented in derived contracts. - * @param mode The mode of the post operation (opSucceeded, opReverted, or postOpReverted). * @param context The context value returned by validatePaymasterUserOp. * @param actualGasCost Actual gas used so far (excluding this postOp call). * @param actualUserOpFeePerGas The gas price this UserOp pays. */ function _postOp( - PostOpMode mode, + PostOpMode, bytes calldata context, uint256 actualGasCost, uint256 actualUserOpFeePerGas diff --git a/foundry.toml b/foundry.toml index 80fe03b..eae956e 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,7 +4,6 @@ auto_detect_solc = false block_timestamp = 1_680_220_800 # March 31, 2023 at 00:00 GMT bytecode_hash = "none" - evm_version = "paris" # See https://www.evmdiff.com/features?name=PUSH0&kind=opcode gas_reports = ["*"] optimizer = true optimizer_runs = 1_000_000 diff --git a/lib/account-abstraction b/lib/account-abstraction new file mode 160000 index 0000000..7af70c8 --- /dev/null +++ b/lib/account-abstraction @@ -0,0 +1 @@ +Subproject commit 7af70c8993a6f42973f520ae0752386a5032abe7 diff --git a/lib/forge-std b/lib/forge-std index 8948d45..1ce7535 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 8948d45d3d9022c508b83eb5d26fd3a7a93f2f32 +Subproject commit 1ce7535a517406b9aec7ea1ea27c1b41376f712c diff --git a/lib/nexus b/lib/nexus index ab9616b..b8085a3 160000 --- a/lib/nexus +++ b/lib/nexus @@ -1 +1 @@ -Subproject commit ab9616bd71fcd51048e834f87a7b60dccbfc0adb +Subproject commit b8085a3d688afb9149c129a34b4bb9cefb93ae38 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/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..b73bcb2 --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit b73bcb231fb6f5e7bc973edc75ab7f6c812a2255 diff --git a/lib/v3-core b/lib/v3-core index e3589b1..d8b1c63 160000 --- a/lib/v3-core +++ b/lib/v3-core @@ -1 +1 @@ -Subproject commit e3589b192d0be27e100cd0daaf6c97204fdb1899 +Subproject commit d8b1c635c275d2a9450bd6a78f3fa2484fef73eb diff --git a/lib/v3-periphery b/lib/v3-periphery index 80f26c8..0682387 160000 --- a/lib/v3-periphery +++ b/lib/v3-periphery @@ -1 +1 @@ -Subproject commit 80f26c86c57b8a5e4b913f42844d4c8bd274d058 +Subproject commit 0682387198a24c7cd63566a2c58398533860a5d1 diff --git a/remappings.txt b/remappings.txt index 9622f6b..d8e3f38 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,10 +1,11 @@ -@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ @prb/test/=node_modules/@prb/test/ @nexus/=lib/nexus/ @forge-std/=lib/forge-std/ -@uniswap/v3-periphery/=lib/v3-periphery -@uniswap/v3-core/=lib/v3-core -@account-abstraction=node_modules/account-abstraction/ +@uniswap/v3-periphery/=lib/v3-periphery/ +@uniswap/v3-core/=lib/v3-core/ +@account-abstraction=lib/account-abstraction/ +account-abstraction=lib/account-abstraction/ @modulekit/=node_modules/modulekit/src/ @sentinellist/=node_modules/sentinellist/ -@solady/=node_modules/solady +@solady/=node_modules/solady/ diff --git a/test/base/TestBase.sol b/test/base/TestBase.sol index cd8c222..b21cfb8 100644 --- a/test/base/TestBase.sol +++ b/test/base/TestBase.sol @@ -5,6 +5,7 @@ import { Test } from "forge-std/src/Test.sol"; import { Vm } from "forge-std/src/Vm.sol"; import "@solady/src/utils/ECDSA.sol"; +import { TestHelper } from "@nexus/test/foundry/utils/TestHelper.t.sol"; import { EntryPoint } from "@account-abstraction/contracts/core/EntryPoint.sol"; import { IEntryPoint } from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; @@ -14,54 +15,18 @@ import { IPaymaster } from "@account-abstraction/contracts/interfaces/IPaymaster 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 { BootstrapLib } from "@nexus/contracts/lib/BootstrapLib.sol"; -import { Bootstrap, BootstrapConfig } from "@nexus/contracts/utils/Bootstrap.sol"; import { CheatCodes } from "@nexus/test/foundry/utils/CheatCodes.sol"; import { BaseEventsAndErrors } from "./BaseEventsAndErrors.sol"; import { BiconomySponsorshipPaymaster } from "../../contracts/sponsorship/BiconomySponsorshipPaymaster.sol"; -abstract contract TestBase is CheatCodes, BaseEventsAndErrors { - // ----------------------------------------- - // State Variables - // ----------------------------------------- +abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { + address constant ENTRYPOINT_ADDRESS = address(0x0000000071727De22E5E9d8BAf0edAc6f37da032); - Vm.Wallet internal DEPLOYER; - 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 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 ALICE_ADDRESS; - address internal BOB_ADDRESS; - address internal CHARLIE_ADDRESS; - address internal DAN_ADDRESS; - address internal EMMA_ADDRESS; - - Nexus internal ALICE_ACCOUNT; - Nexus internal BOB_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; - MockValidator internal VALIDATOR_MODULE; - Nexus internal ACCOUNT_IMPLEMENTATION; - Bootstrap internal BOOTSTRAPPER; // Used to buffer user op gas limits // GAS_LIMIT = (ESTIMATED_GAS * GAS_BUFFER_RATIO) / 100 @@ -83,6 +48,7 @@ abstract contract TestBase is CheatCodes, BaseEventsAndErrors { function setupTestEnvironment() internal virtual { /// Initializes the testing environment setupPredefinedWallets(); + setupPaymasterPredefinedWallets(); deployTestContracts(); deployNexusForPredefinedWallets(); } @@ -93,22 +59,7 @@ abstract contract TestBase is CheatCodes, BaseEventsAndErrors { return wallet; } - function setupPredefinedWallets() internal { - DEPLOYER = createAndFundWallet("DEPLOYER", 1000 ether); - BUNDLER = createAndFundWallet("BUNDLER", 1000 ether); - - 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; - + function setupPaymasterPredefinedWallets() internal { PAYMASTER_OWNER = createAndFundWallet("PAYMASTER_OWNER", 1000 ether); PAYMASTER_SIGNER = createAndFundWallet("PAYMASTER_SIGNER", 1000 ether); PAYMASTER_FEE_COLLECTOR = createAndFundWallet("PAYMASTER_FEE_COLLECTOR", 1000 ether); @@ -116,227 +67,6 @@ abstract contract TestBase is CheatCodes, BaseEventsAndErrors { 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)); - 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"); - 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 - // ----------------------------------------- - - /// @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; - - bytes memory signature = signUserOp(wallet, userOp); - userOp.signature = signature; - } - - /// @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; - - 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); - } - - /// @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); - } - - // ----------------------------------------- - // Utility Functions - // ----------------------------------------- - - /// @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); - } - - /// @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 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"); - } - function estimateUserOpGasCosts(PackedUserOperation memory userOp) internal prankModifier(ENTRYPOINT_ADDRESS) diff --git a/test/unit/concrete/TestSponsorshipPaymaster.t.sol b/test/unit/concrete/TestSponsorshipPaymaster.t.sol index 3457c50..5193952 100644 --- a/test/unit/concrete/TestSponsorshipPaymaster.t.sol +++ b/test/unit/concrete/TestSponsorshipPaymaster.t.sol @@ -71,9 +71,9 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { 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); + emit OwnershipTransferred(PAYMASTER_OWNER.addr, BOB_ADDRESS); + bicoPaymaster.transferOwnership(BOB_ADDRESS); + assertEq(bicoPaymaster.owner(), BOB_ADDRESS); } function test_RevertIf_OwnershipTransferToZeroAddress() external prankModifier(PAYMASTER_OWNER.addr) { @@ -83,16 +83,16 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { function test_RevertIf_UnauthorizedOwnershipTransfer() external { vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); - bicoPaymaster.transferOwnership(DAN_ADDRESS); + bicoPaymaster.transferOwnership(BOB_ADDRESS); } 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 + PAYMASTER_SIGNER.addr, BOB_ADDRESS, PAYMASTER_OWNER.addr ); - bicoPaymaster.setSigner(DAN_ADDRESS); - assertEq(bicoPaymaster.verifyingSigner(), DAN_ADDRESS); + bicoPaymaster.setSigner(BOB_ADDRESS); + assertEq(bicoPaymaster.verifyingSigner(), BOB_ADDRESS); } function test_RevertIf_SetVerifyingSignerToContract() external prankModifier(PAYMASTER_OWNER.addr) { @@ -107,16 +107,16 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { function test_RevertIf_UnauthorizedSetVerifyingSigner() external { vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); - bicoPaymaster.setSigner(DAN_ADDRESS); + bicoPaymaster.setSigner(BOB_ADDRESS); } 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 + PAYMASTER_FEE_COLLECTOR.addr, BOB_ADDRESS, PAYMASTER_OWNER.addr ); - bicoPaymaster.setFeeCollector(DAN_ADDRESS); - assertEq(bicoPaymaster.feeCollector(), DAN_ADDRESS); + bicoPaymaster.setFeeCollector(BOB_ADDRESS); + assertEq(bicoPaymaster.feeCollector(), BOB_ADDRESS); } function test_RevertIf_SetFeeCollectorToZeroAddress() external prankModifier(PAYMASTER_OWNER.addr) { @@ -126,7 +126,7 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { function test_RevertIf_UnauthorizedSetFeeCollector() external { vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector)); - bicoPaymaster.setFeeCollector(DAN_ADDRESS); + bicoPaymaster.setFeeCollector(BOB_ADDRESS); } function test_SetUnaccountedGas() external prankModifier(PAYMASTER_OWNER.addr) { @@ -178,16 +178,16 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { function test_WithdrawTo() external prankModifier(DAPP_ACCOUNT.addr) { uint256 depositAmount = 10 ether; bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); - uint256 danInitialBalance = DAN_ADDRESS.balance; + uint256 danInitialBalance = BOB_ADDRESS.balance; vm.expectEmit(true, true, true, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.GasWithdrawn(DAPP_ACCOUNT.addr, DAN_ADDRESS, depositAmount); - bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), depositAmount); + emit IBiconomySponsorshipPaymaster.GasWithdrawn(DAPP_ACCOUNT.addr, BOB_ADDRESS, depositAmount); + bicoPaymaster.withdrawTo(payable(BOB_ADDRESS), depositAmount); uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); assertEq(dappPaymasterBalance, 0 ether); uint256 expectedDanBalance = danInitialBalance + depositAmount; - assertEq(DAN_ADDRESS.balance, expectedDanBalance); + assertEq(BOB_ADDRESS.balance, expectedDanBalance); } function test_RevertIf_WithdrawToZeroAddress() external prankModifier(DAPP_ACCOUNT.addr) { @@ -197,7 +197,7 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { function test_RevertIf_WithdrawToExceedsBalance() external prankModifier(DAPP_ACCOUNT.addr) { vm.expectRevert(abi.encodeWithSelector(InsufficientFundsInGasTank.selector)); - bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), 1 ether); + bicoPaymaster.withdrawTo(payable(BOB_ADDRESS), 1 ether); } function test_ValidatePaymasterAndPostOpWithoutDynamicAdjustment() external prankModifier(DAPP_ACCOUNT.addr) { diff --git a/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol b/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol index 1cf605a..983f610 100644 --- a/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol +++ b/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol @@ -38,16 +38,16 @@ contract TestFuzz_SponsorshipPaymasterWithDynamicAdjustment is TestBase { vm.deal(DAPP_ACCOUNT.addr, withdrawAmount); bicoPaymaster.depositFor{ value: withdrawAmount }(DAPP_ACCOUNT.addr); - uint256 danInitialBalance = DAN_ADDRESS.balance; + uint256 danInitialBalance = BOB_ADDRESS.balance; vm.expectEmit(true, true, true, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.GasWithdrawn(DAPP_ACCOUNT.addr, DAN_ADDRESS, withdrawAmount); - bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), withdrawAmount); + emit IBiconomySponsorshipPaymaster.GasWithdrawn(DAPP_ACCOUNT.addr, BOB_ADDRESS, withdrawAmount); + bicoPaymaster.withdrawTo(payable(BOB_ADDRESS), withdrawAmount); uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); assertEq(dappPaymasterBalance, 0 ether); uint256 expectedDanBalance = danInitialBalance + withdrawAmount; - assertEq(DAN_ADDRESS.balance, expectedDanBalance); + assertEq(BOB_ADDRESS.balance, expectedDanBalance); } function testFuzz_Receive(uint256 ethAmount) external prankModifier(ALICE_ADDRESS) { From a1faf39b0e98e9b3558794d78bbd9987430968d1 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Wed, 4 Sep 2024 18:34:47 +0400 Subject: [PATCH 51/69] fix submodule for nexus url --- .gitmodules | 2 +- test/base/TestBase.sol | 6 ++---- test/unit/concrete/TestSponsorshipPaymaster.t.sol | 2 +- test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.gitmodules b/.gitmodules index 72d8123..6e9767e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "lib/nexus"] path = lib/nexus - url = https://github.com/lib/nexus + url = https://github.com/bcnmy/nexus branch = dev [submodule "lib/forge-std"] path = lib/forge-std diff --git a/test/base/TestBase.sol b/test/base/TestBase.sol index b21cfb8..95ed18b 100644 --- a/test/base/TestBase.sol +++ b/test/base/TestBase.sol @@ -5,14 +5,12 @@ import { Test } from "forge-std/src/Test.sol"; import { Vm } from "forge-std/src/Vm.sol"; import "@solady/src/utils/ECDSA.sol"; -import { TestHelper } from "@nexus/test/foundry/utils/TestHelper.t.sol"; +import { TestHelper, IEntryPoint, EntryPoint } from "@nexus/test/foundry/utils/TestHelper.t.sol"; -import { EntryPoint } from "@account-abstraction/contracts/core/EntryPoint.sol"; -import { IEntryPoint } from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; import { IAccount } from "@account-abstraction/contracts/interfaces/IAccount.sol"; import { Exec } from "@account-abstraction/contracts/utils/Exec.sol"; import { IPaymaster } from "@account-abstraction/contracts/interfaces/IPaymaster.sol"; -import { PackedUserOperation } from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol"; +import { PackedUserOperation } from "@nexus/contracts/Nexus.sol"; import { Nexus } from "@nexus/contracts/Nexus.sol"; import { CheatCodes } from "@nexus/test/foundry/utils/CheatCodes.sol"; diff --git a/test/unit/concrete/TestSponsorshipPaymaster.t.sol b/test/unit/concrete/TestSponsorshipPaymaster.t.sol index 5193952..ffa5c90 100644 --- a/test/unit/concrete/TestSponsorshipPaymaster.t.sol +++ b/test/unit/concrete/TestSponsorshipPaymaster.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.26; import { TestBase } from "../../base/TestBase.sol"; import { IBiconomySponsorshipPaymaster } from "../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; import { BiconomySponsorshipPaymaster } from "../../../contracts/sponsorship/BiconomySponsorshipPaymaster.sol"; -import { PackedUserOperation } from "@account-abstraction/contracts/core/UserOperationLib.sol"; +import { PackedUserOperation } from "@nexus/contracts/Nexus.sol"; import { MockToken } from "@nexus/contracts/mocks/MockToken.sol"; contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { diff --git a/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol b/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol index 983f610..7b871ed 100644 --- a/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol +++ b/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol @@ -5,7 +5,7 @@ import { TestBase } from "../../base/TestBase.sol"; import { IBiconomySponsorshipPaymaster } from "../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; import { BiconomySponsorshipPaymaster } from "../../../contracts/sponsorship/BiconomySponsorshipPaymaster.sol"; import { MockToken } from "@nexus/contracts/mocks/MockToken.sol"; -import { PackedUserOperation } from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol"; +import { PackedUserOperation } from "@nexus/contracts/Nexus.sol"; contract TestFuzz_SponsorshipPaymasterWithDynamicAdjustment is TestBase { BiconomySponsorshipPaymaster public bicoPaymaster; From 3eee044c57eb75149e4fd4f3dba0103aca3bcbb2 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Wed, 4 Sep 2024 18:55:20 +0400 Subject: [PATCH 52/69] fix submodules --- .gitmodules | 21 +++++++++------------ lib/forge-std | 2 +- lib/v3-core | 2 +- lib/v3-periphery | 2 +- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/.gitmodules b/.gitmodules index 6e9767e..2be2205 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,21 +1,18 @@ -[submodule "lib/nexus"] - path = lib/nexus - url = https://github.com/bcnmy/nexus - branch = dev [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std -[submodule "lib/v3-periphery"] - path = lib/v3-periphery - url = https://github.com/Uniswap/v3-periphery -[submodule "lib/v3-core"] - path = lib/v3-core - url = https://github.com/Uniswap/v3-core [submodule "lib/account-abstraction"] path = lib/account-abstraction url = https://github.com/eth-infinitism/account-abstraction - branch = releases/v0.7 +[submodule "lib/nexus"] + path = lib/nexus + url = https://github.com/bcnmy/nexus [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts - branch = master +[submodule "lib/v3-core"] + path = lib/v3-core + url = https://github.com/Uniswap/v3-core +[submodule "lib/v3-periphery"] + path = lib/v3-periphery + url = https://github.com/Uniswap/v3-periphery diff --git a/lib/forge-std b/lib/forge-std index 1ce7535..1714bee 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 1ce7535a517406b9aec7ea1ea27c1b41376f712c +Subproject commit 1714bee72e286e73f76e320d110e0eaf5c4e649d diff --git a/lib/v3-core b/lib/v3-core index d8b1c63..e3589b1 160000 --- a/lib/v3-core +++ b/lib/v3-core @@ -1 +1 @@ -Subproject commit d8b1c635c275d2a9450bd6a78f3fa2484fef73eb +Subproject commit e3589b192d0be27e100cd0daaf6c97204fdb1899 diff --git a/lib/v3-periphery b/lib/v3-periphery index 0682387..80f26c8 160000 --- a/lib/v3-periphery +++ b/lib/v3-periphery @@ -1 +1 @@ -Subproject commit 0682387198a24c7cd63566a2c58398533860a5d1 +Subproject commit 80f26c86c57b8a5e4b913f42844d4c8bd274d058 From 8c1cf1283f589874cdc92ee500231354cb383608 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Thu, 5 Sep 2024 17:09:44 +0400 Subject: [PATCH 53/69] got tests compiling with correct submodules installed --- foundry.toml | 1 + test/base/TestBase.sol | 11 +- test/base/TestHelper.sol | 585 ++++++++++++++++++ .../concrete/TestSponsorshipPaymaster.t.sol | 5 +- .../TestFuzz_TestSponsorshipPaymaster.t.sol | 5 +- 5 files changed, 592 insertions(+), 15 deletions(-) create mode 100644 test/base/TestHelper.sol diff --git a/foundry.toml b/foundry.toml index eae956e..a4d5063 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,6 +4,7 @@ auto_detect_solc = false block_timestamp = 1_680_220_800 # March 31, 2023 at 00:00 GMT bytecode_hash = "none" + evm_version = "cancun" # See https://www.evmdiff.com/features?name=PUSH0&kind=opcode gas_reports = ["*"] optimizer = true optimizer_runs = 1_000_000 diff --git a/test/base/TestBase.sol b/test/base/TestBase.sol index 95ed18b..5e57f04 100644 --- a/test/base/TestBase.sol +++ b/test/base/TestBase.sol @@ -5,12 +5,11 @@ import { Test } from "forge-std/src/Test.sol"; import { Vm } from "forge-std/src/Vm.sol"; import "@solady/src/utils/ECDSA.sol"; -import { TestHelper, IEntryPoint, EntryPoint } from "@nexus/test/foundry/utils/TestHelper.t.sol"; +import "./TestHelper.sol"; import { IAccount } from "@account-abstraction/contracts/interfaces/IAccount.sol"; import { Exec } from "@account-abstraction/contracts/utils/Exec.sol"; import { IPaymaster } from "@account-abstraction/contracts/interfaces/IPaymaster.sol"; -import { PackedUserOperation } from "@nexus/contracts/Nexus.sol"; import { Nexus } from "@nexus/contracts/Nexus.sol"; import { CheatCodes } from "@nexus/test/foundry/utils/CheatCodes.sol"; @@ -43,7 +42,7 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { // Setup Functions // ----------------------------------------- /// @notice Initializes the testing environment with wallets, contracts, and accounts - function setupTestEnvironment() internal virtual { + function setupPaymasterTestEnvironment() internal virtual { /// Initializes the testing environment setupPredefinedWallets(); setupPaymasterPredefinedWallets(); @@ -51,12 +50,6 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { 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 setupPaymasterPredefinedWallets() internal { PAYMASTER_OWNER = createAndFundWallet("PAYMASTER_OWNER", 1000 ether); PAYMASTER_SIGNER = createAndFundWallet("PAYMASTER_SIGNER", 1000 ether); diff --git a/test/base/TestHelper.sol b/test/base/TestHelper.sol new file mode 100644 index 0000000..d8103e9 --- /dev/null +++ b/test/base/TestHelper.sol @@ -0,0 +1,585 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +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/test/foundry/utils/CheatCodes.sol"; +import "@nexus/test/foundry/utils/EventsAndErrors.sol"; +import "@nexus/contracts/lib/ModeLib.sol"; +import "@nexus/contracts/lib/ExecLib.sol"; +import { Nexus } from "@nexus/contracts/Nexus.sol"; +import { MockHook } from "@nexus/contracts/mocks/MockHook.sol"; +import { MockHandler } from "@nexus/contracts/mocks/MockHandler.sol"; +import { MockDelegateTarget } from "@nexus/contracts/mocks/MockDelegateTarget.sol"; +import { MockValidator } from "@nexus/contracts/mocks/MockValidator.sol"; +import { MockMultiModule } from "@nexus/contracts/mocks/MockMultiModule.sol"; +import { MockPaymaster } from "@nexus/contracts/mocks/MockPaymaster.sol"; +import { Bootstrap, BootstrapConfig } from "@nexus/contracts/utils/RegistryBootstrap.sol"; +import { BiconomyMetaFactory } from "@nexus/contracts/factory/BiconomyMetaFactory.sol"; +import { NexusAccountFactory } from "@nexus/contracts/factory/NexusAccountFactory.sol"; +import { BootstrapLib } from "@nexus/contracts/lib/BootstrapLib.sol"; +import { MODE_VALIDATION } from "@nexus/contracts/types/Constants.sol"; +import { MockRegistry } from "@nexus/contracts/mocks/MockRegistry.sol"; + +contract TestHelper 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 internal BOB_ADDRESS; + address internal ALICE_ADDRESS; + address internal CHARLIE_ADDRESS; + address payable internal BUNDLER_ADDRESS; + + address[] internal ATTESTERS; + uint8 internal THRESHOLD; + + Nexus internal BOB_ACCOUNT; + Nexus internal ALICE_ACCOUNT; + Nexus internal CHARLIE_ACCOUNT; + + IEntryPoint internal ENTRYPOINT; + NexusAccountFactory internal FACTORY; + BiconomyMetaFactory internal META_FACTORY; + MockRegistry internal REGISTRY; + MockHook internal HOOK_MODULE; + MockHandler internal HANDLER_MODULE; + MockValidator internal VALIDATOR_MODULE; + MockMultiModule internal MULTI_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; + + BUNDLER = createAndFundWallet("BUNDLER", 1000 ether); + BUNDLER_ADDRESS = payable(BUNDLER.addr); + + FACTORY_OWNER = createAndFundWallet("FACTORY_OWNER", 1000 ether); + + ATTESTERS = new address[](1); + ATTESTERS[0] = ALICE.addr; + THRESHOLD = 1; + } + + function deployTestContracts() internal { + ENTRYPOINT = new EntryPoint(); + vm.etch(address(0x0000000071727De22E5E9d8BAf0edAc6f37da032), address(ENTRYPOINT).code); + ENTRYPOINT = IEntryPoint(0x0000000071727De22E5E9d8BAf0edAc6f37da032); + 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(); + VALIDATOR_MODULE = new MockValidator(); + MULTI_MODULE = new MockMultiModule(); + BOOTSTRAPPER = new Bootstrap(); + REGISTRY = new MockRegistry(); + } + + // ----------------------------------------- + // 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, REGISTRY, ATTESTERS, THRESHOLD); + 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, REGISTRY, ATTESTERS, THRESHOLD); + + 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; + + bytes memory signature = signUserOp(wallet, userOp); + userOp.signature = signature; + } + + /// @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, MODE_VALIDATION, validator); + userOp = buildPackedUserOp(account, nonce); + userOp.callData = callData; + + 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 vMode Validation Mode + /// @param validator The validator address + /// @return nonce The retrieved nonce + function getNonce(address account, bytes1 vMode, address validator) internal view returns (uint256 nonce) { + uint192 key = makeNonceKey(vMode, validator); + nonce = ENTRYPOINT.getNonce(address(account), key); + } + + /// @notice Composes the nonce key + /// @param vMode Validation Mode + /// @param validator The validator address + /// @return key The nonce key + function makeNonceKey(bytes1 vMode, address validator) internal pure returns (uint192 key) { + assembly { + key := or(shr(88, vMode), validator) + } + } + + /// @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); + } + + // ----------------------------------------- + // Utility Functions + // ----------------------------------------- + + /// @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); + } + + /// @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 7579 execution calldata + /// @param execType The execution type + /// @param executions The executions to include + /// @return executionCalldata The prepared callData + function prepareERC7579ExecuteCallData( + ExecType execType, + Execution[] memory executions + ) internal virtual view returns (bytes memory executionCalldata) { + // Determine mode and calldata based on callType and executions length + ExecutionMode mode; + 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"); + } + } + + /// @notice Prepares a callData for single execution + /// @param execType The execution type + /// @param target The call target + /// @param value The call value + /// @param data The call data + /// @return executionCalldata The prepared callData + function prepareERC7579SingleExecuteCallData( + ExecType execType, + address target, + uint256 value, + bytes memory data + ) internal virtual view returns (bytes memory executionCalldata) { + ExecutionMode mode; + mode = (execType == EXECTYPE_DEFAULT) ? ModeLib.encodeSimpleSingle() : ModeLib.encodeTrySingle(); + executionCalldata = abi.encodeCall( + Nexus.execute, + (mode, ExecLib.encodeSingle(target, value, data)) + ); + } + + /// @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 + /// @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"); + + // Initialize the userOps array with one operation + userOps = new PackedUserOperation[](1); + + // Build the UserOperation + userOps[0] = buildPackedUserOp(address(account), getNonce(address(account), MODE_VALIDATION, validator)); + userOps[0].callData = prepareERC7579ExecuteCallData(execType, executions); + + // 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 + 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"); + } + + /// @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 + 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(); + } +} diff --git a/test/unit/concrete/TestSponsorshipPaymaster.t.sol b/test/unit/concrete/TestSponsorshipPaymaster.t.sol index ffa5c90..4800594 100644 --- a/test/unit/concrete/TestSponsorshipPaymaster.t.sol +++ b/test/unit/concrete/TestSponsorshipPaymaster.t.sol @@ -1,17 +1,16 @@ // SPDX-License-Identifier: Unlicensed pragma solidity ^0.8.26; -import { TestBase } from "../../base/TestBase.sol"; +import "../../base/TestBase.sol"; import { IBiconomySponsorshipPaymaster } from "../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; import { BiconomySponsorshipPaymaster } from "../../../contracts/sponsorship/BiconomySponsorshipPaymaster.sol"; -import { PackedUserOperation } from "@nexus/contracts/Nexus.sol"; import { MockToken } from "@nexus/contracts/mocks/MockToken.sol"; contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { BiconomySponsorshipPaymaster public bicoPaymaster; function setUp() public { - setupTestEnvironment(); + setupPaymasterTestEnvironment(); // Deploy Sponsorship Paymaster bicoPaymaster = new BiconomySponsorshipPaymaster( PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr, 7e3 diff --git a/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol b/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol index 7b871ed..a04ec3a 100644 --- a/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol +++ b/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol @@ -1,17 +1,16 @@ // SPDX-License-Identifier: Unlicensed pragma solidity ^0.8.26; -import { TestBase } from "../../base/TestBase.sol"; +import "../../base/TestBase.sol"; import { IBiconomySponsorshipPaymaster } from "../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; import { BiconomySponsorshipPaymaster } from "../../../contracts/sponsorship/BiconomySponsorshipPaymaster.sol"; import { MockToken } from "@nexus/contracts/mocks/MockToken.sol"; -import { PackedUserOperation } from "@nexus/contracts/Nexus.sol"; contract TestFuzz_SponsorshipPaymasterWithDynamicAdjustment is TestBase { BiconomySponsorshipPaymaster public bicoPaymaster; function setUp() public { - setupTestEnvironment(); + setupPaymasterTestEnvironment(); // Deploy Sponsorship Paymaster bicoPaymaster = new BiconomySponsorshipPaymaster( PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr, 7e3 From 49a48b09619baa87da8bd59f8ae7e205b6ae666b Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Thu, 5 Sep 2024 17:56:32 +0400 Subject: [PATCH 54/69] basic setters for token paymaster --- contracts/base/BasePaymaster.sol | 11 - .../common/BiconomyTokenPaymasterErrors.sol | 7 +- .../interfaces/IBiconomyTokenPaymaster.sol | 13 +- .../BiconomySponsorshipPaymaster.sol | 11 + contracts/token/BiconomyTokenPaymaster.sol | 188 ++++-------------- 5 files changed, 55 insertions(+), 175 deletions(-) diff --git a/contracts/base/BasePaymaster.sol b/contracts/base/BasePaymaster.sol index f3394c1..d7dd243 100644 --- a/contracts/base/BasePaymaster.sol +++ b/contracts/base/BasePaymaster.sol @@ -162,15 +162,4 @@ abstract contract BasePaymaster is IPaymaster, SoladyOwnable { function _requireFromEntryPoint() internal virtual { require(msg.sender == address(entryPoint), "Sender not EntryPoint"); } - - /** - * Check if address is a contract - */ - function _isContract(address _addr) internal view returns (bool) { - uint256 size; - assembly ("memory-safe") { - size := extcodesize(_addr) - } - return size > 0; - } } diff --git a/contracts/common/BiconomyTokenPaymasterErrors.sol b/contracts/common/BiconomyTokenPaymasterErrors.sol index e034e77..882e638 100644 --- a/contracts/common/BiconomyTokenPaymasterErrors.sol +++ b/contracts/common/BiconomyTokenPaymasterErrors.sol @@ -12,11 +12,6 @@ contract BiconomyTokenPaymasterErrors { */ error FeeCollectorCanNotBeZero(); - /** - * @notice Throws when the fee collector address provided is a deployed contract - */ - error VerifyingSignerCanNotBeContract(); - /** * @notice Throws when trying unaccountedGas is too high */ @@ -35,5 +30,5 @@ contract BiconomyTokenPaymasterErrors { /** * @notice Throws when invalid signature length in paymasterAndData */ - error InvalidSignatureLength(); + error InvalidDynamicAdjustment(); } diff --git a/contracts/interfaces/IBiconomyTokenPaymaster.sol b/contracts/interfaces/IBiconomyTokenPaymaster.sol index 97ef79c..7ac2633 100644 --- a/contracts/interfaces/IBiconomyTokenPaymaster.sol +++ b/contracts/interfaces/IBiconomyTokenPaymaster.sol @@ -2,14 +2,8 @@ pragma solidity ^0.8.26; interface IBiconomyTokenPaymaster { - enum PriceSource { - EXTERNAL, - ORACLE - } - event UnaccountedGasChanged(uint256 indexed oldValue, uint256 indexed newValue); event FixedDynamicAdjustmentChanged(uint32 indexed oldValue, uint32 indexed newValue); - event VerifyingSignerChanged(address indexed oldSigner, address indexed newSigner, address indexed actor); event FeeCollectorChanged(address indexed oldFeeCollector, address indexed newFeeCollector, address indexed actor); event GasDeposited(address indexed paymasterId, uint256 indexed value); event GasWithdrawn(address indexed paymasterId, address indexed to, uint256 indexed value); @@ -18,10 +12,9 @@ interface IBiconomyTokenPaymaster { event Received(address indexed sender, uint256 value); event TokensWithdrawn(address indexed token, address indexed to, uint256 indexed amount, address actor); - - function setSigner(address _newVerifyingSigner) external payable; - function setFeeCollector(address _newFeeCollector) external payable; - function setUnaccountedGas(uint16 value) external payable; + function setUnaccountedGas(uint256 value) external payable; + + function setDynamicAdjustment(uint256 _newUnaccountedGas) external payable; } diff --git a/contracts/sponsorship/BiconomySponsorshipPaymaster.sol b/contracts/sponsorship/BiconomySponsorshipPaymaster.sol index 76460de..9db7657 100644 --- a/contracts/sponsorship/BiconomySponsorshipPaymaster.sol +++ b/contracts/sponsorship/BiconomySponsorshipPaymaster.sol @@ -363,4 +363,15 @@ contract BiconomySponsorshipPaymaster is revert UnaccountedGasTooHigh(); } } + + /** + * Check if address is a contract + */ + function _isContract(address _addr) internal view returns (bool) { + uint256 size; + assembly ("memory-safe") { + size := extcodesize(_addr) + } + return size > 0; + } } diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index a28a398..0810fc0 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -1,10 +1,8 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.26; -import { ECDSA as ECDSA_solady } from "@solady/src/utils/ECDSA.sol"; import { ReentrancyGuardTransient } from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; import { IEntryPoint } from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; -import { SignatureCheckerLib } from "@solady/src/utils/SignatureCheckerLib.sol"; import { PackedUserOperation, UserOperationLib } from "@account-abstraction/contracts/core/UserOperationLib.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeTransferLib } from "@solady/src/utils/SafeTransferLib.sol"; @@ -13,7 +11,6 @@ import { BiconomyTokenPaymasterErrors } from "../common/BiconomyTokenPaymasterEr import { IBiconomyTokenPaymaster } from "../interfaces/IBiconomyTokenPaymaster.sol"; import "@account-abstraction/contracts/core/Helpers.sol"; - /** * @title BiconomyTokenPaymaster * @author ShivaanshK @@ -31,30 +28,34 @@ contract BiconomyTokenPaymaster is IBiconomyTokenPaymaster { using UserOperationLib for PackedUserOperation; - using SignatureCheckerLib for address; - address public verifyingSigner; address public feeCollector; - uint16 public unaccountedGas; + uint256 public unaccountedGas; + uint256 public dynamicAdjustment; // Limit for unaccounted gas cost - uint16 private constant UNACCOUNTED_GAS_LIMIT = 50_000; + uint256 private constant UNACCOUNTED_GAS_LIMIT = 50_000; + uint256 private constant PRICE_DENOMINATOR = 1e6; + uint256 private constant MAX_DYNAMIC_ADJUSTMENT = 2e6; constructor( address _owner, IEntryPoint _entryPoint, - address _verifyingSigner, - uint16 _unaccountedGas + uint256 _unaccountedGas, + uint256 _dynamicAdjustment ) BasePaymaster(_owner, _entryPoint) { - _checkConstructorArgs(_verifyingSigner, _unaccountedGas); + if (_unaccountedGas > UNACCOUNTED_GAS_LIMIT) { + revert UnaccountedGasTooHigh(); + } else if (_dynamicAdjustment > MAX_DYNAMIC_ADJUSTMENT || _dynamicAdjustment == 0) { + revert InvalidDynamicAdjustment(); + } assembly ("memory-safe") { - sstore(verifyingSigner.slot, _verifyingSigner) + sstore(feeCollector.slot, address()) // initialize fee collector to this contract + sstore(unaccountedGas.slot, _unaccountedGas) + sstore(dynamicAdjustment.slot, _dynamicAdjustment) } - verifyingSigner = _verifyingSigner; - feeCollector = address(this); // initialize fee collector to this contract - unaccountedGas = _unaccountedGas; } /** @@ -146,25 +147,6 @@ contract BiconomyTokenPaymaster is } } - /** - * @dev Set a new verifying signer address. - * Can only be called by the owner of the contract. - * @param _newVerifyingSigner The new address to be set as the verifying signer. - * @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 override onlyOwner { - if (_isContract(_newVerifyingSigner)) revert VerifyingSignerCanNotBeContract(); - if (_newVerifyingSigner == address(0)) { - revert VerifyingSignerCanNotBeZero(); - } - address oldSigner = verifyingSigner; - assembly ("memory-safe") { - sstore(verifyingSigner.slot, _newVerifyingSigner) - } - emit VerifyingSignerChanged(oldSigner, _newVerifyingSigner, msg.sender); - } - /** * @dev Set a new fee collector address. * Can only be called by the owner of the contract. @@ -175,94 +157,42 @@ contract BiconomyTokenPaymaster is function setFeeCollector(address _newFeeCollector) external payable override onlyOwner { if (_newFeeCollector == address(0)) revert FeeCollectorCanNotBeZero(); address oldFeeCollector = feeCollector; - feeCollector = _newFeeCollector; + assembly ("memory-safe") { + sstore(feeCollector.slot, _newFeeCollector) + } emit FeeCollectorChanged(oldFeeCollector, _newFeeCollector, msg.sender); } /** * @dev Set a new unaccountedEPGasOverhead value. - * @param value The new value to be set as the unaccountedEPGasOverhead. + * @param _newUnaccountedGas The new value to be set as the unaccounted gas value * @notice only to be called by the owner of the contract. */ - function setUnaccountedGas(uint16 value) external payable override onlyOwner { - if (value > UNACCOUNTED_GAS_LIMIT) { + function setUnaccountedGas(uint256 _newUnaccountedGas) external payable override onlyOwner { + if (_newUnaccountedGas > UNACCOUNTED_GAS_LIMIT) { revert UnaccountedGasTooHigh(); } - uint16 oldValue = unaccountedGas; - unaccountedGas = value; - emit UnaccountedGasChanged(oldValue, value); + uint256 oldUnaccountedGas = unaccountedGas; + assembly ("memory-safe") { + sstore(unaccountedGas.slot, _newUnaccountedGas) + } + emit UnaccountedGasChanged(oldUnaccountedGas, _newUnaccountedGas); } /** - * 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. - * it is called on-chain from the validatePaymasterUserOp, to validate the signature. - * note that this signature covers all fields of the UserOperation, except the "paymasterAndData", - * which will carry the signature itself. + * @dev Set a new unaccountedEPGasOverhead value. + * @param _newDynamicAdjustment The new value to be set as the unaccounted gas value + * @notice only to be called by the owner of the contract. */ - function getHash( - PackedUserOperation calldata userOp, - PriceSource priceSource, - uint48 validUntil, - uint48 validAfter, - address feeToken, - address oracleAggregator, - uint256 exchangeRate, - uint32 dynamicAdjustment - ) - public - view - returns (bytes32) - { - //can't use userOp.hash(), since it contains also the paymasterAndData itself. - address sender = userOp.getSender(); - 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])), - userOp.preVerificationGas, - userOp.gasFees, - block.chainid, - address(this), - priceSource, // treated as a uint8 - validUntil, - validAfter, - feeToken, - oracleAggregator, - exchangeRate, - dynamicAdjustment - ) - ); - } - - function parsePaymasterAndData(bytes calldata paymasterAndData) - public - pure - returns ( - PriceSource priceSource, - uint48 validUntil, - uint48 validAfter, - address feeToken, - address oracleAggregator, - uint256 exchangeRate, - uint32 dynamicAdjustment, - bytes calldata signature - ) - { - unchecked { - priceSource = PriceSource(uint8(bytes1(paymasterAndData[PAYMASTER_DATA_OFFSET]))); - validUntil = uint48(bytes6(paymasterAndData[PAYMASTER_DATA_OFFSET + 1:PAYMASTER_DATA_OFFSET + 7])); - validAfter = uint48(bytes6(paymasterAndData[PAYMASTER_DATA_OFFSET + 7:PAYMASTER_DATA_OFFSET + 13])); - feeToken = address(bytes20(paymasterAndData[PAYMASTER_DATA_OFFSET + 13:PAYMASTER_DATA_OFFSET + 33])); - oracleAggregator = address(bytes20(paymasterAndData[PAYMASTER_DATA_OFFSET + 33:PAYMASTER_DATA_OFFSET + 53])); - exchangeRate = uint256(bytes32(paymasterAndData[PAYMASTER_DATA_OFFSET + 53:PAYMASTER_DATA_OFFSET + 85])); - dynamicAdjustment = uint32(bytes4(paymasterAndData[PAYMASTER_DATA_OFFSET + 85:PAYMASTER_DATA_OFFSET + 89])); - signature = paymasterAndData[PAYMASTER_DATA_OFFSET + 89:]; + function setDynamicAdjustment(uint256 _newDynamicAdjustment) external payable override onlyOwner { + if (_newDynamicAdjustment > MAX_DYNAMIC_ADJUSTMENT || _newDynamicAdjustment == 0) { + revert InvalidDynamicAdjustment(); } + uint256 oldDynamicAdjustment = dynamicAdjustment; + assembly ("memory-safe") { + sstore(dynamicAdjustment.slot, _newDynamicAdjustment) + } + emit FixedDynamicAdjustmentChanged(oldDynamicAdjustment, _newDynamicAdjustment); } /** @@ -281,37 +211,8 @@ contract BiconomyTokenPaymaster is override returns (bytes memory context, uint256 validationData) { - // review: in this method try to resolve stack too deep (though via-ir is good enough) - ( - PriceSource priceSource, - uint48 validUntil, - uint48 validAfter, - address feeToken, - address oracleAggregator, - uint256 exchangeRate, - uint32 dynamicAdjustment, - bytes calldata signature - ) = parsePaymasterAndData(userOp.paymasterAndData); - - if (signature.length != 64 && signature.length != 65) { - revert InvalidSignatureLength(); - } - - bool validSig = verifyingSigner.isValidSignatureNow( - ECDSA_solady.toEthSignedMessageHash( - getHash( - userOp, priceSource, validUntil, validAfter, feeToken, oracleAggregator, exchangeRate, dynamicAdjustment - ) - ), - signature - ); - - // Return with SIG_VALIDATION_FAILED instead of reverting - if (!validSig) { - return ("", _packValidationData(true, validUntil, validAfter)); - } - - + (maxCost); + // Implementation of post-operation logic } /** @@ -330,19 +231,10 @@ contract BiconomyTokenPaymaster is internal override { + (context); // Implementation of post-operation logic } - function _checkConstructorArgs(address _verifyingSigner, uint16 _unaccountedGas) internal view { - if (_verifyingSigner == address(0)) { - revert VerifyingSignerCanNotBeZero(); - } else if (_isContract(_verifyingSigner)) { - revert VerifyingSignerCanNotBeContract(); - } else if (_unaccountedGas > UNACCOUNTED_GAS_LIMIT) { - revert UnaccountedGasTooHigh(); - } - } - function _withdrawERC20(IERC20 token, address target, uint256 amount) private { if (target == address(0)) revert CanNotWithdrawToZeroAddress(); SafeTransferLib.safeTransfer(address(token), target, amount); From 31681cc1d14af326e362b66772f439c73099181e Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Thu, 5 Sep 2024 19:47:59 +0400 Subject: [PATCH 55/69] added initial oracle storage and price calc --- .../common/BiconomyTokenPaymasterErrors.sol | 10 ++ .../IBiconomySponsorshipPaymaster.sol | 4 +- .../interfaces/IBiconomyTokenPaymaster.sol | 2 +- contracts/interfaces/oracles/IOracle.sol | 10 ++ .../BiconomySponsorshipPaymaster.sol | 15 +-- contracts/token/BiconomyTokenPaymaster.sol | 53 ++++++++- contracts/token/oracles/TwapOracle.sol | 107 ++++++++++++++++++ lib/v3-core | 2 +- lib/v3-periphery | 2 +- .../concrete/TestSponsorshipPaymaster.t.sol | 6 +- 10 files changed, 192 insertions(+), 19 deletions(-) create mode 100644 contracts/interfaces/oracles/IOracle.sol create mode 100644 contracts/token/oracles/TwapOracle.sol diff --git a/contracts/common/BiconomyTokenPaymasterErrors.sol b/contracts/common/BiconomyTokenPaymasterErrors.sol index 882e638..9b0c365 100644 --- a/contracts/common/BiconomyTokenPaymasterErrors.sol +++ b/contracts/common/BiconomyTokenPaymasterErrors.sol @@ -31,4 +31,14 @@ contract BiconomyTokenPaymasterErrors { * @notice Throws when invalid signature length in paymasterAndData */ error InvalidDynamicAdjustment(); + + /** + * @notice Throws when each token doesnt have a corresponding oracle + */ + error TokensAndOraclesLengthMismatch(); + + /** + * @notice Throws when oracle returns invalid price + */ + error OraclePriceNotPositive(); } diff --git a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol index 7ad2651..2948273 100644 --- a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol +++ b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol @@ -6,7 +6,7 @@ import { PackedUserOperation } from "@account-abstraction/contracts/core/UserOpe interface IBiconomySponsorshipPaymaster{ event UnaccountedGasChanged(uint256 indexed oldValue, uint256 indexed newValue); - event FixedDynamicAdjustmentChanged(uint32 indexed oldValue, uint32 indexed newValue); + event FixedDynamicAdjustmentChanged(uint256 indexed oldValue, uint256 indexed newValue); event VerifyingSignerChanged(address indexed oldSigner, address indexed newSigner, address indexed actor); event FeeCollectorChanged(address indexed oldFeeCollector, address indexed newFeeCollector, address indexed actor); event GasDeposited(address indexed paymasterId, uint256 indexed value); @@ -22,7 +22,7 @@ interface IBiconomySponsorshipPaymaster{ function setFeeCollector(address _newFeeCollector) external payable; - function setUnaccountedGas(uint16 value) external payable; + function setUnaccountedGas(uint256 value) external payable; function withdrawERC20(IERC20 token, address target, uint256 amount) external; diff --git a/contracts/interfaces/IBiconomyTokenPaymaster.sol b/contracts/interfaces/IBiconomyTokenPaymaster.sol index 7ac2633..58157f9 100644 --- a/contracts/interfaces/IBiconomyTokenPaymaster.sol +++ b/contracts/interfaces/IBiconomyTokenPaymaster.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.26; interface IBiconomyTokenPaymaster { event UnaccountedGasChanged(uint256 indexed oldValue, uint256 indexed newValue); - event FixedDynamicAdjustmentChanged(uint32 indexed oldValue, uint32 indexed newValue); + event FixedDynamicAdjustmentChanged(uint256 indexed oldValue, uint256 indexed newValue); event FeeCollectorChanged(address indexed oldFeeCollector, address indexed newFeeCollector, address indexed actor); event GasDeposited(address indexed paymasterId, uint256 indexed value); event GasWithdrawn(address indexed paymasterId, address indexed to, uint256 indexed value); diff --git a/contracts/interfaces/oracles/IOracle.sol b/contracts/interfaces/oracles/IOracle.sol new file mode 100644 index 0000000..5a15d66 --- /dev/null +++ b/contracts/interfaces/oracles/IOracle.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IOracle { + function decimals() external view returns (uint8); + function latestRoundData() + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); +} \ No newline at end of file diff --git a/contracts/sponsorship/BiconomySponsorshipPaymaster.sol b/contracts/sponsorship/BiconomySponsorshipPaymaster.sol index 9db7657..706b38e 100644 --- a/contracts/sponsorship/BiconomySponsorshipPaymaster.sol +++ b/contracts/sponsorship/BiconomySponsorshipPaymaster.sol @@ -40,13 +40,14 @@ contract BiconomySponsorshipPaymaster is address public verifyingSigner; address public feeCollector; - uint16 public unaccountedGas; - uint32 private constant PRICE_DENOMINATOR = 1e6; + uint256 public unaccountedGas; + // Denominator to prevent precision errors when applying dynamic adjustment + uint256 private constant PRICE_DENOMINATOR = 1e6; // Offset in PaymasterAndData to get to PAYMASTER_ID_OFFSET uint256 private constant PAYMASTER_ID_OFFSET = PAYMASTER_DATA_OFFSET; // Limit for unaccounted gas cost - uint16 private constant UNACCOUNTED_GAS_LIMIT = 50_000; + uint256 private constant UNACCOUNTED_GAS_LIMIT = 50_000; mapping(address => uint256) public paymasterIdBalances; @@ -55,7 +56,7 @@ contract BiconomySponsorshipPaymaster is IEntryPoint _entryPoint, address _verifyingSigner, address _feeCollector, - uint16 _unaccountedGas + uint256 _unaccountedGas ) BasePaymaster(_owner, _entryPoint) { @@ -123,11 +124,11 @@ contract BiconomySponsorshipPaymaster is * @param value The new value to be set as the unaccountedEPGasOverhead. * @notice only to be called by the owner of the contract. */ - function setUnaccountedGas(uint16 value) external payable override onlyOwner { + function setUnaccountedGas(uint256 value) external payable override onlyOwner { if (value > UNACCOUNTED_GAS_LIMIT) { revert UnaccountedGasTooHigh(); } - uint16 oldValue = unaccountedGas; + uint256 oldValue = unaccountedGas; unaccountedGas = value; emit UnaccountedGasChanged(oldValue, value); } @@ -346,7 +347,7 @@ contract BiconomySponsorshipPaymaster is function _checkConstructorArgs( address _verifyingSigner, address _feeCollector, - uint16 _unaccountedGas + uint256 _unaccountedGas ) internal view diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index 0810fc0..df36782 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -4,11 +4,12 @@ pragma solidity ^0.8.26; import { ReentrancyGuardTransient } from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; import { IEntryPoint } from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; import { PackedUserOperation, UserOperationLib } from "@account-abstraction/contracts/core/UserOperationLib.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20, ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { SafeTransferLib } from "@solady/src/utils/SafeTransferLib.sol"; import { BasePaymaster } from "../base/BasePaymaster.sol"; import { BiconomyTokenPaymasterErrors } from "../common/BiconomyTokenPaymasterErrors.sol"; import { IBiconomyTokenPaymaster } from "../interfaces/IBiconomyTokenPaymaster.sol"; +import { IOracle } from "../interfaces/oracles/IOracle.sol"; import "@account-abstraction/contracts/core/Helpers.sol"; /** @@ -29,9 +30,17 @@ contract BiconomyTokenPaymaster is { using UserOperationLib for PackedUserOperation; + struct TokenInfo { + IOracle oracle; + uint8 decimals; + } + + // State variables address public feeCollector; uint256 public unaccountedGas; uint256 public dynamicAdjustment; + IOracle public nativeOracle; // ETH -> USD price + mapping(address => TokenInfo) tokenDirectory; // Limit for unaccounted gas cost uint256 private constant UNACCOUNTED_GAS_LIMIT = 50_000; @@ -42,7 +51,10 @@ contract BiconomyTokenPaymaster is address _owner, IEntryPoint _entryPoint, uint256 _unaccountedGas, - uint256 _dynamicAdjustment + uint256 _dynamicAdjustment, + IOracle _nativeOracle, + address[] memory _tokens, // Array of token addresses + IOracle[] memory _oracles // Array of corresponding oracle addresses ) BasePaymaster(_owner, _entryPoint) { @@ -50,11 +62,19 @@ contract BiconomyTokenPaymaster is revert UnaccountedGasTooHigh(); } else if (_dynamicAdjustment > MAX_DYNAMIC_ADJUSTMENT || _dynamicAdjustment == 0) { revert InvalidDynamicAdjustment(); + } else if (_tokens.length != _oracles.length) { + revert TokensAndOraclesLengthMismatch(); } assembly ("memory-safe") { sstore(feeCollector.slot, address()) // initialize fee collector to this contract sstore(unaccountedGas.slot, _unaccountedGas) sstore(dynamicAdjustment.slot, _dynamicAdjustment) + sstore(nativeOracle.slot, _nativeOracle) + } + + // Populate the tokenToOracle mapping + for (uint256 i = 0; i < _tokens.length; i++) { + tokenDirectory[_tokens[i]] = TokenInfo(_oracles[i], ERC20(_tokens[i]).decimals()); } } @@ -211,8 +231,7 @@ contract BiconomyTokenPaymaster is override returns (bytes memory context, uint256 validationData) { - (maxCost); - // Implementation of post-operation logic + } /** @@ -239,4 +258,30 @@ contract BiconomyTokenPaymaster is if (target == address(0)) revert CanNotWithdrawToZeroAddress(); SafeTransferLib.safeTransfer(address(token), target, amount); } + + /// @notice Fetches the latest token price. + + /// @return price The latest token price fetched from the oracles. + function getPrice(address tokenAddress) internal view returns (uint192) { + TokenInfo memory tokenInfo = tokenDirectory[tokenAddress]; + uint192 tokenPrice = _fetchPrice(tokenInfo.oracle); + uint192 nativeAssetPrice = _fetchPrice(nativeOracle); + uint192 price = nativeAssetPrice * uint192(tokenInfo.decimals) / tokenPrice; + return price; + } + + /// @notice Fetches the latest price from the given oracle. + /// @dev This function is used to get the latest price from the tokenOracle or nativeAssetOracle. + /// @param _oracle The oracle contract to fetch the price from. + /// @return price The latest price fetched from the oracle. + function _fetchPrice(IOracle _oracle) internal view returns (uint192 price) { + (, int256 answer,, uint256 updatedAt,) = _oracle.latestRoundData(); + if (answer <= 0) { + revert OraclePriceNotPositive(); + } + // if (updatedAt < block.timestamp - stalenessThreshold) { + // revert OraclePriceStale(); + // } + price = uint192(int192(answer)); + } } diff --git a/contracts/token/oracles/TwapOracle.sol b/contracts/token/oracles/TwapOracle.sol new file mode 100644 index 0000000..93e64d7 --- /dev/null +++ b/contracts/token/oracles/TwapOracle.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IOracle} from "../../interfaces/oracles/IOracle.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {OracleLibrary} from "@uniswap/v3-periphery/contracts/libraries/OracleLibrary.sol"; +import {IUniswapV3PoolImmutables} from "@uniswap/v3-core/contracts/interfaces/pool/IUniswapV3PoolImmutables.sol"; + + +contract TwapOracle is IOracle { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CUSTOM ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + /// @dev Invalid TWAP age, either too low or too high + error InvalidTwapAge(); + + /// @dev Pool doesn't contain the base token + error InvalidTokenOrPool(); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS AND IMMUTABLES */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + /// @dev The Uniswap V3 pool address + address public immutable pool; + + /// @dev The base token address (the one which price is being fetched) + address public immutable baseToken; + + /// @dev The base token decimals + uint256 public immutable baseTokenDecimals; + + /// @dev The quote token address (WETH or USD stable coin) + address public immutable quoteToken; + + /// @dev The quote token decimals + uint256 public immutable quoteTokenDecimals; + + /// @dev Default TWAP age, used to fetch the price + uint32 public immutable twapAge; + + uint32 public constant MINIMUM_TWAP_AGE = 1 minutes; + uint32 public constant MAXIMUM_TWAP_AGE = 7 days; + + uint256 public constant ORACLE_DECIMALS = 1e8; + + constructor( + address _pool, + uint32 _twapAge, + address _baseToken + ) { + pool = _pool; + + if (_twapAge < MINIMUM_TWAP_AGE || _twapAge > MAXIMUM_TWAP_AGE) revert InvalidTwapAge(); + twapAge = _twapAge; + + address token0 = IUniswapV3PoolImmutables(_pool).token0(); + address token1 = IUniswapV3PoolImmutables(_pool).token1(); + + if (_baseToken != token0 && _baseToken != token1) revert InvalidTokenOrPool(); + + baseToken = _baseToken; + baseTokenDecimals = 10 ** IERC20Metadata(baseToken).decimals(); + + quoteToken = token0 == baseToken ? token1 : token0; + quoteTokenDecimals = 10 ** IERC20Metadata(quoteToken).decimals(); + } + + function decimals() external override pure returns (uint8) { + return 8; + } + + function latestRoundData() external override view returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) { + uint256 _price = _fetchTwap(); + + // Normalize the price to the oracle decimals + uint256 price = _price * ORACLE_DECIMALS / quoteTokenDecimals; + + return _buildLatestRoundData(price); + } + + function _buildLatestRoundData(uint256 price) internal view returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) { + return (0, int256(price), 0, block.timestamp, 0); + } + + function _fetchTwap() internal view returns (uint256) { + (int24 arithmeticMeanTick,) = OracleLibrary.consult(pool, twapAge); + + return OracleLibrary.getQuoteAtTick( + arithmeticMeanTick, + uint128(baseTokenDecimals), // Base token amount is equal to 1 token + baseToken, + quoteToken + ); + } +} \ No newline at end of file diff --git a/lib/v3-core b/lib/v3-core index e3589b1..6562c52 160000 --- a/lib/v3-core +++ b/lib/v3-core @@ -1 +1 @@ -Subproject commit e3589b192d0be27e100cd0daaf6c97204fdb1899 +Subproject commit 6562c52e8f75f0c10f9deaf44861847585fc8129 diff --git a/lib/v3-periphery b/lib/v3-periphery index 80f26c8..b325bb0 160000 --- a/lib/v3-periphery +++ b/lib/v3-periphery @@ -1 +1 @@ -Subproject commit 80f26c86c57b8a5e4b913f42844d4c8bd274d058 +Subproject commit b325bb0905d922ae61fcc7df85ee802e8df5e96c diff --git a/test/unit/concrete/TestSponsorshipPaymaster.t.sol b/test/unit/concrete/TestSponsorshipPaymaster.t.sol index 4800594..cc8bc8c 100644 --- a/test/unit/concrete/TestSponsorshipPaymaster.t.sol +++ b/test/unit/concrete/TestSponsorshipPaymaster.t.sol @@ -129,14 +129,14 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { } function test_SetUnaccountedGas() external prankModifier(PAYMASTER_OWNER.addr) { - uint16 initialUnaccountedGas = bicoPaymaster.unaccountedGas(); - uint16 newUnaccountedGas = 5000; + uint256 initialUnaccountedGas = bicoPaymaster.unaccountedGas(); + uint256 newUnaccountedGas = 5000; vm.expectEmit(true, true, false, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.UnaccountedGasChanged(initialUnaccountedGas, newUnaccountedGas); bicoPaymaster.setUnaccountedGas(newUnaccountedGas); - uint48 resultingUnaccountedGas = bicoPaymaster.unaccountedGas(); + uint256 resultingUnaccountedGas = bicoPaymaster.unaccountedGas(); assertEq(resultingUnaccountedGas, newUnaccountedGas); } From 06f93ff029fc3d6226421b6f15e9483dfd203464 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Mon, 9 Sep 2024 12:07:16 +0400 Subject: [PATCH 56/69] tokenDirectory to support multiple tokens --- .../common/BiconomyTokenPaymasterErrors.sol | 2 +- .../interfaces/IBiconomyTokenPaymaster.sol | 11 ++++++ contracts/token/BiconomyTokenPaymaster.sol | 38 +++++++++++++------ 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/contracts/common/BiconomyTokenPaymasterErrors.sol b/contracts/common/BiconomyTokenPaymasterErrors.sol index 9b0c365..48de59d 100644 --- a/contracts/common/BiconomyTokenPaymasterErrors.sol +++ b/contracts/common/BiconomyTokenPaymasterErrors.sol @@ -35,7 +35,7 @@ contract BiconomyTokenPaymasterErrors { /** * @notice Throws when each token doesnt have a corresponding oracle */ - error TokensAndOraclesLengthMismatch(); + error TokensAndInfoLengthMismatch(); /** * @notice Throws when oracle returns invalid price diff --git a/contracts/interfaces/IBiconomyTokenPaymaster.sol b/contracts/interfaces/IBiconomyTokenPaymaster.sol index 58157f9..724e2b4 100644 --- a/contracts/interfaces/IBiconomyTokenPaymaster.sol +++ b/contracts/interfaces/IBiconomyTokenPaymaster.sol @@ -1,7 +1,15 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.26; +import { IOracle } from "./oracles/IOracle.sol"; + interface IBiconomyTokenPaymaster { + // Struct for storing information about the token + struct TokenInfo { + IOracle oracle; + uint8 decimals; + } + event UnaccountedGasChanged(uint256 indexed oldValue, uint256 indexed newValue); event FixedDynamicAdjustmentChanged(uint256 indexed oldValue, uint256 indexed newValue); event FeeCollectorChanged(address indexed oldFeeCollector, address indexed newFeeCollector, address indexed actor); @@ -11,10 +19,13 @@ interface IBiconomyTokenPaymaster { event DynamicAdjustmentCollected(address indexed paymasterId, uint256 indexed dynamicAdjustment); event Received(address indexed sender, uint256 value); event TokensWithdrawn(address indexed token, address indexed to, uint256 indexed amount, address actor); + event UpdatedTokenDirectory(address indexed tokenAddress, IOracle indexed oracle, uint8 decimals); function setFeeCollector(address _newFeeCollector) external payable; function setUnaccountedGas(uint256 value) external payable; function setDynamicAdjustment(uint256 _newUnaccountedGas) external payable; + + function setTokenInfo(address _tokenAddress, IOracle _oracle, uint8 _decimals) external payable; } diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index df36782..7a62cb0 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -30,11 +30,6 @@ contract BiconomyTokenPaymaster is { using UserOperationLib for PackedUserOperation; - struct TokenInfo { - IOracle oracle; - uint8 decimals; - } - // State variables address public feeCollector; uint256 public unaccountedGas; @@ -54,6 +49,7 @@ contract BiconomyTokenPaymaster is uint256 _dynamicAdjustment, IOracle _nativeOracle, address[] memory _tokens, // Array of token addresses + uint8[] memory _decimals, // Array of corresponding token decimals IOracle[] memory _oracles // Array of corresponding oracle addresses ) BasePaymaster(_owner, _entryPoint) @@ -62,8 +58,8 @@ contract BiconomyTokenPaymaster is revert UnaccountedGasTooHigh(); } else if (_dynamicAdjustment > MAX_DYNAMIC_ADJUSTMENT || _dynamicAdjustment == 0) { revert InvalidDynamicAdjustment(); - } else if (_tokens.length != _oracles.length) { - revert TokensAndOraclesLengthMismatch(); + } else if (_tokens.length != _oracles.length || _tokens.length != _decimals.length) { + revert TokensAndInfoLengthMismatch(); } assembly ("memory-safe") { sstore(feeCollector.slot, address()) // initialize fee collector to this contract @@ -74,7 +70,7 @@ contract BiconomyTokenPaymaster is // Populate the tokenToOracle mapping for (uint256 i = 0; i < _tokens.length; i++) { - tokenDirectory[_tokens[i]] = TokenInfo(_oracles[i], ERC20(_tokens[i]).decimals()); + tokenDirectory[_tokens[i]] = TokenInfo(_oracles[i], _decimals[i]); } } @@ -200,7 +196,7 @@ contract BiconomyTokenPaymaster is } /** - * @dev Set a new unaccountedEPGasOverhead value. + * @dev Set a new dynamicAdjustment value. * @param _newDynamicAdjustment The new value to be set as the unaccounted gas value * @notice only to be called by the owner of the contract. */ @@ -215,6 +211,26 @@ contract BiconomyTokenPaymaster is emit FixedDynamicAdjustmentChanged(oldDynamicAdjustment, _newDynamicAdjustment); } + /** + * @dev Set or update a TokenInfo entry in the tokenDirectory mapping. + * @param _tokenAddress The new value to be set as the unaccounted gas value + * @param _oracle The new value to be set as the unaccounted gas value + * @param _decimals The new value to be set as the unaccounted gas value + * @notice only to be called by the owner of the contract. + */ + function setTokenInfo( + address _tokenAddress, + IOracle _oracle, + uint8 _decimals + ) + external + payable + override + onlyOwner + { + tokenDirectory[_tokenAddress] = TokenInfo(_oracle, _decimals); + } + /** * @dev Validate a user operation. * This method is abstract in BasePaymaster and must be implemented in derived contracts. @@ -230,9 +246,7 @@ contract BiconomyTokenPaymaster is internal override returns (bytes memory context, uint256 validationData) - { - - } + { } /** * @dev Post-operation handler. From e747ec72eca61a9a1fc67dacb68e10fc848a1b7b Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Mon, 9 Sep 2024 12:27:01 +0400 Subject: [PATCH 57/69] added price expiry duration --- .../common/BiconomyTokenPaymasterErrors.sol | 5 +++ .../interfaces/IBiconomyTokenPaymaster.sol | 9 ++-- contracts/token/BiconomyTokenPaymaster.sol | 42 +++++++++++++------ 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/contracts/common/BiconomyTokenPaymasterErrors.sol b/contracts/common/BiconomyTokenPaymasterErrors.sol index 48de59d..6a3235a 100644 --- a/contracts/common/BiconomyTokenPaymasterErrors.sol +++ b/contracts/common/BiconomyTokenPaymasterErrors.sol @@ -41,4 +41,9 @@ contract BiconomyTokenPaymasterErrors { * @notice Throws when oracle returns invalid price */ error OraclePriceNotPositive(); + + /** + * @notice Throws when oracle price hasn't been updated for a duration of time the owner is comfortable with + */ + error OraclePriceExpired(); } diff --git a/contracts/interfaces/IBiconomyTokenPaymaster.sol b/contracts/interfaces/IBiconomyTokenPaymaster.sol index 724e2b4..50189f0 100644 --- a/contracts/interfaces/IBiconomyTokenPaymaster.sol +++ b/contracts/interfaces/IBiconomyTokenPaymaster.sol @@ -10,9 +10,10 @@ interface IBiconomyTokenPaymaster { uint8 decimals; } - event UnaccountedGasChanged(uint256 indexed oldValue, uint256 indexed newValue); - event FixedDynamicAdjustmentChanged(uint256 indexed oldValue, uint256 indexed newValue); - event FeeCollectorChanged(address indexed oldFeeCollector, address indexed newFeeCollector, address indexed actor); + event UpdatedUnaccountedGas(uint256 indexed oldValue, uint256 indexed newValue); + event UpdatedFixedDynamicAdjustment(uint256 indexed oldValue, uint256 indexed newValue); + event UpdatedFeeCollector(address indexed oldFeeCollector, address indexed newFeeCollector, address indexed actor); + event UpdatedPriceExpiryDuration(uint256 indexed oldValue, uint256 indexed newValue); event GasDeposited(address indexed paymasterId, uint256 indexed value); event GasWithdrawn(address indexed paymasterId, address indexed to, uint256 indexed value); event GasBalanceDeducted(address indexed paymasterId, uint256 indexed charge, bytes32 indexed userOpHash); @@ -27,5 +28,7 @@ interface IBiconomyTokenPaymaster { function setDynamicAdjustment(uint256 _newUnaccountedGas) external payable; + function setPriceExpiryDuration(uint256 _newPriceExpiryDuration) external payable; + function setTokenInfo(address _tokenAddress, IOracle _oracle, uint8 _decimals) external payable; } diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index 7a62cb0..af3dc4c 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -17,10 +17,12 @@ import "@account-abstraction/contracts/core/Helpers.sol"; * @author ShivaanshK * @author livingrockrises * @notice Token Paymaster for Entry Point v0.7 - * @dev A paymaster that allows user to pay gas fees in ERC20 tokens. The paymaster owner chooses which tokens to - * accept. The payment manager (usually the owner) first deposits native gas into the EntryPoint. Then, for each - * transaction, it takes the gas fee from the user's ERC20 token balance. The exchange rate between ETH and the token is - * calculated using 1 of three methods: external price source, off-chain oracle, or a TWAP oracle. + * @dev A paymaster that allows user to pay gas fees in ERC20 tokens. The paymaster uses the precharge and refund model + * to handle gas remittances. For fair and "always available" operation, it relies on price oracles which + * implement the IOracle interface to calculate the gas cost of the transaction in a supported token. The owner has full + * discretion over the supported tokens, premium and discounts applied (if any), and how to manage the assets + * received by the paymaster. The properties described above, make the paymaster self-relaint: independent of any + * offchain service for use. */ contract BiconomyTokenPaymaster is BasePaymaster, @@ -34,13 +36,14 @@ contract BiconomyTokenPaymaster is address public feeCollector; uint256 public unaccountedGas; uint256 public dynamicAdjustment; + uint256 public priceExpiryDuration; IOracle public nativeOracle; // ETH -> USD price mapping(address => TokenInfo) tokenDirectory; // Limit for unaccounted gas cost uint256 private constant UNACCOUNTED_GAS_LIMIT = 50_000; uint256 private constant PRICE_DENOMINATOR = 1e6; - uint256 private constant MAX_DYNAMIC_ADJUSTMENT = 2e6; + uint256 private constant MAX_DYNAMIC_ADJUSTMENT = 2e6; // 100% premium on price (2e6/PRICE_DENOMINATOR) constructor( address _owner, @@ -48,6 +51,7 @@ contract BiconomyTokenPaymaster is uint256 _unaccountedGas, uint256 _dynamicAdjustment, IOracle _nativeOracle, + uint256 _priceExpiryDuration, address[] memory _tokens, // Array of token addresses uint8[] memory _decimals, // Array of corresponding token decimals IOracle[] memory _oracles // Array of corresponding oracle addresses @@ -176,7 +180,7 @@ contract BiconomyTokenPaymaster is assembly ("memory-safe") { sstore(feeCollector.slot, _newFeeCollector) } - emit FeeCollectorChanged(oldFeeCollector, _newFeeCollector, msg.sender); + emit UpdatedFeeCollector(oldFeeCollector, _newFeeCollector, msg.sender); } /** @@ -192,12 +196,12 @@ contract BiconomyTokenPaymaster is assembly ("memory-safe") { sstore(unaccountedGas.slot, _newUnaccountedGas) } - emit UnaccountedGasChanged(oldUnaccountedGas, _newUnaccountedGas); + emit UpdatedUnaccountedGas(oldUnaccountedGas, _newUnaccountedGas); } /** * @dev Set a new dynamicAdjustment value. - * @param _newDynamicAdjustment The new value to be set as the unaccounted gas value + * @param _newDynamicAdjustment The new value to be set as the dynamic adjustment * @notice only to be called by the owner of the contract. */ function setDynamicAdjustment(uint256 _newDynamicAdjustment) external payable override onlyOwner { @@ -208,7 +212,20 @@ contract BiconomyTokenPaymaster is assembly ("memory-safe") { sstore(dynamicAdjustment.slot, _newDynamicAdjustment) } - emit FixedDynamicAdjustmentChanged(oldDynamicAdjustment, _newDynamicAdjustment); + emit UpdatedFixedDynamicAdjustment(oldDynamicAdjustment, _newDynamicAdjustment); + } + + /** + * @dev Set a new dynamicAdjustment value. + * @param _newPriceExpiryDuration The new value to be set as the unaccounted gas value + * @notice only to be called by the owner of the contract. + */ + function setPriceExpiryDuration(uint256 _newPriceExpiryDuration) external payable override onlyOwner { + uint256 oldPriceExpiryDuration = priceExpiryDuration; + assembly ("memory-safe") { + sstore(priceExpiryDuration.slot, _newPriceExpiryDuration) + } + emit UpdatedPriceExpiryDuration(oldPriceExpiryDuration, _newPriceExpiryDuration); } /** @@ -229,6 +246,7 @@ contract BiconomyTokenPaymaster is onlyOwner { tokenDirectory[_tokenAddress] = TokenInfo(_oracle, _decimals); + emit UpdatedTokenDirectory(_tokenAddress, _oracle, _decimals); } /** @@ -293,9 +311,9 @@ contract BiconomyTokenPaymaster is if (answer <= 0) { revert OraclePriceNotPositive(); } - // if (updatedAt < block.timestamp - stalenessThreshold) { - // revert OraclePriceStale(); - // } + if (updatedAt < block.timestamp - priceExpiryDuration) { + revert OraclePriceExpired(); + } price = uint192(int192(answer)); } } From a4d56bb15213d23024de67ee4756a794f9012356 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Mon, 9 Sep 2024 16:05:36 +0400 Subject: [PATCH 58/69] Independent paymaster mode validation --- .../common/BiconomyTokenPaymasterErrors.sol | 15 ++++ .../interfaces/IBiconomyTokenPaymaster.sol | 7 +- contracts/libraries/PaymasterParser.sol | 34 +++++++ contracts/token/BiconomyTokenPaymaster.sol | 88 +++++++++++++------ contracts/token/oracles/TwapOracle.sol | 2 +- 5 files changed, 117 insertions(+), 29 deletions(-) create mode 100644 contracts/libraries/PaymasterParser.sol diff --git a/contracts/common/BiconomyTokenPaymasterErrors.sol b/contracts/common/BiconomyTokenPaymasterErrors.sol index 6a3235a..2ad54c3 100644 --- a/contracts/common/BiconomyTokenPaymasterErrors.sol +++ b/contracts/common/BiconomyTokenPaymasterErrors.sol @@ -37,6 +37,11 @@ contract BiconomyTokenPaymasterErrors { */ error TokensAndInfoLengthMismatch(); + /** + * @notice Throws when invalid PaymasterMode specified in paymasterAndData + */ + error InvalidPaymasterMode(); + /** * @notice Throws when oracle returns invalid price */ @@ -46,4 +51,14 @@ contract BiconomyTokenPaymasterErrors { * @notice Throws when oracle price hasn't been updated for a duration of time the owner is comfortable with */ error OraclePriceExpired(); + + /** + * @notice Throws when token address to pay with is invalid + */ + error InvalidTokenAddress(); + + /** + * @notice Throws when user tries to pay with an unsupported token + */ + error TokenNotSupported(); } diff --git a/contracts/interfaces/IBiconomyTokenPaymaster.sol b/contracts/interfaces/IBiconomyTokenPaymaster.sol index 50189f0..97116cc 100644 --- a/contracts/interfaces/IBiconomyTokenPaymaster.sol +++ b/contracts/interfaces/IBiconomyTokenPaymaster.sol @@ -4,6 +4,11 @@ pragma solidity ^0.8.26; import { IOracle } from "./oracles/IOracle.sol"; interface IBiconomyTokenPaymaster { + enum PaymasterMode { + EXTERNAL, + INDEPENDENT + } + // Struct for storing information about the token struct TokenInfo { IOracle oracle; @@ -30,5 +35,5 @@ interface IBiconomyTokenPaymaster { function setPriceExpiryDuration(uint256 _newPriceExpiryDuration) external payable; - function setTokenInfo(address _tokenAddress, IOracle _oracle, uint8 _decimals) external payable; + function setTokenInfo(address _tokenAddress, IOracle _oracle) external payable; } diff --git a/contracts/libraries/PaymasterParser.sol b/contracts/libraries/PaymasterParser.sol new file mode 100644 index 0000000..d60ab1e --- /dev/null +++ b/contracts/libraries/PaymasterParser.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.26; + +import { IBiconomyTokenPaymaster } from "../interfaces/IBiconomyTokenPaymaster.sol"; +import "@account-abstraction/contracts/core/UserOperationLib.sol"; + +// A helper library to parse paymaster and data +library PaymasterParser { + uint256 private constant PAYMASTER_MODE_OFFSET = UserOperationLib.PAYMASTER_DATA_OFFSET; // Start offset of mode in + // PND + + function parsePaymasterAndData(bytes calldata paymasterAndData) + external + pure + returns (IBiconomyTokenPaymaster.PaymasterMode mode, bytes memory modeSpecificData) + { + unchecked { + mode = IBiconomyTokenPaymaster.PaymasterMode( + uint8(bytes1(paymasterAndData[PAYMASTER_MODE_OFFSET:PAYMASTER_MODE_OFFSET + 8])) + ); + modeSpecificData = paymasterAndData[PAYMASTER_MODE_OFFSET + 8:]; + } + } + + function parseExternalModeSpecificData(bytes calldata modeSpecificData) external pure { } + + function parseIndependentModeSpecificData(bytes calldata modeSpecificData) + external + pure + returns (address tokenAddress) + { + tokenAddress = address(bytes20(modeSpecificData[:20])); + } +} diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index af3dc4c..fc6aba4 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -4,12 +4,14 @@ pragma solidity ^0.8.26; import { ReentrancyGuardTransient } from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; import { IEntryPoint } from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; import { PackedUserOperation, UserOperationLib } from "@account-abstraction/contracts/core/UserOperationLib.sol"; -import { IERC20, ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { SafeTransferLib } from "@solady/src/utils/SafeTransferLib.sol"; import { BasePaymaster } from "../base/BasePaymaster.sol"; import { BiconomyTokenPaymasterErrors } from "../common/BiconomyTokenPaymasterErrors.sol"; import { IBiconomyTokenPaymaster } from "../interfaces/IBiconomyTokenPaymaster.sol"; import { IOracle } from "../interfaces/oracles/IOracle.sol"; +import { PaymasterParser } from "../libraries/PaymasterParser.sol"; import "@account-abstraction/contracts/core/Helpers.sol"; /** @@ -18,8 +20,9 @@ import "@account-abstraction/contracts/core/Helpers.sol"; * @author livingrockrises * @notice Token Paymaster for Entry Point v0.7 * @dev A paymaster that allows user to pay gas fees in ERC20 tokens. The paymaster uses the precharge and refund model - * to handle gas remittances. For fair and "always available" operation, it relies on price oracles which - * implement the IOracle interface to calculate the gas cost of the transaction in a supported token. The owner has full + * to handle gas remittances. For fair and "always available" operation, it supports a mode that relies purely on price + * oracles (Offchain and TWAP) which + * implement the IOracle interface to calculate the token cost of a transaction. The owner has full * discretion over the supported tokens, premium and discounts applied (if any), and how to manage the assets * received by the paymaster. The properties described above, make the paymaster self-relaint: independent of any * offchain service for use. @@ -31,6 +34,7 @@ contract BiconomyTokenPaymaster is IBiconomyTokenPaymaster { using UserOperationLib for PackedUserOperation; + using PaymasterParser for bytes; // State variables address public feeCollector; @@ -40,9 +44,9 @@ contract BiconomyTokenPaymaster is IOracle public nativeOracle; // ETH -> USD price mapping(address => TokenInfo) tokenDirectory; - // Limit for unaccounted gas cost - uint256 private constant UNACCOUNTED_GAS_LIMIT = 50_000; - uint256 private constant PRICE_DENOMINATOR = 1e6; + // PAYMASTER_ID_OFFSET + uint256 private constant UNACCOUNTED_GAS_LIMIT = 50_000; // Limit for unaccounted gas cost + uint256 private constant PRICE_DENOMINATOR = 1e6; // Denominator used when calculating cost with dynamic adjustment uint256 private constant MAX_DYNAMIC_ADJUSTMENT = 2e6; // 100% premium on price (2e6/PRICE_DENOMINATOR) constructor( @@ -53,7 +57,6 @@ contract BiconomyTokenPaymaster is IOracle _nativeOracle, uint256 _priceExpiryDuration, address[] memory _tokens, // Array of token addresses - uint8[] memory _decimals, // Array of corresponding token decimals IOracle[] memory _oracles // Array of corresponding oracle addresses ) BasePaymaster(_owner, _entryPoint) @@ -62,19 +65,20 @@ contract BiconomyTokenPaymaster is revert UnaccountedGasTooHigh(); } else if (_dynamicAdjustment > MAX_DYNAMIC_ADJUSTMENT || _dynamicAdjustment == 0) { revert InvalidDynamicAdjustment(); - } else if (_tokens.length != _oracles.length || _tokens.length != _decimals.length) { + } else if (_tokens.length != _oracles.length) { revert TokensAndInfoLengthMismatch(); } assembly ("memory-safe") { sstore(feeCollector.slot, address()) // initialize fee collector to this contract sstore(unaccountedGas.slot, _unaccountedGas) sstore(dynamicAdjustment.slot, _dynamicAdjustment) + sstore(priceExpiryDuration.slot, _priceExpiryDuration) sstore(nativeOracle.slot, _nativeOracle) } // Populate the tokenToOracle mapping for (uint256 i = 0; i < _tokens.length; i++) { - tokenDirectory[_tokens[i]] = TokenInfo(_oracles[i], _decimals[i]); + tokenDirectory[_tokens[i]] = TokenInfo(_oracles[i], IERC20Metadata(_tokens[i]).decimals()); } } @@ -232,21 +236,12 @@ contract BiconomyTokenPaymaster is * @dev Set or update a TokenInfo entry in the tokenDirectory mapping. * @param _tokenAddress The new value to be set as the unaccounted gas value * @param _oracle The new value to be set as the unaccounted gas value - * @param _decimals The new value to be set as the unaccounted gas value * @notice only to be called by the owner of the contract. */ - function setTokenInfo( - address _tokenAddress, - IOracle _oracle, - uint8 _decimals - ) - external - payable - override - onlyOwner - { - tokenDirectory[_tokenAddress] = TokenInfo(_oracle, _decimals); - emit UpdatedTokenDirectory(_tokenAddress, _oracle, _decimals); + function setTokenInfo(address _tokenAddress, IOracle _oracle) external payable override onlyOwner { + uint8 decimals = IERC20Metadata(_tokenAddress).decimals(); + tokenDirectory[_tokenAddress] = TokenInfo(_oracle, decimals); + emit UpdatedTokenDirectory(_tokenAddress, _oracle, decimals); } /** @@ -264,7 +259,41 @@ contract BiconomyTokenPaymaster is internal override returns (bytes memory context, uint256 validationData) - { } + { + (PaymasterMode mode, bytes memory modeSpecificData) = userOp.paymasterAndData.parsePaymasterAndData(); + + if (uint8(mode) > 1) { + revert InvalidPaymasterMode(); + } + + if (mode == PaymasterMode.EXTERNAL) { + // Use the price and other params specified in modeSpecificData by the verifyingSigner + // Useful for supporting tokens which don't have oracle support + } else if (mode == PaymasterMode.INDEPENDENT) { + // Use only oracles for the token specified in modeSpecificData + if (modeSpecificData.length != 20) { + revert InvalidTokenAddress(); + } + + // Get address for token used to pay + address tokenAddress = modeSpecificData.parseIndependentModeSpecificData(); + uint192 tokenPrice = getPrice(tokenAddress); + uint256 tokenAmount; + + { + // Calculate token amount to precharge + uint256 maxFeePerGas = UserOperationLib.unpackMaxFeePerGas(userOp); + tokenAmount = (maxCost + (unaccountedGas) * maxFeePerGas) * dynamicAdjustment * tokenPrice + / (1e18 * PRICE_DENOMINATOR); + } + + // Transfer full amount to this address. Unused amount will be refunded in postOP + SafeTransferLib.safeTransferFrom(tokenAddress, userOp.sender, address(this), tokenAmount); + + context = abi.encodePacked(tokenAmount, tokenPrice, userOp.sender, userOpHash); + validationData = 0; + } + } /** * @dev Post-operation handler. @@ -292,14 +321,19 @@ contract BiconomyTokenPaymaster is } /// @notice Fetches the latest token price. - /// @return price The latest token price fetched from the oracles. - function getPrice(address tokenAddress) internal view returns (uint192) { + function getPrice(address tokenAddress) internal view returns (uint192 price) { TokenInfo memory tokenInfo = tokenDirectory[tokenAddress]; + + if (address(tokenInfo.oracle) == address(0)) { + // If oracle not set, token isn't supported + revert TokenNotSupported(); + } + uint192 tokenPrice = _fetchPrice(tokenInfo.oracle); uint192 nativeAssetPrice = _fetchPrice(nativeOracle); - uint192 price = nativeAssetPrice * uint192(tokenInfo.decimals) / tokenPrice; - return price; + + price = nativeAssetPrice * uint192(tokenInfo.decimals) / tokenPrice; } /// @notice Fetches the latest price from the given oracle. diff --git a/contracts/token/oracles/TwapOracle.sol b/contracts/token/oracles/TwapOracle.sol index 93e64d7..a34b8d9 100644 --- a/contracts/token/oracles/TwapOracle.sol +++ b/contracts/token/oracles/TwapOracle.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; +pragma solidity ^0.8.26; import {IOracle} from "../../interfaces/oracles/IOracle.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; From b9188cff58d6969eacff700b48003cefe5b1cbf7 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Mon, 9 Sep 2024 17:44:03 +0400 Subject: [PATCH 59/69] external price should validation done --- contracts/base/BasePaymaster.sol | 11 ++ .../common/BiconomyTokenPaymasterErrors.sol | 14 ++ .../interfaces/IBiconomyTokenPaymaster.sol | 16 +- contracts/libraries/PaymasterParser.sol | 24 ++- .../BiconomySponsorshipPaymaster.sol | 11 -- contracts/token/BiconomyTokenPaymaster.sol | 177 ++++++++++++++++-- 6 files changed, 224 insertions(+), 29 deletions(-) diff --git a/contracts/base/BasePaymaster.sol b/contracts/base/BasePaymaster.sol index d7dd243..f3394c1 100644 --- a/contracts/base/BasePaymaster.sol +++ b/contracts/base/BasePaymaster.sol @@ -162,4 +162,15 @@ abstract contract BasePaymaster is IPaymaster, SoladyOwnable { function _requireFromEntryPoint() internal virtual { require(msg.sender == address(entryPoint), "Sender not EntryPoint"); } + + /** + * Check if address is a contract + */ + function _isContract(address _addr) internal view returns (bool) { + uint256 size; + assembly ("memory-safe") { + size := extcodesize(_addr) + } + return size > 0; + } } diff --git a/contracts/common/BiconomyTokenPaymasterErrors.sol b/contracts/common/BiconomyTokenPaymasterErrors.sol index 2ad54c3..7447993 100644 --- a/contracts/common/BiconomyTokenPaymasterErrors.sol +++ b/contracts/common/BiconomyTokenPaymasterErrors.sol @@ -6,6 +6,10 @@ contract BiconomyTokenPaymasterErrors { * @notice Throws when the verifiying signer address provided is address(0) */ error VerifyingSignerCanNotBeZero(); + /** + * @notice Throws when the fee collector address provided is a deployed contract + */ + error VerifyingSignerCanNotBeContract(); /** * @notice Throws when the fee collector address provided is address(0) @@ -61,4 +65,14 @@ contract BiconomyTokenPaymasterErrors { * @notice Throws when user tries to pay with an unsupported token */ error TokenNotSupported(); + + /** + * @notice Throws when oracle decimals aren't equal to 8 + */ + error InvalidOracleDecimals(); + + /** + * @notice Throws when external signer's signature has invalid length + */ + error InvalidSignatureLength(); } diff --git a/contracts/interfaces/IBiconomyTokenPaymaster.sol b/contracts/interfaces/IBiconomyTokenPaymaster.sol index 97116cc..82110e5 100644 --- a/contracts/interfaces/IBiconomyTokenPaymaster.sol +++ b/contracts/interfaces/IBiconomyTokenPaymaster.sol @@ -4,19 +4,22 @@ pragma solidity ^0.8.26; import { IOracle } from "./oracles/IOracle.sol"; interface IBiconomyTokenPaymaster { + // Modes that paymaster can be used in enum PaymasterMode { - EXTERNAL, - INDEPENDENT + EXTERNAL, // Price provided by external service. Authenticated using signature from verifyingSigner + INDEPENDENT // Price queried from oracle. No signature needed from external service. + } // Struct for storing information about the token struct TokenInfo { IOracle oracle; - uint8 decimals; + uint256 decimals; } event UpdatedUnaccountedGas(uint256 indexed oldValue, uint256 indexed newValue); event UpdatedFixedDynamicAdjustment(uint256 indexed oldValue, uint256 indexed newValue); + event UpdatedVerifyingSigner(address indexed oldSigner, address indexed newSigner, address indexed actor); event UpdatedFeeCollector(address indexed oldFeeCollector, address indexed newFeeCollector, address indexed actor); event UpdatedPriceExpiryDuration(uint256 indexed oldValue, uint256 indexed newValue); event GasDeposited(address indexed paymasterId, uint256 indexed value); @@ -26,6 +29,9 @@ interface IBiconomyTokenPaymaster { event Received(address indexed sender, uint256 value); event TokensWithdrawn(address indexed token, address indexed to, uint256 indexed amount, address actor); event UpdatedTokenDirectory(address indexed tokenAddress, IOracle indexed oracle, uint8 decimals); + event UpdatedNativeAssetOracle(IOracle indexed oldOracle, IOracle indexed newOracle); + + function setSigner(address _newVerifyingSigner) external payable; function setFeeCollector(address _newFeeCollector) external payable; @@ -35,5 +41,7 @@ interface IBiconomyTokenPaymaster { function setPriceExpiryDuration(uint256 _newPriceExpiryDuration) external payable; - function setTokenInfo(address _tokenAddress, IOracle _oracle) external payable; + function setNativeOracle(IOracle _oracle) external payable; + + function updateTokenDirectory(address _tokenAddress, IOracle _oracle) external payable; } diff --git a/contracts/libraries/PaymasterParser.sol b/contracts/libraries/PaymasterParser.sol index d60ab1e..4763996 100644 --- a/contracts/libraries/PaymasterParser.sol +++ b/contracts/libraries/PaymasterParser.sol @@ -6,8 +6,8 @@ import "@account-abstraction/contracts/core/UserOperationLib.sol"; // A helper library to parse paymaster and data library PaymasterParser { - uint256 private constant PAYMASTER_MODE_OFFSET = UserOperationLib.PAYMASTER_DATA_OFFSET; // Start offset of mode in - // PND + // Start offset of mode in PND + uint256 private constant PAYMASTER_MODE_OFFSET = UserOperationLib.PAYMASTER_DATA_OFFSET; function parsePaymasterAndData(bytes calldata paymasterAndData) external @@ -22,7 +22,25 @@ library PaymasterParser { } } - function parseExternalModeSpecificData(bytes calldata modeSpecificData) external pure { } + function parseExternalModeSpecificData(bytes calldata modeSpecificData) + external + pure + returns ( + uint48 validUntil, + uint48 validAfter, + address tokenAddress, + uint128 tokenPrice, + uint32 externalDynamicAdjustment, + bytes memory signature + ) + { + validUntil = uint48(bytes6(modeSpecificData[:6])); + validAfter = uint48(bytes6(modeSpecificData[6:12])); + tokenAddress = address(bytes20(modeSpecificData[12:32])); + tokenPrice = uint128(bytes16(modeSpecificData[32:48])); + externalDynamicAdjustment = uint32(bytes4(modeSpecificData[48:52])); + signature = modeSpecificData[52:]; + } function parseIndependentModeSpecificData(bytes calldata modeSpecificData) external diff --git a/contracts/sponsorship/BiconomySponsorshipPaymaster.sol b/contracts/sponsorship/BiconomySponsorshipPaymaster.sol index 706b38e..c4fecb9 100644 --- a/contracts/sponsorship/BiconomySponsorshipPaymaster.sol +++ b/contracts/sponsorship/BiconomySponsorshipPaymaster.sol @@ -364,15 +364,4 @@ contract BiconomySponsorshipPaymaster is revert UnaccountedGasTooHigh(); } } - - /** - * Check if address is a contract - */ - function _isContract(address _addr) internal view returns (bool) { - uint256 size; - assembly ("memory-safe") { - size := extcodesize(_addr) - } - return size > 0; - } } diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index fc6aba4..71b9a88 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -12,6 +12,8 @@ import { BiconomyTokenPaymasterErrors } from "../common/BiconomyTokenPaymasterEr import { IBiconomyTokenPaymaster } from "../interfaces/IBiconomyTokenPaymaster.sol"; import { IOracle } from "../interfaces/oracles/IOracle.sol"; import { PaymasterParser } from "../libraries/PaymasterParser.sol"; +import { SignatureCheckerLib } from "@solady/src/utils/SignatureCheckerLib.sol"; +import { ECDSA as ECDSA_solady } from "@solady/src/utils/ECDSA.sol"; import "@account-abstraction/contracts/core/Helpers.sol"; /** @@ -35,9 +37,11 @@ contract BiconomyTokenPaymaster is { using UserOperationLib for PackedUserOperation; using PaymasterParser for bytes; + using SignatureCheckerLib for address; // State variables address public feeCollector; + address public verifyingSigner; uint256 public unaccountedGas; uint256 public dynamicAdjustment; uint256 public priceExpiryDuration; @@ -51,6 +55,7 @@ contract BiconomyTokenPaymaster is constructor( address _owner, + address _verifyingSigner, IEntryPoint _entryPoint, uint256 _unaccountedGas, uint256 _dynamicAdjustment, @@ -61,14 +66,29 @@ contract BiconomyTokenPaymaster is ) BasePaymaster(_owner, _entryPoint) { + if (_isContract(_verifyingSigner)) { + revert VerifyingSignerCanNotBeContract(); + } + if (_verifyingSigner == address(0)) { + revert VerifyingSignerCanNotBeZero(); + } if (_unaccountedGas > UNACCOUNTED_GAS_LIMIT) { revert UnaccountedGasTooHigh(); - } else if (_dynamicAdjustment > MAX_DYNAMIC_ADJUSTMENT || _dynamicAdjustment == 0) { + } + if (_dynamicAdjustment > MAX_DYNAMIC_ADJUSTMENT || _dynamicAdjustment < PRICE_DENOMINATOR) { revert InvalidDynamicAdjustment(); - } else if (_tokens.length != _oracles.length) { + } + if (_tokens.length != _oracles.length) { revert TokensAndInfoLengthMismatch(); } + if (_nativeOracle.decimals() != 8) { + // ETH -> USD will always have 8 decimals for Chainlink and TWAP + revert InvalidOracleDecimals(); + } + + // Set state variables assembly ("memory-safe") { + sstore(verifyingSigner.slot, _verifyingSigner) sstore(feeCollector.slot, address()) // initialize fee collector to this contract sstore(unaccountedGas.slot, _unaccountedGas) sstore(dynamicAdjustment.slot, _dynamicAdjustment) @@ -78,7 +98,11 @@ contract BiconomyTokenPaymaster is // Populate the tokenToOracle mapping for (uint256 i = 0; i < _tokens.length; i++) { - tokenDirectory[_tokens[i]] = TokenInfo(_oracles[i], IERC20Metadata(_tokens[i]).decimals()); + if (_oracles[i].decimals() != 8) { + // Token -> USD will always have 8 decimals + revert InvalidOracleDecimals(); + } + tokenDirectory[_tokens[i]] = TokenInfo(_oracles[i], 10 ** IERC20Metadata(_tokens[i]).decimals()); } } @@ -171,6 +195,25 @@ contract BiconomyTokenPaymaster is } } + /** + * @dev Set a new verifying signer address. + * Can only be called by the owner of the contract. + * @param _newVerifyingSigner The new address to be set as the verifying signer. + * @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 override onlyOwner { + if (_isContract(_newVerifyingSigner)) revert VerifyingSignerCanNotBeContract(); + if (_newVerifyingSigner == address(0)) { + revert VerifyingSignerCanNotBeZero(); + } + address oldSigner = verifyingSigner; + assembly ("memory-safe") { + sstore(verifyingSigner.slot, _newVerifyingSigner) + } + emit UpdatedVerifyingSigner(oldSigner, _newVerifyingSigner, msg.sender); + } + /** * @dev Set a new fee collector address. * Can only be called by the owner of the contract. @@ -209,7 +252,7 @@ contract BiconomyTokenPaymaster is * @notice only to be called by the owner of the contract. */ function setDynamicAdjustment(uint256 _newDynamicAdjustment) external payable override onlyOwner { - if (_newDynamicAdjustment > MAX_DYNAMIC_ADJUSTMENT || _newDynamicAdjustment == 0) { + if (_newDynamicAdjustment > MAX_DYNAMIC_ADJUSTMENT || _newDynamicAdjustment < PRICE_DENOMINATOR) { revert InvalidDynamicAdjustment(); } uint256 oldDynamicAdjustment = dynamicAdjustment; @@ -232,18 +275,85 @@ contract BiconomyTokenPaymaster is emit UpdatedPriceExpiryDuration(oldPriceExpiryDuration, _newPriceExpiryDuration); } + /** + * @dev Update the native oracle address + * @param _oracle The new native asset oracle + * @notice only to be called by the owner of the contract. + */ + function setNativeOracle(IOracle _oracle) external payable override onlyOwner { + if (_oracle.decimals() != 8) { + // Native -> USD will always have 8 decimals + revert InvalidOracleDecimals(); + } + + IOracle oldNativeOracle = nativeOracle; + assembly ("memory-safe") { + sstore(nativeOracle.slot, _oracle) + } + + emit UpdatedNativeAssetOracle(oldNativeOracle, _oracle); + } + /** * @dev Set or update a TokenInfo entry in the tokenDirectory mapping. - * @param _tokenAddress The new value to be set as the unaccounted gas value - * @param _oracle The new value to be set as the unaccounted gas value + * @param _tokenAddress The token address to add or update in directory + * @param _oracle The oracle to use for the specified token * @notice only to be called by the owner of the contract. */ - function setTokenInfo(address _tokenAddress, IOracle _oracle) external payable override onlyOwner { + function updateTokenDirectory(address _tokenAddress, IOracle _oracle) external payable override onlyOwner { + if (_oracle.decimals() != 8) { + // Token -> USD will always have 8 decimals + revert InvalidOracleDecimals(); + } + uint8 decimals = IERC20Metadata(_tokenAddress).decimals(); - tokenDirectory[_tokenAddress] = TokenInfo(_oracle, decimals); + tokenDirectory[_tokenAddress] = TokenInfo(_oracle, 10 ** decimals); + emit UpdatedTokenDirectory(_tokenAddress, _oracle, decimals); } + /** + * 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. + * it is called on-chain from the validatePaymasterUserOp, to validate the signature. + * 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, + address tokenAddress, + uint128 tokenPrice, + uint32 externalDynamicAdjustment + ) + public + view + returns (bytes32) + { + //can't use userOp.hash(), since it contains also the paymasterAndData itself. + address sender = userOp.getSender(); + 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])), + userOp.preVerificationGas, + userOp.gasFees, + block.chainid, + address(this), + validUntil, + validAfter, + tokenAddress, + tokenPrice, + externalDynamicAdjustment + ) + ); + } + /** * @dev Validate a user operation. * This method is abstract in BasePaymaster and must be implemented in derived contracts. @@ -269,6 +379,48 @@ contract BiconomyTokenPaymaster is if (mode == PaymasterMode.EXTERNAL) { // Use the price and other params specified in modeSpecificData by the verifyingSigner // Useful for supporting tokens which don't have oracle support + + ( + uint48 validUntil, + uint48 validAfter, + address tokenAddress, + uint128 tokenPrice, + uint32 externalDynamicAdjustment, + bytes memory signature + ) = modeSpecificData.parseExternalModeSpecificData(); + + if (signature.length != 64 && signature.length != 65) { + revert InvalidSignatureLength(); + } + + bool validSig = verifyingSigner.isValidSignatureNow( + ECDSA_solady.toEthSignedMessageHash( + getHash(userOp, validUntil, validAfter, tokenAddress, tokenPrice, externalDynamicAdjustment) + ), + signature + ); + + //don't revert on signature failure: return SIG_VALIDATION_FAILED + if (!validSig) { + return ("", _packValidationData(true, validUntil, validAfter)); + } + + if (externalDynamicAdjustment > MAX_DYNAMIC_ADJUSTMENT || externalDynamicAdjustment < PRICE_DENOMINATOR) { + revert InvalidDynamicAdjustment(); + } + + uint256 tokenAmount; + { + uint256 maxFeePerGas = UserOperationLib.unpackMaxFeePerGas(userOp); + tokenAmount = ((maxCost + (unaccountedGas) * maxFeePerGas) * externalDynamicAdjustment * tokenPrice) + / (1e18 * PRICE_DENOMINATOR); + } + + // Transfer full amount to this address. Unused amount will be refunded in postOP + SafeTransferLib.safeTransferFrom(tokenAddress, userOp.sender, address(this), tokenAmount); + + context = abi.encode(tokenAddress, tokenAmount, tokenPrice, userOp.sender, userOpHash); + validationData = _packValidationData(false, validUntil, validAfter); } else if (mode == PaymasterMode.INDEPENDENT) { // Use only oracles for the token specified in modeSpecificData if (modeSpecificData.length != 20) { @@ -283,15 +435,15 @@ contract BiconomyTokenPaymaster is { // Calculate token amount to precharge uint256 maxFeePerGas = UserOperationLib.unpackMaxFeePerGas(userOp); - tokenAmount = (maxCost + (unaccountedGas) * maxFeePerGas) * dynamicAdjustment * tokenPrice + tokenAmount = ((maxCost + (unaccountedGas) * maxFeePerGas) * dynamicAdjustment * tokenPrice) / (1e18 * PRICE_DENOMINATOR); } // Transfer full amount to this address. Unused amount will be refunded in postOP SafeTransferLib.safeTransferFrom(tokenAddress, userOp.sender, address(this), tokenAmount); - context = abi.encodePacked(tokenAmount, tokenPrice, userOp.sender, userOpHash); - validationData = 0; + context = abi.encodePacked(tokenAddress, tokenAmount, tokenPrice, userOp.sender, userOpHash); + validationData = 0; // Validation success and price is valid indefinetly } } @@ -323,6 +475,7 @@ contract BiconomyTokenPaymaster is /// @notice Fetches the latest token price. /// @return price The latest token price fetched from the oracles. function getPrice(address tokenAddress) internal view returns (uint192 price) { + // Fetch token information from directory TokenInfo memory tokenInfo = tokenDirectory[tokenAddress]; if (address(tokenInfo.oracle) == address(0)) { @@ -330,9 +483,11 @@ contract BiconomyTokenPaymaster is revert TokenNotSupported(); } + // Calculate price by using token and native oracle uint192 tokenPrice = _fetchPrice(tokenInfo.oracle); uint192 nativeAssetPrice = _fetchPrice(nativeOracle); + // Adjust to token decimals price = nativeAssetPrice * uint192(tokenInfo.decimals) / tokenPrice; } From 86e2c75ee0586d2f247f891cc12b0bbbe3555070 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Mon, 9 Sep 2024 18:09:42 +0400 Subject: [PATCH 60/69] postOp initial implementation --- .../interfaces/IBiconomyTokenPaymaster.sol | 8 +-- contracts/token/BiconomyTokenPaymaster.sol | 50 +++++++++++++++---- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/contracts/interfaces/IBiconomyTokenPaymaster.sol b/contracts/interfaces/IBiconomyTokenPaymaster.sol index 82110e5..7c777d4 100644 --- a/contracts/interfaces/IBiconomyTokenPaymaster.sol +++ b/contracts/interfaces/IBiconomyTokenPaymaster.sol @@ -22,10 +22,10 @@ interface IBiconomyTokenPaymaster { event UpdatedVerifyingSigner(address indexed oldSigner, address indexed newSigner, address indexed actor); event UpdatedFeeCollector(address indexed oldFeeCollector, address indexed newFeeCollector, address indexed actor); event UpdatedPriceExpiryDuration(uint256 indexed oldValue, uint256 indexed newValue); - event GasDeposited(address indexed paymasterId, uint256 indexed value); - event GasWithdrawn(address indexed paymasterId, address indexed to, uint256 indexed value); - event GasBalanceDeducted(address indexed paymasterId, uint256 indexed charge, bytes32 indexed userOpHash); - event DynamicAdjustmentCollected(address indexed paymasterId, uint256 indexed dynamicAdjustment); + event TokensRefunded(address indexed userOpSender, uint256 refundAmount, bytes32 indexed userOpHash); + event PaidGasInTokens( + address indexed userOpSender, uint256 charge, uint256 dynamicAdjustment, bytes32 indexed userOpHash + ); event Received(address indexed sender, uint256 value); event TokensWithdrawn(address indexed token, address indexed to, uint256 indexed amount, address actor); event UpdatedTokenDirectory(address indexed tokenAddress, IOracle indexed oracle, uint8 decimals); diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index 71b9a88..6aa3a68 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -20,14 +20,18 @@ import "@account-abstraction/contracts/core/Helpers.sol"; * @title BiconomyTokenPaymaster * @author ShivaanshK * @author livingrockrises - * @notice Token Paymaster for Entry Point v0.7 + * @notice Biconomy's Token Paymaster for Entry Point v0.7 * @dev A paymaster that allows user to pay gas fees in ERC20 tokens. The paymaster uses the precharge and refund model - * to handle gas remittances. For fair and "always available" operation, it supports a mode that relies purely on price - * oracles (Offchain and TWAP) which - * implement the IOracle interface to calculate the token cost of a transaction. The owner has full - * discretion over the supported tokens, premium and discounts applied (if any), and how to manage the assets - * received by the paymaster. The properties described above, make the paymaster self-relaint: independent of any - * offchain service for use. + * to handle gas remittances. + * + * Currently, the paymaster supports two modes: + * 1. EXTERNAL - Relies on a quoted token price from a trusted entity (verifyingSigner). + * 2. INDEPENDENT - Relies purely on price oracles (Offchain and TWAP) which implement the IOracle interface. This mode + * doesn't require a signature and is always "available" to use. + * + * The paymaster's owner has full discretion over the supported tokens (for independent mode), price adjustments + * applied, and how + * to manage the assets received by the paymaster. */ contract BiconomyTokenPaymaster is BasePaymaster, @@ -419,7 +423,9 @@ contract BiconomyTokenPaymaster is // Transfer full amount to this address. Unused amount will be refunded in postOP SafeTransferLib.safeTransferFrom(tokenAddress, userOp.sender, address(this), tokenAmount); - context = abi.encode(tokenAddress, tokenAmount, tokenPrice, userOp.sender, userOpHash); + context = abi.encode( + userOp.sender, tokenAddress, tokenAmount, tokenPrice, uint256(externalDynamicAdjustment), userOpHash + ); validationData = _packValidationData(false, validUntil, validAfter); } else if (mode == PaymasterMode.INDEPENDENT) { // Use only oracles for the token specified in modeSpecificData @@ -442,7 +448,7 @@ contract BiconomyTokenPaymaster is // Transfer full amount to this address. Unused amount will be refunded in postOP SafeTransferLib.safeTransferFrom(tokenAddress, userOp.sender, address(this), tokenAmount); - context = abi.encodePacked(tokenAddress, tokenAmount, tokenPrice, userOp.sender, userOpHash); + context = abi.encode(userOp.sender, tokenAddress, tokenAmount, tokenPrice, dynamicAdjustment, userOpHash); validationData = 0; // Validation success and price is valid indefinetly } } @@ -463,8 +469,30 @@ contract BiconomyTokenPaymaster is internal override { - (context); - // Implementation of post-operation logic + // Decode context data + ( + address userOpSender, + address tokenAddress, + uint256 prechargedAmount, + uint192 tokenPrice, + uint256 appliedDynamicAdjustment, + bytes32 userOpHash + ) = abi.decode(context, (address, address, uint256, uint192, uint256, bytes32)); + + // Calculate the actual cost in tokens based on the actual gas cost and the token price + uint256 actualTokenAmount = ( + (actualGasCost + (unaccountedGas) * actualUserOpFeePerGas) * appliedDynamicAdjustment * tokenPrice + ) / (1e18 * PRICE_DENOMINATOR); + + // If the user was overcharged, refund the excess tokens + if (prechargedAmount > actualTokenAmount) { + uint256 refundAmount = prechargedAmount - actualTokenAmount; + SafeTransferLib.safeTransfer(tokenAddress, userOpSender, refundAmount); + emit TokensRefunded(userOpSender, refundAmount, userOpHash); + } + + // Emit an event for post-operation completion (optional) + emit PaidGasInTokens(userOpSender, actualGasCost, appliedDynamicAdjustment, userOpHash); } function _withdrawERC20(IERC20 token, address target, uint256 amount) private { From 735f2a62635a4ae9b0eefdcaf9544351e1979968 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Tue, 10 Sep 2024 18:01:01 +0400 Subject: [PATCH 61/69] tests and fixes --- .../interfaces/IBiconomyTokenPaymaster.sol | 4 +- ...Parser.sol => TokenPaymasterParserLib.sol} | 8 +- contracts/token/BiconomyTokenPaymaster.sol | 16 +- test/base/TestBase.sol | 83 +++++ test/mocks/MockOracle.sol | 80 +++++ test/unit/concrete/TestTokenPaymaster.t.sol | 293 ++++++++++++++++++ 6 files changed, 470 insertions(+), 14 deletions(-) rename contracts/libraries/{PaymasterParser.sol => TokenPaymasterParserLib.sol} (88%) create mode 100644 test/mocks/MockOracle.sol create mode 100644 test/unit/concrete/TestTokenPaymaster.t.sol diff --git a/contracts/interfaces/IBiconomyTokenPaymaster.sol b/contracts/interfaces/IBiconomyTokenPaymaster.sol index 7c777d4..ade1f13 100644 --- a/contracts/interfaces/IBiconomyTokenPaymaster.sol +++ b/contracts/interfaces/IBiconomyTokenPaymaster.sol @@ -22,9 +22,9 @@ interface IBiconomyTokenPaymaster { event UpdatedVerifyingSigner(address indexed oldSigner, address indexed newSigner, address indexed actor); event UpdatedFeeCollector(address indexed oldFeeCollector, address indexed newFeeCollector, address indexed actor); event UpdatedPriceExpiryDuration(uint256 indexed oldValue, uint256 indexed newValue); - event TokensRefunded(address indexed userOpSender, uint256 refundAmount, bytes32 indexed userOpHash); + event TokensRefunded(address indexed userOpSender, address indexed token, uint256 refundAmount, bytes32 indexed userOpHash); event PaidGasInTokens( - address indexed userOpSender, uint256 charge, uint256 dynamicAdjustment, bytes32 indexed userOpHash + address indexed userOpSender, address indexed token, uint256 nativeCharge, uint256 tokenCharge, uint256 dynamicAdjustment, bytes32 indexed userOpHash ); event Received(address indexed sender, uint256 value); event TokensWithdrawn(address indexed token, address indexed to, uint256 indexed amount, address actor); diff --git a/contracts/libraries/PaymasterParser.sol b/contracts/libraries/TokenPaymasterParserLib.sol similarity index 88% rename from contracts/libraries/PaymasterParser.sol rename to contracts/libraries/TokenPaymasterParserLib.sol index 4763996..67a7fb8 100644 --- a/contracts/libraries/PaymasterParser.sol +++ b/contracts/libraries/TokenPaymasterParserLib.sol @@ -5,7 +5,7 @@ import { IBiconomyTokenPaymaster } from "../interfaces/IBiconomyTokenPaymaster.s import "@account-abstraction/contracts/core/UserOperationLib.sol"; // A helper library to parse paymaster and data -library PaymasterParser { +library TokenPaymasterParserLib { // Start offset of mode in PND uint256 private constant PAYMASTER_MODE_OFFSET = UserOperationLib.PAYMASTER_DATA_OFFSET; @@ -15,10 +15,8 @@ library PaymasterParser { returns (IBiconomyTokenPaymaster.PaymasterMode mode, bytes memory modeSpecificData) { unchecked { - mode = IBiconomyTokenPaymaster.PaymasterMode( - uint8(bytes1(paymasterAndData[PAYMASTER_MODE_OFFSET:PAYMASTER_MODE_OFFSET + 8])) - ); - modeSpecificData = paymasterAndData[PAYMASTER_MODE_OFFSET + 8:]; + mode = IBiconomyTokenPaymaster.PaymasterMode(uint8(bytes1(paymasterAndData[PAYMASTER_MODE_OFFSET]))); + modeSpecificData = paymasterAndData[PAYMASTER_MODE_OFFSET + 1:]; } } diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index 6aa3a68..fef1213 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -11,7 +11,7 @@ import { BasePaymaster } from "../base/BasePaymaster.sol"; import { BiconomyTokenPaymasterErrors } from "../common/BiconomyTokenPaymasterErrors.sol"; import { IBiconomyTokenPaymaster } from "../interfaces/IBiconomyTokenPaymaster.sol"; import { IOracle } from "../interfaces/oracles/IOracle.sol"; -import { PaymasterParser } from "../libraries/PaymasterParser.sol"; +import { TokenPaymasterParserLib } from "../libraries/TokenPaymasterParserLib.sol"; import { SignatureCheckerLib } from "@solady/src/utils/SignatureCheckerLib.sol"; import { ECDSA as ECDSA_solady } from "@solady/src/utils/ECDSA.sol"; import "@account-abstraction/contracts/core/Helpers.sol"; @@ -40,7 +40,7 @@ contract BiconomyTokenPaymaster is IBiconomyTokenPaymaster { using UserOperationLib for PackedUserOperation; - using PaymasterParser for bytes; + using TokenPaymasterParserLib for bytes; using SignatureCheckerLib for address; // State variables @@ -423,9 +423,8 @@ contract BiconomyTokenPaymaster is // Transfer full amount to this address. Unused amount will be refunded in postOP SafeTransferLib.safeTransferFrom(tokenAddress, userOp.sender, address(this), tokenAmount); - context = abi.encode( - userOp.sender, tokenAddress, tokenAmount, tokenPrice, uint256(externalDynamicAdjustment), userOpHash - ); + context = + abi.encode(userOp.sender, tokenAddress, tokenAmount, tokenPrice, externalDynamicAdjustment, userOpHash); validationData = _packValidationData(false, validUntil, validAfter); } else if (mode == PaymasterMode.INDEPENDENT) { // Use only oracles for the token specified in modeSpecificData @@ -488,16 +487,19 @@ contract BiconomyTokenPaymaster is if (prechargedAmount > actualTokenAmount) { uint256 refundAmount = prechargedAmount - actualTokenAmount; SafeTransferLib.safeTransfer(tokenAddress, userOpSender, refundAmount); - emit TokensRefunded(userOpSender, refundAmount, userOpHash); + emit TokensRefunded(userOpSender, tokenAddress, refundAmount, userOpHash); } // Emit an event for post-operation completion (optional) - emit PaidGasInTokens(userOpSender, actualGasCost, appliedDynamicAdjustment, userOpHash); + emit PaidGasInTokens( + userOpSender, tokenAddress, actualGasCost, actualTokenAmount, appliedDynamicAdjustment, userOpHash + ); } 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); } /// @notice Fetches the latest token price. diff --git a/test/base/TestBase.sol b/test/base/TestBase.sol index 5e57f04..8a369c3 100644 --- a/test/base/TestBase.sol +++ b/test/base/TestBase.sol @@ -17,6 +17,13 @@ import { BaseEventsAndErrors } from "./BaseEventsAndErrors.sol"; import { BiconomySponsorshipPaymaster } from "../../contracts/sponsorship/BiconomySponsorshipPaymaster.sol"; +import { + BiconomyTokenPaymaster, + IBiconomyTokenPaymaster, + BiconomyTokenPaymasterErrors, + IOracle +} from "../../../contracts/token/BiconomyTokenPaymaster.sol"; + abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { address constant ENTRYPOINT_ADDRESS = address(0x0000000071727De22E5E9d8BAf0edAc6f37da032); @@ -207,6 +214,70 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { ); } + /// @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 finalPmData Full Pm Data. + /// @return signature Pm Signature on Data. + function generateAndSignTokenPaymasterData( + PackedUserOperation memory userOp, + Vm.Wallet memory signer, + BiconomyTokenPaymaster paymaster, + uint128 paymasterValGasLimit, + uint128 paymasterPostOpGasLimit, + IBiconomyTokenPaymaster.PaymasterMode mode, + uint48 validUntil, + uint48 validAfter, + address tokenAddress, + uint128 tokenPrice, + uint32 externalDynamicAdjustment + ) + internal + view + returns (bytes memory finalPmData, bytes memory signature) + { + // Initial paymaster data with zero signature + bytes memory initialPmData = abi.encodePacked( + address(paymaster), + paymasterValGasLimit, + paymasterPostOpGasLimit, + uint8(mode), + validUntil, + validAfter, + tokenAddress, + tokenPrice, + externalDynamicAdjustment, + 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, tokenAddress, tokenPrice, externalDynamicAdjustment); + + // Sign the hash + signature = signMessage(signer, paymasterHash); + require(signature.length == 65, "Invalid Paymaster Signature length"); + + // Final paymaster data with the actual signature + finalPmData = abi.encodePacked( + address(paymaster), + paymasterValGasLimit, + paymasterPostOpGasLimit, + uint8(mode), + validUntil, + validAfter, + tokenAddress, + tokenPrice, + externalDynamicAdjustment, + signature + ); + } + 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); @@ -268,4 +339,16 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { // paymaster) assertApproxEqRel(totalGasFeePaid + actualDynamicAdjustment, gasPaidByDapp, 0.01e18); } + + function _toSingletonArray(address addr) internal pure returns (address[] memory) { + address[] memory array = new address[](1); + array[0] = addr; + return array; + } + + function _toSingletonArray(IOracle oracle) internal pure returns (IOracle[] memory) { + IOracle[] memory array = new IOracle[](1); + array[0] = oracle; + return array; + } } diff --git a/test/mocks/MockOracle.sol b/test/mocks/MockOracle.sol new file mode 100644 index 0000000..ce572e6 --- /dev/null +++ b/test/mocks/MockOracle.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "contracts/interfaces/oracles/IOracle.sol"; + +contract MockOracle is IOracle { + int256 public price; + uint8 public priceDecimals; + uint256 public updatedAtDelay; + + constructor(int256 _initialPrice, uint8 _decimals) { + price = _initialPrice; + priceDecimals = _decimals; + updatedAtDelay = 0; + } + + /** + * @dev Allows setting a new price manually for testing purposes. + * @param _price The new price to be set. + */ + function setPrice(int256 _price) external { + price = _price; + } + + /** + * @dev Allows setting the delay for the `updatedAt` timestamp. + * @param _updatedAtDelay The delay in seconds to simulate a stale price. + */ + function setUpdatedAtDelay(uint256 _updatedAtDelay) external { + updatedAtDelay = _updatedAtDelay; + } + + /** + * @dev Returns the number of decimals for the oracle price feed. + */ + function decimals() external view override returns (uint8) { + return priceDecimals; + } + + /** + * @dev Mocks a random price within a given range. + * @param minPrice The minimum price range (inclusive). + * @param maxPrice The maximum price range (inclusive). + */ + function setRandomPrice(int256 minPrice, int256 maxPrice) external { + require(minPrice <= maxPrice, "Min price must be less than or equal to max price"); + + // Generate a random price within the range [minPrice, maxPrice] + price = minPrice + int256(uint256(keccak256(abi.encodePacked(block.timestamp, block.difficulty))) % uint256(maxPrice - minPrice + 1)); + } + + /** + * @dev Returns mocked data for the latest round of the price feed. + * @return _roundId The round ID. + * @return answer The current price. + * @return startedAt The timestamp when the round started. + * @return _updatedAt The timestamp when the round was last updated. + * @return answeredInRound The round ID in which the answer was computed. + */ + function latestRoundData() + external + view + override + returns ( + uint80 _roundId, + int256 answer, + uint256 startedAt, + uint256 _updatedAt, + uint80 answeredInRound + ) + { + return ( + 73786976294838215802, // Mock round ID + price, // The current price + block.timestamp, // Simulate round started at the current block timestamp + block.timestamp - updatedAtDelay, // Simulate price last updated with delay + 73786976294838215802 // Mock round ID for answeredInRound + ); + } +} diff --git a/test/unit/concrete/TestTokenPaymaster.t.sol b/test/unit/concrete/TestTokenPaymaster.t.sol new file mode 100644 index 0000000..512ef55 --- /dev/null +++ b/test/unit/concrete/TestTokenPaymaster.t.sol @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity ^0.8.26; + +import "../../base/TestBase.sol"; +import { + BiconomyTokenPaymaster, + IBiconomyTokenPaymaster, + BiconomyTokenPaymasterErrors, + IOracle +} from "../../../contracts/token/BiconomyTokenPaymaster.sol"; +import { MockOracle } from "../../mocks/MockOracle.sol"; +import { MockToken } from "@nexus/contracts/mocks/MockToken.sol"; + +contract TestTokenPaymaster is TestBase { + BiconomyTokenPaymaster public tokenPaymaster; + MockOracle public nativeOracle; + MockToken public testToken; + MockOracle public tokenOracle; + + function setUp() public { + setupPaymasterTestEnvironment(); + + // Deploy mock oracles and tokens + nativeOracle = new MockOracle(100_000_000, 8); // Oracle with 8 decimals for ETH + tokenOracle = new MockOracle(100_000_000, 8); // Oracle with 8 decimals for ERC20 token + testToken = new MockToken("Test Token", "TKN"); + + // Deploy the token paymaster + tokenPaymaster = new BiconomyTokenPaymaster( + PAYMASTER_OWNER.addr, + PAYMASTER_SIGNER.addr, + ENTRYPOINT, + 5000, // unaccounted gas + 1e6, // dynamic adjustment + nativeOracle, + 1 days, // price expiry duration + _toSingletonArray(address(testToken)), + _toSingletonArray(IOracle(address(tokenOracle))) + ); + } + + function test_Deploy() external { + BiconomyTokenPaymaster testArtifact = new BiconomyTokenPaymaster( + PAYMASTER_OWNER.addr, + PAYMASTER_SIGNER.addr, + ENTRYPOINT, + 5000, + 1e6, + nativeOracle, + 1 days, + _toSingletonArray(address(testToken)), + _toSingletonArray(IOracle(address(tokenOracle))) + ); + + assertEq(testArtifact.owner(), PAYMASTER_OWNER.addr); + assertEq(address(testArtifact.entryPoint()), ENTRYPOINT_ADDRESS); + assertEq(testArtifact.verifyingSigner(), PAYMASTER_SIGNER.addr); + assertEq(address(testArtifact.nativeOracle()), address(nativeOracle)); + assertEq(testArtifact.unaccountedGas(), 5000); + assertEq(testArtifact.dynamicAdjustment(), 1e6); + } + + function test_RevertIf_DeployWithSignerSetToZero() external { + vm.expectRevert(BiconomyTokenPaymasterErrors.VerifyingSignerCanNotBeZero.selector); + new BiconomyTokenPaymaster( + PAYMASTER_OWNER.addr, + address(0), + ENTRYPOINT, + 5000, + 1e6, + nativeOracle, + 1 days, + _toSingletonArray(address(testToken)), + _toSingletonArray(IOracle(address(tokenOracle))) + ); + } + + function test_RevertIf_DeployWithSignerAsContract() external { + vm.expectRevert(BiconomyTokenPaymasterErrors.VerifyingSignerCanNotBeContract.selector); + new BiconomyTokenPaymaster( + PAYMASTER_OWNER.addr, + address(ENTRYPOINT), + ENTRYPOINT, + 5000, + 1e6, + nativeOracle, + 1 days, + _toSingletonArray(address(testToken)), + _toSingletonArray(IOracle(address(tokenOracle))) + ); + } + + function test_RevertIf_UnaccountedGasTooHigh() external { + vm.expectRevert(BiconomyTokenPaymasterErrors.UnaccountedGasTooHigh.selector); + new BiconomyTokenPaymaster( + PAYMASTER_OWNER.addr, + PAYMASTER_SIGNER.addr, + ENTRYPOINT, + 50_001, // too high unaccounted gas + 1e6, + nativeOracle, + 1 days, + _toSingletonArray(address(testToken)), + _toSingletonArray(IOracle(address(tokenOracle))) + ); + } + + function test_RevertIf_InvalidDynamicAdjustment() external { + vm.expectRevert(BiconomyTokenPaymasterErrors.InvalidDynamicAdjustment.selector); + new BiconomyTokenPaymaster( + PAYMASTER_OWNER.addr, + PAYMASTER_SIGNER.addr, + ENTRYPOINT, + 5000, + 2e6 + 1, // too high dynamic adjustment + nativeOracle, + 1 days, + _toSingletonArray(address(testToken)), + _toSingletonArray(IOracle(address(tokenOracle))) + ); + } + + function test_SetVerifyingSigner() external prankModifier(PAYMASTER_OWNER.addr) { + vm.expectEmit(true, true, true, true, address(tokenPaymaster)); + emit IBiconomyTokenPaymaster.UpdatedVerifyingSigner(PAYMASTER_SIGNER.addr, BOB_ADDRESS, PAYMASTER_OWNER.addr); + tokenPaymaster.setSigner(BOB_ADDRESS); + assertEq(tokenPaymaster.verifyingSigner(), BOB_ADDRESS); + } + + function test_RevertIf_SetVerifyingSignerToZero() external prankModifier(PAYMASTER_OWNER.addr) { + vm.expectRevert(BiconomyTokenPaymasterErrors.VerifyingSignerCanNotBeZero.selector); + tokenPaymaster.setSigner(address(0)); + } + + function test_SetFeeCollector() external prankModifier(PAYMASTER_OWNER.addr) { + // Set the expected fee collector change and expect the event to be emitted + vm.expectEmit(true, true, true, true, address(tokenPaymaster)); + emit IBiconomyTokenPaymaster.UpdatedFeeCollector( + address(tokenPaymaster), BOB_ADDRESS, PAYMASTER_OWNER.addr + ); + + // Call the function to set the fee collector + tokenPaymaster.setFeeCollector(BOB_ADDRESS); + + // Assert the change has been applied correctly + assertEq(tokenPaymaster.feeCollector(), BOB_ADDRESS); + } + + function test_Deposit() external prankModifier(PAYMASTER_OWNER.addr) { + uint256 depositAmount = 10 ether; + assertEq(tokenPaymaster.getDeposit(), 0); + + tokenPaymaster.deposit{ value: depositAmount }(); + assertEq(tokenPaymaster.getDeposit(), depositAmount); + } + + function test_WithdrawTo() external prankModifier(PAYMASTER_OWNER.addr) { + uint256 depositAmount = 10 ether; + tokenPaymaster.deposit{ value: depositAmount }(); + uint256 initialBalance = BOB_ADDRESS.balance; + + // Withdraw ETH to BOB_ADDRESS and verify the balance changes + tokenPaymaster.withdrawTo(payable(BOB_ADDRESS), depositAmount); + + assertEq(BOB_ADDRESS.balance, initialBalance + depositAmount); + assertEq(tokenPaymaster.getDeposit(), 0); + } + + function test_WithdrawERC20() external prankModifier(PAYMASTER_OWNER.addr) { + uint256 mintAmount = 10 * (10 ** testToken.decimals()); + testToken.mint(address(tokenPaymaster), mintAmount); + + // Ensure that the paymaster has the tokens + assertEq(testToken.balanceOf(address(tokenPaymaster)), mintAmount); + assertEq(testToken.balanceOf(ALICE_ADDRESS), 0); + + // Expect the `TokensWithdrawn` event to be emitted with the correct values + vm.expectEmit(true, true, true, true, address(tokenPaymaster)); + emit IBiconomyTokenPaymaster.TokensWithdrawn( + address(testToken), ALICE_ADDRESS, mintAmount, PAYMASTER_OWNER.addr + ); + + // Withdraw tokens and validate balances + tokenPaymaster.withdrawERC20(testToken, ALICE_ADDRESS, mintAmount); + + assertEq(testToken.balanceOf(address(tokenPaymaster)), 0); + assertEq(testToken.balanceOf(ALICE_ADDRESS), mintAmount); + } + + function test_RevertIf_InvalidOracleDecimals() external { + MockOracle invalidOracle = new MockOracle(100_000_000, 18); // invalid oracle with 18 decimals + vm.expectRevert(BiconomyTokenPaymasterErrors.InvalidOracleDecimals.selector); + new BiconomyTokenPaymaster( + PAYMASTER_OWNER.addr, + PAYMASTER_SIGNER.addr, + ENTRYPOINT, + 5000, + 1e6, + invalidOracle, // incorrect oracle decimals + 1 days, + _toSingletonArray(address(testToken)), + _toSingletonArray(IOracle(address(tokenOracle))) + ); + } + + function test_SetNativeOracle() external prankModifier(PAYMASTER_OWNER.addr) { + MockOracle newOracle = new MockOracle(100_000_000, 8); + + vm.expectEmit(true, true, false, true, address(tokenPaymaster)); + emit IBiconomyTokenPaymaster.UpdatedNativeAssetOracle(nativeOracle, newOracle); + tokenPaymaster.setNativeOracle(newOracle); + + assertEq(address(tokenPaymaster.nativeOracle()), address(newOracle)); + } + + function test_ValidatePaymasterUserOp_ExternalMode() external { + tokenPaymaster.deposit{ value: 10 ether }(); + testToken.mint(address(ALICE_ACCOUNT), 100_000 * (10 ** testToken.decimals())); + vm.startPrank(address(ALICE_ACCOUNT)); + testToken.approve(address(tokenPaymaster), testToken.balanceOf(address(ALICE_ACCOUNT))); + vm.stopPrank(); + + // Build the user operation for external mode + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + uint48 validUntil = uint48(block.timestamp + 1 days); + uint48 validAfter = uint48(block.timestamp); + uint128 tokenPrice = 1e8; // Assume 1 token = 1 USD + uint32 externalDynamicAdjustment = 1e6; + + // Generate and sign the token paymaster data + (bytes memory paymasterAndData,) = generateAndSignTokenPaymasterData( + userOp, + PAYMASTER_SIGNER, + tokenPaymaster, + 3e6, // assumed gas limit for test + 3e6, // assumed verification gas for test + IBiconomyTokenPaymaster.PaymasterMode.EXTERNAL, + validUntil, + validAfter, + address(testToken), + tokenPrice, + externalDynamicAdjustment + ); + + userOp.paymasterAndData = paymasterAndData; + userOp.signature = signUserOp(ALICE, userOp); + + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + ops[0] = userOp; + + vm.expectEmit(true, true, false, false, address(tokenPaymaster)); + emit IBiconomyTokenPaymaster.TokensRefunded(address(ALICE_ACCOUNT), address(testToken), 0, bytes32(0)); + + vm.expectEmit(true, true, false, false, address(tokenPaymaster)); + emit IBiconomyTokenPaymaster.PaidGasInTokens(address(ALICE_ACCOUNT), address(testToken), 0, 0, 1e6, bytes32(0)); + + // Execute the operation + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + } + + function test_ValidatePaymasterUserOp_IndependentMode() external { + tokenPaymaster.deposit{ value: 10 ether }(); + testToken.mint(address(ALICE_ACCOUNT), 100_000 * (10 ** testToken.decimals())); + vm.startPrank(address(ALICE_ACCOUNT)); + testToken.approve(address(tokenPaymaster), testToken.balanceOf(address(ALICE_ACCOUNT))); + vm.stopPrank(); + + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + + // Encode paymasterAndData for independent mode + bytes memory paymasterAndData = abi.encodePacked( + address(tokenPaymaster), + uint128(3e6), // assumed gas limit for test + uint128(3e6), // assumed verification gas for test + uint8(IBiconomyTokenPaymaster.PaymasterMode.INDEPENDENT), + address(testToken) + ); + + userOp.paymasterAndData = paymasterAndData; + userOp.signature = signUserOp(ALICE, userOp); + + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + ops[0] = userOp; + + vm.expectEmit(true, true, false, false, address(tokenPaymaster)); + emit IBiconomyTokenPaymaster.TokensRefunded(address(ALICE_ACCOUNT), address(testToken), 0, bytes32(0)); + + vm.expectEmit(true, true, false, false, address(tokenPaymaster)); + emit IBiconomyTokenPaymaster.PaidGasInTokens(address(ALICE_ACCOUNT), address(testToken), 0, 0, 1e6, bytes32(0)); + + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + } +} From 19fe90adf4812f95e04dca9e2fba68e385977427 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Tue, 10 Sep 2024 18:21:47 +0400 Subject: [PATCH 62/69] some more tests --- test/base/TestBase.sol | 1 + test/unit/concrete/TestTokenPaymaster.t.sol | 124 +++++++++++++++++++- 2 files changed, 122 insertions(+), 3 deletions(-) diff --git a/test/base/TestBase.sol b/test/base/TestBase.sol index 8a369c3..29d6320 100644 --- a/test/base/TestBase.sol +++ b/test/base/TestBase.sol @@ -351,4 +351,5 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { array[0] = oracle; return array; } + } diff --git a/test/unit/concrete/TestTokenPaymaster.t.sol b/test/unit/concrete/TestTokenPaymaster.t.sol index 512ef55..2ac3273 100644 --- a/test/unit/concrete/TestTokenPaymaster.t.sol +++ b/test/unit/concrete/TestTokenPaymaster.t.sol @@ -10,11 +10,14 @@ import { } from "../../../contracts/token/BiconomyTokenPaymaster.sol"; import { MockOracle } from "../../mocks/MockOracle.sol"; import { MockToken } from "@nexus/contracts/mocks/MockToken.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + contract TestTokenPaymaster is TestBase { BiconomyTokenPaymaster public tokenPaymaster; MockOracle public nativeOracle; MockToken public testToken; + MockToken public testToken2; MockOracle public tokenOracle; function setUp() public { @@ -24,6 +27,8 @@ contract TestTokenPaymaster is TestBase { nativeOracle = new MockOracle(100_000_000, 8); // Oracle with 8 decimals for ETH tokenOracle = new MockOracle(100_000_000, 8); // Oracle with 8 decimals for ERC20 token testToken = new MockToken("Test Token", "TKN"); + testToken2 = new MockToken("Test Token 2", "TKN2"); + // Deploy the token paymaster tokenPaymaster = new BiconomyTokenPaymaster( @@ -135,9 +140,7 @@ contract TestTokenPaymaster is TestBase { function test_SetFeeCollector() external prankModifier(PAYMASTER_OWNER.addr) { // Set the expected fee collector change and expect the event to be emitted vm.expectEmit(true, true, true, true, address(tokenPaymaster)); - emit IBiconomyTokenPaymaster.UpdatedFeeCollector( - address(tokenPaymaster), BOB_ADDRESS, PAYMASTER_OWNER.addr - ); + emit IBiconomyTokenPaymaster.UpdatedFeeCollector(address(tokenPaymaster), BOB_ADDRESS, PAYMASTER_OWNER.addr); // Call the function to set the fee collector tokenPaymaster.setFeeCollector(BOB_ADDRESS); @@ -290,4 +293,119 @@ contract TestTokenPaymaster is TestBase { ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); } + + // Test multiple ERC20 token withdrawals + function test_WithdrawMultipleERC20Tokens() external prankModifier(PAYMASTER_OWNER.addr) { + // Mint tokens to paymaster + testToken.mint(address(tokenPaymaster), 1000 * (10 ** testToken.decimals())); + testToken2.mint(address(tokenPaymaster), 2000 * (10 ** testToken2.decimals())); + + assertEq(testToken.balanceOf(address(tokenPaymaster)), 1000 * (10 ** testToken.decimals())); + assertEq(testToken2.balanceOf(address(tokenPaymaster)), 2000 * (10 ** testToken2.decimals())); + + // Withdraw both tokens + IERC20[] memory tokens = new IERC20[](2); + tokens[0] = IERC20(testToken); + tokens[1] = IERC20(testToken2); + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 500 * (10 ** testToken.decimals()); + amounts[1] = 1000 * (10 ** testToken2.decimals()); + + vm.expectEmit(true, true, true, true, address(tokenPaymaster)); + emit IBiconomyTokenPaymaster.TokensWithdrawn( + address(testToken), ALICE_ADDRESS, amounts[0], PAYMASTER_OWNER.addr + ); + + vm.expectEmit(true, true, true, true, address(tokenPaymaster)); + emit IBiconomyTokenPaymaster.TokensWithdrawn( + address(testToken2), ALICE_ADDRESS, amounts[1], PAYMASTER_OWNER.addr + ); + + tokenPaymaster.withdrawMultipleERC20(tokens, ALICE_ADDRESS, amounts); + + assertEq(testToken.balanceOf(address(ALICE_ADDRESS)), amounts[0]); + assertEq(testToken2.balanceOf(address(ALICE_ADDRESS)), amounts[1]); + } + + // Test scenario where the token price has expired + function test_RevertIf_PriceExpired() external { + // Set price expiry duration to a short time for testing + vm.warp(block.timestamp + 2 days); // Move forward in time to simulate price expiry + + testToken.mint(address(ALICE_ACCOUNT), 100_000 * (10 ** testToken.decimals())); + vm.startPrank(address(ALICE_ACCOUNT)); + testToken.approve(address(tokenPaymaster), testToken.balanceOf(address(ALICE_ACCOUNT))); + vm.stopPrank(); + + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + uint128 tokenPrice = 1e8; // Assume 1 token = 1 USD + + (bytes memory paymasterAndData,) = generateAndSignTokenPaymasterData( + userOp, + PAYMASTER_SIGNER, + tokenPaymaster, + 3e6, + 3e6, + IBiconomyTokenPaymaster.PaymasterMode.INDEPENDENT, + uint48(block.timestamp + 1 days), + uint48(block.timestamp), + address(testToken), + tokenPrice, + 1e6 + ); + + userOp.paymasterAndData = paymasterAndData; + userOp.signature = signUserOp(ALICE, userOp); + + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + ops[0] = userOp; + + vm.expectRevert(); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + } + + // Test setting a high dynamic adjustment + function test_SetDynamicAdjustmentTooHigh() external prankModifier(PAYMASTER_OWNER.addr) { + vm.expectRevert(BiconomyTokenPaymasterErrors.InvalidDynamicAdjustment.selector); + tokenPaymaster.setDynamicAdjustment(2e6 + 1); // Setting too high + } + + // Test invalid signature in external mode + function test_RevertIf_InvalidSignature_ExternalMode() external { + tokenPaymaster.deposit{ value: 10 ether }(); + testToken.mint(address(ALICE_ACCOUNT), 100_000 * (10 ** testToken.decimals())); + vm.startPrank(address(ALICE_ACCOUNT)); + testToken.approve(address(tokenPaymaster), testToken.balanceOf(address(ALICE_ACCOUNT))); + vm.stopPrank(); + + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + uint128 tokenPrice = 1e8; + + // Create a valid paymasterAndData + (bytes memory paymasterAndData,) = generateAndSignTokenPaymasterData( + userOp, + PAYMASTER_SIGNER, + tokenPaymaster, + 3e6, + 3e6, + IBiconomyTokenPaymaster.PaymasterMode.EXTERNAL, + uint48(block.timestamp + 1 days), + uint48(block.timestamp), + address(testToken), + tokenPrice, + 1e6 + ); + + // Tamper the signature by altering the last byte + paymasterAndData[paymasterAndData.length - 1] = bytes1(uint8(paymasterAndData[paymasterAndData.length - 1]) + 1); + userOp.paymasterAndData = paymasterAndData; + + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + ops[0] = userOp; + + vm.expectRevert(); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + } + } From 3a97ec180d63cfb1a81c5af2c27379cbf619390c Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Tue, 10 Sep 2024 18:51:11 +0400 Subject: [PATCH 63/69] test for token paymaster parser library --- .../TestTokenPaymasterParserLib.t.sol | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 test/unit/concrete/TestTokenPaymasterParserLib.t.sol diff --git a/test/unit/concrete/TestTokenPaymasterParserLib.t.sol b/test/unit/concrete/TestTokenPaymasterParserLib.t.sol new file mode 100644 index 0000000..694c672 --- /dev/null +++ b/test/unit/concrete/TestTokenPaymasterParserLib.t.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity ^0.8.26; + +import "lib/forge-std/src/Test.sol"; +import "../../../contracts/libraries/TokenPaymasterParserLib.sol"; +import { IBiconomyTokenPaymaster } from "../../../contracts/interfaces/IBiconomyTokenPaymaster.sol"; + +// Mock contract to test the TokenPaymasterParserLib +contract TestTokenPaymasterParserLib is Test { + using TokenPaymasterParserLib for bytes; + + function test_ParsePaymasterAndData_ExternalMode() public { + // Simulate an example paymasterAndData for External Mode + IBiconomyTokenPaymaster.PaymasterMode expectedMode = IBiconomyTokenPaymaster.PaymasterMode.EXTERNAL; + + // Encode the mode (0 for EXTERNAL) + bytes memory modeSpecificData = hex"000102030405060708091011121314151617181920212223242526"; + + // The PAYMASTER_MODE_OFFSET must be accounted for by placing the mode at the correct offset + bytes memory paymasterAndData = abi.encodePacked( + address(this), + uint128(1e6), // Example gas value + uint128(1e6), + uint8(expectedMode), // Mode (0 for EXTERNAL) + modeSpecificData // Mode specific data + ); + + // Parse the paymasterAndData + (IBiconomyTokenPaymaster.PaymasterMode parsedMode, bytes memory parsedModeSpecificData) = + paymasterAndData.parsePaymasterAndData(); + + // Validate the mode and modeSpecificData + assertEq(uint8(parsedMode), uint8(expectedMode), "Mode should match External"); + assertEq(parsedModeSpecificData, modeSpecificData, "Mode specific data should match"); + } + + function test_ParsePaymasterAndData_IndependentMode() public { + // Simulate an example paymasterAndData for Independent Mode + IBiconomyTokenPaymaster.PaymasterMode expectedMode = IBiconomyTokenPaymaster.PaymasterMode.INDEPENDENT; + + // Encode the mode (1 for INDEPENDENT) + bytes memory modeSpecificData = hex"11223344556677889900aabbccddeeff"; + + // The PAYMASTER_MODE_OFFSET must be accounted for by placing the mode at the correct offset + bytes memory paymasterAndData = abi.encodePacked( + address(this), + uint128(1e6), // Example gas value + uint128(1e6), + uint8(expectedMode), // Mode (1 for INDEPENDENT) + modeSpecificData // Mode specific data + ); + + // Parse the paymasterAndData + (IBiconomyTokenPaymaster.PaymasterMode parsedMode, bytes memory parsedModeSpecificData) = + paymasterAndData.parsePaymasterAndData(); + + // Validate the mode and modeSpecificData + assertEq(uint8(parsedMode), uint8(expectedMode), "Mode should match Independent"); + assertEq(parsedModeSpecificData, modeSpecificData, "Mode specific data should match"); + } + + function test_ParseExternalModeSpecificData() public view { + // Simulate valid external mode specific data + uint48 expectedValidUntil = uint48(block.timestamp + 1 days); + uint48 expectedValidAfter = uint48(block.timestamp); + address expectedTokenAddress = address(0x1234567890AbcdEF1234567890aBcdef12345678); + uint128 expectedTokenPrice = 1e8; + uint32 expectedExternalDynamicAdjustment = 1e6; + bytes memory expectedSignature = hex"abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef"; + + // Construct external mode specific data + bytes memory externalModeSpecificData = abi.encodePacked( + bytes6(abi.encodePacked(expectedValidUntil)), + bytes6(abi.encodePacked(expectedValidAfter)), + bytes20(expectedTokenAddress), + bytes16(abi.encodePacked(expectedTokenPrice)), + bytes4(abi.encodePacked(expectedExternalDynamicAdjustment)), + expectedSignature + ); + + // Parse the mode specific data + ( + uint48 parsedValidUntil, + uint48 parsedValidAfter, + address parsedTokenAddress, + uint128 parsedTokenPrice, + uint32 parsedExternalDynamicAdjustment, + bytes memory parsedSignature + ) = externalModeSpecificData.parseExternalModeSpecificData(); + + // Validate the parsed values + assertEq(parsedValidUntil, expectedValidUntil, "ValidUntil should match"); + assertEq(parsedValidAfter, expectedValidAfter, "ValidAfter should match"); + assertEq(parsedTokenAddress, expectedTokenAddress, "Token address should match"); + assertEq(parsedTokenPrice, expectedTokenPrice, "Token price should match"); + assertEq(parsedExternalDynamicAdjustment, expectedExternalDynamicAdjustment, "Dynamic adjustment should match"); + assertEq(parsedSignature, expectedSignature, "Signature should match"); + } + + function test_ParseIndependentModeSpecificData() public pure { + // Simulate valid independent mode specific data + address expectedTokenAddress = address(0x9876543210AbCDef9876543210ABCdEf98765432); + bytes memory independentModeSpecificData = abi.encodePacked(bytes20(expectedTokenAddress)); + + // Parse the mode specific data + address parsedTokenAddress = independentModeSpecificData.parseIndependentModeSpecificData(); + + // Validate the parsed token address + assertEq(parsedTokenAddress, expectedTokenAddress, "Token address should match"); + } + + function test_RevertIf_InvalidExternalModeSpecificDataLength() public { + // Simulate invalid external mode specific data (incorrect length) + bytes memory invalidExternalModeSpecificData = hex"0001020304050607"; + + // Expect the test to revert due to invalid data length + vm.expectRevert(); + invalidExternalModeSpecificData.parseExternalModeSpecificData(); + } + + function test_RevertIf_InvalidIndependentModeSpecificDataLength() public { + // Simulate invalid independent mode specific data (incorrect length) + bytes memory invalidIndependentModeSpecificData = hex"00010203"; + + // Expect the test to revert due to invalid data length + vm.expectRevert(); + invalidIndependentModeSpecificData.parseIndependentModeSpecificData(); + } +} From 3611b12392da645b3ca6b40a63d517d409bd687a Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Wed, 11 Sep 2024 16:33:17 +0400 Subject: [PATCH 64/69] get rid of feeCollector (will always be contract) --- .../interfaces/IBiconomyTokenPaymaster.sol | 13 ++++++++---- contracts/token/BiconomyTokenPaymaster.sol | 21 +------------------ test/unit/concrete/TestTokenPaymaster.t.sol | 12 ----------- 3 files changed, 10 insertions(+), 36 deletions(-) diff --git a/contracts/interfaces/IBiconomyTokenPaymaster.sol b/contracts/interfaces/IBiconomyTokenPaymaster.sol index ade1f13..51e6023 100644 --- a/contracts/interfaces/IBiconomyTokenPaymaster.sol +++ b/contracts/interfaces/IBiconomyTokenPaymaster.sol @@ -22,9 +22,16 @@ interface IBiconomyTokenPaymaster { event UpdatedVerifyingSigner(address indexed oldSigner, address indexed newSigner, address indexed actor); event UpdatedFeeCollector(address indexed oldFeeCollector, address indexed newFeeCollector, address indexed actor); event UpdatedPriceExpiryDuration(uint256 indexed oldValue, uint256 indexed newValue); - event TokensRefunded(address indexed userOpSender, address indexed token, uint256 refundAmount, bytes32 indexed userOpHash); + event TokensRefunded( + address indexed userOpSender, address indexed token, uint256 refundAmount, bytes32 indexed userOpHash + ); event PaidGasInTokens( - address indexed userOpSender, address indexed token, uint256 nativeCharge, uint256 tokenCharge, uint256 dynamicAdjustment, bytes32 indexed userOpHash + address indexed userOpSender, + address indexed token, + uint256 nativeCharge, + uint256 tokenCharge, + uint256 dynamicAdjustment, + bytes32 indexed userOpHash ); event Received(address indexed sender, uint256 value); event TokensWithdrawn(address indexed token, address indexed to, uint256 indexed amount, address actor); @@ -33,8 +40,6 @@ interface IBiconomyTokenPaymaster { function setSigner(address _newVerifyingSigner) external payable; - function setFeeCollector(address _newFeeCollector) external payable; - function setUnaccountedGas(uint256 value) external payable; function setDynamicAdjustment(uint256 _newUnaccountedGas) external payable; diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index fef1213..b8faed7 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -44,7 +44,6 @@ contract BiconomyTokenPaymaster is using SignatureCheckerLib for address; // State variables - address public feeCollector; address public verifyingSigner; uint256 public unaccountedGas; uint256 public dynamicAdjustment; @@ -93,7 +92,6 @@ contract BiconomyTokenPaymaster is // Set state variables assembly ("memory-safe") { sstore(verifyingSigner.slot, _verifyingSigner) - sstore(feeCollector.slot, address()) // initialize fee collector to this contract sstore(unaccountedGas.slot, _unaccountedGas) sstore(dynamicAdjustment.slot, _dynamicAdjustment) sstore(priceExpiryDuration.slot, _priceExpiryDuration) @@ -218,22 +216,6 @@ contract BiconomyTokenPaymaster is emit UpdatedVerifyingSigner(oldSigner, _newVerifyingSigner, msg.sender); } - /** - * @dev Set a new fee collector address. - * Can only be called by the owner of the contract. - * @param _newFeeCollector The new address to be set as the fee collector. - * @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 override onlyOwner { - if (_newFeeCollector == address(0)) revert FeeCollectorCanNotBeZero(); - address oldFeeCollector = feeCollector; - assembly ("memory-safe") { - sstore(feeCollector.slot, _newFeeCollector) - } - emit UpdatedFeeCollector(oldFeeCollector, _newFeeCollector, msg.sender); - } - /** * @dev Set a new unaccountedEPGasOverhead value. * @param _newUnaccountedGas The new value to be set as the unaccounted gas value @@ -483,14 +465,13 @@ contract BiconomyTokenPaymaster is (actualGasCost + (unaccountedGas) * actualUserOpFeePerGas) * appliedDynamicAdjustment * tokenPrice ) / (1e18 * PRICE_DENOMINATOR); - // If the user was overcharged, refund the excess tokens if (prechargedAmount > actualTokenAmount) { + // If the user was overcharged, refund the excess tokens uint256 refundAmount = prechargedAmount - actualTokenAmount; SafeTransferLib.safeTransfer(tokenAddress, userOpSender, refundAmount); emit TokensRefunded(userOpSender, tokenAddress, refundAmount, userOpHash); } - // Emit an event for post-operation completion (optional) emit PaidGasInTokens( userOpSender, tokenAddress, actualGasCost, actualTokenAmount, appliedDynamicAdjustment, userOpHash ); diff --git a/test/unit/concrete/TestTokenPaymaster.t.sol b/test/unit/concrete/TestTokenPaymaster.t.sol index 2ac3273..b0bd9d8 100644 --- a/test/unit/concrete/TestTokenPaymaster.t.sol +++ b/test/unit/concrete/TestTokenPaymaster.t.sol @@ -137,18 +137,6 @@ contract TestTokenPaymaster is TestBase { tokenPaymaster.setSigner(address(0)); } - function test_SetFeeCollector() external prankModifier(PAYMASTER_OWNER.addr) { - // Set the expected fee collector change and expect the event to be emitted - vm.expectEmit(true, true, true, true, address(tokenPaymaster)); - emit IBiconomyTokenPaymaster.UpdatedFeeCollector(address(tokenPaymaster), BOB_ADDRESS, PAYMASTER_OWNER.addr); - - // Call the function to set the fee collector - tokenPaymaster.setFeeCollector(BOB_ADDRESS); - - // Assert the change has been applied correctly - assertEq(tokenPaymaster.feeCollector(), BOB_ADDRESS); - } - function test_Deposit() external prankModifier(PAYMASTER_OWNER.addr) { uint256 depositAmount = 10 ether; assertEq(tokenPaymaster.getDeposit(), 0); From 70d2ad28dd8be7280281ee8ecff5582b8c0218c3 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Wed, 11 Sep 2024 16:43:46 +0400 Subject: [PATCH 65/69] incorporate some more changes --- .../BiconomySponsorshipPaymasterErrors.sol | 2 +- .../common/BiconomyTokenPaymasterErrors.sol | 2 +- .../IBiconomySponsorshipPaymaster.sol | 8 +- .../interfaces/IBiconomyTokenPaymaster.sol | 6 +- .../libraries/TokenPaymasterParserLib.sol | 4 +- .../BiconomySponsorshipPaymaster.sol | 34 ++++----- contracts/token/BiconomyTokenPaymaster.sol | 74 +++++++++---------- test/base/TestBase.sol | 48 ++++++------ .../concrete/TestSponsorshipPaymaster.t.sol | 34 ++++----- test/unit/concrete/TestTokenPaymaster.t.sol | 44 +++++------ .../TestTokenPaymasterParserLib.t.sol | 8 +- .../TestFuzz_TestSponsorshipPaymaster.t.sol | 22 +++--- 12 files changed, 143 insertions(+), 143 deletions(-) diff --git a/contracts/common/BiconomySponsorshipPaymasterErrors.sol b/contracts/common/BiconomySponsorshipPaymasterErrors.sol index fdaddb8..70f2063 100644 --- a/contracts/common/BiconomySponsorshipPaymasterErrors.sol +++ b/contracts/common/BiconomySponsorshipPaymasterErrors.sol @@ -50,7 +50,7 @@ contract BiconomySponsorshipPaymasterErrors { /** * @notice Throws when invalid signature length in paymasterAndData */ - error InvalidDynamicAdjustment(); + error InvalidPriceMarkup(); /** * @notice Throws when insufficient funds for paymasterid diff --git a/contracts/common/BiconomyTokenPaymasterErrors.sol b/contracts/common/BiconomyTokenPaymasterErrors.sol index 7447993..1d2f775 100644 --- a/contracts/common/BiconomyTokenPaymasterErrors.sol +++ b/contracts/common/BiconomyTokenPaymasterErrors.sol @@ -34,7 +34,7 @@ contract BiconomyTokenPaymasterErrors { /** * @notice Throws when invalid signature length in paymasterAndData */ - error InvalidDynamicAdjustment(); + error InvalidPriceMarkup(); /** * @notice Throws when each token doesnt have a corresponding oracle diff --git a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol index 2948273..22d62dc 100644 --- a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol +++ b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol @@ -6,13 +6,13 @@ import { PackedUserOperation } from "@account-abstraction/contracts/core/UserOpe interface IBiconomySponsorshipPaymaster{ event UnaccountedGasChanged(uint256 indexed oldValue, uint256 indexed newValue); - event FixedDynamicAdjustmentChanged(uint256 indexed oldValue, uint256 indexed newValue); + event FixedPriceMarkupChanged(uint256 indexed oldValue, uint256 indexed newValue); event VerifyingSignerChanged(address indexed oldSigner, address indexed newSigner, address indexed actor); event FeeCollectorChanged(address indexed oldFeeCollector, address indexed newFeeCollector, address indexed actor); event GasDeposited(address indexed paymasterId, uint256 indexed value); event GasWithdrawn(address indexed paymasterId, address indexed to, uint256 indexed value); event GasBalanceDeducted(address indexed paymasterId, uint256 indexed charge, bytes32 indexed userOpHash); - event DynamicAdjustmentCollected(address indexed paymasterId, uint256 indexed dynamicAdjustment); + event PriceMarkupCollected(address indexed paymasterId, uint256 indexed priceMarkup); event Received(address indexed sender, uint256 value); event TokensWithdrawn(address indexed token, address indexed to, uint256 indexed amount, address actor); @@ -35,7 +35,7 @@ interface IBiconomySponsorshipPaymaster{ address paymasterId, uint48 validUntil, uint48 validAfter, - uint32 dynamicAdjustment + uint32 priceMarkup ) external view @@ -48,7 +48,7 @@ interface IBiconomySponsorshipPaymaster{ address paymasterId, uint48 validUntil, uint48 validAfter, - uint32 dynamicAdjustment, + uint32 priceMarkup, bytes calldata signature ); } diff --git a/contracts/interfaces/IBiconomyTokenPaymaster.sol b/contracts/interfaces/IBiconomyTokenPaymaster.sol index 51e6023..b5e1f2f 100644 --- a/contracts/interfaces/IBiconomyTokenPaymaster.sol +++ b/contracts/interfaces/IBiconomyTokenPaymaster.sol @@ -18,7 +18,7 @@ interface IBiconomyTokenPaymaster { } event UpdatedUnaccountedGas(uint256 indexed oldValue, uint256 indexed newValue); - event UpdatedFixedDynamicAdjustment(uint256 indexed oldValue, uint256 indexed newValue); + event UpdatedFixedPriceMarkup(uint256 indexed oldValue, uint256 indexed newValue); event UpdatedVerifyingSigner(address indexed oldSigner, address indexed newSigner, address indexed actor); event UpdatedFeeCollector(address indexed oldFeeCollector, address indexed newFeeCollector, address indexed actor); event UpdatedPriceExpiryDuration(uint256 indexed oldValue, uint256 indexed newValue); @@ -30,7 +30,7 @@ interface IBiconomyTokenPaymaster { address indexed token, uint256 nativeCharge, uint256 tokenCharge, - uint256 dynamicAdjustment, + uint256 priceMarkup, bytes32 indexed userOpHash ); event Received(address indexed sender, uint256 value); @@ -42,7 +42,7 @@ interface IBiconomyTokenPaymaster { function setUnaccountedGas(uint256 value) external payable; - function setDynamicAdjustment(uint256 _newUnaccountedGas) external payable; + function setPriceMarkup(uint256 _newUnaccountedGas) external payable; function setPriceExpiryDuration(uint256 _newPriceExpiryDuration) external payable; diff --git a/contracts/libraries/TokenPaymasterParserLib.sol b/contracts/libraries/TokenPaymasterParserLib.sol index 67a7fb8..e064ff4 100644 --- a/contracts/libraries/TokenPaymasterParserLib.sol +++ b/contracts/libraries/TokenPaymasterParserLib.sol @@ -28,7 +28,7 @@ library TokenPaymasterParserLib { uint48 validAfter, address tokenAddress, uint128 tokenPrice, - uint32 externalDynamicAdjustment, + uint32 externalPriceMarkup, bytes memory signature ) { @@ -36,7 +36,7 @@ library TokenPaymasterParserLib { validAfter = uint48(bytes6(modeSpecificData[6:12])); tokenAddress = address(bytes20(modeSpecificData[12:32])); tokenPrice = uint128(bytes16(modeSpecificData[32:48])); - externalDynamicAdjustment = uint32(bytes4(modeSpecificData[48:52])); + externalPriceMarkup = uint32(bytes4(modeSpecificData[48:52])); signature = modeSpecificData[52:]; } diff --git a/contracts/sponsorship/BiconomySponsorshipPaymaster.sol b/contracts/sponsorship/BiconomySponsorshipPaymaster.sol index c4fecb9..ef64b9a 100644 --- a/contracts/sponsorship/BiconomySponsorshipPaymaster.sol +++ b/contracts/sponsorship/BiconomySponsorshipPaymaster.sol @@ -42,7 +42,7 @@ contract BiconomySponsorshipPaymaster is address public feeCollector; uint256 public unaccountedGas; - // Denominator to prevent precision errors when applying dynamic adjustment + // Denominator to prevent precision errors when applying price markup uint256 private constant PRICE_DENOMINATOR = 1e6; // Offset in PaymasterAndData to get to PAYMASTER_ID_OFFSET uint256 private constant PAYMASTER_ID_OFFSET = PAYMASTER_DATA_OFFSET; @@ -194,7 +194,7 @@ contract BiconomySponsorshipPaymaster is address paymasterId, uint48 validUntil, uint48 validAfter, - uint32 dynamicAdjustment + uint32 priceMarkup ) public view @@ -217,7 +217,7 @@ contract BiconomySponsorshipPaymaster is paymasterId, validUntil, validAfter, - dynamicAdjustment + priceMarkup ) ); } @@ -229,7 +229,7 @@ contract BiconomySponsorshipPaymaster is address paymasterId, uint48 validUntil, uint48 validAfter, - uint32 dynamicAdjustment, + uint32 priceMarkup, bytes calldata signature ) { @@ -237,7 +237,7 @@ contract BiconomySponsorshipPaymaster is paymasterId = address(bytes20(paymasterAndData[PAYMASTER_ID_OFFSET:PAYMASTER_ID_OFFSET + 20])); validUntil = uint48(bytes6(paymasterAndData[PAYMASTER_ID_OFFSET + 20:PAYMASTER_ID_OFFSET + 26])); validAfter = uint48(bytes6(paymasterAndData[PAYMASTER_ID_OFFSET + 26:PAYMASTER_ID_OFFSET + 32])); - dynamicAdjustment = uint32(bytes4(paymasterAndData[PAYMASTER_ID_OFFSET + 32:PAYMASTER_ID_OFFSET + 36])); + priceMarkup = uint32(bytes4(paymasterAndData[PAYMASTER_ID_OFFSET + 32:PAYMASTER_ID_OFFSET + 36])); signature = paymasterAndData[PAYMASTER_ID_OFFSET + 36:]; } } @@ -256,24 +256,24 @@ contract BiconomySponsorshipPaymaster is override { unchecked { - (address paymasterId, uint32 dynamicAdjustment, bytes32 userOpHash) = + (address paymasterId, uint32 priceMarkup, bytes32 userOpHash) = abi.decode(context, (address, uint32, bytes32)); // Include unaccountedGas since EP doesn't include this in actualGasCost // unaccountedGas = postOpGas + EP overhead gas + estimated penalty actualGasCost = actualGasCost + (unaccountedGas * actualUserOpFeePerGas); - // Apply the dynamic adjustment - uint256 adjustedGasCost = (actualGasCost * dynamicAdjustment) / PRICE_DENOMINATOR; + // Apply the price markup + uint256 adjustedGasCost = (actualGasCost * priceMarkup) / PRICE_DENOMINATOR; // Deduct the adjusted cost paymasterIdBalances[paymasterId] -= adjustedGasCost; if (adjustedGasCost > actualGasCost) { - // Apply dynamicAdjustment to fee collector balance + // Apply priceMarkup to fee collector balance uint256 premium = adjustedGasCost - actualGasCost; paymasterIdBalances[feeCollector] += premium; // Review if we should emit adjustedGasCost as well - emit DynamicAdjustmentCollected(paymasterId, premium); + emit PriceMarkupCollected(paymasterId, premium); } emit GasBalanceDeducted(paymasterId, adjustedGasCost, userOpHash); @@ -287,7 +287,7 @@ contract BiconomySponsorshipPaymaster is * paymasterAndData[52:72] : paymasterId (dappDepositor) * paymasterAndData[72:78] : validUntil * paymasterAndData[78:84] : validAfter - * paymasterAndData[84:88] : dynamicAdjustment + * paymasterAndData[84:88] : priceMarkup * paymasterAndData[88:] : signature */ function _validatePaymasterUserOp( @@ -300,7 +300,7 @@ contract BiconomySponsorshipPaymaster is override returns (bytes memory context, uint256 validationData) { - (address paymasterId, uint48 validUntil, uint48 validAfter, uint32 dynamicAdjustment, bytes calldata signature) + (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 @@ -310,7 +310,7 @@ contract BiconomySponsorshipPaymaster is } bool validSig = verifyingSigner.isValidSignatureNow( - ECDSA_solady.toEthSignedMessageHash(getHash(userOp, paymasterId, validUntil, validAfter, dynamicAdjustment)), + ECDSA_solady.toEthSignedMessageHash(getHash(userOp, paymasterId, validUntil, validAfter, priceMarkup)), signature ); @@ -319,19 +319,19 @@ contract BiconomySponsorshipPaymaster is return ("", _packValidationData(true, validUntil, validAfter)); } - if (dynamicAdjustment > 2e6 || dynamicAdjustment == 0) { - revert InvalidDynamicAdjustment(); + if (priceMarkup > 2e6 || priceMarkup == 0) { + revert InvalidPriceMarkup(); } // Send 1e6 for No markup // Send between 0 and 1e6 for discount - uint256 effectiveCost = (requiredPreFund * dynamicAdjustment) / PRICE_DENOMINATOR; + uint256 effectiveCost = (requiredPreFund * priceMarkup) / PRICE_DENOMINATOR; if (effectiveCost > paymasterIdBalances[paymasterId]) { revert InsufficientFundsForPaymasterId(); } - context = abi.encode(paymasterId, dynamicAdjustment, userOpHash); + 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. diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index b8faed7..e49743f 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -46,23 +46,23 @@ contract BiconomyTokenPaymaster is // State variables address public verifyingSigner; uint256 public unaccountedGas; - uint256 public dynamicAdjustment; + uint256 public priceMarkup; uint256 public priceExpiryDuration; - IOracle public nativeOracle; // ETH -> USD price + IOracle public nativeAssetToUsdOracle; // ETH -> USD price oracle mapping(address => TokenInfo) tokenDirectory; // PAYMASTER_ID_OFFSET uint256 private constant UNACCOUNTED_GAS_LIMIT = 50_000; // Limit for unaccounted gas cost - uint256 private constant PRICE_DENOMINATOR = 1e6; // Denominator used when calculating cost with dynamic adjustment - uint256 private constant MAX_DYNAMIC_ADJUSTMENT = 2e6; // 100% premium on price (2e6/PRICE_DENOMINATOR) + uint256 private constant PRICE_DENOMINATOR = 1e6; // Denominator used when calculating cost with price markup + uint256 private constant MAX_PRICE_MARKUP = 2e6; // 100% premium on price (2e6/PRICE_DENOMINATOR) constructor( address _owner, address _verifyingSigner, IEntryPoint _entryPoint, uint256 _unaccountedGas, - uint256 _dynamicAdjustment, - IOracle _nativeOracle, + uint256 _priceMarkup, + IOracle _nativeAssetToUsdOracle, uint256 _priceExpiryDuration, address[] memory _tokens, // Array of token addresses IOracle[] memory _oracles // Array of corresponding oracle addresses @@ -78,13 +78,13 @@ contract BiconomyTokenPaymaster is if (_unaccountedGas > UNACCOUNTED_GAS_LIMIT) { revert UnaccountedGasTooHigh(); } - if (_dynamicAdjustment > MAX_DYNAMIC_ADJUSTMENT || _dynamicAdjustment < PRICE_DENOMINATOR) { - revert InvalidDynamicAdjustment(); + if (_priceMarkup > MAX_PRICE_MARKUP || _priceMarkup < PRICE_DENOMINATOR) { + revert InvalidPriceMarkup(); } if (_tokens.length != _oracles.length) { revert TokensAndInfoLengthMismatch(); } - if (_nativeOracle.decimals() != 8) { + if (_nativeAssetToUsdOracle.decimals() != 8) { // ETH -> USD will always have 8 decimals for Chainlink and TWAP revert InvalidOracleDecimals(); } @@ -93,9 +93,9 @@ contract BiconomyTokenPaymaster is assembly ("memory-safe") { sstore(verifyingSigner.slot, _verifyingSigner) sstore(unaccountedGas.slot, _unaccountedGas) - sstore(dynamicAdjustment.slot, _dynamicAdjustment) + sstore(priceMarkup.slot, _priceMarkup) sstore(priceExpiryDuration.slot, _priceExpiryDuration) - sstore(nativeOracle.slot, _nativeOracle) + sstore(nativeAssetToUsdOracle.slot, _nativeAssetToUsdOracle) } // Populate the tokenToOracle mapping @@ -233,23 +233,23 @@ contract BiconomyTokenPaymaster is } /** - * @dev Set a new dynamicAdjustment value. - * @param _newDynamicAdjustment The new value to be set as the dynamic adjustment + * @dev Set a new priceMarkup value. + * @param _newPriceMarkup The new value to be set as the price markup * @notice only to be called by the owner of the contract. */ - function setDynamicAdjustment(uint256 _newDynamicAdjustment) external payable override onlyOwner { - if (_newDynamicAdjustment > MAX_DYNAMIC_ADJUSTMENT || _newDynamicAdjustment < PRICE_DENOMINATOR) { - revert InvalidDynamicAdjustment(); + function setPriceMarkup(uint256 _newPriceMarkup) external payable override onlyOwner { + if (_newPriceMarkup > MAX_PRICE_MARKUP || _newPriceMarkup < PRICE_DENOMINATOR) { + revert InvalidPriceMarkup(); } - uint256 oldDynamicAdjustment = dynamicAdjustment; + uint256 oldPriceMarkup = priceMarkup; assembly ("memory-safe") { - sstore(dynamicAdjustment.slot, _newDynamicAdjustment) + sstore(priceMarkup.slot, _newPriceMarkup) } - emit UpdatedFixedDynamicAdjustment(oldDynamicAdjustment, _newDynamicAdjustment); + emit UpdatedFixedPriceMarkup(oldPriceMarkup, _newPriceMarkup); } /** - * @dev Set a new dynamicAdjustment value. + * @dev Set a new priceMarkup value. * @param _newPriceExpiryDuration The new value to be set as the unaccounted gas value * @notice only to be called by the owner of the contract. */ @@ -272,9 +272,9 @@ contract BiconomyTokenPaymaster is revert InvalidOracleDecimals(); } - IOracle oldNativeOracle = nativeOracle; + IOracle oldNativeOracle = nativeAssetToUsdOracle; assembly ("memory-safe") { - sstore(nativeOracle.slot, _oracle) + sstore(nativeAssetToUsdOracle.slot, _oracle) } emit UpdatedNativeAssetOracle(oldNativeOracle, _oracle); @@ -311,7 +311,7 @@ contract BiconomyTokenPaymaster is uint48 validAfter, address tokenAddress, uint128 tokenPrice, - uint32 externalDynamicAdjustment + uint32 externalPriceMarkup ) public view @@ -335,7 +335,7 @@ contract BiconomyTokenPaymaster is validAfter, tokenAddress, tokenPrice, - externalDynamicAdjustment + externalPriceMarkup ) ); } @@ -371,7 +371,7 @@ contract BiconomyTokenPaymaster is uint48 validAfter, address tokenAddress, uint128 tokenPrice, - uint32 externalDynamicAdjustment, + uint32 externalPriceMarkup, bytes memory signature ) = modeSpecificData.parseExternalModeSpecificData(); @@ -381,7 +381,7 @@ contract BiconomyTokenPaymaster is bool validSig = verifyingSigner.isValidSignatureNow( ECDSA_solady.toEthSignedMessageHash( - getHash(userOp, validUntil, validAfter, tokenAddress, tokenPrice, externalDynamicAdjustment) + getHash(userOp, validUntil, validAfter, tokenAddress, tokenPrice, externalPriceMarkup) ), signature ); @@ -391,14 +391,14 @@ contract BiconomyTokenPaymaster is return ("", _packValidationData(true, validUntil, validAfter)); } - if (externalDynamicAdjustment > MAX_DYNAMIC_ADJUSTMENT || externalDynamicAdjustment < PRICE_DENOMINATOR) { - revert InvalidDynamicAdjustment(); + if (externalPriceMarkup > MAX_PRICE_MARKUP || externalPriceMarkup < PRICE_DENOMINATOR) { + revert InvalidPriceMarkup(); } uint256 tokenAmount; { uint256 maxFeePerGas = UserOperationLib.unpackMaxFeePerGas(userOp); - tokenAmount = ((maxCost + (unaccountedGas) * maxFeePerGas) * externalDynamicAdjustment * tokenPrice) + tokenAmount = ((maxCost + (unaccountedGas) * maxFeePerGas) * externalPriceMarkup * tokenPrice) / (1e18 * PRICE_DENOMINATOR); } @@ -406,7 +406,7 @@ contract BiconomyTokenPaymaster is SafeTransferLib.safeTransferFrom(tokenAddress, userOp.sender, address(this), tokenAmount); context = - abi.encode(userOp.sender, tokenAddress, tokenAmount, tokenPrice, externalDynamicAdjustment, userOpHash); + abi.encode(userOp.sender, tokenAddress, tokenAmount, tokenPrice, externalPriceMarkup, userOpHash); validationData = _packValidationData(false, validUntil, validAfter); } else if (mode == PaymasterMode.INDEPENDENT) { // Use only oracles for the token specified in modeSpecificData @@ -422,14 +422,14 @@ contract BiconomyTokenPaymaster is { // Calculate token amount to precharge uint256 maxFeePerGas = UserOperationLib.unpackMaxFeePerGas(userOp); - tokenAmount = ((maxCost + (unaccountedGas) * maxFeePerGas) * dynamicAdjustment * tokenPrice) + tokenAmount = ((maxCost + (unaccountedGas) * maxFeePerGas) * priceMarkup * tokenPrice) / (1e18 * PRICE_DENOMINATOR); } // Transfer full amount to this address. Unused amount will be refunded in postOP SafeTransferLib.safeTransferFrom(tokenAddress, userOp.sender, address(this), tokenAmount); - context = abi.encode(userOp.sender, tokenAddress, tokenAmount, tokenPrice, dynamicAdjustment, userOpHash); + context = abi.encode(userOp.sender, tokenAddress, tokenAmount, tokenPrice, priceMarkup, userOpHash); validationData = 0; // Validation success and price is valid indefinetly } } @@ -456,13 +456,13 @@ contract BiconomyTokenPaymaster is address tokenAddress, uint256 prechargedAmount, uint192 tokenPrice, - uint256 appliedDynamicAdjustment, + uint256 appliedPriceMarkup, bytes32 userOpHash ) = abi.decode(context, (address, address, uint256, uint192, uint256, bytes32)); // Calculate the actual cost in tokens based on the actual gas cost and the token price uint256 actualTokenAmount = ( - (actualGasCost + (unaccountedGas) * actualUserOpFeePerGas) * appliedDynamicAdjustment * tokenPrice + (actualGasCost + (unaccountedGas) * actualUserOpFeePerGas) * appliedPriceMarkup * tokenPrice ) / (1e18 * PRICE_DENOMINATOR); if (prechargedAmount > actualTokenAmount) { @@ -473,7 +473,7 @@ contract BiconomyTokenPaymaster is } emit PaidGasInTokens( - userOpSender, tokenAddress, actualGasCost, actualTokenAmount, appliedDynamicAdjustment, userOpHash + userOpSender, tokenAddress, actualGasCost, actualTokenAmount, appliedPriceMarkup, userOpHash ); } @@ -496,14 +496,14 @@ contract BiconomyTokenPaymaster is // Calculate price by using token and native oracle uint192 tokenPrice = _fetchPrice(tokenInfo.oracle); - uint192 nativeAssetPrice = _fetchPrice(nativeOracle); + uint192 nativeAssetPrice = _fetchPrice(nativeAssetToUsdOracle); // Adjust to token decimals price = nativeAssetPrice * uint192(tokenInfo.decimals) / tokenPrice; } /// @notice Fetches the latest price from the given oracle. - /// @dev This function is used to get the latest price from the tokenOracle or nativeAssetOracle. + /// @dev This function is used to get the latest price from the tokenOracle or nativeAssetToUsdOracle. /// @param _oracle The oracle contract to fetch the price from. /// @return price The latest price fetched from the oracle. function _fetchPrice(IOracle _oracle) internal view returns (uint192 price) { diff --git a/test/base/TestBase.sol b/test/base/TestBase.sol index 29d6320..3df22c2 100644 --- a/test/base/TestBase.sol +++ b/test/base/TestBase.sol @@ -114,7 +114,7 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { function createUserOp( Vm.Wallet memory sender, BiconomySponsorshipPaymaster paymaster, - uint32 dynamicAdjustment + uint32 priceMarkup ) internal returns (PackedUserOperation memory userOp, bytes32 userOpHash) @@ -126,7 +126,7 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { userOp = buildUserOpWithCalldata(sender, "", address(VALIDATOR_MODULE)); (userOp.paymasterAndData,) = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, paymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, dynamicAdjustment + userOp, PAYMASTER_SIGNER, paymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, priceMarkup ); userOp.signature = signUserOp(sender, userOp); @@ -151,7 +151,7 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { DAPP_ACCOUNT.addr, validUntil, validAfter, - dynamicAdjustment + priceMarkup ); userOp.signature = signUserOp(sender, userOp); userOpHash = ENTRYPOINT.getUserOpHash(userOp); @@ -173,7 +173,7 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { address paymasterId, uint48 validUntil, uint48 validAfter, - uint32 dynamicAdjustment + uint32 priceMarkup ) internal view @@ -187,7 +187,7 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { paymasterId, validUntil, validAfter, - dynamicAdjustment, + priceMarkup, new bytes(65) // Zero signature ); @@ -195,7 +195,7 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { userOp.paymasterAndData = initialPmData; // Generate hash to be signed - bytes32 paymasterHash = paymaster.getHash(userOp, paymasterId, validUntil, validAfter, dynamicAdjustment); + bytes32 paymasterHash = paymaster.getHash(userOp, paymasterId, validUntil, validAfter, priceMarkup); // Sign the hash signature = signMessage(signer, paymasterHash); @@ -209,7 +209,7 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { paymasterId, validUntil, validAfter, - dynamicAdjustment, + priceMarkup, signature ); } @@ -232,7 +232,7 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { uint48 validAfter, address tokenAddress, uint128 tokenPrice, - uint32 externalDynamicAdjustment + uint32 externalPriceMarkup ) internal view @@ -248,7 +248,7 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { validAfter, tokenAddress, tokenPrice, - externalDynamicAdjustment, + externalPriceMarkup, new bytes(65) // Zero signature ); @@ -257,7 +257,7 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { // Generate hash to be signed bytes32 paymasterHash = - paymaster.getHash(userOp, validUntil, validAfter, tokenAddress, tokenPrice, externalDynamicAdjustment); + paymaster.getHash(userOp, validUntil, validAfter, tokenAddress, tokenPrice, externalPriceMarkup); // Sign the hash signature = signMessage(signer, paymasterHash); @@ -273,7 +273,7 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { validAfter, tokenAddress, tokenPrice, - externalDynamicAdjustment, + externalPriceMarkup, signature ); } @@ -287,27 +287,27 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { return result; } - function getDynamicAdjustments( + function getPriceMarkups( BiconomySponsorshipPaymaster paymaster, uint256 initialDappPaymasterBalance, uint256 initialFeeCollectorBalance, - uint32 dynamicAdjustment + uint32 priceMarkup ) internal view - returns (uint256 expectedDynamicAdjustment, uint256 actualDynamicAdjustment) + returns (uint256 expectedPriceMarkup, uint256 actualPriceMarkup) { uint256 resultingDappPaymasterBalance = paymaster.getBalance(DAPP_ACCOUNT.addr); uint256 resultingFeeCollectorPaymasterBalance = paymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); uint256 totalGasFeesCharged = initialDappPaymasterBalance - resultingDappPaymasterBalance; - if (dynamicAdjustment >= 1e6) { - //dynamicAdjustment - expectedDynamicAdjustment = totalGasFeesCharged - ((totalGasFeesCharged * 1e6) / dynamicAdjustment); - actualDynamicAdjustment = resultingFeeCollectorPaymasterBalance - initialFeeCollectorBalance; + if (priceMarkup >= 1e6) { + //priceMarkup + expectedPriceMarkup = totalGasFeesCharged - ((totalGasFeesCharged * 1e6) / priceMarkup); + actualPriceMarkup = resultingFeeCollectorPaymasterBalance - initialFeeCollectorBalance; } else { - revert("DynamicAdjustment must be more than 1e6"); + revert("PriceMarkup must be more than 1e6"); } } @@ -317,13 +317,13 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { uint256 initialFeeCollectorBalance, uint256 initialBundlerBalance, uint256 initialPaymasterEpBalance, - uint32 dynamicAdjustment + uint32 priceMarkup ) internal view { - (uint256 expectedDynamicAdjustment, uint256 actualDynamicAdjustment) = getDynamicAdjustments( - bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, dynamicAdjustment + (uint256 expectedPriceMarkup, uint256 actualPriceMarkup) = getPriceMarkups( + bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, priceMarkup ); uint256 totalGasFeePaid = BUNDLER.addr.balance - initialBundlerBalance; uint256 gasPaidByDapp = initialDappPaymasterBalance - bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); @@ -331,13 +331,13 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { // Assert that what paymaster paid is the same as what the bundler received assertEq(totalGasFeePaid, initialPaymasterEpBalance - bicoPaymaster.getDeposit()); // Assert that adjustment collected (if any) is correct - assertEq(expectedDynamicAdjustment, actualDynamicAdjustment); + assertEq(expectedPriceMarkup, actualPriceMarkup); // Gas paid by dapp is higher than paymaster // Guarantees that EP always has sufficient deposit to pay back dapps assertGt(gasPaidByDapp, BUNDLER.addr.balance - initialBundlerBalance); // Ensure that max 1% difference between total gas paid + the adjustment premium and gas paid by dapp (from // paymaster) - assertApproxEqRel(totalGasFeePaid + actualDynamicAdjustment, gasPaidByDapp, 0.01e18); + assertApproxEqRel(totalGasFeePaid + actualPriceMarkup, gasPaidByDapp, 0.01e18); } function _toSingletonArray(address addr) internal pure returns (address[] memory) { diff --git a/test/unit/concrete/TestSponsorshipPaymaster.t.sol b/test/unit/concrete/TestSponsorshipPaymaster.t.sol index cc8bc8c..e8d37c9 100644 --- a/test/unit/concrete/TestSponsorshipPaymaster.t.sol +++ b/test/unit/concrete/TestSponsorshipPaymaster.t.sol @@ -6,7 +6,7 @@ import { IBiconomySponsorshipPaymaster } from "../../../contracts/interfaces/IBi import { BiconomySponsorshipPaymaster } from "../../../contracts/sponsorship/BiconomySponsorshipPaymaster.sol"; import { MockToken } from "@nexus/contracts/mocks/MockToken.sol"; -contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { +contract TestSponsorshipPaymasterWithPriceMarkup is TestBase { BiconomySponsorshipPaymaster public bicoPaymaster; function setUp() public { @@ -199,13 +199,13 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { bicoPaymaster.withdrawTo(payable(BOB_ADDRESS), 1 ether); } - function test_ValidatePaymasterAndPostOpWithoutDynamicAdjustment() external prankModifier(DAPP_ACCOUNT.addr) { + function test_ValidatePaymasterAndPostOpWithoutPriceMarkup() external prankModifier(DAPP_ACCOUNT.addr) { bicoPaymaster.depositFor{ value: 10 ether }(DAPP_ACCOUNT.addr); // No adjustment - uint32 dynamicAdjustment = 1e6; + uint32 priceMarkup = 1e6; PackedUserOperation[] memory ops = new PackedUserOperation[](1); - (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, dynamicAdjustment); + (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, priceMarkup); ops[0] = userOp; uint256 initialBundlerBalance = BUNDLER.addr.balance; @@ -218,24 +218,24 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - // Calculate and assert dynamic adjustments and gas payments + // Calculate and assert price markups and gas payments calculateAndAssertAdjustments( bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, initialBundlerBalance, initialPaymasterEpBalance, - dynamicAdjustment + priceMarkup ); } - function test_ValidatePaymasterAndPostOpWithDynamicAdjustment() external { + function test_ValidatePaymasterAndPostOpWithPriceMarkup() external { bicoPaymaster.depositFor{ value: 10 ether }(DAPP_ACCOUNT.addr); - // 10% dynamicAdjustment on gas cost - uint32 dynamicAdjustment = 1e6 + 1e5; + // 10% priceMarkup on gas cost + uint32 priceMarkup = 1e6 + 1e5; PackedUserOperation[] memory ops = new PackedUserOperation[](1); - (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, dynamicAdjustment); + (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, priceMarkup); ops[0] = userOp; uint256 initialBundlerBalance = BUNDLER.addr.balance; @@ -245,19 +245,19 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { // submit userops vm.expectEmit(true, false, false, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.DynamicAdjustmentCollected(DAPP_ACCOUNT.addr, 0); + emit IBiconomySponsorshipPaymaster.PriceMarkupCollected(DAPP_ACCOUNT.addr, 0); vm.expectEmit(true, false, true, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - // Calculate and assert dynamic adjustments and gas payments + // Calculate and assert price markups and gas payments calculateAndAssertAdjustments( bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, initialBundlerBalance, initialPaymasterEpBalance, - dynamicAdjustment + priceMarkup ); } @@ -378,24 +378,24 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { address paymasterId = DAPP_ACCOUNT.addr; uint48 validUntil = uint48(block.timestamp + 1 days); uint48 validAfter = uint48(block.timestamp); - uint32 dynamicAdjustment = 1e6; + uint32 priceMarkup = 1e6; PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); (bytes memory paymasterAndData, bytes memory signature) = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, paymasterId, validUntil, validAfter, dynamicAdjustment + userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, paymasterId, validUntil, validAfter, priceMarkup ); ( address parsedPaymasterId, uint48 parsedValidUntil, uint48 parsedValidAfter, - uint32 parsedDynamicAdjustment, + uint32 parsedPriceMarkup, bytes memory parsedSignature ) = bicoPaymaster.parsePaymasterAndData(paymasterAndData); assertEq(paymasterId, parsedPaymasterId); assertEq(validUntil, parsedValidUntil); assertEq(validAfter, parsedValidAfter); - assertEq(dynamicAdjustment, parsedDynamicAdjustment); + assertEq(priceMarkup, parsedPriceMarkup); assertEq(signature, parsedSignature); } } diff --git a/test/unit/concrete/TestTokenPaymaster.t.sol b/test/unit/concrete/TestTokenPaymaster.t.sol index b0bd9d8..a7de937 100644 --- a/test/unit/concrete/TestTokenPaymaster.t.sol +++ b/test/unit/concrete/TestTokenPaymaster.t.sol @@ -15,7 +15,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract TestTokenPaymaster is TestBase { BiconomyTokenPaymaster public tokenPaymaster; - MockOracle public nativeOracle; + MockOracle public nativeAssetToUsdOracle; MockToken public testToken; MockToken public testToken2; MockOracle public tokenOracle; @@ -24,7 +24,7 @@ contract TestTokenPaymaster is TestBase { setupPaymasterTestEnvironment(); // Deploy mock oracles and tokens - nativeOracle = new MockOracle(100_000_000, 8); // Oracle with 8 decimals for ETH + nativeAssetToUsdOracle = new MockOracle(100_000_000, 8); // Oracle with 8 decimals for ETH tokenOracle = new MockOracle(100_000_000, 8); // Oracle with 8 decimals for ERC20 token testToken = new MockToken("Test Token", "TKN"); testToken2 = new MockToken("Test Token 2", "TKN2"); @@ -36,8 +36,8 @@ contract TestTokenPaymaster is TestBase { PAYMASTER_SIGNER.addr, ENTRYPOINT, 5000, // unaccounted gas - 1e6, // dynamic adjustment - nativeOracle, + 1e6, // price markup + nativeAssetToUsdOracle, 1 days, // price expiry duration _toSingletonArray(address(testToken)), _toSingletonArray(IOracle(address(tokenOracle))) @@ -51,7 +51,7 @@ contract TestTokenPaymaster is TestBase { ENTRYPOINT, 5000, 1e6, - nativeOracle, + nativeAssetToUsdOracle, 1 days, _toSingletonArray(address(testToken)), _toSingletonArray(IOracle(address(tokenOracle))) @@ -60,9 +60,9 @@ contract TestTokenPaymaster is TestBase { assertEq(testArtifact.owner(), PAYMASTER_OWNER.addr); assertEq(address(testArtifact.entryPoint()), ENTRYPOINT_ADDRESS); assertEq(testArtifact.verifyingSigner(), PAYMASTER_SIGNER.addr); - assertEq(address(testArtifact.nativeOracle()), address(nativeOracle)); + assertEq(address(testArtifact.nativeAssetToUsdOracle()), address(nativeAssetToUsdOracle)); assertEq(testArtifact.unaccountedGas(), 5000); - assertEq(testArtifact.dynamicAdjustment(), 1e6); + assertEq(testArtifact.priceMarkup(), 1e6); } function test_RevertIf_DeployWithSignerSetToZero() external { @@ -73,7 +73,7 @@ contract TestTokenPaymaster is TestBase { ENTRYPOINT, 5000, 1e6, - nativeOracle, + nativeAssetToUsdOracle, 1 days, _toSingletonArray(address(testToken)), _toSingletonArray(IOracle(address(tokenOracle))) @@ -88,7 +88,7 @@ contract TestTokenPaymaster is TestBase { ENTRYPOINT, 5000, 1e6, - nativeOracle, + nativeAssetToUsdOracle, 1 days, _toSingletonArray(address(testToken)), _toSingletonArray(IOracle(address(tokenOracle))) @@ -103,22 +103,22 @@ contract TestTokenPaymaster is TestBase { ENTRYPOINT, 50_001, // too high unaccounted gas 1e6, - nativeOracle, + nativeAssetToUsdOracle, 1 days, _toSingletonArray(address(testToken)), _toSingletonArray(IOracle(address(tokenOracle))) ); } - function test_RevertIf_InvalidDynamicAdjustment() external { - vm.expectRevert(BiconomyTokenPaymasterErrors.InvalidDynamicAdjustment.selector); + function test_RevertIf_InvalidPriceMarkup() external { + vm.expectRevert(BiconomyTokenPaymasterErrors.InvalidPriceMarkup.selector); new BiconomyTokenPaymaster( PAYMASTER_OWNER.addr, PAYMASTER_SIGNER.addr, ENTRYPOINT, 5000, - 2e6 + 1, // too high dynamic adjustment - nativeOracle, + 2e6 + 1, // too high price markup + nativeAssetToUsdOracle, 1 days, _toSingletonArray(address(testToken)), _toSingletonArray(IOracle(address(tokenOracle))) @@ -198,10 +198,10 @@ contract TestTokenPaymaster is TestBase { MockOracle newOracle = new MockOracle(100_000_000, 8); vm.expectEmit(true, true, false, true, address(tokenPaymaster)); - emit IBiconomyTokenPaymaster.UpdatedNativeAssetOracle(nativeOracle, newOracle); + emit IBiconomyTokenPaymaster.UpdatedNativeAssetOracle(nativeAssetToUsdOracle, newOracle); tokenPaymaster.setNativeOracle(newOracle); - assertEq(address(tokenPaymaster.nativeOracle()), address(newOracle)); + assertEq(address(tokenPaymaster.nativeAssetToUsdOracle()), address(newOracle)); } function test_ValidatePaymasterUserOp_ExternalMode() external { @@ -216,7 +216,7 @@ contract TestTokenPaymaster is TestBase { uint48 validUntil = uint48(block.timestamp + 1 days); uint48 validAfter = uint48(block.timestamp); uint128 tokenPrice = 1e8; // Assume 1 token = 1 USD - uint32 externalDynamicAdjustment = 1e6; + uint32 externalPriceMarkup = 1e6; // Generate and sign the token paymaster data (bytes memory paymasterAndData,) = generateAndSignTokenPaymasterData( @@ -230,7 +230,7 @@ contract TestTokenPaymaster is TestBase { validAfter, address(testToken), tokenPrice, - externalDynamicAdjustment + externalPriceMarkup ); userOp.paymasterAndData = paymasterAndData; @@ -353,10 +353,10 @@ contract TestTokenPaymaster is TestBase { ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); } - // Test setting a high dynamic adjustment - function test_SetDynamicAdjustmentTooHigh() external prankModifier(PAYMASTER_OWNER.addr) { - vm.expectRevert(BiconomyTokenPaymasterErrors.InvalidDynamicAdjustment.selector); - tokenPaymaster.setDynamicAdjustment(2e6 + 1); // Setting too high + // Test setting a high price markup + function test_SetPriceMarkupTooHigh() external prankModifier(PAYMASTER_OWNER.addr) { + vm.expectRevert(BiconomyTokenPaymasterErrors.InvalidPriceMarkup.selector); + tokenPaymaster.setPriceMarkup(2e6 + 1); // Setting too high } // Test invalid signature in external mode diff --git a/test/unit/concrete/TestTokenPaymasterParserLib.t.sol b/test/unit/concrete/TestTokenPaymasterParserLib.t.sol index 694c672..4abfe48 100644 --- a/test/unit/concrete/TestTokenPaymasterParserLib.t.sol +++ b/test/unit/concrete/TestTokenPaymasterParserLib.t.sol @@ -65,7 +65,7 @@ contract TestTokenPaymasterParserLib is Test { uint48 expectedValidAfter = uint48(block.timestamp); address expectedTokenAddress = address(0x1234567890AbcdEF1234567890aBcdef12345678); uint128 expectedTokenPrice = 1e8; - uint32 expectedExternalDynamicAdjustment = 1e6; + uint32 expectedExternalPriceMarkup = 1e6; bytes memory expectedSignature = hex"abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef"; // Construct external mode specific data @@ -74,7 +74,7 @@ contract TestTokenPaymasterParserLib is Test { bytes6(abi.encodePacked(expectedValidAfter)), bytes20(expectedTokenAddress), bytes16(abi.encodePacked(expectedTokenPrice)), - bytes4(abi.encodePacked(expectedExternalDynamicAdjustment)), + bytes4(abi.encodePacked(expectedExternalPriceMarkup)), expectedSignature ); @@ -84,7 +84,7 @@ contract TestTokenPaymasterParserLib is Test { uint48 parsedValidAfter, address parsedTokenAddress, uint128 parsedTokenPrice, - uint32 parsedExternalDynamicAdjustment, + uint32 parsedExternalPriceMarkup, bytes memory parsedSignature ) = externalModeSpecificData.parseExternalModeSpecificData(); @@ -93,7 +93,7 @@ contract TestTokenPaymasterParserLib is Test { assertEq(parsedValidAfter, expectedValidAfter, "ValidAfter should match"); assertEq(parsedTokenAddress, expectedTokenAddress, "Token address should match"); assertEq(parsedTokenPrice, expectedTokenPrice, "Token price should match"); - assertEq(parsedExternalDynamicAdjustment, expectedExternalDynamicAdjustment, "Dynamic adjustment should match"); + assertEq(parsedExternalPriceMarkup, expectedExternalPriceMarkup, "Dynamic adjustment should match"); assertEq(parsedSignature, expectedSignature, "Signature should match"); } diff --git a/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol b/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol index a04ec3a..78de5ad 100644 --- a/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol +++ b/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol @@ -6,7 +6,7 @@ import { IBiconomySponsorshipPaymaster } from "../../../contracts/interfaces/IBi import { BiconomySponsorshipPaymaster } from "../../../contracts/sponsorship/BiconomySponsorshipPaymaster.sol"; import { MockToken } from "@nexus/contracts/mocks/MockToken.sol"; -contract TestFuzz_SponsorshipPaymasterWithDynamicAdjustment is TestBase { +contract TestFuzz_SponsorshipPaymasterWithPriceMarkup is TestBase { BiconomySponsorshipPaymaster public bicoPaymaster; function setUp() public { @@ -92,12 +92,12 @@ contract TestFuzz_SponsorshipPaymasterWithDynamicAdjustment is TestBase { assertEq(token.balanceOf(ALICE_ADDRESS), mintAmount); } - function testFuzz_ValidatePaymasterAndPostOpWithDynamicAdjustment(uint32 dynamicAdjustment) external { - vm.assume(dynamicAdjustment <= 2e6 && dynamicAdjustment > 1e6); + function testFuzz_ValidatePaymasterAndPostOpWithPriceMarkup(uint32 priceMarkup) external { + vm.assume(priceMarkup <= 2e6 && priceMarkup > 1e6); bicoPaymaster.depositFor{ value: 10 ether }(DAPP_ACCOUNT.addr); PackedUserOperation[] memory ops = new PackedUserOperation[](1); - (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, dynamicAdjustment); + (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, priceMarkup); ops[0] = userOp; uint256 initialBundlerBalance = BUNDLER.addr.balance; @@ -107,19 +107,19 @@ contract TestFuzz_SponsorshipPaymasterWithDynamicAdjustment is TestBase { // submit userops vm.expectEmit(true, false, false, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.DynamicAdjustmentCollected(DAPP_ACCOUNT.addr, 0); + emit IBiconomySponsorshipPaymaster.PriceMarkupCollected(DAPP_ACCOUNT.addr, 0); vm.expectEmit(true, false, true, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); - // Calculate and assert dynamic adjustments and gas payments + // Calculate and assert price markups and gas payments calculateAndAssertAdjustments( bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, initialBundlerBalance, initialPaymasterEpBalance, - dynamicAdjustment + priceMarkup ); } @@ -127,28 +127,28 @@ contract TestFuzz_SponsorshipPaymasterWithDynamicAdjustment is TestBase { address paymasterId, uint48 validUntil, uint48 validAfter, - uint32 dynamicAdjustment + uint32 priceMarkup ) external view { PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); (bytes memory paymasterAndData, bytes memory signature) = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, paymasterId, validUntil, validAfter, dynamicAdjustment + userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, paymasterId, validUntil, validAfter, priceMarkup ); ( address parsedPaymasterId, uint48 parsedValidUntil, uint48 parsedValidAfter, - uint32 parsedDynamicAdjustment, + uint32 parsedPriceMarkup, bytes memory parsedSignature ) = bicoPaymaster.parsePaymasterAndData(paymasterAndData); assertEq(paymasterId, parsedPaymasterId); assertEq(validUntil, parsedValidUntil); assertEq(validAfter, parsedValidAfter); - assertEq(dynamicAdjustment, parsedDynamicAdjustment); + assertEq(priceMarkup, parsedPriceMarkup); assertEq(signature, parsedSignature); } } From 4e2a0e54d4a8b83c9ecda7896fc4b2cd26f882fa Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Wed, 11 Sep 2024 18:17:18 +0400 Subject: [PATCH 66/69] uniswapper to perform token to weth swaps through uniswaps --- .../interfaces/IBiconomyTokenPaymaster.sol | 2 +- contracts/token/BiconomyTokenPaymaster.sol | 24 +++-- contracts/token/swaps/Uniswapper.sol | 87 +++++++++++++++++++ test/unit/concrete/TestTokenPaymaster.t.sol | 4 +- 4 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 contracts/token/swaps/Uniswapper.sol diff --git a/contracts/interfaces/IBiconomyTokenPaymaster.sol b/contracts/interfaces/IBiconomyTokenPaymaster.sol index b5e1f2f..0a3a2a5 100644 --- a/contracts/interfaces/IBiconomyTokenPaymaster.sol +++ b/contracts/interfaces/IBiconomyTokenPaymaster.sol @@ -46,7 +46,7 @@ interface IBiconomyTokenPaymaster { function setPriceExpiryDuration(uint256 _newPriceExpiryDuration) external payable; - function setNativeOracle(IOracle _oracle) external payable; + function setNativeAssetToUsdOracle(IOracle _oracle) external payable; function updateTokenDirectory(address _tokenAddress, IOracle _oracle) external payable; } diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index e49743f..02b836b 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -15,6 +15,8 @@ import { TokenPaymasterParserLib } from "../libraries/TokenPaymasterParserLib.so import { SignatureCheckerLib } from "@solady/src/utils/SignatureCheckerLib.sol"; import { ECDSA as ECDSA_solady } from "@solady/src/utils/ECDSA.sol"; import "@account-abstraction/contracts/core/Helpers.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol"; /** * @title BiconomyTokenPaymaster @@ -62,8 +64,8 @@ contract BiconomyTokenPaymaster is IEntryPoint _entryPoint, uint256 _unaccountedGas, uint256 _priceMarkup, - IOracle _nativeAssetToUsdOracle, uint256 _priceExpiryDuration, + IOracle _nativeAssetToUsdOracle, address[] memory _tokens, // Array of token addresses IOracle[] memory _oracles // Array of corresponding oracle addresses ) @@ -266,18 +268,18 @@ contract BiconomyTokenPaymaster is * @param _oracle The new native asset oracle * @notice only to be called by the owner of the contract. */ - function setNativeOracle(IOracle _oracle) external payable override onlyOwner { + function setNativeAssetToUsdOracle(IOracle _oracle) external payable override onlyOwner { if (_oracle.decimals() != 8) { // Native -> USD will always have 8 decimals revert InvalidOracleDecimals(); } - IOracle oldNativeOracle = nativeAssetToUsdOracle; + IOracle oldNativeAssetToUsdOracle = nativeAssetToUsdOracle; assembly ("memory-safe") { sstore(nativeAssetToUsdOracle.slot, _oracle) } - emit UpdatedNativeAssetOracle(oldNativeOracle, _oracle); + emit UpdatedNativeAssetOracle(oldNativeAssetToUsdOracle, _oracle); } /** @@ -298,6 +300,17 @@ contract BiconomyTokenPaymaster is emit UpdatedTokenDirectory(_tokenAddress, _oracle, decimals); } + /** + * @dev Swap a token in the paymaster for ETH to increase its entry point deposit + * @param _swapRouter The address of the swap router to use to facilitate the swap + * @param _tokenAddress The token address of the token to swap for ETH + * @param _tokenAmount The amount of the token to swap + * @notice only to be called by the owner of the contract. + */ + function swapTokenAndDeposit(ISwapRouter _swapRouter, address _tokenAddress, uint256 _tokenAmount) external payable onlyOwner { + + } + /** * 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. @@ -405,8 +418,7 @@ contract BiconomyTokenPaymaster is // Transfer full amount to this address. Unused amount will be refunded in postOP SafeTransferLib.safeTransferFrom(tokenAddress, userOp.sender, address(this), tokenAmount); - context = - abi.encode(userOp.sender, tokenAddress, tokenAmount, tokenPrice, externalPriceMarkup, userOpHash); + context = abi.encode(userOp.sender, tokenAddress, tokenAmount, tokenPrice, externalPriceMarkup, userOpHash); validationData = _packValidationData(false, validUntil, validAfter); } else if (mode == PaymasterMode.INDEPENDENT) { // Use only oracles for the token specified in modeSpecificData diff --git a/contracts/token/swaps/Uniswapper.sol b/contracts/token/swaps/Uniswapper.sol new file mode 100644 index 0000000..a4d88f9 --- /dev/null +++ b/contracts/token/swaps/Uniswapper.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +/* solhint-disable not-rely-on-time */ + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/IPeripheryPayments.sol"; + +abstract contract Uniswapper { + uint256 private constant SWAP_PRICE_DENOMINATOR = 1e26; + + /// @notice The Uniswap V3 SwapRouter contract + ISwapRouter public immutable uniswapRouter; + + /// @notice The ERC-20 token that wraps the native asset for current chain + address public immutable wrappedNative; + + // Token address -> Fee tier of the pool to swap through + mapping(address => uint24) public tokenToPools; + + event UniswapReverted(address indexed tokenIn, address indexed tokenOut, uint256 amountIn); + + error TokensAndAmountsLengthMismatch(); + + constructor( + ISwapRouter _uniswapRouter, + address _wrappedNative, + address[] memory _tokens, + uint24[] memory _tokenPools + ) { + if (_tokens.length != _tokenPools.length) { + revert TokensAndAmountsLengthMismatch(); + } + + // Set router and native wrapped asset addresses + uniswapRouter = _uniswapRouter; + wrappedNative = _wrappedNative; + + for (uint256 i = 0; i < _tokens.length; ++i) { + IERC20(_tokens[i]).approve(address(_uniswapRouter), type(uint256).max); // one time max approval + tokenToPools[_tokens[i]] = _tokenPools[i]; // set mapping of token to uniswap pool to use for swap + } + } + + function _setTokenPool(address _token, uint24 _feeTier) internal { + tokenToPools[_token] = _feeTier; // set mapping of token to uniswap pool to use for swap + } + + function _swapTokenToWeth(address _tokenIn, uint256 _amountIn) internal returns (uint256 amountOut) { + uint24 poolFee = tokenToPools[_tokenIn]; + + ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ + tokenIn: _tokenIn, + tokenOut: wrappedNative, + fee: poolFee, + recipient: address(this), + deadline: block.timestamp, + amountIn: _amountIn, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + }); + try uniswapRouter.exactInputSingle(params) returns (uint256 _amountOut) { + amountOut = _amountOut; + } catch { + emit UniswapReverted(_tokenIn, wrappedNative, _amountIn); + amountOut = 0; + } + } + + function addSlippage(uint256 amount, uint8 slippage) private pure returns (uint256) { + return amount * (1000 - slippage) / 1000; + } + + function tokenToWei(uint256 amount, uint256 price) public pure returns (uint256) { + return amount * price / SWAP_PRICE_DENOMINATOR; + } + + function weiToToken(uint256 amount, uint256 price) public pure returns (uint256) { + return amount * SWAP_PRICE_DENOMINATOR / price; + } + + function unwrapWeth(uint256 _amount) internal { + IPeripheryPayments(address(uniswapRouter)).unwrapWETH9(_amount, address(this)); + } +} diff --git a/test/unit/concrete/TestTokenPaymaster.t.sol b/test/unit/concrete/TestTokenPaymaster.t.sol index a7de937..2c33a77 100644 --- a/test/unit/concrete/TestTokenPaymaster.t.sol +++ b/test/unit/concrete/TestTokenPaymaster.t.sol @@ -194,12 +194,12 @@ contract TestTokenPaymaster is TestBase { ); } - function test_SetNativeOracle() external prankModifier(PAYMASTER_OWNER.addr) { + function test_SetNativeAssetToUsdOracle() external prankModifier(PAYMASTER_OWNER.addr) { MockOracle newOracle = new MockOracle(100_000_000, 8); vm.expectEmit(true, true, false, true, address(tokenPaymaster)); emit IBiconomyTokenPaymaster.UpdatedNativeAssetOracle(nativeAssetToUsdOracle, newOracle); - tokenPaymaster.setNativeOracle(newOracle); + tokenPaymaster.setNativeAssetToUsdOracle(newOracle); assertEq(address(tokenPaymaster.nativeAssetToUsdOracle()), address(newOracle)); } From 27f9f611a361b498cfa61b3f7ebc0c7cdf9bc7ed Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Wed, 11 Sep 2024 18:28:52 +0400 Subject: [PATCH 67/69] approval on setting token fee tier --- contracts/token/swaps/Uniswapper.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/token/swaps/Uniswapper.sol b/contracts/token/swaps/Uniswapper.sol index a4d88f9..93e3a00 100644 --- a/contracts/token/swaps/Uniswapper.sol +++ b/contracts/token/swaps/Uniswapper.sol @@ -45,6 +45,7 @@ abstract contract Uniswapper { } function _setTokenPool(address _token, uint24 _feeTier) internal { + IERC20(_token).approve(address(uniswapRouter), type(uint256).max); // one time max approval tokenToPools[_token] = _feeTier; // set mapping of token to uniswap pool to use for swap } From 7eb9e97a2766a5bbc142095eb661ad1f1f876b35 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Thu, 12 Sep 2024 16:59:10 +0400 Subject: [PATCH 68/69] integrate uniswapper into token paymaster --- contracts/token/BiconomyTokenPaymaster.sol | 52 +++++++++++++++++----- contracts/token/swaps/Uniswapper.sol | 21 +++++---- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index 02b836b..a2ce442 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -15,8 +15,7 @@ import { TokenPaymasterParserLib } from "../libraries/TokenPaymasterParserLib.so import { SignatureCheckerLib } from "@solady/src/utils/SignatureCheckerLib.sol"; import { ECDSA as ECDSA_solady } from "@solady/src/utils/ECDSA.sol"; import "@account-abstraction/contracts/core/Helpers.sol"; -import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; -import "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol"; +import "./swaps/Uniswapper.sol"; /** * @title BiconomyTokenPaymaster @@ -36,10 +35,11 @@ import "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol"; * to manage the assets received by the paymaster. */ contract BiconomyTokenPaymaster is + IBiconomyTokenPaymaster, BasePaymaster, ReentrancyGuardTransient, BiconomyTokenPaymasterErrors, - IBiconomyTokenPaymaster + Uniswapper { using UserOperationLib for PackedUserOperation; using TokenPaymasterParserLib for bytes; @@ -66,10 +66,15 @@ contract BiconomyTokenPaymaster is uint256 _priceMarkup, uint256 _priceExpiryDuration, IOracle _nativeAssetToUsdOracle, - address[] memory _tokens, // Array of token addresses - IOracle[] memory _oracles // Array of corresponding oracle addresses + ISwapRouter _uniswapRouter, + address _wrappedNative, + address[] memory _tokens, // Array of token addresses supported by the paymaster + IOracle[] memory _oracles, // Array of corresponding oracle addresses + address[] memory _swappableTokens, // Array of tokens that you want swappable by the uniswapper + uint24[] memory _swappableTokenPoolFeeTiers // Array of uniswap pool fee tiers for each swappable token ) BasePaymaster(_owner, _entryPoint) + Uniswapper(_uniswapRouter, _wrappedNative, _swappableTokens, _swappableTokenPoolFeeTiers) { if (_isContract(_verifyingSigner)) { revert VerifyingSignerCanNotBeContract(); @@ -301,14 +306,41 @@ contract BiconomyTokenPaymaster is } /** - * @dev Swap a token in the paymaster for ETH to increase its entry point deposit - * @param _swapRouter The address of the swap router to use to facilitate the swap - * @param _tokenAddress The token address of the token to swap for ETH - * @param _tokenAmount The amount of the token to swap + * @dev Swap a token in the paymaster for ETH and deposit the amount received into the entry point + * @param _tokenAddresses The token address to add/update to/for uniswapper + * @param _poolFeeTiers The pool fee tiers for the corresponding token address to use * @notice only to be called by the owner of the contract. */ - function swapTokenAndDeposit(ISwapRouter _swapRouter, address _tokenAddress, uint256 _tokenAmount) external payable onlyOwner { + function updateSwappableTokens( + address[] memory _tokenAddresses, + uint24[] memory _poolFeeTiers + ) + external + payable + onlyOwner + { + if (_tokenAddresses.length != _poolFeeTiers.length) { + revert TokensAndPoolsLengthMismatch(); + } + for (uint256 i = 0; i < _tokenAddresses.length; ++i) { + _setTokenPool(_tokenAddresses[i], _poolFeeTiers[i]); + } + } + + /** + * @dev Swap a token in the paymaster for ETH and deposit the amount received into the entry point + * @param _tokenAddress The token address of the token to swap + * @param _tokenAmount The amount of the token to swap + * @notice only to be called by the owner of the contract. + */ + function swapTokenAndDeposit(address _tokenAddress, uint256 _tokenAmount) external payable onlyOwner { + // Swap tokens + uint256 amountOut = _swapTokenToWeth(_tokenAddress, _tokenAmount); + // Unwrap WETH to ETH + unwrapWeth(amountOut); + // Deposit into EP + entryPoint.depositTo{ value: amountOut }(address(this)); } /** diff --git a/contracts/token/swaps/Uniswapper.sol b/contracts/token/swaps/Uniswapper.sol index 93e3a00..e4d1fbd 100644 --- a/contracts/token/swaps/Uniswapper.sol +++ b/contracts/token/swaps/Uniswapper.sol @@ -20,18 +20,18 @@ abstract contract Uniswapper { // Token address -> Fee tier of the pool to swap through mapping(address => uint24) public tokenToPools; - event UniswapReverted(address indexed tokenIn, address indexed tokenOut, uint256 amountIn); - - error TokensAndAmountsLengthMismatch(); + // Errors + error UniswapReverted(address tokenIn, address tokenOut, uint256 amountIn); + error TokensAndPoolsLengthMismatch(); constructor( ISwapRouter _uniswapRouter, address _wrappedNative, address[] memory _tokens, - uint24[] memory _tokenPools + uint24[] memory _tokenPoolFeeTiers ) { - if (_tokens.length != _tokenPools.length) { - revert TokensAndAmountsLengthMismatch(); + if (_tokens.length != _tokenPoolFeeTiers.length) { + revert TokensAndPoolsLengthMismatch(); } // Set router and native wrapped asset addresses @@ -40,13 +40,13 @@ abstract contract Uniswapper { for (uint256 i = 0; i < _tokens.length; ++i) { IERC20(_tokens[i]).approve(address(_uniswapRouter), type(uint256).max); // one time max approval - tokenToPools[_tokens[i]] = _tokenPools[i]; // set mapping of token to uniswap pool to use for swap + tokenToPools[_tokens[i]] = _tokenPoolFeeTiers[i]; // set mapping of token to uniswap pool to use for swap } } - function _setTokenPool(address _token, uint24 _feeTier) internal { + function _setTokenPool(address _token, uint24 _poolFeeTier) internal { IERC20(_token).approve(address(uniswapRouter), type(uint256).max); // one time max approval - tokenToPools[_token] = _feeTier; // set mapping of token to uniswap pool to use for swap + tokenToPools[_token] = _poolFeeTier; // set mapping of token to uniswap pool to use for swap } function _swapTokenToWeth(address _tokenIn, uint256 _amountIn) internal returns (uint256 amountOut) { @@ -65,8 +65,7 @@ abstract contract Uniswapper { try uniswapRouter.exactInputSingle(params) returns (uint256 _amountOut) { amountOut = _amountOut; } catch { - emit UniswapReverted(_tokenIn, wrappedNative, _amountIn); - amountOut = 0; + revert UniswapReverted(_tokenIn, wrappedNative, _amountIn); } } From 85313e550f6ce59f97496b287be64c0d743470e8 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Thu, 12 Sep 2024 17:00:38 +0400 Subject: [PATCH 69/69] update sol version of uniswapper --- contracts/token/swaps/Uniswapper.sol | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/contracts/token/swaps/Uniswapper.sol b/contracts/token/swaps/Uniswapper.sol index e4d1fbd..1da7c17 100644 --- a/contracts/token/swaps/Uniswapper.sol +++ b/contracts/token/swaps/Uniswapper.sol @@ -1,10 +1,7 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.23; - -/* solhint-disable not-rely-on-time */ +pragma solidity ^0.8.26; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; import "@uniswap/v3-periphery/contracts/interfaces/IPeripheryPayments.sol";