Skip to content
This repository has been archived by the owner on Sep 30, 2024. It is now read-only.

Add SponsorEverythingPaymaster #256

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/plugins/src/paymaster/README.md
Original file line number Diff line number Diff line change
@@ -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.
35 changes: 35 additions & 0 deletions packages/plugins/src/paymaster/SponsorEverythingPaymaster.sol
Original file line number Diff line number Diff line change
@@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be explicit with return values here. Either:

Suggested change
// Validation logic comes here.
// Validation logic comes here.
// Approve everything
// validUntil & validAfter of 0 means infinite valid time range
return ("", _packValidationData(false, 0, 0))

Or

        // Validation logic comes here.
        // Approve everything
        return ("", 0)

// Approve everything.
return ("", 0);
}
}
112 changes: 112 additions & 0 deletions packages/plugins/test/e2e/SafeSponsorEverythingPaymaster.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof safeECDSAFactory.create.staticCall>;

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);
});
});
16 changes: 15 additions & 1 deletion packages/plugins/test/e2e/utils/createUserOp.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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,
Expand All @@ -109,6 +112,7 @@ export const createUserOperation = async (
callGasLimit,
verificationGasLimit,
preVerificationGas,
paymasterVerificationGasLimit,
maxFeePerGas,
maxPriorityFeePerGas,
} = await getGasEstimates(
Expand All @@ -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;

Expand All @@ -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,
Expand All @@ -153,6 +164,9 @@ export const createAndSendUserOpWithEcdsaSig = async (
userOpCallData,
entryPointAddress,
dummySignature,
paymaster,
paymasterPostOpGasLimit,
paymasterData,
);

const userOpHash = getUserOpHash(
Expand Down
2 changes: 2 additions & 0 deletions packages/plugins/test/e2e/utils/getGasEstimates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const getGasEstimates = async (
)) as {
verificationGasLimit: string;
preVerificationGas: string;
paymasterVerificationGasLimit: string;
callGasLimit: string;
};

Expand All @@ -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,
};
Expand Down
Loading