diff --git a/contracts/common/BiconomySponsorshipPaymasterErrors.sol b/contracts/common/BiconomySponsorshipPaymasterErrors.sol index 3c49295..9d11dd0 100644 --- a/contracts/common/BiconomySponsorshipPaymasterErrors.sol +++ b/contracts/common/BiconomySponsorshipPaymasterErrors.sol @@ -72,6 +72,11 @@ contract BiconomySponsorshipPaymasterErrors { */ error CanNotWithdrawZeroAmount(); + /** + * @notice Throws when no request has been submitted + */ + error NoRequestSubmitted(); + /** * @notice Throws when trying unaccountedGas is too high */ @@ -81,4 +86,29 @@ contract BiconomySponsorshipPaymasterErrors { * @notice Throws when postOp gas limit is too low */ error PostOpGasLimitTooLow(); + + /** + * @notice Thrown when deposit is too low to reach minDeposit + */ + error LowDeposit(); + + /** + * @notice Thrown when trying to withdraw more than the balance + */ + error InsufficientFundsInGasTank(); + + /** + * @notice Thrown when trying to execute withdrawal request before delay has passed + */ + error RequestNotClearedYet(uint256 clearanceTime); + + /** + * @notice Thrown when trying to directly withdraw instead of submitting a request + */ + error SubmitRequestInstead(); + + /** + * @notice Thrown when the array lengths are not equal + */ + error InvalidArrayLengths(); } diff --git a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol index 0580d5e..81c4e0c 100644 --- a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol +++ b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol @@ -5,6 +5,12 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { PackedUserOperation } from "account-abstraction/core/UserOperationLib.sol"; interface IBiconomySponsorshipPaymaster { + struct WithdrawalRequest { + uint256 amount; + address to; + uint256 requestSubmittedTimestamp; + } + event UnaccountedGasChanged(uint256 indexed oldValue, uint256 indexed newValue); event FixedPriceMarkupChanged(uint256 indexed oldValue, uint256 indexed newValue); event VerifyingSignerChanged(address indexed oldSigner, address indexed newSigner, address indexed actor); @@ -14,6 +20,11 @@ interface IBiconomySponsorshipPaymaster { event GasBalanceDeducted(address indexed _paymasterId, uint256 indexed _charge, uint256 indexed _premium); event Received(address indexed sender, uint256 value); event TokensWithdrawn(address indexed token, address indexed to, uint256 indexed amount, address actor); + event WithdrawalRequestSubmitted(address withdrawAddress, uint256 amount); + event WithdrawalRequestCancelledFor(address paymasterId); + event TrustedPaymasterIdSet(address indexed paymasterId, bool isTrusted); + event EthWithdrawn(address indexed recipient, uint256 indexed amount); + event MinDepositChanged(uint256 indexed oldValue, uint256 indexed newValue); function depositFor(address paymasterId) external payable; diff --git a/contracts/sponsorship/BiconomySponsorshipPaymaster.sol b/contracts/sponsorship/BiconomySponsorshipPaymaster.sol index cda8beb..1a37c14 100644 --- a/contracts/sponsorship/BiconomySponsorshipPaymaster.sol +++ b/contracts/sponsorship/BiconomySponsorshipPaymaster.sol @@ -42,6 +42,8 @@ contract BiconomySponsorshipPaymaster is address public verifyingSigner; address public feeCollector; uint256 public unaccountedGas; + uint256 public paymasterIdWithdrawalDelay; + uint256 public minDeposit; // Denominator to prevent precision errors when applying price markup uint256 private constant _PRICE_DENOMINATOR = 1e6; @@ -52,13 +54,17 @@ contract BiconomySponsorshipPaymaster is uint256 private constant _UNACCOUNTED_GAS_LIMIT = 100_000; mapping(address => uint256) public paymasterIdBalances; + mapping(address => bool) internal _trustedPaymasterIds; + mapping(address paymasterId => WithdrawalRequest request) internal _requests; constructor( address owner, IEntryPoint entryPointArg, address verifyingSignerArg, address feeCollectorArg, - uint256 unaccountedGasArg + uint256 unaccountedGasArg, + uint256 paymasterIdWithdrawalDelayArg, + uint256 minDepositArg ) BasePaymaster(owner, entryPointArg) { @@ -68,6 +74,8 @@ contract BiconomySponsorshipPaymaster is } feeCollector = feeCollectorArg; unaccountedGas = unaccountedGasArg; + paymasterIdWithdrawalDelay = paymasterIdWithdrawalDelayArg; + minDeposit = minDepositArg; } receive() external payable { @@ -82,6 +90,7 @@ contract BiconomySponsorshipPaymaster is function depositFor(address paymasterId) external payable nonReentrant { if (paymasterId == address(0)) revert PaymasterIdCanNotBeZero(); if (msg.value == 0) revert DepositCanNotBeZero(); + if (paymasterIdBalances[paymasterId] + msg.value < minDeposit) revert LowDeposit(); paymasterIdBalances[paymasterId] += msg.value; entryPoint.depositTo{ value: msg.value }(address(this)); emit GasDeposited(paymasterId, msg.value); @@ -106,6 +115,45 @@ contract BiconomySponsorshipPaymaster is emit VerifyingSignerChanged(oldSigner, newVerifyingSigner, msg.sender); } + /** + * @dev Refund balances for multiple paymasterIds + * PM charges more than it should to protect itself. + * This function is used to refund the extra amount + * when the real consumption is known. + * @param paymasterIds The paymasterIds to refund + * @param amounts The amounts to refund + */ + function refundBalances(address[] calldata paymasterIds, uint256[] calldata amounts) external payable onlyOwner { + if (paymasterIds.length != amounts.length) revert InvalidArrayLengths(); + for (uint256 i; i < paymasterIds.length; i++) { + paymasterIdBalances[paymasterIds[i]] += amounts[i]; + } + } + + /** + * @dev Set a new trusted paymasterId. + * Can only be called by the owner of the contract. + * @param paymasterId The paymasterId to be set as trusted. + * @param isTrusted Whether the paymasterId is trusted or not. + */ + function setTrustedPaymasterId(address paymasterId, bool isTrusted) external payable onlyOwner { + if (paymasterId == address(0)) revert PaymasterIdCanNotBeZero(); + if (_trustedPaymasterIds[paymasterId] != isTrusted) { + _trustedPaymasterIds[paymasterId] = isTrusted; + emit TrustedPaymasterIdSet(paymasterId, isTrusted); + } + } + + /** + * @dev Set a new minimum deposit value. + * Can only be called by the owner of the contract. + * @param newMinDeposit The new minimum deposit value to be set. + */ + function setMinDeposit(uint256 newMinDeposit) external payable onlyOwner { + minDeposit = newMinDeposit; + emit MinDepositChanged(minDeposit, newMinDeposit); + } + /** * @dev Set a new fee collector address. * Can only be called by the owner of the contract. @@ -153,21 +201,45 @@ contract BiconomySponsorshipPaymaster is } /** - * @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. + * @dev Submit a withdrawal request for the paymasterId (Dapp Depositor address) + * @param withdrawAddress address to send the funds to + * @param amount amount to withdraw */ - function withdrawTo(address payable withdrawAddress, uint256 amount) external override nonReentrant { + function submitWithdrawalRequest(address withdrawAddress, uint256 amount) external { if (withdrawAddress == address(0)) revert CanNotWithdrawToZeroAddress(); if (amount == 0) revert CanNotWithdrawZeroAmount(); uint256 currentBalance = paymasterIdBalances[msg.sender]; - if (amount > currentBalance) { - revert InsufficientFunds(); - } - paymasterIdBalances[msg.sender] = currentBalance - amount; - entryPoint.withdrawTo(withdrawAddress, amount); - emit GasWithdrawn(msg.sender, withdrawAddress, amount); + if (amount > currentBalance) revert InsufficientFundsInGasTank(); + _requests[msg.sender] = + WithdrawalRequest({ amount: amount, to: withdrawAddress, requestSubmittedTimestamp: block.timestamp }); + emit WithdrawalRequestSubmitted(withdrawAddress, amount); + } + + /** + * @dev Execute a withdrawal request for the paymasterId (Dapp Depositor address) + * Request must be cleared by the withdrawal delay period + * @param paymasterId paymasterId (Dapp Depositor address) + */ + function executeWithdrawalRequest(address paymasterId) external nonReentrant { + WithdrawalRequest memory req = _requests[paymasterId]; + if (req.requestSubmittedTimestamp == 0) revert NoRequestSubmitted(); + uint256 clearanceTimestamp = req.requestSubmittedTimestamp + _getDelay(paymasterId); + if (block.timestamp < clearanceTimestamp) revert RequestNotClearedYet(clearanceTimestamp); + uint256 currentBalance = paymasterIdBalances[paymasterId]; + req.amount = req.amount > currentBalance ? currentBalance : req.amount; + if(req.amount == 0) revert CanNotWithdrawZeroAmount(); + paymasterIdBalances[paymasterId] = currentBalance - req.amount; + delete _requests[paymasterId]; + entryPoint.withdrawTo(payable(req.to), req.amount); + emit GasWithdrawn(paymasterId, req.to, req.amount); + } + + /** + * @dev Cancel a withdrawal request for the paymasterId (Dapp Depositor address) + */ + function cancelWithdrawalRequest() external { + delete _requests[msg.sender]; + emit WithdrawalRequestCancelledFor(msg.sender); } function withdrawEth(address payable recipient, uint256 amount) external payable onlyOwner nonReentrant { @@ -175,6 +247,12 @@ contract BiconomySponsorshipPaymaster is if (!success) { revert WithdrawalFailed(); } + emit EthWithdrawn(recipient, amount); + } + + function withdrawTo(address payable withdrawAddress, uint256 amount) external virtual override { + (withdrawAddress, amount); + revert SubmitRequestInstead(); } /** @@ -245,8 +323,10 @@ contract BiconomySponsorshipPaymaster is validUntil = uint48(bytes6(paymasterAndData[_PAYMASTER_ID_OFFSET + 20:_PAYMASTER_ID_OFFSET + 26])); validAfter = uint48(bytes6(paymasterAndData[_PAYMASTER_ID_OFFSET + 26:_PAYMASTER_ID_OFFSET + 32])); priceMarkup = uint32(bytes4(paymasterAndData[_PAYMASTER_ID_OFFSET + 32:_PAYMASTER_ID_OFFSET + 36])); - paymasterValidationGasLimit = uint128(bytes16(paymasterAndData[_PAYMASTER_VALIDATION_GAS_OFFSET:_PAYMASTER_POSTOP_GAS_OFFSET])); - paymasterPostOpGasLimit = uint128(bytes16(paymasterAndData[_PAYMASTER_POSTOP_GAS_OFFSET : _PAYMASTER_DATA_OFFSET])); + paymasterValidationGasLimit = + uint128(bytes16(paymasterAndData[_PAYMASTER_VALIDATION_GAS_OFFSET:_PAYMASTER_POSTOP_GAS_OFFSET])); + paymasterPostOpGasLimit = + uint128(bytes16(paymasterAndData[_PAYMASTER_POSTOP_GAS_OFFSET:_PAYMASTER_DATA_OFFSET])); signature = paymasterAndData[_PAYMASTER_ID_OFFSET + 36:]; } } @@ -264,31 +344,33 @@ contract BiconomySponsorshipPaymaster is internal override { - unchecked { - (address paymasterId, uint32 priceMarkup, uint256 prechargedAmount) = - abi.decode(context, (address, uint32, uint256)); - - // Include unaccountedGas since EP doesn't include this in actualGasCost - // unaccountedGas = postOpGas + EP overhead gas + estimated penalty - actualGasCost = actualGasCost + (unaccountedGas * actualUserOpFeePerGas); - // Apply the price markup - uint256 adjustedGasCost = (actualGasCost * priceMarkup) / _PRICE_DENOMINATOR; - - uint256 premium = adjustedGasCost - actualGasCost; - - // Add priceMarkup to fee collector balance - paymasterIdBalances[feeCollector] += premium; - - if (prechargedAmount > adjustedGasCost) { - // If overcharged refund the excess - paymasterIdBalances[paymasterId] += (prechargedAmount - adjustedGasCost); - } else { - // deduct what needs to be deducted from paymasterId - paymasterIdBalances[paymasterId] -= (adjustedGasCost - prechargedAmount); - } - // here adjustedGasCost does not account for gasPenalty. prechargedAmount accounts for penalty with maxGasPenalty - emit GasBalanceDeducted(paymasterId, adjustedGasCost, premium); + (address paymasterId, uint32 priceMarkup, uint256 prechargedAmount) = + abi.decode(context, (address, uint32, uint256)); + + // Include unaccountedGas since EP doesn't include this in actualGasCost + // unaccountedGas = postOpGas + EP overhead gas + estimated penalty + actualGasCost = actualGasCost + (unaccountedGas * actualUserOpFeePerGas); + // Apply the price markup + uint256 adjustedGasCost = (actualGasCost * priceMarkup) / _PRICE_DENOMINATOR; + + uint256 premium = adjustedGasCost - actualGasCost; + + // Add priceMarkup to fee collector balance + paymasterIdBalances[feeCollector] += premium; + + if (prechargedAmount > adjustedGasCost) { + // If overcharged refund the excess + paymasterIdBalances[paymasterId] += (prechargedAmount - adjustedGasCost); + } else { + // deduct what needs to be deducted from paymasterId + paymasterIdBalances[paymasterId] -= (adjustedGasCost - prechargedAmount); } + // here adjustedGasCost does not account for gasPenalty. prechargedAmount accounts for penalty with maxGasPenalty + emit GasBalanceDeducted(paymasterId, adjustedGasCost, premium); + + // here adjustedGasCost does not account for gasPenalty. prechargedAmount accounts for penalty with + // maxGasPenalty + emit GasBalanceDeducted(paymasterId, adjustedGasCost, premium); } /** @@ -311,10 +393,17 @@ contract BiconomySponsorshipPaymaster is returns (bytes memory context, uint256 validationData) { (userOpHash); - (address paymasterId, uint48 validUntil, uint48 validAfter, uint32 priceMarkup, uint128 paymasterValidationGasLimit, uint128 paymasterPostOpGasLimit, bytes calldata signature) = - parsePaymasterAndData(userOp.paymasterAndData); + ( + address paymasterId, + uint48 validUntil, + uint48 validAfter, + uint32 priceMarkup, + uint128 paymasterValidationGasLimit, + uint128 paymasterPostOpGasLimit, + bytes calldata signature + ) = parsePaymasterAndData(userOp.paymasterAndData); (paymasterValidationGasLimit, paymasterPostOpGasLimit); - + //ECDSA library supports both 64 and 65-byte long signatures. // we only "require" it here so that the revert reason on invalid signature will be of "VerifyingPaymaster", and // not "ECDSA" @@ -344,13 +433,15 @@ contract BiconomySponsorshipPaymaster is // callGasLimit + paymasterPostOpGas uint256 maxPenalty = ( - uint128(uint256(userOp.accountGasLimits)) + - uint128(bytes16(userOp.paymasterAndData[_PAYMASTER_POSTOP_GAS_OFFSET : _PAYMASTER_DATA_OFFSET])) - ) * 10 * userOp.unpackMaxFeePerGas() / 100; + ( + uint128(uint256(userOp.accountGasLimits)) + + uint128(bytes16(userOp.paymasterAndData[_PAYMASTER_POSTOP_GAS_OFFSET:_PAYMASTER_DATA_OFFSET])) + ) * 10 * userOp.unpackMaxFeePerGas() + ) / 100; // Deduct the max gas cost. uint256 effectiveCost = - ((requiredPreFund + unaccountedGas * userOp.unpackMaxFeePerGas()) * priceMarkup / _PRICE_DENOMINATOR); + (((requiredPreFund + unaccountedGas * userOp.unpackMaxFeePerGas()) * priceMarkup) / _PRICE_DENOMINATOR); if (effectiveCost + maxPenalty > paymasterIdBalances[paymasterId]) { revert InsufficientFundsForPaymasterId(); @@ -386,6 +477,11 @@ contract BiconomySponsorshipPaymaster is } } + function _getDelay(address paymasterId) internal view returns (uint256) { + if (_trustedPaymasterIds[paymasterId]) return 0; + return paymasterIdWithdrawalDelay; + } + function _withdrawERC20(IERC20 token, address target, uint256 amount) private { if (target == address(0)) revert CanNotWithdrawToZeroAddress(); SafeTransferLib.safeTransfer(address(token), target, amount); diff --git a/test/base/TestBase.sol b/test/base/TestBase.sol index 3dd4cbf..2808aa4 100644 --- a/test/base/TestBase.sol +++ b/test/base/TestBase.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; + import { Test } from "forge-std/Test.sol"; import { Vm } from "forge-std/Vm.sol"; import { console2 } from "forge-std/console2.sol"; @@ -26,7 +27,6 @@ import { } from "../../../contracts/token/BiconomyTokenPaymaster.sol"; abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { - using UserOperationLib for PackedUserOperation; address constant ENTRYPOINT_ADDRESS = address(0x0000000071727De22E5E9d8BAf0edAc6f37da032); @@ -212,14 +212,14 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { pmData.priceMarkup, new bytes(65) // Zero signature ); - + { - // Generate hash to be signed - bytes32 paymasterHash = - paymaster.getHash(userOp, pmData.paymasterId, pmData.validUntil, pmData.validAfter, pmData.priceMarkup); + // Generate hash to be signed + bytes32 paymasterHash = + paymaster.getHash(userOp, pmData.paymasterId, pmData.validUntil, pmData.validAfter, pmData.priceMarkup); - // Sign the hash - signature = signMessage(signer, paymasterHash); + // Sign the hash + signature = signMessage(signer, paymasterHash); } // Final paymaster data with the actual signature @@ -333,8 +333,8 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { function getMaxPenalty(PackedUserOperation calldata userOp) public view returns (uint256) { return ( - uint128(uint256(userOp.accountGasLimits)) + - uint128(bytes16(userOp.paymasterAndData[_PAYMASTER_POSTOP_GAS_OFFSET : _PAYMASTER_DATA_OFFSET])) + uint128(uint256(userOp.accountGasLimits)) + + uint128(bytes16(userOp.paymasterAndData[_PAYMASTER_POSTOP_GAS_OFFSET:_PAYMASTER_DATA_OFFSET])) ) * 10 * userOp.unpackMaxFeePerGas() / 100; } @@ -351,25 +351,19 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { internal view { - (uint256 expectedPriceMarkup, uint256 actualPriceMarkup) = - getPriceMarkups(bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, priceMarkup, maxPenalty); + (uint256 expectedPriceMarkup, uint256 actualPriceMarkup) = getPriceMarkups( + bicoPaymaster, initialDappPaymasterBalance, initialFeeCollectorBalance, priceMarkup, maxPenalty + ); uint256 totalGasFeePaid = BUNDLER.addr.balance - initialBundlerBalance; uint256 gasPaidByDapp = initialDappPaymasterBalance - bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); - console2.log("1"); // Assert that what paymaster paid is the same as what the bundler received assertEq(totalGasFeePaid, initialPaymasterEpBalance - bicoPaymaster.getDeposit()); - - console2.log("2"); // Assert that adjustment collected (if any) is correct assertEq(expectedPriceMarkup, actualPriceMarkup); - - console2.log("3"); // Gas paid by dapp is higher than paymaster // Guarantees that EP always has sufficient deposit to pay back dapps assertGt(gasPaidByDapp, BUNDLER.addr.balance - initialBundlerBalance); - - console2.log("4"); // Ensure that max 2% difference between total gas paid + the adjustment premium and gas paid by dapp (from // paymaster) assertApproxEqRel(totalGasFeePaid + actualPriceMarkup + maxPenalty, gasPaidByDapp, 0.02e18); diff --git a/test/unit/concrete/TestSponsorshipPaymaster.t.sol b/test/unit/concrete/TestSponsorshipPaymaster.t.sol index 1388828..13529d0 100644 --- a/test/unit/concrete/TestSponsorshipPaymaster.t.sol +++ b/test/unit/concrete/TestSponsorshipPaymaster.t.sol @@ -5,21 +5,32 @@ import "../../base/TestBase.sol"; import { IBiconomySponsorshipPaymaster } from "../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; import { BiconomySponsorshipPaymaster } from "../../../contracts/sponsorship/BiconomySponsorshipPaymaster.sol"; import { MockToken } from "@nexus/contracts/mocks/MockToken.sol"; +import "@ERC4337/account-abstraction/contracts/interfaces/IStakeManager.sol"; contract TestSponsorshipPaymasterWithPriceMarkup is TestBase { BiconomySponsorshipPaymaster public bicoPaymaster; + uint256 public constant WITHDRAWAL_DELAY = 3600; + uint256 public constant MIN_DEPOSIT = 1e15; + uint256 public constant UNACCOUNTED_GAS = 15e3; + function setUp() public { setupPaymasterTestEnvironment(); // Deploy Sponsorship Paymaster - bicoPaymaster = new BiconomySponsorshipPaymaster( - PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr, 7e3 - ); + bicoPaymaster = new BiconomySponsorshipPaymaster({ + owner: PAYMASTER_OWNER.addr, + entryPointArg: ENTRYPOINT, + verifyingSignerArg: PAYMASTER_SIGNER.addr, + feeCollectorArg: PAYMASTER_FEE_COLLECTOR.addr, + unaccountedGasArg: UNACCOUNTED_GAS, //if set too low, PM will lose money + paymasterIdWithdrawalDelayArg: WITHDRAWAL_DELAY, + minDepositArg: MIN_DEPOSIT + }); } function test_Deploy() external { BiconomySponsorshipPaymaster testArtifact = new BiconomySponsorshipPaymaster( - PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr, 7e3 + PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr, 7e3, 3600, 1e15 ); assertEq(testArtifact.owner(), PAYMASTER_OWNER.addr); assertEq(address(testArtifact.entryPoint()), ENTRYPOINT_ADDRESS); @@ -31,33 +42,35 @@ contract TestSponsorshipPaymasterWithPriceMarkup is TestBase { function test_RevertIf_DeployWithSignerSetToZero() external { vm.expectRevert(abi.encodeWithSelector(VerifyingSignerCanNotBeZero.selector)); new BiconomySponsorshipPaymaster( - PAYMASTER_OWNER.addr, ENTRYPOINT, address(0), PAYMASTER_FEE_COLLECTOR.addr, 7e3 + PAYMASTER_OWNER.addr, ENTRYPOINT, address(0), PAYMASTER_FEE_COLLECTOR.addr, 7e3, 3600, 1e15 ); } function test_RevertIf_DeployWithSignerAsContract() external { vm.expectRevert(abi.encodeWithSelector(VerifyingSignerCanNotBeContract.selector)); new BiconomySponsorshipPaymaster( - PAYMASTER_OWNER.addr, ENTRYPOINT, address(ENTRYPOINT), PAYMASTER_FEE_COLLECTOR.addr, 7e3 + PAYMASTER_OWNER.addr, ENTRYPOINT, address(ENTRYPOINT), PAYMASTER_FEE_COLLECTOR.addr, 7e3, 3600, 1e15 ); } function test_RevertIf_DeployWithFeeCollectorSetToZero() external { vm.expectRevert(abi.encodeWithSelector(FeeCollectorCanNotBeZero.selector)); - new BiconomySponsorshipPaymaster(PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, address(0), 7e3); + new BiconomySponsorshipPaymaster( + PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, address(0), 7e3, 3600, 1e15 + ); } function test_RevertIf_DeployWithFeeCollectorAsContract() external { vm.expectRevert(abi.encodeWithSelector(FeeCollectorCanNotBeContract.selector)); new BiconomySponsorshipPaymaster( - PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, address(ENTRYPOINT), 7e3 + PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, address(ENTRYPOINT), 7e3, 3600, 1e15 ); } function test_RevertIf_DeployWithUnaccountedGasCostTooHigh() external { vm.expectRevert(abi.encodeWithSelector(UnaccountedGasTooHigh.selector)); new BiconomySponsorshipPaymaster( - PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr, 100_001 + PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr, 100_001, 3600, 1e15 ); } @@ -66,7 +79,7 @@ contract TestSponsorshipPaymasterWithPriceMarkup is TestBase { assertEq(address(bicoPaymaster.entryPoint()), ENTRYPOINT_ADDRESS); assertEq(bicoPaymaster.verifyingSigner(), PAYMASTER_SIGNER.addr); assertEq(bicoPaymaster.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); - assertEq(bicoPaymaster.unaccountedGas(), 7e3); + assertEq(bicoPaymaster.unaccountedGas(), UNACCOUNTED_GAS); } function test_OwnershipTransfer() external prankModifier(PAYMASTER_OWNER.addr) { @@ -175,34 +188,99 @@ contract TestSponsorshipPaymasterWithPriceMarkup is TestBase { bicoPaymaster.deposit{ value: 1 ether }(); } - function test_WithdrawTo() external prankModifier(DAPP_ACCOUNT.addr) { - uint256 depositAmount = 10 ether; - bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); - uint256 danInitialBalance = BOB_ADDRESS.balance; - - vm.expectEmit(true, true, true, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.GasWithdrawn(DAPP_ACCOUNT.addr, BOB_ADDRESS, depositAmount); - bicoPaymaster.withdrawTo(payable(BOB_ADDRESS), depositAmount); + function test_RevertIf_TriesWithdrawToWithoutRequest() external prankModifier(DAPP_ACCOUNT.addr) { + vm.expectRevert(abi.encodeWithSelector(SubmitRequestInstead.selector)); + bicoPaymaster.withdrawTo(payable(BOB_ADDRESS), 1 ether); + } - uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); - assertEq(dappPaymasterBalance, 0 ether); - uint256 expectedDanBalance = danInitialBalance + depositAmount; - assertEq(BOB_ADDRESS.balance, expectedDanBalance); + function test_submitWithdrawalRequest_Fails_with_ZeroAmount() external prankModifier(DAPP_ACCOUNT.addr) { + vm.expectRevert(abi.encodeWithSelector(CanNotWithdrawZeroAmount.selector)); + bicoPaymaster.submitWithdrawalRequest(BOB_ADDRESS, 0 ether); } - function test_RevertIf_WithdrawToZeroAddress() external prankModifier(DAPP_ACCOUNT.addr) { + function test_submitWithdrawalRequest_Fails_with_ZeroAddress() external prankModifier(DAPP_ACCOUNT.addr) { vm.expectRevert(abi.encodeWithSelector(CanNotWithdrawToZeroAddress.selector)); - bicoPaymaster.withdrawTo(payable(address(0)), 0 ether); + bicoPaymaster.submitWithdrawalRequest(address(0), 1 ether); } - function test_RevertIf_WithdrawZeroAmount() external prankModifier(DAPP_ACCOUNT.addr) { - vm.expectRevert(abi.encodeWithSelector(CanNotWithdrawZeroAmount.selector)); - bicoPaymaster.withdrawTo(payable(BOB_ADDRESS), 0 ether); + function test_submitWithdrawalRequest_Fails_If_not_enough_balance() external prankModifier(DAPP_ACCOUNT.addr) { + uint256 depositAmount = 1 ether; + bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); + vm.expectRevert(abi.encodeWithSelector(InsufficientFundsInGasTank.selector)); + bicoPaymaster.submitWithdrawalRequest(BOB_ADDRESS, depositAmount + 1); } - function test_RevertIf_WithdrawToExceedsBalance() external prankModifier(DAPP_ACCOUNT.addr) { - vm.expectRevert(abi.encodeWithSelector(InsufficientFunds.selector)); - bicoPaymaster.withdrawTo(payable(BOB_ADDRESS), 1 ether); + function test_executeWithdrawalRequest_Fails_with_NoRequestSubmitted() external prankModifier(DAPP_ACCOUNT.addr) { + vm.expectRevert(abi.encodeWithSelector(NoRequestSubmitted.selector)); + bicoPaymaster.executeWithdrawalRequest(DAPP_ACCOUNT.addr); + } + + function test_cancelWithdrawalRequest_Success() external prankModifier(DAPP_ACCOUNT.addr) { + uint256 depositAmount = 1 ether; + bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); + bicoPaymaster.submitWithdrawalRequest(BOB_ADDRESS, depositAmount); + bicoPaymaster.cancelWithdrawalRequest(); + vm.expectRevert(abi.encodeWithSelector(NoRequestSubmitted.selector)); + bicoPaymaster.executeWithdrawalRequest(DAPP_ACCOUNT.addr); + } + + function test_submitWithdrawalRequest_Happy_Scenario() external prankModifier(DAPP_ACCOUNT.addr) { + uint256 depositAmount = 1 ether; + bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); + bicoPaymaster.submitWithdrawalRequest(BOB_ADDRESS, depositAmount); + vm.warp(block.timestamp + WITHDRAWAL_DELAY + 1); + uint256 dappPaymasterBalanceBefore = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + uint256 bobBalanceBefore = BOB_ADDRESS.balance; + bicoPaymaster.executeWithdrawalRequest(DAPP_ACCOUNT.addr); + uint256 dappPaymasterBalanceAfter = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + uint256 bobBalanceAfter = BOB_ADDRESS.balance; + assertEq(dappPaymasterBalanceAfter, dappPaymasterBalanceBefore - depositAmount); + assertEq(bobBalanceAfter, bobBalanceBefore + depositAmount); + // can not withdraw again + vm.expectRevert(abi.encodeWithSelector(NoRequestSubmitted.selector)); + bicoPaymaster.executeWithdrawalRequest(DAPP_ACCOUNT.addr); + } + + // try to use balance while request is cleared + function test_executeWithdrawalRequest_Withdraws_WhateverIsLeft() external prankModifier(DAPP_ACCOUNT.addr) { + uint256 depositAmount = 1 ether; + bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); + bicoPaymaster.submitWithdrawalRequest(BOB_ADDRESS, depositAmount); + + //use balance of the paymaster + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, 1e6, 55_000); + ops[0] = userOp; + startPrank(BUNDLER.addr); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + stopPrank(); + + uint256 dappPaymasterBalanceAfter = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + assertLt(dappPaymasterBalanceAfter, depositAmount); + uint256 bobBalanceBeforeWithdrawal = BOB_ADDRESS.balance; + + IStakeManager.DepositInfo memory depositInfo = ENTRYPOINT.getDepositInfo(address(bicoPaymaster)); + uint256 PAYMASTER_POSTOP_GAS_OFFSET = 36; + uint256 PAYMASTER_DATA_OFFSET = 52; + + vm.warp(block.timestamp + WITHDRAWAL_DELAY + 1); + bicoPaymaster.executeWithdrawalRequest(DAPP_ACCOUNT.addr); + uint256 bobBalanceAfterWithdrawal = BOB_ADDRESS.balance; + assertEq(bobBalanceAfterWithdrawal, bobBalanceBeforeWithdrawal + dappPaymasterBalanceAfter); + assertEq(bicoPaymaster.getBalance(DAPP_ACCOUNT.addr), 0 ether); + } + + function getGasLimit(PackedUserOperation calldata userOp) public pure returns (uint256) { + uint256 PAYMASTER_POSTOP_GAS_OFFSET = 36; + uint256 PAYMASTER_DATA_OFFSET = 52; + return uint128(uint256(userOp.accountGasLimits)) + + uint128(bytes16(userOp.paymasterAndData[PAYMASTER_POSTOP_GAS_OFFSET : PAYMASTER_DATA_OFFSET])); + } + + // test minimal deposit + function test_depositFor_RevertsIf_DepositIsLessThanMinDeposit() external { + vm.expectRevert(abi.encodeWithSelector(LowDeposit.selector)); + bicoPaymaster.depositFor{ value: MIN_DEPOSIT - 1 }(DAPP_ACCOUNT.addr); } function test_ValidatePaymasterAndPostOpWithoutPriceMarkup() external { diff --git a/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol b/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol index 6b2e55f..39b1263 100644 --- a/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol +++ b/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol @@ -8,17 +8,25 @@ import { MockToken } from "@nexus/contracts/mocks/MockToken.sol"; contract TestFuzz_SponsorshipPaymasterWithPriceMarkup is TestBase { BiconomySponsorshipPaymaster public bicoPaymaster; + uint256 public constant WITHDRAWAL_DELAY = 3600; + uint256 public constant MIN_DEPOSIT = 1e15; function setUp() public { setupPaymasterTestEnvironment(); // Deploy Sponsorship Paymaster - bicoPaymaster = new BiconomySponsorshipPaymaster( - PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, PAYMASTER_FEE_COLLECTOR.addr, 7e3 - ); + bicoPaymaster = new BiconomySponsorshipPaymaster({ + owner: PAYMASTER_OWNER.addr, + entryPointArg: ENTRYPOINT, + verifyingSignerArg: PAYMASTER_SIGNER.addr, + feeCollectorArg: PAYMASTER_FEE_COLLECTOR.addr, + unaccountedGasArg: 7e3, + paymasterIdWithdrawalDelayArg: 3600, + minDepositArg: 1e15 + }); } function testFuzz_DepositFor(uint256 depositAmount) external { - vm.assume(depositAmount <= 1000 ether && depositAmount > 0 ether); + vm.assume(depositAmount <= 1000 ether && depositAmount > 1e15); vm.deal(DAPP_ACCOUNT.addr, depositAmount); uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); @@ -32,21 +40,27 @@ contract TestFuzz_SponsorshipPaymasterWithPriceMarkup is TestBase { assertEq(dappPaymasterBalance, depositAmount); } - function testFuzz_WithdrawTo(uint256 withdrawAmount) external prankModifier(DAPP_ACCOUNT.addr) { - vm.assume(withdrawAmount <= 1000 ether && withdrawAmount > 0 ether); - vm.deal(DAPP_ACCOUNT.addr, withdrawAmount); - - bicoPaymaster.depositFor{ value: withdrawAmount }(DAPP_ACCOUNT.addr); - uint256 danInitialBalance = BOB_ADDRESS.balance; - - vm.expectEmit(true, true, true, true, address(bicoPaymaster)); - emit IBiconomySponsorshipPaymaster.GasWithdrawn(DAPP_ACCOUNT.addr, BOB_ADDRESS, withdrawAmount); - bicoPaymaster.withdrawTo(payable(BOB_ADDRESS), withdrawAmount); - - uint256 dappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); - assertEq(dappPaymasterBalance, 0 ether); - uint256 expectedDanBalance = danInitialBalance + withdrawAmount; - assertEq(BOB_ADDRESS.balance, expectedDanBalance); + // Rebuild submitting and exeuting withdraw request fuzz + function test_submitWithdrawalRequest_Happy_Scenario( + uint256 depositAmount + ) + external + prankModifier(DAPP_ACCOUNT.addr) + { + vm.assume(depositAmount <= 1000 ether && depositAmount > MIN_DEPOSIT); + bicoPaymaster.depositFor{ value: depositAmount }(DAPP_ACCOUNT.addr); + bicoPaymaster.submitWithdrawalRequest(BOB_ADDRESS, depositAmount); + vm.warp(block.timestamp + WITHDRAWAL_DELAY + 1); + uint256 dappPaymasterBalanceBefore = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + uint256 bobBalanceBefore = BOB_ADDRESS.balance; + bicoPaymaster.executeWithdrawalRequest(DAPP_ACCOUNT.addr); + uint256 dappPaymasterBalanceAfter = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + uint256 bobBalanceAfter = BOB_ADDRESS.balance; + assertEq(dappPaymasterBalanceAfter, dappPaymasterBalanceBefore - depositAmount); + assertEq(bobBalanceAfter, bobBalanceBefore + depositAmount); + // can not withdraw again + vm.expectRevert(abi.encodeWithSelector(NoRequestSubmitted.selector)); + bicoPaymaster.executeWithdrawalRequest(DAPP_ACCOUNT.addr); } function testFuzz_Receive(uint256 ethAmount) external prankModifier(ALICE_ADDRESS) { @@ -141,13 +155,13 @@ contract TestFuzz_SponsorshipPaymasterWithPriceMarkup is TestBase { { PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); PaymasterData memory pmData = PaymasterData({ - validationGasLimit: 3e6, - postOpGasLimit: 3e6, - paymasterId: paymasterId, - validUntil: validUntil, - validAfter: validAfter, - priceMarkup: priceMarkup - }); + validationGasLimit: 3e6, + postOpGasLimit: 3e6, + paymasterId: paymasterId, + validUntil: validUntil, + validAfter: validAfter, + priceMarkup: priceMarkup + }); (bytes memory paymasterAndData, bytes memory signature) = generateAndSignPaymasterData(userOp, PAYMASTER_SIGNER, bicoPaymaster, pmData);