diff --git a/packages/plugins/src/paymaster/README.md b/packages/plugins/src/paymaster/README.md new file mode 100644 index 00000000..121b19c1 --- /dev/null +++ b/packages/plugins/src/paymaster/README.md @@ -0,0 +1,3 @@ +# Paymaster + +These are exemplary paymaster contracts. The SponsorEverythingPaymaster contract and its test can serve as references when developing more complex paymasters for your specific use cases. diff --git a/packages/plugins/src/paymaster/SponsorEverythingPaymaster.sol b/packages/plugins/src/paymaster/SponsorEverythingPaymaster.sol new file mode 100644 index 00000000..f1c54090 --- /dev/null +++ b/packages/plugins/src/paymaster/SponsorEverythingPaymaster.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.4 <0.9.0; + +import {IEntryPoint} from 'account-abstraction/interfaces/IEntryPoint.sol'; + +import {BasePaymaster} from 'account-abstraction/core/BasePaymaster.sol'; +import {UserOperationLib} from 'account-abstraction/core/UserOperationLib.sol'; +import {PackedUserOperation} from 'account-abstraction/interfaces/PackedUserOperation.sol'; + +/*////////////////////////////////////////////////////////////////////////// + THIS CONTRACT IS STILL IN ACTIVE DEVELOPMENT. NOT FOR PRODUCTION USE +//////////////////////////////////////////////////////////////////////////*/ + +/// @title This paymaster sponsors everything. +contract SponsorEverythingPaymaster is BasePaymaster { + using UserOperationLib for PackedUserOperation; + + constructor(IEntryPoint _entryPoint) BasePaymaster(_entryPoint) {} + + /** + * Validate a user operation. + * @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 virtual override returns (bytes memory context, uint256 validationData) { + // Validation logic comes here. + // Approve everything. + return ("", 0); + } +} diff --git a/packages/plugins/test/e2e/SafeSponsorEverythingPaymaster.test.ts b/packages/plugins/test/e2e/SafeSponsorEverythingPaymaster.test.ts new file mode 100644 index 00000000..8b0d3f11 --- /dev/null +++ b/packages/plugins/test/e2e/SafeSponsorEverythingPaymaster.test.ts @@ -0,0 +1,112 @@ +import { expect } from "chai"; +import { ethers } from "ethers"; +import { + SafeECDSAFactory__factory, + SafeECDSAPlugin__factory, + SponsorEverythingPaymaster__factory, + EntryPoint__factory +} from "../../typechain-types"; +import receiptOf from "./utils/receiptOf"; +import { setupTests } from "./utils/setupTests"; +import { createAndSendUserOpWithEcdsaSig } from "./utils/createUserOp"; + +const oneEther = ethers.parseEther("1"); + +describe("SafeSponsorEverythingPaymasterPlugin", () => { + it("should pass the ERC4337 validation", async () => { + const { + bundlerProvider, + provider, + admin, + owner, + entryPointAddress, + deployer, + safeSingleton, + } = await setupTests(); + + // Deploy paymaster. + const paymaster = await deployer.connectOrDeploy( + SponsorEverythingPaymaster__factory, + [entryPointAddress], + ); + const paymasterAddress = await paymaster.getAddress(); + + // Paymaster deposits. + await paymaster.deposit({ value: oneEther }) + + const recipient = ethers.Wallet.createRandom(); + const transferAmount = oneEther; + const dummySignature = await owner.signMessage("dummy sig"); + + // Deploy ecdsa plugin + const safeECDSAFactory = await deployer.connectOrDeploy( + SafeECDSAFactory__factory, + [], + ); + + const createArgs = [ + safeSingleton, + entryPointAddress, + await owner.getAddress(), + 0, + ] satisfies Parameters; + + const accountAddress = await safeECDSAFactory.create.staticCall( + ...createArgs, + ); + + await receiptOf(safeECDSAFactory.create(...createArgs)); + + const safeEcdsaPlugin = SafeECDSAPlugin__factory.connect( + accountAddress, + owner, + ); + + // Native tokens for the pre-fund + await receiptOf( + admin.sendTransaction({ + to: accountAddress, + value: oneEther, + }), + ); + + // Construct userOp + const userOpCallData = safeEcdsaPlugin.interface.encodeFunctionData( + "execTransaction", + [recipient.address, transferAmount, "0x00"], + ); + + // Note: factoryParams is not used because we need to create both the safe + // proxy and the plugin, and 4337 currently only allows one contract + // creation in this step. Since we need an extra step anyway, it's simpler + // to do the whole create outside of 4337. + const factoryParams = { + factory: "0x", + factoryData: "0x", + }; + + // Check paymaster balances before and after sending UserOp. + const entrypoint = EntryPoint__factory.connect(entryPointAddress, provider) + const paymasterBalanceBefore = await entrypoint.balanceOf(paymasterAddress) + + // Send userOp + await createAndSendUserOpWithEcdsaSig( + provider, + bundlerProvider, + owner, + accountAddress, + factoryParams, + userOpCallData, + entryPointAddress, + dummySignature, + paymasterAddress, + 3e5, + "0x" + ); + + const paymasterBalanceAfter = await entrypoint.balanceOf(paymasterAddress) + + expect(paymasterBalanceBefore).greaterThan(paymasterBalanceAfter) + expect(await provider.getBalance(recipient.address)).to.equal(oneEther); + }); +}); diff --git a/packages/plugins/test/e2e/utils/createUserOp.ts b/packages/plugins/test/e2e/utils/createUserOp.ts index 68a467c1..715fd54a 100644 --- a/packages/plugins/test/e2e/utils/createUserOp.ts +++ b/packages/plugins/test/e2e/utils/createUserOp.ts @@ -1,4 +1,4 @@ -import { ethers, getBytes, NonceManager, Signer } from "ethers"; +import { BigNumberish, BytesLike, ethers, getBytes, NonceManager, Signer } from "ethers"; import { AddressZero } from "@ethersproject/constants"; import { SafeProxyFactory } from "../../../typechain-types/lib/safe-contracts/contracts/proxies/SafeProxyFactory"; @@ -84,6 +84,9 @@ export const createUserOperation = async ( userOpCallData: string, entryPointAddress: string, dummySignature: string, + paymaster?: string, + paymasterPostOpGasLimit?: BigNumberish, + paymasterData?: BytesLike, ) => { const entryPoint = EntryPoint__factory.connect( entryPointAddress, @@ -109,6 +112,7 @@ export const createUserOperation = async ( callGasLimit, verificationGasLimit, preVerificationGas, + paymasterVerificationGasLimit, maxFeePerGas, maxPriorityFeePerGas, } = await getGasEstimates( @@ -129,6 +133,10 @@ export const createUserOperation = async ( preVerificationGas, maxFeePerGas, maxPriorityFeePerGas, + paymaster: paymaster, + paymasterVerificationGasLimit: paymaster ? paymasterVerificationGasLimit : undefined, + paymasterPostOpGasLimit: paymasterPostOpGasLimit, + paymasterData: paymasterData, signature: dummySignature, } satisfies UserOperation; @@ -144,6 +152,9 @@ export const createAndSendUserOpWithEcdsaSig = async ( userOpCallData: string, entryPointAddress: string, dummySignature: string, + paymaster?: string, + paymasterPostOpGasLimit?: BigNumberish, + paymasterData?: BytesLike, ) => { const unsignedUserOperation = await createUserOperation( provider, @@ -153,6 +164,9 @@ export const createAndSendUserOpWithEcdsaSig = async ( userOpCallData, entryPointAddress, dummySignature, + paymaster, + paymasterPostOpGasLimit, + paymasterData, ); const userOpHash = getUserOpHash( diff --git a/packages/plugins/test/e2e/utils/getGasEstimates.ts b/packages/plugins/test/e2e/utils/getGasEstimates.ts index 16be1cb4..cc1d001d 100644 --- a/packages/plugins/test/e2e/utils/getGasEstimates.ts +++ b/packages/plugins/test/e2e/utils/getGasEstimates.ts @@ -13,6 +13,7 @@ export const getGasEstimates = async ( )) as { verificationGasLimit: string; preVerificationGas: string; + paymasterVerificationGasLimit: string; callGasLimit: string; }; @@ -30,6 +31,7 @@ export const getGasEstimates = async ( callGasLimit: gasEstimate.callGasLimit, verificationGasLimit: ethers.toBeHex(safeVerificationGasLimit), preVerificationGas: ethers.toBeHex(safePreVerificationGas), + paymasterVerificationGasLimit: ethers.toBeHex(safeVerificationGasLimit), maxFeePerGas, maxPriorityFeePerGas, };