feat(compliance): add status transition guards with pre-flight reads - #171
Merged
El-swaggerito merged 1 commit intoJul 29, 2026
Merged
Conversation
Adds a guard layer over the compliance lifecycle so a status change can be evaluated — and explained — before it is submitted, using the same code the write path enforces. - src/compliance_guards.rs: ordered guard chain (initialized, not paused, caller authority, no-op, target-not-Unknown, transition matrix) evaluated by a function that never panics and never writes, plus the typed `TransitionGuard` reasons and their error-code mapping. - compliance.rs now reaches its verdict through the same evaluation, so a pre-flight verdict and enforcement cannot drift. Failure shapes (panic vs. typed Err) and the tolerant legacy wrappers are unchanged. - New reads: check_compliance_transition, get_compliance_transition_guard, check_compliance_batch. All are pure, stay callable while paused, and emit no events. - BlockedRequiresAdmin is reported separately from CallerUnauthorized: both map to 3000, but one needs an escalation and the other a role. - 17 tests asserting the guard and the contract agree across all 25 edges for officer, admin, and unauthorized callers, plus pause ordering, uninitialized reads, role revocation, batch atomicity, and duplicate detection. - docs/compliance-transition-guards.md with the guard chain, reason codes, security assumptions, and client guidance; README, capabilities registry (schema v4), and SDK fixtures updated.
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #27
The compliance lifecycle already rejected illegal status changes, but it could not explain a rejection ahead of time. A client had three bad options: re-implement the rules (two copies of a compliance-critical rule set, free to drift), submit and translate the revert (burns a transaction, and a bare
Unauthorizedcannot distinguish "you need a role" from "this address is frozen and only the admin can lift it"), or readis_compliance_transition_allowed(matrix-only — it ignores the caller, the pause, and the admin-only exit fromBlocked, sotruedid not mean the call would succeed).This PR adds a guard layer that closes the gap by making one evaluation serve both purposes: the pre-flight reads and every state-changing compliance call reach their verdict through the same function. A pre-flight verdict cannot disagree with enforcement, because there is nothing to disagree with.
What was added
src/compliance_guards.rs— the ordered guard chain, evaluated by a function that never panics and never writes:NotInitializedContractPausedCallerUnauthorized/BlockedRequiresAdminStatusUnchangedUnknownTargetUnknownForbiddenfrom -> tois in the transition matrixTransitionForbiddenDuplicateUserInBatchTwo ordering choices are deliberate and documented: pause before authority (a paused contract reports the pause to everyone and never leaks whether the caller would otherwise have qualified) and authority before the matrix (an unauthorized caller learns nothing about edges for an address they may not touch).
BlockedRequiresAdminis separated fromCallerUnauthorized. Both map toUnauthorized(3000) on-chain, but the remediation differs — "escalate to the admin" vs. "request a role". A client that collapses them tells a properly-credentialed officer to ask for a permission they already hold.Three pure reads —
check_compliance_transition,get_compliance_transition_guard,check_compliance_batch. No authorization, no writes, no events, callable while paused.ComplianceTransitionCheckcarries the resolvedcurrent_status(so a client cannot race a separate status read against it) and a pre-resolvederror_code.compliance.rsrefactored to use the same evaluation. Backwards compatibility is exact: availability/authorization failures still panic, rule violations still return typedErr, and the tolerant legacy wrappers (whitelist_user/revoke_whitelist) keep their idempotent no-op behaviour while still being unable to lift a freeze.Notes for the reviewer
mainare fixed here so the suite is green. They are unrelated to this issue and I kept them minimal: (1)fixtures/sdk/01-compliance.jsonhad drifted and was regenerated withmake update-fixtures; (2)fixture_errorsassertedAssetNotActive(6000) andAssetLifecyclePaused(6001) for mint attempts that the contract already reports as7002/7000—docs/error-codes.mdalready documents 6000 as "Reserved. Superseded by 7000–7002; no longer emitted", so the expectations were stale, not the contract.cargo fmtalso normalizedsrc/config.rs/src/config_test.rs, which were unformatted onmain. The same three fixes appear in the PR for Implement issuer role separation #28; whichever merges second can drop them.Test Evidence
The load-bearing tests assert agreement rather than a hardcoded expectation, so they fail if the read path and the write path ever diverge: all 5 × 5 source/target edges are walked three times (officer, admin, unauthorized caller), each on a fresh deployment, comparing the pre-flight verdict against the real submission's outcome, error code, and resulting status.
Completion Table
src/compliance_guards.rs—evaluate_from_status()(ordered guard chain),require_transition()(enforcement),check_transition()/check_batch()(pre-flight);compliance.rs::validate_transition()now delegates to ittest_guard_matches_enforcement_for_every_edge_as_officer,..._as_admin,..._as_unauthorized_caller—src/test.rsdocs/compliance-transition-guards.mdaddedBlocked, self-transitions, unreachableUnknowntarget, forbidden edges, duplicate batch entries, empty batch, role revoked mid-flighttest_guard_reports_not_initialized_instead_of_panicking,test_guard_reports_pause_ahead_of_authority,test_guard_reports_blocked_requires_admin_not_generic_unauthorized,test_guard_reports_status_unchanged_for_every_self_edge,test_guard_reports_target_unknown_as_its_own_reason,test_batch_guard_flags_duplicate_addresses,test_batch_guard_accepts_an_empty_batch,test_guard_verdict_tracks_role_revocationdocs/compliance-transition-guards.mdrequire_authin a readtest_guard_reads_never_mutate_state(no state change, no events),test_guard_accepts_emergency_officer_and_rejects_asset_managerdocs/compliance-transition-guards.mdCOMPLIANCE STATUS TRANSITION GUARDSinsrc/test.rs; 2 new SDK fixture scenarioscargo test197 passed;fixtures/sdk/01-compliance.json—check-compliance-transition-allowed,check-compliance-transition-blocked-requires-admindocs/compliance-transition-guards.mdcompliance-lifecycle.mdandcompliance-status-transitions.mdREADME.md,docs/compliance-lifecycle.md,docs/compliance-status-transitions.mdcompliance.transition_guardscapability +compliance_transition_guardsregistry key;CAPABILITY_SCHEMA_VERSION3 → 4;TransitionGuardvariants documented as append-only ABI; no existing entrypoint changeddocs/capabilities.md(field, registry key, version note)Contributor Self-Assessment
fmt-check,clippy(no new warnings from the new module),test, and the wasm release build all pass locally.require_auth. Both are documented as explicit assumptions indocs/compliance-transition-guards.md.