Skip to content

Commit

Permalink
SimpleERC20Paymaster - compatible with our bundler
Browse files Browse the repository at this point in the history
  • Loading branch information
0xFirekeeper committed Feb 15, 2024
1 parent effd208 commit 68ce6b7
Show file tree
Hide file tree
Showing 3 changed files with 446 additions and 0 deletions.
83 changes: 83 additions & 0 deletions contracts/prebuilts/account/paymaster/SimpleERC20Paymaster.sol
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);
}
}
}
112 changes: 112 additions & 0 deletions contracts/prebuilts/account/utils/BasePaymaster.sol
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");
}
}
Loading

0 comments on commit 68ce6b7

Please sign in to comment.