Skip to content

Registry Controls - #2

Open
vwagner wants to merge 39 commits into
FigureTechnologies:mainfrom
vwagner:vdub/registry-controls
Open

Registry Controls#2
vwagner wants to merge 39 commits into
FigureTechnologies:mainfrom
vwagner:vdub/registry-controls

Conversation

@vwagner

@vwagner vwagner commented Jun 30, 2026

Copy link
Copy Markdown

This PR was created solely so that Bugbot can review the code. This PR will not be merged here.

Description

This pull request introduces a major enhancement to the registry module by establishing a robust, multi-tiered framework for multi-party authorization and role-based permissions. It combines a new single-signer approval-accumulation model for executing multi-party authorizations with Registry Class policies for asset class-level (NFT collection) rule management.

Together, these changes make it easier to define, manage, and execute custom authorization policies for NFT collections without requiring governance proposals for every change, while remaining fully compatible with Cosmos SDK authz.

Key Changes & Features

1. Multi-Party Authorization & Approval Accumulation

Because Cosmos SDK authz MsgExec only dispatches single-signer messages, this update introduces a single-signer approval-accumulation model rather than literal multi-signer messages.

  • Propose & Approve Flow: Added MsgProposeRoleChange and MsgApproveRoleChange. Each message takes exactly one signer. Approvals accumulate in the registry state on a PendingRoleChange (keyed by a deterministic ID).
  • Atomic Execution: Once every affected role's policy requirement is satisfied, the batch applies atomically via Keeper.SetRoles. Ineligible approvals are safely ignored to bound state.
  • Authz Compatibility: Approvals are fully delegatable via authz MsgExec. MsgGrantRole and MsgRevokeRole remain single-signer; changes requiring multi-party authorization must use the new propose/approve flow.

2. Registry Class Policies (Asset Class-Level)

Introduces the ability to define asset class-level authorization rules, allowing maintainers to set custom role update policies for entire NFT collections.

  • Registry Classes: Added the RegistryClass message to define these class-level authorization rules (authorization.proto). Added transaction endpoints for creating and updating registry classes.
  • Entry Associations: RegistryEntry can now reference a registry_class_id to determine its authorization policy for role updates (registry.proto). Support for associating new entries with a class during registration (MsgRegisterNFT) has also been added.

3. Authorization Policy Engine & Roles

Establishes the underlying engine for evaluating complex role policies.

  • New Roles & Atomic Updates: Added 4 new RegistryRole enum values: LIEN_OWNER, SECURED_PARTY_FOR_LIEN, SECURED_PARTY_FOR_ENOTE, and PLEDGEE. Added MsgSetRoles / RoleUpdate for atomic, desired-state multi-role updates (used for the fast path and as the proposal payload).
  • Protobuf Definitions: Added authorization.proto defining SignatureType, NftRole, Assignment, RoleAuthorization, Authorization, SignatureRequirement, RoleAssignment, RolePriority, and RolePriorityEntry.
  • Evaluation Engine: Added static role policies (DefaultRoleAuthorizations(), RoleAuthorizationMap()) and the evaluation engine in keeper/authorization.go containing address resolvers, signature requirement evaluators, and policy approver collection (ValidateRoleChangeAuthorization, CollectPolicyApprovers).

4. Governance & Module Parameters

  • Default Fallbacks: Added a Params message to define module-wide default authorization policies, which serve as a fallback when no registry_class_id is set.
  • Parameter Updates: Added a governance endpoint (MsgUpdateParams) to update registry module parameters.
  • Genesis Updates: Genesis state now includes registry classes and module params.

5. Queries, CLI, and Auditability

  • gRPC & CLI Queries: Added endpoints and CLI commands for fetching registry classes, all module parameters, pending role changes (pending-role-changes), and for validating role changes against policies.
  • CLI Transactions: Added propose-role-change and approve-role-change.
  • Events: Added comprehensive diff-based event emission for registry class creation, updates, parameter changes, and role updates detailing authorization specifics to improve auditability.

Note

High Risk
Changes core on-chain authorization for registry role updates, unregister rules, and bulk updates; bugs could allow unauthorized role changes or block legitimate updates.

Overview
Major expansion of the x/registry module so role changes are governed by configurable policies instead of NFT-owner-only checks, with an authz-friendly propose/approve path for multi-party consent.

Registry classes & resolution tier: New RegistryClass types and msgs (CreateRegistryClass, UpdateRegistryClassRoleAuthorization) define per–asset-class RoleAuthorization rules. Entries gain optional registry_class_id (on register or via MsgAssociateRegistryClass), with module Params as the middle tier and legacy NFT-owner auth when no policy applies. Association is allowed for CONTROLLER, scope data owner (metadata scopes), or NFT owner—any one suffices.

Policy engine: authorization.proto and keeper logic evaluate signature requirements (ALL/ANY, CURRENT/NEW assignments, role priority, NFT owner role) with fail-closed behavior on misconfiguration. GrantRole, RevokeRole, SetRoles, and bulk updates on existing entries route through this engine; UnregisterNFT requires CONTROLLER when that role is set.

Multi-party flow: MsgProposeRoleChange / MsgApproveRoleChange / MsgCancelRoleChange accumulate single-signer approvals on PendingRoleChange (genesis-exportable, one pending change per NFT, optional expiry), then apply atomically via SetRoles. New participant roles (lien/eNote/pledgee-related) and richer EventRoleUpdated / pending-class events support auditability.

Surface area: New gRPC queries (pending changes, classes, params, ValidateRoleChange dry-run), CLI tx/query commands, and CreateRegistry(..., registryClassID) call-site updates in ledger tests. Proto Java/Kotlin bindings temporarily stop excluding google/** protos.

Reviewed by Cursor Bugbot for commit 0dd4900. Bugbot is set up for automated code reviews on this repo. Configure here.

vwagner and others added 30 commits June 18, 2026 09:45
- Add 4 new RegistryRole enum values: LIEN_OWNER, SECURED_PARTY_FOR_LIEN,
  SECURED_PARTY_FOR_ENOTE, PLEDGEE
- Change MsgGrantRole/MsgRevokeRole from single signer to repeated signers
- Add MsgSetRoles/RoleUpdate for atomic multi-role updates
- Add authorization.proto defining SignatureType, NftRole, Assignment enums
  and RoleAuthorization, Authorization, SignatureRequirement, RoleAssignment,
  RolePriority, RolePriorityEntry message types
- Add types/authorization.go with static role authorization policies:
  DefaultRoleAuthorizations() and RoleAuthorizationMap()
- Add keeper/authorization.go with full authorization validation engine:
  resolveRegistryRoleAddresses, resolveNFTRoleAddresses,
  resolveRolePriorityAddresses, resolveRoleAssignmentAddresses,
  evaluateSignatureRequirement, evaluateAuthorization,
  ValidateRoleChangeAuthorization
- Update msg_server.go: GrantRole/RevokeRole use multi-signer auth;
  add SetRoles handler
- Add keeper.go SetRoles method with atomic role updates and diff-based
  event emission
- Update CLI grant-role/revoke-role commands for repeated signers

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements multi-party Controller authorization using single-signer messages
whose approvals accumulate in registry state and auto-apply once the role's
policy is satisfied. Unlike the native repeated-signers model, every message is
single-signer, so each approval is delegable via authz.

- proto: RoleChangeOperation enum + PendingRoleChange state; MsgProposeRoleChange
  and MsgApproveRoleChange (single-signer); proposed/approved/applied events.
- keeper: PendingRoleChanges collection + accumulation/auto-apply reusing the
  existing role-typed policy engine.
- types: deterministic change-id helper, ValidateBasic, event constructors.
- tests: full Controller matrix plus authz delegation proof.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Revert MsgGrantRole/MsgRevokeRole/MsgSetRoles from repeated signers back
to a single signer field so all registry messages are single-signer and
therefore authz-delegable. Multi-party Controller updates now flow
exclusively through the Option B propose/approve accumulation path.

- tx.proto: 3 messages use single signer (regenerated tx.pb.go)
- keeper/msg_server.go: handlers pass []string{msg.Signer} to policy engine
- client/cli/tx.go: CLI uses Signer
- types/msgs.go: single-signer ValidateBasic
- types/msgs_test.go: makers and ValidateBasic cases updated
- Delete authorization_acceptance_test.go (multi-signer model removed);
  Controller coverage lives in role_change_accumulation_acceptance_test.go

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Expose and persist the Option B pending role-change state:

- query.proto: PendingRoleChange(id) and PendingRoleChanges(key, pagination)
  RPCs plus REST routes; keeper GetPendingRoleChanges paginated walk with
  optional key filter; query-server handlers and request Validate methods
- genesis.proto: GenesisState.pending_role_changes; InitGenesis/ExportGenesis
  round-trip pending changes so in-flight approvals survive export/import
- CLI query: pending-role-change, pending-role-changes
- CLI tx: propose-role-change, approve-role-change
- Tests: query-by-id, list (all/filtered/empty/paginated), genesis round-trip

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Complete the Option B acceptance matrix:

- Scenario C: Controller REVOKE accumulation (sole-controller revoke applies
  on one approval; with a Secured Party set, revoke requires current
  controller + secured party)
- Invalidation-by-re-resolution: after the controller is rotated out from
  under a pending change, the stale approval no longer counts; the live
  current controller must approve for the change to apply
- Edge cases: duplicate approval is idempotent, approving a non-existent
  change errors, and a registry deleted underneath cleans up the orphaned
  pending change

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…hz reject matrix

Add the three single-signer reject subtests the ticket enumerates for the
Controller + Secured Party scenario (only current controller, only new
controller, only secured party).

Extend authz coverage to the full ticket reject matrix via a neutral executor
that carries each party's delegated authority, with the proposal and every
approval dispatched through authz MsgExec. Add a scenario 1 (no secured party)
authz happy path to mirror "same scenarios as above" via authz.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Sort and de-duplicate addresses in NewPendingRoleChangeID so the id is truly set-based (c1)
- Emit EventRoleChangeApproved only on newly recorded approvals (c2)
- SetRoles returns RegistryNotFound only for collections.ErrNotFound; wrap other errors (c3)
- Reject NftRole used with non-ASSIGNMENT_CURRENT* variants (c4)
- Ignore approvals from addresses not eligible under the role policy to prevent state bloat/DoS (c5)
- Deep-copy the original entry in SetRoles before mutation so diff events are correct (c6)
- Validate role enum (Role.Validate()) per RoleUpdate in MsgSetRoles/MsgProposeRoleChange (c7)
- Enforce exactly-one cardinality for ASSIGNMENT_CURRENT/ASSIGNMENT_NEW (c8)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Reject duplicate roles across the batch and duplicate addresses within a
  RoleUpdate in MsgSetRoles/MsgProposeRoleChange ValidateBasic (shared helper)
- Fix propose-role-change CLI example to use secured_party_for_enote
- Fail closed in evaluateSignatureRequirement on unspecified/unknown signature types
- Fail closed in resolveRegistryRoleAddresses on unsupported/unspecified assignments
- Fail closed in resolveRoleAssignmentAddresses when no role selector is set
- Enforce exactly 0 or 2 args for the pending-role-changes query command

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Reject new role-change proposals from proposers not eligible to approve any
  affected role, so pending-change state only grows from actionable changes
- Fail closed in resolveNFTRoleAddresses on unspecified/unknown NftRole values

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
feat(registry): add multi-signer authorization controls (Phase 1 PoC)
Phase C1: introduce maintainer-managed RegistryClass config that overrides
the module's static default authorization policy via two-tier resolution
(class policy -> static default / NFT-owner fallback).

- proto: RegistryClass message; registry_class_id on RegistryEntry and
  MsgRegisterNFT; MsgCreateRegistryClass and
  MsgUpdateRegistryClassRoleAuthorization; RegistryClass/RegistryClasses
  queries; class created/updated events; genesis registry_classes field
- types: RegistryClass validation; new error codes; AllRequestMsgs and
  ValidateBasic for the new messages; genesis validation with duplicate-id
  check
- keeper: registry class CRUD and roleAuthorizationsForEntry two-tier
  resolver; class-existence checks in RegisterNFT and RegistryBulkUpdate;
  CreateRegistry gains a registryClassID parameter; genesis and query-server
  handlers
- cli: create/update class tx commands and class queries
- tests: registry_class_acceptance_test covering class CRUD, maintainer
  enforcement, two-tier resolution, and genesis round-trip

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… policies

Phase C2: introduce registry module Params holding the default role
authorization policies, forming the middle tier of two-tier resolution
(registry class policy -> module params default -> NFT-owner fallback).

- proto: params.proto with Params{role_authorizations}; MsgUpdateParams
  (authority) + response and UpdateParams rpc; Params query + REST route;
  genesis params field; EventParamsUpdated
- types: DefaultParams (empty by default, preserving legacy NFT-owner
  authorization), Params.Validate, MsgUpdateParams ValidateBasic and codec
  registration, genesis params validation
- keeper: Params collections.Item + internal gov authority; GetParams/
  SetParams/ValidateAuthority; resolver default tier now reads params;
  UpdateParams handler (authority-gated); genesis and query handlers
- cli: update-params tx (governance) and params query
- rename DefaultRoleAuthorizations to ControllerRoleAuthorizations: the
  CONTROLLER policy is a ticket example, not a chain default; the default
  params are empty so behavior matches main (NFT-owner authorization)
- tests: params acceptance coverage (default empty, authority enforcement,
  params-driven default tier, genesis round-trip); accumulation tests now
  install the CONTROLLER policy explicitly

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…hase C3)

The ticket's per-role authorization policies (Originator, Lien Owner,
Controller, Secured Party for Lien/eNote, Pledgee, Servicer, Sub-servicer)
are user/governance-created RegistryClass role_authorizations data, not
hard-coded chain logic. The policy engine already supports every construct
they use, including the unilateral foreclosure path. So C3 adds validation
and tests/examples rather than new role logic:

- Add Validate() for SignatureType, NftRole, Assignment, and
  Assignment.IsCurrent(); deepen validateRoleAuthorizations to reject
  malformed authorization paths at create time (unknown/unspecified enums,
  missing selectors, nft_role with a NEW assignment, empty paths) instead
  of only failing closed at evaluation time.
- Enforce class.Validate() on the registry-class create/update write path
  (defense-in-depth beyond ValidateBasic).
- Add DART role-policy acceptance tests covering each role's consent matrix,
  the Lien Owner and Controller foreclosure paths, and malformed-policy
  rejection.
- Ship x/registry/spec/examples/dart_registry_class.json (a complete DART
  registry class) kept in sync with the test builders.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…, C5)

Phase C4: emit a comprehensive provenance.registry.v1.EventRoleUpdated
(nft_id, asset_class_id, registry_class_id, role, addresses,
previous_addresses, signers) additively alongside the existing granular
EventRoleGranted/EventRoleRevoked events. The satisfying authorization
path's role/assignment/addresses are surfaced via CollectSatisfyingSigners;
the legacy non-policy path reports the NFT owner at ASSIGNMENT_CURRENT.
Emitted from GrantRole/RevokeRole/SetRoles and the pending-apply path
(per-role, with pre-mutation previous_addresses).

Phase C5: add a ValidateRoleChange dry-run query
(QueryValidateRoleChangeRequest{key, role_updates, approvers} ->
{error, authorized}) that reuses the accumulation evaluation read-only via
EvaluateRoleChange with no state writes. Adds the rpc/messages to
query.proto, a query_server handler, and the GetCmdQueryValidateRoleChange
CLI command. Bad request shapes return gRPC InvalidArgument.

Includes role_event_query_acceptance_test.go covering both phases.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- class.go: roleAuthorizationsForEntry now fails fast (panic) on a store/read
  error resolving a registry class, instead of silently downgrading to the
  weaker params/legacy authorization tier.
- genesis.go: GenesisState.Validate now verifies that every entry's
  registry_class_id references a registry class present in genesis, preventing
  an invalid state that would change authorization behavior at runtime.
- msg_server.go SetRoles: authorize and resolve event signers against the
  additions (additions(current, desired)) rather than the full desired set, so
  ASSIGNMENT_NEW resolves only newly-added addresses (matches the pending-apply
  path) and no longer errors when the desired set contains current+new members.
- cli/tx.go CmdUpdateParams: submit MsgUpdateParams wrapped in a governance
  proposal via provcli.GenerateOrBroadcastTxCLIAsGovProp + gov prop flags,
  matching the documented governance-only workflow used by other modules.

Adds regression tests: genesis unknown/known class-id references and a SetRoles
ASSIGNMENT_NEW additions scenario.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…policy engine

Add unit tests for RegistryClass/authorization config validation (every
rejection path), the RegistryClass/RegistryClasses/Params query RPCs,
keeper class store accessors, the policy engine's fail-closed branches,
and revoke EventRoleUpdated emission. Raises keeper coverage 71% -> 79%.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update ledger keeper test call sites to the 4-arg CreateRegistry signature
(pass "" for no registry class), fixing the test compile break. Clarify the
registry_class_id proto comment to document the module-params default tier
before the legacy NFT-owner fallback, and regenerate the pb.go.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lidation)

emitRoleUpdated now panics if the post-mutation registry read-back errors,
rather than silently emitting an EventRoleUpdated with empty current
addresses (a nil entry remains valid for a cleaned-up registry). Validate
QueryRegistryClassRequest.registry_class_id against the defined id format so
malformed ids are rejected at request validation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the company-specific "DART" product name with generic language
throughout the registry module. Rename the acceptance test suite and example
fixture, genericize identifiers (loan-registry-v1 class id, participant role
policy builders), the spec README, and the proto/pb.go doc comments.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The validate-role-change CLI documented string enum values (e.g.
"REGISTRY_ROLE_CONTROLLER") but used encoding/json, which only accepts
numeric enum values for the int32-backed role field. Decode the role_updates
argument through the proto-JSON codec (wrapped in the request message) so the
documented string enum names work.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop the company-internal ticket identifier (sc-512248) and generic "ticket"
mentions from registry test suite comments, the example spec README, and the
ControllerRoleAuthorizations doc comment, replacing them with generic language.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A registry entry referencing a registry_class_id only verified the class
existed, not that the class's asset_class_id matched the entry's. A class
is scoped to a single asset class, so a mismatch would resolve role
authorization against the wrong policy tier across unrelated collections.

Enforce the match at every write/read site: RegisterNFT, RegistryBulkUpdate,
GenesisState.Validate, and (defense-in-depth, fail-closed) the runtime
roleAuthorizationsForEntry resolver. Add ErrRegistryClassAssetMismatch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…xample

The ValidateRoleChange REST endpoint was annotated as GET with path params,
but the request carries role_updates and approvers lists that cannot be
supplied via a query string (and the server rejects empty role_updates),
making the REST route unusable. Switch it to POST with body "*" so clients
can submit the full request message as JSON.

Also make the create-registry-class help JSON example use consistent
snake_case proto field names (registry_class_id) to match the generated
example fixture.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
roleAuthorizationsForEntry now panics when an entry references a registry
class that is absent from state, instead of silently downgrading to the
params/legacy authorization tier. CreateRegistry enforces the registry
class invariant (exists and matches the entry's asset class) at the keeper
layer and rejects a nil key, preventing invalid state from direct keeper
callers that bypass the msg/genesis paths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@semgrep-code-figure-technologies

Copy link
Copy Markdown

Legal Risk

The following dependencies were released under a license that
has been flagged by your organization for consideration.

Recommendation

While merging is not directly blocked, it's best to pause and consider what it means to use this license before continuing. If you are unsure, reach out to your security team or Semgrep admin to address this issue.

CC-BY-SA-4.0

GPL-3.0

Comment thread x/registry/keeper/msg_server.go
Comment thread x/registry/keeper/keeper.go
@SpicyLemon

Copy link
Copy Markdown

Overall, the PR looks good, but I have a few notes.

  1. MsgUnregisterNFT still authorizes on NFT-owner only. So an NFT owner can trivially defeat any policy by unregistering and re-registering with new roles. Are there extra limitations that should be put on that endpoint? MsgRegistryBulkUpdate is in the same boat.
  2. It's possible to create two conflicting pending changes. It's probably best to limit pending changes to one per nft/asset.
  3. There needs to be a way to cancel a pending change. It also might be a good idea to have them expire after a set amount of time.

…ding on delete

RegistryBulkUpdate previously only checked NFT ownership (or hard-coded
authority addresses) before writing role sets. This allowed the bulk path to
bypass the same multi-party authorization policies enforced by GrantRole,
RevokeRole, and SetRoles. Non-authority signers updating an existing entry now
have each role change validated through ValidateRoleChangeAuthorization.

DeleteRegistry now removes all PendingRoleChange records for the deleted key so
that stale approvals cannot be replayed if the same NFT is later re-registered.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread x/registry/keeper/msg_server.go
An NFT owner could bypass multi-party role policies by unregistering and
re-registering the NFT with new roles. UnregisterNFT now checks whether a
CONTROLLER is set on the registry entry; if so, the signer must also be in
the controller list. Ownership alone is insufficient when a controller is
present.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread x/registry/keeper/msg_server.go
vwagner and others added 2 commits July 21, 2026 15:32
A second ProposeRoleChange for a different set of role updates is now
rejected when a pending change already exists for the same NFT key. This
prevents conflicting proposals from accumulating and avoids ambiguity about
which change should be applied. Re-proposing the identical role updates
(same deterministic ID) is still permitted as a co-approval path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds two new capabilities requested in review feedback:

Cancel: A new MsgCancelRoleChange endpoint lets the original proposer of a
pending role change remove it at any time. Non-proposers are rejected.
Emits EventRoleChangeCancelled on success.

Expiry: A new pending_change_expiry Duration param controls how long a
pending role change remains valid. When set, the expires_at timestamp is
recorded on the PendingRoleChange at proposal time. ApproveRoleChange
rejects and cleans up any change that has passed its expiry. A zero value
(the default) preserves the existing behavior of no expiry.

Proto changes: MsgCancelRoleChange/Response and CancelRoleChange RPC in
tx.proto; EventRoleChangeCancelled in events.proto; expires_at field on
PendingRoleChange in registry.proto; pending_change_expiry field on Params
in params.proto.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread x/registry/keeper/pending.go
Comment thread x/registry/keeper/pending.go
vwagner and others added 2 commits July 21, 2026 16:58
BulkUpdate wrong address set: ValidateRoleChangeAuthorization now receives
only the newly added addresses via additions(), matching the semantics of
GrantRole and SetRoles. Passing the full desired list was incorrectly
treating unchanged holders as incoming assignees under ASSIGNMENT_NEW*
policies.

Unregister deadlocks split owner/controller: The previous fix required the
signer to be both the NFT owner and the controller, making unregistration
impossible when the two are different accounts. Now the controller signs
when one is set (NFT ownership not required), and the NFT owner signs only
when no controller is set.

Expiry bypass via propose: ProposeRoleChange now checks expiry when
reusing an existing pending record (co-approval path), closing the bypass
where MsgProposeRoleChange could apply a change after its expiry.

Expired pending blocks proposals: ProposeRoleChange now cleans up expired
pending records eagerly instead of treating them as live blockers, so a
lapsed change no longer prevents new proposals indefinitely.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tries to use a registry class

Adds a new MsgAssociateRegistryClass message that allows an existing legacy
registry entry (one created without a registry_class_id) to be associated with
a registry class without modifying its roles.

Authorization: either the CONTROLLER role address(es) OR the scope data-owner
parties (if the NFT is a Provenance Metadata Scope) OR the NFT owner — no
precedence, any one is sufficient.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit f3310ac. Configure here.

Comment thread x/registry/keeper/nft.go
…PE_OWNER

validateAssociateRegistryClassSigner was accepting any address in
scope.Owners regardless of party role, which could allow non-data-owner
parties (e.g. PARTY_TYPE_ORIGINATOR, PARTY_TYPE_SERVICER) to associate a
registry class. Now requires party.Role == PARTY_TYPE_OWNER.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

2 participants