Skip to content

Commit

Permalink
Paymaster refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
Haypierre committed Feb 7, 2025
1 parent 9a1538d commit ab85233
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 42 deletions.
7 changes: 4 additions & 3 deletions script/deployChainAbstractionSetup.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
pragma solidity ^0.8.0;

import {Script, console} from "forge-std/Script.sol";
import {IEntryPoint} from "account-abstraction/core/EntryPoint.sol";
import {IInvoiceManager} from "../src/interfaces/IInvoiceManager.sol";
import {IVaultManager} from "../src/interfaces/IVaultManager.sol";

import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol";
import {CheckOrDeployEntryPoint} from "./auxiliary/checkOrDeployEntrypoint.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {UpgradeableOpenfortProxy} from "../src/proxy/UpgradeableOpenfortProxy.sol";
Expand Down Expand Up @@ -84,9 +85,9 @@ contract DeployChainAbstractionSetup is Script, CheckOrDeployEntryPoint {
IEntryPoint entryPoint = checkOrDeployEntryPoint();

CABPaymaster paymaster = new CABPaymaster{salt: versionSalt}(
entryPoint, IInvoiceManager(address(invoiceManager)), ICrossL2Prover(crossL2Prover), verifyingSigner, owner
ICrossL2Prover(crossL2Prover), IInvoiceManager(address(invoiceManager)), verifyingSigner, owner
);

paymaster.initialize(tokens);
console.log("Paymaster Address", address(paymaster));
vm.stopBroadcast();
}
Expand Down
10 changes: 10 additions & 0 deletions src/interfaces/IPaymasterVerifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,14 @@ interface IPaymasterVerifier {
* @notice Withdraw the token.
*/
function withdraw(address token, uint256 amount) external;

/**
* @notice Emitted when a CABPaymaster is initialized.
* @param cabPaymaster The address of the CABPaymaster.
* @param supportedTokensLength The length of the supported tokens.
* @param supportedTokensHash The hash of the supported tokens.
*/
event CABPaymasterInitialized(
address indexed cabPaymaster, uint256 indexed supportedTokensLength, bytes32 indexed supportedTokensHash
);
}
60 changes: 36 additions & 24 deletions src/paymasters/CABPaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,23 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import {IInvoiceManager} from "../interfaces/IInvoiceManager.sol";
import {LibTokens} from "./LibTokens.sol";
import {IVault} from "../interfaces/IVault.sol";
import {IPaymasterVerifier} from "../interfaces/IPaymasterVerifier.sol";
import {ICrossL2Prover} from "@vibc-core-smart-contracts/contracts/interfaces/ICrossL2Prover.sol";
import {LibBytes} from "@solady/utils/LibBytes.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

/**
* @title CABPaymaster
* @dev A paymaster used in chain abstracted balance to sponsor the gas fee and tokens cross-chain.
*/
contract CABPaymaster is IPaymasterVerifier, BasePaymaster {
contract CABPaymaster is IPaymasterVerifier, BasePaymaster, Initializable {
using SafeERC20 for IERC20;
using UserOperationLib for PackedUserOperation;
using LibTokens for LibTokens.TokensStore;

LibTokens.TokensStore private tokensStore;

IInvoiceManager public immutable invoiceManager;
ICrossL2Prover public immutable crossL2Prover;
Expand All @@ -30,20 +35,25 @@ contract CABPaymaster is IPaymasterVerifier, BasePaymaster {
uint256 private constant VALID_TIMESTAMP_OFFSET = PAYMASTER_DATA_OFFSET;
uint256 private constant SIGNATURE_OFFSET = VALID_TIMESTAMP_OFFSET + 12;

address public constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
address public constant ENTRY_POINT_V7 = 0x0000000071727De22E5E9d8BAf0edAc6f37da032;

constructor(
IEntryPoint _entryPoint,
IInvoiceManager _invoiceManager,
ICrossL2Prover _crossL2Prover,
IInvoiceManager _invoiceManager,
address _verifyingSigner,
address _owner
) BasePaymaster(_entryPoint, _owner) {
invoiceManager = _invoiceManager;
) BasePaymaster(IEntryPoint(ENTRY_POINT_V7), _owner) {
crossL2Prover = _crossL2Prover;
invoiceManager = _invoiceManager;
verifyingSigner = _verifyingSigner;
}

function initialize(address[] memory _supportedTokens) public initializer {
for (uint256 i = 0; i < _supportedTokens.length; i++) {
tokensStore.addSupportedToken(_supportedTokens[i]);
}
}

/// @inheritdoc IPaymasterVerifier
function verifyInvoice(
bytes32 _invoiceId,
Expand All @@ -67,7 +77,7 @@ contract CABPaymaster is IPaymasterVerifier, BasePaymaster {
}

function withdraw(address token, uint256 amount) external override onlyOwner {
if (token == NATIVE_TOKEN) {
if (token == LibTokens.NATIVE_TOKEN) {
(bool success,) = payable(owner()).call{value: amount}("");
require(success, "Native token transfer failed");
} else {
Expand Down Expand Up @@ -104,18 +114,6 @@ contract CABPaymaster is IPaymasterVerifier, BasePaymaster {
);
}

function getInvoiceHash(IInvoiceManager.InvoiceWithRepayTokens calldata invoice) public pure returns (bytes32) {
return keccak256(
abi.encode(
invoice.account,
invoice.nonce,
invoice.paymaster,
invoice.sponsorChainId,
keccak256(abi.encode(invoice.repayTokenInfos))
)
);
}

function _validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 requiredPreFund)
internal
override
Expand All @@ -134,10 +132,12 @@ contract CABPaymaster is IPaymasterVerifier, BasePaymaster {
// revoke the approval at the end of userOp
for (uint256 i = 0; i < sponsorTokenLength;) {
address token = sponsorTokens[i].token;
require(tokensStore.supported[token], "Unsupported token");

address spender = sponsorTokens[i].spender;
uint256 amount = sponsorTokens[i].amount;

if (token == NATIVE_TOKEN) {
if (token == LibTokens.NATIVE_TOKEN) {
(bool success,) = payable(spender).call{value: amount}("");
require(success, "Native token transfer failed");
} else {
Expand All @@ -161,7 +161,6 @@ contract CABPaymaster is IPaymasterVerifier, BasePaymaster {
_packValidationData(true, validUntil, validAfter)
);
}

return (
abi.encodePacked(invoiceId, sender, userOp.nonce, sponsorTokenData[0:1 + sponsorTokenLength * 72]),
_packValidationData(false, validUntil, validAfter)
Expand All @@ -179,8 +178,8 @@ contract CABPaymaster is IPaymasterVerifier, BasePaymaster {
for (uint8 i = 0; i < sponsorTokenLength;) {
address token = sponsorTokens[i].token;
address spender = sponsorTokens[i].spender;
if (token != NATIVE_TOKEN) {
require(IERC20(token).approve(spender, 0), "Reset approval failed");
if (token != LibTokens.NATIVE_TOKEN) {
require(IERC20(token).approve(spender, 0), "CABPaymaster: Reset approval failed");
}
unchecked {
i++;
Expand All @@ -189,7 +188,6 @@ contract CABPaymaster is IPaymasterVerifier, BasePaymaster {
// TODO: Batch Proving Optimistation -> write in settlement contract on `opSucceeded`
if (mode == PostOpMode.opSucceeded) {
//emit IInvoiceManager.InvoiceCreated(bytes32(context[:32]), address(bytes20(context[32:52])), address(this));

// This add ~= 100k gas compared to only emitting the InvoiceCreated event
// Question: is storing the invoices onchain truly necessary?
bytes32 invoiceId = bytes32(context[:32]);
Expand Down Expand Up @@ -281,5 +279,19 @@ contract CABPaymaster is IPaymasterVerifier, BasePaymaster {
return abi.encodePacked(uint8(repayTokens.length), encodedRepayToken);
}

function addSupportedToken(address token) public onlyOwner {
tokensStore.addSupportedToken(token);
emit LibTokens.SupportedTokenAdded(token);
}

function removeSupportedToken(address token) public onlyOwner {
tokensStore.removeSupportedToken(token);
emit LibTokens.SupportedTokenRemoved(token);
}

function getSupportedTokens() public view returns (address[] memory) {
return tokensStore.getSupportedTokens();
}

receive() external payable {}
}
95 changes: 95 additions & 0 deletions src/paymasters/CABPaymasterFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import "account-abstraction/core/BasePaymaster.sol";
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
import {CABPaymaster} from "./CABPaymaster.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IInvoiceManager} from "../interfaces/IInvoiceManager.sol";
import {ICrossL2Prover} from "@vibc-core-smart-contracts/contracts/interfaces/ICrossL2Prover.sol";

contract CABPaymasterFactory is Ownable {
address public invoiceManager;
address public crossL2Prover;
address public verifyingSigner;

event CABPaymasterCreated(address indexed owner, address indexed cabPaymaster);

event InvoiceManagerUpdated(address indexed newInvoiceManager);
event CrossL2ProverUpdated(address indexed newCrossL2Prover);
event VerifyingSignerUpdated(address indexed newVerifyingSigner);

constructor(address _owner, address _crossL2Prover, address _invoiceManager, address _verifyingSigner)
Ownable(_owner)
{
crossL2Prover = _crossL2Prover;
invoiceManager = _invoiceManager;
verifyingSigner = _verifyingSigner;
}

/*
* @notice Create a CABPaymaster with the given _owner and _salt.
* @param _owner The owner of the CABPaymaster.
* @param _nonce The nonce for the CABPaymaster.
* @return cabPaymaster The address of the CABPaymaster.
*/
function createCABPaymaster(address _owner, bytes32 _nonce, address[] memory _supportedTokens)
external
returns (address cabPaymaster)
{
bytes32 salt = keccak256(abi.encode(_owner, _nonce));
require(_owner != owner(), "CABPaymasterFactory: Wrong owner");
cabPaymaster = getAddressWithNonce(_owner, _nonce);
if (cabPaymaster.code.length > 0) return cabPaymaster;
cabPaymaster = address(
new CABPaymaster{salt: salt}(
ICrossL2Prover(crossL2Prover), IInvoiceManager(invoiceManager), verifyingSigner, _owner
)
);

CABPaymaster(payable(cabPaymaster)).initialize(_supportedTokens);
emit CABPaymasterCreated(_owner, cabPaymaster);
}

/*
* @notice Return the address of a CABPaymaster that would be deployed with the given _salt.
*/
function getAddressWithNonce(address _owner, bytes32 _nonce) public view returns (address) {
bytes32 salt = keccak256(abi.encode(_owner, _nonce));
return Create2.computeAddress(
salt,
keccak256(
abi.encodePacked(
type(CABPaymaster).creationCode, abi.encode(crossL2Prover, invoiceManager, verifyingSigner, _owner)
)
)
);
}

/*
* @notice Update the invoice manager.
* @param _invoiceManager The new invoice manager.
*/
function updateInvoiceManager(address _invoiceManager) public onlyOwner {
require(_invoiceManager != address(0), "Invoice manager cannot be the zero address");
invoiceManager = _invoiceManager;
emit InvoiceManagerUpdated(_invoiceManager);
}

/*
* @notice Update the cross-chain prover.
* @param _crossL2Prover The new cross-chain prover.
*/
function updateCrossL2Prover(address _crossL2Prover) public onlyOwner {
require(_crossL2Prover != address(0), "Cross-chain prover cannot be the zero address");
crossL2Prover = _crossL2Prover;
emit CrossL2ProverUpdated(_crossL2Prover);
}

/*
* @notice Update the verifying signer.
* @param _verifyingSigner The new verifying signer.
*/
function updateVerifyingSigner(address _verifyingSigner) public onlyOwner {
require(_verifyingSigner != address(0), "Verifying signer cannot be the zero address");
verifyingSigner = _verifyingSigner;
emit VerifyingSignerUpdated(_verifyingSigner);
}
}
49 changes: 49 additions & 0 deletions src/paymasters/LibTokens.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.0;

import "@openzeppelin/contracts/access/Ownable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

library LibTokens {
address public constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

event SupportedTokenAdded(address token);
event SupportedTokenRemoved(address token);

struct TokensStore {
address[] tokens;
mapping(address => bool) supported;
}

function addSupportedToken(TokensStore storage store, address token) public {
require(!store.supported[token], "TokenManager: token already supported");
store.supported[token] = true;
store.tokens.push(token);
}

function removeSupportedToken(TokensStore storage store, address token) public {
if (token == NATIVE_TOKEN) {
require(address(this).balance == 0, "TokenManager: native token has balance");
} else {
uint256 balance = IERC20(token).balanceOf(address(this));
require(balance == 0, "TokenManager: token has balance");
}

uint256 length = store.tokens.length;
for (uint256 i = 0; i < length;) {
if (store.tokens[i] == token) {
store.supported[token] = false;
store.tokens[i] = store.tokens[length - 1];
store.tokens.pop();
break;
}
unchecked {
i++;
}
}
}

function getSupportedTokens(TokensStore storage store) public view returns (address[] memory) {
return store.tokens;
}
}
Loading

0 comments on commit ab85233

Please sign in to comment.