```solidity=
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
import "@account-abstraction/contracts/core/BasePaymaster.sol";
import "@account-abstraction/contracts/core/Helpers.sol";
import "@account-abstraction/contracts/interfaces/UserOperation.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import "./interfaces/IOracle.sol";
import "@account-abstraction/contracts/core/EntryPoint.sol";
/// @title PimlicoERC20Paymaster
/// @notice An ERC-4337 Paymaster contract by Pimlico which is able to sponsor gas fees in exchange for ERC-20 tokens.
/// The contract refunds excess tokens if the actual gas cost is lower than the initially provided amount.
/// It also allows updating price configuration and withdrawing tokens by the contract owner.
/// The contract uses an Oracle to fetch the latest token prices.
/// @dev Inherits from BasePaymaster.
contract PimlicoERC20Paymaster is BasePaymaster {
uint256 public constant priceDenominator = 1e6;
uint256 public constant REFUND_POSTOP_COST = 40000; // Estimated gas cost for refunding tokens after the transaction is completed
IERC20 public immutable token; // The ERC20 token used for transaction fee payments
IOracle public immutable oracle; // The Oracle contract used to fetch the latest token prices
uint192 public previousPrice; // The cached token price from the Oracle
uint32 public priceMarkup; // The price markup percentage applied to the token price (1e6 = 100%)
uint32 public priceUpdateThreshold; // The price update threshold percentage that triggers a price update (1e6 = 100%)
event ConfigUpdated(uint32 priceMarkup, uint32 updateThreshold);
/// @notice Initializes the PimlicoERC20Paymaster contract with the given parameters.
/// @param _token The ERC20 token used for transaction fee payments.
/// @param _entryPoint The EntryPoint contract used in the Account Abstraction infrastructure.
/// @param _oracle The Oracle contract used to fetch the latest token prices.
constructor(IERC20 _token, IEntryPoint _entryPoint, IOracle _oracle, address _owner) BasePaymaster(_entryPoint) {
token = _token;
oracle = _oracle;
priceMarkup = 110e4; // 110% 1e6 = 100%
priceUpdateThreshold = 25e3; // 2.5% 1e6 = 100%
transferOwnership(_owner);
}
/// @notice Updates the price markup and price update threshold configurations.
/// @param _priceMarkup The new price markup percentage (1e6 = 100%).
/// @param _updateThreshold The new price update threshold percentage (1e6 = 100%).
function updateConfig(uint32 _priceMarkup, uint32 _updateThreshold) external onlyOwner {
require(_priceMarkup <= 120e4, "price markup too high");
require(_priceMarkup >= 1e6, "price markeup too low");
priceMarkup = _priceMarkup;
priceUpdateThreshold = _updateThreshold;
emit ConfigUpdated(_priceMarkup, _updateThreshold);
}
/// @notice Allows the contract owner to withdraw a specified amount of tokens from the contract.
/// @param to The address to transfer the tokens to.
/// @param amount The amount of tokens to transfer.
function withdrawToken(address to, uint256 amount) external onlyOwner {
token.transfer(to, amount);
}
/// @notice Updates the token price by fetching the latest price from the Oracle.
function updatePrice() external {
// This function updates the cached ERC20/ETH price ratio
(, int256 answer,,,) = oracle.latestRoundData();
previousPrice = uint192(int192(answer));
}
/// @notice Validates a paymaster user operation and calculates the required token amount for the transaction.
/// @param userOp The user operation data.
/// @param requiredPreFund The amount of tokens required for pre-funding.
/// @return context The context containing the token amount and user sender address (if applicable).
/// @return validationResult A uint256 value indicating the result of the validation (always 0 in this implementation).
function _validatePaymasterUserOp(UserOperation calldata userOp, bytes32, uint256 requiredPreFund)
internal
override
returns (bytes memory context, uint256 validationResult)
{
unchecked {
uint256 cachedPrice = previousPrice;
require(cachedPrice != 0, "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, "invalid data length"
);
uint256 tokenAmount = (
requiredPreFund + (REFUND_POSTOP_COST) * userOp.maxFeePerGas
) * priceMarkup / cachedPrice;
if (length == 32) {
require(tokenAmount <= uint256(bytes32(userOp.paymasterAndData[20:52])), "token amount too high");
}
token.transferFrom(userOp.sender, address(this), tokenAmount);
context = abi.encodePacked(tokenAmount, 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.
/// @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
}
(, int256 price,,,) = oracle.latestRoundData();
// uint32 chachedMarkup = priceMarkup;
uint32 cachedUpdateThreshold = priceUpdateThreshold;
uint192 cachedPrice = previousPrice;
unchecked {
if (
uint256(price) * priceDenominator / cachedPrice > priceDenominator + cachedUpdateThreshold
|| uint256(price) * priceDenominator / cachedPrice < priceDenominator - cachedUpdateThreshold
) {
previousPrice = uint192(int192(price));
cachedPrice = uint192(int192(price));
}
// uint256 tokenAmount = uint256(bytes32(context[0:32]));
//address sender = address(bytes20(context[32:52]));
// Refund tokens based on actual gas cost
uint256 actualTokenNeeded =
(actualGasCost + REFUND_POSTOP_COST * tx.gasprice) * priceMarkup / cachedPrice; // We use tx.gasprice here since we don't know the actual gas price used by the user
if (uint256(bytes32(context[0:32])) > actualTokenNeeded) {
// If the initially provided token amount is greater than the actual amount needed, refund the difference
token.transfer(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
}
}
}
```