diff --git a/contracts/interfaces/OpenfortErrorsAndEvents.sol b/contracts/interfaces/OpenfortErrorsAndEvents.sol index bce5f6f..ffdb25f 100644 --- a/contracts/interfaces/OpenfortErrorsAndEvents.sol +++ b/contracts/interfaces/OpenfortErrorsAndEvents.sol @@ -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); } diff --git a/contracts/paymaster/BaseOpenfortPaymaster.sol b/contracts/paymaster/BaseOpenfortPaymaster.sol index 4472a34..cd738c3 100644 --- a/contracts/paymaster/BaseOpenfortPaymaster.sol +++ b/contracts/paymaster/BaseOpenfortPaymaster.sol @@ -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; } /** @@ -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. @@ -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)); @@ -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; + } } diff --git a/contracts/paymaster/OpenfortPaymaster.sol b/contracts/paymaster/OpenfortPaymaster.sol index 8318d98..3401b69 100644 --- a/contracts/paymaster/OpenfortPaymaster.sol +++ b/contracts/paymaster/OpenfortPaymaster.sol @@ -14,24 +14,23 @@ import {OpenfortErrorsAndEvents} from "../interfaces/OpenfortErrorsAndEvents.sol * @title OpenfortPaymaster (Non-upgradeable) * @author Eloi * @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, @@ -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) @@ -76,15 +79,7 @@ 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)); } @@ -92,13 +87,13 @@ contract OpenfortPaymaster is BaseOpenfortPaymaster { * 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); @@ -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). @@ -130,33 +116,31 @@ 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); } } @@ -164,7 +148,7 @@ contract OpenfortPaymaster is BaseOpenfortPaymaster { * 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) @@ -173,7 +157,7 @@ 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:]; } @@ -181,24 +165,22 @@ contract OpenfortPaymaster is BaseOpenfortPaymaster { * @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; } } diff --git a/contracts/paymaster/OpenfortPaymasterV2.sol b/contracts/paymaster/OpenfortPaymasterV2.sol new file mode 100644 index 0000000..9fef2e9 --- /dev/null +++ b/contracts/paymaster/OpenfortPaymasterV2.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {UserOperation, UserOperationLib, IEntryPoint} from "account-abstraction/core/BaseAccount.sol"; +import {BaseOpenfortPaymaster} from "./BaseOpenfortPaymaster.sol"; +import "account-abstraction/core/Helpers.sol" as Helpers; +import {OpenfortErrorsAndEvents} from "../interfaces/OpenfortErrorsAndEvents.sol"; + +/** + * @title OpenfortPaymasterV2 (Non-upgradeable) + * @author Eloi + * @notice A paymaster that uses external service to decide whether to pay for the UserOp. + * 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 + */ +contract OpenfortPaymasterV2 is BaseOpenfortPaymaster { + using ECDSA for bytes32; + using UserOperationLib for UserOperation; + using SafeERC20 for IERC20; + + uint256 private constant ADDRESS_OFFSET = 20; // length of an address + uint256 private constant SIGNATURE_OFFSET = 212; // 20+48+48+32+32+32 = 212 + + mapping(address => uint256) public depositorBalances; + uint256 private totalDepositorBalances; + + enum Mode { + PayForUser, + DynamicRate, + FixedRate + } + + struct PolicyStrategy { + Mode paymasterMode; + address depositor; + address erc20Token; + uint256 exchangeRate; + } + + /// @notice When a transaction has been paid using an ERC20 token + event GasPaidInERC20(address erc20Token, uint256 actualGasCost, uint256 actualTokensSent); + + /// @notice When a depositor deposits gas to the EntryPoint + event GasDeposited(address indexed from, address indexed depositor, uint256 indexed value); + + /// @notice When a depositor withdraws gas from the EntryPoint + event GasWithdrawn(address indexed depositor, address indexed to, uint256 indexed value); + + /// @notice When a depositor uses gas from the EntryPoint deposit and it is deducted from depositorBalances + event GasBalanceDeducted(address depositor, uint256 actualOpCost); + + constructor(IEntryPoint _entryPoint, address _owner) BaseOpenfortPaymaster(_entryPoint, _owner) { + totalDepositorBalances = 0; + } + + /** + * Return the hash we're going to sign off-chain (and validate on-chain) + * this method is called by the off-chain service, to sign the request. + * it is called on-chain from the validatePaymasterUserOp, to validate the signature. + * 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, + PolicyStrategy memory strategy + ) public view returns (bytes32) { + // Dividing the hashing in 2 parts due to the stack too deep error + bytes memory firstHalf = abi.encode( + userOp.getSender(), + userOp.nonce, + keccak256(userOp.initCode), + keccak256(userOp.callData), + userOp.callGasLimit, + userOp.verificationGasLimit, + userOp.preVerificationGas, + userOp.maxFeePerGas, + userOp.maxPriorityFeePerGas + ); + bytes memory secondHalf = abi.encode(block.chainid, address(this), validUntil, validAfter, strategy); + return keccak256(abi.encodePacked(firstHalf, secondHalf)); + } + + /** + * Return current paymaster's deposit on the EntryPoint for a given depositor address. + * Owner deposit is all deposited funds that are not part of other depositors. + */ + function getDepositFor(address _depositor) external view returns (uint256) { + if (_depositor == owner()) return entryPoint.balanceOf(address(this)) - totalDepositorBalances; + return depositorBalances[_depositor]; + } + + /** + * 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) + * paymasterAndData[SIGNATURE_OFFSET:]: signature + */ + function _validatePaymasterUserOp(UserOperation calldata userOp, bytes32, /*userOpHash*/ uint256 requiredPreFund) + internal + view + override + returns (bytes memory context, uint256 validationData) + { + (uint48 validUntil, uint48 validAfter, PolicyStrategy memory strategy, bytes calldata signature) = + parsePaymasterAndData(userOp.paymasterAndData); + + bytes32 hash = ECDSA.toEthSignedMessageHash(getHash(userOp, validUntil, validAfter, strategy)); + + // Don't revert on signature failure: return SIG_VALIDATION_FAILED with empty context + if (owner() != ECDSA.recover(hash, signature)) { + return ("", Helpers._packValidationData(true, validUntil, validAfter)); + } + + if (requiredPreFund > depositorBalances[strategy.depositor]) { + revert OpenfortErrorsAndEvents.InsufficientBalance(requiredPreFund, depositorBalances[strategy.depositor]); + } + + 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). + return (context, Helpers._packValidationData(false, validUntil, validAfter)); + } + + /* + * 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, 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); + + if (strategy.paymasterMode == Mode.DynamicRate) { + uint256 actualTokenCost = actualOpCost * strategy.exchangeRate; + if (mode != PostOpMode.postOpReverted) { + emit GasPaidInERC20(address(strategy.erc20Token), actualOpCost, actualTokenCost); + IERC20(strategy.erc20Token).safeTransferFrom(sender, strategy.depositor, actualTokenCost); + } + } else if (strategy.paymasterMode == Mode.FixedRate) { + emit GasPaidInERC20(address(strategy.erc20Token), actualOpCost, strategy.exchangeRate); + IERC20(strategy.erc20Token).safeTransferFrom(sender, strategy.depositor, strategy.exchangeRate); + } + + // In any of the modes, subtract the right according + if (strategy.depositor != owner()) { + totalDepositorBalances -= actualOpCost; + depositorBalances[strategy.depositor] -= actualOpCost; + emit GasBalanceDeducted(strategy.depositor, actualOpCost); + } + } + + /** + * 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) + * paymasterAndData[SIGNATURE_OFFSET:]: signature + */ + function parsePaymasterAndData(bytes calldata paymasterAndData) + public + pure + returns (uint48 validUntil, uint48 validAfter, PolicyStrategy memory strategy, bytes calldata signature) + { + (validUntil, validAfter, strategy) = + 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 { + if (msg.sender != owner()) revert("Not Owner: 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 + */ + function depositFor(address _depositorAddress) public payable { + if (_depositorAddress == address(0)) revert OpenfortErrorsAndEvents.ZeroValueNotAllowed(); + if (msg.value == 0) revert OpenfortErrorsAndEvents.MustSendNativeToken(); + depositorBalances[_depositorAddress] += msg.value; + entryPoint.depositTo{value: msg.value}(address(this)); + emit GasDeposited(msg.sender, _depositorAddress, msg.value); + } + + /** + * @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 to. + * @param _amount The amount of gas tokens to withdraw. + */ + function withdrawTo(address payable _withdrawAddress, uint256 _amount) public override { + if (_withdrawAddress == address(0)) revert OpenfortErrorsAndEvents.ZeroValueNotAllowed(); + uint256 currentBalance = depositorBalances[msg.sender]; + if (_amount > currentBalance) { + revert OpenfortErrorsAndEvents.InsufficientBalance(_amount, currentBalance); + } + depositorBalances[msg.sender] -= _amount; + entryPoint.withdrawTo(_withdrawAddress, _amount); + emit GasWithdrawn(msg.sender, _withdrawAddress, _amount); + } + + /** + * @dev The new owner accepts the ownership transfer. + * + */ + function acceptOwnership() public override { + depositorBalances[pendingOwner()] = depositorBalances[owner()]; + depositorBalances[owner()] = 0; + super.acceptOwnership(); + // address sender = _msgSender(); + // require(pendingOwner() == sender, "Ownable2Step: caller is not the new owner"); + // _transferOwnership(sender); + } +} diff --git a/test/foundry/paymaster/OpenfortPaymasterTest.t.sol b/test/foundry/paymaster/OpenfortPaymasterTest.t.sol index 8b4f7b5..70ec72d 100644 --- a/test/foundry/paymaster/OpenfortPaymasterTest.t.sol +++ b/test/foundry/paymaster/OpenfortPaymasterTest.t.sol @@ -8,6 +8,7 @@ import {TestCounter} from "account-abstraction/test/TestCounter.sol"; import {TestToken} from "account-abstraction/test/TestToken.sol"; import {StaticOpenfortFactory} from "contracts/core/static/StaticOpenfortFactory.sol"; import {StaticOpenfortAccount} from "contracts/core/static/StaticOpenfortAccount.sol"; +import {OpenfortErrorsAndEvents} from "contracts/interfaces/OpenfortErrorsAndEvents.sol"; import {OpenfortPaymaster} from "contracts/paymaster/OpenfortPaymaster.sol"; contract OpenfortPaymasterTest is Test { @@ -35,8 +36,9 @@ contract OpenfortPaymasterTest is Test { uint48 internal constant VALIDUNTIL = 2 ** 48 - 1; uint48 internal constant VALIDAFTER = 0; - uint256 internal constant EXCHANGERATE = 10_000_000; + uint256 internal constant EXCHANGERATE = 10 ** 3; uint256 internal constant MOCKSIG = 2 ** 256 - 1; + uint256 internal TESTTOKEN_ACCOUNT_PREFUND = 100 * 10 ** 18; error InvalidTokenRecipient(); @@ -201,9 +203,9 @@ contract OpenfortPaymasterTest is Test { new StaticOpenfortFactory((payable(vm.envAddress("ENTRY_POINT_ADDRESS"))), address(staticOpenfortAccount)); // deploy a new TestCounter testCounter = new TestCounter(); - // deploy a new TestToken (ERC20) and mint 100 + // deploy a new TestToken (ERC20) and mint 1000 testToken = new TestToken(); - testToken.mint(address(this), 100); + testToken.mint(address(this), 1_000 * 10 ** 18); // Create an static account wallet and get its address vm.prank(factoryAdmin); @@ -502,8 +504,8 @@ contract OpenfortPaymasterTest is Test { */ function testPaymasterUserOpERC20ValidSigDiffMaxPriorityFeePerGas() public { assertEq(testToken.balanceOf(account), 0); - testToken.mint(account, 100_000); - assertEq(testToken.balanceOf(account), 100_000); + testToken.mint(account, TESTTOKEN_ACCOUNT_PREFUND); + assertEq(testToken.balanceOf(account), TESTTOKEN_ACCOUNT_PREFUND); bytes memory dataEncoded = mockedPaymasterDataERC20Dynamic(); @@ -566,7 +568,7 @@ contract OpenfortPaymasterTest is Test { // Verify that the paymaster has less deposit now assert(paymasterDepositBefore > openfortPaymaster.getDeposit()); // Verifiy that the balance of the smart account has decreased - assert(testToken.balanceOf(account) < 100_000); + assert(testToken.balanceOf(account) < TESTTOKEN_ACCOUNT_PREFUND); } /* @@ -575,8 +577,8 @@ contract OpenfortPaymasterTest is Test { */ function testPaymasterUserOpERC20ValidSig() public { assertEq(testToken.balanceOf(account), 0); - testToken.mint(account, 100_000); - assertEq(testToken.balanceOf(account), 100_000); + testToken.mint(account, TESTTOKEN_ACCOUNT_PREFUND); + assertEq(testToken.balanceOf(account), TESTTOKEN_ACCOUNT_PREFUND); bytes memory dataEncoded = mockedPaymasterDataERC20Dynamic(); @@ -638,7 +640,7 @@ contract OpenfortPaymasterTest is Test { // Verify that the paymaster has less deposit now assert(paymasterDepositBefore > openfortPaymaster.getDeposit()); // Verifiy that the balance of the smart account has decreased - assert(testToken.balanceOf(account) < 100_000); + assert(testToken.balanceOf(account) < TESTTOKEN_ACCOUNT_PREFUND); } /* @@ -647,9 +649,9 @@ contract OpenfortPaymasterTest is Test { */ function testPaymasterUserOpERC20FixedValidSig() public { assertEq(testToken.balanceOf(account), 0); - testToken.mint(account, 100_000); - assertEq(testToken.balanceOf(account), 100_000); - uint256 pricePerTransaction = 1; + testToken.mint(account, TESTTOKEN_ACCOUNT_PREFUND); + assertEq(testToken.balanceOf(account), TESTTOKEN_ACCOUNT_PREFUND); + uint256 pricePerTransaction = 10 ** 18; bytes memory dataEncoded = mockedPaymasterDataERC20Fixed(pricePerTransaction); @@ -669,7 +671,7 @@ contract OpenfortPaymasterTest is Test { OpenfortPaymaster.PolicyStrategy memory strategy; strategy.paymasterMode = OpenfortPaymaster.Mode.FixedRate; strategy.erc20Token = address(testToken); - strategy.exchangeRate = 1; + strategy.exchangeRate = pricePerTransaction; // Simulating that the Paymaster gets the userOp and signs it bytes32 hash; @@ -711,7 +713,7 @@ contract OpenfortPaymasterTest is Test { // Verify that the paymaster has less deposit now assert(paymasterDepositBefore > openfortPaymaster.getDeposit()); // Verifiy that the balance of the smart account has decreased - assert(testToken.balanceOf(account) == 99_999); + assert(testToken.balanceOf(account) == TESTTOKEN_ACCOUNT_PREFUND-pricePerTransaction); } /* @@ -720,8 +722,8 @@ contract OpenfortPaymasterTest is Test { */ function testPaymasterUserOpERC20ValidSigExecBatch() public { assertEq(testToken.balanceOf(account), 0); - testToken.mint(account, 100_000); - assertEq(testToken.balanceOf(account), 100_000); + testToken.mint(account, TESTTOKEN_ACCOUNT_PREFUND); + assertEq(testToken.balanceOf(account), TESTTOKEN_ACCOUNT_PREFUND); assertEq(testCounter.counters(account), 0); @@ -791,7 +793,7 @@ contract OpenfortPaymasterTest is Test { // Verify that the paymaster has less deposit now assert(paymasterDepositBefore > openfortPaymaster.getDeposit()); // Verifiy that the balance of the smart account has decreased - assert(testToken.balanceOf(account) < 100_000); + assert(testToken.balanceOf(account) < TESTTOKEN_ACCOUNT_PREFUND); assertEq(testCounter.counters(account), 1); } @@ -801,12 +803,12 @@ contract OpenfortPaymasterTest is Test { */ function testPaymasterUserOpERC20FixedValidSigExecBatch() public { assertEq(testToken.balanceOf(account), 0); - testToken.mint(account, 100_000); - assertEq(testToken.balanceOf(account), 100_000); + testToken.mint(account, TESTTOKEN_ACCOUNT_PREFUND); + assertEq(testToken.balanceOf(account), TESTTOKEN_ACCOUNT_PREFUND); assertEq(testCounter.counters(account), 0); - uint256 pricePerTransaction = 1; + uint256 pricePerTransaction = 10 ** 18; bytes memory dataEncoded = mockedPaymasterDataERC20Fixed(pricePerTransaction); @@ -874,7 +876,7 @@ contract OpenfortPaymasterTest is Test { // Verify that the paymaster has less deposit now assert(paymasterDepositBefore > openfortPaymaster.getDeposit()); // Verifiy that the balance of the smart account has decreased - assert(testToken.balanceOf(account) == 99_999); + assert(testToken.balanceOf(account) == TESTTOKEN_ACCOUNT_PREFUND-pricePerTransaction); assertEq(testCounter.counters(account), 1); } @@ -884,8 +886,8 @@ contract OpenfortPaymasterTest is Test { */ function testPaymasterUserOpERC20FixedExpensiveValidSigExecBatch() public { assertEq(testToken.balanceOf(account), 0); - testToken.mint(account, 100_000); - assertEq(testToken.balanceOf(account), 100_000); + testToken.mint(account, TESTTOKEN_ACCOUNT_PREFUND); + assertEq(testToken.balanceOf(account), TESTTOKEN_ACCOUNT_PREFUND); assertEq(testCounter.counters(account), 0); @@ -957,7 +959,7 @@ contract OpenfortPaymasterTest is Test { // Verify that the paymaster has less deposit now assert(paymasterDepositBefore > openfortPaymaster.getDeposit()); // Verifiy that the balance of the smart account has decreased - assert(testToken.balanceOf(account) == 99_990); + assert(testToken.balanceOf(account) == TESTTOKEN_ACCOUNT_PREFUND-pricePerTransaction); assertEq(testCounter.counters(account), 1); } @@ -1130,8 +1132,8 @@ contract OpenfortPaymasterTest is Test { */ function testFailPaymasterUserOpERC20ValidSigSmallApprove() public { assertEq(testToken.balanceOf(account), 0); - testToken.mint(account, 100_000); - assertEq(testToken.balanceOf(account), 100_000); + testToken.mint(account, TESTTOKEN_ACCOUNT_PREFUND); + assertEq(testToken.balanceOf(account), TESTTOKEN_ACCOUNT_PREFUND); bytes memory dataEncoded = mockedPaymasterDataERC20Dynamic(); @@ -1193,7 +1195,7 @@ contract OpenfortPaymasterTest is Test { // Verify that the paymaster has the same deposit assert(paymasterDepositBefore == openfortPaymaster.getDeposit()); // Verify that the balance of the smart account has not decreased - assertEq(testToken.balanceOf(account), 100_000); + assertEq(testToken.balanceOf(account), TESTTOKEN_ACCOUNT_PREFUND); // Verify that the counter has not increased assertEq(testCounter.counters(account), 0); @@ -1206,17 +1208,17 @@ contract OpenfortPaymasterTest is Test { /* * Test UpdateTokenRecipient function */ - // function testUpdateTokenRecipient() public { - // assertEq(openfortPaymaster.tokenRecipient(), paymasterAdmin); - // vm.expectRevert("Ownable: caller is not the owner"); - // openfortPaymaster.updateTokenRecipient(factoryAdmin); - - // vm.prank(paymasterAdmin); - // vm.expectRevert(InvalidTokenRecipient.selector); - // openfortPaymaster.updateTokenRecipient(address(0)); - - // vm.prank(paymasterAdmin); - // openfortPaymaster.updateTokenRecipient(factoryAdmin); - // assertEq(openfortPaymaster.tokenRecipient(), factoryAdmin); - // } + function testUpdateTokenRecipient() public { + assertEq(openfortPaymaster.tokenRecipient(), paymasterAdmin); + vm.expectRevert("Ownable: caller is not the owner"); + openfortPaymaster.updateTokenRecipient(factoryAdmin); + + vm.prank(paymasterAdmin); + vm.expectRevert(OpenfortErrorsAndEvents.ZeroValueNotAllowed.selector); + openfortPaymaster.updateTokenRecipient(address(0)); + + vm.prank(paymasterAdmin); + openfortPaymaster.updateTokenRecipient(factoryAdmin); + assertEq(openfortPaymaster.tokenRecipient(), factoryAdmin); + } } diff --git a/test/foundry/paymaster/OpenfortPaymasterV2Test.t.sol b/test/foundry/paymaster/OpenfortPaymasterV2Test.t.sol new file mode 100644 index 0000000..af342d2 --- /dev/null +++ b/test/foundry/paymaster/OpenfortPaymasterV2Test.t.sol @@ -0,0 +1,1226 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import {Test, console} from "lib/forge-std/src/Test.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {EntryPoint, UserOperation, IEntryPoint} from "account-abstraction/core/EntryPoint.sol"; +import {TestCounter} from "account-abstraction/test/TestCounter.sol"; +import {TestToken} from "account-abstraction/test/TestToken.sol"; +import {StaticOpenfortFactory} from "contracts/core/static/StaticOpenfortFactory.sol"; +import {StaticOpenfortAccount} from "contracts/core/static/StaticOpenfortAccount.sol"; +import {OpenfortPaymasterV2} from "contracts/paymaster/OpenfortPaymasterV2.sol"; + +contract OpenfortPaymasterV2Test is Test { + using ECDSA for bytes32; + + EntryPoint public entryPoint; + StaticOpenfortAccount public staticOpenfortAccount; + StaticOpenfortFactory public staticOpenfortFactory; + OpenfortPaymasterV2 public openfortPaymaster; + address public account; + TestCounter public testCounter; + TestToken public testToken; + + // Testing addresses + address private factoryAdmin; + uint256 private factoryAdminPKey; + + address private paymasterAdmin; + uint256 private paymasterAdminPKey; + + address private accountAdmin; + uint256 private accountAdminPKey; + + address payable private beneficiary = payable(makeAddr("beneficiary")); + + uint48 internal constant VALIDUNTIL = 2 ** 48 - 1; + uint48 internal constant VALIDAFTER = 0; + uint256 internal constant EXCHANGERATE = 10 ** 3; + uint256 internal constant MOCKSIG = 2 ** 256 - 1; + uint256 internal TESTTOKEN_ACCOUNT_PREFUND = 100 * 10 ** 18; + + error InvalidTokenRecipient(); + + /* + * Auxiliary function to generate a userOP + */ + function _setupUserOp( + address sender, + uint256 _signerPKey, + bytes memory _initCode, + bytes memory _callDataForEntrypoint, + bytes memory paymasterAndData + ) internal returns (UserOperation[] memory ops) { + // Get user op fields + UserOperation memory op = UserOperation({ + sender: sender, + nonce: entryPoint.getNonce(sender, 0), + initCode: _initCode, + callData: _callDataForEntrypoint, + callGasLimit: 500_000, + verificationGasLimit: 500_000, + preVerificationGas: 500_000, + maxFeePerGas: 1500000000, + maxPriorityFeePerGas: 1500000000, + paymasterAndData: paymasterAndData, + signature: bytes("") + }); + + // Sign UserOp + bytes32 opHash = EntryPoint(entryPoint).getUserOpHash(op); + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerPKey, msgHash); + bytes memory userOpSignature = abi.encodePacked(r, s, v); + + // Verifications below commented to avoid "Stack too deep" error + // address recoveredSigner = ECDSA.recover(msgHash, v, r, s); + // address expectedSigner = vm.addr(_signerPKey); + assertEq(ECDSA.recover(msgHash, v, r, s), vm.addr(_signerPKey)); + + op.signature = userOpSignature; + + // Store UserOp + ops = new UserOperation[](1); + ops[0] = op; + } + + /* + * Auxiliary function to generate a userOP using the execute() + * from the account + */ + function _setupUserOpExecute( + address sender, + uint256 _signerPKey, + bytes memory _initCode, + address _target, + uint256 _value, + bytes memory _callData, + bytes memory paymasterAndData + ) internal returns (UserOperation[] memory) { + bytes memory callDataForEntrypoint = + abi.encodeWithSignature("execute(address,uint256,bytes)", _target, _value, _callData); + + return _setupUserOp(sender, _signerPKey, _initCode, callDataForEntrypoint, paymasterAndData); + } + + /* + * Auxiliary function to generate a userOP using the executeBatch() + * from the account + */ + function _setupUserOpExecuteBatch( + address sender, + uint256 _signerPKey, + bytes memory _initCode, + address[] memory _target, + uint256[] memory _value, + bytes[] memory _callData, + bytes memory paymasterAndData + ) internal returns (UserOperation[] memory) { + bytes memory callDataForEntrypoint = + abi.encodeWithSignature("executeBatch(address[],uint256[],bytes[])", _target, _value, _callData); + + return _setupUserOp(sender, _signerPKey, _initCode, callDataForEntrypoint, paymasterAndData); + } + + function mockedPaymasterDataNative() internal view returns (bytes memory dataEncoded) { + OpenfortPaymasterV2.PolicyStrategy memory strategy; + strategy.paymasterMode = OpenfortPaymasterV2.Mode.PayForUser; + strategy.depositor = address(paymasterAdmin); + strategy.erc20Token = address(0); + strategy.exchangeRate = EXCHANGERATE; + // Looking at the source code, I've found this part was not Packed (filled with 0s) + dataEncoded = abi.encode(VALIDUNTIL, VALIDAFTER, strategy); + } + + function mockedPaymasterDataERC20Dynamic() internal view returns (bytes memory dataEncoded) { + OpenfortPaymasterV2.PolicyStrategy memory strategy; + strategy.paymasterMode = OpenfortPaymasterV2.Mode.DynamicRate; + strategy.depositor = address(paymasterAdmin); + strategy.erc20Token = address(testToken); + strategy.exchangeRate = EXCHANGERATE; + // Looking at the source code, I've found this part was not Packed (filled with 0s) + dataEncoded = abi.encode(VALIDUNTIL, VALIDAFTER, strategy); + } + + function mockedPaymasterDataERC20Fixed(uint256 pricePerTransaction) + internal + view + returns (bytes memory dataEncoded) + { + OpenfortPaymasterV2.PolicyStrategy memory strategy; + strategy.paymasterMode = OpenfortPaymasterV2.Mode.FixedRate; + strategy.depositor = address(paymasterAdmin); + strategy.erc20Token = address(testToken); + strategy.exchangeRate = pricePerTransaction; + // Looking at the source code, I've found this part was not Packed (filled with 0s) + dataEncoded = abi.encode(VALIDUNTIL, VALIDAFTER, strategy); + } + + /** + * @notice Initialize the StaticOpenfortAccount testing contract. + * Scenario: + * - factoryAdmin is the deployer (and owner) of the StaticOpenfortFactory + * - paymasterAdmin is the deployer (and owner) of the OpenfortPaymaster + * - accountAdmin is the account used to deploy new static accounts + * - entryPoint is the singleton EntryPoint + * - testCounter is the counter used to test userOps + */ + function setUp() public { + // Setup and fund signers + (factoryAdmin, factoryAdminPKey) = makeAddrAndKey("factoryAdmin"); + vm.deal(factoryAdmin, 100 ether); + (accountAdmin, accountAdminPKey) = makeAddrAndKey("accountAdmin"); + vm.deal(accountAdmin, 100 ether); + (paymasterAdmin, paymasterAdminPKey) = makeAddrAndKey("paymasterAdmin"); + vm.deal(paymasterAdmin, 100 ether); + + // If we are in a fork + if (vm.envAddress("ENTRY_POINT_ADDRESS").code.length > 0) { + entryPoint = EntryPoint(payable(vm.envAddress("ENTRY_POINT_ADDRESS"))); + } + // If not a fork, deploy entryPoint (at correct address) + else { + EntryPoint entryPoint_aux = new EntryPoint(); + bytes memory code = address(entryPoint_aux).code; + address targetAddr = address(vm.envAddress("ENTRY_POINT_ADDRESS")); + vm.etch(targetAddr, code); + entryPoint = EntryPoint(payable(targetAddr)); + } + vm.prank(paymasterAdmin); + openfortPaymaster = new OpenfortPaymasterV2(IEntryPoint(payable(address(entryPoint))), paymasterAdmin); + // Fund the paymaster with 100 ETH + vm.deal(address(openfortPaymaster), 100 ether); + // Paymaster deposits 50 ETH to EntryPoint + openfortPaymaster.depositFor{value: 50 ether}(paymasterAdmin); + // Paymaster stakes 25 ETH + vm.prank(paymasterAdmin); + openfortPaymaster.addStake{value: 25 ether}(1); + + // deploy account factory + vm.prank(factoryAdmin); + staticOpenfortAccount = new StaticOpenfortAccount(); + vm.prank(factoryAdmin); + staticOpenfortFactory = + new StaticOpenfortFactory((payable(vm.envAddress("ENTRY_POINT_ADDRESS"))), address(staticOpenfortAccount)); + // deploy a new TestCounter + testCounter = new TestCounter(); + // deploy a new TestToken (ERC20) and mint 1000 + testToken = new TestToken(); + testToken.mint(address(this), 1_000 * 10 ** 18); + + // Create an static account wallet and get its address + vm.prank(factoryAdmin); + account = staticOpenfortFactory.createAccountWithNonce(accountAdmin, "1"); + } + + /* + * Test initial parameters + * + */ + function testInitialParameters() public { + assertEq(address(openfortPaymaster.entryPoint()), vm.envAddress("ENTRY_POINT_ADDRESS")); + assertEq(address(openfortPaymaster.owner()), paymasterAdmin); + } + + /** + * Deposit should fail + */ + function testFailDeposit() public { + vm.prank(factoryAdmin); + openfortPaymaster.deposit{value: 50 ether}(); + } + + /* + * Test parsePaymasterAndData() when using the native token + * + */ + function testParsePaymasterDataNative() public { + // Encode the paymaster data + bytes memory dataEncoded = mockedPaymasterDataNative(); + + // Get the related paymaster data signature + bytes32 hash = keccak256(dataEncoded); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(paymasterAdminPKey, hash); + bytes memory signature = abi.encodePacked(r, s, v); + + // Create the paymasterAndData info + bytes memory paymasterAndData = abi.encodePacked(address(openfortPaymaster), dataEncoded, signature); // This part was packed (not filled with 0s) + + ( + uint48 returnedValidUntil, + uint48 returnedValidAfter, + OpenfortPaymasterV2.PolicyStrategy memory strategy, + bytes memory returnedSignature + ) = openfortPaymaster.parsePaymasterAndData(paymasterAndData); + assertEq(returnedValidUntil, VALIDUNTIL); + assertEq(returnedValidAfter, VALIDAFTER); + assertEq(strategy.erc20Token, address(0)); + assertEq(strategy.exchangeRate, EXCHANGERATE); + assertEq(signature, returnedSignature); + } + + /* + * Test parsePaymasterAndData() with an ERC20 dynamic + * + */ + function testParsePaymasterDataERC20() public { + // Encode the paymaster data + bytes memory dataEncoded = mockedPaymasterDataERC20Dynamic(); + + // Get the related paymaster data signature + bytes32 hash = keccak256(dataEncoded); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(paymasterAdminPKey, hash); + bytes memory signature = abi.encodePacked(r, s, v); + + // Create the paymasterAndData info + bytes memory paymasterAndData = abi.encodePacked(address(openfortPaymaster), dataEncoded, signature); // This part was packed (not filled with 0s) + console.logBytes(paymasterAndData); + + ( + uint48 returnedValidUntil, + uint48 returnedValidAfter, + OpenfortPaymasterV2.PolicyStrategy memory strategy, + bytes memory returnedSignature + ) = openfortPaymaster.parsePaymasterAndData(paymasterAndData); + assertEq(returnedValidUntil, VALIDUNTIL); + assertEq(returnedValidAfter, VALIDAFTER); + assertEq(strategy.erc20Token, address(testToken)); + assertEq(strategy.exchangeRate, EXCHANGERATE); + assertEq(signature, returnedSignature); + } + + /* + * The owner (paymasterAdmin) can add stake + * Others cannot + */ + function testPaymasterAddStake() public { + // The owner can add stake + vm.prank(paymasterAdmin); + openfortPaymaster.addStake{value: 2}(1); + + // Others cannot add stake + vm.expectRevert("Ownable: caller is not the owner"); + openfortPaymaster.addStake{value: 2}(1); + } + + /* + * Deposit 2 ETH to the EntryPoint on Paymaster's behalf + * + */ + function testEntryPointDepositToPaymaster() public { + assert(entryPoint.balanceOf(address(openfortPaymaster)) == 50 ether); + + // Directly deposit 1 ETH to EntryPoint on behalf of paymaster + entryPoint.depositTo{value: 1 ether}(address(openfortPaymaster)); + assert(entryPoint.balanceOf(address(openfortPaymaster)) == 51 ether); + + // Paymaster deposits 1 ETH to EntryPoint + openfortPaymaster.depositFor{value: 1 ether}(address(factoryAdmin)); + assert(openfortPaymaster.getDeposit() == 52 ether); + assert(openfortPaymaster.getDepositFor(address(openfortPaymaster)) == 0 ether); + assert(openfortPaymaster.getDepositFor(address(factoryAdmin)) == 1 ether); + } + + /* + * Test sending a userOp with an invalid paymasterAndData (valid paymaster, but invalid sig length) + * Should revert + */ + function testPaymasterUserOpWrongSigLength() public { + bytes memory dataEncoded = mockedPaymasterDataERC20Dynamic(); + + bytes memory paymasterAndData = abi.encodePacked(address(openfortPaymaster), dataEncoded, "0x1234"); // This part was packed (not filled with 0s) + + UserOperation[] memory userOp = _setupUserOpExecute( + account, + accountAdminPKey, + bytes(""), + address(testCounter), + 0, + abi.encodeWithSignature("count()"), + paymasterAndData + ); + + // "ECDSA: invalid signature length" + vm.expectRevert(); + entryPoint.simulateValidation(userOp[0]); + + // Verifiy that the counter has not increased + assertEq(testCounter.counters(account), 0); + } + + /* + * Test sending a userOp with an invalid paymasterAndData (valid paymaster, but invalid sig) + * Should revert + */ + function testPaymasterUserOpWrongSig() public { + bytes memory dataEncoded = mockedPaymasterDataERC20Dynamic(); + + bytes memory paymasterAndData = abi.encodePacked(address(openfortPaymaster), dataEncoded, MOCKSIG, "1", MOCKSIG); // MOCKSIG, "1", MOCKSIG to make sure we send 65 bytes as sig + UserOperation[] memory userOp = _setupUserOpExecute( + account, + accountAdminPKey, + bytes(""), + address(testCounter), + 0, + abi.encodeWithSignature("count()"), + paymasterAndData + ); + + // "AA33 reverted: ECDSA: invalid signature" + vm.expectRevert(); + entryPoint.simulateValidation(userOp[0]); + + // Verifiy that the counter has not increased + assertEq(testCounter.counters(account), 0); + } + + /* + * Test sending a userOp with an valid paymasterAndData (valid paymaster, valid sig) + * Should work + */ + function testPaymasterUserOpNativeValidSig() public { + bytes memory dataEncoded = mockedPaymasterDataNative(); + + bytes memory paymasterAndData = abi.encodePacked(address(openfortPaymaster), dataEncoded, MOCKSIG, "1", MOCKSIG); + + UserOperation[] memory userOps = _setupUserOpExecute( + account, + accountAdminPKey, + bytes(""), + address(testCounter), + 0, + abi.encodeWithSignature("count()"), + paymasterAndData + ); + OpenfortPaymasterV2.PolicyStrategy memory strategy; + strategy.paymasterMode = OpenfortPaymasterV2.Mode.PayForUser; + strategy.erc20Token = address(0); + strategy.exchangeRate = EXCHANGERATE; + bytes32 hash; + { + // Simulating that the Paymaster gets the userOp and signs it + hash = ECDSA.toEthSignedMessageHash(openfortPaymaster.getHash(userOps[0], VALIDUNTIL, VALIDAFTER, strategy)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(paymasterAdminPKey, hash); + bytes memory signature = abi.encodePacked(r, s, v); + bytes memory paymasterAndDataSigned = abi.encodePacked(address(openfortPaymaster), dataEncoded, signature); // This part was packed (not filled with 0s) + assertEq(openfortPaymaster.owner(), ECDSA.recover(hash, signature)); + userOps[0].paymasterAndData = paymasterAndDataSigned; + } + + // Back to the user. Sign the userOp + bytes memory userOpSignature; + bytes32 hash2; + { + bytes32 opHash = EntryPoint(entryPoint).getUserOpHash(userOps[0]); + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountAdminPKey, msgHash); + userOpSignature = abi.encodePacked(r, s, v); + + // Verifications below commented to avoid "Stack too deep" error + assertEq(ECDSA.recover(msgHash, v, r, s), vm.addr(accountAdminPKey)); + + // Should return account admin + hash2 = + ECDSA.toEthSignedMessageHash(openfortPaymaster.getHash(userOps[0], VALIDUNTIL, VALIDAFTER, strategy)); + } + + // The hash of the userOp should not have changed after the inclusion of the sig + assertEq(hash, hash2); + userOps[0].signature = userOpSignature; + + // Get the paymaster deposit before handling the userOp + uint256 paymasterDepositBefore = openfortPaymaster.getDeposit(); + + entryPoint.handleOps(userOps, beneficiary); + // entryPoint.simulateValidation(userOp); + + // Verify that the paymaster has less deposit now + assert(paymasterDepositBefore > openfortPaymaster.getDeposit()); + //Verifiy that the counter has increased + assertEq(testCounter.counters(account), 1); + } + + /* + * Test sending a userOp with signature from a wrong address + * Should not work + */ + function testPaymasterUserOpNativeWrongUserSig() public { + bytes memory dataEncoded = mockedPaymasterDataNative(); + + bytes memory paymasterAndData = abi.encodePacked(address(openfortPaymaster), dataEncoded, MOCKSIG, "1", MOCKSIG); + + UserOperation[] memory userOps = _setupUserOpExecute( + account, + accountAdminPKey, + bytes(""), + address(testCounter), + 0, + abi.encodeWithSignature("count()"), + paymasterAndData + ); + OpenfortPaymasterV2.PolicyStrategy memory strategy; + strategy.paymasterMode = OpenfortPaymasterV2.Mode.PayForUser; + strategy.erc20Token = address(0); + strategy.exchangeRate = EXCHANGERATE; + bytes32 hash; + { + // Simulating that the factory admin gets the userOp and tries to sign it + hash = ECDSA.toEthSignedMessageHash(openfortPaymaster.getHash(userOps[0], VALIDUNTIL, VALIDAFTER, strategy)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(factoryAdminPKey, hash); + bytes memory signature = abi.encodePacked(r, s, v); + bytes memory paymasterAndDataSigned = abi.encodePacked(address(openfortPaymaster), dataEncoded, signature); // This part was packed (not filled with 0s) + assertEq(factoryAdmin, ECDSA.recover(hash, signature)); + userOps[0].paymasterAndData = paymasterAndDataSigned; + } + + // Back to the user. Sign the userOp + bytes memory userOpSignature; + bytes32 hash2; + { + bytes32 opHash = EntryPoint(entryPoint).getUserOpHash(userOps[0]); + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(factoryAdminPKey, msgHash); + userOpSignature = abi.encodePacked(r, s, v); + + // Verifications below commented to avoid "Stack too deep" error + assertEq(ECDSA.recover(msgHash, v, r, s), vm.addr(factoryAdminPKey)); + + // Should return account admin + hash2 = + ECDSA.toEthSignedMessageHash(openfortPaymaster.getHash(userOps[0], VALIDUNTIL, VALIDAFTER, strategy)); + } + + // The hash of the userOp should not have changed after the inclusion of the sig + assertEq(hash, hash2); + userOps[0].signature = userOpSignature; + + // Get the paymaster deposit before handling the userOp + uint256 paymasterDepositBefore = openfortPaymaster.getDeposit(); + + vm.expectRevert(); + entryPoint.handleOps(userOps, beneficiary); + // entryPoint.simulateValidation(userOp); + + // Verify that the paymaster has less deposit now + assert(paymasterDepositBefore == openfortPaymaster.getDeposit()); + //Verifiy that the counter has not increased + assertEq(testCounter.counters(account), 0); + } + + /* + * Test sending a userOp with an valid paymasterAndData (valid paymaster, valid sig) + * Using ERC20. Should work + */ + function testPaymasterUserOpERC20ValidSigDiffMaxPriorityFeePerGas() public { + assertEq(testToken.balanceOf(account), 0); + testToken.mint(account, TESTTOKEN_ACCOUNT_PREFUND); + assertEq(testToken.balanceOf(account), TESTTOKEN_ACCOUNT_PREFUND); + + bytes memory dataEncoded = mockedPaymasterDataERC20Dynamic(); + + bytes memory paymasterAndData = abi.encodePacked(address(openfortPaymaster), dataEncoded, MOCKSIG, "1", MOCKSIG); + + // Create a userOp to let the paymaster use our testTokens + UserOperation[] memory userOps = _setupUserOpExecute( + account, + accountAdminPKey, + bytes(""), + address(testToken), + 0, + abi.encodeWithSignature("approve(address,uint256)", address(openfortPaymaster), 2 ** 256 - 1), + paymasterAndData + ); + + userOps[0].maxPriorityFeePerGas += 1; + OpenfortPaymasterV2.PolicyStrategy memory strategy; + strategy.paymasterMode = OpenfortPaymasterV2.Mode.DynamicRate; + strategy.erc20Token = address(testToken); + strategy.exchangeRate = EXCHANGERATE; + + // Simulating that the Paymaster gets the userOp and signs it + bytes32 hash; + { + hash = ECDSA.toEthSignedMessageHash(openfortPaymaster.getHash(userOps[0], VALIDUNTIL, VALIDAFTER, strategy)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(paymasterAdminPKey, hash); + bytes memory signature = abi.encodePacked(r, s, v); + bytes memory paymasterAndDataSigned = abi.encodePacked(address(openfortPaymaster), dataEncoded, signature); // This part was packed (not filled with 0s) + assertEq(openfortPaymaster.owner(), ECDSA.recover(hash, signature)); + userOps[0].paymasterAndData = paymasterAndDataSigned; + } + + // Back to the user. Sign the userOp + bytes memory userOpSignature; + bytes32 hash2; + { + bytes32 opHash = EntryPoint(entryPoint).getUserOpHash(userOps[0]); + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountAdminPKey, msgHash); + userOpSignature = abi.encodePacked(r, s, v); + + // Verifications below commented to avoid "Stack too deep" error + assertEq(ECDSA.recover(msgHash, v, r, s), vm.addr(accountAdminPKey)); + // Should return account admin + hash2 = + ECDSA.toEthSignedMessageHash(openfortPaymaster.getHash(userOps[0], VALIDUNTIL, VALIDAFTER, strategy)); + } + + // The hash of the userOp should not have changed after the inclusion of the sig + assertEq(hash, hash2); + userOps[0].signature = userOpSignature; + + // Get the paymaster deposit before handling the userOp + uint256 paymasterDepositBefore = openfortPaymaster.getDeposit(); + + entryPoint.handleOps(userOps, beneficiary); + + // Verify that the paymaster has less deposit now + assert(paymasterDepositBefore > openfortPaymaster.getDeposit()); + // Verifiy that the balance of the smart account has decreased + assert(testToken.balanceOf(account) < TESTTOKEN_ACCOUNT_PREFUND); + } + + /* + * Test sending a userOp with an valid paymasterAndData (valid paymaster, valid sig) + * Using ERC20. Should work + */ + function testPaymasterUserOpERC20ValidSig() public { + assertEq(testToken.balanceOf(account), 0); + testToken.mint(account, TESTTOKEN_ACCOUNT_PREFUND); + assertEq(testToken.balanceOf(account), TESTTOKEN_ACCOUNT_PREFUND); + + bytes memory dataEncoded = mockedPaymasterDataERC20Dynamic(); + + bytes memory paymasterAndData = abi.encodePacked(address(openfortPaymaster), dataEncoded, MOCKSIG, "1", MOCKSIG); + + // Create a userOp to let the paymaster use our testTokens + UserOperation[] memory userOps = _setupUserOpExecute( + account, + accountAdminPKey, + bytes(""), + address(testToken), + 0, + abi.encodeWithSignature("approve(address,uint256)", address(openfortPaymaster), 2 ** 256 - 1), + paymasterAndData + ); + + OpenfortPaymasterV2.PolicyStrategy memory strategy; + strategy.paymasterMode = OpenfortPaymasterV2.Mode.DynamicRate; + strategy.erc20Token = address(testToken); + strategy.exchangeRate = EXCHANGERATE; + + // Simulating that the Paymaster gets the userOp and signs it + bytes32 hash; + { + hash = ECDSA.toEthSignedMessageHash(openfortPaymaster.getHash(userOps[0], VALIDUNTIL, VALIDAFTER, strategy)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(paymasterAdminPKey, hash); + bytes memory signature = abi.encodePacked(r, s, v); + bytes memory paymasterAndDataSigned = abi.encodePacked(address(openfortPaymaster), dataEncoded, signature); // This part was packed (not filled with 0s) + assertEq(openfortPaymaster.owner(), ECDSA.recover(hash, signature)); + userOps[0].paymasterAndData = paymasterAndDataSigned; + } + + // Back to the user. Sign the userOp + bytes memory userOpSignature; + bytes32 hash2; + { + bytes32 opHash = EntryPoint(entryPoint).getUserOpHash(userOps[0]); + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountAdminPKey, msgHash); + userOpSignature = abi.encodePacked(r, s, v); + + // Verifications below commented to avoid "Stack too deep" error + assertEq(ECDSA.recover(msgHash, v, r, s), vm.addr(accountAdminPKey)); + // Should return account admin + hash2 = + ECDSA.toEthSignedMessageHash(openfortPaymaster.getHash(userOps[0], VALIDUNTIL, VALIDAFTER, strategy)); + } + + // The hash of the userOp should not have changed after the inclusion of the sig + assertEq(hash, hash2); + userOps[0].signature = userOpSignature; + + // Get the paymaster deposit before handling the userOp + uint256 paymasterDepositBefore = openfortPaymaster.getDeposit(); + + entryPoint.handleOps(userOps, beneficiary); + + // Verify that the paymaster has less deposit now + assert(paymasterDepositBefore > openfortPaymaster.getDeposit()); + // Verifiy that the balance of the smart account has decreased + assert(testToken.balanceOf(account) < TESTTOKEN_ACCOUNT_PREFUND); + } + + /* + * Test sending a userOp with an valid paymasterAndData (valid paymaster, valid sig) + * Using FIXED ERC20. Should work + */ + function testPaymasterUserOpERC20FixedValidSig() public { + assertEq(testToken.balanceOf(account), 0); + testToken.mint(account, TESTTOKEN_ACCOUNT_PREFUND); + assertEq(testToken.balanceOf(account), TESTTOKEN_ACCOUNT_PREFUND); + uint256 pricePerTransaction = 10 ** 18; + + bytes memory dataEncoded = mockedPaymasterDataERC20Fixed(pricePerTransaction); + + bytes memory paymasterAndData = abi.encodePacked(address(openfortPaymaster), dataEncoded, MOCKSIG, "1", MOCKSIG); + + // Create a userOp to let the paymaster use our testTokens + UserOperation[] memory userOps = _setupUserOpExecute( + account, + accountAdminPKey, + bytes(""), + address(testToken), + 0, + abi.encodeWithSignature("approve(address,uint256)", address(openfortPaymaster), 2 ** 256 - 1), + paymasterAndData + ); + + OpenfortPaymasterV2.PolicyStrategy memory strategy; + strategy.paymasterMode = OpenfortPaymasterV2.Mode.FixedRate; + strategy.depositor = address(paymasterAdmin); + strategy.erc20Token = address(testToken); + strategy.exchangeRate = pricePerTransaction; + + // Simulating that the Paymaster gets the userOp and signs it + bytes32 hash; + { + hash = ECDSA.toEthSignedMessageHash(openfortPaymaster.getHash(userOps[0], VALIDUNTIL, VALIDAFTER, strategy)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(paymasterAdminPKey, hash); + bytes memory signature = abi.encodePacked(r, s, v); + bytes memory paymasterAndDataSigned = abi.encodePacked(address(openfortPaymaster), dataEncoded, signature); // This part was packed (not filled with 0s) + assertEq(openfortPaymaster.owner(), ECDSA.recover(hash, signature)); + userOps[0].paymasterAndData = paymasterAndDataSigned; + } + + // Back to the user. Sign the userOp + bytes memory userOpSignature; + bytes32 hash2; + { + bytes32 opHash = EntryPoint(entryPoint).getUserOpHash(userOps[0]); + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountAdminPKey, msgHash); + userOpSignature = abi.encodePacked(r, s, v); + + // Verifications below commented to avoid "Stack too deep" error + assertEq(ECDSA.recover(msgHash, v, r, s), vm.addr(accountAdminPKey)); + // Should return account admin + hash2 = + ECDSA.toEthSignedMessageHash(openfortPaymaster.getHash(userOps[0], VALIDUNTIL, VALIDAFTER, strategy)); + } + + // The hash of the userOp should not have changed after the inclusion of the sig + assertEq(hash, hash2); + userOps[0].signature = userOpSignature; + + // Get the paymaster deposit before handling the userOp + uint256 paymasterDepositBefore = openfortPaymaster.getDeposit(); + + entryPoint.handleOps(userOps, beneficiary); + + // Verify that the paymaster has less deposit now + assert(paymasterDepositBefore > openfortPaymaster.getDeposit()); + // Verifiy that the balance of the smart account has decreased + assert(testToken.balanceOf(account) == TESTTOKEN_ACCOUNT_PREFUND-pricePerTransaction); + } + + /* + * Test sending a userOp with an valid paymasterAndData (valid paymaster, valid sig) + * ExecBatch. Using dynamic ERC20. Should work + */ + function testPaymasterUserOpERC20ValidSigExecBatch() public { + assertEq(testToken.balanceOf(account), 0); + testToken.mint(account, TESTTOKEN_ACCOUNT_PREFUND); + assertEq(testToken.balanceOf(account), TESTTOKEN_ACCOUNT_PREFUND); + + assertEq(testCounter.counters(account), 0); + + bytes memory dataEncoded = mockedPaymasterDataERC20Dynamic(); + + bytes memory paymasterAndData = abi.encodePacked(address(openfortPaymaster), dataEncoded, MOCKSIG, "1", MOCKSIG); + + uint256 count = 2; + address[] memory targets = new address[](count); + uint256[] memory values = new uint256[](count); + bytes[] memory callData = new bytes[](count); + + targets[0] = address(testToken); + values[0] = 0; + callData[0] = abi.encodeWithSignature("approve(address,uint256)", address(openfortPaymaster), 2 ** 256 - 1); + + targets[1] = address(testCounter); + values[1] = 0; + callData[1] = abi.encodeWithSignature("count()"); + + // Create a userOp to let the paymaster use our testTokens + UserOperation[] memory userOps = + _setupUserOpExecuteBatch(account, accountAdminPKey, bytes(""), targets, values, callData, paymasterAndData); + + OpenfortPaymasterV2.PolicyStrategy memory strategy; + strategy.paymasterMode = OpenfortPaymasterV2.Mode.DynamicRate; + strategy.depositor = address(paymasterAdmin); + strategy.erc20Token = address(testToken); + strategy.exchangeRate = EXCHANGERATE; + + // Simulating that the Paymaster gets the userOp and signs it + bytes32 hash; + { + hash = ECDSA.toEthSignedMessageHash(openfortPaymaster.getHash(userOps[0], VALIDUNTIL, VALIDAFTER, strategy)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(paymasterAdminPKey, hash); + bytes memory signature = abi.encodePacked(r, s, v); + bytes memory paymasterAndDataSigned = abi.encodePacked(address(openfortPaymaster), dataEncoded, signature); // This part was packed (not filled with 0s) + assertEq(openfortPaymaster.owner(), ECDSA.recover(hash, signature)); + userOps[0].paymasterAndData = paymasterAndDataSigned; + } + + // Back to the user. Sign the userOp + bytes memory userOpSignature; + bytes32 hash2; + { + bytes32 opHash = EntryPoint(entryPoint).getUserOpHash(userOps[0]); + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountAdminPKey, msgHash); + userOpSignature = abi.encodePacked(r, s, v); + + // Verifications below commented to avoid "Stack too deep" error + assertEq(ECDSA.recover(msgHash, v, r, s), vm.addr(accountAdminPKey)); + // Should return account admin + hash2 = + ECDSA.toEthSignedMessageHash(openfortPaymaster.getHash(userOps[0], VALIDUNTIL, VALIDAFTER, strategy)); + } + + // The hash of the userOp should not have changed after the inclusion of the sig + assertEq(hash, hash2); + userOps[0].signature = userOpSignature; + + // Get the paymaster deposit before handling the userOp + uint256 paymasterDepositBefore = openfortPaymaster.getDeposit(); + + entryPoint.handleOps(userOps, beneficiary); + + // Verify that the paymaster has less deposit now + assert(paymasterDepositBefore > openfortPaymaster.getDeposit()); + // Verifiy that the balance of the smart account has decreased + assert(testToken.balanceOf(account) < TESTTOKEN_ACCOUNT_PREFUND); + assertEq(testCounter.counters(account), 1); + } + + /* + * Test sending a userOp with an valid paymasterAndData (valid paymaster, valid sig) + * ExecBatch. Using fixed ERC20. Should work + */ + function testPaymasterUserOpERC20FixedValidSigExecBatch() public { + assertEq(testToken.balanceOf(account), 0); + testToken.mint(account, TESTTOKEN_ACCOUNT_PREFUND); + assertEq(testToken.balanceOf(account), TESTTOKEN_ACCOUNT_PREFUND); + + assertEq(testCounter.counters(account), 0); + + uint256 pricePerTransaction = 10 ** 18; + + bytes memory dataEncoded = mockedPaymasterDataERC20Fixed(pricePerTransaction); + + bytes memory paymasterAndData = abi.encodePacked(address(openfortPaymaster), dataEncoded, MOCKSIG, "1", MOCKSIG); + + uint256 count = 2; + address[] memory targets = new address[](count); + uint256[] memory values = new uint256[](count); + bytes[] memory callData = new bytes[](count); + + targets[0] = address(testToken); + values[0] = 0; + callData[0] = abi.encodeWithSignature("approve(address,uint256)", address(openfortPaymaster), 2 ** 256 - 1); + + targets[1] = address(testCounter); + values[1] = 0; + callData[1] = abi.encodeWithSignature("count()"); + + // Create a userOp to let the paymaster use our testTokens + UserOperation[] memory userOps = + _setupUserOpExecuteBatch(account, accountAdminPKey, bytes(""), targets, values, callData, paymasterAndData); + + OpenfortPaymasterV2.PolicyStrategy memory strategy; + strategy.paymasterMode = OpenfortPaymasterV2.Mode.FixedRate; + strategy.depositor = address(paymasterAdmin); + strategy.erc20Token = address(testToken); + strategy.exchangeRate = pricePerTransaction; + + // Simulating that the Paymaster gets the userOp and signs it + bytes32 hash; + { + hash = ECDSA.toEthSignedMessageHash(openfortPaymaster.getHash(userOps[0], VALIDUNTIL, VALIDAFTER, strategy)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(paymasterAdminPKey, hash); + bytes memory signature = abi.encodePacked(r, s, v); + bytes memory paymasterAndDataSigned = abi.encodePacked(address(openfortPaymaster), dataEncoded, signature); // This part was packed (not filled with 0s) + assertEq(openfortPaymaster.owner(), ECDSA.recover(hash, signature)); + userOps[0].paymasterAndData = paymasterAndDataSigned; + } + + // Back to the user. Sign the userOp + bytes memory userOpSignature; + bytes32 hash2; + { + bytes32 opHash = EntryPoint(entryPoint).getUserOpHash(userOps[0]); + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountAdminPKey, msgHash); + userOpSignature = abi.encodePacked(r, s, v); + + // Verifications below commented to avoid "Stack too deep" error + assertEq(ECDSA.recover(msgHash, v, r, s), vm.addr(accountAdminPKey)); + // Should return account admin + hash2 = + ECDSA.toEthSignedMessageHash(openfortPaymaster.getHash(userOps[0], VALIDUNTIL, VALIDAFTER, strategy)); + } + + // The hash of the userOp should not have changed after the inclusion of the sig + assertEq(hash, hash2); + userOps[0].signature = userOpSignature; + + // Get the paymaster deposit before handling the userOp + uint256 paymasterDepositBefore = openfortPaymaster.getDeposit(); + + entryPoint.handleOps(userOps, beneficiary); + + // Verify that the paymaster has less deposit now + assert(paymasterDepositBefore > openfortPaymaster.getDeposit()); + // Verifiy that the balance of the smart account has decreased + assert(testToken.balanceOf(account) == TESTTOKEN_ACCOUNT_PREFUND-pricePerTransaction); + assertEq(testCounter.counters(account), 1); + } + + /* + * Test sending a userOp with an valid paymasterAndData (valid paymaster, valid sig) + * ExecBatch. Using fixed ERC20 expensive. Should work + */ + function testPaymasterUserOpERC20FixedExpensiveValidSigExecBatch() public { + assertEq(testToken.balanceOf(account), 0); + testToken.mint(account, TESTTOKEN_ACCOUNT_PREFUND); + assertEq(testToken.balanceOf(account), TESTTOKEN_ACCOUNT_PREFUND); + + assertEq(testCounter.counters(account), 0); + + uint256 pricePerTransaction = 10; + + bytes memory dataEncoded = mockedPaymasterDataERC20Fixed(pricePerTransaction); + + bytes memory paymasterAndData = abi.encodePacked(address(openfortPaymaster), dataEncoded, MOCKSIG, "1", MOCKSIG); + + uint256 count = 2; + address[] memory targets = new address[](count); + uint256[] memory values = new uint256[](count); + bytes[] memory callData = new bytes[](count); + + targets[0] = address(testToken); + values[0] = 0; + callData[0] = abi.encodeWithSignature("approve(address,uint256)", address(openfortPaymaster), 2 ** 256 - 1); + + targets[1] = address(testCounter); + values[1] = 0; + callData[1] = abi.encodeWithSignature("count()"); + + // Create a userOp to let the paymaster use our testTokens + UserOperation[] memory userOps = + _setupUserOpExecuteBatch(account, accountAdminPKey, bytes(""), targets, values, callData, paymasterAndData); + + OpenfortPaymasterV2.PolicyStrategy memory strategy; + strategy.paymasterMode = OpenfortPaymasterV2.Mode.FixedRate; + strategy.depositor = address(paymasterAdmin); + strategy.erc20Token = address(testToken); + strategy.exchangeRate = pricePerTransaction; + + // Simulating that the Paymaster gets the userOp and signs it + bytes32 hash; + { + hash = ECDSA.toEthSignedMessageHash(openfortPaymaster.getHash(userOps[0], VALIDUNTIL, VALIDAFTER, strategy)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(paymasterAdminPKey, hash); + bytes memory signature = abi.encodePacked(r, s, v); + bytes memory paymasterAndDataSigned = abi.encodePacked(address(openfortPaymaster), dataEncoded, signature); // This part was packed (not filled with 0s) + assertEq(openfortPaymaster.owner(), ECDSA.recover(hash, signature)); + userOps[0].paymasterAndData = paymasterAndDataSigned; + } + + // Back to the user. Sign the userOp + bytes memory userOpSignature; + bytes32 hash2; + { + bytes32 opHash = EntryPoint(entryPoint).getUserOpHash(userOps[0]); + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountAdminPKey, msgHash); + userOpSignature = abi.encodePacked(r, s, v); + + // Verifications below commented to avoid "Stack too deep" error + assertEq(ECDSA.recover(msgHash, v, r, s), vm.addr(accountAdminPKey)); + // Should return account admin + hash2 = + ECDSA.toEthSignedMessageHash(openfortPaymaster.getHash(userOps[0], VALIDUNTIL, VALIDAFTER, strategy)); + } + + // The hash of the userOp should not have changed after the inclusion of the sig + assertEq(hash, hash2); + userOps[0].signature = userOpSignature; + + // Get the paymaster deposit before handling the userOp + uint256 paymasterDepositBefore = openfortPaymaster.getDeposit(); + + entryPoint.handleOps(userOps, beneficiary); + + // Verify that the paymaster has less deposit now + assert(paymasterDepositBefore > openfortPaymaster.getDeposit()); + // Verifiy that the balance of the smart account has decreased + assert(testToken.balanceOf(account) == TESTTOKEN_ACCOUNT_PREFUND-pricePerTransaction); + assertEq(testCounter.counters(account), 1); + } + + /* + * Test sending a userOp with an valid paymasterAndData (valid paymaster, valid sig) + * ExecBatch. Should work + */ + function testPaymasterUserOpNativeValidSigExecBatch() public { + bytes memory dataEncoded = mockedPaymasterDataNative(); + + bytes memory paymasterAndData = abi.encodePacked(address(openfortPaymaster), dataEncoded, MOCKSIG, "1", MOCKSIG); + + uint256 count = 2; + address[] memory targets = new address[](count); + uint256[] memory values = new uint256[](count); + bytes[] memory callData = new bytes[](count); + + targets[0] = address(testToken); + values[0] = 0; + callData[0] = abi.encodeWithSignature("approve(address,uint256)", address(openfortPaymaster), 2 ** 256 - 1); + + targets[1] = address(testCounter); + values[1] = 0; + callData[1] = abi.encodeWithSignature("count()"); + + // Create a userOp to let the paymaster use our testTokens + UserOperation[] memory userOps = + _setupUserOpExecuteBatch(account, accountAdminPKey, bytes(""), targets, values, callData, paymasterAndData); + + OpenfortPaymasterV2.PolicyStrategy memory strategy; + strategy.paymasterMode = OpenfortPaymasterV2.Mode.PayForUser; + strategy.depositor = address(paymasterAdmin); + strategy.erc20Token = address(0); + strategy.exchangeRate = EXCHANGERATE; + + bytes32 hash; + { + // Simulating that the Paymaster gets the userOp and signs it + hash = ECDSA.toEthSignedMessageHash(openfortPaymaster.getHash(userOps[0], VALIDUNTIL, VALIDAFTER, strategy)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(paymasterAdminPKey, hash); + bytes memory signature = abi.encodePacked(r, s, v); + bytes memory paymasterAndDataSigned = abi.encodePacked(address(openfortPaymaster), dataEncoded, signature); // This part was packed (not filled with 0s) + assertEq(openfortPaymaster.owner(), ECDSA.recover(hash, signature)); + userOps[0].paymasterAndData = paymasterAndDataSigned; + } + + // Back to the user. Sign the userOp + bytes memory userOpSignature; + bytes32 hash2; + { + bytes32 opHash = EntryPoint(entryPoint).getUserOpHash(userOps[0]); + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountAdminPKey, msgHash); + userOpSignature = abi.encodePacked(r, s, v); + + // Verifications below commented to avoid "Stack too deep" error + assertEq(ECDSA.recover(msgHash, v, r, s), vm.addr(accountAdminPKey)); + // Should return account admin + hash2 = + ECDSA.toEthSignedMessageHash(openfortPaymaster.getHash(userOps[0], VALIDUNTIL, VALIDAFTER, strategy)); + } + + // The hash of the userOp should not have changed after the inclusion of the sig + assertEq(hash, hash2); + userOps[0].signature = userOpSignature; + + // Get the paymaster deposit before handling the userOp + uint256 paymasterDepositBefore = openfortPaymaster.getDeposit(); + + entryPoint.handleOps(userOps, beneficiary); + // entryPoint.simulateValidation(userOp); + + // Verify that the paymaster has less deposit now + assert(paymasterDepositBefore > openfortPaymaster.getDeposit()); + //Verifiy that the counter has increased + assertEq(testCounter.counters(account), 1); + } + + /* + * Test sending a userOp with an valid paymasterAndData (valid paymaster, valid sig) + * ExecBatch. Using ERC20. Should work. + * Test showing that failing to repay in ERC20 still spends some of Paymaster's deposit (DoS) + */ + function testFailPaymasterUserOpERC20ValidSigExecBatchInsufficientERC20() public { + assertEq(testToken.balanceOf(account), 0); + testToken.mint(account, 100); + assertEq(testToken.balanceOf(account), 100); + + assertEq(testCounter.counters(account), 0); + + bytes memory dataEncoded = mockedPaymasterDataERC20Dynamic(); + + bytes memory paymasterAndData = abi.encodePacked(address(openfortPaymaster), dataEncoded, MOCKSIG, "1", MOCKSIG); + + uint256 count = 2; + address[] memory targets = new address[](count); + uint256[] memory values = new uint256[](count); + bytes[] memory callData = new bytes[](count); + + targets[0] = address(testToken); + values[0] = 0; + callData[0] = abi.encodeWithSignature("approve(address,uint256)", address(openfortPaymaster), 2 ** 256 - 1); + + targets[1] = address(testCounter); + values[1] = 0; + callData[1] = abi.encodeWithSignature("count()"); + + // Create a userOp to let the paymaster use our testTokens + UserOperation[] memory userOps = + _setupUserOpExecuteBatch(account, accountAdminPKey, bytes(""), targets, values, callData, paymasterAndData); + + OpenfortPaymasterV2.PolicyStrategy memory strategy; + strategy.paymasterMode = OpenfortPaymasterV2.Mode.DynamicRate; + strategy.depositor = address(paymasterAdmin); + strategy.erc20Token = address(testToken); + strategy.exchangeRate = EXCHANGERATE; + + // Simulating that the Paymaster gets the userOp and signs it + bytes32 hash; + { + hash = ECDSA.toEthSignedMessageHash(openfortPaymaster.getHash(userOps[0], VALIDUNTIL, VALIDAFTER, strategy)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(paymasterAdminPKey, hash); + bytes memory signature = abi.encodePacked(r, s, v); + bytes memory paymasterAndDataSigned = abi.encodePacked(address(openfortPaymaster), dataEncoded, signature); // This part was packed (not filled with 0s) + assertEq(openfortPaymaster.owner(), ECDSA.recover(hash, signature)); + userOps[0].paymasterAndData = paymasterAndDataSigned; + } + + // Back to the user. Sign the userOp + bytes memory userOpSignature; + bytes32 hash2; + { + bytes32 opHash = EntryPoint(entryPoint).getUserOpHash(userOps[0]); + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountAdminPKey, msgHash); + userOpSignature = abi.encodePacked(r, s, v); + + // Verifications below commented to avoid "Stack too deep" error + assertEq(ECDSA.recover(msgHash, v, r, s), vm.addr(accountAdminPKey)); + // Should return account admin + hash2 = + ECDSA.toEthSignedMessageHash(openfortPaymaster.getHash(userOps[0], VALIDUNTIL, VALIDAFTER, strategy)); + } + + // The hash of the userOp should not have changed after the inclusion of the sig + assertEq(hash, hash2); + userOps[0].signature = userOpSignature; + + // Get the paymaster deposit before handling the userOp + uint256 paymasterDepositBefore = openfortPaymaster.getDeposit(); + + entryPoint.handleOps(userOps, beneficiary); + + // Verify that the paymaster has the same deposit + assert(paymasterDepositBefore == openfortPaymaster.getDeposit()); + // Verify that the balance of the smart account has not decreased + assertEq(testToken.balanceOf(account), 100); + // Verify that the counter has not increased + assertEq(testCounter.counters(account), 0); + + // If this fails, it would mean: + // 1- That the paymaster has spent some of its deposit + // 2- That the smart account could not perform the desired actions, but still has all testTokens + // An attacker could DoS the paymaster to drain its deposit + } + + /* + * Test sending a userOp with an valid paymasterAndData (valid paymaster, valid sig) + * Using ERC20. Should work. + */ + function testFailPaymasterUserOpERC20ValidSigSmallApprove() public { + assertEq(testToken.balanceOf(account), 0); + testToken.mint(account, TESTTOKEN_ACCOUNT_PREFUND); + assertEq(testToken.balanceOf(account), TESTTOKEN_ACCOUNT_PREFUND); + + bytes memory dataEncoded = mockedPaymasterDataERC20Dynamic(); + + bytes memory paymasterAndData = abi.encodePacked(address(openfortPaymaster), dataEncoded, MOCKSIG, "1", MOCKSIG); + + // Create a userOp to let the paymaster use our testTokens + UserOperation[] memory userOps = _setupUserOpExecute( + account, + accountAdminPKey, + bytes(""), + address(testToken), + 0, + abi.encodeWithSignature("approve(address,uint256)", address(openfortPaymaster), 1), + paymasterAndData + ); + + OpenfortPaymasterV2.PolicyStrategy memory strategy; + strategy.paymasterMode = OpenfortPaymasterV2.Mode.DynamicRate; + strategy.depositor = address(paymasterAdmin); + strategy.erc20Token = address(testToken); + strategy.exchangeRate = EXCHANGERATE; + + // Simulating that the Paymaster gets the userOp and signs it + bytes32 hash; + { + hash = ECDSA.toEthSignedMessageHash(openfortPaymaster.getHash(userOps[0], VALIDUNTIL, VALIDAFTER, strategy)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(paymasterAdminPKey, hash); + bytes memory signature = abi.encodePacked(r, s, v); + bytes memory paymasterAndDataSigned = abi.encodePacked(address(openfortPaymaster), dataEncoded, signature); // This part was packed (not filled with 0s) + assertEq(openfortPaymaster.owner(), ECDSA.recover(hash, signature)); + userOps[0].paymasterAndData = paymasterAndDataSigned; + } + + // Back to the user. Sign the userOp + bytes memory userOpSignature; + bytes32 hash2; + { + bytes32 opHash = EntryPoint(entryPoint).getUserOpHash(userOps[0]); + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountAdminPKey, msgHash); + userOpSignature = abi.encodePacked(r, s, v); + + // Verifications below commented to avoid "Stack too deep" error + assertEq(ECDSA.recover(msgHash, v, r, s), vm.addr(accountAdminPKey)); + // Should return account admin + hash2 = + ECDSA.toEthSignedMessageHash(openfortPaymaster.getHash(userOps[0], VALIDUNTIL, VALIDAFTER, strategy)); + } + + // The hash of the userOp should not have changed after the inclusion of the sig + assertEq(hash, hash2); + userOps[0].signature = userOpSignature; + + // Get the paymaster deposit before handling the userOp + uint256 paymasterDepositBefore = openfortPaymaster.getDeposit(); + + entryPoint.handleOps(userOps, beneficiary); + + // Verify that the paymaster has the same deposit + assert(paymasterDepositBefore == openfortPaymaster.getDeposit()); + // Verify that the balance of the smart account has not decreased + assertEq(testToken.balanceOf(account), TESTTOKEN_ACCOUNT_PREFUND); + // Verify that the counter has not increased + assertEq(testCounter.counters(account), 0); + + // If this fails, it would mean: + // 1- That the paymaster has spent some of its deposit + // 2- That the smart account could not perform the desired actions, but still has all testTokens + // An attacker could DoS the paymaster to drain its deposit + } +}