@@ -4,7 +4,6 @@ pragma solidity ^0.8.20;
4
4
5
5
import {ERC4337Utils , PackedUserOperation} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol " ;
6
6
import {IERC20 , SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol " ;
7
- import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol " ;
8
7
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol " ;
9
8
import {PaymasterCore} from "./PaymasterCore.sol " ;
10
9
@@ -17,60 +16,105 @@ import {PaymasterCore} from "./PaymasterCore.sol";
17
16
* function _fetchDetails(
18
17
* PackedUserOperation calldata userOp,
19
18
* bytes32 userOpHash
20
- * ) internal view override returns (uint256 validationData, IERC20 token, uint256 tokenPrice, address guarantor ) {
21
- * // Implement logic to fetch the token, token price, and guarantor address from the userOp
19
+ * ) internal view override returns (uint256 validationData, IERC20 token, uint256 tokenPrice) {
20
+ * // Implement logic to fetch the token, and token price from the userOp
22
21
* }
23
22
* ```
23
+ *
24
+ * The contract follows a pre-charge and refund model:
25
+ * 1. During validation, it pre-charges the maximum possible gas cost
26
+ * 2. After execution, it refunds any unused gas back to the user
24
27
*/
25
28
abstract contract PaymasterERC20 is PaymasterCore {
26
29
using ERC4337Utils for * ;
27
30
using Math for * ;
28
31
using SafeERC20 for IERC20 ;
29
32
33
+ /**
34
+ * @dev Emitted when a user operation identified by `userOpHash` is sponsored by this paymaster
35
+ * using the specified ERC-20 `token`. The `tokenAmount` is the amount charged for the operation,
36
+ * and `tokenPrice` is the price of the token in native currency (e.g., ETH).
37
+ */
30
38
event UserOperationSponsored (
31
39
bytes32 indexed userOpHash ,
32
- address indexed user ,
33
- address indexed guarantor ,
40
+ address indexed token ,
34
41
uint256 tokenAmount ,
35
- uint256 tokenPrice ,
36
- bool paidByGuarantor
42
+ uint256 tokenPrice
37
43
);
38
44
39
- // Over-estimations: ERC-20 balances/allowances may be cold and contracts may not be optimized
40
- uint256 private constant POST_OP_COST = 30_000 ;
41
- uint256 private constant POST_OP_COST_WITH_GUARANTOR = 45_000 ;
45
+ /**
46
+ * @dev Throws when the paymaster fails to refund the difference between the `prefundAmount`
47
+ * and the `actualAmount` of `token`.
48
+ */
49
+ error PaymasterERC20FailedRefund (IERC20 token , uint256 prefundAmount , uint256 actualAmount , bytes prefundContext );
42
50
43
- /// @inheritdoc PaymasterCore
51
+ /**
52
+ * @dev See {PaymasterCore-_validatePaymasterUserOp}.
53
+ *
54
+ * Attempts to retrieve the `token` and `tokenPrice` from the user operation (see {_fetchDetails})
55
+ * and prefund the user operation using these values and the `maxCost` argument (see {_prefund}).
56
+ *
57
+ * Returns `abi.encodePacked(userOpHash, token, tokenPrice, prefundAmount, prefunder, prefundContext)` in
58
+ * `context` if the prefund is successful. Otherwise, it returns empty bytes.
59
+ */
44
60
function _validatePaymasterUserOp (
45
61
PackedUserOperation calldata userOp ,
46
62
bytes32 userOpHash ,
47
63
uint256 maxCost
48
64
) internal virtual override returns (bytes memory context , uint256 validationData ) {
49
- (uint256 validationData_ , IERC20 token , uint256 tokenPrice , address guarantor ) = _fetchDetails (
65
+ IERC20 token;
66
+ uint256 tokenPrice;
67
+ (validationData, token, tokenPrice) = _fetchDetails (userOp, userOpHash);
68
+ context = abi.encodePacked (userOpHash, token, tokenPrice);
69
+ (bool prefunded , uint256 prefundAmount , address prefunder , bytes memory prefundContext ) = _prefund (
50
70
userOp,
51
- userOpHash
71
+ userOpHash,
72
+ token,
73
+ tokenPrice,
74
+ userOp.sender,
75
+ maxCost
52
76
);
53
77
54
- uint256 prefundAmount = (maxCost +
55
- (guarantor == address (0 )).ternary (POST_OP_COST, POST_OP_COST_WITH_GUARANTOR) *
56
- userOp.maxFeePerGas ()).mulDiv (tokenPrice, _tokenPriceDenominator ());
57
-
58
- // if validation is obviously failed, don't even try to do the ERC-20 transfer
59
- return
60
- (validationData_ != ERC4337Utils .SIG_VALIDATION_FAILED &&
61
- token.trySafeTransferFrom (
62
- guarantor == address (0 ) ? userOp.sender : guarantor,
63
- address (this ),
64
- prefundAmount
65
- ))
66
- ? (
67
- abi.encodePacked (userOpHash, token, prefundAmount, tokenPrice, userOp.sender, guarantor),
68
- validationData_
69
- )
70
- : (bytes ("" ), ERC4337Utils .SIG_VALIDATION_FAILED);
78
+ if (validationData == ERC4337Utils .SIG_VALIDATION_FAILED || ! prefunded)
79
+ return (bytes ("" ), ERC4337Utils .SIG_VALIDATION_FAILED);
80
+
81
+ return (abi.encodePacked (context, prefundAmount, prefunder, prefundContext), validationData);
82
+ }
83
+
84
+ /**
85
+ * @dev Prefunds the `userOp` by charging the maximum possible gas cost (`maxCost`) in ERC-20 `token`.
86
+ *
87
+ * The `token` and `tokenPrice` is obtained from the {_fetchDetails} function and are funded by the `prefunder_`,
88
+ * which is the user operation sender by default. The `prefundAmount` is calculated using {_erc20Cost}.
89
+ *
90
+ * Returns a `prefundContext` that's passed to the {_postOp} function through its `context` return value.
91
+ *
92
+ * NOTE: Consider not reverting if the prefund fails when overriding this function. This is to avoid reverting
93
+ * during the validation phase of the user operation, which may penalize the paymaster's reputation according
94
+ * to ERC-7562 validation rules.
95
+ */
96
+ function _prefund (
97
+ PackedUserOperation calldata userOp ,
98
+ bytes32 /* userOpHash */ ,
99
+ IERC20 token ,
100
+ uint256 tokenPrice ,
101
+ address prefunder_ ,
102
+ uint256 maxCost
103
+ ) internal virtual returns (bool prefunded , uint256 prefundAmount , address prefunder , bytes memory prefundContext ) {
104
+ uint256 feePerGas = userOp.maxFeePerGas ();
105
+ uint256 _prefundAmount = _erc20Cost (maxCost, feePerGas, tokenPrice);
106
+ return (token.trySafeTransferFrom (prefunder_, address (this ), _prefundAmount), _prefundAmount, prefunder_, "" );
71
107
}
72
108
73
- /// @inheritdoc PaymasterCore
109
+ /**
110
+ * @dev Attempts to refund the user operation after execution. See {_refund}.
111
+ *
112
+ * Reverts with {PaymasterERC20FailedRefund} if the refund fails.
113
+ *
114
+ * IMPORTANT: This function may revert after the user operation has been executed without
115
+ * reverting the user operation itself. Consider implementing a mechanism to handle
116
+ * this case gracefully.
117
+ */
74
118
function _postOp (
75
119
PostOpMode /* mode */ ,
76
120
bytes calldata context ,
@@ -79,36 +123,54 @@ abstract contract PaymasterERC20 is PaymasterCore {
79
123
) internal virtual override {
80
124
bytes32 userOpHash = bytes32 (context[0x00 :0x20 ]);
81
125
IERC20 token = IERC20 (address (bytes20 (context[0x20 :0x34 ])));
82
- uint256 prefundAmount = uint256 (bytes32 (context[0x34 :0x54 ]));
83
- uint256 tokenPrice = uint256 (bytes32 (context[0x54 :0x74 ]));
84
- address user = address (bytes20 (context[0x74 :0x88 ]));
85
- address guarantor = address (bytes20 (context[0x88 :0x9C ]));
86
-
87
- uint256 actualAmount = (actualGasCost +
88
- (guarantor == address (0 )).ternary (POST_OP_COST, POST_OP_COST_WITH_GUARANTOR) *
89
- actualUserOpFeePerGas).mulDiv (tokenPrice, _tokenPriceDenominator ());
90
-
91
- if (guarantor == address (0 )) {
92
- token.safeTransfer (user, prefundAmount - actualAmount);
93
- emit UserOperationSponsored (userOpHash, user, address (0 ), actualAmount, tokenPrice, false );
94
- } else if (token.trySafeTransferFrom (user, address (this ), actualAmount)) {
95
- token.safeTransfer (guarantor, prefundAmount);
96
- emit UserOperationSponsored (userOpHash, user, guarantor, actualAmount, tokenPrice, false );
97
- } else {
98
- token.safeTransfer (guarantor, prefundAmount - actualAmount);
99
- emit UserOperationSponsored (userOpHash, user, guarantor, actualAmount, tokenPrice, true );
126
+ uint256 tokenPrice = uint256 (bytes32 (context[0x34 :0x54 ]));
127
+ uint256 prefundAmount = uint256 (bytes32 (context[0x54 :0x74 ]));
128
+ address prefunder = address (bytes20 (context[0x74 :0x88 ]));
129
+ bytes calldata prefundContext = context[0x88 :];
130
+
131
+ (bool refunded , uint256 actualAmount ) = _refund (
132
+ token,
133
+ tokenPrice,
134
+ actualGasCost,
135
+ actualUserOpFeePerGas,
136
+ prefunder,
137
+ prefundAmount,
138
+ prefundContext
139
+ );
140
+ if (! refunded) {
141
+ revert PaymasterERC20FailedRefund (token, prefundAmount, actualAmount, prefundContext);
100
142
}
143
+
144
+ emit UserOperationSponsored (userOpHash, address (token), actualAmount, tokenPrice);
145
+ }
146
+
147
+ /**
148
+ * @dev Refunds any unused gas back to the user (i.e. `prefundAmount - actualAmount`) in `token`.
149
+ *
150
+ * The `actualAmount` is calculated using {_erc20Cost} and the `actualGasCost`, `actualUserOpFeePerGas`, `prefundContext`
151
+ * and the `tokenPrice` from the {_postOp}'s context.
152
+ */
153
+ function _refund (
154
+ IERC20 token ,
155
+ uint256 tokenPrice ,
156
+ uint256 actualGasCost ,
157
+ uint256 actualUserOpFeePerGas ,
158
+ address prefunder ,
159
+ uint256 prefundAmount ,
160
+ bytes calldata /* prefundContext */
161
+ ) internal virtual returns (bool refunded , uint256 actualAmount ) {
162
+ uint256 actualAmount_ = _erc20Cost (actualGasCost, actualUserOpFeePerGas, tokenPrice);
163
+ return (token.trySafeTransfer (prefunder, prefundAmount - actualAmount_), actualAmount_);
101
164
}
102
165
103
166
/**
104
- * @dev Retrieves payment details for a user operation
167
+ * @dev Retrieves payment details for a user operation.
105
168
*
106
169
* The values returned by this internal function are:
107
170
*
108
171
* * `validationData`: ERC-4337 validation data, indicating success/failure and optional time validity (`validAfter`, `validUntil`).
109
172
* * `token`: Address of the ERC-20 token used for payment to the paymaster.
110
173
* * `tokenPrice`: Price of the token in native currency, scaled by `_tokenPriceDenominator()`.
111
- * * `guarantor`: Address of an entity advancing funds if the user lacks them; receives tokens during execution or pays if the user can't.
112
174
*
113
175
* ==== Calculating the token price
114
176
*
@@ -121,23 +183,27 @@ abstract contract PaymasterERC20 is PaymasterCore {
121
183
* For example, suppose token is USDC ($1 with 6 decimals) and native currency is ETH (assuming $2524.86 with 18 decimals),
122
184
* then each unit (1e-6) of USDC is worth `(1 / 1e6) / ((252486 / 1e2) / 1e18) = 396061563.8094785` wei. The `_tokenPriceDenominator()`
123
185
* ensures precision by avoiding fractional value loss. (i.e. the 0.8094785 part).
124
- *
125
- * ==== Guarantor
126
- *
127
- * To support a guarantor, developers can use the `paymasterData` field to store the guarantor's address. Developers can disable
128
- * support for a guarantor by returning `address(0)`. If supported, ensure explicit consent (e.g., signature verification) to prevent
129
- * unauthorized use.
130
186
*/
131
187
function _fetchDetails (
132
188
PackedUserOperation calldata userOp ,
133
189
bytes32 userOpHash
134
- ) internal view virtual returns (uint256 validationData , IERC20 token , uint256 tokenPrice , address guarantor );
190
+ ) internal view virtual returns (uint256 validationData , IERC20 token , uint256 tokenPrice );
191
+
192
+ /// @dev Over-estimates the cost of the post-operation logic.
193
+ function _postOpCost () internal view virtual returns (uint256 ) {
194
+ return 30_000 ;
195
+ }
135
196
136
- /// @dev Denominator used for interpreting the `tokenPrice` returned by {_fetchDetails} as "fixed point".
197
+ /// @dev Denominator used for interpreting the `tokenPrice` returned by {_fetchDetails} as "fixed point" in {_erc20Cost} .
137
198
function _tokenPriceDenominator () internal view virtual returns (uint256 ) {
138
199
return 1e18 ;
139
200
}
140
201
202
+ /// @dev Calculates the cost of the user operation in ERC-20 tokens.
203
+ function _erc20Cost (uint256 cost , uint256 feePerGas , uint256 tokenPrice ) internal view virtual returns (uint256 ) {
204
+ return (cost + _postOpCost () * feePerGas).mulDiv (tokenPrice, _tokenPriceDenominator ());
205
+ }
206
+
141
207
/// @dev Public function that allows the withdrawer to extract ERC-20 tokens resulting from gas payments.
142
208
function withdrawTokens (IERC20 token , address recipient , uint256 amount ) public virtual onlyWithdrawer {
143
209
if (amount == type (uint256 ).max) amount = token.balanceOf (address (this ));
0 commit comments