Skip to main content

PolicyRegistry Fuzz Coverage

Vertical entrypoint: test/fuzzing/FuzzPolicyRegistryIntegrity.sol Invariants: test/fuzzing/properties/Properties_PR.sol Descriptions: test/fuzzing/properties/PropertiesDescriptions.sol

Scope

Direct exercise of PolicyRegistry wallet-scoped policy configuration, delegated policy-admin permissioning, role bitmap updates, operation-module lookup, module allowlist behavior, CompanyWallet admin surfaces, CompanyWallet.execute enforcement, and manual CompanyWallet.advancePolicyEpoch invalidation.

The harness deploys a real PolicyRegistry, three real CompanyWallet beacon proxies (two for the role/module surface and one dedicated policyEpochWallet for the policy-version invariants), a fuzz target contract, and four policy modules:

  • policyAllowModule returns true.
  • policyDenyModule returns false.
  • policyRevertingModule reverts from canExecute.
  • policyInvalidReturnModule returns an invalid one-byte payload.

Two wallets are used for the role/module surface so every wallet-scoped mutation also checks that the sibling wallet remains unchanged. A third dedicated wallet, policyEpochWallet, is reserved for the policy-version (ownership-epoch) invariants: its ownership is rotated so the epoch can be advanced repeatedly without disturbing the epoch-scoped state of the other two wallets.

Handlers

EntrypointCaller modelTargetPurpose
fuzz_grantWalletPolicyAdminwallet owner / admin / user / outsiderpolicyRegistry.grantWalletPolicyAdminOwner-only admin grant path plus unauthorized and duplicate-grant boundaries
fuzz_revokeWalletPolicyAdminwallet owner / admin / user / outsiderpolicyRegistry.revokeWalletPolicyAdminOwner-only admin revoke path plus unauthorized and not-granted boundaries
fuzz_advancePolicyEpochwallet owner / seeded admin / outsiderCompanyWallet.advancePolicyEpochUses a three-seed surface to seed current-epoch delegated policy, advance the epoch, and verify old policy state becomes unreachable; also covers unauthorized and zero-reason failures
fuzz_grantUserRoleswallet owner, granted admin, or unauthorized callerpolicyRegistry.grantUserRolesOR new user-role bits into a wallet-local bitmap
fuzz_revokeUserRoleswallet owner, granted admin, or unauthorized callerpolicyRegistry.revokeUserRolesClear selected user-role bits
fuzz_setUserRoleswallet owner, granted admin, or unauthorized callerpolicyRegistry.setUserRolesOverwrite exact user-role bitmap, including zero
fuzz_grantOperationRoleswallet owner, granted admin, or unauthorized callerpolicyRegistry.grantOperationRolesOR new operation-role bits into a wallet-local operation bitmap
fuzz_revokeOperationRoleswallet owner, granted admin, or unauthorized callerpolicyRegistry.revokeOperationRolesClear selected operation-role bits
fuzz_setOperationRoleswallet owner, granted admin, or unauthorized callerpolicyRegistry.setOperationRolesOverwrite exact operation-role bitmap, including zero
fuzz_setOperationModulewallet owner, granted admin, or unauthorized callerpolicyRegistry.setOperationModuleConfigure or clear the optional operation policy module
fuzz_grantUserRolesBatchwallet owner, granted admin, or unauthorized callerpolicyRegistry.grantUserRoles(address,address[],uint256[])Batch variant: single-element array exercises the overload's loop logic with the same invariants as the scalar form
fuzz_revokeUserRolesBatchwallet owner, granted admin, or unauthorized callerpolicyRegistry.revokeUserRoles(address,address[],uint256[])Batch variant for revoke
fuzz_setUserRolesBatchwallet owner, granted admin, or unauthorized callerpolicyRegistry.setUserRoles(address,address[],uint256[])Batch variant for set
fuzz_grantOperationRolesBatchwallet owner, granted admin, or unauthorized callerpolicyRegistry.grantOperationRoles(address,OperationRoles[])Batch variant for operation-role grant
fuzz_revokeOperationRolesBatchwallet owner, granted admin, or unauthorized callerpolicyRegistry.revokeOperationRoles(address,OperationRoles[])Batch variant for operation-role revoke
fuzz_setOperationRolesBatchwallet owner, granted admin, or unauthorized callerpolicyRegistry.setOperationRoles(address,OperationRoles[])Batch variant for operation-role set
fuzz_setOperationModuleBatchwallet owner, granted admin, or unauthorized callerpolicyRegistry.setOperationModule(address,OperationModule[])Batch variant for operation module configuration
fuzz_setPolicyModuleAllowedregistry owner (address(this))policyRegistry.setPolicyModuleAllowedToggle global allowlist status for fuzz policy modules
fuzz_setRoleLabelregistry owner (address(this))policyRegistry.setRoleLabelStore compact role metadata
fuzz_checkPolicyLookupview-only modelpolicyRegistry.canExecute / checkOperationModule / computeOperationKeyCompare lookup results against the harness model, including structurally invalid requests
fuzz_checkPolicyLookupWithAllowModuleview-only model seeded through wallet ownerpolicyRegistry.canExecute / checkOperationModuleSeed matching user roles, operation roles, and an allowlisted allow-module so the module-authorized success branch is reachable
fuzz_companyWalletAdminSurfacewallet owner / outsiderCompanyWallet.receive, setPolicyRegistry, renounceOwnershipExercise receive, idempotent policy registry setter, and disabled ownership renounce
fuzz_executePolicyCallwallet owner / admin / user / outsiderCompanyWallet.executeVerify owner bypass plus PolicyRegistry.canExecute enforcement against real wallet execution
fuzz_advancePolicyVersionResetsStatepolicyEpochWallet owner (rotated)CompanyWallet.transferOwnershipSeed wallet-scoped policy at the current epoch, then advance the ownership epoch and verify the policy version increments monotonically and prior-epoch policy is reset

Invariants

Wallet policy admins - group 10

IDConditionChecked
PR-10grantWalletPolicyAdmin marks the selected admin as granted for the selected walleton success
PR-11revokeWalletPolicyAdmin clears the selected admin for the selected walleton success
PR-12Admin grant/revoke does not mutate the sibling wallet's admin flagon success
PR-13Admin grant/revoke reverts only for the expected permission or state reasonon revert

Policy epoch - group 70

IDConditionChecked
PR-70advancePolicyEpoch increments the selected wallet epoch and preserves the wallet owneron success
PR-71Successful epoch advance makes selected delegated admin, user role, operation role, and module state unreachable in the new epochon success
PR-72advancePolicyEpoch reverts only for unauthorized caller or zero reasonon revert
PR-73Failed epoch advance preserves the selected wallet epoch and seeded delegated policy stateon revert
PR-74Epoch advancement is scoped to the selected wallet; sibling wallet epoch, owner, admin, role, module, and execution state are unchangedalways
PR-75The snapshotted pre-state contains reachable delegated policy state before attempting the epoch advancebefore action

User roles - group 20

IDConditionChecked
PR-20grantUserRoles stores `beforeRolesroles`
PR-21revokeUserRoles stores beforeRoles & ~roleson success
PR-22setUserRoles stores the exact requested bitmapon success
PR-23User role mutations do not affect the sibling walleton success
PR-24User role mutations revert only for unauthorized caller, already-granted bits, or missing bits as applicableon revert

Operation roles - group 30

IDConditionChecked
PR-30grantOperationRoles stores `beforeRolesroles`
PR-31revokeOperationRoles stores beforeRoles & ~roleson success
PR-32setOperationRoles stores the exact requested bitmapon success
PR-33Operation role mutations do not affect the sibling walleton success
PR-34Operation role mutations revert only for unauthorized caller, already-granted bits, or missing bits as applicableon revert

Operation modules and metadata - groups 40 and 50

IDConditionChecked
PR-40setOperationModule stores the selected module, or zero when clearingon success
PR-41Operation module mutations do not affect the sibling walleton success
PR-42Module updates revert only for unauthorized callers or non-allowlisted moduleson revert
PR-50setPolicyModuleAllowed stores the exact allowlist statuson success
PR-51setRoleLabel stores the exact role labelon success
PR-52setPolicyModuleAllowed does not unexpectedly revert when called by the owneron revert
PR-53setRoleLabel does not unexpectedly revert when called by the owneron revert

Lookup and enforcement - group 60

IDConditionChecked
PR-60canExecute equals the harness model: structurally valid request, non-zero user roles, intersecting operation roles, and optional module authorizationon lookup
PR-61checkOperationModule diagnostics match configured module, allowlist state, staticcall success, and module authorizationon lookup
PR-62CompanyWallet.execute succeeds only for wallet owner bypass or PolicyRegistry.canExecute == true; rejected calls do not mutate the targeton execute
PR-63computeOperationKey(target, selector) equals keccak256(abi.encode(target, selector))on lookup
PR-64CompanyWallet directed admin and view calls do not unexpectedly reverton receive, policy-registry setter, ERC165 views
PR-65Disabled renounceOwnership reverts with the expected selectoron renounce attempt

Policy version (ownership epoch) - group 70

IDConditionChecked
PR-70Each wallet ownership transfer advances the policy version (ownership epoch) by exactly one and never decreases iton success
PR-71Advancing the policy version resets wallet-scoped policy (admin, user roles, operation roles, and operation module) for the new epoch; seeded state is verified present at the prior epoch firston success
PR-72Advancing the policy version (transferOwnership) does not unexpectedly reverton revert
PR-76Policy allow-module seed call succeedsbefore allow-module lookup
PR-77Policy owner seed calls succeedbefore policy-epoch reset

Summary of the clamping and state-selection logic applied before each handler issues a protocol call. Full implementation in helper/preconditions/PreconditionsPolicyRegistry.sol.

HandlerClamp rules
Admin grant/revokeWallet selected from the two policy wallets. Admin selected from POLICY_ADMINS. Caller selected from wallet owner, policy admins, regular users, or POLICY_OUTSIDER.
advancePolicyEpochWallet selected from the two policy wallets. pathSeed selects owner success, owner zero-reason failure, seeded-admin unauthorized failure, or outsider unauthorized failure. stateSeed derives delegated admin, user, operation, roles, and non-zero reason material. The precondition builder seeds delegated admin, user-role, operation-role, and allow-module state for the current epoch, snapshots selected and sibling wallet state, then the handler calls the wallet.
User role handlers (scalar and batch)User selected from tracked users. Role bitmap clamped to [1, 255] for grant/revoke and [0, 255] for set. Authorization is recorded before the call so postconditions distinguish expected success from expected PolicyRegistry__Unauthorized. Batch variants use single-element arrays.
Operation role handlers (scalar and batch)Operation selected from FuzzPolicyTarget.setFlag(bool) or FuzzPolicyTarget.setNumber(uint256). Role bitmap clamping matches user roles. Batch variants use single-element arrays.
setOperationModule (scalar and batch)Module selected from zero address, allow, deny, reverting, or invalid-return module. The handler records whether the module is currently globally allowlisted. Batch variant uses a single-element array.
setPolicyModuleAllowedModule selected from the four non-zero fuzz policy modules.
setRoleLabelRole id clamped to [0, 255]; label is derived directly from the seed.
checkPolicyLookupSelects either a structurally valid request or one invalid field: zero wallet, zero caller, zero target, or calldata shorter than four bytes.
checkPolicyLookupWithAllowModuleUses a valid wallet/user/operation tuple, re-allowlists policyAllowModule if needed, seeds user roles, operation roles, and the operation module as the wallet owner, then expects the module-authorized lookup path to be executable.
companyWalletAdminSurfaceSelects one policy wallet, calls empty calldata from an outsider to cover receive, calls setPolicyRegistry as the owner with the existing registry address, and asserts renounceOwnership reverts with the disabled-renounce selector.
executePolicyCallUses a valid target operation and computes expected authorization from wallet owner bypass or the same role/module model used for canExecute.
advancePolicyVersionResetsStateOperates on the dedicated policyEpochWallet. Reads the current owner and rotates ownership to the other epoch-wallet owner (never a self-transfer). User and admin are selected from the tracked pools, the operation from FuzzPolicyTarget, the module is the allowlisted allow-module, and roles are forced non-zero so the seeded state is guaranteed present before the version advance.

All public fuzz entrypoints delegate to handlers through FuzzIntegrityBase._testSelf. Direct handler_* calls remain blocked in echidna-config.yaml, and Medusa allowlists only fuzz_* entrypoints.