-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathBiconomyTokenPaymaster.sol
634 lines (578 loc) · 26.2 KB
/
BiconomyTokenPaymaster.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
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.27;
import { ReentrancyGuardTransient } from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
import { IEntryPoint } from "account-abstraction/interfaces/IEntryPoint.sol";
import { PackedUserOperation, UserOperationLib } from "account-abstraction/core/UserOperationLib.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol";
import { BasePaymaster } from "../base/BasePaymaster.sol";
import { BiconomyTokenPaymasterErrors } from "../common/BiconomyTokenPaymasterErrors.sol";
import { IBiconomyTokenPaymaster } from "../interfaces/IBiconomyTokenPaymaster.sol";
import { IOracle } from "../interfaces/oracles/IOracle.sol";
import { TokenPaymasterParserLib } from "../libraries/TokenPaymasterParserLib.sol";
import { SignatureCheckerLib } from "solady/utils/SignatureCheckerLib.sol";
import { ECDSA as ECDSA_solady } from "solady/utils/ECDSA.sol";
import "account-abstraction/core/Helpers.sol";
import "./swaps/Uniswapper.sol";
// Todo: marked for removal
import "forge-std/console2.sol";
/**
* @title BiconomyTokenPaymaster
* @author ShivaanshK<[email protected]>
* @author livingrockrises<[email protected]>
* @notice Biconomy's Token Paymaster for Entry Point v0.7
* @dev A paymaster that allows users to pay gas fees in ERC20 tokens. The paymaster uses the precharge and refund
* model
* to handle gas remittances.
*
* Currently, the paymaster supports two modes:
* 1. EXTERNAL - Relies on a quoted token price from a trusted entity (verifyingSigner).
* 2. INDEPENDENT - Relies purely on price oracles (Chainlink and TWAP) which implement the IOracle interface. This mode
* doesn't require a signature and is "always available" to use.
*
* The paymaster's owner has full discretion over the supported tokens (for independent mode), price adjustments
* applied, and how
* to manage the assets received by the paymaster.
*/
contract BiconomyTokenPaymaster is
IBiconomyTokenPaymaster,
BasePaymaster,
ReentrancyGuardTransient,
BiconomyTokenPaymasterErrors,
Uniswapper
{
using UserOperationLib for PackedUserOperation;
using TokenPaymasterParserLib for bytes;
using SignatureCheckerLib for address;
// State variables
address public verifyingSigner; // entity used to provide external token price and markup
uint256 public unaccountedGas;
uint32 public independentPriceMarkup; // price markup used for independent mode
uint256 public priceExpiryDuration; // oracle price expiry duration
IOracle public nativeAssetToUsdOracle; // ETH -> USD price oracle
mapping(address => TokenInfo) public independentTokenDirectory; // mapping of token address => info for tokens
// supported in // independent mode
uint256 private constant _UNACCOUNTED_GAS_LIMIT = 200_000; // Limit for unaccounted gas cost
uint32 private constant _PRICE_DENOMINATOR = 1e6; // Denominator used when calculating cost with price markup
uint32 private constant _MAX_PRICE_MARKUP = 2e6; // 100% premium on price (2e6/PRICE_DENOMINATOR)
uint256 private immutable _NATIVE_TOKEN_DECIMALS;
constructor(
address owner,
address verifyingSignerArg,
IEntryPoint entryPoint,
uint256 unaccountedGasArg,
uint32 independentPriceMarkupArg, // price markup used for independent mode
uint256 priceExpiryDurationArg,
uint256 nativeAssetDecimalsArg,
IOracle nativeAssetToUsdOracleArg,
ISwapRouter uniswapRouterArg,
address wrappedNativeArg,
address[] memory independentTokensArg, // Array of token addresses supported by the paymaster in independent
// mode
IOracle[] memory oraclesArg, // Array of corresponding oracle addresses for independently supported tokens
address[] memory swappableTokens, // Array of tokens that you want swappable by the uniswapper
uint24[] memory swappableTokenPoolFeeTiers // Array of uniswap pool fee tiers for each swappable token
)
BasePaymaster(owner, entryPoint)
Uniswapper(uniswapRouterArg, wrappedNativeArg, swappableTokens, swappableTokenPoolFeeTiers)
{
_NATIVE_TOKEN_DECIMALS = nativeAssetDecimalsArg;
if (_isContract(verifyingSignerArg)) {
revert VerifyingSignerCanNotBeContract();
}
if (verifyingSignerArg == address(0)) {
revert VerifyingSignerCanNotBeZero();
}
if (unaccountedGasArg > _UNACCOUNTED_GAS_LIMIT) {
revert UnaccountedGasTooHigh();
}
if (independentPriceMarkupArg > _MAX_PRICE_MARKUP || independentPriceMarkupArg < _PRICE_DENOMINATOR) {
// Not between 0% and 100% markup
revert InvalidPriceMarkup();
}
if (independentTokensArg.length != oraclesArg.length) {
revert TokensAndInfoLengthMismatch();
}
if (nativeAssetToUsdOracleArg.decimals() != 8) {
// ETH -> USD will always have 8 decimals for Chainlink and TWAP
revert InvalidOracleDecimals();
}
require(block.timestamp >= priceExpiryDurationArg, "Price expiry duration cannot be in the past");
// Set state variables
assembly ("memory-safe") {
sstore(verifyingSigner.slot, verifyingSignerArg)
sstore(unaccountedGas.slot, unaccountedGasArg)
sstore(independentPriceMarkup.slot, independentPriceMarkupArg)
sstore(priceExpiryDuration.slot, priceExpiryDurationArg)
sstore(nativeAssetToUsdOracle.slot, nativeAssetToUsdOracleArg)
}
// Populate the tokenToOracle mapping
for (uint256 i = 0; i < independentTokensArg.length; i++) {
if (oraclesArg[i].decimals() != 8) {
// Token -> USD will always have 8 decimals
revert InvalidOracleDecimals();
}
independentTokenDirectory[independentTokensArg[i]] =
TokenInfo(oraclesArg[i], 10 ** IERC20Metadata(independentTokensArg[i]).decimals());
}
}
receive() external payable {
// no need to emit an event here
}
/**
* @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 payable onlyOwner nonReentrant {
_withdrawERC20(token, target, amount);
}
/**
* @dev Withdraw ETH from the paymaster
* @param recipient The address to send the ETH to
* @param amount The amount of ETH to withdraw
*/
function withdrawEth(address payable recipient, uint256 amount) external payable onlyOwner nonReentrant {
(bool success,) = recipient.call{ value: amount }("");
if (!success) {
revert WithdrawalFailed();
}
emit EthWithdrawn(recipient, amount);
}
/**
* @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
*/
function withdrawERC20Full(IERC20 token, address target) external payable onlyOwner nonReentrant {
uint256 amount = token.balanceOf(address(this));
_withdrawERC20(token, target, amount);
}
/**
* @dev pull multiple tokens out of paymaster in case they were sent to the paymaster at any point.
* @param token the tokens deposit to withdraw
* @param target address to send to
* @param amount amounts to withdraw
*/
function withdrawMultipleERC20(
IERC20[] calldata token,
address target,
uint256[] calldata amount
)
external
payable
onlyOwner
nonReentrant
{
if (token.length != amount.length) {
revert TokensAndAmountsLengthMismatch();
}
unchecked {
for (uint256 i; i < token.length;) {
_withdrawERC20(token[i], target, amount[i]);
++i;
}
}
}
/**
* @dev pull multiple tokens out of paymaster in case they were sent to the paymaster at any point.
* @param token the tokens deposit to withdraw
* @param target address to send to
*/
function withdrawMultipleERC20Full(
IERC20[] calldata token,
address target
)
external
payable
onlyOwner
nonReentrant
{
unchecked {
for (uint256 i; i < token.length;) {
uint256 amount = token[i].balanceOf(address(this));
_withdrawERC20(token[i], target, amount);
++i;
}
}
}
/**
* @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 UpdatedVerifyingSigner.
*/
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 UpdatedVerifyingSigner(oldSigner, newVerifyingSigner, msg.sender);
}
/**
* @dev Set a new unaccountedEPGasOverhead value.
* @param newUnaccountedGas The new value to be set as the unaccounted gas value
* @notice only to be called by the owner of the contract.
*/
function setUnaccountedGas(uint256 newUnaccountedGas) external payable onlyOwner {
if (newUnaccountedGas > _UNACCOUNTED_GAS_LIMIT) {
revert UnaccountedGasTooHigh();
}
uint256 oldUnaccountedGas = unaccountedGas;
assembly ("memory-safe") {
sstore(unaccountedGas.slot, newUnaccountedGas)
}
emit UpdatedUnaccountedGas(oldUnaccountedGas, newUnaccountedGas);
}
/**
* @dev Set a new priceMarkup value.
* @param newIndependentPriceMarkup The new value to be set as the price markup
* @notice only to be called by the owner of the contract.
*/
function setPriceMarkup(uint32 newIndependentPriceMarkup) external payable onlyOwner {
if (newIndependentPriceMarkup > _MAX_PRICE_MARKUP || newIndependentPriceMarkup < _PRICE_DENOMINATOR) {
// Not between 0% and 100% markup
revert InvalidPriceMarkup();
}
uint32 oldIndependentPriceMarkup = independentPriceMarkup;
assembly ("memory-safe") {
sstore(independentPriceMarkup.slot, newIndependentPriceMarkup)
}
emit UpdatedFixedPriceMarkup(oldIndependentPriceMarkup, newIndependentPriceMarkup);
}
/**
* @dev Set a new price expiry duration.
* @param newPriceExpiryDuration The new value to be set as the price expiry duration
* @notice only to be called by the owner of the contract.
*/
function setPriceExpiryDuration(uint256 newPriceExpiryDuration) external payable onlyOwner {
require(block.timestamp >= newPriceExpiryDuration, "Price expiry duration cannot be in the past");
uint256 oldPriceExpiryDuration = priceExpiryDuration;
assembly ("memory-safe") {
sstore(priceExpiryDuration.slot, newPriceExpiryDuration)
}
emit UpdatedPriceExpiryDuration(oldPriceExpiryDuration, newPriceExpiryDuration);
}
/**
* @dev Update the native oracle address
* @param oracle The new native asset oracle
* @notice only to be called by the owner of the contract.
*/
function setNativeAssetToUsdOracle(IOracle oracle) external payable onlyOwner {
if (oracle.decimals() != 8) {
// Native -> USD will always have 8 decimals
revert InvalidOracleDecimals();
}
IOracle oldNativeAssetToUsdOracle = nativeAssetToUsdOracle;
assembly ("memory-safe") {
sstore(nativeAssetToUsdOracle.slot, oracle)
}
emit UpdatedNativeAssetOracle(oldNativeAssetToUsdOracle, oracle);
}
/**
* @dev Set or update a TokenInfo entry in the independentTokenDirectory mapping.
* @param tokenAddress The token address to add or update in directory
* @param oracle The oracle to use for the specified token
* @notice only to be called by the owner of the contract.
*/
function addToTokenDirectory(address tokenAddress, IOracle oracle) external payable onlyOwner {
if (oracle.decimals() != 8) {
// Token -> USD will always have 8 decimals
revert InvalidOracleDecimals();
}
uint8 decimals = IERC20Metadata(tokenAddress).decimals();
independentTokenDirectory[tokenAddress] = TokenInfo(oracle, 10 ** decimals);
emit AddedToTokenDirectory(tokenAddress, oracle, decimals);
}
/**
* @dev Remove a token from the independentTokenDirectory mapping.
* @param tokenAddress The token address to remove from directory
* @notice only to be called by the owner of the contract.
*/
function removeFromTokenDirectory(address tokenAddress) external payable onlyOwner {
delete independentTokenDirectory[tokenAddress];
emit RemovedFromTokenDirectory(tokenAddress );
}
/**
* @dev Update or add a swappable token to the Uniswapper
* @param tokenAddresses The token address to add/update to/for uniswapper
* @param poolFeeTiers The pool fee tiers for the corresponding token address to use
* @notice only to be called by the owner of the contract.
*/
function updateSwappableTokens(
address[] memory tokenAddresses,
uint24[] memory poolFeeTiers
)
external
payable
onlyOwner
{
if (tokenAddresses.length != poolFeeTiers.length) {
revert TokensAndPoolsLengthMismatch();
}
for (uint256 i = 0; i < tokenAddresses.length; ++i) {
_setTokenPool(tokenAddresses[i], poolFeeTiers[i]);
}
emit SwappableTokensAdded(tokenAddresses);
}
/**
* @dev Swap a token in the paymaster for ETH and deposit the amount received into the entry point
* @param tokenAddress The token address of the token to swap
* @param tokenAmount The amount of the token to swap
* @param minEthAmountRecevied The minimum amount of ETH amount recevied post-swap
* @notice only to be called by the owner of the contract.
*/
function swapTokenAndDeposit(
address tokenAddress,
uint256 tokenAmount,
uint256 minEthAmountRecevied
)
external
payable
nonReentrant
{
// Swap tokens for WETH
uint256 amountOut = _swapTokenToWeth(tokenAddress, tokenAmount, minEthAmountRecevied);
if(amountOut > 0) {
// Unwrap WETH to ETH
_unwrapWeth(amountOut);
// Deposit ETH into EP
entryPoint.depositTo{ value: amountOut }(address(this));
}
emit TokensSwappedAndRefilledEntryPoint(tokenAddress, tokenAmount, amountOut, msg.sender);
}
/**
* Add a deposit in native currency for this paymaster, used for paying for transaction fees.
* This is ideally done by the entity who is managing the received ERC20 gas tokens.
*/
function deposit() public payable virtual override nonReentrant {
entryPoint.depositTo{ value: msg.value }(address(this));
}
/**
* @dev Withdraws the specified amount of gas tokens from the paymaster's balance and transfers them to the
* specified address.
* @param withdrawAddress The address to which the gas tokens should be transferred.
* @param amount The amount of gas tokens to withdraw.
*/
function withdrawTo(address payable withdrawAddress, uint256 amount) public override onlyOwner nonReentrant {
if (withdrawAddress == address(0)) revert CanNotWithdrawToZeroAddress();
entryPoint.withdrawTo(withdrawAddress, amount);
}
/**
* 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,
uint48 validUntil,
uint48 validAfter,
address tokenAddress,
uint256 tokenPrice,
uint32 externalPriceMarkup
)
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),
validUntil,
validAfter,
tokenAddress,
tokenPrice,
externalPriceMarkup
)
);
}
/**
* @dev Validate a user operation.
* This method is abstract in BasePaymaster and must be implemented in derived contracts.
* @param userOp The user operation.
* @param userOpHash The hash of the user operation.
* @param maxCost The maximum cost of the user operation.
*/
function _validatePaymasterUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
uint256 maxCost
)
internal
override
returns (bytes memory context, uint256 validationData)
{
(PaymasterMode mode, bytes memory modeSpecificData) = userOp.paymasterAndData.parsePaymasterAndData();
if (uint8(mode) > 1) {
revert InvalidPaymasterMode();
}
// callGasLimit + paymasterPostOpGas
uint256 maxPenalty = (
(
uint128(uint256(userOp.accountGasLimits))
+ uint128(bytes16(userOp.paymasterAndData[_PAYMASTER_POSTOP_GAS_OFFSET:_PAYMASTER_DATA_OFFSET]))
) * 10 * userOp.unpackMaxFeePerGas()
) / 100;
if (mode == PaymasterMode.EXTERNAL) {
// Use the price and other params specified in modeSpecificData by the verifyingSigner
// Useful for supporting tokens which don't have oracle support
(
uint48 validUntil,
uint48 validAfter,
address tokenAddress,
uint256 tokenPrice, // NotE: what backend should pass is token/native * 10^token decimals
uint32 externalPriceMarkup,
bytes memory signature
) = modeSpecificData.parseExternalModeSpecificData();
if (signature.length != 64 && signature.length != 65) {
revert InvalidSignatureLength();
}
bool validSig = verifyingSigner.isValidSignatureNow(
ECDSA_solady.toEthSignedMessageHash(
getHash(userOp, validUntil, validAfter, tokenAddress, tokenPrice, externalPriceMarkup)
),
signature
);
//don't revert on signature failure: return SIG_VALIDATION_FAILED
if (!validSig) {
return ("", _packValidationData(true, validUntil, validAfter));
}
if (externalPriceMarkup > _MAX_PRICE_MARKUP || externalPriceMarkup < _PRICE_DENOMINATOR) {
revert InvalidPriceMarkup();
}
uint256 tokenAmount;
// Review
{
uint256 maxFeePerGas = UserOperationLib.unpackMaxFeePerGas(userOp);
tokenAmount = ((maxCost + maxPenalty + (unaccountedGas * maxFeePerGas)) * externalPriceMarkup * tokenPrice)
/ (_NATIVE_TOKEN_DECIMALS * _PRICE_DENOMINATOR);
}
// Transfer full amount to this address. Unused amount will be refunded in postOP
SafeTransferLib.safeTransferFrom(tokenAddress, userOp.sender, address(this), tokenAmount);
// deduct max penalty from the token amount we pass to the postOp
// so we don't refund it at postOp
context = abi.encode(userOp.sender, tokenAddress, tokenAmount-((maxPenalty*tokenPrice*externalPriceMarkup)/(1e18*_PRICE_DENOMINATOR)), tokenPrice, externalPriceMarkup, userOpHash);
validationData = _packValidationData(false, validUntil, validAfter);
} else if (mode == PaymasterMode.INDEPENDENT) {
// Use only oracles for the token specified in modeSpecificData
if (modeSpecificData.length != 20) {
revert InvalidTokenAddress();
}
// Get address for token used to pay
address tokenAddress = modeSpecificData.parseIndependentModeSpecificData();
uint256 tokenPrice = _getPrice(tokenAddress);
if(tokenPrice == 0) {
revert TokenNotSupported();
}
uint256 tokenAmount;
// TODO: Account for penalties here
{
// Calculate token amount to precharge
uint256 maxFeePerGas = UserOperationLib.unpackMaxFeePerGas(userOp);
tokenAmount = ((maxCost + maxPenalty + (unaccountedGas * maxFeePerGas)) * independentPriceMarkup * tokenPrice)
/ (_NATIVE_TOKEN_DECIMALS * _PRICE_DENOMINATOR);
}
// Transfer full amount to this address. Unused amount will be refunded in postOP
SafeTransferLib.safeTransferFrom(tokenAddress, userOp.sender, address(this), tokenAmount);
context =
abi.encode(userOp.sender, tokenAddress, tokenAmount-((maxPenalty*tokenPrice*independentPriceMarkup)/(1e18*_PRICE_DENOMINATOR)), tokenPrice, independentPriceMarkup, userOpHash);
validationData = 0; // Validation success and price is valid indefinetly
}
}
/**
* @dev Post-operation handler.
* This method is abstract in BasePaymaster and must be implemented in derived contracts.
* @param context The context value returned by validatePaymasterUserOp.
* @param actualGasCost Actual gas used so far (excluding this postOp call).
* @param actualUserOpFeePerGas The gas price this UserOp pays.
*/
function _postOp(
PostOpMode,
bytes calldata context,
uint256 actualGasCost,
uint256 actualUserOpFeePerGas
)
internal
override
{
// Decode context data
(
address userOpSender,
address tokenAddress,
uint256 prechargedAmount,
uint256 tokenPrice,
uint32 appliedPriceMarkup,
bytes32 userOpHash
) = abi.decode(context, (address, address, uint256, uint256, uint32, bytes32));
// Calculate the actual cost in tokens based on the actual gas cost and the token price
uint256 actualTokenAmount = (
(actualGasCost + (unaccountedGas * actualUserOpFeePerGas)) * appliedPriceMarkup * tokenPrice
) / (_NATIVE_TOKEN_DECIMALS * _PRICE_DENOMINATOR);
if (prechargedAmount > actualTokenAmount) {
// If the user was overcharged, refund the excess tokens
uint256 refundAmount = prechargedAmount - actualTokenAmount;
SafeTransferLib.safeTransfer(tokenAddress, userOpSender, refundAmount);
emit TokensRefunded(userOpSender, tokenAddress, refundAmount, userOpHash);
}
// Todo: Review events and what we need to emit.
emit PaidGasInTokens(
userOpSender, tokenAddress, actualGasCost, actualTokenAmount, appliedPriceMarkup, tokenPrice, userOpHash
);
}
/// @notice Fetches the latest token price.
/// @return price The latest token price fetched from the oracles.
function _getPrice(address tokenAddress) internal view returns (uint256 price) {
// Fetch token information from directory
TokenInfo memory tokenInfo = independentTokenDirectory[tokenAddress];
if (address(tokenInfo.oracle) == address(0)) {
// If oracle not set, token isn't supported
revert TokenNotSupported();
}
// Calculate price by using token and native oracle
uint256 tokenPrice = _fetchPrice(tokenInfo.oracle);
uint256 nativeAssetPrice = _fetchPrice(nativeAssetToUsdOracle);
// Adjust to token decimals
price = (nativeAssetPrice * tokenInfo.decimals) / tokenPrice;
}
/// @notice Fetches the latest price from the given oracle.
/// @dev This function is used to get the latest price from the tokenOracle or nativeAssetToUsdOracle.
/// @param oracle The oracle contract to fetch the price from.
/// @return price The latest price fetched from the oracle.
/// Note: We could do this using oracle aggregator, so we can also use Pyth. or Twap based oracle and just not chainlink.
function _fetchPrice(IOracle oracle) internal view returns (uint256 price) {
(, int256 answer,, uint256 updatedAt,) = oracle.latestRoundData();
if (answer <= 0) {
revert OraclePriceNotPositive();
}
if (updatedAt < block.timestamp - priceExpiryDuration) {
revert OraclePriceExpired();
}
price = uint256(answer);
}
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);
}
}