diff --git a/src/PaymasterHub.sol b/src/PaymasterHub.sol index da0e926..c74b7b8 100644 --- a/src/PaymasterHub.sol +++ b/src/PaymasterHub.sol @@ -11,6 +11,10 @@ import { ReentrancyGuardUpgradeable } from "lib/openzeppelin-contracts-upgradeable/contracts/utils/ReentrancyGuardUpgradeable.sol"; import {IERC165} from "lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol"; +import {PaymasterHubErrors} from "./libs/PaymasterHubErrors.sol"; +import {PaymasterGraceLib} from "./libs/PaymasterGraceLib.sol"; +import {PaymasterPostOpLib} from "./libs/PaymasterPostOpLib.sol"; +import {PaymasterCalldataLib} from "./libs/PaymasterCalldataLib.sol"; /** * @title PaymasterHub @@ -23,46 +27,6 @@ import {IERC165} from "lib/openzeppelin-contracts/contracts/utils/introspection/ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyGuardUpgradeable, IERC165 { using UserOpLib for bytes32; - // ============ Custom Errors ============ - error EPOnly(); - error Paused(); - error NotAdmin(); - error NotOperator(); - error NotPoaManager(); - error RuleDenied(address target, bytes4 selector); - error FeeTooHigh(); - error GasTooHigh(); - error Ineligible(); - error BudgetExceeded(); - error InvalidRuleId(); - error PaymentFailed(); - error InvalidSubjectType(); - error InvalidVersion(); - error InvalidPaymasterData(); - error ZeroAddress(); - error InvalidEpochLength(); - error ContractNotDeployed(); - error ArrayLengthMismatch(); - error OrgNotRegistered(); - error OrgAlreadyRegistered(); - error GracePeriodSpendLimitReached(); - error InsufficientDepositForSolidarity(); - error SolidarityLimitExceeded(); - error InsufficientOrgBalance(); - error OrgIsBanned(); - error InsufficientFunds(); - error SolidarityDistributionIsPaused(); - error OnboardingDisabled(); - error OnboardingDailyLimitExceeded(); - error Overflow(); - error InvalidOnboardingRequest(); - error OrgDeployDisabled(); - error OrgDeployLimitExceeded(); - error OrgDeployDailyLimitExceeded(); - error InvalidOrgDeployRequest(); - error InvalidOrgId(); - error ZeroAmount(); - // ============ Constants ============ uint8 private constant PAYMASTER_DATA_VERSION = 1; uint8 private constant SUBJECT_TYPE_ACCOUNT = 0x00; @@ -76,49 +40,10 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG uint8 private constant SPONSORSHIP_ORG_DEPLOY = 2; uint32 private constant RULE_ID_GENERIC = 0x00000000; - uint32 private constant RULE_ID_EXECUTOR = 0x00000001; uint32 private constant RULE_ID_COARSE = 0x000000FF; uint32 private constant MIN_EPOCH_LENGTH = 1 hours; uint32 private constant MAX_EPOCH_LENGTH = 365 days; - // ============ Events ============ - event PaymasterInitialized(address indexed entryPoint, address indexed hats, address indexed poaManager); - event OrgRegistered(bytes32 indexed orgId, uint256 adminHatId, uint256 operatorHatId); - event RuleSet( - bytes32 indexed orgId, address indexed target, bytes4 indexed selector, bool allowed, uint32 maxCallGasHint - ); - event BudgetSet(bytes32 indexed orgId, bytes32 subjectKey, uint128 capPerEpoch, uint32 epochLen, uint32 epochStart); - event FeeCapsSet( - bytes32 indexed orgId, - uint256 maxFeePerGas, - uint256 maxPriorityFeePerGas, - uint32 maxCallGas, - uint32 maxVerificationGas, - uint32 maxPreVerificationGas - ); - event PauseSet(bytes32 indexed orgId, bool paused); - event OperatorHatSet(bytes32 indexed orgId, uint256 operatorHatId); - event DepositIncrease(uint256 amount, uint256 newDeposit); - event DepositWithdraw(address indexed to, uint256 amount); - event UsageIncreased( - bytes32 indexed orgId, bytes32 subjectKey, uint256 delta, uint128 usedInEpoch, uint32 epochStart - ); - event EmergencyWithdraw(address indexed to, uint256 amount); - event OrgDepositReceived(bytes32 indexed orgId, address indexed from, uint256 amount); - event SolidarityFeeCollected(bytes32 indexed orgId, uint256 amount); - event SolidarityDonationReceived(address indexed from, uint256 amount); - event GracePeriodConfigUpdated(uint32 initialGraceDays, uint128 maxSpendDuringGrace, uint128 minDepositRequired); - event OrgBannedFromSolidarity(bytes32 indexed orgId, bool banned); - event OnboardingConfigUpdated( - uint128 maxGasPerCreation, uint128 dailyCreationLimit, bool enabled, address accountRegistry - ); - event OnboardingAccountCreated(address indexed account, uint256 gasCost); - event OrgDeployConfigUpdated( - uint128 maxGasPerDeploy, uint128 dailyDeployLimit, uint8 maxDeploysPerAccount, bool enabled, address orgDeployer - ); - event OrgDeploymentSponsored(address indexed account, uint256 gasCost); - event SolidarityDistributionPaused(); - event SolidarityDistributionUnpaused(); // ============ Storage Variables ============ /// @custom:storage-location erc7201:poa.paymasterhub.main @@ -139,13 +64,11 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG * Storage optimization: registeredAt packed with paused to save gas */ struct OrgConfig { - uint256 adminHatId; // Slot 0 - uint256 operatorHatId; // Slot 1: Optional role for budget/rule management - uint256 __deprecated_voucherHatId; // Slot 2: Reserved for storage layout compatibility - bool paused; // Slot 3 (1 byte) - uint40 registeredAt; // Slot 3 (5 bytes): UNIX timestamp, good until year 36812 - bool bannedFromSolidarity; // Slot 3 (1 byte) - // 25 bytes remaining in slot 3 for future use + uint256 adminHatId; + uint256 operatorHatId; // Optional role for budget/rule management + bool paused; + uint40 registeredAt; // UNIX timestamp, good until year 36812 + bool bannedFromSolidarity; } /** @@ -153,11 +76,9 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG */ struct OrgFinancials { uint128 deposited; // Current balance deposited by org - uint128 totalDeposited; // Cumulative lifetime deposits (never decreases) uint128 spent; // Total spent from org's own deposits uint128 solidarityUsedThisPeriod; // Solidarity used in current 90-day period uint32 periodStart; // Timestamp when current 90-day period started - uint224 reserved; // Padding for future use } /** @@ -168,7 +89,6 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG uint32 numActiveOrgs; // Number of orgs with deposits > 0 uint16 feePercentageBps; // Fee as basis points (100 = 1%) bool distributionPaused; // When true, only collect fees, no payouts - uint200 reserved; // Padding } /** @@ -262,16 +182,16 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG * @param _poaManager PoaManager address for upgrade authorization */ function initialize(address _entryPoint, address _hats, address _poaManager) public initializer { - if (_entryPoint == address(0)) revert ZeroAddress(); - if (_hats == address(0)) revert ZeroAddress(); - if (_poaManager == address(0)) revert ZeroAddress(); + if (_entryPoint == address(0)) revert PaymasterHubErrors.ZeroAddress(); + if (_hats == address(0)) revert PaymasterHubErrors.ZeroAddress(); + if (_poaManager == address(0)) revert PaymasterHubErrors.ZeroAddress(); // Verify entryPoint is a contract uint256 codeSize; assembly { codeSize := extcodesize(_entryPoint) } - if (codeSize == 0) revert ContractNotDeployed(); + if (codeSize == 0) revert PaymasterHubErrors.ContractNotDeployed(); // Initialize upgradeable contracts __ReentrancyGuard_init(); @@ -309,7 +229,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG orgDeploy.maxDeploysPerAccount = 2; // 2 free deployments per account lifetime orgDeploy.enabled = false; // Requires explicit setOrgDeployConfig to activate - emit PaymasterInitialized(_entryPoint, _hats, _poaManager); + emit PaymasterHubErrors.PaymasterInitialized(_entryPoint, _hats, _poaManager); } // ============ Deploy Config Struct ============ @@ -374,7 +294,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG feeCaps.maxVerificationGas = config.maxVerificationGas; feeCaps.maxPreVerificationGas = config.maxPreVerificationGas; - emit FeeCapsSet( + emit PaymasterHubErrors.FeeCapsSet( orgId, config.maxFeePerGas, config.maxPriorityFeePerGas, @@ -386,41 +306,16 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG // Set rules if arrays provided if (config.ruleTargets.length > 0) { - uint256 length = config.ruleTargets.length; - if ( - length != config.ruleSelectors.length || length != config.ruleAllowed.length - || length != config.ruleMaxCallGasHints.length - ) { - revert ArrayLengthMismatch(); - } - - mapping(address => mapping(bytes4 => Rule)) storage rules = _getRulesStorage()[orgId]; - - for (uint256 i; i < length;) { - if (config.ruleTargets[i] == address(0)) revert ZeroAddress(); - - rules[config.ruleTargets[i]][config.ruleSelectors[i]] = - Rule({allowed: config.ruleAllowed[i], maxCallGasHint: config.ruleMaxCallGasHints[i]}); - - emit RuleSet( - orgId, - config.ruleTargets[i], - config.ruleSelectors[i], - config.ruleAllowed[i], - config.ruleMaxCallGasHints[i] - ); - - unchecked { - ++i; - } - } + _setRulesBatch( + orgId, config.ruleTargets, config.ruleSelectors, config.ruleAllowed, config.ruleMaxCallGasHints + ); } // Set budgets if arrays provided if (config.budgetSubjectKeys.length > 0) { uint256 budgetLen = config.budgetSubjectKeys.length; if (budgetLen != config.budgetCapsPerEpoch.length || budgetLen != config.budgetEpochLens.length) { - revert ArrayLengthMismatch(); + revert PaymasterHubErrors.ArrayLengthMismatch(); } mapping(bytes32 => Budget) storage budgets = _getBudgetsStorage()[orgId]; @@ -428,7 +323,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG for (uint256 j; j < budgetLen;) { uint32 epochLen = config.budgetEpochLens[j]; if (epochLen < MIN_EPOCH_LENGTH || epochLen > MAX_EPOCH_LENGTH) { - revert InvalidEpochLength(); + revert PaymasterHubErrors.InvalidEpochLength(); } Budget storage budget = budgets[config.budgetSubjectKeys[j]]; @@ -436,7 +331,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG budget.epochLen = epochLen; budget.epochStart = uint32(block.timestamp); - emit BudgetSet( + emit PaymasterHubErrors.BudgetSet( orgId, config.budgetSubjectKeys[j], config.budgetCapsPerEpoch[j], epochLen, uint32(block.timestamp) ); @@ -454,56 +349,57 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG function _onlyRegistrar() internal view { MainStorage storage main = _getMainStorage(); - if (msg.sender != main.poaManager && msg.sender != main.orgRegistrar) revert NotPoaManager(); + if (msg.sender != main.poaManager && msg.sender != main.orgRegistrar) { + revert PaymasterHubErrors.NotPoaManager(); + } } function _registerOrg(bytes32 orgId, uint256 adminHatId, uint256 operatorHatId) internal { - if (orgId == bytes32(0)) revert InvalidOrgId(); - if (adminHatId == 0) revert ZeroAddress(); + if (orgId == bytes32(0)) revert PaymasterHubErrors.InvalidOrgId(); + if (adminHatId == 0) revert PaymasterHubErrors.ZeroAddress(); mapping(bytes32 => OrgConfig) storage orgs = _getOrgsStorage(); - if (orgs[orgId].adminHatId != 0) revert OrgAlreadyRegistered(); + if (orgs[orgId].adminHatId != 0) revert PaymasterHubErrors.OrgAlreadyRegistered(); orgs[orgId] = OrgConfig({ adminHatId: adminHatId, operatorHatId: operatorHatId, - __deprecated_voucherHatId: 0, paused: false, registeredAt: uint40(block.timestamp), bannedFromSolidarity: false }); - emit OrgRegistered(orgId, adminHatId, operatorHatId); + emit PaymasterHubErrors.OrgRegistered(orgId, adminHatId, operatorHatId); } // ============ Modifiers ============ modifier onlyEntryPoint() { - if (msg.sender != _getMainStorage().entryPoint) revert EPOnly(); + if (msg.sender != _getMainStorage().entryPoint) revert PaymasterHubErrors.EPOnly(); _; } modifier onlyOrgAdmin(bytes32 orgId) { OrgConfig storage org = _getOrgsStorage()[orgId]; - if (org.adminHatId == 0) revert OrgNotRegistered(); + if (org.adminHatId == 0) revert PaymasterHubErrors.OrgNotRegistered(); if (!IHats(_getMainStorage().hats).isWearerOfHat(msg.sender, org.adminHatId)) { - revert NotAdmin(); + revert PaymasterHubErrors.NotAdmin(); } _; } modifier onlyOrgOperator(bytes32 orgId) { OrgConfig storage org = _getOrgsStorage()[orgId]; - if (org.adminHatId == 0) revert OrgNotRegistered(); + if (org.adminHatId == 0) revert PaymasterHubErrors.OrgNotRegistered(); bool isAdmin = IHats(_getMainStorage().hats).isWearerOfHat(msg.sender, org.adminHatId); bool isOperator = org.operatorHatId != 0 && IHats(_getMainStorage().hats).isWearerOfHat(msg.sender, org.operatorHatId); - if (!isAdmin && !isOperator) revert NotOperator(); + if (!isAdmin && !isOperator) revert PaymasterHubErrors.NotOperator(); _; } modifier whenOrgNotPaused(bytes32 orgId) { - if (_getOrgsStorage()[orgId].paused) revert Paused(); + if (_getOrgsStorage()[orgId].paused) revert PaymasterHubErrors.Paused(); _; } @@ -546,42 +442,41 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG uint256 depositAvailable = org.deposited > org.spent ? org.deposited - org.spent : 0; // Check if org is banned from solidarity - if (config.bannedFromSolidarity) revert OrgIsBanned(); + if (config.bannedFromSolidarity) revert PaymasterHubErrors.OrgIsBanned(); - // Calculate grace period end time - uint256 graceEndTime = config.registeredAt + (uint256(grace.initialGraceDays) * 1 days); - bool inInitialGrace = block.timestamp < graceEndTime; + // Check grace period + bool inInitialGrace = PaymasterGraceLib.isInGracePeriod(config.registeredAt, grace.initialGraceDays); if (inInitialGrace) { // Startup phase: can use solidarity even with zero deposits // Enforce spending limit only (configured to represent ~3000 tx worth of value) if (org.solidarityUsedThisPeriod + maxCost > grace.maxSpendDuringGrace) { - revert GracePeriodSpendLimitReached(); + revert PaymasterHubErrors.GracePeriodSpendLimitReached(); } // In initial grace period, operations are paid 100% from solidarity. - if (solidarity.balance < maxCost) revert InsufficientFunds(); + if (solidarity.balance < maxCost) revert PaymasterHubErrors.InsufficientFunds(); } else { // After startup: must MAINTAIN minimum deposit (like $10/month commitment) - // This checks deposited (current balance), not totalDeposited (cumulative) // Orgs must keep funds in reserve to access solidarity if (depositAvailable < grace.minDepositRequired) { - revert InsufficientDepositForSolidarity(); + revert PaymasterHubErrors.InsufficientDepositForSolidarity(); } // Check against tier-based allowance (calculated in payment logic) // Tier 1: deposit 0.003 ETH → 0.006 ETH match → 0.009 ETH total per 90 days // Tier 2: deposit 0.006 ETH → 0.009 ETH match → 0.015 ETH total per 90 days // Tier 3: deposit >= 0.017 ETH → no match, self-funded - uint256 matchAllowance = _calculateMatchAllowance(depositAvailable, grace.minDepositRequired); + uint256 matchAllowance = + PaymasterGraceLib.calculateMatchAllowance(depositAvailable, grace.minDepositRequired); uint256 solidarityRemaining = matchAllowance > org.solidarityUsedThisPeriod ? matchAllowance - org.solidarityUsedThisPeriod : 0; // Minimum solidarity needed after using available deposits. uint256 requiredSolidarity = maxCost > depositAvailable ? maxCost - depositAvailable : 0; if (requiredSolidarity > solidarityRemaining) { - revert SolidarityLimitExceeded(); + revert PaymasterHubErrors.SolidarityLimitExceeded(); } - if (solidarity.balance < requiredSolidarity) revert InsufficientFunds(); + if (solidarity.balance < requiredSolidarity) revert PaymasterHubErrors.InsufficientFunds(); } } @@ -596,10 +491,10 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG * @param orgId The organization to deposit for */ function depositForOrg(bytes32 orgId) external payable { - if (msg.value == 0) revert ZeroAmount(); + if (msg.value == 0) revert PaymasterHubErrors.ZeroAmount(); // Verify org exists - if (_getOrgsStorage()[orgId].adminHatId == 0) revert OrgNotRegistered(); + if (_getOrgsStorage()[orgId].adminHatId == 0) revert PaymasterHubErrors.OrgNotRegistered(); _depositForOrg(orgId, msg.value); } @@ -634,11 +529,10 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG bool wasUnfunded = org.deposited == 0; // Safe cast check - if (amount > type(uint128).max) revert Overflow(); + if (amount > type(uint128).max) revert PaymasterHubErrors.Overflow(); // Update org financials org.deposited += uint128(amount); - org.totalDeposited += uint128(amount); // Reset period when triggered if (shouldResetPeriod) { @@ -658,7 +552,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG // Deposit to EntryPoint IEntryPoint(_getMainStorage().entryPoint).depositTo{value: amount}(address(this)); - emit OrgDepositReceived(orgId, msg.sender, amount); + emit PaymasterHubErrors.OrgDepositReceived(orgId, msg.sender, amount); } /** @@ -666,8 +560,8 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG * @dev Anyone can donate to support all orgs */ function donateToSolidarity() external payable { - if (msg.value == 0) revert ZeroAmount(); - if (msg.value > type(uint128).max) revert Overflow(); + if (msg.value == 0) revert PaymasterHubErrors.ZeroAmount(); + if (msg.value > type(uint128).max) revert PaymasterHubErrors.Overflow(); SolidarityFund storage solidarity = _getSolidarityStorage(); solidarity.balance += uint128(msg.value); @@ -675,7 +569,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG // Deposit to EntryPoint IEntryPoint(_getMainStorage().entryPoint).depositTo{value: msg.value}(address(this)); - emit SolidarityDonationReceived(msg.sender, msg.value); + emit PaymasterHubErrors.SolidarityDonationReceived(msg.sender, msg.value); } // ============ ERC-4337 Paymaster Functions ============ @@ -699,7 +593,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG (uint8 version, bytes32 orgId, uint8 subjectType, bytes32 subjectId, uint32 ruleId) = _decodePaymasterData(userOp.paymasterAndData); - if (version != PAYMASTER_DATA_VERSION) revert InvalidVersion(); + if (version != PAYMASTER_DATA_VERSION) revert PaymasterHubErrors.InvalidVersion(); bytes32 subjectKey; uint32 currentEpochStart; @@ -713,7 +607,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG sponsorshipType = SPONSORSHIP_ONBOARDING; // Global-only onboarding path: never org-scoped billing. if (orgId != bytes32(0) || subjectId != bytes32(0) || ruleId != RULE_ID_GENERIC) { - revert InvalidOnboardingRequest(); + revert PaymasterHubErrors.InvalidOnboardingRequest(); } // Validate POA onboarding eligibility @@ -727,7 +621,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG sponsorshipType = SPONSORSHIP_ORG_DEPLOY; // Global-only org deploy path: never org-scoped billing. if (orgId != bytes32(0) || subjectId != bytes32(0) || ruleId != RULE_ID_GENERIC) { - revert InvalidOrgDeployRequest(); + revert PaymasterHubErrors.InvalidOrgDeployRequest(); } // Validate free org deployment eligibility @@ -737,8 +631,8 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG } else { // Validate org is registered and not paused OrgConfig storage org = _getOrgsStorage()[orgId]; - if (org.adminHatId == 0) revert OrgNotRegistered(); - if (org.paused) revert Paused(); + if (org.adminHatId == 0) revert PaymasterHubErrors.OrgNotRegistered(); + if (org.paused) revert PaymasterHubErrors.Paused(); // Validate subject eligibility subjectKey = _validateSubjectEligibility(userOp.sender, subjectType, subjectId); @@ -793,7 +687,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG if (solidarity.distributionPaused) { // When distribution is paused, org must cover 100% from deposits if (depositAvailable < maxCost) { - revert InsufficientOrgBalance(); + revert PaymasterHubErrors.InsufficientOrgBalance(); } org.spent += uint128(maxCost); // Reserve for bundle safety return maxCost; @@ -801,14 +695,12 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG // Deposits are fully exhausted — check if grace period allows solidarity-only coverage if (depositAvailable == 0) { - mapping(bytes32 => OrgConfig) storage orgs = _getOrgsStorage(); + OrgConfig storage config = _getOrgsStorage()[orgId]; GracePeriodConfig storage grace = _getGracePeriodStorage(); - OrgConfig storage config = orgs[orgId]; - uint256 graceEndTime = config.registeredAt + (uint256(grace.initialGraceDays) * 1 days); - if (block.timestamp < graceEndTime) { + if (PaymasterGraceLib.isInGracePeriod(config.registeredAt, grace.initialGraceDays)) { return 0; // Grace period with zero deposits: no reservation needed } - revert InsufficientOrgBalance(); + revert PaymasterHubErrors.InsufficientOrgBalance(); } // Deposits cover fully or partially — solidarity covers the rest @@ -893,8 +785,8 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG mapping(bytes32 => Budget) storage budgets = _getBudgetsStorage()[orgId]; Budget storage budget = budgets[subjectKey]; if (budget.epochStart == epochStart) { - budget.usedInEpoch = budget.usedInEpoch - uint128(reservedBudget) + uint128(actualGasCost); - emit UsageIncreased(orgId, subjectKey, actualGasCost, budget.usedInEpoch, epochStart); + budget.usedInEpoch = PaymasterPostOpLib.adjustBudget(budget.usedInEpoch, reservedBudget, actualGasCost); + emit PaymasterHubErrors.UsageIncreased(orgId, subjectKey, actualGasCost, budget.usedInEpoch, epochStart); } // Unreserve org balance, then charge actual + fee from deposits only (no solidarity) @@ -907,15 +799,10 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG // Zero the fee during grace — same as _updateOrgFinancials. // Grace orgs have no deposits; charging a fee would create phantom debt // and circular solidarity accounting (solidarity pays itself). - uint256 solidarityFee; - mapping(bytes32 => OrgConfig) storage orgs = _getOrgsStorage(); GracePeriodConfig storage grace = _getGracePeriodStorage(); - uint256 graceEndTime = orgs[orgId].registeredAt + (uint256(grace.initialGraceDays) * 1 days); - if (block.timestamp < graceEndTime) { - solidarityFee = 0; - } else { - solidarityFee = (actualGasCost * uint256(solidarity.feePercentageBps)) / 10000; - } + uint256 solidarityFee = PaymasterGraceLib.solidarityFee( + actualGasCost, solidarity.feePercentageBps, _getOrgsStorage()[orgId].registeredAt, grace.initialGraceDays + ); org.spent += uint128(actualGasCost + solidarityFee); solidarity.balance += uint128(solidarityFee); @@ -926,7 +813,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG org.solidarityUsedThisPeriod += uint128(actualGasCost); if (solidarityFee > 0) { - emit SolidarityFeeCollected(orgId, solidarityFee); + emit PaymasterHubErrors.SolidarityFeeCollected(orgId, solidarityFee); } } @@ -946,8 +833,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG function _sponsorshipPostOpFallback(uint8 sponsorshipType, address sender, uint256 actualGasCost) private { // Deduct from solidarity (clamped to available balance — never revert) SolidarityFund storage solidarity = _getSolidarityStorage(); - uint128 deduction = solidarity.balance < uint128(actualGasCost) ? solidarity.balance : uint128(actualGasCost); - solidarity.balance -= deduction; + (solidarity.balance,) = PaymasterPostOpLib.clampedDeduction(solidarity.balance, actualGasCost); // Refund optimistic counters. The first postOp's refunds were rolled back by EntryPoint, // so counters are still at their validation-incremented values. @@ -969,42 +855,6 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG } } - /** - * @notice Calculate solidarity match allowance based on deposit tier - * @dev Progressive tier system with declining marginal match rates - * - * Tier 1: deposit = 1x min → match = 2x → total budget = 3x (e.g. 0.003 ETH → 0.009 ETH) - * Tier 2: deposit = 2x min → match = 3x → total budget = 5x (e.g. 0.006 ETH → 0.015 ETH) - * Tier 3: deposit >= 5x min → no match, self-funded - * - * @param deposited Current deposit balance - * @param minDeposit Minimum deposit requirement (from grace config) - * @return matchAllowance How much solidarity can be used per 90-day period - */ - function _calculateMatchAllowance(uint256 deposited, uint256 minDeposit) internal pure returns (uint256) { - if (minDeposit == 0 || deposited < minDeposit) { - return 0; - } - // Tier 1: deposit <= 1x minimum -> 2x match - if (deposited <= minDeposit) { - return deposited * 2; - } - // Tier 2: deposit <= 2x minimum -> first tier at 2x, remainder at 1x - if (deposited <= minDeposit * 2) { - uint256 firstTierMatch = minDeposit * 2; - uint256 secondTierMatch = deposited - minDeposit; - return firstTierMatch + secondTierMatch; - } - // Tier 3: deposit < 5x minimum -> capped match from first two tiers - if (deposited < minDeposit * 5) { - uint256 firstTierMatch = minDeposit * 2; - uint256 secondTierMatch = minDeposit; - return firstTierMatch + secondTierMatch; - } - // Tier 4: >= 5x minimum -> self-sufficient, no match - return 0; - } - /** * @notice Update org's financial tracking and collect 1% solidarity fee * @dev Called in postOp after actual gas cost is known @@ -1036,13 +886,12 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG if (solidarity.distributionPaused) { org.spent += uint128(actualGasCost + solidarityFee); solidarity.balance += uint128(solidarityFee); - emit SolidarityFeeCollected(orgId, solidarityFee); + emit PaymasterHubErrors.SolidarityFeeCollected(orgId, solidarityFee); return; } // Check if in initial grace period - uint256 graceEndTime = config.registeredAt + (uint256(grace.initialGraceDays) * 1 days); - bool inInitialGrace = block.timestamp < graceEndTime; + bool inInitialGrace = PaymasterGraceLib.isInGracePeriod(config.registeredAt, grace.initialGraceDays); // Determine how much comes from org's deposits vs solidarity uint256 fromDeposits = 0; @@ -1051,7 +900,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG if (inInitialGrace) { // Grace period: 100% from solidarity (deposits untouched) - if (solidarityLiquidity < actualGasCost) revert InsufficientFunds(); + if (solidarityLiquidity < actualGasCost) revert PaymasterHubErrors.InsufficientFunds(); fromSolidarity = actualGasCost; // No fee during grace — would be circular (solidarity pays itself) solidarityFee = 0; @@ -1061,7 +910,8 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG uint256 depositAvailable = org.deposited > org.spent ? org.deposited - org.spent : 0; // Match allowance based on CURRENT BALANCE, not lifetime deposits - uint256 matchAllowance = _calculateMatchAllowance(depositAvailable, grace.minDepositRequired); + uint256 matchAllowance = + PaymasterGraceLib.calculateMatchAllowance(depositAvailable, grace.minDepositRequired); uint256 solidarityRemaining = matchAllowance > org.solidarityUsedThisPeriod ? matchAllowance - org.solidarityUsedThisPeriod : 0; if (solidarityRemaining > solidarityLiquidity) { @@ -1099,7 +949,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG // If still can't cover, revert if (shortfall > 0) { - revert InsufficientFunds(); + revert PaymasterHubErrors.InsufficientFunds(); } } } @@ -1112,7 +962,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG solidarity.balance -= uint128(fromSolidarity); solidarity.balance += uint128(solidarityFee); - emit SolidarityFeeCollected(orgId, solidarityFee); + emit PaymasterHubErrors.SolidarityFeeCollected(orgId, solidarityFee); } // ============ Admin Functions ============ @@ -1125,11 +975,11 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG external onlyOrgOperator(orgId) { - if (target == address(0)) revert ZeroAddress(); + if (target == address(0)) revert PaymasterHubErrors.ZeroAddress(); mapping(address => mapping(bytes4 => Rule)) storage rules = _getRulesStorage()[orgId]; rules[target][selector] = Rule({allowed: allowed, maxCallGasHint: maxCallGasHint}); - emit RuleSet(orgId, target, selector, allowed, maxCallGasHint); + emit PaymasterHubErrors.RuleSet(orgId, target, selector, allowed, maxCallGasHint); } /** @@ -1142,19 +992,29 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG bool[] calldata allowed, uint32[] calldata maxCallGasHints ) external onlyOrgOperator(orgId) { + _setRulesBatch(orgId, targets, selectors, allowed, maxCallGasHints); + } + + function _setRulesBatch( + bytes32 orgId, + address[] calldata targets, + bytes4[] calldata selectors, + bool[] calldata allowed, + uint32[] calldata maxCallGasHints + ) internal { uint256 length = targets.length; if (length != selectors.length || length != allowed.length || length != maxCallGasHints.length) { - revert ArrayLengthMismatch(); + revert PaymasterHubErrors.ArrayLengthMismatch(); } mapping(address => mapping(bytes4 => Rule)) storage rules = _getRulesStorage()[orgId]; for (uint256 i; i < length;) { - if (targets[i] == address(0)) revert ZeroAddress(); + if (targets[i] == address(0)) revert PaymasterHubErrors.ZeroAddress(); rules[targets[i]][selectors[i]] = Rule({allowed: allowed[i], maxCallGasHint: maxCallGasHints[i]}); - emit RuleSet(orgId, targets[i], selectors[i], allowed[i], maxCallGasHints[i]); + emit PaymasterHubErrors.RuleSet(orgId, targets[i], selectors[i], allowed[i], maxCallGasHints[i]); unchecked { ++i; @@ -1168,7 +1028,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG function clearRule(bytes32 orgId, address target, bytes4 selector) external onlyOrgOperator(orgId) { mapping(address => mapping(bytes4 => Rule)) storage rules = _getRulesStorage()[orgId]; delete rules[target][selector]; - emit RuleSet(orgId, target, selector, false, 0); + emit PaymasterHubErrors.RuleSet(orgId, target, selector, false, 0); } /** @@ -1180,7 +1040,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG onlyOrgOperator(orgId) { if (epochLen < MIN_EPOCH_LENGTH || epochLen > MAX_EPOCH_LENGTH) { - revert InvalidEpochLength(); + revert PaymasterHubErrors.InvalidEpochLength(); } mapping(bytes32 => Budget) storage budgets = _getBudgetsStorage()[orgId]; @@ -1199,7 +1059,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG budget.epochStart = uint32(block.timestamp); } - emit BudgetSet(orgId, subjectKey, capPerEpoch, epochLen, budget.epochStart); + emit PaymasterHubErrors.BudgetSet(orgId, subjectKey, capPerEpoch, epochLen, budget.epochStart); } /** @@ -1212,7 +1072,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG budget.epochStart = epochStart; budget.usedInEpoch = 0; // Reset usage when manually setting epoch - emit BudgetSet(orgId, subjectKey, budget.capPerEpoch, budget.epochLen, epochStart); + emit PaymasterHubErrors.BudgetSet(orgId, subjectKey, budget.capPerEpoch, budget.epochLen, epochStart); } /** @@ -1234,7 +1094,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG feeCaps.maxVerificationGas = maxVerificationGas; feeCaps.maxPreVerificationGas = maxPreVerificationGas; - emit FeeCapsSet( + emit PaymasterHubErrors.FeeCapsSet( orgId, maxFeePerGas, maxPriorityFeePerGas, maxCallGas, maxVerificationGas, maxPreVerificationGas ); } @@ -1245,7 +1105,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG */ function setPause(bytes32 orgId, bool paused) external onlyOrgAdmin(orgId) { _getOrgsStorage()[orgId].paused = paused; - emit PauseSet(orgId, paused); + emit PaymasterHubErrors.PauseSet(orgId, paused); } /** @@ -1253,7 +1113,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG */ function setOperatorHat(bytes32 orgId, uint256 operatorHatId) external onlyOrgAdmin(orgId) { _getOrgsStorage()[orgId].operatorHatId = operatorHatId; - emit OperatorHatSet(orgId, operatorHatId); + emit PaymasterHubErrors.OperatorHatSet(orgId, operatorHatId); } /** @@ -1265,26 +1125,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG IEntryPoint(entryPoint).depositTo{value: msg.value}(address(this)); uint256 newDeposit = IEntryPoint(entryPoint).balanceOf(address(this)); - emit DepositIncrease(msg.value, newDeposit); - } - - /** - * @notice Withdraw funds from EntryPoint deposit (requires global admin) - * @dev Withdrawals affect shared pool, so restricted to prevent abuse - */ - function withdrawFromEntryPoint(address payable to, uint256 amount) external { - // TODO: Add global admin mechanism or require multi-org consensus - // For now, disabled to protect shared pool - revert NotAdmin(); - } - - /** - * @notice Emergency withdrawal in case of critical issues - * @dev Requires global admin - affects all orgs - */ - function emergencyWithdraw(address payable to) external { - // TODO: Add global admin mechanism - revert NotAdmin(); + emit PaymasterHubErrors.DepositIncrease(msg.value, newDeposit); } /** @@ -1297,17 +1138,17 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG function setGracePeriodConfig(uint32 _initialGraceDays, uint128 _maxSpendDuringGrace, uint128 _minDepositRequired) external { - if (msg.sender != _getMainStorage().poaManager) revert NotPoaManager(); - if (_initialGraceDays == 0) revert InvalidEpochLength(); - if (_maxSpendDuringGrace == 0) revert InvalidEpochLength(); - if (_minDepositRequired == 0) revert InvalidEpochLength(); + if (msg.sender != _getMainStorage().poaManager) revert PaymasterHubErrors.NotPoaManager(); + if (_initialGraceDays == 0) revert PaymasterHubErrors.InvalidEpochLength(); + if (_maxSpendDuringGrace == 0) revert PaymasterHubErrors.InvalidEpochLength(); + if (_minDepositRequired == 0) revert PaymasterHubErrors.InvalidEpochLength(); GracePeriodConfig storage grace = _getGracePeriodStorage(); grace.initialGraceDays = _initialGraceDays; grace.maxSpendDuringGrace = _maxSpendDuringGrace; grace.minDepositRequired = _minDepositRequired; - emit GracePeriodConfigUpdated(_initialGraceDays, _maxSpendDuringGrace, _minDepositRequired); + emit PaymasterHubErrors.GracePeriodConfigUpdated(_initialGraceDays, _maxSpendDuringGrace, _minDepositRequired); } /** @@ -1316,7 +1157,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG * @param registrar Address authorized to call registerOrg */ function setOrgRegistrar(address registrar) external { - if (msg.sender != _getMainStorage().poaManager) revert NotPoaManager(); + if (msg.sender != _getMainStorage().poaManager) revert PaymasterHubErrors.NotPoaManager(); _getMainStorage().orgRegistrar = registrar; } @@ -1327,14 +1168,14 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG * @param banned True to ban, false to unban */ function setBanFromSolidarity(bytes32 orgId, bool banned) external { - if (msg.sender != _getMainStorage().poaManager) revert NotPoaManager(); + if (msg.sender != _getMainStorage().poaManager) revert PaymasterHubErrors.NotPoaManager(); mapping(bytes32 => OrgConfig) storage orgs = _getOrgsStorage(); - if (orgs[orgId].adminHatId == 0) revert OrgNotRegistered(); + if (orgs[orgId].adminHatId == 0) revert PaymasterHubErrors.OrgNotRegistered(); orgs[orgId].bannedFromSolidarity = banned; - emit OrgBannedFromSolidarity(orgId, banned); + emit PaymasterHubErrors.OrgBannedFromSolidarity(orgId, banned); } /** @@ -1343,8 +1184,8 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG * @param feePercentageBps Fee as basis points (100 = 1%) */ function setSolidarityFee(uint16 feePercentageBps) external { - if (msg.sender != _getMainStorage().poaManager) revert NotPoaManager(); - if (feePercentageBps > 1000) revert FeeTooHigh(); // Cap at 10% + if (msg.sender != _getMainStorage().poaManager) revert PaymasterHubErrors.NotPoaManager(); + if (feePercentageBps > 1000) revert PaymasterHubErrors.FeeTooHigh(); // Cap at 10% SolidarityFund storage solidarity = _getSolidarityStorage(); solidarity.feePercentageBps = feePercentageBps; @@ -1357,11 +1198,11 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG * Only PoaManager can pause/unpause. */ function pauseSolidarityDistribution() external { - if (msg.sender != _getMainStorage().poaManager) revert NotPoaManager(); + if (msg.sender != _getMainStorage().poaManager) revert PaymasterHubErrors.NotPoaManager(); SolidarityFund storage solidarity = _getSolidarityStorage(); if (!solidarity.distributionPaused) { solidarity.distributionPaused = true; - emit SolidarityDistributionPaused(); + emit PaymasterHubErrors.SolidarityDistributionPaused(); } } @@ -1371,11 +1212,11 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG * Only PoaManager can pause/unpause. */ function unpauseSolidarityDistribution() external { - if (msg.sender != _getMainStorage().poaManager) revert NotPoaManager(); + if (msg.sender != _getMainStorage().poaManager) revert PaymasterHubErrors.NotPoaManager(); SolidarityFund storage solidarity = _getSolidarityStorage(); if (solidarity.distributionPaused) { solidarity.distributionPaused = false; - emit SolidarityDistributionUnpaused(); + emit PaymasterHubErrors.SolidarityDistributionUnpaused(); } } @@ -1393,7 +1234,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG bool _enabled, address _accountRegistry ) external { - if (msg.sender != _getMainStorage().poaManager) revert NotPoaManager(); + if (msg.sender != _getMainStorage().poaManager) revert PaymasterHubErrors.NotPoaManager(); OnboardingConfig storage onboarding = _getOnboardingStorage(); onboarding.maxGasPerCreation = _maxGasPerCreation; @@ -1401,7 +1242,9 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG onboarding.enabled = _enabled; onboarding.accountRegistry = _accountRegistry; - emit OnboardingConfigUpdated(_maxGasPerCreation, _dailyCreationLimit, _enabled, _accountRegistry); + emit PaymasterHubErrors.OnboardingConfigUpdated( + _maxGasPerCreation, _dailyCreationLimit, _enabled, _accountRegistry + ); } /** @@ -1420,7 +1263,9 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG bool _enabled, address _orgDeployer ) external { - if (msg.sender != _getMainStorage().poaManager) revert NotPoaManager(); + if (msg.sender != _getMainStorage().poaManager) { + revert PaymasterHubErrors.NotPoaManager(); + } OrgDeployConfig storage deployConfig = _getOrgDeployStorage(); deployConfig.maxGasPerDeploy = _maxGasPerDeploy; @@ -1429,7 +1274,9 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG deployConfig.enabled = _enabled; deployConfig.orgDeployer = _orgDeployer; - emit OrgDeployConfigUpdated(_maxGasPerDeploy, _dailyDeployLimit, _maxDeploysPerAccount, _enabled, _orgDeployer); + emit PaymasterHubErrors.OrgDeployConfigUpdated( + _maxGasPerDeploy, _dailyDeployLimit, _maxDeploysPerAccount, _enabled, _orgDeployer + ); } // ============ Storage Getters (for Lens) ============ @@ -1522,53 +1369,6 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG return _getOrgDeployCountsStorage()[account]; } - /** - * @notice Get org's grace period status and limits - * @param orgId The organization identifier - * @return inGrace True if in initial grace period - * @return spendRemaining Spending remaining during grace (0 if not in grace) - * @return requiresDeposit True if org needs to deposit to access solidarity - * @return solidarityLimit Current solidarity allocation for org (per 90-day period) - */ - function getOrgGraceStatus(bytes32 orgId) - external - view - returns (bool inGrace, uint128 spendRemaining, bool requiresDeposit, uint256 solidarityLimit) - { - mapping(bytes32 => OrgConfig) storage orgs = _getOrgsStorage(); - mapping(bytes32 => OrgFinancials) storage financials = _getFinancialsStorage(); - GracePeriodConfig storage grace = _getGracePeriodStorage(); - SolidarityFund storage solidarity = _getSolidarityStorage(); - - OrgConfig storage config = orgs[orgId]; - OrgFinancials storage org = financials[orgId]; - - uint256 graceEndTime = config.registeredAt + (uint256(grace.initialGraceDays) * 1 days); - inGrace = block.timestamp < graceEndTime; - - // When distribution is paused, no solidarity is available regardless of grace/tier - if (solidarity.distributionPaused) { - spendRemaining = 0; - requiresDeposit = true; - solidarityLimit = 0; - return (inGrace, spendRemaining, requiresDeposit, solidarityLimit); - } - - if (inGrace) { - // During grace: track spending limit - uint128 spendUsed = org.solidarityUsedThisPeriod; - spendRemaining = spendUsed < grace.maxSpendDuringGrace ? grace.maxSpendDuringGrace - spendUsed : 0; - requiresDeposit = false; - solidarityLimit = uint256(grace.maxSpendDuringGrace); - } else { - // After grace: check current balance (not cumulative deposits) - spendRemaining = 0; - uint256 depositAvailable = org.deposited > org.spent ? org.deposited - org.spent : 0; - requiresDeposit = depositAvailable < grace.minDepositRequired; - solidarityLimit = _calculateMatchAllowance(depositAvailable, grace.minDepositRequired); - } - } - // ============ Storage Accessors ============ function _getMainStorage() private pure returns (MainStorage storage $) { bytes32 slot = MAIN_STORAGE_LOCATION; @@ -1686,9 +1486,9 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG */ function _authorizeUpgrade(address newImplementation) internal override { MainStorage storage main = _getMainStorage(); - if (msg.sender != main.poaManager) revert NotPoaManager(); - if (newImplementation == address(0)) revert ZeroAddress(); - if (newImplementation.code.length == 0) revert ContractNotDeployed(); + if (msg.sender != main.poaManager) revert PaymasterHubErrors.NotPoaManager(); + if (newImplementation == address(0)) revert PaymasterHubErrors.ZeroAddress(); + if (newImplementation.code.length == 0) revert PaymasterHubErrors.ContractNotDeployed(); } // ============ Internal Functions ============ @@ -1701,7 +1501,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG // ERC-4337 v0.7 packed format: // [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(); + if (paymasterAndData.length < 122) revert PaymasterHubErrors.InvalidPaymasterData(); // Skip first 52 bytes (paymaster address + v0.7 gas limits) and decode the rest version = uint8(paymasterAndData[52]); @@ -1723,22 +1523,22 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG returns (bytes32 subjectKey) { if (subjectType == SUBJECT_TYPE_ACCOUNT) { - if (address(uint160(uint256(subjectId))) != sender) revert Ineligible(); + if (address(uint160(uint256(subjectId))) != sender) revert PaymasterHubErrors.Ineligible(); subjectKey = keccak256(abi.encodePacked(subjectType, subjectId)); } else if (subjectType == SUBJECT_TYPE_HAT) { uint256 hatId = uint256(subjectId); IHats hatsContract = IHats(_getMainStorage().hats); if (!hatsContract.isEligible(sender, hatId)) { - revert Ineligible(); + revert PaymasterHubErrors.Ineligible(); } // Ensure the hat itself is still active (toggle module check) (,,,,,,,, bool active) = hatsContract.viewHat(hatId); if (!active) { - revert Ineligible(); + revert PaymasterHubErrors.Ineligible(); } subjectKey = keccak256(abi.encodePacked(subjectType, subjectId)); } else { - revert InvalidSubjectType(); + revert PaymasterHubErrors.InvalidSubjectType(); } } @@ -1757,10 +1557,10 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG OnboardingConfig storage onboarding = _getOnboardingStorage(); // Check onboarding is enabled - if (!onboarding.enabled) revert OnboardingDisabled(); + if (!onboarding.enabled) revert PaymasterHubErrors.OnboardingDisabled(); // Onboarding must only sponsor account creation, not arbitrary operations. - if (userOp.initCode.length == 0) revert InvalidOnboardingRequest(); + if (userOp.initCode.length == 0) revert PaymasterHubErrors.InvalidOnboardingRequest(); // NOTE: We intentionally do NOT check `account.code.length != 0` here. // In ERC-4337 v0.7, the EntryPoint deploys the account via _createSenderIfNeeded() // BEFORE calling validatePaymasterUserOp(), so the account already has code by the @@ -1773,10 +1573,10 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG // Onboarding is paid from solidarity fund, so block when distribution is paused SolidarityFund storage solidarity = _getSolidarityStorage(); - if (solidarity.distributionPaused) revert SolidarityDistributionIsPaused(); + if (solidarity.distributionPaused) revert PaymasterHubErrors.SolidarityDistributionIsPaused(); // Check gas cost limit - if (maxCost > onboarding.maxGasPerCreation) revert GasTooHigh(); + if (maxCost > onboarding.maxGasPerCreation) revert PaymasterHubErrors.GasTooHigh(); // Check daily rate limit uint32 today = uint32(block.timestamp / 1 days); @@ -1785,49 +1585,24 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG onboarding.attemptsToday = 0; } if (onboarding.attemptsToday >= onboarding.dailyCreationLimit) { - revert OnboardingDailyLimitExceeded(); + revert PaymasterHubErrors.OnboardingDailyLimitExceeded(); } onboarding.attemptsToday++; // Check solidarity fund has sufficient balance - if (solidarity.balance < maxCost) revert InsufficientFunds(); + if (solidarity.balance < maxCost) revert PaymasterHubErrors.InsufficientFunds(); // Subject key for onboarding is based on the account address (natural nonce) subjectKey = keccak256(abi.encodePacked(SUBJECT_TYPE_POA_ONBOARDING, bytes20(account))); } /// @dev Validates that onboarding callData is execute(registryAddress, 0, registerAccount(...)). - /// Reuses the same ABI layout parsing as _extractTargetSelector. function _validateOnboardingCallData(bytes calldata callData, address registry) private pure { - if (registry == address(0)) revert InvalidOnboardingRequest(); - if (callData.length < 4) revert InvalidOnboardingRequest(); - - // Must be execute(address,uint256,bytes) = 0xb61d27f6 - bytes4 outerSelector = bytes4(callData[0:4]); - if (outerSelector != bytes4(0xb61d27f6)) revert InvalidOnboardingRequest(); - if (callData.length < 0x64) revert InvalidOnboardingRequest(); - - address target; - uint256 value; - bytes4 innerSelector; - assembly { - target := calldataload(add(callData.offset, 0x04)) - value := calldataload(add(callData.offset, 0x24)) - // Read inner bytes data (offset at 0x44, must be standard 0x60) - let dataOffset := calldataload(add(callData.offset, 0x44)) - if eq(dataOffset, 0x60) { - let dataStart := add(add(0x04, dataOffset), 0x20) - if lt(dataStart, callData.length) { - innerSelector := calldataload(add(callData.offset, dataStart)) - } - } - } - innerSelector = bytes4(innerSelector); - - // Must target the registry with zero value and registerAccount(string) = 0xbff6de20 - if (target != registry) revert InvalidOnboardingRequest(); - if (value != 0) revert InvalidOnboardingRequest(); - if (innerSelector != bytes4(0xbff6de20)) revert InvalidOnboardingRequest(); + if (registry == address(0)) revert PaymasterHubErrors.InvalidOnboardingRequest(); + (bool valid, bytes4 innerSelector) = PaymasterCalldataLib.parseExecuteCall(callData, registry); + if (!valid) revert PaymasterHubErrors.InvalidOnboardingRequest(); + // Must call registerAccount(string) = 0xbff6de20 + if (innerSelector != bytes4(0xbff6de20)) revert PaymasterHubErrors.InvalidOnboardingRequest(); } /** @@ -1845,24 +1620,24 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG OrgDeployConfig storage deployConfig = _getOrgDeployStorage(); // Check feature is enabled - if (!deployConfig.enabled) revert OrgDeployDisabled(); + if (!deployConfig.enabled) revert PaymasterHubErrors.OrgDeployDisabled(); // No initCode for org deployment (account must already exist) - if (userOp.initCode.length != 0) revert InvalidOrgDeployRequest(); + if (userOp.initCode.length != 0) revert PaymasterHubErrors.InvalidOrgDeployRequest(); // Validate calldata: must be execute(orgDeployerAddress, 0, ...) _validateOrgDeployCallData(userOp.callData, deployConfig.orgDeployer); // Org deploy is paid from solidarity fund, so block when distribution is paused SolidarityFund storage solidarity = _getSolidarityStorage(); - if (solidarity.distributionPaused) revert SolidarityDistributionIsPaused(); + if (solidarity.distributionPaused) revert PaymasterHubErrors.SolidarityDistributionIsPaused(); // Check gas cost limit - if (maxCost > deployConfig.maxGasPerDeploy) revert GasTooHigh(); + if (maxCost > deployConfig.maxGasPerDeploy) revert PaymasterHubErrors.GasTooHigh(); // Check per-account lifetime limit (optimistic increment for bundle safety) mapping(address => uint8) storage counts = _getOrgDeployCountsStorage(); - if (counts[account] >= deployConfig.maxDeploysPerAccount) revert OrgDeployLimitExceeded(); + if (counts[account] >= deployConfig.maxDeploysPerAccount) revert PaymasterHubErrors.OrgDeployLimitExceeded(); counts[account]++; // Check daily rate limit (same pattern as onboarding) @@ -1872,44 +1647,28 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG deployConfig.attemptsToday = 0; } if (deployConfig.attemptsToday >= deployConfig.dailyDeployLimit) { - revert OrgDeployDailyLimitExceeded(); + revert PaymasterHubErrors.OrgDeployDailyLimitExceeded(); } deployConfig.attemptsToday++; // Check solidarity fund has sufficient balance - if (solidarity.balance < maxCost) revert InsufficientFunds(); + if (solidarity.balance < maxCost) revert PaymasterHubErrors.InsufficientFunds(); // Subject key based on account address subjectKey = keccak256(abi.encodePacked(SUBJECT_TYPE_ORG_DEPLOY, bytes20(account))); } /// @dev Validates that org deploy callData is execute(orgDeployerAddress, 0, ...). - /// Checks outer selector, target address, and value. Does NOT parse inner deployFullOrg - /// params because the struct is complex and may change. + /// Does NOT parse inner deployFullOrg params because the struct is complex and may change. function _validateOrgDeployCallData(bytes calldata callData, address orgDeployer) private pure { - if (orgDeployer == address(0)) revert InvalidOrgDeployRequest(); - if (callData.length < 4) revert InvalidOrgDeployRequest(); - - // Must be execute(address,uint256,bytes) = 0xb61d27f6 - bytes4 outerSelector = bytes4(callData[0:4]); - if (outerSelector != bytes4(0xb61d27f6)) revert InvalidOrgDeployRequest(); - if (callData.length < 0x64) revert InvalidOrgDeployRequest(); - - address target; - uint256 value; - assembly { - target := calldataload(add(callData.offset, 0x04)) - value := calldataload(add(callData.offset, 0x24)) - } - - // Must target the orgDeployer with zero value (free deployment) - if (target != orgDeployer) revert InvalidOrgDeployRequest(); - if (value != 0) revert InvalidOrgDeployRequest(); + if (orgDeployer == address(0)) revert PaymasterHubErrors.InvalidOrgDeployRequest(); + (bool valid,) = PaymasterCalldataLib.parseExecuteCall(callData, orgDeployer); + if (!valid) revert PaymasterHubErrors.InvalidOrgDeployRequest(); } function _validateRules(PackedUserOperation calldata userOp, uint32 ruleId, bytes32 orgId) private view { bytes calldata callData = userOp.callData; - if (callData.length < 4) revert InvalidPaymasterData(); + if (callData.length < 4) revert PaymasterHubErrors.InvalidPaymasterData(); // For RULE_ID_GENERIC, executeBatch needs per-inner-call validation if (ruleId == RULE_ID_GENERIC) { @@ -1927,12 +1686,12 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG mapping(address => mapping(bytes4 => Rule)) storage rules = _getRulesStorage()[orgId]; Rule storage rule = rules[target][selector]; - if (!rule.allowed) revert RuleDenied(target, selector); + if (!rule.allowed) revert PaymasterHubErrors.RuleDenied(target, selector); // Check gas hint if set if (rule.maxCallGasHint > 0) { (, uint128 callGasLimit) = UserOpLib.unpackAccountGasLimits(userOp.accountGasLimits); - if (callGasLimit > rule.maxCallGasHint) revert GasTooHigh(); + if (callGasLimit > rule.maxCallGasHint) revert PaymasterHubErrors.GasTooHigh(); } } @@ -1943,39 +1702,31 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG function _validateBatchRules(bytes calldata callData, bytes4 outerSelector, bytes32 orgId) private view { mapping(address => mapping(bytes4 => Rule)) storage rules = _getRulesStorage()[orgId]; + // Decode targets and datas from either batch format + address[] memory targets; + bytes[] memory datas; if (outerSelector == bytes4(0x47e1da2a)) { // executeBatch(address[],uint256[],bytes[]) — PasskeyAccount pattern - (address[] memory targets,, bytes[] memory datas) = - abi.decode(callData[4:], (address[], uint256[], bytes[])); - if (targets.length != datas.length) revert ArrayLengthMismatch(); - for (uint256 i = 0; i < targets.length;) { - bytes4 innerSelector; - if (datas[i].length >= 4) { - bytes memory d = datas[i]; - assembly { innerSelector := mload(add(d, 0x20)) } - } - Rule storage rule = rules[targets[i]][innerSelector]; - if (!rule.allowed) revert RuleDenied(targets[i], innerSelector); - unchecked { - ++i; - } - } + (targets,, datas) = abi.decode(callData[4:], (address[], uint256[], bytes[])); } else { // executeBatch(address[],bytes[]) — SimpleAccount pattern (0x18dfb3c7) - (address[] memory targets, bytes[] memory datas) = abi.decode(callData[4:], (address[], bytes[])); - if (targets.length != datas.length) revert ArrayLengthMismatch(); - for (uint256 i = 0; i < targets.length;) { - bytes4 innerSelector; - if (datas[i].length >= 4) { - bytes memory d = datas[i]; - assembly { innerSelector := mload(add(d, 0x20)) } - } - Rule storage rule = rules[targets[i]][innerSelector]; - if (!rule.allowed) revert RuleDenied(targets[i], innerSelector); - unchecked { - ++i; + (targets, datas) = abi.decode(callData[4:], (address[], bytes[])); + } + + if (targets.length != datas.length) revert PaymasterHubErrors.ArrayLengthMismatch(); + for (uint256 i = 0; i < targets.length;) { + bytes4 innerSelector; + if (datas[i].length >= 4) { + bytes memory d = datas[i]; + assembly { + innerSelector := mload(add(d, 0x20)) } } + Rule storage rule = rules[targets[i]][innerSelector]; + if (!rule.allowed) revert PaymasterHubErrors.RuleDenied(targets[i], innerSelector); + unchecked { + ++i; + } } } @@ -1986,7 +1737,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG { bytes calldata callData = userOp.callData; - if (callData.length < 4) revert InvalidPaymasterData(); + if (callData.length < 4) revert PaymasterHubErrors.InvalidPaymasterData(); if (ruleId == RULE_ID_GENERIC) { // ERC-4337 account execute patterns (SimpleAccount, PasskeyAccount, etc.) @@ -2021,16 +1772,12 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG else { target = userOp.sender; } - } else if (ruleId == RULE_ID_EXECUTOR) { - // Custom Executor pattern - target = userOp.sender; - selector = bytes4(callData[0:4]); } else if (ruleId == RULE_ID_COARSE) { // Coarse mode: only check account's selector target = userOp.sender; selector = bytes4(callData[0:4]); } else { - revert InvalidRuleId(); + revert PaymasterHubErrors.InvalidRuleId(); } } @@ -2039,22 +1786,22 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG (uint128 maxPriorityFeePerGas, uint128 maxFeePerGas) = UserOpLib.unpackGasFees(userOp.gasFees); if (caps.maxFeePerGas > 0 && maxFeePerGas > caps.maxFeePerGas) { - revert FeeTooHigh(); + revert PaymasterHubErrors.FeeTooHigh(); } if (caps.maxPriorityFeePerGas > 0 && maxPriorityFeePerGas > caps.maxPriorityFeePerGas) { - revert FeeTooHigh(); + revert PaymasterHubErrors.FeeTooHigh(); } (uint128 verificationGasLimit, uint128 callGasLimit) = UserOpLib.unpackAccountGasLimits(userOp.accountGasLimits); if (caps.maxCallGas > 0 && callGasLimit > caps.maxCallGas) { - revert GasTooHigh(); + revert PaymasterHubErrors.GasTooHigh(); } if (caps.maxVerificationGas > 0 && verificationGasLimit > caps.maxVerificationGas) { - revert GasTooHigh(); + revert PaymasterHubErrors.GasTooHigh(); } if (caps.maxPreVerificationGas > 0 && userOp.preVerificationGas > caps.maxPreVerificationGas) { - revert GasTooHigh(); + revert PaymasterHubErrors.GasTooHigh(); } } @@ -2076,7 +1823,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG // Check budget capacity (safe conversion as maxCost is bounded by EntryPoint) if (budget.usedInEpoch + uint128(maxCost) > budget.capPerEpoch) { - revert BudgetExceeded(); + revert PaymasterHubErrors.BudgetExceeded(); } // Reserve maxCost for bundle safety — ensures a second UserOp for the same @@ -2100,8 +1847,8 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG // Only update if we're still in the same epoch if (budget.epochStart == epochStart) { // Replace reservation (maxCost) with actual cost (actualGasCost <= maxCost) - budget.usedInEpoch = budget.usedInEpoch - uint128(reservedMaxCost) + uint128(actualGasCost); - emit UsageIncreased(orgId, subjectKey, actualGasCost, budget.usedInEpoch, epochStart); + budget.usedInEpoch = PaymasterPostOpLib.adjustBudget(budget.usedInEpoch, reservedMaxCost, actualGasCost); + emit PaymasterHubErrors.UsageIncreased(orgId, subjectKey, actualGasCost, budget.usedInEpoch, epochStart); } // If epoch rolled since validation, reservation was already cleared by epoch reset } @@ -2116,7 +1863,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG SolidarityFund storage solidarity = _getSolidarityStorage(); if (countAsCreation) { - emit OnboardingAccountCreated(address(0), actualGasCost); + emit PaymasterHubErrors.OnboardingAccountCreated(address(0), actualGasCost); } else { // Refund the daily counter slot for failed operations (incremented during validation for bundle safety) OnboardingConfig storage onboarding = _getOnboardingStorage(); @@ -2126,7 +1873,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG } // Deduct from solidarity fund (validated during _validateOnboardingEligibility) - if (solidarity.balance < actualGasCost) revert InsufficientFunds(); + if (solidarity.balance < actualGasCost) revert PaymasterHubErrors.InsufficientFunds(); solidarity.balance -= uint128(actualGasCost); } @@ -2143,7 +1890,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG if (countAsDeployment) { // Per-account counter already incremented during validation (bundle safety). // Just emit the event. - emit OrgDeploymentSponsored(sender, actualGasCost); + emit PaymasterHubErrors.OrgDeploymentSponsored(sender, actualGasCost); } else { // Refund both counters for failed operations (incremented during validation for bundle safety) mapping(address => uint8) storage counts = _getOrgDeployCountsStorage(); @@ -2157,7 +1904,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG } // Deduct from solidarity fund (validated during _validateOrgDeployEligibility) - if (solidarity.balance < actualGasCost) revert InsufficientFunds(); + if (solidarity.balance < actualGasCost) revert PaymasterHubErrors.InsufficientFunds(); solidarity.balance -= uint128(actualGasCost); } } diff --git a/src/PaymasterHubLens.sol b/src/PaymasterHubLens.sol index a6d8ebc..dc04208 100644 --- a/src/PaymasterHubLens.sol +++ b/src/PaymasterHubLens.sol @@ -6,12 +6,13 @@ import {IEntryPoint} from "./interfaces/IEntryPoint.sol"; import {PackedUserOperation, UserOpLib} from "./interfaces/PackedUserOperation.sol"; import {IHats} from "lib/hats-protocol/src/Interfaces/IHats.sol"; import {IERC165} from "lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol"; +import {PaymasterHubErrors} from "./libs/PaymasterHubErrors.sol"; +import {PaymasterGraceLib} from "./libs/PaymasterGraceLib.sol"; // Storage structs matching PaymasterHub struct OrgConfig { uint256 adminHatId; uint256 operatorHatId; - uint256 __deprecated_voucherHatId; bool paused; uint40 registeredAt; bool bannedFromSolidarity; @@ -19,11 +20,9 @@ struct OrgConfig { struct OrgFinancials { uint128 deposited; - uint128 totalDeposited; uint128 spent; uint128 solidarityUsedThisPeriod; uint32 periodStart; - uint224 reserved; } struct SolidarityFund { @@ -31,7 +30,6 @@ struct SolidarityFund { uint32 numActiveOrgs; uint16 feePercentageBps; bool distributionPaused; - uint200 reserved; } struct GracePeriodConfig { @@ -79,10 +77,6 @@ interface IPaymasterHubStorage { function getOrgFinancials(bytes32 orgId) external view returns (OrgFinancials memory); function getSolidarityFund() external view returns (SolidarityFund memory); function getGracePeriodConfig() external view returns (GracePeriodConfig memory); - function getOrgGraceStatus(bytes32 orgId) - external - view - returns (bool inGrace, uint128 spendRemaining, bool requiresDeposit, uint256 solidarityLimit); function getOrgDeployConfig() external view returns (OrgDeployConfig memory); function getOrgDeployCount(address account) external view returns (uint8); function ENTRY_POINT() external view returns (address); @@ -99,11 +93,6 @@ interface IPaymasterHubStorage { contract PaymasterHubLens { using UserOpLib for bytes32; - // ============ Custom Errors ============ - error InvalidRuleId(); - error InvalidPaymasterData(); - error ZeroAddress(); - // ============ Constants ============ uint8 private constant PAYMASTER_DATA_VERSION = 1; uint8 private constant SUBJECT_TYPE_ACCOUNT = 0x00; @@ -112,7 +101,6 @@ contract PaymasterHubLens { uint8 private constant SUBJECT_TYPE_ORG_DEPLOY = 0x04; uint32 private constant RULE_ID_GENERIC = 0x00000000; - uint32 private constant RULE_ID_EXECUTOR = 0x00000001; uint32 private constant RULE_ID_COARSE = 0x000000FF; // ============ Immutable ============ @@ -120,7 +108,7 @@ contract PaymasterHubLens { // ============ Constructor ============ constructor(address _hub) { - if (_hub == address(0)) revert ZeroAddress(); + if (_hub == address(0)) revert PaymasterHubErrors.ZeroAddress(); hub = IPaymasterHubStorage(_hub); } @@ -176,6 +164,44 @@ contract PaymasterHubLens { return IEntryPoint(entryPoint).balanceOf(address(hub)); } + /** + * @notice Get org's grace period status and limits + * @dev Moved from PaymasterHub to reduce main contract bytecode size + * @param orgId The organization identifier + * @return inGrace True if in initial grace period + * @return spendRemaining Spending remaining during grace (0 if not in grace) + * @return requiresDeposit True if org needs to deposit to access solidarity + * @return solidarityLimit Current solidarity allocation for org (per 90-day period) + */ + function getOrgGraceStatus(bytes32 orgId) + external + view + returns (bool inGrace, uint128 spendRemaining, bool requiresDeposit, uint256 solidarityLimit) + { + OrgConfig memory config = hub.getOrgConfig(orgId); + OrgFinancials memory org = hub.getOrgFinancials(orgId); + GracePeriodConfig memory grace = hub.getGracePeriodConfig(); + SolidarityFund memory solidarity = hub.getSolidarityFund(); + + inGrace = PaymasterGraceLib.isInGracePeriod(config.registeredAt, grace.initialGraceDays); + + // When distribution is paused, no solidarity is available regardless of grace/tier + if (solidarity.distributionPaused) { + return (inGrace, 0, true, 0); + } + + if (inGrace) { + uint128 spendUsed = org.solidarityUsedThisPeriod; + spendRemaining = spendUsed < grace.maxSpendDuringGrace ? grace.maxSpendDuringGrace - spendUsed : 0; + requiresDeposit = false; + solidarityLimit = uint256(grace.maxSpendDuringGrace); + } else { + uint256 depositAvailable = org.deposited > org.spent ? org.deposited - org.spent : 0; + requiresDeposit = depositAvailable < grace.minDepositRequired; + solidarityLimit = PaymasterGraceLib.calculateMatchAllowance(depositAvailable, grace.minDepositRequired); + } + } + /** * @notice Check if a UserOperation would be valid without state changes * @param orgId The org to validate against @@ -273,7 +299,7 @@ contract PaymasterHubLens { // 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)] // = 122 bytes total. Custom data starts at offset 52. - if (paymasterAndData.length < 122) revert InvalidPaymasterData(); + if (paymasterAndData.length < 122) revert PaymasterHubErrors.InvalidPaymasterData(); version = uint8(paymasterAndData[52]); @@ -300,7 +326,7 @@ contract PaymasterHubLens { { bytes calldata callData = userOp.callData; - if (callData.length < 4) revert InvalidPaymasterData(); + if (callData.length < 4) revert PaymasterHubErrors.InvalidPaymasterData(); if (ruleId == RULE_ID_GENERIC) { // ERC-4337 account execute patterns (SimpleAccount, PasskeyAccount, etc.) @@ -337,14 +363,11 @@ contract PaymasterHubLens { } else { target = userOp.sender; } - } else if (ruleId == RULE_ID_EXECUTOR) { - target = userOp.sender; - selector = bytes4(callData[0:4]); } else if (ruleId == RULE_ID_COARSE) { target = userOp.sender; selector = bytes4(callData[0:4]); } else { - revert InvalidRuleId(); + revert PaymasterHubErrors.InvalidRuleId(); } } } diff --git a/src/libs/PaymasterCalldataLib.sol b/src/libs/PaymasterCalldataLib.sol new file mode 100644 index 0000000..4faef44 --- /dev/null +++ b/src/libs/PaymasterCalldataLib.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.24; + +/// @title PaymasterCalldataLib +/// @author POA Engineering +/// @notice Calldata parsing for ERC-4337 execute(address,uint256,bytes) envelope validation +/// @dev Extracts and validates the target, value, and inner selector from a UserOp's callData +/// when it follows the standard SimpleAccount/PasskeyAccount execute pattern. +/// All functions are internal pure (inlined at compile time) for zero gas overhead. +library PaymasterCalldataLib { + /// @notice Selector for execute(address,uint256,bytes) + bytes4 internal constant EXECUTE_SELECTOR = 0xb61d27f6; + + /// @notice Parse and validate an execute(address,uint256,bytes) calldata envelope + /// @dev Checks: callData >= 4 bytes, outer selector = EXECUTE_SELECTOR, callData >= 0x64 bytes, + /// target matches expectedTarget, and value == 0. If all pass, extracts the inner selector + /// from the nested bytes parameter (if available). + /// @param callData The full callData from the UserOperation + /// @param expectedTarget The required target address in the execute call + /// @return valid True if all structural checks pass (selector, length, target, value) + /// @return innerSelector First 4 bytes of the inner data payload (bytes4(0) if unavailable) + function parseExecuteCall(bytes calldata callData, address expectedTarget) + internal + pure + returns (bool valid, bytes4 innerSelector) + { + // Must have at least a 4-byte selector + if (callData.length < 4) return (false, bytes4(0)); + + // Outer selector must be execute(address,uint256,bytes) + if (bytes4(callData[0:4]) != EXECUTE_SELECTOR) return (false, bytes4(0)); + + // Need at least 0x64 bytes for execute(address, uint256, bytes offset) + if (callData.length < 0x64) return (false, bytes4(0)); + + address target; + uint256 value; + assembly { + target := calldataload(add(callData.offset, 0x04)) + value := calldataload(add(callData.offset, 0x24)) + // Read inner bytes data (offset at 0x44, must be standard 0x60) + let dataOffset := calldataload(add(callData.offset, 0x44)) + if eq(dataOffset, 0x60) { + let dataStart := add(add(0x04, dataOffset), 0x20) + if lt(dataStart, callData.length) { + innerSelector := calldataload(add(callData.offset, dataStart)) + } + } + } + innerSelector = bytes4(innerSelector); + + // Target must match and value must be zero + if (target != expectedTarget || value != 0) return (false, bytes4(0)); + + valid = true; + } +} diff --git a/src/libs/PaymasterGraceLib.sol b/src/libs/PaymasterGraceLib.sol new file mode 100644 index 0000000..52bfb48 --- /dev/null +++ b/src/libs/PaymasterGraceLib.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.24; + +/// @title PaymasterGraceLib +/// @author POA Engineering +/// @notice Grace period calculations and solidarity tier matching for PaymasterHub +/// @dev All functions are internal (inlined at compile time) for zero gas overhead +library PaymasterGraceLib { + /// @notice Check if an organization is currently in its initial grace period + /// @param registeredAt Timestamp when the org was registered (uint40) + /// @param initialGraceDays Configured grace period duration in days (uint32) + /// @return True if the current block is before the grace period end + function isInGracePeriod(uint40 registeredAt, uint32 initialGraceDays) internal view returns (bool) { + return block.timestamp < uint256(registeredAt) + (uint256(initialGraceDays) * 1 days); + } + + /// @notice Calculate solidarity fee, returning zero during the grace period + /// @dev Replaces the repeated pattern of computing graceEndTime then branching on fee + /// @param actualGasCost The actual gas cost of the operation + /// @param feePercentageBps Solidarity fee in basis points (e.g. 100 = 1%) + /// @param registeredAt Timestamp when the org was registered + /// @param initialGraceDays Configured grace period duration in days + /// @return fee The calculated solidarity fee (0 during grace period) + function solidarityFee(uint256 actualGasCost, uint16 feePercentageBps, uint40 registeredAt, uint32 initialGraceDays) + internal + view + returns (uint256 fee) + { + if (isInGracePeriod(registeredAt, initialGraceDays)) { + return 0; + } + return (actualGasCost * uint256(feePercentageBps)) / 10000; + } + + /// @notice Calculate solidarity match allowance based on deposit tier + /// @dev Progressive tier system with declining marginal match rates: + /// - Tier 1: deposit <= 1x min -> 2x match (total 3x) + /// - Tier 2: deposit <= 2x min -> 3x match total (total 5x) + /// - Tier 3: deposit < 5x min -> capped at 3x min match + /// - Tier 4: deposit >= 5x min -> no match (self-funded) + /// @param deposited Current available deposit balance + /// @param minDeposit Minimum deposit requirement from grace period config + /// @return matchAllowance Maximum solidarity usage per 90-day period + function calculateMatchAllowance(uint256 deposited, uint256 minDeposit) internal pure returns (uint256) { + if (minDeposit == 0 || deposited < minDeposit) { + return 0; + } + // Tier 1: deposit <= 1x minimum -> 2x match + if (deposited <= minDeposit) { + return deposited * 2; + } + // Tier 2: deposit <= 2x minimum -> first tier at 2x, remainder at 1x + if (deposited <= minDeposit * 2) { + uint256 firstTierMatch = minDeposit * 2; + uint256 secondTierMatch = deposited - minDeposit; + return firstTierMatch + secondTierMatch; + } + // Tier 3: deposit < 5x minimum -> capped match from first two tiers + if (deposited < minDeposit * 5) { + uint256 firstTierMatch = minDeposit * 2; + uint256 secondTierMatch = minDeposit; + return firstTierMatch + secondTierMatch; + } + // Tier 4: >= 5x minimum -> self-sufficient, no match + return 0; + } +} diff --git a/src/libs/PaymasterHubErrors.sol b/src/libs/PaymasterHubErrors.sol new file mode 100644 index 0000000..5a99cf9 --- /dev/null +++ b/src/libs/PaymasterHubErrors.sol @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.24; + +/// @title PaymasterHubErrors +/// @author POA Engineering +/// @notice Shared errors and events for PaymasterHub and PaymasterHubLens +library PaymasterHubErrors { + // ============ Access Control Errors ============ + + /// @notice Caller is not the ERC-4337 EntryPoint + error EPOnly(); + + /// @notice Organization is paused by its admin + error Paused(); + + /// @notice Caller does not wear the org admin hat + error NotAdmin(); + + /// @notice Caller does not wear the org operator hat + error NotOperator(); + + /// @notice Caller is not the PoaManager contract + error NotPoaManager(); + + // ============ Validation Errors ============ + + /// @notice Target + selector combination is not in the org's allowlist + error RuleDenied(address target, bytes4 selector); + + /// @notice Gas fee exceeds the org's configured fee cap + error FeeTooHigh(); + + /// @notice Gas limit exceeds the org's configured gas cap + error GasTooHigh(); + + /// @notice Sender is not eligible for the specified subject (hat or account) + error Ineligible(); + + /// @notice Subject's per-epoch budget would be exceeded + error BudgetExceeded(); + + /// @notice Rule ID is not a recognized rule type + error InvalidRuleId(); + + /// @notice ETH transfer to recipient failed + error PaymentFailed(); + + /// @notice Subject type byte is not a recognized type (0x00, 0x01, 0x03, 0x04) + error InvalidSubjectType(); + + /// @notice Paymaster data version does not match expected version + error InvalidVersion(); + + /// @notice Paymaster data is malformed or too short + error InvalidPaymasterData(); + + /// @notice Address parameter must not be zero + error ZeroAddress(); + + /// @notice Epoch length is outside the allowed range + error InvalidEpochLength(); + + /// @notice Expected contract at address has no code deployed + error ContractNotDeployed(); + + /// @notice Array parameters have mismatched lengths + error ArrayLengthMismatch(); + + // ============ Organization Errors ============ + + /// @notice Organization is not registered in the PaymasterHub + error OrgNotRegistered(); + + /// @notice Organization is already registered + error OrgAlreadyRegistered(); + + /// @notice Organization ID is invalid (zero) + error InvalidOrgId(); + + // ============ Solidarity & Grace Period Errors ============ + + /// @notice Org has exceeded its grace period spending limit + error GracePeriodSpendLimitReached(); + + /// @notice Org's deposit is below the minimum required to access solidarity + error InsufficientDepositForSolidarity(); + + /// @notice Org has exceeded its tier-based solidarity match allowance + error SolidarityLimitExceeded(); + + /// @notice Org's available deposit balance is insufficient + error InsufficientOrgBalance(); + + /// @notice Organization is banned from using the solidarity fund + error OrgIsBanned(); + + /// @notice Solidarity fund or org balance has insufficient funds + error InsufficientFunds(); + + /// @notice Solidarity fund distribution is currently paused + error SolidarityDistributionIsPaused(); + + /// @notice Arithmetic overflow detected + error Overflow(); + + /// @notice Amount parameter must not be zero + error ZeroAmount(); + + // ============ Onboarding Errors ============ + + /// @notice Onboarding sponsorship feature is disabled + error OnboardingDisabled(); + + /// @notice Global daily onboarding creation limit has been reached + error OnboardingDailyLimitExceeded(); + + /// @notice Onboarding request is malformed (bad calldata, missing initCode, etc.) + error InvalidOnboardingRequest(); + + // ============ Org Deploy Errors ============ + + /// @notice Org deployment sponsorship feature is disabled + error OrgDeployDisabled(); + + /// @notice Account has reached its lifetime org deployment limit + error OrgDeployLimitExceeded(); + + /// @notice Global daily org deployment limit has been reached + error OrgDeployDailyLimitExceeded(); + + /// @notice Org deploy request is malformed (bad calldata, has initCode, etc.) + error InvalidOrgDeployRequest(); + + // ============ Events ============ + + /// @notice Emitted when the PaymasterHub is initialized + event PaymasterInitialized(address indexed entryPoint, address indexed hats, address indexed poaManager); + + /// @notice Emitted when a new organization is registered + event OrgRegistered(bytes32 indexed orgId, uint256 adminHatId, uint256 operatorHatId); + + /// @notice Emitted when a rule is set or updated for an org + event RuleSet( + bytes32 indexed orgId, address indexed target, bytes4 indexed selector, bool allowed, uint32 maxCallGasHint + ); + + /// @notice Emitted when a per-subject budget is configured + event BudgetSet(bytes32 indexed orgId, bytes32 subjectKey, uint128 capPerEpoch, uint32 epochLen, uint32 epochStart); + + /// @notice Emitted when an org's fee caps are updated + event FeeCapsSet( + bytes32 indexed orgId, + uint256 maxFeePerGas, + uint256 maxPriorityFeePerGas, + uint32 maxCallGas, + uint32 maxVerificationGas, + uint32 maxPreVerificationGas + ); + + /// @notice Emitted when an org's pause state changes + event PauseSet(bytes32 indexed orgId, bool paused); + + /// @notice Emitted when an org's operator hat is updated + event OperatorHatSet(bytes32 indexed orgId, uint256 operatorHatId); + + /// @notice Emitted when ETH is deposited to the EntryPoint + event DepositIncrease(uint256 amount, uint256 newDeposit); + + /// @notice Emitted when ETH is withdrawn from the EntryPoint deposit + event DepositWithdraw(address indexed to, uint256 amount); + + /// @notice Emitted when a subject's budget usage increases + event UsageIncreased( + bytes32 indexed orgId, bytes32 subjectKey, uint256 delta, uint128 usedInEpoch, uint32 epochStart + ); + + /// @notice Emitted on emergency withdrawal by PoaManager + event EmergencyWithdraw(address indexed to, uint256 amount); + + /// @notice Emitted when ETH is deposited for a specific org + event OrgDepositReceived(bytes32 indexed orgId, address indexed from, uint256 amount); + + /// @notice Emitted when solidarity fee is collected from an org's operation + event SolidarityFeeCollected(bytes32 indexed orgId, uint256 amount); + + /// @notice Emitted when a direct donation is made to the solidarity fund + event SolidarityDonationReceived(address indexed from, uint256 amount); + + /// @notice Emitted when grace period configuration is updated + event GracePeriodConfigUpdated(uint32 initialGraceDays, uint128 maxSpendDuringGrace, uint128 minDepositRequired); + + /// @notice Emitted when an org's solidarity ban status changes + event OrgBannedFromSolidarity(bytes32 indexed orgId, bool banned); + + /// @notice Emitted when onboarding configuration is updated + event OnboardingConfigUpdated( + uint128 maxGasPerCreation, uint128 dailyCreationLimit, bool enabled, address accountRegistry + ); + + /// @notice Emitted when a new account is created via onboarding sponsorship + event OnboardingAccountCreated(address indexed account, uint256 gasCost); + + /// @notice Emitted when org deployment configuration is updated + event OrgDeployConfigUpdated( + uint128 maxGasPerDeploy, uint128 dailyDeployLimit, uint8 maxDeploysPerAccount, bool enabled, address orgDeployer + ); + + /// @notice Emitted when an org deployment is sponsored + event OrgDeploymentSponsored(address indexed account, uint256 gasCost); + + /// @notice Emitted when solidarity fund distribution is paused + event SolidarityDistributionPaused(); + + /// @notice Emitted when solidarity fund distribution is unpaused + event SolidarityDistributionUnpaused(); +} diff --git a/src/libs/PaymasterPostOpLib.sol b/src/libs/PaymasterPostOpLib.sol new file mode 100644 index 0000000..a07b99a --- /dev/null +++ b/src/libs/PaymasterPostOpLib.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.24; + +/// @title PaymasterPostOpLib +/// @author POA Engineering +/// @notice PostOp accounting helpers for PaymasterHub budget and solidarity adjustments +/// @dev All functions are internal pure (inlined at compile time) for zero gas overhead +library PaymasterPostOpLib { + /// @notice Replace a budget reservation with the actual gas cost + /// @dev Used in postOp to swap the maxCost reservation made during validation + /// with the actual (lower) gas cost: usedInEpoch - reserved + actual + /// @param usedInEpoch Current used amount in the epoch (includes reservation) + /// @param reserved Amount reserved during validation (maxCost) + /// @param actual Actual gas cost from EntryPoint (actual <= reserved) + /// @return Updated usedInEpoch value + function adjustBudget(uint128 usedInEpoch, uint256 reserved, uint256 actual) internal pure returns (uint128) { + return usedInEpoch - uint128(reserved) + uint128(actual); + } + + /// @notice Deduct from solidarity balance, clamped to prevent underflow + /// @dev Used in sponsorship fallback paths where the function must never revert. + /// Returns min(balance, cost) as the deduction amount. + /// @param balance Current solidarity fund balance + /// @param cost Amount to deduct + /// @return newBalance Updated solidarity balance after deduction + /// @return deducted Actual amount deducted (may be less than cost if balance is insufficient) + function clampedDeduction(uint128 balance, uint256 cost) + internal + pure + returns (uint128 newBalance, uint128 deducted) + { + deducted = balance < uint128(cost) ? balance : uint128(cost); + newBalance = balance - deducted; + } +} diff --git a/test/DeployerTest.t.sol b/test/DeployerTest.t.sol index 726b92b..8951030 100644 --- a/test/DeployerTest.t.sol +++ b/test/DeployerTest.t.sol @@ -4748,7 +4748,6 @@ contract DeployerTest is Test, IEligibilityModuleEvents { // Verify deposit was credited PaymasterHub.OrgFinancials memory financials = paymasterHub.getOrgFinancials(orgId); assertEq(financials.deposited, 0.1 ether, "Org should have 0.1 ETH deposited"); - assertEq(financials.totalDeposited, 0.1 ether, "Total deposited should be 0.1 ETH"); } function testDeployFullOrgWithPaymasterAutoWhitelist() public { diff --git a/test/PasskeyPaymasterIntegration.t.sol b/test/PasskeyPaymasterIntegration.t.sol index 3649209..5b1c12c 100644 --- a/test/PasskeyPaymasterIntegration.t.sol +++ b/test/PasskeyPaymasterIntegration.t.sol @@ -13,6 +13,7 @@ import {IPasskeyAccount} from "../src/interfaces/IPasskeyAccount.sol"; // PaymasterHub import {PaymasterHub} from "../src/PaymasterHub.sol"; +import {PaymasterHubErrors} from "../src/libs/PaymasterHubErrors.sol"; import {IPaymaster} from "../src/interfaces/IPaymaster.sol"; import {IEntryPoint} from "../src/interfaces/IEntryPoint.sol"; import {PackedUserOperation, UserOpLib} from "../src/interfaces/PackedUserOperation.sol"; @@ -84,7 +85,6 @@ contract PasskeyPaymasterIntegrationTest is Test { uint8 constant SUBJECT_TYPE_ACCOUNT = 0x00; uint8 constant SUBJECT_TYPE_HAT = 0x01; uint32 constant RULE_ID_GENERIC = 0x00000000; - uint32 constant RULE_ID_EXECUTOR = 0x00000001; uint32 constant RULE_ID_COARSE = 0x000000FF; // Function selectors @@ -290,7 +290,7 @@ contract PasskeyPaymasterIntegrationTest is Test { vm.prank(address(entryPoint)); vm.expectRevert( abi.encodeWithSelector( - PaymasterHub.RuleDenied.selector, address(mockTarget), MockTarget.doSomething.selector + PaymasterHubErrors.RuleDenied.selector, address(mockTarget), MockTarget.doSomething.selector ) ); hub.validatePaymasterUserOp(userOp, bytes32(0), 0.001 ether); @@ -330,7 +330,9 @@ contract PasskeyPaymasterIntegrationTest is Test { vm.prank(address(entryPoint)); vm.expectRevert( - abi.encodeWithSelector(PaymasterHub.RuleDenied.selector, address(target2), MockTarget.doSomething.selector) + abi.encodeWithSelector( + PaymasterHubErrors.RuleDenied.selector, address(target2), MockTarget.doSomething.selector + ) ); hub.validatePaymasterUserOp(userOp2, bytes32(0), 0.001 ether); } @@ -400,7 +402,7 @@ contract PasskeyPaymasterIntegrationTest is Test { vm.prank(address(entryPoint)); vm.expectRevert( abi.encodeWithSelector( - PaymasterHub.RuleDenied.selector, address(mockTarget), MockTarget.doSomething.selector + PaymasterHubErrors.RuleDenied.selector, address(mockTarget), MockTarget.doSomething.selector ) ); hub.validatePaymasterUserOp(userOp, bytes32(0), 0.001 ether); @@ -437,7 +439,7 @@ contract PasskeyPaymasterIntegrationTest is Test { vm.prank(address(entryPoint)); vm.expectRevert( abi.encodeWithSelector( - PaymasterHub.RuleDenied.selector, address(mockTarget), MockTarget.doSomethingElse.selector + PaymasterHubErrors.RuleDenied.selector, address(mockTarget), MockTarget.doSomethingElse.selector ) ); hub.validatePaymasterUserOp(userOp, bytes32(0), 0.001 ether); @@ -477,7 +479,9 @@ contract PasskeyPaymasterIntegrationTest is Test { vm.prank(address(entryPoint)); vm.expectRevert( - abi.encodeWithSelector(PaymasterHub.RuleDenied.selector, address(target2), MockTarget.doSomething.selector) + abi.encodeWithSelector( + PaymasterHubErrors.RuleDenied.selector, address(target2), MockTarget.doSomething.selector + ) ); hub.validatePaymasterUserOp(userOp, bytes32(0), 0.001 ether); } @@ -619,7 +623,9 @@ contract PasskeyPaymasterIntegrationTest is Test { vm.prank(address(entryPoint)); vm.expectRevert( abi.encodeWithSelector( - PaymasterHub.RuleDenied.selector, address(mockEligibility), MockEligibility.claimVouchedHat.selector + PaymasterHubErrors.RuleDenied.selector, + address(mockEligibility), + MockEligibility.claimVouchedHat.selector ) ); hub.validatePaymasterUserOp(userOp, bytes32(0), 0.001 ether); @@ -693,7 +699,7 @@ contract PasskeyPaymasterIntegrationTest is Test { vm.prank(address(entryPoint)); vm.expectRevert( abi.encodeWithSelector( - PaymasterHub.RuleDenied.selector, address(target2), MockTarget.doSomethingElse.selector + PaymasterHubErrors.RuleDenied.selector, address(target2), MockTarget.doSomethingElse.selector ) ); hub.validatePaymasterUserOp(userOp, bytes32(0), 0.001 ether); @@ -749,7 +755,7 @@ contract PasskeyPaymasterIntegrationTest is Test { vm.prank(address(entryPoint)); vm.expectRevert( abi.encodeWithSelector( - PaymasterHub.RuleDenied.selector, address(mockTarget), MockTarget.doSomething.selector + PaymasterHubErrors.RuleDenied.selector, address(mockTarget), MockTarget.doSomething.selector ) ); hub.validatePaymasterUserOp(userOp, bytes32(0), 0.001 ether); @@ -776,7 +782,7 @@ contract PasskeyPaymasterIntegrationTest is Test { // Should be denied: (mockTarget, bytes4(0)) is not whitelisted vm.prank(address(entryPoint)); - vm.expectRevert(abi.encodeWithSelector(PaymasterHub.RuleDenied.selector, address(mockTarget), bytes4(0))); + vm.expectRevert(abi.encodeWithSelector(PaymasterHubErrors.RuleDenied.selector, address(mockTarget), bytes4(0))); hub.validatePaymasterUserOp(userOp, bytes32(0), 0.001 ether); } @@ -800,7 +806,7 @@ contract PasskeyPaymasterIntegrationTest is Test { PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); vm.prank(address(entryPoint)); - vm.expectRevert(abi.encodeWithSelector(PaymasterHub.RuleDenied.selector, address(mockTarget), bytes4(0))); + vm.expectRevert(abi.encodeWithSelector(PaymasterHubErrors.RuleDenied.selector, address(mockTarget), bytes4(0))); hub.validatePaymasterUserOp(userOp, bytes32(0), 0.001 ether); } @@ -867,7 +873,7 @@ contract PasskeyPaymasterIntegrationTest is Test { // Should be denied: (account, executeBatch_selector) is not whitelisted vm.prank(address(entryPoint)); vm.expectRevert( - abi.encodeWithSelector(PaymasterHub.RuleDenied.selector, address(account), EXECUTE_BATCH_SELECTOR) + abi.encodeWithSelector(PaymasterHubErrors.RuleDenied.selector, address(account), EXECUTE_BATCH_SELECTOR) ); hub.validatePaymasterUserOp(userOp, bytes32(0), 0.001 ether); } @@ -900,7 +906,9 @@ contract PasskeyPaymasterIntegrationTest is Test { // Should be denied: (account, execute_selector) is not whitelisted vm.prank(address(entryPoint)); - vm.expectRevert(abi.encodeWithSelector(PaymasterHub.RuleDenied.selector, address(account), EXECUTE_SELECTOR)); + vm.expectRevert( + abi.encodeWithSelector(PaymasterHubErrors.RuleDenied.selector, address(account), EXECUTE_SELECTOR) + ); hub.validatePaymasterUserOp(userOp, bytes32(0), 0.001 ether); } @@ -929,7 +937,7 @@ contract PasskeyPaymasterIntegrationTest is Test { vm.prank(address(entryPoint)); vm.expectRevert( - abi.encodeWithSelector(PaymasterHub.RuleDenied.selector, address(account), EXECUTE_BATCH_SELECTOR) + abi.encodeWithSelector(PaymasterHubErrors.RuleDenied.selector, address(account), EXECUTE_BATCH_SELECTOR) ); hub.validatePaymasterUserOp(userOp, bytes32(0), 0.001 ether); } @@ -995,35 +1003,6 @@ contract PasskeyPaymasterIntegrationTest is Test { assertTrue(context.length > 0); } - function testExecuteBatch_ExecutorMode_StillUsesAccountLevel() public { - PasskeyAccount account = _createPasskeyAccount(); - _setupDefaultBudget(address(account)); - - // With RULE_ID_EXECUTOR, executeBatch should still use (sender, executeBatch) - vm.prank(orgAdmin); - hub.setRule(ORG_ID, address(account), EXECUTE_BATCH_SELECTOR, true, 0); - - address[] memory targets = new address[](1); - targets[0] = address(mockTarget); - - uint256[] memory values = new uint256[](1); - bytes[] memory datas = new bytes[](1); - datas[0] = abi.encodeWithSelector(MockTarget.doSomething.selector); - - bytes memory callData = _buildExecuteBatchCalldata(targets, values, datas); - bytes memory paymasterAndData = _buildPaymasterData( - ORG_ID, SUBJECT_TYPE_ACCOUNT, bytes32(uint256(uint160(address(account)))), RULE_ID_EXECUTOR - ); - - PackedUserOperation memory userOp = _createUserOp(address(account), callData, paymasterAndData, ""); - - vm.prank(address(entryPoint)); - (bytes memory context, uint256 validationData) = hub.validatePaymasterUserOp(userOp, bytes32(0), 0.001 ether); - - assertEq(validationData, 0, "EXECUTOR mode should still use account-level validation for batch"); - assertTrue(context.length > 0); - } - function testExecuteBatch_OrgOperationSelectors_VouchAndVote() public { PasskeyAccount account = _createPasskeyAccount(); _setupDefaultBudget(address(account)); @@ -1195,7 +1174,7 @@ contract PasskeyPaymasterIntegrationTest is Test { }); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.GasTooHigh.selector); + vm.expectRevert(PaymasterHubErrors.GasTooHigh.selector); hub.validatePaymasterUserOp(userOp, bytes32(0), 0.001 ether); } @@ -1251,7 +1230,7 @@ contract PasskeyPaymasterIntegrationTest is Test { // Try to use more than budget allows vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.BudgetExceeded.selector); + vm.expectRevert(PaymasterHubErrors.BudgetExceeded.selector); hub.validatePaymasterUserOp(userOp, bytes32(0), 0.01 ether); // maxCost > budget } @@ -1333,7 +1312,7 @@ contract PasskeyPaymasterIntegrationTest is Test { PackedUserOperation memory userOp = _createUserOp(address(account), callData, shortData, ""); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.InvalidPaymasterData.selector); + vm.expectRevert(PaymasterHubErrors.InvalidPaymasterData.selector); hub.validatePaymasterUserOp(userOp, bytes32(0), 0.001 ether); } @@ -1394,7 +1373,7 @@ contract PasskeyPaymasterIntegrationTest is Test { PackedUserOperation memory userOp = _createUserOp(ineligibleUser, callData, paymasterAndData, ""); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.Ineligible.selector); + vm.expectRevert(PaymasterHubErrors.Ineligible.selector); hub.validatePaymasterUserOp(userOp, bytes32(0), 0.01 ether); } @@ -1446,7 +1425,7 @@ contract PasskeyPaymasterIntegrationTest is Test { PackedUserOperation memory userOp = _createUserOp(eligibleUser, callData, paymasterAndData, ""); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.Ineligible.selector); + vm.expectRevert(PaymasterHubErrors.Ineligible.selector); hub.validatePaymasterUserOp(userOp, bytes32(0), 0.01 ether); } @@ -1470,7 +1449,7 @@ contract PasskeyPaymasterIntegrationTest is Test { PackedUserOperation memory userOp = _createUserOp(user, callData, paymasterAndData, ""); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.Ineligible.selector); + vm.expectRevert(PaymasterHubErrors.Ineligible.selector); hub.validatePaymasterUserOp(userOp, bytes32(0), 0.01 ether); } @@ -1526,7 +1505,7 @@ contract PasskeyPaymasterIntegrationTest is Test { PackedUserOperation memory userOp = _createUserOp(address(account), callData, tooShort, ""); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.InvalidPaymasterData.selector); + vm.expectRevert(PaymasterHubErrors.InvalidPaymasterData.selector); hub.validatePaymasterUserOp(userOp, bytes32(0), 0.001 ether); } @@ -1740,7 +1719,7 @@ contract PasskeyPaymasterIntegrationTest is Test { // Op 2 validation: maxCost=0.005 ETH, but budget already has 0.005 reserved // Total would be 0.01 > 0.009 cap → must revert vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.BudgetExceeded.selector); + vm.expectRevert(PaymasterHubErrors.BudgetExceeded.selector); hub.validatePaymasterUserOp(userOp, bytes32(uint256(2)), 0.005 ether); } @@ -1791,7 +1770,7 @@ contract PasskeyPaymasterIntegrationTest is Test { // Op 2: maxCost=0.005, but only 0.003 remaining → must revert vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.InsufficientOrgBalance.selector); + vm.expectRevert(PaymasterHubErrors.InsufficientOrgBalance.selector); hub.validatePaymasterUserOp(userOp, bytes32(uint256(2)), 0.005 ether); } @@ -1999,7 +1978,7 @@ contract PasskeyPaymasterIntegrationTest is Test { // Op 3: 0.005 would make total 0.015 > cap (0.015 - 1), must revert vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.BudgetExceeded.selector); + vm.expectRevert(PaymasterHubErrors.BudgetExceeded.selector); hub.validatePaymasterUserOp(userOp, bytes32(uint256(3)), 0.005 ether); } @@ -2179,7 +2158,7 @@ contract PasskeyPaymasterIntegrationTest is Test { // solidarityUsedThisPeriod is now 75_000. Next validate: 75_000 + 30_000 = 105_000 > 100_000 vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.GracePeriodSpendLimitReached.selector); + vm.expectRevert(PaymasterHubErrors.GracePeriodSpendLimitReached.selector); hub.validatePaymasterUserOp(userOp, bytes32(uint256(4)), 30_000); } @@ -2641,7 +2620,7 @@ contract PasskeyPaymasterIntegrationTest is Test { // depositAvailable for op2's solidarity check = 0.005 - 0.004 = 0.001 ether // 0.001 ether < minDepositRequired (0.003 ether), so op2 should fail InsufficientDepositForSolidarity vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.InsufficientDepositForSolidarity.selector); + vm.expectRevert(PaymasterHubErrors.InsufficientDepositForSolidarity.selector); hub.validatePaymasterUserOp(userOp, bytes32(uint256(2)), 0.004 ether); // But after op1's postOp settles, deposits are freed for the next validation diff --git a/test/PaymasterHub.t.sol.skip b/test/PaymasterHub.t.sol.skip index 1ed7ca5..03d94e1 100644 --- a/test/PaymasterHub.t.sol.skip +++ b/test/PaymasterHub.t.sol.skip @@ -3,6 +3,7 @@ pragma solidity ^0.8.24; import {Test, console2} from "forge-std/Test.sol"; import {PaymasterHub} from "../src/PaymasterHub.sol"; +import {PaymasterHubErrors} from "../src/libs/PaymasterHubErrors.sol"; import {PaymasterHubLens, Config, Budget, Rule, FeeCaps, Bounty} from "../src/PaymasterHubLens.sol"; import {IPaymaster} from "../src/interfaces/IPaymaster.sol"; import {IEntryPoint} from "../src/interfaces/IEntryPoint.sol"; @@ -351,7 +352,7 @@ contract PaymasterHubTest is Test { function testAdminAccessControl() public { // Non-admin should fail vm.prank(user); - vm.expectRevert(PaymasterHub.NotAdmin.selector); + vm.expectRevert(PaymasterHubErrors.NotAdmin.selector); hub.setPause(true); // Admin should succeed @@ -445,7 +446,7 @@ contract PaymasterHubTest is Test { // Should revert when paused vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.Paused.selector); + vm.expectRevert(PaymasterHubErrors.Paused.selector); hub.validatePaymasterUserOp(userOp, userOpHash, 0.1 ether); } @@ -466,7 +467,7 @@ contract PaymasterHubTest is Test { userOp.paymasterAndData = invalidPaymasterData; vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.InvalidVersion.selector); + vm.expectRevert(PaymasterHubErrors.InvalidVersion.selector); hub.validatePaymasterUserOp(userOp, bytes32(0), 0.1 ether); } @@ -478,7 +479,7 @@ contract PaymasterHubTest is Test { uint256 maxCost = 2 ether; // Exceeds budget vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.BudgetExceeded.selector); + vm.expectRevert(PaymasterHubErrors.BudgetExceeded.selector); hub.validatePaymasterUserOp(userOp, userOpHash, maxCost); } @@ -495,7 +496,7 @@ contract PaymasterHubTest is Test { // Contract extracts target from execute parameters (0x123) and uses execute selector vm.expectRevert( abi.encodeWithSelector( - PaymasterHub.RuleDenied.selector, address(0x123), bytes4(keccak256("execute(address,uint256,bytes)")) + PaymasterHubErrors.RuleDenied.selector, address(0x123), bytes4(keccak256("execute(address,uint256,bytes)")) ) ); hub.validatePaymasterUserOp(userOp, userOpHash, 0.1 ether); @@ -612,7 +613,7 @@ contract PaymasterHubTest is Test { bytes32 userOpHash = keccak256(abi.encode(userOp)); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.FeeTooHigh.selector); + vm.expectRevert(PaymasterHubErrors.FeeTooHigh.selector); hub.validatePaymasterUserOp(userOp, userOpHash, 0.1 ether); } @@ -639,7 +640,7 @@ contract PaymasterHubTest is Test { bytes32 expectedHash = keccak256(packedOp); vm.expectEmit(true, true, false, false); - emit PaymasterHub.UserOpPosted(expectedHash, user); + emit PaymasterHubErrors.UserOpPosted(expectedHash, user); vm.prank(user); bytes32 returnedHash = hub.postUserOp(packedOp); @@ -771,7 +772,7 @@ contract PaymasterHubTest is Test { vm.prank(address(entryPoint)); if (used + maxCost > cap) { - vm.expectRevert(PaymasterHub.BudgetExceeded.selector); + vm.expectRevert(PaymasterHubErrors.BudgetExceeded.selector); hub.validatePaymasterUserOp(userOp, userOpHash, maxCost); } else { (bytes memory context,) = hub.validatePaymasterUserOp(userOp, userOpHash, maxCost); diff --git a/test/PaymasterHubIntegration.t.sol.skip b/test/PaymasterHubIntegration.t.sol.skip index 0ab7f81..e61a52a 100644 --- a/test/PaymasterHubIntegration.t.sol.skip +++ b/test/PaymasterHubIntegration.t.sol.skip @@ -3,6 +3,7 @@ pragma solidity ^0.8.24; import {Test, console2} from "forge-std/Test.sol"; import {PaymasterHub} from "../src/PaymasterHub.sol"; +import {PaymasterHubErrors} from "../src/libs/PaymasterHubErrors.sol"; import {PaymasterHubLens, Config, Budget, Rule, FeeCaps, Bounty} from "../src/PaymasterHubLens.sol"; import {IPaymaster} from "../src/interfaces/IPaymaster.sol"; import {PackedUserOperation, UserOpLib} from "../src/interfaces/PackedUserOperation.sol"; @@ -267,7 +268,7 @@ contract PaymasterHubIntegrationTest is Test { _createUserOp(address(account1), targetContract, abi.encodeWithSignature("operation2()"), 0); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.BudgetExceeded.selector); + vm.expectRevert(PaymasterHubErrors.BudgetExceeded.selector); hub.validatePaymasterUserOp(op2, keccak256(abi.encode(op2)), 0.3 ether); // Move to next epoch diff --git a/test/PaymasterHubInvariants.t.sol.skip b/test/PaymasterHubInvariants.t.sol.skip index 87fefdd..340c4e6 100644 --- a/test/PaymasterHubInvariants.t.sol.skip +++ b/test/PaymasterHubInvariants.t.sol.skip @@ -3,6 +3,7 @@ pragma solidity ^0.8.24; import {Test, console2} from "forge-std/Test.sol"; import {PaymasterHub} from "../src/PaymasterHub.sol"; +import {PaymasterHubErrors} from "../src/libs/PaymasterHubErrors.sol"; import {PaymasterHubLens, Config, Budget, Rule, FeeCaps, Bounty} from "../src/PaymasterHubLens.sol"; import {PackedUserOperation, UserOpLib} from "../src/interfaces/PackedUserOperation.sol"; import {IPaymaster} from "../src/interfaces/IPaymaster.sol"; @@ -118,7 +119,7 @@ contract PaymasterHubInvariantsTest is Test { } else { // Should fail vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.BudgetExceeded.selector); + vm.expectRevert(PaymasterHubErrors.BudgetExceeded.selector); hub.validatePaymasterUserOp(userOp, userOpHash, maxCost); } } @@ -136,7 +137,7 @@ contract PaymasterHubInvariantsTest is Test { PackedUserOperation memory userOp = _createUserOp(address(account)); vm.prank(caller); - vm.expectRevert(PaymasterHub.EPOnly.selector); + vm.expectRevert(PaymasterHubErrors.EPOnly.selector); hub.validatePaymasterUserOp(userOp, bytes32(0), 0.1 ether); } @@ -148,7 +149,7 @@ contract PaymasterHubInvariantsTest is Test { vm.assume(caller != address(0)); vm.prank(caller); - vm.expectRevert(PaymasterHub.EPOnly.selector); + vm.expectRevert(PaymasterHubErrors.EPOnly.selector); hub.postOp(IPaymaster.PostOpMode.opSucceeded, context, 0.01 ether); } @@ -271,30 +272,30 @@ contract PaymasterHubInvariantsTest is Test { vm.startPrank(caller); // Test operator function (setRule can be called by operator OR admin) - vm.expectRevert(PaymasterHub.NotOperator.selector); + vm.expectRevert(PaymasterHubErrors.NotOperator.selector); hub.setRule(address(0x1), bytes4(0x12345678), true, 100000); - vm.expectRevert(PaymasterHub.NotOperator.selector); + vm.expectRevert(PaymasterHubErrors.NotOperator.selector); hub.setBudget(bytes32(0), 1 ether, 1 days); - vm.expectRevert(PaymasterHub.NotOperator.selector); + vm.expectRevert(PaymasterHubErrors.NotOperator.selector); hub.setFeeCaps(100 gwei, 10 gwei, 1000000, 500000, 200000); - vm.expectRevert(PaymasterHub.NotAdmin.selector); + vm.expectRevert(PaymasterHubErrors.NotAdmin.selector); hub.setPause(true); - vm.expectRevert(PaymasterHub.NotAdmin.selector); + vm.expectRevert(PaymasterHubErrors.NotAdmin.selector); hub.setBounty(true, 0.1 ether, 1000); // Test depositToEntryPoint with a small amount that won't cause OutOfFunds vm.deal(caller, 0.01 ether); - vm.expectRevert(PaymasterHub.NotOperator.selector); + vm.expectRevert(PaymasterHubErrors.NotOperator.selector); hub.depositToEntryPoint{value: 0.01 ether}(); - vm.expectRevert(PaymasterHub.NotAdmin.selector); + vm.expectRevert(PaymasterHubErrors.NotAdmin.selector); hub.withdrawFromEntryPoint(payable(caller), 1 ether); - vm.expectRevert(PaymasterHub.NotAdmin.selector); + vm.expectRevert(PaymasterHubErrors.NotAdmin.selector); hub.sweepBounty(payable(caller), 1 ether); vm.stopPrank(); diff --git a/test/PaymasterHubSolidarity.t.sol b/test/PaymasterHubSolidarity.t.sol index 3fb5298..bf5e1af 100644 --- a/test/PaymasterHubSolidarity.t.sol +++ b/test/PaymasterHubSolidarity.t.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.24; import {Test, Vm, console2} from "forge-std/Test.sol"; import {PaymasterHub} from "../src/PaymasterHub.sol"; +import {PaymasterHubErrors} from "../src/libs/PaymasterHubErrors.sol"; +import {PaymasterHubLens} from "../src/PaymasterHubLens.sol"; import {IPaymaster} from "../src/interfaces/IPaymaster.sol"; import {IEntryPoint} from "../src/interfaces/IEntryPoint.sol"; import {PackedUserOperation, UserOpLib} from "../src/interfaces/PackedUserOperation.sol"; @@ -265,6 +267,7 @@ contract MockHats is IHats { */ contract PaymasterHubSolidarityTest is Test { PaymasterHub public hub; + PaymasterHubLens public lens; MockEntryPoint public entryPoint; MockHats public hats; @@ -301,6 +304,7 @@ contract PaymasterHubSolidarityTest is Test { abi.encodeWithSelector(PaymasterHub.initialize.selector, address(entryPoint), address(hats), poaManager); ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initData); hub = PaymasterHub(payable(address(proxy))); + lens = new PaymasterHubLens(address(hub)); // Setup hats hats.mintHat(ADMIN_HAT, orgAdmin); @@ -367,7 +371,7 @@ contract PaymasterHubSolidarityTest is Test { function testGracePeriodSpendingLimit() public view { // New org should be in grace period PaymasterHub.GracePeriodConfig memory grace = hub.getGracePeriodConfig(); - (bool inGrace, uint128 spendRemaining,,) = hub.getOrgGraceStatus(ORG_ALPHA); + (bool inGrace, uint128 spendRemaining,,) = lens.getOrgGraceStatus(ORG_ALPHA); assertTrue(inGrace); assertEq(spendRemaining, grace.maxSpendDuringGrace); @@ -377,7 +381,7 @@ contract PaymasterHubSolidarityTest is Test { // Fast forward past grace period vm.warp(block.timestamp + 91 days); - (bool inGrace,,,) = hub.getOrgGraceStatus(ORG_ALPHA); + (bool inGrace,,,) = lens.getOrgGraceStatus(ORG_ALPHA); assertFalse(inGrace); } @@ -399,7 +403,7 @@ contract PaymasterHubSolidarityTest is Test { function testGracePeriodConfigOnlyPoaManager() public { vm.prank(orgAdmin); - vm.expectRevert(PaymasterHub.NotPoaManager.selector); + vm.expectRevert(PaymasterHubErrors.NotPoaManager.selector); hub.setGracePeriodConfig(90, 0.01 ether, 0.003 ether); } @@ -417,7 +421,6 @@ contract PaymasterHubSolidarityTest is Test { // Check org financials PaymasterHub.OrgFinancials memory fin = hub.getOrgFinancials(ORG_ALPHA); assertEq(fin.deposited, depositAmount); - assertEq(fin.totalDeposited, depositAmount); assertEq(fin.spent, 0); assertEq(fin.solidarityUsedThisPeriod, 0); @@ -435,14 +438,13 @@ contract PaymasterHubSolidarityTest is Test { PaymasterHub.OrgFinancials memory fin = hub.getOrgFinancials(ORG_ALPHA); assertEq(fin.deposited, 0.01 ether); - assertEq(fin.totalDeposited, 0.01 ether); } function testDepositForNonExistentOrg() public { bytes32 fakeOrg = keccak256("FAKE"); vm.prank(user1); - vm.expectRevert(PaymasterHub.OrgNotRegistered.selector); + vm.expectRevert(PaymasterHubErrors.OrgNotRegistered.selector); hub.depositForOrg{value: 0.01 ether}(fakeOrg); } @@ -464,14 +466,14 @@ contract PaymasterHubSolidarityTest is Test { function testDepositForOrgOverflowReverts() public { vm.deal(user1, type(uint256).max); vm.prank(user1); - vm.expectRevert(PaymasterHub.Overflow.selector); + vm.expectRevert(PaymasterHubErrors.Overflow.selector); hub.depositForOrg{value: uint256(type(uint128).max) + 1}(ORG_ALPHA); } function testDonateToSolidarityOverflowReverts() public { vm.deal(user1, type(uint256).max); vm.prank(user1); - vm.expectRevert(PaymasterHub.Overflow.selector); + vm.expectRevert(PaymasterHubErrors.Overflow.selector); hub.donateToSolidarity{value: uint256(type(uint128).max) + 1}(); } @@ -529,7 +531,7 @@ contract PaymasterHubSolidarityTest is Test { vm.prank(user1); hub.depositForOrg{value: 0.003 ether}(ORG_ALPHA); - (,, bool requiresDeposit, uint256 solidarityLimit) = hub.getOrgGraceStatus(ORG_ALPHA); + (,, bool requiresDeposit, uint256 solidarityLimit) = lens.getOrgGraceStatus(ORG_ALPHA); assertFalse(requiresDeposit); assertEq(solidarityLimit, 0.006 ether); // 2x match } @@ -541,7 +543,7 @@ contract PaymasterHubSolidarityTest is Test { vm.prank(user1); hub.depositForOrg{value: 0.006 ether}(ORG_ALPHA); - (,,, uint256 solidarityLimit) = hub.getOrgGraceStatus(ORG_ALPHA); + (,,, uint256 solidarityLimit) = lens.getOrgGraceStatus(ORG_ALPHA); // First 0.003 at 2x = 0.006, second 0.003 at 1x = 0.003, total = 0.009 assertEq(solidarityLimit, 0.009 ether); } @@ -553,7 +555,7 @@ contract PaymasterHubSolidarityTest is Test { vm.prank(user1); hub.depositForOrg{value: 0.02 ether}(ORG_ALPHA); - (,,, uint256 solidarityLimit) = hub.getOrgGraceStatus(ORG_ALPHA); + (,,, uint256 solidarityLimit) = lens.getOrgGraceStatus(ORG_ALPHA); assertEq(solidarityLimit, 0); // No match for large deposits } @@ -564,7 +566,7 @@ contract PaymasterHubSolidarityTest is Test { vm.prank(user1); hub.depositForOrg{value: 0.002 ether}(ORG_ALPHA); - (,, bool requiresDeposit, uint256 solidarityLimit) = hub.getOrgGraceStatus(ORG_ALPHA); + (,, bool requiresDeposit, uint256 solidarityLimit) = lens.getOrgGraceStatus(ORG_ALPHA); assertTrue(requiresDeposit); // Below minimum assertEq(solidarityLimit, 0); // No match } @@ -598,7 +600,7 @@ contract PaymasterHubSolidarityTest is Test { function testBanOnlyPoaManager() public { vm.prank(orgAdmin); - vm.expectRevert(PaymasterHub.NotPoaManager.selector); + vm.expectRevert(PaymasterHubErrors.NotPoaManager.selector); hub.setBanFromSolidarity(ORG_ALPHA, true); } @@ -616,13 +618,13 @@ contract PaymasterHubSolidarityTest is Test { function testSolidarityFeeCapAt10Percent() public { vm.prank(poaManager); - vm.expectRevert(PaymasterHub.FeeTooHigh.selector); + vm.expectRevert(PaymasterHubErrors.FeeTooHigh.selector); hub.setSolidarityFee(1001); // >10% } function testSolidarityFeeOnlyPoaManager() public { vm.prank(orgAdmin); - vm.expectRevert(PaymasterHub.NotPoaManager.selector); + vm.expectRevert(PaymasterHubErrors.NotPoaManager.selector); hub.setSolidarityFee(200); } @@ -637,7 +639,6 @@ contract PaymasterHubSolidarityTest is Test { PaymasterHub.OrgFinancials memory fin = hub.getOrgFinancials(ORG_ALPHA); assertEq(fin.deposited, amount); - assertEq(fin.totalDeposited, amount); } function testFuzz_TierMatchCalculation(uint128 depositAmount) public { @@ -648,7 +649,7 @@ contract PaymasterHubSolidarityTest is Test { vm.deal(user1, depositAmount); hub.depositForOrg{value: depositAmount}(ORG_ALPHA); - (,,, uint256 solidarityLimit) = hub.getOrgGraceStatus(ORG_ALPHA); + (,,, uint256 solidarityLimit) = lens.getOrgGraceStatus(ORG_ALPHA); // Verify tier logic if (depositAmount <= 0.003 ether) { @@ -688,7 +689,7 @@ contract PaymasterHubSolidarityTest is Test { // (Note: Actual spending happens in postOp during validatePaymasterUserOp, // which requires full EntryPoint integration. This tests the state.) - (bool inGrace, uint128 spendRemaining,,) = hub.getOrgGraceStatus(ORG_ALPHA); + (bool inGrace, uint128 spendRemaining,,) = lens.getOrgGraceStatus(ORG_ALPHA); assertTrue(inGrace); assertEq(spendRemaining, 0.01 ether); @@ -699,7 +700,7 @@ contract PaymasterHubSolidarityTest is Test { hub.depositForOrg{value: 0.003 ether}(ORG_ALPHA); uint256 solidarityLimit; - (inGrace,,, solidarityLimit) = hub.getOrgGraceStatus(ORG_ALPHA); + (inGrace,,, solidarityLimit) = lens.getOrgGraceStatus(ORG_ALPHA); assertFalse(inGrace); assertEq(solidarityLimit, 0.006 ether); // 2x match @@ -714,7 +715,7 @@ contract PaymasterHubSolidarityTest is Test { vm.prank(user1); hub.depositForOrg{value: 0.006 ether}(ORG_ALPHA); - (,,, uint256 solidarityLimit) = hub.getOrgGraceStatus(ORG_ALPHA); + (,,, uint256 solidarityLimit) = lens.getOrgGraceStatus(ORG_ALPHA); assertEq(solidarityLimit, 0.009 ether); // First 0.003 at 2x + second 0.003 at 1x PaymasterHub.OrgFinancials memory fin = hub.getOrgFinancials(ORG_ALPHA); @@ -728,7 +729,7 @@ contract PaymasterHubSolidarityTest is Test { vm.prank(user1); hub.depositForOrg{value: 0.05 ether}(ORG_ALPHA); - (,,, uint256 solidarityLimit) = hub.getOrgGraceStatus(ORG_ALPHA); + (,,, uint256 solidarityLimit) = lens.getOrgGraceStatus(ORG_ALPHA); assertEq(solidarityLimit, 0); // No match for large deposits PaymasterHub.OrgFinancials memory fin = hub.getOrgFinancials(ORG_ALPHA); @@ -743,7 +744,7 @@ contract PaymasterHubSolidarityTest is Test { // Can't deposit next month, access cut off vm.warp(block.timestamp + 91 days); - (,, bool requiresDeposit,) = hub.getOrgGraceStatus(ORG_ALPHA); + (,, bool requiresDeposit,) = lens.getOrgGraceStatus(ORG_ALPHA); assertFalse(requiresDeposit); // Still has initial deposit // But if deposit was spent and not replenished... @@ -780,13 +781,13 @@ contract PaymasterHubSolidarityTest is Test { function testCannotDepositZero() public { vm.prank(user1); - vm.expectRevert(PaymasterHub.ZeroAmount.selector); + vm.expectRevert(PaymasterHubErrors.ZeroAmount.selector); hub.depositForOrg{value: 0}(ORG_ALPHA); } function testCannotDonateZero() public { vm.prank(user1); - vm.expectRevert(PaymasterHub.ZeroAmount.selector); + vm.expectRevert(PaymasterHubErrors.ZeroAmount.selector); hub.donateToSolidarity{value: 0}(); } @@ -836,7 +837,7 @@ contract PaymasterHubSolidarityTest is Test { hub.depositForOrg{value: 0.003 ether}(ORG_ALPHA); // Check eligible for Tier 1 - (,, bool requiresDeposit1, uint256 solidarityLimit1) = hub.getOrgGraceStatus(ORG_ALPHA); + (,, bool requiresDeposit1, uint256 solidarityLimit1) = lens.getOrgGraceStatus(ORG_ALPHA); assertFalse(requiresDeposit1); assertEq(solidarityLimit1, 0.006 ether); // 2x match @@ -859,7 +860,7 @@ contract PaymasterHubSolidarityTest is Test { hub.depositForOrg{value: 0.003 ether}(ORG_ALPHA); // Check eligible - (,, bool requiresDeposit1, uint256 solidarityLimit1) = hub.getOrgGraceStatus(ORG_ALPHA); + (,, bool requiresDeposit1, uint256 solidarityLimit1) = lens.getOrgGraceStatus(ORG_ALPHA); assertFalse(requiresDeposit1); assertEq(solidarityLimit1, 0.006 ether); @@ -868,7 +869,7 @@ contract PaymasterHubSolidarityTest is Test { hub.depositForOrg{value: 0.0015 ether}(ORG_ALPHA); // Check still Tier 1 (balance = 0.0045 ETH, which is > minDeposit but < 2x minDeposit) - (,, bool requiresDeposit2, uint256 solidarityLimit2) = hub.getOrgGraceStatus(ORG_ALPHA); + (,, bool requiresDeposit2, uint256 solidarityLimit2) = lens.getOrgGraceStatus(ORG_ALPHA); assertFalse(requiresDeposit2); // Should get 2x match on 0.003 (first tier) + 1x match on 0.0015 (second tier) @@ -894,7 +895,7 @@ contract PaymasterHubSolidarityTest is Test { assertEq(fin1.spent, 0); // Check eligible for $20 match - (,, bool requiresDeposit1, uint256 solidarityLimit1) = hub.getOrgGraceStatus(ORG_ALPHA); + (,, bool requiresDeposit1, uint256 solidarityLimit1) = lens.getOrgGraceStatus(ORG_ALPHA); assertFalse(requiresDeposit1); assertEq(solidarityLimit1, 0.006 ether); @@ -913,7 +914,7 @@ contract PaymasterHubSolidarityTest is Test { vm.warp(block.timestamp + 91 days); // Without any deposits, should require deposit and have no match - (,, bool requiresDeposit1, uint256 match1) = hub.getOrgGraceStatus(ORG_ALPHA); + (,, bool requiresDeposit1, uint256 match1) = lens.getOrgGraceStatus(ORG_ALPHA); assertTrue(requiresDeposit1); assertEq(match1, 0); @@ -921,7 +922,7 @@ contract PaymasterHubSolidarityTest is Test { vm.prank(user1); hub.depositForOrg{value: 0.003 ether}(ORG_ALPHA); - (,, bool requiresDeposit2, uint256 match2) = hub.getOrgGraceStatus(ORG_ALPHA); + (,, bool requiresDeposit2, uint256 match2) = lens.getOrgGraceStatus(ORG_ALPHA); assertFalse(requiresDeposit2); assertEq(match2, 0.006 ether); // 2x match on available balance } @@ -934,7 +935,7 @@ contract PaymasterHubSolidarityTest is Test { vm.prank(user1); hub.depositForOrg{value: 0.003 ether}(ORG_ALPHA); - (,, bool requiresDeposit1, uint256 limit1) = hub.getOrgGraceStatus(ORG_ALPHA); + (,, bool requiresDeposit1, uint256 limit1) = lens.getOrgGraceStatus(ORG_ALPHA); assertFalse(requiresDeposit1); assertEq(limit1, 0.006 ether); // 2x match @@ -942,7 +943,7 @@ contract PaymasterHubSolidarityTest is Test { vm.prank(user1); hub.depositForOrg{value: 0.003 ether}(ORG_ALPHA); - (,, bool requiresDeposit2, uint256 limit2) = hub.getOrgGraceStatus(ORG_ALPHA); + (,, bool requiresDeposit2, uint256 limit2) = lens.getOrgGraceStatus(ORG_ALPHA); assertFalse(requiresDeposit2); assertEq(limit2, 0.009 ether); // 0.006 (first tier 2x) + 0.003 (second tier 1x) @@ -950,7 +951,7 @@ contract PaymasterHubSolidarityTest is Test { vm.prank(user1); hub.depositForOrg{value: 0.011 ether}(ORG_ALPHA); - (,, bool requiresDeposit3, uint256 limit3) = hub.getOrgGraceStatus(ORG_ALPHA); + (,, bool requiresDeposit3, uint256 limit3) = lens.getOrgGraceStatus(ORG_ALPHA); assertFalse(requiresDeposit3); assertEq(limit3, 0); // No match for self-sufficient orgs } @@ -964,7 +965,7 @@ contract PaymasterHubSolidarityTest is Test { hub.depositForOrg{value: 0.002 ether}(ORG_ALPHA); // Should require deposit and have no match - (,, bool requiresDeposit, uint256 solidarityLimit) = hub.getOrgGraceStatus(ORG_ALPHA); + (,, bool requiresDeposit, uint256 solidarityLimit) = lens.getOrgGraceStatus(ORG_ALPHA); assertTrue(requiresDeposit); assertEq(solidarityLimit, 0); } @@ -977,7 +978,7 @@ contract PaymasterHubSolidarityTest is Test { vm.prank(user1); hub.depositForOrg{value: 0.003 ether}(ORG_ALPHA); - (,, bool requiresDeposit, uint256 solidarityLimit) = hub.getOrgGraceStatus(ORG_ALPHA); + (,, bool requiresDeposit, uint256 solidarityLimit) = lens.getOrgGraceStatus(ORG_ALPHA); assertFalse(requiresDeposit); assertEq(solidarityLimit, 0.006 ether); // 2x match } @@ -1001,13 +1002,13 @@ contract PaymasterHubSolidarityTest is Test { function testPauseDistributionOnlyPoaManager() public { vm.prank(orgAdmin); - vm.expectRevert(PaymasterHub.NotPoaManager.selector); + vm.expectRevert(PaymasterHubErrors.NotPoaManager.selector); hub.pauseSolidarityDistribution(); } function testUnpauseDistributionOnlyPoaManager() public { vm.prank(orgAdmin); - vm.expectRevert(PaymasterHub.NotPoaManager.selector); + vm.expectRevert(PaymasterHubErrors.NotPoaManager.selector); hub.unpauseSolidarityDistribution(); } @@ -1154,7 +1155,7 @@ contract PaymasterHubSolidarityTest is Test { // ORG_ALPHA was just registered, so it's in grace period (bool inGrace, uint128 spendRemaining, bool requiresDeposit, uint256 solidarityLimit) = - hub.getOrgGraceStatus(ORG_ALPHA); + lens.getOrgGraceStatus(ORG_ALPHA); // inGrace still reflects the time-based status (useful info) assertTrue(inGrace); @@ -1175,7 +1176,7 @@ contract PaymasterHubSolidarityTest is Test { hub.depositForOrg{value: 0.003 ether}(ORG_ALPHA); (bool inGrace, uint128 spendRemaining, bool requiresDeposit, uint256 solidarityLimit) = - hub.getOrgGraceStatus(ORG_ALPHA); + lens.getOrgGraceStatus(ORG_ALPHA); assertFalse(inGrace); assertEq(spendRemaining, 0); @@ -1186,7 +1187,7 @@ contract PaymasterHubSolidarityTest is Test { function testGetOrgGraceStatus_UnpausedShowsNormalValues() public view { // setUp already unpaused, so this should show normal grace values (bool inGrace, uint128 spendRemaining, bool requiresDeposit, uint256 solidarityLimit) = - hub.getOrgGraceStatus(ORG_ALPHA); + lens.getOrgGraceStatus(ORG_ALPHA); assertTrue(inGrace); assertEq(spendRemaining, 0.01 ether); // Full grace spending available @@ -1204,14 +1205,14 @@ contract PaymasterHubSolidarityTest is Test { vm.prank(poaManager); hub.pauseSolidarityDistribution(); - (,,, uint256 limit1) = hub.getOrgGraceStatus(ORG_ALPHA); + (,,, uint256 limit1) = lens.getOrgGraceStatus(ORG_ALPHA); assertEq(limit1, 0); // Unpause — match restored vm.prank(poaManager); hub.unpauseSolidarityDistribution(); - (,,, uint256 limit2) = hub.getOrgGraceStatus(ORG_ALPHA); + (,,, uint256 limit2) = lens.getOrgGraceStatus(ORG_ALPHA); assertEq(limit2, 0.006 ether); // 2x match restored } @@ -1221,7 +1222,7 @@ contract PaymasterHubSolidarityTest is Test { bytes32 newOrgId = keccak256("UNAUTHORIZED_ORG"); vm.prank(orgAdmin); - vm.expectRevert(PaymasterHub.NotPoaManager.selector); + vm.expectRevert(PaymasterHubErrors.NotPoaManager.selector); hub.registerOrg(newOrgId, ADMIN_HAT, OPERATOR_HAT); } @@ -1229,7 +1230,7 @@ contract PaymasterHubSolidarityTest is Test { bytes32 newOrgId = keccak256("RANDOM_ORG"); vm.prank(user1); - vm.expectRevert(PaymasterHub.NotPoaManager.selector); + vm.expectRevert(PaymasterHubErrors.NotPoaManager.selector); hub.registerOrg(newOrgId, ADMIN_HAT, OPERATOR_HAT); } @@ -1250,7 +1251,7 @@ contract PaymasterHubSolidarityTest is Test { function testSetOrgRegistrarUnauthorizedReverts() public { vm.prank(orgAdmin); - vm.expectRevert(PaymasterHub.NotPoaManager.selector); + vm.expectRevert(PaymasterHubErrors.NotPoaManager.selector); hub.setOrgRegistrar(address(0x99)); } @@ -1260,7 +1261,7 @@ contract PaymasterHubSolidarityTest is Test { function testGracePeriodZeroDepositOrgGraceStatus() public { // A newly registered org with zero deposits should be in grace - (bool inGrace, uint128 spendRemaining,,) = hub.getOrgGraceStatus(ORG_ALPHA); + (bool inGrace, uint128 spendRemaining,,) = lens.getOrgGraceStatus(ORG_ALPHA); assertTrue(inGrace, "New org should be in grace period"); PaymasterHub.OrgFinancials memory fin = hub.getOrgFinancials(ORG_ALPHA); @@ -1272,7 +1273,7 @@ contract PaymasterHubSolidarityTest is Test { // Warp past grace period (default 90 days) vm.warp(block.timestamp + 91 days); - (bool inGrace,,,) = hub.getOrgGraceStatus(ORG_ALPHA); + (bool inGrace,,,) = lens.getOrgGraceStatus(ORG_ALPHA); assertFalse(inGrace, "Org should no longer be in grace after 91 days"); // Verify the org still has zero deposits @@ -1300,7 +1301,7 @@ contract PaymasterHubSolidarityTest is Test { vm.prank(user1); hub.depositForOrg{value: smallDeposit}(ORG_ALPHA); - (bool inGrace,,,) = hub.getOrgGraceStatus(ORG_ALPHA); + (bool inGrace,,,) = lens.getOrgGraceStatus(ORG_ALPHA); assertTrue(inGrace, "Org should still be in grace"); PaymasterHub.OrgFinancials memory fin = hub.getOrgFinancials(ORG_ALPHA); @@ -1365,7 +1366,7 @@ contract PaymasterHubSolidarityTest is Test { PackedUserOperation memory userOp = _buildUserOp(address(0xdead), "", pmData); userOp.initCode = hex"01"; vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.InvalidOnboardingRequest.selector); + vm.expectRevert(PaymasterHubErrors.InvalidOnboardingRequest.selector); hub.validatePaymasterUserOp(userOp, keccak256("hash"), MAX_COST); } @@ -1446,7 +1447,7 @@ contract PaymasterHubSolidarityTest is Test { PackedUserOperation memory userOp3 = _buildUserOp(account3, "", pmData3); userOp3.initCode = hex"01"; vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.OnboardingDailyLimitExceeded.selector); + vm.expectRevert(PaymasterHubErrors.OnboardingDailyLimitExceeded.selector); hub.validatePaymasterUserOp(userOp3, keccak256("hash3"), MAX_COST); } @@ -1486,7 +1487,7 @@ contract PaymasterHubSolidarityTest is Test { PackedUserOperation memory userOp = _buildUserOp(deployed, execCallData, pmData); userOp.initCode = hex"01"; vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.InvalidOnboardingRequest.selector); + vm.expectRevert(PaymasterHubErrors.InvalidOnboardingRequest.selector); hub.validatePaymasterUserOp(userOp, keccak256("hash"), MAX_COST); } @@ -1506,7 +1507,7 @@ contract PaymasterHubSolidarityTest is Test { PackedUserOperation memory userOp = _buildUserOp(deployed, execCallData, pmData); userOp.initCode = hex"01"; vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.InvalidOnboardingRequest.selector); + vm.expectRevert(PaymasterHubErrors.InvalidOnboardingRequest.selector); hub.validatePaymasterUserOp(userOp, keccak256("hash"), MAX_COST); } @@ -1525,7 +1526,7 @@ contract PaymasterHubSolidarityTest is Test { PackedUserOperation memory userOp = _buildUserOp(deployed, badCallData, pmData); userOp.initCode = hex"01"; vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.InvalidOnboardingRequest.selector); + vm.expectRevert(PaymasterHubErrors.InvalidOnboardingRequest.selector); hub.validatePaymasterUserOp(userOp, keccak256("hash"), MAX_COST); } @@ -1597,7 +1598,7 @@ contract PaymasterHubSolidarityTest is Test { // Third deploy should revert PackedUserOperation memory userOp3 = _buildUserOp(sender, callData, pmData); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.OrgDeployLimitExceeded.selector); + vm.expectRevert(PaymasterHubErrors.OrgDeployLimitExceeded.selector); hub.validatePaymasterUserOp(userOp3, keccak256("h3"), MAX_COST); } @@ -1625,7 +1626,7 @@ contract PaymasterHubSolidarityTest is Test { address sender2 = address(new DummySender()); PackedUserOperation memory userOp2 = _buildUserOp(sender2, callData, pmData); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.OrgDeployDailyLimitExceeded.selector); + vm.expectRevert(PaymasterHubErrors.OrgDeployDailyLimitExceeded.selector); hub.validatePaymasterUserOp(userOp2, keccak256("h2"), MAX_COST); } @@ -1640,7 +1641,7 @@ contract PaymasterHubSolidarityTest is Test { PackedUserOperation memory userOp = _buildUserOp(sender, callData, pmData); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.GasTooHigh.selector); + vm.expectRevert(PaymasterHubErrors.GasTooHigh.selector); hub.validatePaymasterUserOp(userOp, keccak256("hash"), MAX_COST + 1); } @@ -1659,7 +1660,7 @@ contract PaymasterHubSolidarityTest is Test { PackedUserOperation memory userOp = _buildUserOp(sender, callData, pmData); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.OrgDeployDisabled.selector); + vm.expectRevert(PaymasterHubErrors.OrgDeployDisabled.selector); hub.validatePaymasterUserOp(userOp, keccak256("hash"), MAX_COST); } @@ -1673,7 +1674,7 @@ contract PaymasterHubSolidarityTest is Test { PackedUserOperation memory userOp = _buildUserOp(address(0xdead), callData, pmData); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.InvalidOrgDeployRequest.selector); + vm.expectRevert(PaymasterHubErrors.InvalidOrgDeployRequest.selector); hub.validatePaymasterUserOp(userOp, keccak256("hash"), MAX_COST); } @@ -1688,7 +1689,7 @@ contract PaymasterHubSolidarityTest is Test { PackedUserOperation memory userOp = _buildUserOp(address(0xdead), callData, pmData); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.InvalidOrgDeployRequest.selector); + vm.expectRevert(PaymasterHubErrors.InvalidOrgDeployRequest.selector); hub.validatePaymasterUserOp(userOp, keccak256("hash"), MAX_COST); } @@ -1704,7 +1705,7 @@ contract PaymasterHubSolidarityTest is Test { userOp.initCode = hex"01"; vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.InvalidOrgDeployRequest.selector); + vm.expectRevert(PaymasterHubErrors.InvalidOrgDeployRequest.selector); hub.validatePaymasterUserOp(userOp, keccak256("hash"), MAX_COST); } @@ -1721,7 +1722,7 @@ contract PaymasterHubSolidarityTest is Test { PackedUserOperation memory userOp = _buildUserOp(sender, callData, pmData); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.InvalidOrgDeployRequest.selector); + vm.expectRevert(PaymasterHubErrors.InvalidOrgDeployRequest.selector); hub.validatePaymasterUserOp(userOp, keccak256("hash"), MAX_COST); } @@ -1738,7 +1739,7 @@ contract PaymasterHubSolidarityTest is Test { PackedUserOperation memory userOp = _buildUserOp(sender, callData, pmData); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.InvalidOrgDeployRequest.selector); + vm.expectRevert(PaymasterHubErrors.InvalidOrgDeployRequest.selector); hub.validatePaymasterUserOp(userOp, keccak256("hash"), MAX_COST); } @@ -1758,7 +1759,7 @@ contract PaymasterHubSolidarityTest is Test { PackedUserOperation memory userOp = _buildUserOp(sender, callData, pmData); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.SolidarityDistributionIsPaused.selector); + vm.expectRevert(PaymasterHubErrors.SolidarityDistributionIsPaused.selector); hub.validatePaymasterUserOp(userOp, keccak256("hash"), MAX_COST); } @@ -1778,7 +1779,7 @@ contract PaymasterHubSolidarityTest is Test { PackedUserOperation memory userOp = _buildUserOp(sender, callData, pmData); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.InsufficientFunds.selector); + vm.expectRevert(PaymasterHubErrors.InsufficientFunds.selector); hub.validatePaymasterUserOp(userOp, keccak256("hash"), MAX_COST); } @@ -1820,7 +1821,7 @@ contract PaymasterHubSolidarityTest is Test { // Non-poaManager should revert vm.prank(user1); - vm.expectRevert(PaymasterHub.NotPoaManager.selector); + vm.expectRevert(PaymasterHubErrors.NotPoaManager.selector); hub.setOrgDeployConfig(0.1 ether, 50, 3, true, deployer); // PoaManager can set @@ -1859,7 +1860,7 @@ contract PaymasterHubSolidarityTest is Test { address sender2 = address(new DummySender()); PackedUserOperation memory userOp2 = _buildUserOp(sender2, callData, pmData); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.OrgDeployDailyLimitExceeded.selector); + vm.expectRevert(PaymasterHubErrors.OrgDeployDailyLimitExceeded.selector); hub.validatePaymasterUserOp(userOp2, keccak256("h2"), MAX_COST); // Warp to next day @@ -1883,7 +1884,7 @@ contract PaymasterHubSolidarityTest is Test { PackedUserOperation memory userOp = _buildUserOp(sender, callData, pmData); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.InvalidOrgDeployRequest.selector); + vm.expectRevert(PaymasterHubErrors.InvalidOrgDeployRequest.selector); hub.validatePaymasterUserOp(userOp, keccak256("hash"), MAX_COST); } @@ -1913,7 +1914,7 @@ contract PaymasterHubSolidarityTest is Test { vm.prank(poaManager); vm.expectEmit(false, false, false, true); - emit PaymasterHub.OrgDeployConfigUpdated(0.1 ether, 50, 3, true, deployer); + emit PaymasterHubErrors.OrgDeployConfigUpdated(0.1 ether, 50, 3, true, deployer); hub.setOrgDeployConfig(0.1 ether, 50, 3, true, deployer); } @@ -2076,7 +2077,7 @@ contract PaymasterHubSolidarityTest is Test { PackedUserOperation memory userOp = _buildUserOp(sender, callData, pmData); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.InvalidOrgDeployRequest.selector); + vm.expectRevert(PaymasterHubErrors.InvalidOrgDeployRequest.selector); hub.validatePaymasterUserOp(userOp, keccak256("hash"), MAX_COST); } @@ -2095,7 +2096,7 @@ contract PaymasterHubSolidarityTest is Test { PackedUserOperation memory userOp = _buildUserOp(sender, callData, pmData); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.OrgDeployLimitExceeded.selector); + vm.expectRevert(PaymasterHubErrors.OrgDeployLimitExceeded.selector); hub.validatePaymasterUserOp(userOp, keccak256("hash"), MAX_COST); } @@ -2109,7 +2110,7 @@ contract PaymasterHubSolidarityTest is Test { PackedUserOperation memory userOp = _buildUserOp(address(new DummySender()), callData, pmData); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.InvalidOrgDeployRequest.selector); + vm.expectRevert(PaymasterHubErrors.InvalidOrgDeployRequest.selector); hub.validatePaymasterUserOp(userOp, keccak256("hash"), MAX_COST); } @@ -2123,7 +2124,7 @@ contract PaymasterHubSolidarityTest is Test { PackedUserOperation memory userOp = _buildUserOp(sender, "", pmData); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.InvalidOrgDeployRequest.selector); + vm.expectRevert(PaymasterHubErrors.InvalidOrgDeployRequest.selector); hub.validatePaymasterUserOp(userOp, keccak256("hash"), MAX_COST); } @@ -2139,7 +2140,7 @@ contract PaymasterHubSolidarityTest is Test { PackedUserOperation memory userOp = _buildUserOp(sender, callData, pmData); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.InvalidOrgDeployRequest.selector); + vm.expectRevert(PaymasterHubErrors.InvalidOrgDeployRequest.selector); hub.validatePaymasterUserOp(userOp, keccak256("hash"), MAX_COST); } @@ -2165,7 +2166,7 @@ contract PaymasterHubSolidarityTest is Test { // sender1 is blocked PackedUserOperation memory blockedOp = _buildUserOp(sender1, callData, pmData); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.OrgDeployLimitExceeded.selector); + vm.expectRevert(PaymasterHubErrors.OrgDeployLimitExceeded.selector); hub.validatePaymasterUserOp(blockedOp, keccak256("blocked"), MAX_COST); // sender2 should still succeed (independent counter) @@ -2202,7 +2203,7 @@ contract PaymasterHubSolidarityTest is Test { // Second validation from SAME sender should fail (counter is already 1 >= limit of 1) PackedUserOperation memory userOp2 = _buildUserOp(sender, callData, pmData); vm.prank(address(entryPoint)); - vm.expectRevert(PaymasterHub.OrgDeployLimitExceeded.selector); + vm.expectRevert(PaymasterHubErrors.OrgDeployLimitExceeded.selector); hub.validatePaymasterUserOp(userOp2, keccak256("h2"), MAX_COST); } @@ -2251,7 +2252,7 @@ contract PaymasterHubSolidarityTest is Test { /// @notice registerOrg must reject bytes32(0) as orgId function testRegisterOrgRejectsZeroOrgId() public { vm.prank(poaManager); - vm.expectRevert(PaymasterHub.InvalidOrgId.selector); + vm.expectRevert(PaymasterHubErrors.InvalidOrgId.selector); hub.registerOrg(bytes32(0), ADMIN_HAT, OPERATOR_HAT); } @@ -2272,7 +2273,7 @@ contract PaymasterHubSolidarityTest is Test { _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); + vm.expectRevert(PaymasterHubErrors.InsufficientFunds.selector); hub.validatePaymasterUserOp(userOp, keccak256("hash"), 2 ether); } } @@ -2340,7 +2341,7 @@ contract PaymasterHubBalanceCheckTest is Test { function testCheckOrgBalance_ZeroDeposit_PostGrace_Reverts() public { vm.warp(block.timestamp + 91 days); - vm.expectRevert(PaymasterHub.InsufficientOrgBalance.selector); + vm.expectRevert(PaymasterHubErrors.InsufficientOrgBalance.selector); hub.exposed_checkOrgBalance(ORG_A, 0.001 ether); } @@ -2349,7 +2350,7 @@ contract PaymasterHubBalanceCheckTest is Test { hub.pauseSolidarityDistribution(); // Even in grace, paused distribution means org must cover 100% from deposits - vm.expectRevert(PaymasterHub.InsufficientOrgBalance.selector); + vm.expectRevert(PaymasterHubErrors.InsufficientOrgBalance.selector); hub.exposed_checkOrgBalance(ORG_A, 0.001 ether); } @@ -2415,7 +2416,7 @@ contract PaymasterHubBalanceCheckTest is Test { if (depositCovers) { hub.exposed_checkOrgBalance(ORG_A, maxCost); } else { - vm.expectRevert(PaymasterHub.InsufficientOrgBalance.selector); + vm.expectRevert(PaymasterHubErrors.InsufficientOrgBalance.selector); hub.exposed_checkOrgBalance(ORG_A, maxCost); } } else if (depositCovers) { @@ -2428,7 +2429,7 @@ contract PaymasterHubBalanceCheckTest is Test { hub.exposed_checkOrgBalance(ORG_A, maxCost); } else { // Zero deposit, post grace — should revert - vm.expectRevert(PaymasterHub.InsufficientOrgBalance.selector); + vm.expectRevert(PaymasterHubErrors.InsufficientOrgBalance.selector); hub.exposed_checkOrgBalance(ORG_A, maxCost); } } @@ -2443,7 +2444,7 @@ contract PaymasterHubBalanceCheckTest is Test { function testCheckSolidarityAccess_InGrace_ExceedsMaxSpend_Reverts() public { PaymasterHub.GracePeriodConfig memory grace = hub.getGracePeriodConfig(); - vm.expectRevert(PaymasterHub.GracePeriodSpendLimitReached.selector); + vm.expectRevert(PaymasterHubErrors.GracePeriodSpendLimitReached.selector); hub.exposed_checkSolidarityAccess(ORG_A, grace.maxSpendDuringGrace + 1); } @@ -2451,7 +2452,7 @@ contract PaymasterHubBalanceCheckTest is Test { vm.warp(block.timestamp + 91 days); // No deposit — below min required - vm.expectRevert(PaymasterHub.InsufficientDepositForSolidarity.selector); + vm.expectRevert(PaymasterHubErrors.InsufficientDepositForSolidarity.selector); hub.exposed_checkSolidarityAccess(ORG_A, 0.001 ether); } @@ -2468,7 +2469,7 @@ contract PaymasterHubBalanceCheckTest is Test { vm.prank(poaManager); hub.setBanFromSolidarity(ORG_A, true); - vm.expectRevert(PaymasterHub.OrgIsBanned.selector); + vm.expectRevert(PaymasterHubErrors.OrgIsBanned.selector); hub.exposed_checkSolidarityAccess(ORG_A, 0.001 ether); } @@ -2492,7 +2493,7 @@ contract PaymasterHubBalanceCheckTest is Test { vm.warp(block.timestamp + 91 days); // Balance check fails first (same order as validatePaymasterUserOp) - vm.expectRevert(PaymasterHub.InsufficientOrgBalance.selector); + vm.expectRevert(PaymasterHubErrors.InsufficientOrgBalance.selector); hub.exposed_checkOrgBalance(ORG_A, 0.001 ether); } @@ -2520,7 +2521,7 @@ contract PaymasterHubBalanceCheckTest is Test { // At exact boundary: block.timestamp == graceEndTime, NOT < graceEndTime // So inInitialGrace is false — should revert - vm.expectRevert(PaymasterHub.InsufficientOrgBalance.selector); + vm.expectRevert(PaymasterHubErrors.InsufficientOrgBalance.selector); hub.exposed_checkOrgBalance(ORG_A, 0.001 ether); } diff --git a/test/PaymasterLibsUnit.t.sol b/test/PaymasterLibsUnit.t.sol new file mode 100644 index 0000000..51e9784 --- /dev/null +++ b/test/PaymasterLibsUnit.t.sol @@ -0,0 +1,436 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {PaymasterGraceLib} from "../src/libs/PaymasterGraceLib.sol"; +import {PaymasterPostOpLib} from "../src/libs/PaymasterPostOpLib.sol"; +import {PaymasterCalldataLib} from "../src/libs/PaymasterCalldataLib.sol"; + +// ============ Wrapper Contracts ============ +// Libraries with internal functions need wrappers so tests can call them externally. + +contract GraceLibWrapper { + function isInGracePeriod(uint40 registeredAt, uint32 initialGraceDays) external view returns (bool) { + return PaymasterGraceLib.isInGracePeriod(registeredAt, initialGraceDays); + } + + function solidarityFee(uint256 actualGasCost, uint16 feePercentageBps, uint40 registeredAt, uint32 initialGraceDays) + external + view + returns (uint256) + { + return PaymasterGraceLib.solidarityFee(actualGasCost, feePercentageBps, registeredAt, initialGraceDays); + } + + function calculateMatchAllowance(uint256 deposited, uint256 minDeposit) external pure returns (uint256) { + return PaymasterGraceLib.calculateMatchAllowance(deposited, minDeposit); + } +} + +contract PostOpLibWrapper { + function adjustBudget(uint128 usedInEpoch, uint256 reserved, uint256 actual) external pure returns (uint128) { + return PaymasterPostOpLib.adjustBudget(usedInEpoch, reserved, actual); + } + + function clampedDeduction(uint128 balance, uint256 cost) + external + pure + returns (uint128 newBalance, uint128 deducted) + { + return PaymasterPostOpLib.clampedDeduction(balance, cost); + } +} + +contract CalldataLibWrapper { + function parseExecuteCall(bytes calldata callData, address expectedTarget) + external + pure + returns (bool valid, bytes4 innerSelector) + { + return PaymasterCalldataLib.parseExecuteCall(callData, expectedTarget); + } +} + +// ============ PaymasterGraceLib Tests ============ + +contract PaymasterGraceLibTest is Test { + GraceLibWrapper lib; + + // Use a realistic timestamp so arithmetic doesn't underflow + uint256 constant START_TIME = 1_700_000_000; // Nov 2023 + + function setUp() public { + lib = new GraceLibWrapper(); + vm.warp(START_TIME); + } + + // -------- isInGracePeriod -------- + + function testIsInGracePeriod_DuringGrace() public view { + uint40 registeredAt = uint40(block.timestamp); + assertTrue(lib.isInGracePeriod(registeredAt, 90)); + } + + function testIsInGracePeriod_ExactlyAtExpiry() public view { + uint40 registeredAt = uint40(block.timestamp - 90 days); + // At exactly the boundary, grace has ended (not strictly less than) + assertFalse(lib.isInGracePeriod(registeredAt, 90)); + } + + function testIsInGracePeriod_OneSecondBeforeExpiry() public view { + uint40 registeredAt = uint40(block.timestamp - 90 days + 1); + assertTrue(lib.isInGracePeriod(registeredAt, 90)); + } + + function testIsInGracePeriod_AfterExpiry() public view { + uint40 registeredAt = uint40(block.timestamp - 91 days); + assertFalse(lib.isInGracePeriod(registeredAt, 90)); + } + + function testIsInGracePeriod_ZeroGraceDays() public view { + uint40 registeredAt = uint40(block.timestamp); + // 0 grace days means grace ends immediately at registeredAt + assertFalse(lib.isInGracePeriod(registeredAt, 0)); + } + + function testIsInGracePeriod_ZeroRegisteredAt() public view { + // registeredAt=0 with 90 grace days: graceEnd = 90 days = 7,776,000 + // At realistic timestamp this is long expired + assertFalse(lib.isInGracePeriod(0, 90)); + } + + function testIsInGracePeriod_ZeroBoth() public view { + // registeredAt=0 + 0 days = 0, block.timestamp > 0 + assertFalse(lib.isInGracePeriod(0, 0)); + } + + function testIsInGracePeriod_MaxGraceDays() public view { + uint40 registeredAt = uint40(block.timestamp); + // Max uint32 days = ~11.7 million years, should be in grace + assertTrue(lib.isInGracePeriod(registeredAt, type(uint32).max)); + } + + function testFuzz_IsInGracePeriod(uint40 registeredAt, uint32 graceDays) public view { + // Skip if registeredAt is in the future (unrealistic) + vm.assume(registeredAt <= block.timestamp); + bool result = lib.isInGracePeriod(registeredAt, graceDays); + uint256 graceEnd = uint256(registeredAt) + uint256(graceDays) * 1 days; + assertEq(result, block.timestamp < graceEnd); + } + + // -------- solidarityFee -------- + + function testSolidarityFee_DuringGraceIsZero() public view { + uint40 registeredAt = uint40(block.timestamp); + uint256 fee = lib.solidarityFee(1 ether, 100, registeredAt, 90); + assertEq(fee, 0); + } + + function testSolidarityFee_AfterGraceCalculatesFee() public view { + uint40 registeredAt = uint40(block.timestamp - 91 days); + // 1 ether * 100 bps / 10000 = 0.01 ether + uint256 fee = lib.solidarityFee(1 ether, 100, registeredAt, 90); + assertEq(fee, 0.01 ether); + } + + function testSolidarityFee_ZeroBpsIsZero() public view { + uint40 registeredAt = uint40(block.timestamp - 91 days); + uint256 fee = lib.solidarityFee(1 ether, 0, registeredAt, 90); + assertEq(fee, 0); + } + + function testSolidarityFee_ZeroCostIsZero() public view { + uint40 registeredAt = uint40(block.timestamp - 91 days); + uint256 fee = lib.solidarityFee(0, 100, registeredAt, 90); + assertEq(fee, 0); + } + + function testSolidarityFee_MaxBps() public view { + uint40 registeredAt = uint40(block.timestamp - 91 days); + // 10000 bps = 100% + uint256 fee = lib.solidarityFee(1 ether, 10000, registeredAt, 90); + assertEq(fee, 1 ether); + } + + function testSolidarityFee_PrecisionLoss() public view { + uint40 registeredAt = uint40(block.timestamp - 91 days); + // 1 wei * 1 bps / 10000 = 0 (rounds down) + uint256 fee = lib.solidarityFee(1, 1, registeredAt, 90); + assertEq(fee, 0); + } + + function testFuzz_SolidarityFee_AfterGrace(uint128 gasCost, uint16 bps) public view { + vm.assume(bps <= 10000); + uint40 registeredAt = uint40(block.timestamp - 91 days); + uint256 fee = lib.solidarityFee(gasCost, bps, registeredAt, 90); + assertEq(fee, (uint256(gasCost) * uint256(bps)) / 10000); + } + + // -------- calculateMatchAllowance -------- + + function testMatchAllowance_ZeroMinDeposit() public pure { + assertEq(PaymasterGraceLib.calculateMatchAllowance(1 ether, 0), 0); + } + + function testMatchAllowance_BelowMinDeposit() public pure { + assertEq(PaymasterGraceLib.calculateMatchAllowance(0.002 ether, 0.003 ether), 0); + } + + function testMatchAllowance_ExactlyMinDeposit() public pure { + // Tier 1: deposit == minDeposit → 2x match + assertEq(PaymasterGraceLib.calculateMatchAllowance(0.003 ether, 0.003 ether), 0.006 ether); + } + + function testMatchAllowance_BetweenMinAndTwoX() public pure { + // Tier 2: minDeposit < deposit <= 2x minDeposit + // First tier: 0.003 * 2 = 0.006; Second tier: (0.005 - 0.003) = 0.002 + // Total = 0.008 + assertEq(PaymasterGraceLib.calculateMatchAllowance(0.005 ether, 0.003 ether), 0.008 ether); + } + + function testMatchAllowance_ExactlyTwoXMin() public pure { + // Tier 2 boundary: deposit == 2x minDeposit + // First tier: 0.003 * 2 = 0.006; Second tier: 0.003 + // Total = 0.009 + assertEq(PaymasterGraceLib.calculateMatchAllowance(0.006 ether, 0.003 ether), 0.009 ether); + } + + function testMatchAllowance_BetweenTwoXAndFiveX() public pure { + // Tier 3: 2x < deposit < 5x → capped at firstTier + secondTier + // First tier: 0.003 * 2 = 0.006; Second tier: 0.003 + // Total = 0.009 (same cap) + assertEq(PaymasterGraceLib.calculateMatchAllowance(0.01 ether, 0.003 ether), 0.009 ether); + } + + function testMatchAllowance_ExactlyFiveXMin() public pure { + // Tier 4: deposit >= 5x → self-sufficient, no match + assertEq(PaymasterGraceLib.calculateMatchAllowance(0.015 ether, 0.003 ether), 0); + } + + function testMatchAllowance_AboveFiveXMin() public pure { + assertEq(PaymasterGraceLib.calculateMatchAllowance(1 ether, 0.003 ether), 0); + } + + function testMatchAllowance_ZeroDeposit() public pure { + assertEq(PaymasterGraceLib.calculateMatchAllowance(0, 0.003 ether), 0); + } + + function testFuzz_MatchAllowance_TierBoundaries(uint128 minDeposit) public pure { + vm.assume(minDeposit > 0 && minDeposit < type(uint128).max / 5); + + uint256 min = uint256(minDeposit); + + // Below min → 0 + assertEq(PaymasterGraceLib.calculateMatchAllowance(min - 1, min), 0); + + // Exactly min → 2x + assertEq(PaymasterGraceLib.calculateMatchAllowance(min, min), min * 2); + + // Exactly 2x min → 3x min (first tier 2x + second tier 1x) + assertEq(PaymasterGraceLib.calculateMatchAllowance(min * 2, min), min * 3); + + // Between 2x and 5x → capped at 3x min + assertEq(PaymasterGraceLib.calculateMatchAllowance(min * 3, min), min * 3); + + // Exactly 5x → no match + assertEq(PaymasterGraceLib.calculateMatchAllowance(min * 5, min), 0); + } + + function testFuzz_MatchAllowance_NeverExceedsCap(uint128 deposited, uint128 minDeposit) public pure { + vm.assume(minDeposit > 0); + uint256 result = PaymasterGraceLib.calculateMatchAllowance(deposited, minDeposit); + // Match can never exceed 3x the minimum deposit + assertTrue(result <= uint256(minDeposit) * 3); + } +} + +// ============ PaymasterPostOpLib Tests ============ + +contract PaymasterPostOpLibTest is Test { + PostOpLibWrapper lib; + + function setUp() public { + lib = new PostOpLibWrapper(); + } + + // -------- adjustBudget -------- + + function testAdjustBudget_BasicReplacement() public view { + // Reserved 1000, actual 700 → usage drops by 300 + uint128 result = lib.adjustBudget(5000, 1000, 700); + assertEq(result, 4700); // 5000 - 1000 + 700 + } + + function testAdjustBudget_ActualEqualsReserved() public view { + // No change when actual == reserved + uint128 result = lib.adjustBudget(5000, 1000, 1000); + assertEq(result, 5000); + } + + function testAdjustBudget_ActualIsZero() public view { + // UserOp used no gas (edge case) + uint128 result = lib.adjustBudget(5000, 1000, 0); + assertEq(result, 4000); // 5000 - 1000 + 0 + } + + function testAdjustBudget_ExactlyReservedEqualsUsed() public view { + // All usage was from this single reservation + uint128 result = lib.adjustBudget(1000, 1000, 500); + assertEq(result, 500); + } + + function testFuzz_AdjustBudget(uint128 usedInEpoch, uint128 reserved, uint128 actual) public view { + // reserved <= usedInEpoch (reservation was included in used) + // actual <= reserved (ERC-4337 guarantee) + vm.assume(reserved <= usedInEpoch); + vm.assume(actual <= reserved); + + uint128 result = lib.adjustBudget(usedInEpoch, reserved, actual); + assertEq(result, usedInEpoch - reserved + actual); + // Result should always be <= original + assertTrue(result <= usedInEpoch); + } + + // -------- clampedDeduction -------- + + function testClampedDeduction_SufficientBalance() public view { + (uint128 newBal, uint128 deducted) = lib.clampedDeduction(1 ether, 0.5 ether); + assertEq(newBal, 0.5 ether); + assertEq(deducted, 0.5 ether); + } + + function testClampedDeduction_ExactBalance() public view { + (uint128 newBal, uint128 deducted) = lib.clampedDeduction(1 ether, 1 ether); + assertEq(newBal, 0); + assertEq(deducted, 1 ether); + } + + function testClampedDeduction_InsufficientBalance() public view { + // Deduction clamped to available balance + (uint128 newBal, uint128 deducted) = lib.clampedDeduction(0.3 ether, 1 ether); + assertEq(newBal, 0); + assertEq(deducted, 0.3 ether); + } + + function testClampedDeduction_ZeroBalance() public view { + (uint128 newBal, uint128 deducted) = lib.clampedDeduction(0, 1 ether); + assertEq(newBal, 0); + assertEq(deducted, 0); + } + + function testClampedDeduction_ZeroCost() public view { + (uint128 newBal, uint128 deducted) = lib.clampedDeduction(1 ether, 0); + assertEq(newBal, 1 ether); + assertEq(deducted, 0); + } + + function testFuzz_ClampedDeduction_NeverUnderflows(uint128 balance, uint128 cost) public view { + (uint128 newBal, uint128 deducted) = lib.clampedDeduction(balance, cost); + // Balance never goes negative + assertTrue(newBal <= balance); + // Deducted never exceeds balance or cost + assertTrue(deducted <= balance); + assertTrue(deducted <= cost); + // Conservation: newBal + deducted == balance + assertEq(uint256(newBal) + uint256(deducted), uint256(balance)); + } +} + +// ============ PaymasterCalldataLib Tests ============ + +contract PaymasterCalldataLibTest is Test { + CalldataLibWrapper lib; + + address constant TARGET = address(0xBEEF); + bytes4 constant EXECUTE_SELECTOR = 0xb61d27f6; + + function setUp() public { + lib = new CalldataLibWrapper(); + } + + // -------- parseExecuteCall -------- + + function testParseExecute_ValidCallWithInnerSelector() public view { + // Build valid execute(address,uint256,bytes) calldata + bytes memory innerData = abi.encodeWithSelector(bytes4(0xdeadbeef), uint256(42)); + bytes memory callData = abi.encodeWithSelector(EXECUTE_SELECTOR, TARGET, uint256(0), innerData); + + (bool valid, bytes4 innerSelector) = lib.parseExecuteCall(callData, TARGET); + assertTrue(valid); + assertEq(innerSelector, bytes4(0xdeadbeef)); + } + + function testParseExecute_ValidCallEmptyInnerData() public view { + // execute(target, 0, "") — empty inner data + bytes memory callData = abi.encodeWithSelector(EXECUTE_SELECTOR, TARGET, uint256(0), bytes("")); + + (bool valid, bytes4 innerSelector) = lib.parseExecuteCall(callData, TARGET); + assertTrue(valid); + assertEq(innerSelector, bytes4(0)); // No inner selector + } + + function testParseExecute_WrongOuterSelector() public view { + bytes memory callData = abi.encodeWithSelector(bytes4(0x12345678), TARGET, uint256(0), bytes("")); + + (bool valid,) = lib.parseExecuteCall(callData, TARGET); + assertFalse(valid); + } + + function testParseExecute_TargetMismatch() public view { + bytes memory callData = abi.encodeWithSelector(EXECUTE_SELECTOR, address(0xDEAD), uint256(0), bytes("")); + + (bool valid,) = lib.parseExecuteCall(callData, TARGET); + assertFalse(valid); + } + + function testParseExecute_NonZeroValue() public view { + // value must be 0 for sponsored calls + bytes memory callData = abi.encodeWithSelector(EXECUTE_SELECTOR, TARGET, uint256(1), bytes("")); + + (bool valid,) = lib.parseExecuteCall(callData, TARGET); + assertFalse(valid); + } + + function testParseExecute_TooShortCalldata_LessThan4() public view { + (bool valid,) = lib.parseExecuteCall(hex"b61d27", TARGET); + assertFalse(valid); + } + + function testParseExecute_TooShortCalldata_LessThan0x64() public view { + // 4-byte selector + some data but not enough for full execute args + bytes memory callData = abi.encodePacked(EXECUTE_SELECTOR, bytes32(0)); + (bool valid,) = lib.parseExecuteCall(callData, TARGET); + assertFalse(valid); + } + + function testParseExecute_EmptyCalldata() public view { + (bool valid,) = lib.parseExecuteCall(hex"", TARGET); + assertFalse(valid); + } + + function testParseExecute_RegisterAccountSelector() public view { + // This is the real-world scenario: execute(registry, 0, registerAccount(bytes32,bytes)) + bytes4 registerAccount = bytes4(0xbff6de20); + bytes memory innerData = abi.encodeWithSelector(registerAccount, bytes32(uint256(1)), bytes("pubkey")); + bytes memory callData = abi.encodeWithSelector(EXECUTE_SELECTOR, TARGET, uint256(0), innerData); + + (bool valid, bytes4 innerSelector) = lib.parseExecuteCall(callData, TARGET); + assertTrue(valid); + assertEq(innerSelector, registerAccount); + } + + function testFuzz_ParseExecute_CorrectTarget(address target) public view { + vm.assume(target != address(0)); + bytes memory innerData = abi.encodeWithSelector(bytes4(0xaabbccdd)); + bytes memory callData = abi.encodeWithSelector(EXECUTE_SELECTOR, target, uint256(0), innerData); + + (bool valid,) = lib.parseExecuteCall(callData, target); + assertTrue(valid); + + // Wrong target should fail + if (target != address(1)) { + (bool valid2,) = lib.parseExecuteCall(callData, address(1)); + assertFalse(valid2); + } + } +}