Skip to content

Commit a8100be

Browse files
ernestognwAmxx
andauthored
Add PaymasterCore and PaymasterSigner (#71)
Co-authored-by: Hadrien Croubois <[email protected]>
1 parent d611369 commit a8100be

File tree

12 files changed

+644
-3
lines changed

12 files changed

+644
-3
lines changed

contracts/account/AccountCore.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ abstract contract AccountCore is AbstractSigner, IAccount {
4747
* @dev Canonical entry point for the account that forwards and validates user operations.
4848
*/
4949
function entryPoint() public view virtual returns (IEntryPoint) {
50-
return IEntryPoint(0x0000000071727De22E5E9d8BAf0edAc6f37da032);
50+
return ERC4337Utils.ENTRYPOINT;
5151
}
5252

5353
/**
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol";
6+
import {IEntryPoint, IPaymaster, PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
7+
import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol";
8+
9+
/**
10+
* @dev A simple ERC4337 paymaster implementation. This base implementation only includes the minimal logic to validate
11+
* and pay for user operations.
12+
*
13+
* Developers must implement the {PaymasterCore-_validatePaymasterUserOp} function to define the paymaster's validation
14+
* and payment logic. The `context` parameter is used to pass data between the validation and execution phases.
15+
*
16+
* The paymaster includes support to call the {IEntryPointStake} interface to manage the paymaster's deposits and stakes
17+
* through the internal functions {_deposit}, {_withdraw}, {_addStake}, {_unlockStake} and {_withdrawStake}.
18+
*
19+
* * Deposits are used to pay for user operations.
20+
* * Stakes are used to guarantee the paymaster's reputation and obtain more flexibility in accessing storage.
21+
*
22+
* NOTE: See [Paymaster's unstaked reputation rules](https://eips.ethereum.org/EIPS/eip-7562#unstaked-paymasters-reputation-rules)
23+
* for more details on the paymaster's storage access limitations.
24+
*/
25+
abstract contract PaymasterCore is IPaymaster {
26+
/// @dev Unauthorized call to the paymaster.
27+
error PaymasterUnauthorized(address sender);
28+
29+
/// @dev Revert if the caller is not the entry point.
30+
modifier onlyEntryPoint() {
31+
_checkEntryPoint();
32+
_;
33+
}
34+
35+
modifier onlyWithdrawer() {
36+
_authorizeWithdraw();
37+
_;
38+
}
39+
40+
/// @dev Canonical entry point for the account that forwards and validates user operations.
41+
function entryPoint() public view virtual returns (IEntryPoint) {
42+
return ERC4337Utils.ENTRYPOINT;
43+
}
44+
45+
/// @inheritdoc IPaymaster
46+
function validatePaymasterUserOp(
47+
PackedUserOperation calldata userOp,
48+
bytes32 userOpHash,
49+
uint256 maxCost
50+
) public virtual onlyEntryPoint returns (bytes memory context, uint256 validationData) {
51+
return _validatePaymasterUserOp(userOp, userOpHash, maxCost);
52+
}
53+
54+
/// @inheritdoc IPaymaster
55+
function postOp(
56+
PostOpMode mode,
57+
bytes calldata context,
58+
uint256 actualGasCost,
59+
uint256 actualUserOpFeePerGas
60+
) public virtual onlyEntryPoint {
61+
_postOp(mode, context, actualGasCost, actualUserOpFeePerGas);
62+
}
63+
64+
/**
65+
* @dev Internal validation of whether the paymaster is willing to pay for the user operation.
66+
* Returns the context to be passed to postOp and the validation data.
67+
*
68+
* The `requiredPreFund` is the amount the paymaster has to pay (in native tokens). It's calculated
69+
* as `requiredGas * userOp.maxFeePerGas`, where `required` gas can be calculated from the user operation
70+
* as `verificationGasLimit + callGasLimit + paymasterVerificationGasLimit + paymasterPostOpGasLimit + preVerificationGas`
71+
*/
72+
function _validatePaymasterUserOp(
73+
PackedUserOperation calldata userOp,
74+
bytes32 userOpHash,
75+
uint256 requiredPreFund
76+
) internal virtual returns (bytes memory context, uint256 validationData);
77+
78+
/**
79+
* @dev Handles post user operation execution logic. The caller must be the entry point.
80+
*
81+
* It receives the `context` returned by `_validatePaymasterUserOp`. Reverts by default
82+
* since the function is not called if no context is returned by {validatePaymasterUserOp}.
83+
*
84+
* NOTE: The `actualUserOpFeePerGas` is not `tx.gasprice`. A user operation can be bundled with other transactions
85+
* making the gas price of the user operation to differ.
86+
*/
87+
function _postOp(
88+
PostOpMode /* mode */,
89+
bytes calldata /* context */,
90+
uint256 /* actualGasCost */,
91+
uint256 /* actualUserOpFeePerGas */
92+
) internal virtual {}
93+
94+
/// @dev Calls {IEntryPointStake-depositTo}.
95+
function deposit() public payable virtual {
96+
ERC4337Utils.depositTo(address(this), msg.value);
97+
}
98+
99+
/// @dev Calls {IEntryPointStake-withdrawTo}.
100+
function withdraw(address payable to, uint256 value) public virtual onlyWithdrawer {
101+
ERC4337Utils.withdrawTo(to, value);
102+
}
103+
104+
/// @dev Calls {IEntryPointStake-addStake}.
105+
function addStake(uint32 unstakeDelaySec) public payable virtual {
106+
ERC4337Utils.addStake(msg.value, unstakeDelaySec);
107+
}
108+
109+
/// @dev Calls {IEntryPointStake-unlockStake}.
110+
function unlockStake() public virtual onlyWithdrawer {
111+
ERC4337Utils.unlockStake();
112+
}
113+
114+
/// @dev Calls {IEntryPointStake-withdrawStake}.
115+
function withdrawStake(address payable to) public virtual onlyWithdrawer {
116+
ERC4337Utils.withdrawStake(to);
117+
}
118+
119+
/// @dev Ensures the caller is the {entrypoint}.
120+
function _checkEntryPoint() internal view virtual {
121+
address sender = msg.sender;
122+
if (sender != address(entryPoint())) {
123+
revert PaymasterUnauthorized(sender);
124+
}
125+
}
126+
127+
/**
128+
* @dev Checks whether `msg.sender` withdraw funds stake or deposit from the entrypoint on paymaster's behalf.
129+
*
130+
* Use of an https://docs.openzeppelin.com/contracts/5.x/access-control[access control]
131+
* modifier such as {Ownable-onlyOwner} is recommended.
132+
*
133+
* ```solidity
134+
* function _authorizeUpgrade() internal onlyOwner {}
135+
* ```
136+
*/
137+
function _authorizeWithdraw() internal virtual;
138+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
6+
import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol";
7+
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
8+
import {Calldata} from "@openzeppelin/contracts/utils/Calldata.sol";
9+
import {PaymasterCore} from "./PaymasterCore.sol";
10+
import {AbstractSigner} from "../../utils/cryptography/AbstractSigner.sol";
11+
12+
/**
13+
* @dev Extension of {PaymasterCore} that adds signature validation. See {SignerECDSA}, {SignerP256} or {SignerRSA}.
14+
*
15+
* Example of usage:
16+
*
17+
* ```solidity
18+
* contract MyPaymasterECDSASigner is PaymasterSigner, SignerECDSA {
19+
* constructor() EIP712("MyPaymasterECDSASigner", "1") {
20+
* // Will revert if the signer is already initialized
21+
* _setSigner(signerAddr);
22+
* }
23+
* }
24+
* ```
25+
*/
26+
abstract contract PaymasterSigner is AbstractSigner, EIP712, PaymasterCore {
27+
using ERC4337Utils for *;
28+
29+
bytes32 internal constant _USER_OPERATION_REQUEST =
30+
keccak256(
31+
"UserOperationRequest(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,uint256 paymasterVerificationGasLimit,uint256 paymasterPostOpGasLimit,uint48 validAfter,uint48 validUntil)"
32+
);
33+
34+
/**
35+
* @dev Virtual function that returns the signable hash for a user operations. Given the `userOpHash`
36+
* contains the `paymasterAndData` itself, it's not possible to sign that value directly. Instead,
37+
* this function must be used to provide a custom mechanism to authorize an user operation.
38+
*/
39+
function _signableUserOpHash(
40+
PackedUserOperation calldata userOp,
41+
uint48 validAfter,
42+
uint48 validUntil
43+
) internal view virtual returns (bytes32) {
44+
return
45+
_hashTypedDataV4(
46+
keccak256(
47+
abi.encode(
48+
_USER_OPERATION_REQUEST,
49+
userOp.sender,
50+
userOp.nonce,
51+
keccak256(userOp.initCode),
52+
keccak256(userOp.callData),
53+
userOp.accountGasLimits,
54+
userOp.preVerificationGas,
55+
userOp.gasFees,
56+
userOp.paymasterVerificationGasLimit(),
57+
userOp.paymasterPostOpGasLimit(),
58+
validAfter,
59+
validUntil
60+
)
61+
)
62+
);
63+
}
64+
65+
/**
66+
* @dev Internal validation of whether the paymaster is willing to pay for the user operation.
67+
* Returns the context to be passed to postOp and the validation data.
68+
*
69+
* NOTE: The `context` returned is `bytes(0)`. Developers overriding this function MUST
70+
* override {_postOp} to process the context passed along.
71+
*/
72+
function _validatePaymasterUserOp(
73+
PackedUserOperation calldata userOp,
74+
bytes32 /* userOpHash */,
75+
uint256 /* maxCost */
76+
) internal virtual override returns (bytes memory context, uint256 validationData) {
77+
(uint48 validAfter, uint48 validUntil, bytes calldata signature) = _decodePaymasterUserOp(userOp);
78+
return (
79+
bytes(""),
80+
_rawSignatureValidation(_signableUserOpHash(userOp, validAfter, validUntil), signature).packValidationData(
81+
validAfter,
82+
validUntil
83+
)
84+
);
85+
}
86+
87+
/// @dev Decodes the user operation's data from `paymasterAndData`.
88+
function _decodePaymasterUserOp(
89+
PackedUserOperation calldata userOp
90+
) internal pure virtual returns (uint48 validAfter, uint48 validUntil, bytes calldata signature) {
91+
bytes calldata paymasterData = userOp.paymasterData();
92+
return (uint48(bytes6(paymasterData[0:6])), uint48(bytes6(paymasterData[6:12])), paymasterData[12:]);
93+
}
94+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
6+
import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
7+
import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol";
8+
import {PaymasterSigner} from "../../../account/paymaster/PaymasterSigner.sol";
9+
import {SignerECDSA} from "../../../utils/cryptography/SignerECDSA.sol";
10+
11+
abstract contract PaymasterCoreContextNoPostOpMock is PaymasterSigner, SignerECDSA, Ownable {
12+
using ERC4337Utils for *;
13+
14+
function _validatePaymasterUserOp(
15+
PackedUserOperation calldata userOp,
16+
bytes32 userOpHash,
17+
uint256 requiredPreFund
18+
) internal override returns (bytes memory context, uint256 validationData) {
19+
// use the userOp's paymasterData as context;
20+
context = userOp.paymasterData();
21+
// super call (PaymasterSigner + SignerECDSA) for the validation data
22+
(, validationData) = super._validatePaymasterUserOp(userOp, userOpHash, requiredPreFund);
23+
}
24+
25+
function _authorizeWithdraw() internal override onlyOwner {}
26+
}
27+
28+
abstract contract PaymasterCoreMock is PaymasterCoreContextNoPostOpMock {
29+
event PaymasterDataPostOp(bytes paymasterData);
30+
31+
function _postOp(
32+
PostOpMode mode,
33+
bytes calldata context,
34+
uint256 actualGasCost,
35+
uint256 actualUserOpFeePerGas
36+
) internal override {
37+
emit PaymasterDataPostOp(context);
38+
super._postOp(mode, context, actualGasCost, actualUserOpFeePerGas);
39+
}
40+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// contracts/MyPaymaster.sol
2+
// SPDX-License-Identifier: MIT
3+
4+
pragma solidity ^0.8.20;
5+
6+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
7+
import {PaymasterCore} from "../../../../account/paymaster/PaymasterCore.sol";
8+
import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
9+
10+
contract MyPaymaster is PaymasterCore, Ownable {
11+
constructor(address withdrawer) Ownable(withdrawer) {}
12+
13+
/// @dev Paymaster user op validation logic
14+
function _validatePaymasterUserOp(
15+
PackedUserOperation calldata userOp,
16+
bytes32 userOpHash,
17+
uint256 requiredPreFund
18+
) internal override returns (bytes memory context, uint256 validationData) {
19+
// Custom validation logic
20+
}
21+
22+
function _authorizeWithdraw() internal override onlyOwner {}
23+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// contracts/MyPaymasterECDSA.sol
2+
// SPDX-License-Identifier: MIT
3+
4+
pragma solidity ^0.8.20;
5+
6+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
7+
import {PaymasterSigner, EIP712} from "../../../../account/paymaster/PaymasterSigner.sol";
8+
import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
9+
import {SignerECDSA} from "../../../../utils/cryptography/SignerECDSA.sol";
10+
11+
contract MyPaymasterECDSA is PaymasterSigner, SignerECDSA, Ownable {
12+
constructor(address paymasterSignerAddr, address withdrawer) EIP712("MyPaymasterECDSA", "1") Ownable(withdrawer) {
13+
_setSigner(paymasterSignerAddr);
14+
}
15+
16+
function _authorizeWithdraw() internal override onlyOwner {}
17+
}

contracts/utils/cryptography/SignerP256.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {AbstractSigner} from "./AbstractSigner.sol";
1818
* contract MyAccountP256 is Account, SignerP256, Initializable {
1919
* constructor() EIP712("MyAccountP256", "1") {}
2020
*
21-
* function initializeSigner(bytes32 qx, bytes32 qy) public initializer {
21+
* function initialize(bytes32 qx, bytes32 qy) public initializer {
2222
* _setSigner(qx, qy);
2323
* }
2424
* }

contracts/utils/cryptography/SignerRSA.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {AbstractSigner} from "./AbstractSigner.sol";
1818
* contract MyAccountRSA is Account, SignerRSA, Initializable {
1919
* constructor() EIP712("MyAccountRSA", "1") {}
2020
*
21-
* function initializeSigner(bytes memory e, bytes memory n) public initializer {
21+
* function initialize(bytes memory e, bytes memory n) public initializer {
2222
* _setSigner(e, n);
2323
* }
2424
* }

docs/modules/ROOT/pages/account-abstraction.adoc

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,30 @@ include::api:example$account/MyFactoryAccount.sol[]
7070

7171
You've setup your own account and its corresponding factory. Both are ready to be used with ERC-4337 infrastructure. Customizing the factory to other validation mechanisms must be straightforward.
7272

73+
== Paymaster
74+
75+
In case you want to sponsor user operation for your users, the ERC-4337 defines a special type of contract called Paymaster, whose purpose is to pay the gas fees consumed by the user operation. Developers can bootstrap their own paymaster with xref:api:utils.adoc#PaymasterCore[`PaymasterCore`] and implement a signature-based paymaster authorization with xref:api:utils.adoc#PaymasterSigner[`PaymasterSigner`] that they can combine with any xref:api:utils.adoc#AbstractSigner[`AbstractSigner`] quite easily.
76+
77+
To enable operation sponsorship, users sign their user operation including a special field called `paymasterAndData` resulting from the concatenation of the paymaster they're using and the calldata that's going to be passed into xref:api:utils.adoc#PaymasterCore-validatePaymasterUserOp[`validatePaymasterUserOp`]. This function will use the passed bytes buffer to determine whether it will pay for the user operation or not.
78+
79+
=== Setting up a paymaster
80+
81+
To start your paymaster from scratch, the library provides xref:api:utils.adoc#PaymasterCore[`PaymasterCore`] with the basic logic you can extend to implement your own validation logic.
82+
83+
[source,solidity]
84+
----
85+
include::api:example$account/paymaster/MyPaymaster.sol[]
86+
----
87+
88+
TIP: Use https://docs.openzeppelin.com/contracts/5.x/api/account#ERC4337Utils[`ERC4337Utils`] to access paymaster-related fields of the userOp (e.g. `paymasterData`, `paymasterVerificationGasLimit`)
89+
90+
The library also includes the xref:api:utils.adoc#PaymasterSigner[`PaymasterSigner`] that allows developers to setup a signature-based authorization paymaster. This is the easiest setup to start sponsoring user operations with an ECDSA signature (i.e. a regular ethereum signature).
91+
92+
[source,solidity]
93+
----
94+
include::api:example$account/paymaster/MyPaymasterECDSA.sol[]
95+
----
96+
7397
== ERC-4337 Overview
7498

7599
The ERC-4337 is a detailed specification of how to implement the necessary logic to handle operations without making changes to the protocol level (i.e. the rules of the blockchain itself). This specification defines the following components:
@@ -136,6 +160,8 @@ To build your own factory, see xref:account-abstraction.adoc#account_factory[Acc
136160

137161
A Paymaster is an optional entity that can sponsor gas fees for Accounts, or allow them to pay for those fees in ERC-20 instead of native currency. This abstracts gas away of the user experience in the same way that computational costs of cloud servers are abstracted away from end-users.
138162

163+
To build your own paymaster, see xref:account-abstraction.adoc#paymaster[Paymaster]
164+
139165
== Further notes
140166

141167
=== ERC-7739 Signatures

0 commit comments

Comments
 (0)