Skip to content

Commit 450122b

Browse files
authored
Merge pull request #38 from bcnmy/release/v0.0.4
Release/v0.0.4
2 parents c56d6b3 + 300bacc commit 450122b

33 files changed

+1777
-855
lines changed

.github/workflows/test.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ jobs:
2929
run: |
3030
forge --version
3131
32+
- name: Install dependencies
33+
run: |
34+
npm install
35+
3236
- name: Run Forge fmt
3337
run: |
3438
forge fmt --check

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,5 @@ node_modules
4646
cache_forge/solidity-files-cache.json
4747
.vscode/settings.json
4848

49+
artifacts
50+
.states

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@ $ forge build
2020
### Test
2121

2222
```shell
23-
$ forge test
23+
$ forge test --isolate
2424
```
Binary file not shown.

contracts/BaseNodePaymaster.sol

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
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+
}

contracts/MEEEntryPoint.sol

Lines changed: 0 additions & 49 deletions
This file was deleted.

0 commit comments

Comments
 (0)