|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | + |
| 3 | +pragma solidity ^0.8.20; |
| 4 | + |
| 5 | +import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; |
| 6 | +import {IEntryPoint, IPaymaster, PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; |
| 7 | +import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; |
| 8 | + |
| 9 | +/** |
| 10 | + * @dev A simple ERC4337 paymaster implementation. This base implementation only includes the minimal logic to validate |
| 11 | + * and pay for user operations. |
| 12 | + * |
| 13 | + * Developers must implement the {PaymasterCore-_validatePaymasterUserOp} function to define the paymaster's validation |
| 14 | + * and payment logic. The `context` parameter is used to pass data between the validation and execution phases. |
| 15 | + * |
| 16 | + * The paymaster includes support to call the {IEntryPointStake} interface to manage the paymaster's deposits and stakes |
| 17 | + * through the internal functions {_deposit}, {_withdraw}, {_addStake}, {_unlockStake} and {_withdrawStake}. |
| 18 | + * |
| 19 | + * * Deposits are used to pay for user operations. |
| 20 | + * * Stakes are used to guarantee the paymaster's reputation and obtain more flexibility in accessing storage. |
| 21 | + * |
| 22 | + * NOTE: See [Paymaster's unstaked reputation rules](https://eips.ethereum.org/EIPS/eip-7562#unstaked-paymasters-reputation-rules) |
| 23 | + * for more details on the paymaster's storage access limitations. |
| 24 | + */ |
| 25 | +abstract contract PaymasterCore is IPaymaster { |
| 26 | + /// @dev Unauthorized call to the paymaster. |
| 27 | + error PaymasterUnauthorized(address sender); |
| 28 | + |
| 29 | + /// @dev Revert if the caller is not the entry point. |
| 30 | + modifier onlyEntryPoint() { |
| 31 | + _checkEntryPoint(); |
| 32 | + _; |
| 33 | + } |
| 34 | + |
| 35 | + modifier onlyWithdrawer() { |
| 36 | + _authorizeWithdraw(); |
| 37 | + _; |
| 38 | + } |
| 39 | + |
| 40 | + /// @dev Canonical entry point for the account that forwards and validates user operations. |
| 41 | + function entryPoint() public view virtual returns (IEntryPoint) { |
| 42 | + return ERC4337Utils.ENTRYPOINT; |
| 43 | + } |
| 44 | + |
| 45 | + /// @inheritdoc IPaymaster |
| 46 | + function validatePaymasterUserOp( |
| 47 | + PackedUserOperation calldata userOp, |
| 48 | + bytes32 userOpHash, |
| 49 | + uint256 maxCost |
| 50 | + ) public virtual onlyEntryPoint returns (bytes memory context, uint256 validationData) { |
| 51 | + return _validatePaymasterUserOp(userOp, userOpHash, maxCost); |
| 52 | + } |
| 53 | + |
| 54 | + /// @inheritdoc IPaymaster |
| 55 | + function postOp( |
| 56 | + PostOpMode mode, |
| 57 | + bytes calldata context, |
| 58 | + uint256 actualGasCost, |
| 59 | + uint256 actualUserOpFeePerGas |
| 60 | + ) public virtual onlyEntryPoint { |
| 61 | + _postOp(mode, context, actualGasCost, actualUserOpFeePerGas); |
| 62 | + } |
| 63 | + |
| 64 | + /** |
| 65 | + * @dev Internal validation of whether the paymaster is willing to pay for the user operation. |
| 66 | + * Returns the context to be passed to postOp and the validation data. |
| 67 | + * |
| 68 | + * The `requiredPreFund` is the amount the paymaster has to pay (in native tokens). It's calculated |
| 69 | + * as `requiredGas * userOp.maxFeePerGas`, where `required` gas can be calculated from the user operation |
| 70 | + * as `verificationGasLimit + callGasLimit + paymasterVerificationGasLimit + paymasterPostOpGasLimit + preVerificationGas` |
| 71 | + */ |
| 72 | + function _validatePaymasterUserOp( |
| 73 | + PackedUserOperation calldata userOp, |
| 74 | + bytes32 userOpHash, |
| 75 | + uint256 requiredPreFund |
| 76 | + ) internal virtual returns (bytes memory context, uint256 validationData); |
| 77 | + |
| 78 | + /** |
| 79 | + * @dev Handles post user operation execution logic. The caller must be the entry point. |
| 80 | + * |
| 81 | + * It receives the `context` returned by `_validatePaymasterUserOp`. Reverts by default |
| 82 | + * since the function is not called if no context is returned by {validatePaymasterUserOp}. |
| 83 | + * |
| 84 | + * NOTE: The `actualUserOpFeePerGas` is not `tx.gasprice`. A user operation can be bundled with other transactions |
| 85 | + * making the gas price of the user operation to differ. |
| 86 | + */ |
| 87 | + function _postOp( |
| 88 | + PostOpMode /* mode */, |
| 89 | + bytes calldata /* context */, |
| 90 | + uint256 /* actualGasCost */, |
| 91 | + uint256 /* actualUserOpFeePerGas */ |
| 92 | + ) internal virtual {} |
| 93 | + |
| 94 | + /// @dev Calls {IEntryPointStake-depositTo}. |
| 95 | + function deposit() public payable virtual { |
| 96 | + ERC4337Utils.depositTo(address(this), msg.value); |
| 97 | + } |
| 98 | + |
| 99 | + /// @dev Calls {IEntryPointStake-withdrawTo}. |
| 100 | + function withdraw(address payable to, uint256 value) public virtual onlyWithdrawer { |
| 101 | + ERC4337Utils.withdrawTo(to, value); |
| 102 | + } |
| 103 | + |
| 104 | + /// @dev Calls {IEntryPointStake-addStake}. |
| 105 | + function addStake(uint32 unstakeDelaySec) public payable virtual { |
| 106 | + ERC4337Utils.addStake(msg.value, unstakeDelaySec); |
| 107 | + } |
| 108 | + |
| 109 | + /// @dev Calls {IEntryPointStake-unlockStake}. |
| 110 | + function unlockStake() public virtual onlyWithdrawer { |
| 111 | + ERC4337Utils.unlockStake(); |
| 112 | + } |
| 113 | + |
| 114 | + /// @dev Calls {IEntryPointStake-withdrawStake}. |
| 115 | + function withdrawStake(address payable to) public virtual onlyWithdrawer { |
| 116 | + ERC4337Utils.withdrawStake(to); |
| 117 | + } |
| 118 | + |
| 119 | + /// @dev Ensures the caller is the {entrypoint}. |
| 120 | + function _checkEntryPoint() internal view virtual { |
| 121 | + address sender = msg.sender; |
| 122 | + if (sender != address(entryPoint())) { |
| 123 | + revert PaymasterUnauthorized(sender); |
| 124 | + } |
| 125 | + } |
| 126 | + |
| 127 | + /** |
| 128 | + * @dev Checks whether `msg.sender` withdraw funds stake or deposit from the entrypoint on paymaster's behalf. |
| 129 | + * |
| 130 | + * Use of an https://docs.openzeppelin.com/contracts/5.x/access-control[access control] |
| 131 | + * modifier such as {Ownable-onlyOwner} is recommended. |
| 132 | + * |
| 133 | + * ```solidity |
| 134 | + * function _authorizeUpgrade() internal onlyOwner {} |
| 135 | + * ``` |
| 136 | + */ |
| 137 | + function _authorizeWithdraw() internal virtual; |
| 138 | +} |
0 commit comments