Skip to content

Commit

Permalink
feat(paymaster): add simple GenerousPaymaster for local development (#…
Browse files Browse the repository at this point in the history
…3422)

Co-authored-by: Kevin Ingersoll <kingersoll@gmail.com>
  • Loading branch information
alvrs and holic authored Jan 7, 2025
1 parent 68d9d70 commit a7625b9
Show file tree
Hide file tree
Showing 11 changed files with 348 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .changeset/young-seals-travel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@latticexyz/paymaster": patch
---

Added `GenerousPaymaster`, a simple paymaster that sponsors all user operations for local development purposes.
2 changes: 2 additions & 0 deletions packages/paymaster/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
cache
out
1 change: 1 addition & 0 deletions packages/paymaster/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @latticexyz/paymaster
3 changes: 3 additions & 0 deletions packages/paymaster/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Paymaster contracts

> :warning: **Important note: these contracts have not been audited yet, so any production use is discouraged for now.**
15 changes: 15 additions & 0 deletions packages/paymaster/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[profile.default]
solc = "0.8.24"
ffi = false
fuzz_runs = 256
optimizer = true
optimizer_runs = 3000
verbosity = 2
allow_paths = ["../../node_modules", "../"]
src = "src"
out = "out"
bytecode_hash = "none"
extra_output_files = [
"abi",
"evm.bytecode"
]
44 changes: 44 additions & 0 deletions packages/paymaster/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "@latticexyz/paymaster",
"version": "2.2.14",
"description": "Paymaster contracts",
"repository": {
"type": "git",
"url": "https://github.com/latticexyz/mud.git",
"directory": "packages/paymaster"
},
"license": "MIT",
"type": "module",
"exports": {
"./out/*": "./out/*"
},
"typesVersions": {
"*": {}
},
"files": [
"out/GenerousPaymaster.sol",
"src"
],
"scripts": {
"build": "pnpm run build:abi && pnpm run build:abi-ts",
"build:abi": "forge build",
"build:abi-ts": "abi-ts",
"clean": "pnpm run clean:abi",
"clean:abi": "forge clean",
"dev": "echo 'nothing to watch'",
"lint": "solhint --config ./.solhint.json 'src/**/*.sol'",
"test": "forge test",
"test:ci": "pnpm run test"
},
"dependencies": {},
"devDependencies": {
"@account-abstraction/contracts": "0.7.0",
"@latticexyz/abi-ts": "workspace:*",
"@openzeppelin/contracts": "5.1.0",
"forge-std": "https://github.com/foundry-rs/forge-std.git#1eea5bae12ae557d589f9f0f0edae2faa47cb262",
"solhint": "^3.3.7"
},
"publishConfig": {
"access": "public"
}
}
2 changes: 2 additions & 0 deletions packages/paymaster/remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
forge-std/=node_modules/forge-std/src/
@account-abstraction/=node_modules/@account-abstraction/
61 changes: 61 additions & 0 deletions packages/paymaster/src/experimental/GenerousPaymaster.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;

import { IPaymaster } from "@account-abstraction/contracts/interfaces/IPaymaster.sol";
import { IEntryPoint } from "@account-abstraction/contracts/interfaces/IEntryPoint.sol";
import { PackedUserOperation } from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol";
import { BasePaymaster } from "@account-abstraction/contracts/core/BasePaymaster.sol";

/**
* @title Generous Paymaster
* @author MUD (https://mud.dev) by Lattice (https://lattice.xyz)
* @dev This contract is a simple paymaster that sponsors all user operations.
* It is intended for local development purposes.
*/
contract GenerousPaymaster is BasePaymaster {
constructor(IEntryPoint _entryPoint) BasePaymaster(_entryPoint) {}

/**
* Payment validation: check if paymaster agrees to pay.
* Revert to reject this request.
* Note that bundlers will reject this method if it changes the state, unless the paymaster is trusted (whitelisted).
* The paymaster pre-pays using its deposit, and receive back a refund after the postOp method returns.
* @param userOp - The user operation.
* @param userOpHash - Hash of the user's request data.
* @param maxCost - The maximum cost of this transaction (based on maximum gas and gas price from userOp).
* @return context - Value to send to a postOp. Zero length to signify postOp is not required.
* @return validationData - Signature and time-range of this operation, encoded the same as the return
* value of validateUserOperation.
* <20-byte> sigAuthorizer - 0 for valid signature, 1 to mark signature failure,
* other values are invalid for paymaster.
* <6-byte> validUntil - last timestamp this operation is valid. 0 for "indefinite"
* <6-byte> validAfter - first timestamp this operation is valid
* Note that the validation code cannot use block.timestamp (or block.number) directly.
*/
function _validatePaymasterUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
uint256 maxCost
) internal override returns (bytes memory context, uint256 validationData) {
// No validation required, since this paymaster sponsors all user operations.
}

/**
* Post-operation handler.
* @param mode - Enum with the following options:
* opSucceeded - User operation succeeded.
* opReverted - User op reverted. The paymaster still has to pay for gas.
* postOpReverted - never passed in a call to postOp().
* @param context - The context value returned by validatePaymasterUserOp
* @param actualGasCost - Actual gas used so far (without this postOp call).
* @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(
IPaymaster.PostOpMode mode,
bytes calldata context,
uint256 actualGasCost,
uint256 actualUserOpFeePerGas
) internal override {}
}
108 changes: 108 additions & 0 deletions packages/paymaster/test/GenerousPaymaster.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;

import "forge-std/Test.sol";
import { EntryPoint, IEntryPoint } from "@account-abstraction/contracts/core/EntryPoint.sol";
import { PackedUserOperation } from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol";
import { SimpleAccountFactory, SimpleAccount } from "@account-abstraction/contracts/samples/SimpleAccountFactory.sol";
import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import { TestCounter } from "./utils/TestCounter.sol";
import { GenerousPaymaster } from "../src/experimental/GenerousPaymaster.sol";

contract GenerousPaymasterTest is Test {
EntryPoint entryPoint;
SimpleAccountFactory accountFactory;
GenerousPaymaster paymaster;
TestCounter counter;

address payable beneficiary;
address user;
uint256 userKey;
SimpleAccount account;

uint256 grantAllowance = 10 ether;
uint256 paymasterDeposit = 10 ether;

function setUp() public {
entryPoint = new EntryPoint();
accountFactory = new SimpleAccountFactory(entryPoint);
paymaster = new GenerousPaymaster(entryPoint);
counter = new TestCounter();

beneficiary = payable(makeAddr("beneficiary"));
(user, userKey) = makeAddrAndKey("user");
account = accountFactory.createAccount(user, 0);

entryPoint.depositTo{ value: paymasterDeposit }(address(paymaster));
}

// sanity check for everything works without paymaster
function testCall() external {
vm.deal(address(account), 1e18);
PackedUserOperation memory op = fillUserOp(
account,
userKey,
address(counter),
0,
abi.encodeWithSelector(TestCounter.count.selector)
);
op.signature = signUserOp(op, userKey);
submitUserOp(op);
assertEq(counter.counters(address(account)), 1);
}

function testCallWithPaymaster() external {
PackedUserOperation memory op = fillUserOp(
account,
userKey,
address(counter),
0,
abi.encodeWithSelector(TestCounter.count.selector)
);

op.paymasterAndData = abi.encodePacked(address(paymaster), uint128(100000), uint128(100000));
op.signature = signUserOp(op, userKey);

assertEq(beneficiary.balance, 0);
submitUserOp(op);
assertEq(counter.counters(address(account)), 1);
assertLt(entryPoint.balanceOf(address(paymaster)), paymasterDeposit);
}

function fillUserOp(
SimpleAccount _sender,
uint256 _key,
address _to,
uint256 _value,
bytes memory _data
) internal view returns (PackedUserOperation memory op) {
op.sender = address(_sender);
op.nonce = entryPoint.getNonce(address(_sender), 0);
op.callData = abi.encodeWithSelector(SimpleAccount.execute.selector, _to, _value, _data);
op.accountGasLimits = bytes32(abi.encodePacked(bytes16(uint128(80000)), bytes16(uint128(50000))));
op.preVerificationGas = 50000;
op.gasFees = bytes32(abi.encodePacked(bytes16(uint128(100)), bytes16(uint128(1000000000))));
// NOTE: gas fees are set to 0 on purpose to not require paymaster to have a deposit
// op.gasFees = bytes32(abi.encodePacked(bytes16(uint128(0)), bytes16(uint128(0))));
op.signature = signUserOp(op, _key);
return op;
}

function signUserOp(PackedUserOperation memory op, uint256 _key) internal view returns (bytes memory signature) {
bytes32 hash = entryPoint.getUserOpHash(op);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(_key, MessageHashUtils.toEthSignedMessageHash(hash));
signature = abi.encodePacked(r, s, v);
}

function submitUserOp(PackedUserOperation memory op) internal {
PackedUserOperation[] memory ops = new PackedUserOperation[](1);
ops[0] = op;
entryPoint.handleOps(ops, beneficiary);
}

function expectUserOpRevert(bytes memory message) internal {
vm.expectRevert(
abi.encodeWithSelector(IEntryPoint.FailedOpWithRevert.selector, uint256(0), "AA33 reverted", message)
);
}
}
34 changes: 34 additions & 0 deletions packages/paymaster/test/utils/TestCounter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;

//sample "receiver" contract, for testing "exec" from account.
contract TestCounter {
mapping(address => uint256) public counters;

function count() public {
counters[msg.sender] = counters[msg.sender] + 1;
}

function countFail() public pure {
revert("count failed");
}

function justemit() public {
emit CalledFrom(msg.sender);
}

event CalledFrom(address sender);

//helper method to waste gas
// repeat - waste gas on writing storage in a loop
// junk - dynamic buffer to stress the function size.
mapping(uint256 => uint256) public xxx;
uint256 public offset;

function gasWaster(uint256 repeat, string calldata /*junk*/) external {
for (uint256 i = 1; i <= repeat; i++) {
offset++;
xxx[offset] = i;
}
}
}
Loading

0 comments on commit a7625b9

Please sign in to comment.