1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^ 0.8.27 ;
3
+
4
+ import {BasePaymaster} from "account-abstraction/core/BasePaymaster.sol " ;
5
+ import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol " ;
6
+ import {IEntryPointSimulations} from "account-abstraction/interfaces/IEntryPointSimulations.sol " ;
7
+ import "account-abstraction/core/Helpers.sol " ;
8
+ import {UserOperationLib} from "account-abstraction/core/UserOperationLib.sol " ;
9
+ import {PackedUserOperation} from "account-abstraction/core/UserOperationLib.sol " ;
10
+ import {EcdsaLib} from "./lib/util/EcdsaLib.sol " ;
11
+ import {NODE_PM_MODE_USER, NODE_PM_MODE_DAPP, NODE_PM_MODE_KEEP, NODE_PM_PREMIUM_PERCENT, NODE_PM_PREMIUM_FIXED} from "./types/Constants.sol " ;
12
+
13
+ /**
14
+ * @title BaseNode Paymaster
15
+ * @notice Base PM functionality for MEE Node PMs.
16
+ * It is used to sponsor userOps. Introduced for gas efficient MEE flow.
17
+ */
18
+ abstract contract BaseNodePaymaster is BasePaymaster {
19
+
20
+ error InvalidNodePMRefundMode (bytes4 mode );
21
+ error InvalidNodePMPremiumMode (bytes4 mode );
22
+ error InvalidContext (uint256 length );
23
+
24
+ using UserOperationLib for PackedUserOperation;
25
+ using UserOperationLib for bytes32 ;
26
+
27
+ // 100% with 5 decimals precision
28
+ uint256 private constant PREMIUM_CALCULATION_BASE = 100_00000 ;
29
+
30
+ error EmptyMessageValue ();
31
+ error InsufficientBalance ();
32
+ error PaymasterVerificationGasLimitTooHigh ();
33
+ error Disabled ();
34
+ error PostOpGasLimitTooLow ();
35
+
36
+ constructor (IEntryPoint _entryPoint , address _meeNodeMasterEOA ) payable BasePaymaster (_entryPoint) {
37
+ _transferOwnership (_meeNodeMasterEOA);
38
+ }
39
+
40
+ /**
41
+ * @dev Accepts all userOps
42
+ * Verifies that the handleOps is called by the MEE Node, so it sponsors only for superTxns by owner MEE Node
43
+ * @dev The use of tx.origin makes the NodePaymaster incompatible with the general ERC4337 mempool.
44
+ * This is intentional, and the NodePaymaster is restricted to the MEE node owner anyway.
45
+ *
46
+ * PaymasterAndData is encoded as follows:
47
+ * 20 bytes: Paymaster address
48
+ * 32 bytes: pm gas values
49
+ * === PM_DATA_START ===
50
+ * 4 bytes: mode
51
+ * 4 bytes: premium mode
52
+ * 24 bytes: financial data:: premiumPercentage (only for according premium mode)
53
+ * 20 bytes: refundReceiver (only for DAPP refund mode)
54
+ *
55
+ * @param userOp the userOp to validate
56
+ * param userOpHash the hash of the userOp
57
+ * @param maxCost the max cost of the userOp
58
+ * @return context the context to be used in the postOp
59
+ * @return validationData the validationData to be used in the postOp
60
+ */
61
+ function _validate (PackedUserOperation calldata userOp , bytes32 /*userOpHash*/ , uint256 maxCost )
62
+ internal
63
+ virtual
64
+ returns (bytes memory , uint256 )
65
+ {
66
+ bytes4 refundMode;
67
+ bytes4 premiumMode;
68
+ bytes calldata pmAndData = userOp.paymasterAndData;
69
+ assembly {
70
+ // 0x34 = 52 => PAYMASTER_DATA_OFFSET
71
+ refundMode := calldataload (add (pmAndData.offset, 0x34 ))
72
+ }
73
+
74
+ address refundReceiver;
75
+ // Handle refund mode
76
+ if (refundMode == NODE_PM_MODE_KEEP) { // NO REFUND
77
+ return ("" , 0 );
78
+ } else {
79
+ assembly {
80
+ // 0x38 = 56 => PAYMASTER_DATA_OFFSET + 4
81
+ premiumMode := calldataload (add (pmAndData.offset, 0x38 ))
82
+ }
83
+ if (refundMode == NODE_PM_MODE_USER) {
84
+ refundReceiver = userOp.sender;
85
+ } else if (refundMode == NODE_PM_MODE_DAPP) {
86
+ // if fixed premium => no financial data => offset is 0x08
87
+ // if % premium => financial data => offset is 0x08 + 0x18 = 0x20
88
+ uint256 refundReceiverOffset = premiumMode == NODE_PM_PREMIUM_FIXED ? 0x08 : 0x20 ;
89
+ assembly {
90
+ let o := add (0x34 , refundReceiverOffset)
91
+ refundReceiver := shr (96 , calldataload (add (pmAndData.offset, o)))
92
+ }
93
+ } else {
94
+ revert InvalidNodePMRefundMode (refundMode);
95
+ }
96
+ }
97
+
98
+ bytes memory context = _prepareContext ({
99
+ refundReceiver: refundReceiver,
100
+ premiumMode: premiumMode,
101
+ maxCost: maxCost,
102
+ postOpGasLimit: userOp.unpackPostOpGasLimit (),
103
+ paymasterAndData: userOp.paymasterAndData
104
+ });
105
+
106
+ return (context, 0 );
107
+ }
108
+
109
+ /**
110
+ * Post-operation handler.
111
+ * Checks mode and refunds the userOp.sender if needed.
112
+ * param PostOpMode enum with the following options: // not used
113
+ * opSucceeded - user operation succeeded.
114
+ * opReverted - user op reverted. still has to pay for gas.
115
+ * postOpReverted - user op succeeded, but caused postOp (in mode=opSucceeded) to revert.
116
+ * Now this is the 2nd call, after user's op was deliberately reverted.
117
+ * @dev postOpGasLimit is very important parameter that Node SHOULD use to balance its economic interests
118
+ since penalty is not involved with refunds to sponsor here,
119
+ postOpGasLimit should account for gas that is spend by AA-EP after benchmarking actualGasSpent
120
+ if it is too low (still enough for _postOp), nodePM will be underpaid
121
+ if it is too high, nodePM will be overcharging the superTxn sponsor as refund is going to be lower
122
+ * @param context - the context value returned by validatePaymasterUserOp
123
+ * context is encoded as follows:
124
+ * if mode is KEEP:
125
+ * 0 bytes
126
+ * ==== if there is a refund, always add ===
127
+ * 20 bytes: refundReceiver
128
+ * >== if % premium mode also add ===
129
+ * 24 bytes: financial data:: premiumPercentage
130
+ * 32 bytes: maxGasCost
131
+ * 32 bytes: postOpGasLimit
132
+ * (108 bytes total)
133
+ * >== if fixed premium ====
134
+ * 32 bytes: maxGasCost
135
+ * 32 bytes: postOpGasLimit
136
+ * (84 bytes total)
137
+ * @param actualGasCost - actual gas used so far (without this postOp call).
138
+ * @param actualUserOpFeePerGas - actual userOp fee per gas
139
+ */
140
+ function _postOp (PostOpMode, bytes calldata context , uint256 actualGasCost , uint256 actualUserOpFeePerGas )
141
+ internal
142
+ virtual
143
+ override
144
+ {
145
+ uint256 refund;
146
+ address refundReceiver;
147
+
148
+ // Prepare refund info if any
149
+ if (context.length == 0x00 ) { // 0 bytes => KEEP mode => NO REFUND
150
+ // do nothing
151
+ } else if (context.length == 0x54 ) { // 84 bytes => REFUND: fixed premium mode.
152
+ (refundReceiver, refund) = _handleFixedPremium (context, actualGasCost, actualUserOpFeePerGas);
153
+ } else if (context.length == 0x6c ) { // 108 bytes => REFUND: % premium mode.
154
+ (refundReceiver, refund) = _handlePercentagePremium (context, actualGasCost, actualUserOpFeePerGas);
155
+ } else {
156
+ revert InvalidContext (context.length );
157
+ }
158
+
159
+ // send refund to the superTxn sponsor
160
+ if (refund > 0 ) {
161
+ // Note: At this point the paymaster hasn't received the refund yet, so this withdrawTo() is
162
+ // using the paymaster's existing balance. The paymaster's deposit in the entrypoint will be
163
+ // incremented after postOp() concludes.
164
+ entryPoint.withdrawTo (payable (refundReceiver), refund);
165
+ }
166
+ }
167
+
168
+ // ==== Helper functions ====
169
+
170
+ function _prepareContext (
171
+ address refundReceiver ,
172
+ bytes4 premiumMode ,
173
+ uint256 maxCost ,
174
+ uint256 postOpGasLimit ,
175
+ bytes calldata paymasterAndData
176
+ ) internal pure returns (bytes memory context ) {
177
+ context = abi.encodePacked (
178
+ refundReceiver
179
+ );
180
+
181
+ if (premiumMode == NODE_PM_PREMIUM_PERCENT) {
182
+ uint192 premiumPercentage;
183
+ // 0x3c = 60 => PAYMASTER_DATA_OFFSET + 8
184
+ assembly {
185
+ premiumPercentage := shr (64 , calldataload (add (paymasterAndData.offset, 0x3c )))
186
+ }
187
+ context = abi.encodePacked (
188
+ context,
189
+ premiumPercentage,
190
+ maxCost,
191
+ postOpGasLimit
192
+ ); // 108 bytes
193
+ } else if (premiumMode == NODE_PM_PREMIUM_FIXED) {
194
+ context = abi.encodePacked (
195
+ context,
196
+ maxCost,
197
+ postOpGasLimit
198
+ ); // 84 bytes
199
+ } else {
200
+ revert InvalidNodePMPremiumMode (premiumMode);
201
+ }
202
+ }
203
+
204
+ function _handleFixedPremium (
205
+ bytes calldata context ,
206
+ uint256 actualGasCost ,
207
+ uint256 actualUserOpFeePerGas
208
+ ) internal pure returns (address refundReceiver , uint256 refund ) {
209
+
210
+ uint256 maxGasCost;
211
+ uint256 postOpGasLimit;
212
+
213
+ assembly {
214
+ refundReceiver := shr (96 , calldataload (context.offset))
215
+ maxGasCost := calldataload (add (context.offset, 0x14 ))
216
+ postOpGasLimit := calldataload (add (context.offset, 0x34 ))
217
+ }
218
+
219
+ // account for postOpGas
220
+ actualGasCost += postOpGasLimit * actualUserOpFeePerGas;
221
+
222
+ // when premium is fixed, payment by superTxn sponsor is maxGasCost + fixedPremium
223
+ // so we refund just the gas difference, while fixedPremium is going to the MEE Node
224
+ if (actualGasCost < maxGasCost) {
225
+ refund = maxGasCost - actualGasCost;
226
+ }
227
+ }
228
+
229
+ function _handlePercentagePremium (
230
+ bytes calldata context ,
231
+ uint256 actualGasCost ,
232
+ uint256 actualUserOpFeePerGas
233
+ ) internal pure returns (address refundReceiver , uint256 refund ) {
234
+
235
+ uint192 premiumPercentage;
236
+ uint256 maxGasCost;
237
+ uint256 postOpGasLimit;
238
+
239
+ assembly {
240
+ refundReceiver := shr (96 , calldataload (context.offset))
241
+ premiumPercentage := shr (64 , calldataload (add (context.offset, 0x14 )))
242
+ maxGasCost := calldataload (add (context.offset, 0x2c ))
243
+ postOpGasLimit := calldataload (add (context.offset, 0x4c ))
244
+ }
245
+
246
+ // account for postOpGas
247
+ actualGasCost += postOpGasLimit * actualUserOpFeePerGas;
248
+
249
+ // we do not need to account for the penalty here because it goes to the beneficiary
250
+ // which is the MEE Node itself, so we do not have to charge user for the penalty
251
+
252
+ // account for MEE Node premium
253
+ uint256 costWithPremium = _applyPercentagePremium (actualGasCost, premiumPercentage);
254
+
255
+ // as MEE_NODE charges user with the premium
256
+ uint256 maxCostWithPremium = _applyPercentagePremium (maxGasCost, premiumPercentage);
257
+
258
+ // We do not check for the case, when costWithPremium > maxCost
259
+ // maxCost charged by the MEE Node should include the premium
260
+ // if this is done, costWithPremium can never be > maxCost
261
+ if (costWithPremium < maxCostWithPremium) {
262
+ refund = maxCostWithPremium - costWithPremium;
263
+ }
264
+ }
265
+
266
+ function _applyPercentagePremium (uint256 amount , uint256 premiumPercentage ) internal pure returns (uint256 ) {
267
+ return amount * (PREMIUM_CALCULATION_BASE + premiumPercentage) / PREMIUM_CALCULATION_BASE;
268
+ }
269
+
270
+ /// @dev This function is used to receive ETH from the user and immediately deposit it to the entryPoint
271
+ receive () external payable {
272
+ entryPoint.depositTo {value: msg .value }(address (this ));
273
+ }
274
+ }
0 commit comments