Skip to content

Commit

Permalink
Almost final V2
Browse files Browse the repository at this point in the history
  • Loading branch information
eloi010 committed Sep 22, 2023
1 parent 47d3a6b commit 9eccad1
Show file tree
Hide file tree
Showing 6 changed files with 1,593 additions and 117 deletions.
13 changes: 11 additions & 2 deletions contracts/interfaces/OpenfortErrorsAndEvents.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@
pragma solidity ^0.8.19;

interface OpenfortErrorsAndEvents {
/// @notice Error when an address parameter is 0.
error ZeroAddressNotAllowed();
/// @notice Error when a parameter is 0.
error ZeroValueNotAllowed();

/// @notice Error when a function requires msg.value to be different than 0
error MustSendNativeToken();

// Paymaster specifics

/**
* @notice Throws when trying to withdraw more than balance available
* @param amountRequired required balance
* @param currentBalance available balance
*/
error InsufficientBalance(uint256 amountRequired, uint256 currentBalance);
}
30 changes: 23 additions & 7 deletions contracts/paymaster/BaseOpenfortPaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,21 @@ import {OpenfortErrorsAndEvents} from "../interfaces/OpenfortErrorsAndEvents.sol
/**
* Helper class for creating a paymaster.
* Provides helper methods for staking.
* Validates that the postOp is called only by the entryPoint
* Validates that the postOp is called only by the EntryPoint
*/
abstract contract BaseOpenfortPaymaster is IPaymaster, Ownable2Step {
uint256 private constant INIT_POST_OP_GAS = 35_000; // Initial value for postOpGas
IEntryPoint public immutable entryPoint;
uint256 internal postOpGas; // Reference value for gas used by the EntryPoint._handlePostOp() method.

/// @notice When the paymaster owner updates the postOpGas variable
event PostOpGasUpdated(uint256 oldPostOpGas, uint256 _newPostOpGas);

constructor(IEntryPoint _entryPoint, address _owner) {
if (address(_entryPoint) == address(0)) revert OpenfortErrorsAndEvents.ZeroAddressNotAllowed();
if (address(_entryPoint) == address(0)) revert OpenfortErrorsAndEvents.ZeroValueNotAllowed();
entryPoint = _entryPoint;
_transferOwnership(_owner);
postOpGas = INIT_POST_OP_GAS;
}

/**
Expand Down Expand Up @@ -72,11 +78,11 @@ abstract contract BaseOpenfortPaymaster is IPaymaster, Ownable2Step {
function deposit() public payable virtual;

/**
* Withdraw value from the deposit
* @param withdrawAddress target to send to
* @param amount to withdraw
* Withdraw value from the deposit.
* @param _withdrawAddress - Target to send to
* @param _amount - Amount to withdraw
*/
function withdrawTo(address payable withdrawAddress, uint256 amount) public virtual;
function withdrawTo(address payable _withdrawAddress, uint256 _amount) public virtual;

/**
* Add stake for this paymaster.
Expand All @@ -89,7 +95,7 @@ abstract contract BaseOpenfortPaymaster is IPaymaster, Ownable2Step {
}

/**
* Return current paymaster's deposit on the entryPoint.
* Return current paymaster's deposit on the EntryPoint.
*/
function getDeposit() public view returns (uint256) {
return entryPoint.balanceOf(address(this));
Expand Down Expand Up @@ -118,4 +124,14 @@ abstract contract BaseOpenfortPaymaster is IPaymaster, Ownable2Step {
function _requireFromEntryPoint() internal virtual {
require(msg.sender == address(entryPoint), "Sender not EntryPoint");
}

/**
* @dev Updates the reference value for gas used by the EntryPoint._handlePostOp() method.
* @param _newPostOpGas The new postOpGas value.
*/
function setPostOpGas(uint256 _newPostOpGas) external onlyOwner {
if (_newPostOpGas == 0) revert OpenfortErrorsAndEvents.ZeroValueNotAllowed();
emit PostOpGasUpdated(postOpGas, _newPostOpGas);
postOpGas = _newPostOpGas;
}
}
118 changes: 50 additions & 68 deletions contracts/paymaster/OpenfortPaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,23 @@ import {OpenfortErrorsAndEvents} from "../interfaces/OpenfortErrorsAndEvents.sol
* @title OpenfortPaymaster (Non-upgradeable)
* @author Eloi<eloi@openfort.xyz>
* @notice A paymaster that uses external service to decide whether to pay for the UserOp.
* The paymaster trusts an external signer to sign the transaction.
* The paymaster trusts an external signer (owner) to sign the transaction.
* The calling user must pass the UserOp to that external signer first, which performs
* whatever off-chain verification before signing the UserOp.
* It has the following features:
* - Sponsor the whole UserOp
* - Let the sender pay fees in ERC20 (both using an exchange rate per gas or per userOp)
* - Let multiple actors deposit native tokens to sponsor transactions
* - Sponsor the whole UserOp
* - Let the sender pay fees in ERC20 (both using an exchange rate per gas or per userOp)
* - All ERC20s used to sponsor gas go to the address `tokenRecipient`
*/
contract OpenfortPaymaster is BaseOpenfortPaymaster {
using ECDSA for bytes32;
using UserOperationLib for UserOperation;
using SafeERC20 for IERC20;

uint256 private constant VALID_PND_OFFSET = 20; // length of an address
uint256 private constant SIGNATURE_OFFSET = 180; // 20+48+48+64 = 180
uint256 private constant POST_OP_GAS = 35000;
uint256 private constant ADDRESS_OFFSET = 20; // length of an address
uint256 private constant SIGNATURE_OFFSET = 180; // 20+48+48+32+32 = 180

mapping(address => uint256) public depositorBalances;
address public tokenRecipient;

enum Mode {
PayForUser,
Expand All @@ -41,15 +40,19 @@ contract OpenfortPaymaster is BaseOpenfortPaymaster {

struct PolicyStrategy {
Mode paymasterMode;
address depositor;
address erc20Token;
uint256 exchangeRate;
}

/// @notice For a Paymaster, emit when a transaction has been paid using an ERC20 token
/// @notice When a transaction has been paid using an ERC20 token
event GasPaidInERC20(address erc20Token, uint256 actualGasCost, uint256 actualTokensSent);

constructor(IEntryPoint _entryPoint, address _owner) BaseOpenfortPaymaster(_entryPoint, _owner) {}
/// @notice When tokenRecipient changes
event TokenRecipientUpdated(address oldTokenRecipient, address newTokenRecipient);

constructor(IEntryPoint _entryPoint, address _owner) BaseOpenfortPaymaster(_entryPoint, _owner) {
tokenRecipient = _owner;
}

/**
* Return the hash we're going to sign off-chain (and validate on-chain)
Expand All @@ -76,29 +79,21 @@ contract OpenfortPaymaster is BaseOpenfortPaymaster {
userOp.maxFeePerGas,
userOp.maxPriorityFeePerGas
);
bytes memory secondHalf = abi.encode(
block.chainid,
address(this),
validUntil,
validAfter,
strategy.paymasterMode,
strategy.erc20Token,
strategy.exchangeRate
);
bytes memory secondHalf = abi.encode(block.chainid, address(this), validUntil, validAfter, strategy);
return keccak256(abi.encodePacked(firstHalf, secondHalf));
}

/**
* Verify that the paymaster owner has signed this request.
* The "paymasterAndData" is expected to be the paymaster and a signature over the entire request params
* paymasterAndData[:20]: address(this)
* paymasterAndData[20:148]: abi.encode(validUntil, validAfter, strategy) // 20+48+48+64
* paymasterAndData[20:148]: abi.encode(validUntil, validAfter, strategy) // 20+48+48+32+32+32
* paymasterAndData[SIGNATURE_OFFSET:]: signature
*/
function _validatePaymasterUserOp(
UserOperation calldata userOp,
bytes32, /*userOpHash*/
uint256 requiredPreFund
uint256 /*requiredPreFund*/
) internal view override returns (bytes memory context, uint256 validationData) {
(uint48 validUntil, uint48 validAfter, PolicyStrategy memory strategy, bytes calldata signature) =
parsePaymasterAndData(userOp.paymasterAndData);
Expand All @@ -110,16 +105,7 @@ contract OpenfortPaymaster is BaseOpenfortPaymaster {
return ("", Helpers._packValidationData(true, validUntil, validAfter));
}

if (requiredPreFund > paymasterIdBalances[paymasterData.paymasterId]) revert InsufficientBalance(requiredPreFund, paymasterIdBalances[paymasterData.paymasterId]);

context = abi.encode(
userOp.sender,
strategy.paymasterMode,
strategy.erc20Token,
strategy.exchangeRate,
userOp.maxFeePerGas,
userOp.maxPriorityFeePerGas
);
context = abi.encode(userOp.sender, strategy, userOp.maxFeePerGas, userOp.maxPriorityFeePerGas);

// If the parsePaymasterAndData was signed by the owner of the paymaster
// return the context and validity (validUntil, validAfter).
Expand All @@ -130,41 +116,39 @@ contract OpenfortPaymaster is BaseOpenfortPaymaster {
* For ERC20 modes (DynamicRate and FixedRate), transfer the right amount of tokens from the sender to the designated recipient
*/
function _postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost) internal override {
(
address sender,
Mode paymasterMode,
IERC20 erc20Token,
uint256 exchangeRate,
uint256 maxFeePerGas,
uint256 maxPriorityFeePerGas
) = abi.decode(context, (address, Mode, IERC20, uint256, uint256, uint256));

if (paymasterMode == Mode.DynamicRate) {
uint256 opGasPrice;
unchecked {
if (maxFeePerGas == maxPriorityFeePerGas) {
opGasPrice = maxFeePerGas;
} else {
opGasPrice = Math.min(maxFeePerGas, maxPriorityFeePerGas + block.basefee);
}
(address sender, PolicyStrategy memory strategy, uint256 maxFeePerGas, uint256 maxPriorityFeePerGas) =
abi.decode(context, (address, PolicyStrategy, uint256, uint256));

// Getting OP gas price
uint256 opGasPrice;
unchecked {
if (maxFeePerGas == maxPriorityFeePerGas) {
// Legacy mode (for networks that do not support basefee opcode)
opGasPrice = maxFeePerGas;
} else {
opGasPrice = Math.min(maxFeePerGas, maxPriorityFeePerGas + block.basefee);
}
}

uint256 actualOpCost = actualGasCost + (postOpGas * opGasPrice);

uint256 actualTokenCost = ((actualGasCost + (POST_OP_GAS * opGasPrice)) * exchangeRate) / 1e18;
if (strategy.paymasterMode == Mode.DynamicRate) {
uint256 actualTokenCost = actualOpCost * strategy.exchangeRate;
if (mode != PostOpMode.postOpReverted) {
emit GasPaidInERC20(address(erc20Token), actualGasCost, actualTokenCost);
erc20Token.safeTransferFrom(sender, tokenRecipient, actualTokenCost);
emit GasPaidInERC20(address(strategy.erc20Token), actualOpCost, actualTokenCost);
IERC20(strategy.erc20Token).safeTransferFrom(sender, tokenRecipient, actualTokenCost);
}
} else if (paymasterMode == Mode.FixedRate) {
emit GasPaidInERC20(address(erc20Token), actualGasCost, exchangeRate);
erc20Token.safeTransferFrom(sender, tokenRecipient, exchangeRate);
} else if (strategy.paymasterMode == Mode.FixedRate) {
emit GasPaidInERC20(address(strategy.erc20Token), actualOpCost, strategy.exchangeRate);
IERC20(strategy.erc20Token).safeTransferFrom(sender, tokenRecipient, strategy.exchangeRate);
}
}

/**
* Parse paymasterAndData
* The "paymasterAndData" is expected to be the paymaster and a signature over the entire request params
* paymasterAndData[:20]: address(this)
* paymasterAndData[20:SIGNATURE_OFFSET]: (validUntil, validAfter, strategy) // 20+48+48+64
* paymasterAndData[20:SIGNATURE_OFFSET]: (validUntil, validAfter, strategy)
* paymasterAndData[SIGNATURE_OFFSET:]: signature
*/
function parsePaymasterAndData(bytes calldata paymasterAndData)
Expand All @@ -173,32 +157,30 @@ contract OpenfortPaymaster is BaseOpenfortPaymaster {
returns (uint48 validUntil, uint48 validAfter, PolicyStrategy memory strategy, bytes calldata signature)
{
(validUntil, validAfter, strategy) =
abi.decode(paymasterAndData[VALID_PND_OFFSET:SIGNATURE_OFFSET], (uint48, uint48, PolicyStrategy));
abi.decode(paymasterAndData[ADDRESS_OFFSET:SIGNATURE_OFFSET], (uint48, uint48, PolicyStrategy));
signature = paymasterAndData[SIGNATURE_OFFSET:];
}

/**
* @dev Override the default implementation.
*/
function deposit() public payable virtual override {
revert("Use depositFor() instead");
entryPoint.depositTo{value: msg.value}(address(this));
}

/**
* @dev Add a deposit for this paymaster and given depositor (Dapp Depositor address), used for paying for transaction fees
* @param _depositorAddress depositor address for which deposit is being made
* @inheritdoc BaseOpenfortPaymaster
*/
function depositFor(address _depositorAddress) external payable {
if (_depositorAddress == address(0)) revert OpenfortErrorsAndEvents.ZeroAddressNotAllowed();
if (msg.value == 0) revert OpenfortErrorsAndEvents.MustSendNativeToken();
function withdrawTo(address payable _withdrawAddress, uint256 _amount) public override onlyOwner {
entryPoint.withdrawTo(_withdrawAddress, _amount);
}

/**
* @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.
* Allows the owner of the paymaster to update the token recipient address
*/
function withdrawTo(address payable withdrawAddress, uint256 amount) public override {
if (withdrawAddress == address(0)) revert OpenfortErrorsAndEvents.ZeroAddressNotAllowed();
function updateTokenRecipient(address _newTokenRecipient) external onlyOwner {
if (_newTokenRecipient == address(0)) revert OpenfortErrorsAndEvents.ZeroValueNotAllowed();
emit TokenRecipientUpdated(tokenRecipient, _newTokenRecipient);
tokenRecipient = _newTokenRecipient;
}
}
Loading

0 comments on commit 9eccad1

Please sign in to comment.