Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Feat/verifying singleton paymaster #1

Open
wants to merge 2 commits into
base: releases/v0.6
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions contracts/common/Errors.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;
contract BasePaymasterErrors {
/**
* @notice Throws at onlyEntryPoint when msg.sender is not an EntryPoint set for this paymaster
* @param caller address that tried to call protected method
*/
error CallerIsNotAnEntryPoint(address caller);
}

contract VerifyingPaymasterErrors {
/**
* @notice Throws when the Entrypoint address provided is address(0)
*/
error EntryPointCannotBeZero();

/**
* @notice Throws when the verifiying signer address provided is address(0)
*/
error VerifyingSignerCannotBeZero();

/**
* @notice Throws when the paymaster address provided is address(0)
*/
error PaymasterIdCannotBeZero();

/**
* @notice Throws when the 0 has been provided as deposit
*/
error DepositCanNotBeZero();

/**
* @notice Throws when trying to withdraw to address(0)
*/
error CanNotWithdrawToZeroAddress();

/**
* @notice Throws when trying to withdraw more than balance available
* @param amountRequired required balance
* @param currentBalance available balance
*/
error InsufficientBalance(uint256 amountRequired, uint256 currentBalance);

/**
* @notice Throws when signature provided has invalid length
* @param sigLength length oif the signature provided
*/
error InvalidPaymasterSignatureLength(uint256 sigLength);
}
4 changes: 2 additions & 2 deletions contracts/core/BasePaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ abstract contract BasePaymaster is IPaymaster, Ownable {
/**
* add a deposit for this paymaster, used for paying for transaction fees
*/
function deposit() public payable {
function deposit() public payable virtual {
entryPoint.depositTo{value : msg.value}(address(this));
}

Expand All @@ -69,7 +69,7 @@ abstract contract BasePaymaster is IPaymaster, Ownable {
* @param withdrawAddress target to send to
* @param amount to withdraw
*/
function withdrawTo(address payable withdrawAddress, uint256 amount) public onlyOwner {
function withdrawTo(address payable withdrawAddress, uint256 amount) public virtual onlyOwner {
entryPoint.withdrawTo(withdrawAddress, amount);
}
/**
Expand Down
165 changes: 153 additions & 12 deletions contracts/samples/VerifyingPaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ pragma solidity ^0.8.12;

import "../core/BasePaymaster.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {VerifyingPaymasterErrors} from "../common/Errors.sol";
/**
* A sample paymaster that uses external service to decide whether to pay for the UserOp.
* The paymaster trusts an external signer to sign the transaction.
Expand All @@ -15,22 +17,120 @@ import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
* - the paymaster checks a signature to agree to PAY for GAS.
* - the account checks a signature to prove identity and account ownership.
*/
contract VerifyingPaymaster is BasePaymaster {
contract VerifyingPaymaster is
BasePaymaster,
ReentrancyGuard,
VerifyingPaymasterErrors
{

using ECDSA for bytes32;
using UserOperationLib for UserOperation;

address public immutable verifyingSigner;

uint256 private constant VALID_TIMESTAMP_OFFSET = 20;
uint256 private constant PAYMASTER_ID_OFFSET = 20;
uint256 private constant SIGNATURE_OFFSET = 116;

uint256 private constant SIGNATURE_OFFSET = 84;
mapping(address => uint256) public senderNonce;

constructor(IEntryPoint _entryPoint, address _verifyingSigner) BasePaymaster(_entryPoint) {
uint256 private unaccountedEPGasOverhead;
mapping(uint48 => uint256) public paymasterIdBalances;

event EPGasOverheadChanged(
uint256 indexed _oldValue,
uint256 indexed _newValue
);

event GasDeposited(uint48 indexed _paymasterId, uint256 indexed _value);
event GasWithdrawn(
uint48 indexed _paymasterId,
address indexed _to,
uint256 indexed _value
);
event GasBalanceDeducted(
uint48 indexed _paymasterId,
uint256 indexed _charge
);

constructor(
IEntryPoint _entryPoint,
address _verifyingSigner
) payable BasePaymaster(_entryPoint) {
if (address(_entryPoint) == address(0)) revert EntryPointCannotBeZero();
if (_verifyingSigner == address(0))
revert VerifyingSignerCannotBeZero();
verifyingSigner = _verifyingSigner;
unaccountedEPGasOverhead = 12000;
}

mapping(address => uint256) public senderNonce;


/**
* @dev Add a deposit for this paymaster and given paymasterId (Dapp Depositor address), used for paying for transaction fees
* @param paymasterId dapp identifier for which deposit is being made
*/
function depositFor(uint48 paymasterId) external payable nonReentrant {
if (paymasterId == 0) revert PaymasterIdCannotBeZero();
if (msg.value == 0) revert DepositCanNotBeZero();
paymasterIdBalances[paymasterId] =
paymasterIdBalances[paymasterId] +
msg.value;
entryPoint.depositTo{value: msg.value}(address(this));
emit GasDeposited(paymasterId, msg.value);
}

function setUnaccountedEPGasOverhead(uint256 value) external onlyOwner {
uint256 oldValue = unaccountedEPGasOverhead;
unaccountedEPGasOverhead = value;
emit EPGasOverheadChanged(oldValue, value);
}

/**
* @dev get the current deposit for paymasterId (Dapp Depositor address)
* @param paymasterId dapp identifier
*/
function getBalance(
uint48 paymasterId
) external view returns (uint256 balance) {
balance = paymasterIdBalances[paymasterId];
}

/**
* @dev Overrides the base function to maintain compatibility.
* Calls the internal function with a default paymasterId.
*/
function withdrawTo(address payable withdrawAddress, uint256 amount) public override nonReentrant {
revert("Use withdrawTo with paymasterId parameter");
}

/**
* @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.
*/
function withdrawTo(
address payable withdrawAddress,
uint256 amount,
uint48 paymasterId
) public onlyOwner nonReentrant {
if (withdrawAddress == address(0)) revert CanNotWithdrawToZeroAddress();
uint256 currentBalance = paymasterIdBalances[paymasterId];
if (amount > currentBalance)
revert InsufficientBalance(amount, currentBalance);
paymasterIdBalances[paymasterId] =
paymasterIdBalances[paymasterId] -
amount;
entryPoint.withdrawTo(withdrawAddress, amount);
emit GasWithdrawn(paymasterId, withdrawAddress, amount);
}


/**
@dev Override the default implementation.
*/
function deposit() public payable virtual override {
revert("user DepositFor instead");
}

function pack(UserOperation calldata userOp) internal pure returns (bytes memory ret) {
// lighter signature scheme. must match UserOp.ts#packUserOp
Expand All @@ -55,7 +155,7 @@ contract VerifyingPaymaster is BasePaymaster {
* note that this signature covers all fields of the UserOperation, except the "paymasterAndData",
* which will carry the signature itself.
*/
function getHash(UserOperation calldata userOp, uint48 validUntil, uint48 validAfter)
function getHash(UserOperation calldata userOp, uint48 paymasterId, uint48 validUntil, uint48 validAfter)
public view returns (bytes32) {
//can't use userOp.hash(), since it contains also the paymasterAndData itself.

Expand All @@ -64,11 +164,30 @@ contract VerifyingPaymaster is BasePaymaster {
block.chainid,
address(this),
senderNonce[userOp.getSender()],
paymasterId,
validUntil,
validAfter
));
}

/**
* @dev Executes the paymaster's payment conditions
* @param context payment conditions signed by the paymaster in `validatePaymasterUserOp`
* @param actualGasCost amount to be paid to the entry point in wei
*/
function _postOp(
PostOpMode /*mode*/,
bytes calldata context,
uint256 actualGasCost
) internal virtual override {
(uint48 paymasterId, uint256 maxFeePerGas, uint256 maxPriorityFeePerGas) = abi.decode(context, (uint48, uint256, uint256));
uint256 effectiveGasPrice = getGasPrice(maxFeePerGas, maxPriorityFeePerGas);
uint256 balToDeduct = actualGasCost + (unaccountedEPGasOverhead * effectiveGasPrice);
paymasterIdBalances[paymasterId] -= balToDeduct;
emit GasBalanceDeducted(paymasterId, balToDeduct);
}


/**
* verify our external signer signed this request.
* the "paymasterAndData" is expected to be the paymaster and a signature over the entire request params
Expand All @@ -80,25 +199,47 @@ contract VerifyingPaymaster is BasePaymaster {
internal override returns (bytes memory context, uint256 validationData) {
(requiredPreFund);

(uint48 validUntil, uint48 validAfter, bytes calldata signature) = parsePaymasterAndData(userOp.paymasterAndData);
(uint48 paymasterId, uint48 validUntil, uint48 validAfter, bytes calldata signature) = parsePaymasterAndData(userOp.paymasterAndData);
//ECDSA library supports both 64 and 65-byte long signatures.
// we only "require" it here so that the revert reason on invalid signature will be of "VerifyingPaymaster", and not "ECDSA"
require(signature.length == 64 || signature.length == 65, "VerifyingPaymaster: invalid signature length in paymasterAndData");
bytes32 hash = ECDSA.toEthSignedMessageHash(getHash(userOp, validUntil, validAfter));
bytes32 hash = ECDSA.toEthSignedMessageHash(getHash(userOp, paymasterId, validUntil, validAfter));
senderNonce[userOp.getSender()]++;

//don't revert on signature failure: return SIG_VALIDATION_FAILED
if (verifyingSigner != ECDSA.recover(hash, signature)) {
return ("",_packValidationData(true,validUntil,validAfter));
}

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

//no need for other on-chain validation: entire UserOp should have been checked
// by the external service prior to signing it.
return ("",_packValidationData(false,validUntil,validAfter));
return (abi.encode(paymasterId, userOp.maxFeePerGas, userOp.maxPriorityFeePerGas),
_packValidationData(false,validUntil,validAfter));
}

function parsePaymasterAndData(bytes calldata paymasterAndData) public pure returns(uint48 validUntil, uint48 validAfter, bytes calldata signature) {
(validUntil, validAfter) = abi.decode(paymasterAndData[VALID_TIMESTAMP_OFFSET:SIGNATURE_OFFSET],(uint48, uint48));
function parsePaymasterAndData(bytes calldata paymasterAndData) public pure returns(uint48 paymasterId, uint48 validUntil, uint48 validAfter, bytes calldata signature) {
(paymasterId, validUntil, validAfter) = abi.decode(paymasterAndData[PAYMASTER_ID_OFFSET:SIGNATURE_OFFSET], (uint48, uint48, uint48));
signature = paymasterAndData[SIGNATURE_OFFSET:];
}
}

function getGasPrice(
uint256 maxFeePerGas,
uint256 maxPriorityFeePerGas
) internal view returns (uint256) {
if (maxFeePerGas == maxPriorityFeePerGas) {
//legacy mode (for networks that don't support basefee opcode)
return maxFeePerGas;
}
return min(maxFeePerGas, maxPriorityFeePerGas + block.basefee);
}

function min(uint256 a, uint256 b) internal pure returns (uint256) {
return a < b ? a : b;
}
}
Loading