diff --git a/test/base/TestBase.sol b/test/base/TestBase.sol index 3cb77bc..0390d72 100644 --- a/test/base/TestBase.sol +++ b/test/base/TestBase.sol @@ -37,6 +37,15 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { Vm.Wallet internal PAYMASTER_FEE_COLLECTOR; Vm.Wallet internal DAPP_ACCOUNT; + struct PaymasterData { + uint128 validationGasLimit; + uint128 postOpGasLimit; + address paymasterId; + uint48 validUntil; + uint48 validAfter; + uint32 priceMarkup; + } + // Used to buffer user op gas limits // GAS_LIMIT = (ESTIMATED_GAS * GAS_BUFFER_RATIO) / 100 uint8 private constant GAS_BUFFER_RATIO = 110; @@ -80,11 +89,11 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOp); verificationGasUsed = gasleft(); IAccount(userOp.sender).validateUserOp(userOp, userOpHash, 0); - verificationGasUsed = verificationGasUsed - gasleft(); + verificationGasUsed = verificationGasUsed - gasleft(); //+ 21000; callGasUsed = gasleft(); bool success = Exec.call(userOp.sender, 0, userOp.callData, 3e6); - callGasUsed = callGasUsed - gasleft(); + callGasUsed = callGasUsed - gasleft(); //+ 21000; assert(success); verificationGasLimit = (verificationGasUsed * GAS_BUFFER_RATIO) / 100; @@ -108,68 +117,65 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { // Estimate gas used validationGasUsed = gasleft(); (context,) = paymaster.validatePaymasterUserOp(userOp, userOpHash, requiredPreFund); - validationGasUsed = validationGasUsed - gasleft(); + validationGasUsed = validationGasUsed - gasleft(); //+ 21000; postopGasUsed = gasleft(); paymaster.postOp(IPaymaster.PostOpMode.opSucceeded, context, 1e12, 3e6); - postopGasUsed = (postopGasUsed - gasleft()); + postopGasUsed = (postopGasUsed - gasleft()); //+ 21000; validationGasLimit = (validationGasUsed * GAS_BUFFER_RATIO) / 100; postopGasLimit = (postopGasUsed * GAS_BUFFER_RATIO) / 100; } + // Note: we can use externally provided gas limits to override. + // Note: we can pass callData and callGasLimit as args to test with more tx types + // Note: we can pass Nexus instance instead of sender EOA and comuting counterfactual within buildUserOpWithCalldata + function createUserOp( Vm.Wallet memory sender, BiconomySponsorshipPaymaster paymaster, - uint32 priceMarkup + uint32 priceMarkup, + uint128 postOpGasLimitOverride ) internal returns (PackedUserOperation memory userOp, bytes32 userOpHash) { // Create userOp with no gas estimates - uint48 validUntil = uint48(block.timestamp + 1 days); - uint48 validAfter = uint48(block.timestamp); - userOp = buildUserOpWithCalldata(sender, "", address(VALIDATOR_MODULE)); - (userOp.paymasterAndData,) = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, paymaster, 3e6, 8e3, DAPP_ACCOUNT.addr, validUntil, validAfter, priceMarkup + PaymasterData memory pmData = PaymasterData( + 24_000, + uint128(postOpGasLimitOverride), + DAPP_ACCOUNT.addr, + uint48(block.timestamp + 1 days), + uint48(block.timestamp), + priceMarkup ); + (userOp.paymasterAndData,) = generateAndSignPaymasterData(userOp, PAYMASTER_SIGNER, paymaster, pmData); userOp.signature = signUserOp(sender, userOp); - (,, uint256 verificationGasLimit, uint256 callGasLimit) = estimateUserOpGasCosts(userOp); - // Estimate paymaster gas limits - (, uint256 postopGasUsed, uint256 validationGasLimit, uint256 postopGasLimit) = - estimatePaymasterGasCosts(paymaster, userOp, 5e4); - - // console2.log("postOpGasUsed"); - // console2.logUint(postopGasUsed); - - // uint256 prevValUnaccountedGas = paymaster.unaccountedGas(); - // console2.logUint(prevValUnaccountedGas); - - // Below may not be needed if unaccountedGas is set correctly - vm.startPrank(paymaster.owner()); - // Set unaccounted gas to be gas used in postop + 1000 for EP overhead and penalty - paymaster.setUnaccountedGas(postopGasUsed + 500); - vm.stopPrank(); - - // uint256 currentValUnaccountedGas = paymaster.unaccountedGas(); - // console2.logUint(currentValUnaccountedGas); - - // Ammend the userop to have new gas limits and signature - userOp.accountGasLimits = bytes32(abi.encodePacked(uint128(verificationGasLimit), uint128(callGasLimit))); - (userOp.paymasterAndData,) = generateAndSignPaymasterData( - userOp, - PAYMASTER_SIGNER, - paymaster, - uint128(validationGasLimit), - uint128(postopGasLimit), + // Estimate account gas limits + // (,, uint256 verificationGasLimit, uint256 callGasLimit) = estimateUserOpGasCosts(userOp); + // // Estimate paymaster gas limits + // (, uint256 postopGasUsed, uint256 validationGasLimit, uint256 postopGasLimit) = + // estimatePaymasterGasCosts(paymaster, userOp, 5e4); + + // vm.startPrank(paymaster.owner()); + // paymaster.setUnaccountedGas(postopGasUsed + 500); + // vm.stopPrank(); + + // Ammend the userop to have updated / overridden gas limits + userOp.accountGasLimits = bytes32(abi.encodePacked(uint128(55_000), uint128(0))); + PaymasterData memory pmDataNew = PaymasterData( + uint128(24_000), + uint128(postOpGasLimitOverride), DAPP_ACCOUNT.addr, - validUntil, - validAfter, + uint48(block.timestamp + 1 days), + uint48(block.timestamp), priceMarkup ); + + (userOp.paymasterAndData,) = generateAndSignPaymasterData(userOp, PAYMASTER_SIGNER, paymaster, pmDataNew); userOp.signature = signUserOp(sender, userOp); userOpHash = ENTRYPOINT.getUserOpHash(userOp); } @@ -185,12 +191,7 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { PackedUserOperation memory userOp, Vm.Wallet memory signer, BiconomySponsorshipPaymaster paymaster, - uint128 paymasterValGasLimit, - uint128 paymasterPostOpGasLimit, - address paymasterId, - uint48 validUntil, - uint48 validAfter, - uint32 priceMarkup + PaymasterData memory pmData ) internal view @@ -199,12 +200,12 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { // Initial paymaster data with zero signature bytes memory initialPmData = abi.encodePacked( address(paymaster), - paymasterValGasLimit, - paymasterPostOpGasLimit, - paymasterId, - validUntil, - validAfter, - priceMarkup, + pmData.validationGasLimit, + pmData.postOpGasLimit, + pmData.paymasterId, + pmData.validUntil, + pmData.validAfter, + pmData.priceMarkup, new bytes(65) // Zero signature ); @@ -212,7 +213,8 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { userOp.paymasterAndData = initialPmData; // Generate hash to be signed - bytes32 paymasterHash = paymaster.getHash(userOp, paymasterId, validUntil, validAfter, priceMarkup); + bytes32 paymasterHash = + paymaster.getHash(userOp, pmData.paymasterId, pmData.validUntil, pmData.validAfter, pmData.priceMarkup); // Sign the hash signature = signMessage(signer, paymasterHash); @@ -221,16 +223,18 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { // Final paymaster data with the actual signature finalPmData = abi.encodePacked( address(paymaster), - paymasterValGasLimit, - paymasterPostOpGasLimit, - paymasterId, - validUntil, - validAfter, - priceMarkup, + pmData.validationGasLimit, + pmData.postOpGasLimit, + pmData.paymasterId, + pmData.validUntil, + pmData.validAfter, + pmData.priceMarkup, signature ); } + // Note: Token paymaster could also get into stack deep issues. + // TODO: Refactor to reduce stack depth /// @notice Generates and signs the paymaster data for a user operation. /// @dev This function prepares the `paymasterAndData` field for a `PackedUserOperation` with the correct signature. /// @param userOp The user operation to be signed. @@ -319,15 +323,11 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { uint256 totalGasFeesCharged = initialDappPaymasterBalance - resultingDappPaymasterBalance; - if (priceMarkup >= 1e6) { - //priceMarkup - expectedPriceMarkup = totalGasFeesCharged - ((totalGasFeesCharged * 1e6) / priceMarkup); - actualPriceMarkup = resultingFeeCollectorPaymasterBalance - initialFeeCollectorBalance; - } else { - revert("PriceMarkup must be more than 1e6"); - } + expectedPriceMarkup = totalGasFeesCharged - ((totalGasFeesCharged * 1e6) / priceMarkup); + actualPriceMarkup = resultingFeeCollectorPaymasterBalance - initialFeeCollectorBalance; } + // Note: can pack values into one struct function calculateAndAssertAdjustments( BiconomySponsorshipPaymaster bicoPaymaster, uint256 initialDappPaymasterBalance, @@ -350,9 +350,6 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { assertEq(expectedPriceMarkup, actualPriceMarkup); // Gas paid by dapp is higher than paymaster // Guarantees that EP always has sufficient deposit to pay back dapps - - // TODO - // Review: fix this properly. avoid out of stack errors assertGt(gasPaidByDapp, BUNDLER.addr.balance - initialBundlerBalance); // Ensure that max 1% difference between total gas paid + the adjustment premium and gas paid by dapp (from // paymaster) diff --git a/test/base/TestHelper.sol b/test/base/TestHelper.sol index 65945c8..58cff6a 100644 --- a/test/base/TestHelper.sol +++ b/test/base/TestHelper.sol @@ -251,6 +251,41 @@ contract TestHelper is CheatCodes, EventsAndErrors { userOp.signature = signature; } + function buildPackedUserOperation( + Vm.Wallet memory wallet, + Nexus account, + ExecType execType, + Execution[] memory executions, + address validator, + uint256 nonce + ) + internal + view + returns (PackedUserOperation[] memory userOps) + { + // Validate execType + require(execType == EXECTYPE_DEFAULT || execType == EXECTYPE_TRY, "Invalid ExecType"); + + // Initialize the userOps array with one operation + userOps = new PackedUserOperation[](1); + + uint256 nonceToUse; + if (nonce == 0) { + nonceToUse = getNonce(address(account), MODE_VALIDATION, validator); + } else { + nonceToUse = nonce; + } + + // Build the UserOperation + userOps[0] = buildPackedUserOp(address(account), nonceToUse); + userOps[0].callData = prepareERC7579ExecuteCallData(execType, executions); + + // Sign the operation + bytes32 userOpHash = ENTRYPOINT.getUserOpHash(userOps[0]); + userOps[0].signature = signMessage(wallet, userOpHash); + return userOps; + } + /// @notice Retrieves the nonce for a given account and validator /// @param account The account address /// @param vMode Validation Mode diff --git a/test/unit/concrete/TestSponsorshipPaymaster.t.sol b/test/unit/concrete/TestSponsorshipPaymaster.t.sol index 94178a5..8fd986e 100644 --- a/test/unit/concrete/TestSponsorshipPaymaster.t.sol +++ b/test/unit/concrete/TestSponsorshipPaymaster.t.sol @@ -205,13 +205,16 @@ contract TestSponsorshipPaymasterWithPriceMarkup is TestBase { bicoPaymaster.withdrawTo(payable(BOB_ADDRESS), 1 ether); } - function skip_test_ValidatePaymasterAndPostOpWithoutPriceMarkup() external prankModifier(DAPP_ACCOUNT.addr) { + function test_ValidatePaymasterAndPostOpWithoutPriceMarkup() external { bicoPaymaster.depositFor{ value: 10 ether }(DAPP_ACCOUNT.addr); - // No adjustment - uint32 priceMarkup = 1e6; + + startPrank(PAYMASTER_OWNER.addr); + bicoPaymaster.setUnaccountedGas(9e3); + stopPrank(); PackedUserOperation[] memory ops = new PackedUserOperation[](1); - (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, priceMarkup); + // price markup of 1e6 + (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, 1e6, 10_000); ops[0] = userOp; uint256 initialBundlerBalance = BUNDLER.addr.balance; @@ -222,7 +225,9 @@ contract TestSponsorshipPaymasterWithPriceMarkup is TestBase { // submit userops vm.expectEmit(true, false, true, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); + startPrank(BUNDLER.addr); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + stopPrank(); // Calculate and assert price markups and gas payments calculateAndAssertAdjustments( @@ -231,17 +236,20 @@ contract TestSponsorshipPaymasterWithPriceMarkup is TestBase { initialFeeCollectorBalance, initialBundlerBalance, initialPaymasterEpBalance, - priceMarkup + 1e6 ); } - function skip_test_ValidatePaymasterAndPostOpWithPriceMarkup() external { + function test_ValidatePaymasterAndPostOpWithPriceMarkup() external { bicoPaymaster.depositFor{ value: 10 ether }(DAPP_ACCOUNT.addr); - // 10% priceMarkup on gas cost - uint32 priceMarkup = 1e6 + 1e5; + startPrank(PAYMASTER_OWNER.addr); + bicoPaymaster.setUnaccountedGas(25_000); + stopPrank(); + + // 10% priceMarkup on gas cost PackedUserOperation[] memory ops = new PackedUserOperation[](1); - (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, priceMarkup); + (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, 1_100_000, 35_000); ops[0] = userOp; uint256 initialBundlerBalance = BUNDLER.addr.balance; @@ -254,7 +262,10 @@ contract TestSponsorshipPaymasterWithPriceMarkup is TestBase { emit IBiconomySponsorshipPaymaster.PriceMarkupCollected(DAPP_ACCOUNT.addr, 0); vm.expectEmit(true, false, true, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); + + startPrank(BUNDLER.addr); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + stopPrank(); // Calculate and assert price markups and gas payments calculateAndAssertAdjustments( @@ -263,7 +274,7 @@ contract TestSponsorshipPaymasterWithPriceMarkup is TestBase { initialFeeCollectorBalance, initialBundlerBalance, initialPaymasterEpBalance, - priceMarkup + 1_100_000 ); } @@ -274,9 +285,8 @@ contract TestSponsorshipPaymasterWithPriceMarkup is TestBase { uint48 validAfter = uint48(block.timestamp); PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); - (userOp.paymasterAndData,) = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 - ); + PaymasterData memory pmData = PaymasterData(3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6); + (userOp.paymasterAndData,) = generateAndSignPaymasterData(userOp, PAYMASTER_SIGNER, bicoPaymaster, pmData); userOp.paymasterAndData = excludeLastNBytes(userOp.paymasterAndData, 2); userOp.signature = signUserOp(ALICE, userOp); @@ -293,9 +303,8 @@ contract TestSponsorshipPaymasterWithPriceMarkup is TestBase { uint48 validAfter = uint48(block.timestamp); PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); - (userOp.paymasterAndData,) = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 - ); + PaymasterData memory pmData = PaymasterData(3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6); + (userOp.paymasterAndData,) = generateAndSignPaymasterData(userOp, PAYMASTER_SIGNER, bicoPaymaster, pmData); userOp.signature = signUserOp(ALICE, userOp); ops[0] = userOp; @@ -311,9 +320,8 @@ contract TestSponsorshipPaymasterWithPriceMarkup is TestBase { uint48 validAfter = uint48(block.timestamp); PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); - (userOp.paymasterAndData,) = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6 - ); + PaymasterData memory pmData = PaymasterData(3e6, 3e6, DAPP_ACCOUNT.addr, validUntil, validAfter, 1e6); + (userOp.paymasterAndData,) = generateAndSignPaymasterData(userOp, PAYMASTER_SIGNER, bicoPaymaster, pmData); userOp.signature = signUserOp(ALICE, userOp); ops[0] = userOp; @@ -386,9 +394,9 @@ contract TestSponsorshipPaymasterWithPriceMarkup is TestBase { uint48 validAfter = uint48(block.timestamp); uint32 priceMarkup = 1e6; PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); - (bytes memory paymasterAndData, bytes memory signature) = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, paymasterId, validUntil, validAfter, priceMarkup - ); + PaymasterData memory pmData = PaymasterData(3e6, 3e6, paymasterId, validUntil, validAfter, priceMarkup); + (bytes memory paymasterAndData, bytes memory signature) = + generateAndSignPaymasterData(userOp, PAYMASTER_SIGNER, bicoPaymaster, pmData); ( address parsedPaymasterId, diff --git a/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol b/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol index c8bc619..c927dc5 100644 --- a/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol +++ b/test/unit/fuzz/TestFuzz_TestSponsorshipPaymaster.t.sol @@ -92,12 +92,18 @@ contract TestFuzz_SponsorshipPaymasterWithPriceMarkup is TestBase { assertEq(token.balanceOf(ALICE_ADDRESS), mintAmount); } - function skip_testFuzz_ValidatePaymasterAndPostOpWithPriceMarkup(uint32 priceMarkup) external { + // Review: fuzz with high markeup and current set values. + function testFuzz_ValidatePaymasterAndPostOpWithPriceMarkup(uint32 priceMarkup) external { vm.assume(priceMarkup <= 2e6 && priceMarkup > 1e6); bicoPaymaster.depositFor{ value: 10 ether }(DAPP_ACCOUNT.addr); + startPrank(PAYMASTER_OWNER.addr); + bicoPaymaster.setUnaccountedGas(30_000); + stopPrank(); + PackedUserOperation[] memory ops = new PackedUserOperation[](1); - (PackedUserOperation memory userOp, bytes32 userOpHash) = createUserOp(ALICE, bicoPaymaster, priceMarkup); + (PackedUserOperation memory userOp, bytes32 userOpHash) = + createUserOp(ALICE, bicoPaymaster, priceMarkup, 45_000); ops[0] = userOp; uint256 initialBundlerBalance = BUNDLER.addr.balance; @@ -110,7 +116,10 @@ contract TestFuzz_SponsorshipPaymasterWithPriceMarkup is TestBase { emit IBiconomySponsorshipPaymaster.PriceMarkupCollected(DAPP_ACCOUNT.addr, 0); vm.expectEmit(true, false, true, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.GasBalanceDeducted(DAPP_ACCOUNT.addr, 0, userOpHash); + + startPrank(BUNDLER.addr); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); + stopPrank(); // Calculate and assert price markups and gas payments calculateAndAssertAdjustments( @@ -133,9 +142,9 @@ contract TestFuzz_SponsorshipPaymasterWithPriceMarkup is TestBase { view { PackedUserOperation memory userOp = buildUserOpWithCalldata(ALICE, "", address(VALIDATOR_MODULE)); - (bytes memory paymasterAndData, bytes memory signature) = generateAndSignPaymasterData( - userOp, PAYMASTER_SIGNER, bicoPaymaster, 3e6, 3e6, paymasterId, validUntil, validAfter, priceMarkup - ); + PaymasterData memory pmData = PaymasterData(3e6, 3e6, paymasterId, validUntil, validAfter, priceMarkup); + (bytes memory paymasterAndData, bytes memory signature) = + generateAndSignPaymasterData(userOp, PAYMASTER_SIGNER, bicoPaymaster, pmData); ( address parsedPaymasterId,