-
Notifications
You must be signed in to change notification settings - Fork 195
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(paymaster): add simple GenerousPaymaster for local development (#…
…3422) Co-authored-by: Kevin Ingersoll <kingersoll@gmail.com>
- Loading branch information
Showing
11 changed files
with
348 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
cache | ||
out |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# @latticexyz/paymaster |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.** |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
Oops, something went wrong.