Skip to content

fix: enforce homogeneous policies and commit every matching policy - #116

Open
alberto-crossmint wants to merge 1 commit into
mainfrom
fix/homogeneous-policies-and-commit-all-matches
Open

fix: enforce homogeneous policies and commit every matching policy#116
alberto-crossmint wants to merge 1 commit into
mainfrom
fix/homogeneous-policies-and-commit-all-matches

Conversation

@alberto-crossmint

Copy link
Copy Markdown
Collaborator

Summary

Two interlocking fixes for a Standard-signer authorization-bypass and a partial-state-commit hazard found during security review.

  • Reject mixed SignerPolicy variants on Standard signers. A signer's policy vector must contain a single kind — either all ExternalValidatorPolicy or all TokenTransferPolicy. Mixing them let SignerRole::is_authorized's OR-short-circuit authorize through a permissive external policy while a strict TokenTransferPolicy cap was never consulted (its is_authorized/on_authorized were skipped).
  • Run on_authorized for every matching policy. With the homogeneity gate in place, OR remains the decision rule (multi-currency configurations still work) but the loop no longer short-circuits on the first match — every policy that authorizes commits its state. Overlapping same-token policies (e.g. daily + monthly cap) now both advance their trackers.
  • README rewritten to match the implemented semantics. The previous "all policies must pass" wording was misleading; the new "Policy semantics" section spells out OR, the homogeneity rule, and the commit-every-match guarantee.

Why this shape

A blanket switch to AND semantics would have broken the multi-currency use case the existing tests lock in (test_multi_policy_usdc_transfer_passes etc.). Locking out the dangerous combination at registration is a minimal change that preserves the working configurations and removes the bypass surface.

The full design discussion (alternatives B/C/D — strict AND, tri-state policies, explicit composition) is deferred to a separate RFC. This PR is the safe, minimal fix.

Test plan

  • New test_mixed_policy_variants_rejected_at_constructor[ExternalPolicy, TokenTransferPolicy] panics with InvalidPolicy (#80) at register time.
  • New test_mixed_policy_variants_rejected_on_update_signer — same via update_signer.
  • New test_two_token_transfer_policies_same_variant_allowed — multi-currency use case remains valid.
  • New test_two_external_policies_same_variant_allowed — defense-in-depth external-only stack still allowed.
  • New test_overlapping_token_policies_both_commit_spend — regression guard for the commit-every-match change; both same-token tracker entries advance.
  • Existing 126 tests still pass — cargo test -p smart-account reports 131 passed, 0 failed.

Backwards compatibility

  • Storage layer: unchanged. No schema or enum changes.
  • Existing accounts with mixed-variant signers in storage: __check_auth still works (auth path doesn't re-validate the signer set on every call). Only add_signer / update_signer enforce the new rule, which acts as upgrade pressure to a safe configuration. If any production accounts are in this state, an enumeration script can find them via get_signer over known signer keys.
  • v1→v2 migrated signers: convert_role in migration/v1_to_v2.rs already produces Some(vec![external_only]) or None — always homogeneous. No migration changes needed.

Out of scope

  • Strict AND semantics across heterogeneous policy sets — needs a tri-state is_authorized (Allow/Deny/NotApplicable), tracked separately.
  • Bounding the number of on_authorized calls per auth (gas concern). Common configurations have ≤ 2 policies; abuse is admin-self-paid.
  • handle_policy_set_changes (update-signer policy diff) is unchanged — the new validator runs ahead of it.

Two interlocking fixes that together close a Standard-signer
authorization-bypass and a partial-state-commit hazard.

1. Reject mixed SignerPolicy variants on Standard signers
   (account.rs::validate_homogeneous_policies). A signer's policy
   vector must be either all ExternalValidatorPolicy or all
   TokenTransferPolicy. Mixing them let the OR-short-circuit in
   SignerRole::is_authorized authorize through a permissive external
   policy while a strict TokenTransferPolicy spending cap was never
   consulted.

2. Run on_authorized for every matching policy, not just the first
   (permissions.rs). With the homogeneity check in place, OR semantics
   are kept (multi-currency configurations work as before), but
   overlapping same-variant policies (e.g. daily and monthly cap on
   the same token) all advance their spending trackers instead of
   only the first matching one.

The README is rewritten to match the implemented semantics — earlier
text said "all policies must pass" while the code implements OR.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 60f1e01800

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 110 to +113
for policy in policies.iter() {
if policy.is_authorized(env, signer_key, contexts) {
policy.on_authorized(env, signer_key, contexts);
return true;
authorized = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip duplicate policy callbacks by identity

When a signer has duplicate policy identities (e.g. two TokenTransferPolicy entries sharing the same policy_id), this loop can call on_authorized multiple times in one auth pass. Because token spend tracking is keyed by policy_id, one transfer can be recorded twice (or more), prematurely exhausting limits and making authorization/order effects depend on vector ordering. This regression appears after removing the early return; consider deduplicating policies on add/update or ensuring each policy identity is committed at most once per authorization.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant