diff --git a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol b/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol index 3011264..5c78e91 100644 --- a/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol +++ b/contracts/sponsorship/SponsorshipPaymasterWithPremium.sol @@ -232,14 +232,11 @@ contract BiconomySponsorshipPaymaster is /// @dev This function is called after a user operation has been executed or reverted. /// @param context The context containing the token amount and user sender address. /// @param actualGasCost The actual gas cost of the transaction. - /// @param actualUserOpFeePerGas - the gas price this UserOp pays. This value is based on the UserOp's maxFeePerGas - // and maxPriorityFee (and basefee) - // It is not the same as tx.gasprice, which is what the bundler pays. function _postOp( PostOpMode, bytes calldata context, uint256 actualGasCost, - uint256 actualUserOpFeePerGas + uint256 ) internal override @@ -253,13 +250,15 @@ contract BiconomySponsorshipPaymaster is // deduct with premium paymasterIdBalances[paymasterId] -= costIncludingPremium; - uint256 actualPremium = costIncludingPremium - actualGasCost; - // "collect" premium - paymasterIdBalances[feeCollector] += actualPremium; + if (actualGasCost < costIncludingPremium) { + // "collect" premium + uint256 actualPremium = costIncludingPremium - actualGasCost; + paymasterIdBalances[feeCollector] += actualPremium; + // Review if we should emit balToDeduct as well + emit PremiumCollected(paymasterId, actualPremium); + } emit GasBalanceDeducted(paymasterId, costIncludingPremium, userOpHash); - // Review if we should emit balToDeduct as well - emit PremiumCollected(paymasterId, actualPremium); } } @@ -288,7 +287,7 @@ contract BiconomySponsorshipPaymaster is //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" - if(signature.length != 64 && signature.length != 65){ + if (signature.length != 64 && signature.length != 65) { revert InvalidSignatureLength(); } diff --git a/test/foundry/base/NexusTestBase.sol b/test/foundry/base/NexusTestBase.sol index 6bc0df7..308bf26 100644 --- a/test/foundry/base/NexusTestBase.sol +++ b/test/foundry/base/NexusTestBase.sol @@ -8,6 +8,7 @@ import "solady/src/utils/ECDSA.sol"; import { EntryPoint } from "account-abstraction/contracts/core/EntryPoint.sol"; import { IEntryPoint } from "account-abstraction/contracts/interfaces/IEntryPoint.sol"; +import { IPaymaster } from "account-abstraction/contracts/interfaces/IPaymaster.sol"; import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol"; import { Nexus } from "nexus/contracts/Nexus.sol"; @@ -331,87 +332,23 @@ abstract contract NexusTestBase is CheatCodes, BaseEventsAndErrors { assertTrue(res, "Pre-funding account should succeed"); } - /// @notice Calculates the gas cost of the calldata - /// @param data The calldata - /// @return calldataGas The gas cost of the calldata - function calculateCalldataCost(bytes memory data) internal pure returns (uint256 calldataGas) { - for (uint256 i = 0; i < data.length; i++) { - if (uint8(data[i]) == 0) { - calldataGas += 4; - } else { - calldataGas += 16; - } - } - } - - /// @notice Helper function to measure and log gas for simple EOA calls - /// @param description The description for the log - /// @param target The target contract address - /// @param value The value to be sent with the call - /// @param callData The calldata for the call - function measureAndLogGasEOA( - string memory description, - address target, - uint256 value, - bytes memory callData + function estimatePaymasterGasCosts( + BiconomySponsorshipPaymaster paymaster, + PackedUserOperation memory userOp, + bytes32 userOpHash, + uint256 requiredPreFund ) internal + prankModifier(ENTRYPOINT_ADDRESS) + returns (uint256 validationGasLimit, uint256 postopGasLimit) { - uint256 calldataCost = 0; - for (uint256 i = 0; i < callData.length; i++) { - if (uint8(callData[i]) == 0) { - calldataCost += 4; - } else { - calldataCost += 16; - } - } - - uint256 baseGas = 21_000; - - uint256 initialGas = gasleft(); - (bool res,) = target.call{ value: value }(callData); - uint256 gasUsed = initialGas - gasleft() + baseGas + calldataCost; - assertTrue(res); - emit log_named_uint(description, gasUsed); - } + validationGasLimit = gasleft(); + (bytes memory context,) = paymaster.validatePaymasterUserOp(userOp, userOpHash, requiredPreFund); + validationGasLimit = validationGasLimit - gasleft(); - /// @notice Helper function to calculate calldata cost and log gas usage - /// @param description The description for the log - /// @param userOps The user operations to be executed - function measureAndLogGas(string memory description, PackedUserOperation[] memory userOps) internal { - bytes memory callData = abi.encodeWithSelector(ENTRYPOINT.handleOps.selector, userOps, payable(BUNDLER.addr)); - - uint256 calldataCost = 0; - for (uint256 i = 0; i < callData.length; i++) { - if (uint8(callData[i]) == 0) { - calldataCost += 4; - } else { - calldataCost += 16; - } - } - - uint256 baseGas = 21_000; - - uint256 initialGas = gasleft(); - ENTRYPOINT.handleOps(userOps, payable(BUNDLER.addr)); - uint256 gasUsed = initialGas - gasleft() + baseGas + calldataCost; - emit log_named_uint(description, gasUsed); - } - - /// @notice Handles a user operation and measures gas usage - /// @param userOps The user operations to handle - /// @param refundReceiver The address to receive the gas refund - /// @return gasUsed The amount of gas used - function handleUserOpAndMeasureGas( - PackedUserOperation[] memory userOps, - address refundReceiver - ) - internal - returns (uint256 gasUsed) - { - uint256 gasStart = gasleft(); - ENTRYPOINT.handleOps(userOps, payable(refundReceiver)); - gasUsed = gasStart - gasleft(); + postopGasLimit = gasleft(); + paymaster.postOp(IPaymaster.PostOpMode.opSucceeded, context, 3e4, 2e9); + postopGasLimit = postopGasLimit - gasleft(); } /// @notice Generates and signs the paymaster data for a user operation. diff --git a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol index 4f58bbe..c4b5113 100644 --- a/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol +++ b/test/foundry/unit/concrete/TestSponsorshipPaymasterWithPremiumTest.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Unlicensed pragma solidity ^0.8.26; +import { console2 } from "forge-std/src/console2.sol"; import { NexusTestBase } from "../../base/NexusTestBase.sol"; import { IBiconomySponsorshipPaymaster } from "../../../../contracts/interfaces/IBiconomySponsorshipPaymaster.sol"; import { BiconomySponsorshipPaymaster } from "../../../../contracts/sponsorship/SponsorshipPaymasterWithPremium.sol"; @@ -28,6 +29,16 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { assertEq(testArtifact.feeCollector(), PAYMASTER_FEE_COLLECTOR.addr); } + function test_RevertIf_DeployWithSignerSetToZero() external { + vm.expectRevert(abi.encodeWithSelector(VerifyingSignerCanNotBeZero.selector)); + new BiconomySponsorshipPaymaster(PAYMASTER_OWNER.addr, ENTRYPOINT, address(0), PAYMASTER_FEE_COLLECTOR.addr); + } + + function test_RevertIf_DeployWithFeeCollectorSetToZero() external { + vm.expectRevert(abi.encodeWithSelector(FeeCollectorCanNotBeZero.selector)); + new BiconomySponsorshipPaymaster(PAYMASTER_OWNER.addr, ENTRYPOINT, PAYMASTER_SIGNER.addr, address(0)); + } + function test_CheckInitialPaymasterState() external view { assertEq(bicoPaymaster.owner(), PAYMASTER_OWNER.addr); assertEq(address(bicoPaymaster.entryPoint()), ENTRYPOINT_ADDRESS); @@ -148,7 +159,7 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { bicoPaymaster.withdrawTo(payable(DAN_ADDRESS), 1 ether); } - function test_ValidatePaymasterAndPostOp() external { + function test_ValidatePaymasterAndPostOpWithoutPremium() external { uint256 initialDappPaymasterBalance = 10 ether; bicoPaymaster.depositFor{ value: initialDappPaymasterBalance }(DAPP_ACCOUNT.addr); @@ -169,12 +180,71 @@ contract TestSponsorshipPaymasterWithPremium is NexusTestBase { vm.expectEmit(true, false, true, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); + ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + + uint256 resultingDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); + assertNotEq(initialDappPaymasterBalance, resultingDappPaymasterBalance); + } + + function test_ValidatePaymasterAndPostOpWithPremium() external { + uint256 initialDappPaymasterBalance = 10 ether; + bicoPaymaster.depositFor{ value: initialDappPaymasterBalance }(DAPP_ACCOUNT.addr); + uint256 initialBundlerBalance = BUNDLER.addr.balance; + + PackedUserOperation[] memory ops = new PackedUserOperation[](1); + + uint48 validUntil = uint48(block.timestamp + 1 days); + uint48 validAfter = uint48(block.timestamp); + + PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); + + // Charge a 10% premium + uint32 premium = 1e6 + 1e5; + userOp.paymasterAndData = generateAndSignPaymasterData( + userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, premium + ); + userOp.signature = signUserOp(ALICE, userOp); + + // Estimate paymaster gas limits + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); + (uint256 validationGasLimit, uint256 postopGasLimit) = + estimatePaymasterGasCosts(bicoPaymaster, userOp, userOpHash, 5e4); + + // Ammend the userop to have new gas limits and signature + userOp.paymasterAndData = generateAndSignPaymasterData( + userOp, + PAYMASTER_SIGNER, + bicoPaymaster, + uint128(validationGasLimit), + uint128(postopGasLimit), + DAPP_ACCOUNT.addr, + validUntil, + validAfter, + premium + ); + userOp.signature = signUserOp(ALICE, userOp); + ops[0] = userOp; + userOpHash = ENTRYPOINT.getUserOpHash(userOp); + + // submit userops vm.expectEmit(true, false, false, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.PremiumCollected(DAPP_ACCOUNT.addr, 0); + vm.expectEmit(true, false, true, true, address(bicoPaymaster)); + emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + // Check that gas fees ended up in the right wallets uint256 resultingDappPaymasterBalance = bicoPaymaster.getBalance(DAPP_ACCOUNT.addr); - assertNotEq(initialDappPaymasterBalance, resultingDappPaymasterBalance); + uint256 resultingFeeCollectorPaymasterBalance = bicoPaymaster.getBalance(PAYMASTER_FEE_COLLECTOR.addr); + uint256 resultingBundlerBalance = BUNDLER.addr.balance - initialBundlerBalance; + uint256 totalGasFeesCharged = initialDappPaymasterBalance - resultingDappPaymasterBalance; + console2.log(resultingDappPaymasterBalance); + console2.log(resultingFeeCollectorPaymasterBalance); + console2.log(resultingBundlerBalance); + console2.log(totalGasFeesCharged); + + // resultingDappPaymasterBalance + assertEq(resultingFeeCollectorPaymasterBalance, (totalGasFeesCharged * 1e5) / premium); } function test_RevertIf_ValidatePaymasterUserOpWithIncorrectSignatureLength() external {