-
Notifications
You must be signed in to change notification settings - Fork 522
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
SimpleERC20Paymaster - compatible with our bundler
- Loading branch information
1 parent
effd208
commit 68ce6b7
Showing
3 changed files
with
446 additions
and
0 deletions.
There are no files selected for viewing
83 changes: 83 additions & 0 deletions
83
contracts/prebuilts/account/paymaster/SimpleERC20Paymaster.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
pragma solidity ^0.8.11; | ||
|
||
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
import { SafeTransferLib } from "../utils/SafeTransferLib.sol"; | ||
|
||
import "../utils/BasePaymaster.sol"; | ||
|
||
contract SimpleERC20Paymaster is BasePaymaster { | ||
using UserOperationLib for UserOperation; | ||
using SafeERC20 for IERC20; | ||
|
||
IERC20 public token; | ||
uint256 public tokenPricePerOp; | ||
|
||
event UserOperationSponsored(address indexed user, uint256 actualTokenNeeded, uint256 actualGasCost); | ||
|
||
constructor(IEntryPoint _entryPoint, IERC20 _token, uint256 _tokenPricePerOp) BasePaymaster(_entryPoint) { | ||
token = _token; | ||
tokenPricePerOp = _tokenPricePerOp; | ||
} | ||
|
||
function setTokenPricePerOp(uint256 _tokenPricePerOp) external onlyOwner { | ||
tokenPricePerOp = _tokenPricePerOp; | ||
} | ||
|
||
function withdrawToken(address to, uint256 amount) external onlyOwner { | ||
SafeTransferLib.safeTransfer(address(token), to, amount); | ||
} | ||
|
||
function _validatePaymasterUserOp( | ||
UserOperation calldata userOp, | ||
bytes32, | ||
uint256 | ||
) internal override returns (bytes memory context, uint256 validationResult) { | ||
unchecked { | ||
uint256 cachedTokenPrice = tokenPricePerOp; | ||
require(cachedTokenPrice != 0, "SPM : price not set"); | ||
uint256 length = userOp.paymasterAndData.length - 20; | ||
// 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdf is the mask for the last 6 bits 011111 which mean length should be 100000(32) || 000000(0) | ||
require( | ||
length & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdf == 0, | ||
"SPM : invalid data length" | ||
); | ||
// NOTE: we assumed that nativeAsset's decimals is 18 | ||
if (length == 32) { | ||
require( | ||
cachedTokenPrice <= uint256(bytes32(userOp.paymasterAndData[20:52])), | ||
"SPM : token amount too high" | ||
); | ||
} | ||
SafeTransferLib.safeTransferFrom(address(token), userOp.sender, address(this), cachedTokenPrice); | ||
context = abi.encodePacked(cachedTokenPrice, userOp.sender); | ||
// No return here since validationData == 0 and we have context saved in memory | ||
validationResult = 0; | ||
} | ||
} | ||
|
||
/// @notice Performs post-operation tasks, such as updating the token price and refunding excess tokens. | ||
/// @dev This function is called after a user operation has been executed or reverted. | ||
/// @param mode The post-operation mode (either successful 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 mode, bytes calldata context, uint256 actualGasCost) internal override { | ||
if (mode == PostOpMode.postOpReverted) { | ||
return; // Do nothing here to not revert the whole bundle and harm reputation | ||
} | ||
unchecked { | ||
uint256 actualTokenNeeded = tokenPricePerOp; | ||
if (uint256(bytes32(context[0:32])) > actualTokenNeeded) { | ||
// If the initially provided token amount is greater than the actual amount needed, refund the difference | ||
SafeTransferLib.safeTransfer( | ||
address(token), | ||
address(bytes20(context[32:52])), | ||
uint256(bytes32(context[0:32])) - actualTokenNeeded | ||
); | ||
} // If the token amount is not greater than the actual amount needed, no refund occurs | ||
|
||
emit UserOperationSponsored(address(bytes20(context[32:52])), actualTokenNeeded, actualGasCost); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
pragma solidity ^0.8.12; | ||
|
||
|
||
/* solhint-disable reason-string */ | ||
|
||
import "@openzeppelin/contracts/access/Ownable.sol"; | ||
import "../interface/IPaymaster.sol"; | ||
import "../interface/IEntryPoint.sol"; | ||
import "../utils/Helpers.sol"; | ||
|
||
/** | ||
* Helper class for creating a paymaster. | ||
* provides helper methods for staking. | ||
* validates that the postOp is called only by the entryPoint | ||
*/ | ||
abstract contract BasePaymaster is IPaymaster, Ownable { | ||
|
||
IEntryPoint immutable public entryPoint; | ||
|
||
constructor(IEntryPoint _entryPoint) { | ||
entryPoint = _entryPoint; | ||
} | ||
|
||
/// @inheritdoc IPaymaster | ||
function validatePaymasterUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost) | ||
external override returns (bytes memory context, uint256 validationData) { | ||
_requireFromEntryPoint(); | ||
return _validatePaymasterUserOp(userOp, userOpHash, maxCost); | ||
} | ||
|
||
function _validatePaymasterUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost) | ||
internal virtual returns (bytes memory context, uint256 validationData); | ||
|
||
/// @inheritdoc IPaymaster | ||
function postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost) external override { | ||
_requireFromEntryPoint(); | ||
_postOp(mode, context, actualGasCost); | ||
} | ||
|
||
/** | ||
* post-operation handler. | ||
* (verified to be called only through the entryPoint) | ||
* @dev if subclass returns a non-empty context from validatePaymasterUserOp, it must also implement this method. | ||
* @param mode enum with the following options: | ||
* opSucceeded - user operation succeeded. | ||
* opReverted - user op reverted. still has to pay for gas. | ||
* postOpReverted - user op succeeded, but caused postOp (in mode=opSucceeded) to revert. | ||
* Now this is the 2nd call, after user's op was deliberately reverted. | ||
* @param context - the context value returned by validatePaymasterUserOp | ||
* @param actualGasCost - actual gas used so far (without this postOp call). | ||
*/ | ||
function _postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost) internal virtual { | ||
|
||
(mode,context,actualGasCost); // unused params | ||
// subclass must override this method if validatePaymasterUserOp returns a context | ||
revert("must override"); | ||
} | ||
|
||
/** | ||
* add a deposit for this paymaster, used for paying for transaction fees | ||
*/ | ||
function deposit() public payable { | ||
entryPoint.depositTo{value : msg.value}(address(this)); | ||
} | ||
|
||
/** | ||
* withdraw value from the deposit | ||
* @param withdrawAddress target to send to | ||
* @param amount to withdraw | ||
*/ | ||
function withdrawTo(address payable withdrawAddress, uint256 amount) public onlyOwner { | ||
entryPoint.withdrawTo(withdrawAddress, amount); | ||
} | ||
/** | ||
* add stake for this paymaster. | ||
* This method can also carry eth value to add to the current stake. | ||
* @param unstakeDelaySec - the unstake delay for this paymaster. Can only be increased. | ||
*/ | ||
function addStake(uint32 unstakeDelaySec) external payable onlyOwner { | ||
entryPoint.addStake{value : msg.value}(unstakeDelaySec); | ||
} | ||
|
||
/** | ||
* return current paymaster's deposit on the entryPoint. | ||
*/ | ||
function getDeposit() public view returns (uint256) { | ||
return entryPoint.balanceOf(address(this)); | ||
} | ||
|
||
/** | ||
* unlock the stake, in order to withdraw it. | ||
* The paymaster can't serve requests once unlocked, until it calls addStake again | ||
*/ | ||
function unlockStake() external onlyOwner { | ||
entryPoint.unlockStake(); | ||
} | ||
|
||
/** | ||
* withdraw the entire paymaster's stake. | ||
* stake must be unlocked first (and then wait for the unstakeDelay to be over) | ||
* @param withdrawAddress the address to send withdrawn value. | ||
*/ | ||
function withdrawStake(address payable withdrawAddress) external onlyOwner { | ||
entryPoint.withdrawStake(withdrawAddress); | ||
} | ||
|
||
/// validate the call is made from a valid entrypoint | ||
function _requireFromEntryPoint() internal virtual { | ||
require(msg.sender == address(entryPoint), "Sender not EntryPoint"); | ||
} | ||
} |
Oops, something went wrong.