-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathBiconomySponsorshipPaymaster.sol
403 lines (363 loc) · 16.3 KB
/
BiconomySponsorshipPaymaster.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.27;
/* solhint-disable reason-string */
import "../base/BasePaymaster.sol";
import "account-abstraction/core/UserOperationLib.sol";
import "account-abstraction/core/Helpers.sol";
import { SignatureCheckerLib } from "solady/utils/SignatureCheckerLib.sol";
import { ECDSA as ECDSA_solady } from "solady/utils/ECDSA.sol";
import { BiconomySponsorshipPaymasterErrors } from "../common/BiconomySponsorshipPaymasterErrors.sol";
import { ReentrancyGuardTransient } from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol";
import { IBiconomySponsorshipPaymaster } from "../interfaces/IBiconomySponsorshipPaymaster.sol";
/**
* @title BiconomySponsorshipPaymaster
* @author livingrockrises<[email protected]>
* @author ShivaanshK<[email protected]>
* @notice Based on Infinitism's 'VerifyingPaymaster' contract
* @dev This contract is used to sponsor the transaction fees of the user operations
* Uses a verifying signer to provide the signature if predetermined conditions are met
* regarding the user operation calldata. Also this paymaster is Singleton in nature which
* means multiple Dapps/Wallet clients willing to sponsor the transactions can share this paymaster.
* Maintains it's own accounting of the gas balance for each Dapp/Wallet client
* and Manages it's own deposit on the EntryPoint.
*/
// @Todo: Add more methods in interface
contract BiconomySponsorshipPaymaster is
BasePaymaster,
ReentrancyGuardTransient,
BiconomySponsorshipPaymasterErrors,
IBiconomySponsorshipPaymaster
{
using UserOperationLib for PackedUserOperation;
using SignatureCheckerLib for address;
using ECDSA_solady for bytes32;
address public verifyingSigner;
address public feeCollector;
uint256 public unaccountedGas;
uint256 public paymasterIdWithdrawalDelay;
uint256 public minDeposit;
// Denominator to prevent precision errors when applying price markup
uint256 private constant _PRICE_DENOMINATOR = 1e6;
// Offset in PaymasterAndData to get to PAYMASTER_ID_OFFSET
uint256 private constant _PAYMASTER_ID_OFFSET = _PAYMASTER_DATA_OFFSET;
// Limit for unaccounted gas cost
// Review cap
uint256 private constant _UNACCOUNTED_GAS_LIMIT = 100_000;
mapping(address => uint256) public paymasterIdBalances;
mapping(address => bool) internal trustedPaymasterIds;
mapping (address paymasterId => WithdrawalRequest request) requests;
constructor(
address owner,
IEntryPoint entryPointArg,
address verifyingSignerArg,
address feeCollectorArg,
uint256 unaccountedGasArg,
uint256 _paymasterIdWithdrawalDelay,
uint256 _minDeposit
)
BasePaymaster(owner, entryPointArg)
{
_checkConstructorArgs(verifyingSignerArg, feeCollectorArg, unaccountedGasArg);
assembly ("memory-safe") {
sstore(verifyingSigner.slot, verifyingSignerArg)
}
feeCollector = feeCollectorArg;
unaccountedGas = unaccountedGasArg;
paymasterIdWithdrawalDelay = _paymasterIdWithdrawalDelay;
minDeposit = _minDeposit;
}
receive() external payable {
emit Received(msg.sender, msg.value);
}
/**
* @dev Add a deposit for this paymaster and given paymasterId (Dapp Depositor address), used for paying for
* transaction fees
* @param paymasterId dapp identifier for which deposit is being made
*/
function depositFor(address paymasterId) external payable nonReentrant {
if (paymasterId == address(0)) revert PaymasterIdCanNotBeZero();
if (msg.value == 0) revert DepositCanNotBeZero();
if(paymasterIdBalances[paymasterId] + msg.value < minDeposit)
revert LowDeposit();
paymasterIdBalances[paymasterId] += msg.value;
entryPoint.depositTo{ value: msg.value }(address(this));
emit GasDeposited(paymasterId, msg.value);
}
/**
* @dev Set a new verifying signer address.
* Can only be called by the owner of the contract.
* @param newVerifyingSigner The new address to be set as the verifying signer.
* @notice If _newVerifyingSigner is set to zero address, it will revert with an error.
* After setting the new signer address, it will emit an event VerifyingSignerChanged.
*/
function setSigner(address newVerifyingSigner) external payable onlyOwner {
if (_isContract(newVerifyingSigner)) revert VerifyingSignerCanNotBeContract();
if (newVerifyingSigner == address(0)) {
revert VerifyingSignerCanNotBeZero();
}
address oldSigner = verifyingSigner;
assembly ("memory-safe") {
sstore(verifyingSigner.slot, newVerifyingSigner)
}
emit VerifyingSignerChanged(oldSigner, newVerifyingSigner, msg.sender);
}
/**
* @dev Set a new fee collector address.
* Can only be called by the owner of the contract.
* @param newFeeCollector The new address to be set as the fee collector.
* @notice If _newFeeCollector is set to zero address, it will revert with an error.
* After setting the new fee collector address, it will emit an event FeeCollectorChanged.
*/
function setFeeCollector(address newFeeCollector) external payable override onlyOwner {
if (_isContract(newFeeCollector)) revert FeeCollectorCanNotBeContract();
if (newFeeCollector == address(0)) revert FeeCollectorCanNotBeZero();
address oldFeeCollector = feeCollector;
feeCollector = newFeeCollector;
emit FeeCollectorChanged(oldFeeCollector, newFeeCollector, msg.sender);
}
/**
* @dev Set a new unaccountedGas value.
* @param value The new value to be set as the unaccountedGas.
* @notice only to be called by the owner of the contract.
*/
function setUnaccountedGas(uint256 value) external payable onlyOwner {
if (value > _UNACCOUNTED_GAS_LIMIT) {
revert UnaccountedGasTooHigh();
}
uint256 oldValue = unaccountedGas;
unaccountedGas = value;
emit UnaccountedGasChanged(oldValue, value);
}
/**
* @dev Override the default implementation.
*/
function deposit() external payable virtual override {
revert UseDepositForInstead();
}
/**
* @dev pull tokens out of paymaster in case they were sent to the paymaster at any point.
* @param token the token deposit to withdraw
* @param target address to send to
* @param amount amount to withdraw
*/
function withdrawERC20(IERC20 token, address target, uint256 amount) external onlyOwner nonReentrant {
_withdrawERC20(token, target, amount);
}
function submitWithdrawalRequest(address withdrawAddress, uint256 amount) external {
if (withdrawAddress == address(0)) revert CanNotWithdrawToZeroAddress();
uint256 currentBalance = paymasterIdBalances[msg.sender];
if (amount > currentBalance)
revert InsufficientFundsInGasTank();
requests[msg.sender] = WithdrawalRequest({amount: amount, to: withdrawAddress, requestSubmittedTimestamp: block.timestamp });
emit WithdrawalRequestSubmitted(withdrawAddress, amount);
}
function executeWithdrawalRequest(address paymasterId) external nonReentrant {
WithdrawalRequest memory req = requests[paymasterId];
uint256 clearanceTimestamp = req.requestSubmittedTimestamp + getDelay(paymasterId);
if (block.timestamp < clearanceTimestamp)
revert RequestNotClearedYet(clearanceTimestamp);
uint256 currentBalance = paymasterIdBalances[paymasterId];
if (req.amount > currentBalance)
revert InsufficientFundsInGasTank();
paymasterIdBalances[paymasterId] = currentBalance - req.amount;
entryPoint.withdrawTo(payable(req.to), req.amount);
emit GasWithdrawn(paymasterId, req.to, req.amount);
}
function withdrawEth(address payable recipient, uint256 amount) external payable onlyOwner nonReentrant {
(bool success,) = recipient.call{ value: amount }("");
if (!success) {
revert WithdrawalFailed();
}
}
/**
* @dev get the current deposit for paymasterId (Dapp Depositor address)
* @param paymasterId dapp identifier
*/
function getBalance(address paymasterId) external view returns (uint256 balance) {
balance = paymasterIdBalances[paymasterId];
}
/**
* return the hash we're going to sign off-chain (and validate on-chain)
* this method is called by the off-chain service, to sign the request.
* it is called on-chain from the validatePaymasterUserOp, to validate the signature.
* note that this signature covers all fields of the UserOperation, except the "paymasterAndData",
* which will carry the signature itself.
*/
function getHash(
PackedUserOperation calldata userOp,
address paymasterId,
uint48 validUntil,
uint48 validAfter,
uint32 priceMarkup
)
public
view
returns (bytes32)
{
//can't use userOp.hash(), since it contains also the paymasterAndData itself.
address sender = userOp.getSender();
return keccak256(
abi.encode(
sender,
userOp.nonce,
keccak256(userOp.initCode),
keccak256(userOp.callData),
userOp.accountGasLimits,
uint256(bytes32(userOp.paymasterAndData[_PAYMASTER_VALIDATION_GAS_OFFSET:_PAYMASTER_DATA_OFFSET])),
userOp.preVerificationGas,
userOp.gasFees,
block.chainid,
address(this),
paymasterId,
validUntil,
validAfter,
priceMarkup
)
);
}
function parsePaymasterAndData(
bytes calldata paymasterAndData
)
public
pure
returns (
address paymasterId,
uint48 validUntil,
uint48 validAfter,
uint32 priceMarkup,
bytes calldata signature
)
{
unchecked {
paymasterId = address(bytes20(paymasterAndData[_PAYMASTER_ID_OFFSET:_PAYMASTER_ID_OFFSET + 20]));
validUntil = uint48(bytes6(paymasterAndData[_PAYMASTER_ID_OFFSET + 20:_PAYMASTER_ID_OFFSET + 26]));
validAfter = uint48(bytes6(paymasterAndData[_PAYMASTER_ID_OFFSET + 26:_PAYMASTER_ID_OFFSET + 32]));
priceMarkup = uint32(bytes4(paymasterAndData[_PAYMASTER_ID_OFFSET + 32:_PAYMASTER_ID_OFFSET + 36]));
signature = paymasterAndData[_PAYMASTER_ID_OFFSET + 36:];
}
}
/// @notice Performs post-operation tasks, such as deducting the sponsored gas cost from the paymasterId's balance
/// @dev This function is called after a user operation has been executed or reverted.
/// @param context The context containing the token amount and user sender address.
/// @param actualGasCost The actual gas cost of the transaction.
function _postOp(
PostOpMode,
bytes calldata context,
uint256 actualGasCost,
uint256 actualUserOpFeePerGas
)
internal
override
{
unchecked {
(address paymasterId, uint32 priceMarkup, bytes32 userOpHash, uint256 prechargedAmount) =
abi.decode(context, (address, uint32, bytes32, uint256));
// Include unaccountedGas since EP doesn't include this in actualGasCost
// unaccountedGas = postOpGas + EP overhead gas + estimated penalty
actualGasCost = actualGasCost + (unaccountedGas * actualUserOpFeePerGas);
// Apply the price markup
uint256 adjustedGasCost = (actualGasCost * priceMarkup) / _PRICE_DENOMINATOR;
if (prechargedAmount > adjustedGasCost) {
// If overcharged refund the excess
paymasterIdBalances[paymasterId] += (prechargedAmount - adjustedGasCost);
}
// Should always be true
// if (adjustedGasCost > actualGasCost) {
// Apply priceMarkup to fee collector balance
uint256 premium = adjustedGasCost - actualGasCost;
paymasterIdBalances[feeCollector] += premium;
// Review: if we should emit adjustedGasCost as well
emit PriceMarkupCollected(paymasterId, premium);
// }
// Review: emit min required information
emit GasBalanceDeducted(paymasterId, adjustedGasCost, userOpHash);
}
}
/**
* verify our external signer signed this request.
* the "paymasterAndData" is expected to be the paymaster and a signature over the entire request params
* paymasterAndData[:20] : address(this)
* paymasterAndData[52:72] : paymasterId (dappDepositor)
* paymasterAndData[72:78] : validUntil
* paymasterAndData[78:84] : validAfter
* paymasterAndData[84:88] : priceMarkup
* paymasterAndData[88:] : signature
*/
function _validatePaymasterUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
uint256 requiredPreFund
)
internal
override
returns (bytes memory context, uint256 validationData)
{
(address paymasterId, uint48 validUntil, uint48 validAfter, uint32 priceMarkup, bytes calldata signature) =
parsePaymasterAndData(userOp.paymasterAndData);
//ECDSA library supports both 64 and 65-byte long signatures.
// we only "require" it here so that the revert reason on invalid signature will be of "VerifyingPaymaster", and
// not "ECDSA"
if (signature.length != 64 && signature.length != 65) {
revert InvalidSignatureLength();
}
if (unaccountedGas > userOp.unpackPostOpGasLimit()) {
revert PostOpGasLimitTooLow();
}
bool validSig = (
(getHash(userOp, paymasterId, validUntil, validAfter, priceMarkup).toEthSignedMessageHash()).tryRecover(
signature
)
) == verifyingSigner ? true : false;
//don't revert on signature failure: return SIG_VALIDATION_FAILED
if (!validSig) {
return ("", _packValidationData(true, validUntil, validAfter));
}
// Send 1e6 for No markup
if (priceMarkup > 2e6 || priceMarkup < 1e6) {
revert InvalidPriceMarkup();
}
// Deduct the max gas cost.
uint256 effectiveCost =
((requiredPreFund + unaccountedGas * userOp.unpackMaxFeePerGas()) * priceMarkup) / _PRICE_DENOMINATOR;
if (effectiveCost > paymasterIdBalances[paymasterId]) {
revert InsufficientFundsForPaymasterId();
}
paymasterIdBalances[paymasterId] -= effectiveCost;
context = abi.encode(paymasterId, priceMarkup, userOpHash, effectiveCost);
//no need for other on-chain validation: entire UserOp should have been checked
// by the external service prior to signing it.
return (context, _packValidationData(false, validUntil, validAfter));
}
function _checkConstructorArgs(
address verifyingSignerArg,
address feeCollectorArg,
uint256 unaccountedGasArg
)
internal
view
{
if (verifyingSignerArg == address(0)) {
revert VerifyingSignerCanNotBeZero();
} else if (_isContract(verifyingSignerArg)) {
revert VerifyingSignerCanNotBeContract();
} else if (feeCollectorArg == address(0)) {
revert FeeCollectorCanNotBeZero();
} else if (_isContract(feeCollectorArg)) {
revert FeeCollectorCanNotBeContract();
} else if (unaccountedGasArg > _UNACCOUNTED_GAS_LIMIT) {
revert UnaccountedGasTooHigh();
}
}
function _withdrawERC20(IERC20 token, address target, uint256 amount) private {
if (target == address(0)) revert CanNotWithdrawToZeroAddress();
SafeTransferLib.safeTransfer(address(token), target, amount);
emit TokensWithdrawn(address(token), target, amount, msg.sender);
}
function getDelay(address paymasterId) internal view returns (uint256) {
if (trustedPaymasterIds[paymasterId])
return 0;
return paymasterIdWithdrawalDelay;
}
}