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 3 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.
33 changes: 33 additions & 0 deletions packages/plugins/src/paymaster/SponsorEverythingPaymaster.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// 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)

}
}
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("SafePaymasterPlugin", () => {
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: ethers.parseEther("1") })
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor suggestion

Suggested change
await paymaster.deposit({ value: ethers.parseEther("1") })
await paymaster.deposit({ value: oneEther })

Can also do this in a few other spots.


const recipient = ethers.Wallet.createRandom();
const transferAmount = ethers.parseEther("1");
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: ethers.parseEther("1"),
}),
);

// 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