diff --git a/docs/PAYMASTER_HUB.md b/docs/PAYMASTER_HUB.md index 3ea5d9a..e2dae84 100644 --- a/docs/PAYMASTER_HUB.md +++ b/docs/PAYMASTER_HUB.md @@ -118,7 +118,6 @@ mapping(bytes32 orgId => mapping(bytes32 => Budget)) private _budgets; - Rules (which contracts/functions are allowed) - Budgets (spending limits per user/role/org) - Fee caps (gas price limits) -- Bounty config (bundler incentives) **What is shared globally:** - Solidarity fund balance @@ -562,102 +561,6 @@ hub.setFeeCaps( --- -### Bundler Bounties - -Optional rewards to incentivize fast transaction inclusion. - -#### Bounty Structure - -```solidity -struct Bounty { - bool enabled; // Is bounty system enabled? - uint96 maxBountyWeiPerOp; // Max bounty per operation - uint16 pctBpCap; // Max % of gas cost (basis points) - uint144 totalPaid; // Lifetime bounties paid (tracking) -} - -// Storage: orgId → Bounty -mapping(bytes32 => Bounty) private _bounty; -``` - -#### Bounty Calculation - -```solidity -bountyAmount = min( - maxBountyWeiPerOp, - (actualGasCost * pctBpCap) / 10000 -); -``` - -**Example:** -- `maxBountyWeiPerOp = 0.001 ether` (1 finney) -- `pctBpCap = 500` (5%) -- `actualGasCost = 0.05 ether` - -Calculation: `min(0.001 ether, 0.05 ether * 500 / 10000) = min(0.001, 0.0025) = 0.001 ether` - -#### Bounty Payment - -Bounties are paid in `postOp` **only on successful execution**: - -```solidity -function _processBounty(bytes32 orgId, bytes32 userOpHash, address bundlerOrigin, uint256 actualGasCost) { - Bounty storage bounty = _bounty[orgId]; - - if (!bounty.enabled) return; - - uint256 bountyAmount = min( - bounty.maxBountyWeiPerOp, - (actualGasCost * bounty.pctBpCap) / 10000 - ); - - if (bountyAmount == 0) return; - - // Check bounty balance (separate from deposits) - if (address(this).balance < bountyAmount) { - emit BountyPayFailed(userOpHash, bundlerOrigin, bountyAmount); - return; - } - - // Pay bundler - (bool success,) = payable(bundlerOrigin).call{value: bountyAmount}(""); - - if (success) { - bounty.totalPaid += uint144(bountyAmount); - emit BountyPaid(userOpHash, bundlerOrigin, bountyAmount); - } else { - emit BountyPayFailed(userOpHash, bundlerOrigin, bountyAmount); - } -} -``` - -#### Funding Bounties - -Bounties are funded **separately** from paymaster deposits: - -```solidity -// Anyone can fund bounty pool -hub.fundBounty{value: 1 ether}(); - -// Admin can sweep unused bounty funds -hub.sweepBounty(payable(recipient), amount); -``` - -#### Setting Bounty Config - -```solidity -hub.setBounty( - orgId, - true, // enabled - 0.001 ether, // maxBountyWeiPerOp (1 finney) - 500 // pctBpCap (5%) -); -``` - -**Access:** Admin hat only - ---- - ### Access Control PaymasterHub uses **Hats Protocol** for decentralized, role-based access control. Each organization has two hats: @@ -1026,9 +929,8 @@ paymasterAndData = PaymasterHub address (20 bytes) + version (1 byte) + orgId (32 bytes) + subjectType (1 byte) - + subjectId (20 bytes) + + subjectId (32 bytes) + ruleId (4 bytes) - + mailboxCommit8 (8 bytes) ``` 2. **EntryPoint calls `validatePaymasterUserOp`:** @@ -1048,7 +950,6 @@ paymasterAndData = PaymasterHub address (20 bytes) - Update per-subject budget usage - Update org financials (50/50 split) - Collect 1% solidarity fee - - Pay bundler bounty (if configured) ### Storage Layout (ERC-7201) @@ -1063,7 +964,6 @@ FINANCIALS_STORAGE_LOCATION = 0x1234... // orgId → OrgFinancials FEECAPS_STORAGE_LOCATION = 0x31c1... // orgId → FeeCaps RULES_STORAGE_LOCATION = 0xbe22... // orgId → target → selector → Rule BUDGETS_STORAGE_LOCATION = 0xf14d... // orgId → subjectKey → Budget -BOUNTY_STORAGE_LOCATION = 0x5aef... // orgId → Bounty SOLIDARITY_STORAGE_LOCATION = 0xabcd... // SolidarityFund GRACEPERIOD_STORAGE_LOCATION = 0xfedc... // GracePeriodConfig ``` @@ -1188,11 +1088,6 @@ event FeeCapsSet(bytes32 indexed orgId, uint256 maxFeePerGas, ...); // Usage tracking event UsageIncreased(bytes32 indexed orgId, bytes32 subjectKey, uint256 delta, uint128 usedInEpoch, uint32 epochStart); -event UserOpPosted(bytes32 indexed opHash, address indexed postedBy); - -// Bounties -event BountyPaid(bytes32 indexed userOpHash, address indexed to, uint256 amount); -event BountyPayFailed(bytes32 indexed userOpHash, address indexed to, uint256 amount); // Governance event GracePeriodConfigUpdated(uint32 initialGraceDays, uint128 maxSpendDuringGrace, uint128 minDepositRequired); diff --git a/src/PaymasterHub.sol b/src/PaymasterHub.sol index f445dd4..6f2bafc 100644 --- a/src/PaymasterHub.sol +++ b/src/PaymasterHub.sol @@ -41,7 +41,6 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG error InvalidPaymasterData(); error ZeroAddress(); error InvalidEpochLength(); - error InvalidBountyConfig(); error ContractNotDeployed(); error ArrayLengthMismatch(); error OrgNotRegistered(); @@ -71,8 +70,6 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG uint32 private constant MIN_EPOCH_LENGTH = 1 hours; uint32 private constant MAX_EPOCH_LENGTH = 365 days; - uint256 private constant MAX_BOUNTY_PCT_BP = 10000; // 100% - // ============ Events ============ event PaymasterInitialized(address indexed entryPoint, address indexed hats, address indexed poaManager); event OrgRegistered(bytes32 indexed orgId, uint256 adminHatId, uint256 operatorHatId); @@ -92,15 +89,9 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG event OperatorHatSet(bytes32 indexed orgId, uint256 operatorHatId); event DepositIncrease(uint256 amount, uint256 newDeposit); event DepositWithdraw(address indexed to, uint256 amount); - event BountyConfig(bytes32 indexed orgId, bool enabled, uint96 maxPerOp, uint16 pctBpCap); - event BountyFunded(uint256 amount, uint256 newBalance); - event BountySweep(address indexed to, uint256 amount); - event BountyPaid(bytes32 indexed userOpHash, address indexed to, uint256 amount); - event BountyPayFailed(bytes32 indexed userOpHash, address indexed to, uint256 amount); event UsageIncreased( bytes32 indexed orgId, bytes32 subjectKey, uint256 delta, uint128 usedInEpoch, uint32 epochStart ); - event UserOpPosted(bytes32 indexed opHash, address indexed postedBy); event EmergencyWithdraw(address indexed to, uint256 amount); event OrgDepositReceived(bytes32 indexed orgId, address indexed from, uint256 amount); event SolidarityFeeCollected(bytes32 indexed orgId, uint256 amount); @@ -206,13 +197,6 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG uint32 epochStart; } - struct Bounty { - bool enabled; - uint96 maxBountyWeiPerOp; - uint16 pctBpCap; - uint144 totalPaid; - } - // ============ ERC-7201 Storage Locations ============ bytes32 private constant ORGS_STORAGE_LOCATION = keccak256(abi.encode(uint256(keccak256("poa.paymasterhub.orgs")) - 1)); @@ -222,8 +206,6 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG keccak256(abi.encode(uint256(keccak256("poa.paymasterhub.rules")) - 1)); bytes32 private constant BUDGETS_STORAGE_LOCATION = keccak256(abi.encode(uint256(keccak256("poa.paymasterhub.budgets")) - 1)); - bytes32 private constant BOUNTY_STORAGE_LOCATION = - keccak256(abi.encode(uint256(keccak256("poa.paymasterhub.bounty")) - 1)); bytes32 private constant FINANCIALS_STORAGE_LOCATION = keccak256(abi.encode(uint256(keccak256("poa.paymasterhub.financials")) - 1)); bytes32 private constant SOLIDARITY_STORAGE_LOCATION = @@ -674,7 +656,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG returns (bytes memory context, uint256 validationData) { // Decode and validate paymasterAndData - (uint8 version, bytes32 orgId, uint8 subjectType, bytes32 subjectId, uint32 ruleId, uint64 mailboxCommit8) = + (uint8 version, bytes32 orgId, uint8 subjectType, bytes32 subjectId, uint32 ruleId) = _decodePaymasterData(userOp.paymasterAndData); if (version != PAYMASTER_DATA_VERSION) revert InvalidVersion(); @@ -725,9 +707,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG } // Prepare context for postOp - context = abi.encode( - isOnboarding, contextOrgId, subjectKey, currentEpochStart, userOpHash, mailboxCommit8, uint160(tx.origin) - ); + context = abi.encode(isOnboarding, contextOrgId, subjectKey, currentEpochStart); // Return 0 for no signature failure and no time restrictions validationData = 0; @@ -791,15 +771,8 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG onlyEntryPoint nonReentrant { - ( - bool isOnboarding, - bytes32 orgId, - bytes32 subjectKey, - uint32 epochStart, - bytes32 userOpHash, - uint64 mailboxCommit8, - address bundlerOrigin - ) = abi.decode(context, (bool, bytes32, bytes32, uint32, bytes32, uint64, address)); + (bool isOnboarding, bytes32 orgId, bytes32 subjectKey, uint32 epochStart) = + abi.decode(context, (bool, bytes32, bytes32, uint32)); // Onboarding uses a dedicated context flag (not orgId sentinel). if (isOnboarding) { @@ -811,13 +784,8 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG // Update per-subject budget usage (existing functionality) _updateUsage(orgId, subjectKey, epochStart, actualGasCost); - // Update per-org financial tracking and collect solidarity fee (new) + // Update per-org financial tracking and collect solidarity fee _updateOrgFinancials(orgId, actualGasCost); - - // Process bounty only on successful execution - if (mode == IPaymaster.PostOpMode.opSucceeded && mailboxCommit8 != 0) { - _processBounty(orgId, userOpHash, bundlerOrigin, actualGasCost); - } } } @@ -1102,23 +1070,6 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG emit OperatorHatSet(orgId, operatorHatId); } - /** - * @notice Configure bounty parameters for an org - */ - function setBounty(bytes32 orgId, bool enabled, uint96 maxBountyWeiPerOp, uint16 pctBpCap) - external - onlyOrgAdmin(orgId) - { - if (pctBpCap > MAX_BOUNTY_PCT_BP) revert InvalidBountyConfig(); - - Bounty storage bounty = _getBountyStorage()[orgId]; - bounty.enabled = enabled; - bounty.maxBountyWeiPerOp = maxBountyWeiPerOp; - bounty.pctBpCap = pctBpCap; - - emit BountyConfig(orgId, enabled, maxBountyWeiPerOp, pctBpCap); - } - /** * @notice Deposit funds to EntryPoint for gas reimbursement (shared pool) * @dev Any org operator can deposit to shared pool @@ -1141,22 +1092,6 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG revert NotAdmin(); } - /** - * @notice Fund bounty pool (contract balance) - */ - function fundBounty() external payable { - emit BountyFunded(msg.value, address(this).balance); - } - - /** - * @notice Withdraw from bounty pool - * @dev Bounties are shared across all orgs, requires careful governance - */ - function sweepBounty(address payable to, uint256 amount) external { - // TODO: Add global admin mechanism - revert NotAdmin(); - } - /** * @notice Emergency withdrawal in case of critical issues * @dev Requires global admin - affects all orgs @@ -1283,18 +1218,6 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG emit OnboardingConfigUpdated(_maxGasPerCreation, _dailyCreationLimit, _enabled, _accountRegistry); } - // ============ Mailbox Function ============ - - /** - * @notice Post a UserOperation to the on-chain mailbox - * @param packedUserOp The packed user operation data - * @return opHash Hash of the posted operation - */ - function postUserOp(bytes calldata packedUserOp) external returns (bytes32 opHash) { - opHash = keccak256(packedUserOp); - emit UserOpPosted(opHash, msg.sender); - } - // ============ Storage Getters (for Lens) ============ /** @@ -1335,15 +1258,6 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG return _getFeeCapsStorage()[orgId]; } - /** - * @notice Get the bounty configuration for an org - * @param orgId Organization identifier - * @return The Bounty struct - */ - function getBountyConfig(bytes32 orgId) external view returns (Bounty memory) { - return _getBountyStorage()[orgId]; - } - /** * @notice Get org's financial tracking data * @param orgId Organization identifier @@ -1464,13 +1378,6 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG } } - function _getBountyStorage() private pure returns (mapping(bytes32 => Bounty) storage $) { - bytes32 slot = BOUNTY_STORAGE_LOCATION; - assembly { - $.slot := slot - } - } - function _getFinancialsStorage() private pure returns (mapping(bytes32 => OrgFinancials) storage $) { bytes32 slot = FINANCIALS_STORAGE_LOCATION; assembly { @@ -1544,19 +1451,12 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG function _decodePaymasterData(bytes calldata paymasterAndData) private pure - returns ( - uint8 version, - bytes32 orgId, - uint8 subjectType, - bytes32 subjectId, - uint32 ruleId, - uint64 mailboxCommit8 - ) + returns (uint8 version, bytes32 orgId, uint8 subjectType, bytes32 subjectId, uint32 ruleId) { // ERC-4337 v0.7 packed format: - // [paymaster(20) | verificationGasLimit(16) | postOpGasLimit(16) | version(1) | orgId(32) | subjectType(1) | subjectId(32) | ruleId(4) | mailboxCommit(8)] - // = 130 bytes total. Custom data starts at offset 52. - if (paymasterAndData.length < 130) revert InvalidPaymasterData(); + // [paymaster(20) | verificationGasLimit(16) | postOpGasLimit(16) | version(1) | orgId(32) | subjectType(1) | subjectId(32) | ruleId(4)] + // = 122 bytes total. Custom data starts at offset 52. + if (paymasterAndData.length < 122) revert InvalidPaymasterData(); // Skip first 52 bytes (paymaster address + v0.7 gas limits) and decode the rest version = uint8(paymasterAndData[52]); @@ -1570,9 +1470,6 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG // Extract ruleId from bytes 118-121 ruleId = uint32(bytes4(paymasterAndData[118:122])); - - // Extract mailboxCommit8 from bytes 122-129 - mailboxCommit8 = uint64(bytes8(paymasterAndData[122:130])); } function _validateSubjectEligibility(address sender, uint8 subjectType, bytes32 subjectId) @@ -1899,40 +1796,4 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG if (solidarity.balance < actualGasCost) revert InsufficientFunds(); solidarity.balance -= uint128(actualGasCost); } - - function _processBounty(bytes32 orgId, bytes32 userOpHash, address bundlerOrigin, uint256 actualGasCost) private { - Bounty storage bounty = _getBountyStorage()[orgId]; - - if (!bounty.enabled) return; - - // Calculate tip amount - uint256 tip = bounty.maxBountyWeiPerOp; - if (bounty.pctBpCap > 0) { - uint256 pctTip = (actualGasCost * bounty.pctBpCap) / 10000; - if (pctTip < tip) { - tip = pctTip; - } - } - - // Ensure we have sufficient balance - if (tip > address(this).balance) { - tip = address(this).balance; - } - - if (tip > 0) { - (bool success,) = bundlerOrigin.call{value: tip, gas: 30000}(""); - - if (success) { - bounty.totalPaid += uint144(tip); - emit BountyPaid(userOpHash, bundlerOrigin, tip); - } else { - emit BountyPayFailed(userOpHash, bundlerOrigin, tip); - } - } - } - - // ============ Receive Function ============ - receive() external payable { - emit BountyFunded(msg.value, address(this).balance); - } } diff --git a/src/PaymasterHubLens.sol b/src/PaymasterHubLens.sol index 5e72dc8..cd87591 100644 --- a/src/PaymasterHubLens.sol +++ b/src/PaymasterHubLens.sol @@ -38,20 +38,12 @@ struct Budget { uint32 epochStart; } -struct Bounty { - bool enabled; - uint96 maxBountyWeiPerOp; - uint16 pctBpCap; - uint144 totalPaid; -} - // Interface for PaymasterHub Storage Getters interface IPaymasterHubStorage { function getConfig() external view returns (Config memory); function getBudget(bytes32 key) external view returns (Budget memory); function getRule(address target, bytes4 selector) external view returns (Rule memory); function getFeeCaps() external view returns (FeeCaps memory); - function getBountyConfig() external view returns (Bounty memory); function ENTRY_POINT() external view returns (address); } @@ -134,19 +126,11 @@ contract PaymasterHubLens { return hub.getFeeCaps(); } - function bountyInfo() external view returns (Bounty memory) { - return hub.getBountyConfig(); - } - function entryPointDeposit() external view returns (uint256) { address entryPoint = hub.ENTRY_POINT(); return IEntryPoint(entryPoint).balanceOf(address(hub)); } - function bountyBalance() external view returns (uint256) { - return address(hub).balance; - } - /** * @notice Check if a UserOperation would be valid without state changes */ @@ -160,7 +144,7 @@ contract PaymasterHubLens { if (cfg.paused) return (false, "Paused"); // Decode paymasterAndData - (uint8 version, uint8 subjectType, bytes32 subjectId, uint32 ruleId,) = + (uint8 version, uint8 subjectType, bytes32 subjectId, uint32 ruleId) = _decodePaymasterData(userOp.paymasterAndData); if (version != PAYMASTER_DATA_VERSION) return (false, "InvalidVersion"); @@ -209,12 +193,12 @@ contract PaymasterHubLens { function _decodePaymasterData(bytes calldata paymasterAndData) private pure - returns (uint8 version, uint8 subjectType, bytes32 subjectId, uint32 ruleId, uint64 mailboxCommit8) + returns (uint8 version, uint8 subjectType, bytes32 subjectId, uint32 ruleId) { // ERC-4337 v0.7 packed format (must match PaymasterHub._decodePaymasterData): - // [paymaster(20) | verificationGasLimit(16) | postOpGasLimit(16) | version(1) | orgId(32) | subjectType(1) | subjectId(32) | ruleId(4) | mailboxCommit(8)] - // = 130 bytes total. Custom data starts at offset 52. - if (paymasterAndData.length < 130) revert InvalidPaymasterData(); + // [paymaster(20) | verificationGasLimit(16) | postOpGasLimit(16) | version(1) | orgId(32) | subjectType(1) | subjectId(32) | ruleId(4)] + // = 122 bytes total. Custom data starts at offset 52. + if (paymasterAndData.length < 122) revert InvalidPaymasterData(); // Skip first 52 bytes (paymaster address + v0.7 gas limits) and decode the rest // orgId at [53:85] is skipped (not needed by Lens) @@ -228,9 +212,6 @@ contract PaymasterHubLens { // Extract ruleId from bytes 118-121 ruleId = uint32(bytes4(paymasterAndData[118:122])); - - // Extract mailboxCommit8 from bytes 122-129 - mailboxCommit8 = uint64(bytes8(paymasterAndData[122:130])); } function _extractTargetSelector(PackedUserOperation calldata userOp, uint32 ruleId) diff --git a/test/PasskeyPaymasterIntegration.t.sol b/test/PasskeyPaymasterIntegration.t.sol index 56a9f3e..a7556c6 100644 --- a/test/PasskeyPaymasterIntegration.t.sol +++ b/test/PasskeyPaymasterIntegration.t.sol @@ -190,14 +190,12 @@ contract PasskeyPaymasterIntegrationTest is Test { hub.setBudget(ORG_ID, subjectKey, 1 ether, 1 days); } - function _buildPaymasterData( - bytes32 orgId, - uint8 subjectType, - bytes32 subjectId, - uint32 ruleId, - uint64 mailboxCommit8 - ) internal view returns (bytes memory) { - // ERC-4337 v0.7 format: paymaster(20) | verificationGasLimit(16) | postOpGasLimit(16) | version(1) | orgId(32) | subjectType(1) | subjectId(32) | ruleId(4) | mailboxCommit(8) = 130 bytes + function _buildPaymasterData(bytes32 orgId, uint8 subjectType, bytes32 subjectId, uint32 ruleId) + internal + view + returns (bytes memory) + { + // ERC-4337 v0.7 format: paymaster(20) | verificationGasLimit(16) | postOpGasLimit(16) | version(1) | orgId(32) | subjectType(1) | subjectId(32) | ruleId(4) = 122 bytes return abi.encodePacked( address(hub), // 20 bytes uint128(200_000), // paymasterVerificationGasLimit - 16 bytes @@ -206,8 +204,7 @@ contract PasskeyPaymasterIntegrationTest is Test { orgId, // 32 bytes subjectType, // 1 byte subjectId, // 32 bytes - ruleId, // 4 bytes - mailboxCommit8 // 8 bytes + ruleId // 4 bytes ); } @@ -261,7 +258,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory innerCall = abi.encodeWithSelector(MockTarget.doSomething.selector); bytes memory callData = _buildExecuteCalldata(address(mockTarget), 0, innerCall); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -284,7 +281,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory innerCall = abi.encodeWithSelector(MockTarget.doSomething.selector); bytes memory callData = _buildExecuteCalldata(address(mockTarget), 0, innerCall); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -315,7 +312,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory innerCall1 = abi.encodeWithSelector(MockTarget.doSomething.selector); bytes memory callData1 = _buildExecuteCalldata(address(target1), 0, innerCall1); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp1 = _createUserOp(address(account), callData1, paymasterAndData, ""); @@ -367,7 +364,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory callData = _buildExecuteBatchCalldata(targets, values, datas); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -395,7 +392,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory callData = _buildExecuteBatchCalldata(targets, values, datas); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -432,7 +429,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory callData = _buildExecuteBatchCalldata(targets, values, datas); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -473,7 +470,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory callData = _buildExecuteBatchCalldata(targets, values, datas); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -496,7 +493,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory callData = _buildExecuteBatchCalldata(targets, values, datas); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -528,7 +525,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory callData = _buildExecuteBatchCalldata(targets, values, datas); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -574,7 +571,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory callData = _buildExecuteBatchCalldata(targets, values, datas); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -614,7 +611,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory callData = _buildExecuteBatchCalldata(targets, values, datas); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -655,7 +652,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory callData = _buildExecuteBatchCalldata(targets, values, datas); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -688,7 +685,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory callData = _buildExecuteBatchCalldata(targets, values, datas); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -719,7 +716,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory callData = abi.encodeWithSelector(SIMPLE_EXECUTE_BATCH_SELECTOR, targets, datas); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -744,7 +741,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory callData = abi.encodeWithSelector(SIMPLE_EXECUTE_BATCH_SELECTOR, targets, datas); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -772,7 +769,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory callData = _buildExecuteBatchCalldata(targets, values, datas); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -797,7 +794,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory callData = _buildExecuteBatchCalldata(targets, values, datas); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -824,7 +821,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory callData = _buildExecuteBatchCalldata(targets, values, datas); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -862,7 +859,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory callData = _buildExecuteBatchCalldata(targets, values, datas); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -896,7 +893,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory callData = _buildExecuteBatchCalldata(targets, values, datas); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -925,7 +922,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory callData = _buildExecuteBatchCalldata(targets, values, datas); // Use COARSE mode — needs (account, executeBatch) rule, not inner rules bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_COARSE, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_COARSE ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -954,7 +951,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory callData = _buildExecuteBatchCalldata(targets, values, datas); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); // UserOp has callGasLimit=500000 (from _createUserOp), which exceeds the 50k hint @@ -986,7 +983,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory callData = _buildExecuteBatchCalldata(targets, values, datas); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_COARSE, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_COARSE ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -1015,7 +1012,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory callData = _buildExecuteBatchCalldata(targets, values, datas); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_EXECUTOR, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_EXECUTOR ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -1055,7 +1052,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory callData = _buildExecuteBatchCalldata(targets, values, datas); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -1082,7 +1079,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory innerCall = abi.encodeWithSelector(MockTarget.doSomething.selector); bytes memory callData = _buildExecuteCalldata(address(mockTarget), 0, innerCall); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -1106,7 +1103,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory innerCall = abi.encodeWithSelector(MockTarget.doSomething.selector); bytes memory callData = _buildExecuteCalldata(address(mockTarget), 0, innerCall); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_COARSE, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_COARSE ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -1145,7 +1142,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory innerCall = abi.encodeWithSelector(MockTarget.doSomething.selector); bytes memory callData = _buildExecuteCalldata(address(mockTarget), 0, innerCall); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); // UserOp with gas limits within caps @@ -1181,7 +1178,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory innerCall = abi.encodeWithSelector(MockTarget.doSomething.selector); bytes memory callData = _buildExecuteCalldata(address(mockTarget), 0, innerCall); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); // UserOp with verification gas exceeding cap (400k > 300k) @@ -1221,7 +1218,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory innerCall = abi.encodeWithSelector(MockTarget.doSomething.selector); bytes memory callData = _buildExecuteCalldata(address(mockTarget), 0, innerCall); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -1247,7 +1244,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory innerCall = abi.encodeWithSelector(MockTarget.doSomething.selector); bytes memory callData = _buildExecuteCalldata(address(mockTarget), 0, innerCall); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -1273,7 +1270,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory innerCall = abi.encodeWithSelector(MockTarget.doSomething.selector); bytes memory callData = _buildExecuteCalldata(address(mockTarget), 0, innerCall); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); @@ -1286,8 +1283,7 @@ contract PasskeyPaymasterIntegrationTest is Test { assertTrue(context.length > 0, "Context should be returned"); // Verify context contains explicit onboarding flag + correct orgId. - (bool isOnboarding, bytes32 decodedOrgId,,,,,) = - abi.decode(context, (bool, bytes32, bytes32, uint32, bytes32, uint64, address)); + (bool isOnboarding, bytes32 decodedOrgId,,) = abi.decode(context, (bool, bytes32, bytes32, uint32)); assertFalse(isOnboarding, "Regular org operation should not be flagged as onboarding"); assertEq(decodedOrgId, ORG_ID, "Context should contain correct orgId"); } @@ -1300,11 +1296,11 @@ contract PasskeyPaymasterIntegrationTest is Test { PasskeyAccount account = _createPasskeyAccount(); bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC, 0 + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC ); - // Should be exactly 130 bytes (86 + 32 subjectId + 12 ruleId/mailbox for v0.7 gas limits) - assertEq(paymasterAndData.length, 130, "paymasterAndData should be 130 bytes"); + // Should be exactly 122 bytes (86 + 32 subjectId + 4 ruleId for v0.7 gas limits) + assertEq(paymasterAndData.length, 122, "paymasterAndData should be 122 bytes"); // Verify structure using assembly to extract values from memory address paymaster; @@ -1365,7 +1361,7 @@ contract PasskeyPaymasterIntegrationTest is Test { // Build UserOp with SUBJECT_TYPE_HAT bytes memory innerCall = abi.encodeWithSelector(MockTarget.doSomething.selector); bytes memory callData = _buildExecuteCalldata(address(mockTarget), 0, innerCall); - bytes memory paymasterAndData = _buildPaymasterData(ORG_ID, SUBJECT_TYPE_HAT, bytes32(USER_HAT), 0, 0); + bytes memory paymasterAndData = _buildPaymasterData(ORG_ID, SUBJECT_TYPE_HAT, bytes32(USER_HAT), 0); PackedUserOperation memory userOp = _createUserOp(eligibleUser, callData, paymasterAndData, ""); @@ -1393,7 +1389,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory innerCall = abi.encodeWithSelector(MockTarget.doSomething.selector); bytes memory callData = _buildExecuteCalldata(address(mockTarget), 0, innerCall); - bytes memory paymasterAndData = _buildPaymasterData(ORG_ID, SUBJECT_TYPE_HAT, bytes32(USER_HAT), 0, 0); + bytes memory paymasterAndData = _buildPaymasterData(ORG_ID, SUBJECT_TYPE_HAT, bytes32(USER_HAT), 0); PackedUserOperation memory userOp = _createUserOp(ineligibleUser, callData, paymasterAndData, ""); @@ -1414,7 +1410,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory innerCall = abi.encodeWithSelector(MockTarget.doSomething.selector); bytes memory callData = _buildExecuteCalldata(address(mockTarget), 0, innerCall); - bytes memory paymasterAndData = _buildPaymasterData(ORG_ID, SUBJECT_TYPE_HAT, bytes32(USER_HAT), 0, 0); + bytes memory paymasterAndData = _buildPaymasterData(ORG_ID, SUBJECT_TYPE_HAT, bytes32(USER_HAT), 0); PackedUserOperation memory userOp = _createUserOp(user, callData, paymasterAndData, ""); @@ -1445,7 +1441,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory innerCall = abi.encodeWithSelector(MockTarget.doSomething.selector); bytes memory callData = _buildExecuteCalldata(address(mockTarget), 0, innerCall); - bytes memory paymasterAndData = _buildPaymasterData(ORG_ID, SUBJECT_TYPE_HAT, bytes32(USER_HAT), 0, 0); + bytes memory paymasterAndData = _buildPaymasterData(ORG_ID, SUBJECT_TYPE_HAT, bytes32(USER_HAT), 0); PackedUserOperation memory userOp = _createUserOp(eligibleUser, callData, paymasterAndData, ""); @@ -1469,7 +1465,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory innerCall = abi.encodeWithSelector(MockTarget.doSomething.selector); bytes memory callData = _buildExecuteCalldata(address(mockTarget), 0, innerCall); - bytes memory paymasterAndData = _buildPaymasterData(ORG_ID, SUBJECT_TYPE_HAT, bytes32(USER_HAT), 0, 0); + bytes memory paymasterAndData = _buildPaymasterData(ORG_ID, SUBJECT_TYPE_HAT, bytes32(USER_HAT), 0); PackedUserOperation memory userOp = _createUserOp(user, callData, paymasterAndData, ""); @@ -1496,7 +1492,7 @@ contract PasskeyPaymasterIntegrationTest is Test { bytes memory innerCall = abi.encodeWithSelector(MockTarget.doSomething.selector); bytes memory callData = _buildExecuteCalldata(address(mockTarget), 0, innerCall); - bytes memory paymasterAndData = _buildPaymasterData(ORG_ID, SUBJECT_TYPE_HAT, bytes32(USER_HAT), 0, 0); + bytes memory paymasterAndData = _buildPaymasterData(ORG_ID, SUBJECT_TYPE_HAT, bytes32(USER_HAT), 0); PackedUserOperation memory userOp = _createUserOp(eligibleUser, callData, paymasterAndData, ""); @@ -1507,6 +1503,199 @@ contract PasskeyPaymasterIntegrationTest is Test { assertEq(validationData, 0, "Validation should succeed after hat reactivation"); assertTrue(context.length > 0, "Context should be returned"); } + + /*══════════════════════════════════════════════════════════════════════ + PAYLOAD FORMAT BOUNDARY TESTS + ══════════════════════════════════════════════════════════════════════*/ + + /// @notice Exactly 121 bytes (one short) must revert + function testPaymasterAndData_ExactBoundary121Reverts() public { + PasskeyAccount account = _createPasskeyAccount(); + + // Build 122-byte valid data, then trim last byte to get 121 + bytes memory valid = _buildPaymasterData( + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC + ); + bytes memory tooShort = new bytes(121); + for (uint256 i = 0; i < 121; i++) { + tooShort[i] = valid[i]; + } + + bytes memory innerCall = abi.encodeWithSelector(MockTarget.doSomething.selector); + bytes memory callData = _buildExecuteCalldata(address(mockTarget), 0, innerCall); + PackedUserOperation memory userOp = _createUserOp(address(account), callData, tooShort, ""); + + vm.prank(address(entryPoint)); + vm.expectRevert(PaymasterHub.InvalidPaymasterData.selector); + hub.validatePaymasterUserOp(userOp, bytes32(0), 0.001 ether); + } + + /// @notice Exactly 122 bytes must be accepted + function testPaymasterAndData_ExactBoundary122Succeeds() public { + PasskeyAccount account = _createPasskeyAccount(); + _setupDefaultBudget(address(account)); + + vm.prank(orgAdmin); + hub.setRule(ORG_ID, address(mockTarget), MockTarget.doSomething.selector, true, 0); + + bytes memory paymasterAndData = _buildPaymasterData( + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC + ); + assertEq(paymasterAndData.length, 122, "Should be exactly 122 bytes"); + + bytes memory innerCall = abi.encodeWithSelector(MockTarget.doSomething.selector); + bytes memory callData = _buildExecuteCalldata(address(mockTarget), 0, innerCall); + PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); + + vm.prank(address(entryPoint)); + (bytes memory context, uint256 validationData) = + hub.validatePaymasterUserOp(userOp, bytes32(uint256(1)), 0.01 ether); + assertEq(validationData, 0, "Validation should succeed with exactly 122 bytes"); + assertTrue(context.length > 0, "Context should be returned"); + } + + /// @notice Longer-than-minimum payloads (e.g. old 130-byte format) must still be accepted + function testPaymasterAndData_LongerThan122Succeeds() public { + PasskeyAccount account = _createPasskeyAccount(); + _setupDefaultBudget(address(account)); + + vm.prank(orgAdmin); + hub.setRule(ORG_ID, address(mockTarget), MockTarget.doSomething.selector, true, 0); + + // Build valid 122-byte data, then append 8 trailing bytes (simulating old 130-byte format) + bytes memory base = _buildPaymasterData( + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC + ); + bytes memory extended = abi.encodePacked(base, uint64(0)); + assertEq(extended.length, 130, "Should be 130 bytes (old format)"); + + bytes memory innerCall = abi.encodeWithSelector(MockTarget.doSomething.selector); + bytes memory callData = _buildExecuteCalldata(address(mockTarget), 0, innerCall); + PackedUserOperation memory userOp = _createUserOp(address(account), callData, extended, ""); + + vm.prank(address(entryPoint)); + (bytes memory context, uint256 validationData) = + hub.validatePaymasterUserOp(userOp, bytes32(uint256(1)), 0.01 ether); + assertEq(validationData, 0, "Validation should succeed with 130 bytes (backward compat)"); + assertTrue(context.length > 0, "Context should be returned"); + } + + /*══════════════════════════════════════════════════════════════════════ + CONTEXT ENCODING / POSTOP ROUND-TRIP TESTS + ══════════════════════════════════════════════════════════════════════*/ + + /// @notice Validate → postOp round-trip succeeds for normal org operation (opSucceeded) + function testPostOp_NormalOrgOp_Succeeds() public { + PasskeyAccount account = _createPasskeyAccount(); + _setupDefaultBudget(address(account)); + + vm.prank(orgAdmin); + hub.setRule(ORG_ID, address(mockTarget), MockTarget.doSomething.selector, true, 0); + + bytes memory paymasterAndData = _buildPaymasterData( + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC + ); + + bytes memory innerCall = abi.encodeWithSelector(MockTarget.doSomething.selector); + bytes memory callData = _buildExecuteCalldata(address(mockTarget), 0, innerCall); + PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); + + // Validate + vm.prank(address(entryPoint)); + (bytes memory context,) = hub.validatePaymasterUserOp(userOp, bytes32(uint256(1)), 0.01 ether); + + // PostOp with opSucceeded should not revert + vm.prank(address(entryPoint)); + hub.postOp(IPaymaster.PostOpMode.opSucceeded, context, 50_000, 1); + } + + /// @notice Validate → postOp round-trip succeeds for normal org operation (opReverted) + function testPostOp_NormalOrgOp_Reverted() public { + PasskeyAccount account = _createPasskeyAccount(); + _setupDefaultBudget(address(account)); + + vm.prank(orgAdmin); + hub.setRule(ORG_ID, address(mockTarget), MockTarget.doSomething.selector, true, 0); + + bytes memory paymasterAndData = _buildPaymasterData( + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC + ); + + bytes memory innerCall = abi.encodeWithSelector(MockTarget.doSomething.selector); + bytes memory callData = _buildExecuteCalldata(address(mockTarget), 0, innerCall); + PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); + + // Validate + vm.prank(address(entryPoint)); + (bytes memory context,) = hub.validatePaymasterUserOp(userOp, bytes32(uint256(1)), 0.01 ether); + + // PostOp with opReverted should not revert + vm.prank(address(entryPoint)); + hub.postOp(IPaymaster.PostOpMode.opReverted, context, 50_000, 1); + } + + /// @notice Context encodes exactly 4 fields: isOnboarding, orgId, subjectKey, epochStart + function testContext_FourFieldEncoding() public { + PasskeyAccount account = _createPasskeyAccount(); + _setupDefaultBudget(address(account)); + + vm.prank(orgAdmin); + hub.setRule(ORG_ID, address(mockTarget), MockTarget.doSomething.selector, true, 0); + + bytes memory paymasterAndData = _buildPaymasterData( + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC + ); + + bytes memory innerCall = abi.encodeWithSelector(MockTarget.doSomething.selector); + bytes memory callData = _buildExecuteCalldata(address(mockTarget), 0, innerCall); + PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); + + vm.prank(address(entryPoint)); + (bytes memory context,) = hub.validatePaymasterUserOp(userOp, bytes32(uint256(1)), 0.01 ether); + + // Decode all 4 fields — would revert if encoding doesn't match + (bool isOnboarding, bytes32 orgId, bytes32 subjectKey, uint32 epochStart) = + abi.decode(context, (bool, bytes32, bytes32, uint32)); + + assertFalse(isOnboarding, "Normal op should not be onboarding"); + assertEq(orgId, ORG_ID, "OrgId should match"); + assertTrue(subjectKey != bytes32(0), "SubjectKey should be non-zero"); + assertTrue(epochStart > 0, "EpochStart should be non-zero"); + } + + /// @notice Verify postOp updates budget usage (accounting works without bounty) + function testPostOp_UpdatesBudget() public { + PasskeyAccount account = _createPasskeyAccount(); + bytes32 subjectKey = keccak256(abi.encodePacked(uint8(0), bytes32(uint256(uint160(address(account)))))); + _setupDefaultBudget(address(account)); + + vm.prank(orgAdmin); + hub.setRule(ORG_ID, address(mockTarget), MockTarget.doSomething.selector, true, 0); + + bytes memory paymasterAndData = _buildPaymasterData( + ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_GENERIC + ); + + bytes memory innerCall = abi.encodeWithSelector(MockTarget.doSomething.selector); + bytes memory callData = _buildExecuteCalldata(address(mockTarget), 0, innerCall); + PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); + + // Check budget before + PaymasterHub.Budget memory budgetBefore = hub.getBudget(ORG_ID, subjectKey); + assertEq(budgetBefore.usedInEpoch, 0, "Budget should start at 0"); + + // Validate + postOp + vm.prank(address(entryPoint)); + (bytes memory context,) = hub.validatePaymasterUserOp(userOp, bytes32(uint256(1)), 0.01 ether); + + uint256 gasCost = 50_000; + vm.prank(address(entryPoint)); + hub.postOp(IPaymaster.PostOpMode.opSucceeded, context, gasCost, 1); + + // Budget should have increased + PaymasterHub.Budget memory budgetAfter = hub.getBudget(ORG_ID, subjectKey); + assertEq(budgetAfter.usedInEpoch, gasCost, "Budget usage should reflect gas cost"); + } } /*══════════════════════════════════════════════════════════════════════════ diff --git a/test/PaymasterHubSolidarity.t.sol b/test/PaymasterHubSolidarity.t.sol index 423c079..42d3d7a 100644 --- a/test/PaymasterHubSolidarity.t.sol +++ b/test/PaymasterHubSolidarity.t.sol @@ -1318,14 +1318,12 @@ contract PaymasterHubSolidarityTest is Test { event OnboardingAccountCreated(address indexed account, uint256 gasCost); - function _buildPaymasterData( - bytes32 orgId, - uint8 subjectType, - bytes32 subjectId, - uint32 ruleId, - uint64 mailboxCommit8 - ) internal view returns (bytes memory) { - // ERC-4337 v0.7 format: paymaster(20) | verificationGasLimit(16) | postOpGasLimit(16) | version(1) | orgId(32) | subjectType(1) | subjectId(32) | ruleId(4) | mailboxCommit(8) = 130 bytes + function _buildPaymasterData(bytes32 orgId, uint8 subjectType, bytes32 subjectId, uint32 ruleId) + internal + view + returns (bytes memory) + { + // ERC-4337 v0.7 format: paymaster(20) | verificationGasLimit(16) | postOpGasLimit(16) | version(1) | orgId(32) | subjectType(1) | subjectId(32) | ruleId(4) = 122 bytes return abi.encodePacked( address(hub), uint128(200_000), @@ -1334,8 +1332,7 @@ contract PaymasterHubSolidarityTest is Test { orgId, subjectType, subjectId, - ruleId, - mailboxCommit8 + ruleId ); } @@ -1362,8 +1359,7 @@ contract PaymasterHubSolidarityTest is Test { hub.donateToSolidarity{value: 1 ether}(); vm.prank(poaManager); hub.setOnboardingConfig(uint128(MAX_COST), 10, true, address(0)); - bytes memory pmData = - _buildPaymasterData(ORG_ALPHA, SUBJECT_TYPE_POA_ONBOARDING, bytes32(0), RULE_ID_GENERIC, uint64(0)); + bytes memory pmData = _buildPaymasterData(ORG_ALPHA, SUBJECT_TYPE_POA_ONBOARDING, bytes32(0), RULE_ID_GENERIC); PackedUserOperation memory userOp = _buildUserOp(address(0xdead), "", pmData); userOp.initCode = hex"01"; vm.prank(address(entryPoint)); @@ -1383,8 +1379,7 @@ contract PaymasterHubSolidarityTest is Test { vm.prank(poaManager); hub.setOnboardingConfig(uint128(MAX_COST), 10, true, address(0)); address deployed = address(new DummySender()); - bytes memory pmData = - _buildPaymasterData(bytes32(0), SUBJECT_TYPE_POA_ONBOARDING, bytes32(0), RULE_ID_GENERIC, uint64(0)); + bytes memory pmData = _buildPaymasterData(bytes32(0), SUBJECT_TYPE_POA_ONBOARDING, bytes32(0), RULE_ID_GENERIC); PackedUserOperation memory userOp = _buildUserOp(deployed, "", pmData); userOp.initCode = hex"01"; vm.prank(address(entryPoint)); @@ -1400,8 +1395,7 @@ contract PaymasterHubSolidarityTest is Test { vm.prank(poaManager); hub.setOnboardingConfig(uint128(MAX_COST), 10, true, address(0)); address newAccount = address(0xbeef); - bytes memory pmData = - _buildPaymasterData(bytes32(0), SUBJECT_TYPE_POA_ONBOARDING, bytes32(0), RULE_ID_GENERIC, uint64(0)); + bytes memory pmData = _buildPaymasterData(bytes32(0), SUBJECT_TYPE_POA_ONBOARDING, bytes32(0), RULE_ID_GENERIC); PackedUserOperation memory userOp = _buildUserOp(newAccount, "", pmData); userOp.initCode = hex"01"; vm.prank(address(entryPoint)); @@ -1426,8 +1420,7 @@ contract PaymasterHubSolidarityTest is Test { vm.prank(poaManager); hub.setOnboardingConfig(uint128(MAX_COST), 1, true, address(0)); address account1 = address(0xaa01); - bytes memory pmData1 = - _buildPaymasterData(bytes32(0), SUBJECT_TYPE_POA_ONBOARDING, bytes32(0), RULE_ID_GENERIC, uint64(0)); + bytes memory pmData1 = _buildPaymasterData(bytes32(0), SUBJECT_TYPE_POA_ONBOARDING, bytes32(0), RULE_ID_GENERIC); PackedUserOperation memory userOp1 = _buildUserOp(account1, "", pmData1); userOp1.initCode = hex"01"; vm.prank(address(entryPoint)); @@ -1437,8 +1430,7 @@ contract PaymasterHubSolidarityTest is Test { hub.postOp(IPaymaster.PostOpMode.opReverted, context1, 50_000, 1); // Second onboarding should succeed because the failed op's slot was refunded address account2 = address(0xaa02); - bytes memory pmData2 = - _buildPaymasterData(bytes32(0), SUBJECT_TYPE_POA_ONBOARDING, bytes32(0), RULE_ID_GENERIC, uint64(0)); + bytes memory pmData2 = _buildPaymasterData(bytes32(0), SUBJECT_TYPE_POA_ONBOARDING, bytes32(0), RULE_ID_GENERIC); PackedUserOperation memory userOp2 = _buildUserOp(account2, "", pmData2); userOp2.initCode = hex"01"; vm.prank(address(entryPoint)); @@ -1448,8 +1440,7 @@ contract PaymasterHubSolidarityTest is Test { hub.postOp(IPaymaster.PostOpMode.opSucceeded, context2, 50_000, 1); // Third onboarding should now be blocked (limit of 1 reached) address account3 = address(0xaa03); - bytes memory pmData3 = - _buildPaymasterData(bytes32(0), SUBJECT_TYPE_POA_ONBOARDING, bytes32(0), RULE_ID_GENERIC, uint64(0)); + bytes memory pmData3 = _buildPaymasterData(bytes32(0), SUBJECT_TYPE_POA_ONBOARDING, bytes32(0), RULE_ID_GENERIC); PackedUserOperation memory userOp3 = _buildUserOp(account3, "", pmData3); userOp3.initCode = hex"01"; vm.prank(address(entryPoint)); @@ -1466,8 +1457,7 @@ contract PaymasterHubSolidarityTest is Test { vm.prank(poaManager); hub.setOnboardingConfig(uint128(MAX_COST), 10, true, registry); address deployed = address(new DummySender()); - bytes memory pmData = - _buildPaymasterData(bytes32(0), SUBJECT_TYPE_POA_ONBOARDING, bytes32(0), RULE_ID_GENERIC, uint64(0)); + bytes memory pmData = _buildPaymasterData(bytes32(0), SUBJECT_TYPE_POA_ONBOARDING, bytes32(0), RULE_ID_GENERIC); // Build execute(registryAddress, 0, registerAccount("alice")) bytes memory innerData = abi.encodeWithSelector(bytes4(0xbff6de20), "alice"); bytes memory execCallData = abi.encodeWithSelector(bytes4(0xb61d27f6), registry, uint256(0), innerData); @@ -1487,8 +1477,7 @@ contract PaymasterHubSolidarityTest is Test { vm.prank(poaManager); hub.setOnboardingConfig(uint128(MAX_COST), 10, true, registry); address deployed = address(new DummySender()); - bytes memory pmData = - _buildPaymasterData(bytes32(0), SUBJECT_TYPE_POA_ONBOARDING, bytes32(0), RULE_ID_GENERIC, uint64(0)); + bytes memory pmData = _buildPaymasterData(bytes32(0), SUBJECT_TYPE_POA_ONBOARDING, bytes32(0), RULE_ID_GENERIC); // Build execute(wrongTarget, 0, registerAccount("alice")) bytes memory innerData = abi.encodeWithSelector(bytes4(0xbff6de20), "alice"); bytes memory execCallData = abi.encodeWithSelector(bytes4(0xb61d27f6), address(0xBAD), uint256(0), innerData); @@ -1508,8 +1497,7 @@ contract PaymasterHubSolidarityTest is Test { vm.prank(poaManager); hub.setOnboardingConfig(uint128(MAX_COST), 10, true, registry); address deployed = address(new DummySender()); - bytes memory pmData = - _buildPaymasterData(bytes32(0), SUBJECT_TYPE_POA_ONBOARDING, bytes32(0), RULE_ID_GENERIC, uint64(0)); + bytes memory pmData = _buildPaymasterData(bytes32(0), SUBJECT_TYPE_POA_ONBOARDING, bytes32(0), RULE_ID_GENERIC); // Build execute(registry, 0, someOtherFunction("data")) bytes memory innerData = abi.encodeWithSelector(bytes4(0xdeadbeef), "alice"); bytes memory execCallData = abi.encodeWithSelector(bytes4(0xb61d27f6), registry, uint256(0), innerData); @@ -1529,8 +1517,7 @@ contract PaymasterHubSolidarityTest is Test { vm.prank(poaManager); hub.setOnboardingConfig(uint128(MAX_COST), 10, true, registry); address deployed = address(new DummySender()); - bytes memory pmData = - _buildPaymasterData(bytes32(0), SUBJECT_TYPE_POA_ONBOARDING, bytes32(0), RULE_ID_GENERIC, uint64(0)); + bytes memory pmData = _buildPaymasterData(bytes32(0), SUBJECT_TYPE_POA_ONBOARDING, bytes32(0), RULE_ID_GENERIC); // Build arbitrary callData (not execute()) bytes memory badCallData = abi.encodeWithSelector(bytes4(0x12345678), address(0), uint256(0)); PackedUserOperation memory userOp = _buildUserOp(deployed, badCallData, pmData); @@ -1560,9 +1547,8 @@ contract PaymasterHubSolidarityTest is Test { bytes memory innerCall = abi.encodeWithSelector( bytes4(0xb61d27f6), address(0x9999), uint256(0), abi.encodeWithSelector(bytes4(0xdeadbeef)) ); - bytes memory pmData = _buildPaymasterData( - ORG_ALPHA, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(user1))), RULE_ID_GENERIC, uint64(0) - ); + bytes memory pmData = + _buildPaymasterData(ORG_ALPHA, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(user1))), RULE_ID_GENERIC); PackedUserOperation memory userOp = _buildUserOp(user1, innerCall, pmData); vm.prank(address(entryPoint)); vm.expectRevert(PaymasterHub.InsufficientFunds.selector);