From c83c212f6adcf13acd2ba009a0fa22bcc5dc6844 Mon Sep 17 00:00:00 2001 From: Eric Mann Date: Thu, 14 May 2026 15:49:34 -0700 Subject: [PATCH 1/9] Add SV governance voter CIP draft Signed-off-by: Eric Mann --- .../cip-XXXX-SV-Governance-Voter.md | 457 ++++++++++++++++++ 1 file changed, 457 insertions(+) create mode 100644 cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md diff --git a/cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md b/cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md new file mode 100644 index 00000000..c20b27fd --- /dev/null +++ b/cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md @@ -0,0 +1,457 @@ +
+  CIP: ?
+  Layer: Daml
+  Title: SV Governance Voter Authority
+  Author: Avro Digital (Eric Mann)
+  Status: Draft
+  Type: Standards Track
+  Created: 2026-05-14
+  License: CC0-1.0
+
+ +## Abstract + +The current Splice SV governance flow uses the SV operator party as both the node-automation identity and the governance-voting identity. This CIP adds a first-class governance-voter authority path for a Phase 1 subset of non-operational votes. + +The intended workflow has one active governance voter per SV, declared through an `SvGovernanceVoter` binding. That voter may open and cast or update the represented SV's vote on explicitly allowlisted non-operational requests. The single-active-binding shape is preserved by the consuming `RotateGovernanceVoter` lifecycle and the self-binding onboarding default; the template itself does not enforce that invariant at the contract level (see *Open Review Questions*). The operator path continues to handle operational requests and rejects governance-voter-eligible actions; the governance-voter path rejects everything else. The vote still counts as the SV's existing vote — it does not create a second voting unit. + +The on-ledger surface is intentionally compatible with the external-party submission flow defined by [CIP-0103][cip-0103]: the governance-voter cast choice is controlled by the governance-voter party, takes plain contract IDs for the vote request and binding, and the binding is observable by the governance voter so it can be supplied as a disclosed contract. The dApp client, Scan-based discovery, and wallet/signing-provider choices live downstream of this CIP. + +[cip-0103]: ../cip-0103/cip-0103.md + +## Copyright + +This CIP is licensed under CC0-1.0: [Creative Commons CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/). + +## Specification + +This CIP covers the first contract slice needed for a separated SV governance-voter workflow: define the governance-voting authority model, classify Phase 1 vote actions, preserve one-vote-per-SV semantics, and submit a Daml reference implementation for maintainer review. The standalone dApp, Scan/read API packaging, wallet integration, deployment packaging, and UX hardening remain downstream work. + +### Review Scope + +The public review points for this CIP are: + +- An SV-declared `SvGovernanceVoter` binding authorizes a governance voter to act only on governance-voter-eligible requests for the represented SV. +- `isGovernanceVoterAction` is a hardcoded Phase 1 allowlist; new action constructors remain operator-only until deliberately reviewed. +- The contract change is limited to the adjacent governance-voter module, `DsoRules` request/cast choices, vote attribution fields, and the vote map key. +- Both authority paths write the represented SV's single vote slot; attribution records who signed, not additional weight. +- The contract surface is compatible with explicit-disclosure submission by a governance voter and leaves production read/API packaging to downstream review. +- The reference implementation includes Daml tests for binding lifecycle, action taxonomy, strict role split, attribution, cooldown, deadlines, and one-slot tallying. + +### Affected Contract Surface + +The affected upstream surface is `daml/splice-dso-governance/daml/Splice/` in the [Splice](https://github.com/canton-network/splice) repository: + +- `Splice/DSO/GovernanceVoter.daml` (new module) — `SvGovernanceVoter` template and `RotateGovernanceVoter` choice. +- `Splice/DsoRules.daml` — extended `Vote` record (with `castBy`, `castByRole`), `VoteCastRole` enum, `isGovernanceVoterAction` classifier, new `DsoRules_RequestGovernanceVote` and `DsoRules_CastGovernanceVote` choices, and the eligibility/deadline/attribution guards added to `DsoRules_RequestVote` and `DsoRules_CastVote`. + +The following existing surfaces remain operator-controlled or otherwise stable: + +- `DsoRules_RequestVote` — still operator-controlled; now rejects governance-voter-eligible actions and directs callers to `DsoRules_RequestGovernanceVote`. +- `DsoRules_CastVote` — still operator-controlled; now rejects governance-voter-eligible actions, requires the caller-supplied `Vote` to carry consistent operator attribution, and enforces an explicit request-deadline check. +- `DsoRules_ConfirmAction`, `DsoRules_ExecuteConfirmedAction`, `Confirmation`, `DsoRules_CloseVoteRequest`, `VoteRequest.trackingCid` — unchanged. + +`DsoRules_CloseVoteRequest` continues to count at most one vote per represented SV using its existing semantics; the role attribution on `Vote` changes accountability, not weight. + +### Governance-Voter Binding + +The proposal adds a separate authority contract instead of storing governance-voter state on `SvInfo`. `SvInfo` describes SV membership and operational identity. The governance voter is related to the SV but is not the operator identity. + +```daml +template SvGovernanceVoter + with + dso : Party + sv : Party + governanceVoter : Party + where + signatory sv + observer dso, governanceVoter + + ensure + sv /= dso + && governanceVoter /= dso + + choice RotateGovernanceVoter : RotateGovernanceVoterResult + with + newGovernanceVoter : Party + controller sv + do + require "New governance voter must differ" (newGovernanceVoter /= governanceVoter) + require "Governance voter must not be dso" (newGovernanceVoter /= dso) + bindingCid <- create this with governanceVoter = newGovernanceVoter + pure RotateGovernanceVoterResult with .. +``` + +Rules: + +- `sv` is the sole signatory because the SV declares who may cast its non-operational vote. The relationship between the SV and the chosen governance voter is treated as offchain trust; Phase 1 does not require on-ledger acceptance by the prospective voter. +- `governanceVoter` is an observer so wallets and external participants can inspect the binding before signing, and so the binding can be supplied as a disclosed contract on a CIP-0103-conforming submission. +- `dso` is an observer so DSO-side workflows and read APIs can discover active bindings without elevating the DSO to a signatory. +- `governanceVoter == sv` is allowed and is the onboarding default. Every SV starts with a self-binding so the represented SV is always covered. +- `governanceVoter == dso` is rejected on both create and rotate; the DSO must never appear as an SV's governance voter. +- There is intentionally **no `Clear` choice**. "Returning control to the operator" is expressed as `RotateGovernanceVoter` back to the represented SV. Without a binding nobody would be authorized to cast on governance-voter actions for the represented SV, which has no useful semantics. +- `RotateGovernanceVoter` is a consuming choice. It does not call `archive self`; Daml archives the consumed binding when the new one is created. The reference implementation also rejects no-op rotations (`newGovernanceVoter /= governanceVoter`). +- There is intentionally **no contract key**. The intended workflow keeps one active binding per `(dso, sv)`, shaped by the consuming rotation lifecycle and the self-binding onboarding default. The template does not prevent the represented SV from bare-creating additional bindings for itself; the cast guard still records one vote per represented SV under last-writer-wins, so the residual risk is observability (cast log ambiguity) rather than tally integrity. Whether the workflow shape should be promoted to a contract-level invariant — via a contract key, a DSO-owned registry, or an explicit duplicate-create guard — is left as an open question for maintainer review (see *Open Review Questions*). +- Because `sv` is the sole signatory, the implicit per-signatory `Archive` choice lets the SV unilaterally archive any of its bindings. That is self-harm only — the SV temporarily loses the ability to cast on governance-voter actions and recovers by creating a new self-binding — and is left for the SV workflow to police rather than enforced at the contract level. + +### Vote Attribution + +`Vote` is extended to carry signer attribution alongside the existing fields. Tallying continues to use `Vote.sv` (the represented SV). + +```daml +data VoteCastRole + = VCR_Operator + | VCR_GovernanceVoter + deriving (Eq, Show) + +data Vote = Vote with + sv : Party -- represented SV whose vote slot is updated + castBy : Party -- party that signed the cast + castByRole : VoteCastRole -- authority path that wrote the slot + accept : Bool + reason : Reason + optCastAt : Optional Time + deriving (Eq, Show) +``` + +Operator votes use `castBy = sv` and `castByRole = VCR_Operator`. Governance-voter votes use `castBy = governanceVoter` and `castByRole = VCR_GovernanceVoter`. + +`VoteRequest.votes` is keyed by represented SV `Party` (the reference implementation changes the map from `Map.Map Text Vote` to `Map.Map Party Vote`). Both cast paths write into the same represented-SV slot using `Map.insert binding.sv recordedVote request.votes` (operator path uses `vote.sv`). `castByRole` changes attribution, not voting weight. + +### Governance-Voter Action Classifier + +The classifier is allowlist-based. New `ActionRequiringConfirmation` constructors do not become governance-voter-eligible by default; the classifier must be extended deliberately. + +```daml +isGovernanceVoterAction : ActionRequiringConfirmation -> Bool +isGovernanceVoterAction action = + case action of + ARC_DsoRules dsoAction -> + case dsoAction of + SRARC_GrantFeaturedAppRight _ -> True + SRARC_RevokeFeaturedAppRight _ -> True + SRARC_SetConfig _ -> True + SRARC_UpdateSvRewardWeight _ -> True + SRARC_CreateUnallocatedUnclaimedActivityRecord _ -> True + SRARC_OffboardSv _ -> True + _ -> False + ARC_AmuletRules amuletAction -> + case amuletAction of + CRARC_SetConfig _ -> True + _ -> False + _ -> False +``` + +Eligibility errors on the operator path use: + +```text +"Action is governance-voter eligible; use DsoRules_RequestGovernanceVote" +"Action is governance-voter eligible; use DsoRules_CastGovernanceVote" +``` + +The symmetric errors on the governance-voter path use: + +```text +"Action must be governance-voter eligible" -- on request +"Action is not governance-voter eligible; use DsoRules_CastVote" -- on cast +``` + +These are distinct from binding, authority, and request-state errors surfaced elsewhere in the cast logic. + +### Vote Request Creation + +The operator-path `DsoRules_RequestVote` choice gains an eligibility rejection so it cannot be used to open requests for governance-voter-eligible actions: + +```daml +nonconsuming choice DsoRules_RequestVote : DsoRules_RequestVoteResult + with + requester : Party + action : ActionRequiringConfirmation + reason : Reason + voteRequestTimeout : Optional RelTime + targetEffectiveAt : Optional Time + controller requester + do + require "Action is governance-voter eligible; use DsoRules_RequestGovernanceVote" + (not (isGovernanceVoterAction action)) + -- ... existing operator-path behavior ... +``` + +A new symmetric choice handles governance-voter-eligible request creation: + +```daml +nonconsuming choice DsoRules_RequestGovernanceVote : DsoRules_RequestGovernanceVoteResult + with + governanceVoter : Party + bindingCid : ContractId SvGovernanceVoter + action : ActionRequiringConfirmation + reason : Reason + voteRequestTimeout : Optional RelTime + targetEffectiveAt : Optional Time + controller governanceVoter + do + require "Action must be governance-voter eligible" (isGovernanceVoterAction action) + binding <- fetch bindingCid + require "Binding dso must match rules dso" (binding.dso == dso) + require "Caller must match binding governance voter" + (governanceVoter == binding.governanceVoter) + requesterName <- case binding.sv `Map.lookup` svs of + None -> fail "Represented SV is not an SV" + Some info -> pure info.name + -- represented SV is taken from binding.sv; + -- requester remains the represented SV display name for existing outputs; + -- initial vote is recorded against binding.sv with VCR_GovernanceVoter. +``` + +The governance voter is the sole creator of a non-operational vote request, consistent with the design intent that operational voting remains an operator concern and non-operational voting belongs to the governance voter (which may be the SV itself under the self-binding default). + +`DsoRules_RequestGovernanceVote` records an auto-accept initial vote for the represented SV (`castBy = binding.governanceVoter`, `castByRole = VCR_GovernanceVoter`, `accept = True`, reason "I accept, as I requested the vote.") mirroring the operator path's convention on `DsoRules_RequestVote`. The initial vote occupies the represented SV's slot in `VoteRequest.votes` and may be updated through `DsoRules_CastGovernanceVote` while the request is still open. + +### Governance-Voter Cast Choice + +The cast choice mirrors the request choice. It takes the same `Vote` record as the operator path so wallets and frontends can share serialization, and the choice itself canonicalizes `sv`, `castBy`, `castByRole`, and `optCastAt` before writing the slot: + +```daml +nonconsuming choice DsoRules_CastGovernanceVote : DsoRules_CastGovernanceVoteResult + with + requestCid : ContractId VoteRequest + bindingCid : ContractId SvGovernanceVoter + vote : Vote + controller vote.castBy + do + requireWellformedVote config vote + binding <- fetch bindingCid + require "Binding dso must match rules dso" (binding.dso == dso) + require "Vote SV must match binding SV" (vote.sv == binding.sv) + require "Vote signer must match binding governance voter" + (vote.castBy == binding.governanceVoter) + require "Vote signer role must be governance voter" + (vote.castByRole == VCR_GovernanceVoter) + -- represented SV must be active + case Map.lookup binding.sv svs of + None -> fail "Voter is not an SV" + Some _ -> pure () + request <- fetchChecked (ForDso with dso) requestCid + require "Action is not governance-voter eligible; use DsoRules_CastVote" + (isGovernanceVoterAction request.action) + now <- getTime + let castDeadline = fromOptional request.voteBefore request.targetEffectiveAt + require "Vote request has expired" (now < castDeadline) + archive requestCid + -- per-represented-SV cooldown using the slot's last cast time + enforceCooldown ... + let recordedVote = vote with + sv = binding.sv + castBy = binding.governanceVoter + castByRole = VCR_GovernanceVoter + optCastAt = Some now + create request with + votes = Map.insert binding.sv recordedVote request.votes + trackingCid = Some (fromOptional requestCid request.trackingCid) +``` + +The governance-voter path is an explicit choice rather than an overload of the operator choice. Operator and governance-voter responsibilities are partitioned by the eligibility predicate (see *Operator Vote Path* below and *Strict Role Split*). + +### Operator Vote Path + +`DsoRules_CastVote` continues to work for operational actions. It gains attribution pre-validation, a request-deadline check, and the eligibility rejection that completes the strict role split: + +```daml +nonconsuming choice DsoRules_CastVote : DsoRules_CastVoteResult + with + requestCid : ContractId VoteRequest + vote : Vote + controller vote.sv + do + requireWellformedVote config vote + require "Vote castBy must match SV on operator path" + (vote.castBy == vote.sv) + require "Vote castByRole must be VCR_Operator on operator path" + (vote.castByRole == VCR_Operator) + -- ... SV membership check ... + request <- fetchAndArchive (ForDso with dso) requestCid + require "Action is governance-voter eligible; use DsoRules_CastGovernanceVote" + (not (isGovernanceVoterAction request.action)) + now <- getTime + let castDeadline = fromOptional request.voteBefore request.targetEffectiveAt + require "Vote request has expired" (now < castDeadline) + -- ... per-SV cooldown + slot write ... +``` + +Operator-side callers that already construct votes correctly are unaffected. Callers that supplied wrong attribution values were relying on the previous silent server-side overwrite and now see an explicit failure rather than a misattributed vote. + +### Strict Role Split + +Each action class is partitioned into exactly one cast path: + +- `isGovernanceVoterAction request.action == True` → the request is opened by `DsoRules_RequestGovernanceVote` and cast via `DsoRules_CastGovernanceVote`. The operator path rejects. +- `isGovernanceVoterAction request.action == False` → the request is opened by `DsoRules_RequestVote` and cast via `DsoRules_CastVote`. The governance-voter path rejects. + +There is no operator override of a governance-voter vote, and no governance-voter override of an operator vote. The represented SV's vote slot can only be written through the path that owns the request's action class. This is a deliberate change from earlier drafts that allowed operator override on the shared slot. + +### Authority Rules + +The SV/operator path remains responsible for operational and automation-oriented actions: + +- creating vote requests for operational actions, +- casting/updating votes for operational actions, +- confirming actions and executing confirmed actions, +- onboarding or activating SV membership, +- bootstrapping external-party or transfer infrastructure, +- running round lifecycle automation, +- ANS payment workflow actions, +- any action not listed by `isGovernanceVoterAction`. + +The governance voter may open a request and cast or update the represented SV's vote only when all of these are true: + +1. The represented SV has an active `SvGovernanceVoter` binding (onboarding establishes one by default). +2. The submitting party is the bound `governanceVoter`. +3. The vote request belongs to the same `dso`. +4. The represented SV is active in `DsoRules.svs`. +5. The action is allowed by `isGovernanceVoterAction`. +6. The request is still open (`now < castDeadline`). + +The governance voter does not receive general SV authority. It receives only the authority to open and cast the SV's vote on the listed non-operational governance requests. + +### Bootstrap And Lifecycle + +Phase 1 onboarding initializes each SV with `governanceVoter == sv` (a self-binding). The SV's existing operator-led workflow keeps working without an explicit setup step; the long-term model with a distinct governance-voter party is reached via `RotateGovernanceVoter`. + +Rotation applies at cast time. If the SV rotates from voter A to voter B while a request is open, voter B can update the represented SV's vote after the rotation; voter A cannot. A vote already cast by voter A remains valid as the represented SV's vote unless voter B updates it through the still-open request. + +Returning control to the operator is expressed by rotating back to the represented SV. There is no separate `Clear` operation. + +### One-Vote-Per-Node Semantics + +This proposal does not change vote weight or tallying. + +- `VoteRequest.votes` remains a per-represented-SV slot. The key changes from `Text` (SV display name) to `Party` (the represented SV), removing the dependence on display-name lookup during cast. +- Both the operator path and the governance-voter path write the represented SV's existing slot. +- Re-casting through either path updates the same slot under the existing request semantics, subject to the per-SV cooldown. +- `DsoRules_CloseVoteRequest` continues to count at most one vote per represented SV. +- There is no vote slot keyed by governance-voter party, wallet, user, or participant. + +### Visibility And Read Assumptions + +The write path is not enough. A governance voter must be able to inspect the proposal before signing and must have enough contract visibility on the submitting participant to exercise the ledger choice. + +Phase 1 proposes the following visibility position: + +- `SvGovernanceVoter` is visible to the governance voter by observer, so a CIP-0103-conforming Wallet can supply the binding as a disclosed contract. +- Proposal discovery and proposal-detail rendering are served through Scan or an SV-hosted read API rather than by making every proposal contract directly visible for browsing. +- The supported unaffiliated-voter submit path is explicit disclosure: the governance voter submits the target contract IDs together with the disclosed contracts needed to exercise `DsoRules_CastGovernanceVote`. +- SV-hosted submission or relay remains a valid deployment option, but it is not required by the on-ledger design. +- The remaining production decision is how Scan or an SV-hosted read API packages the proposal details and disclosed-contract material needed for review and submission, similar to the existing `AmuletRules` flow used by validators. + +The exact `VoteRequest` read/disclosure packaging is the main remaining boundary case. This CIP claims compatibility with external participant submission and lists the production decision as a maintainer-owned open question rather than a hidden TODO. + +### Security Considerations + +- The governance-voter path rejects unsupported action constructors by default. +- Governance voters cannot exercise `DsoRules_ConfirmAction`, `DsoRules_ExecuteConfirmedAction`, or any operator-only operational choice. +- Both cast paths reject votes after the request's deadline (`now < castDeadline`, where `castDeadline = fromOptional voteBefore targetEffectiveAt`), matching documented `DsoRules_CloseVoteRequest` semantics. +- Both cast paths enforce a per-represented-SV cooldown to rate-limit rapid re-casts. +- The operator path pre-validates `vote.castBy` and `vote.castByRole` before recording, so caller-side attribution bugs surface immediately. +- Binding rotation is checked at cast time, not only at request creation. +- Audit records distinguish operator-cast and governance-voter-cast votes via `Vote.castBy` / `Vote.castByRole`. + +## Motivation + +Governance voting and node operations are different responsibilities. + +The operator party runs or controls the SV node, signs automation commands, and participates in workflows such as confirmation and execution. A governance voter expresses the SV organization's governance intent on non-operational proposals. Those roles may be held by the same party during bootstrap, but the contract model should not require them to remain the same forever. + +Today an SV-funded organization that wants direct, auditable governance participation must hold node-operator credentials. The status quo also offers no way to distinguish, in a vote record, whether a vote was cast through an operator-automation path or by a human governance representative. + +This CIP separates governance voting from node operation on the ledger without redesigning either. The governance voter is a signer for the represented SV's vote on an explicit allowlist of non-operational actions, not a new voting unit. The SV remains the unit of voting weight; the cast simply carries an accountability stamp identifying which party signed it through which authority path. + +## Rationale + +The proposal keeps the first standards-track change narrow. It separates the governance-voting identity from the operator identity without changing voting weight, confirmation, execution, round automation, or broader governance process. + +This CIP does not standardize the standalone governance dApp, wallet/provider selection, deployment packaging, mobile or notification workflows, generalized identity, multiple voters per SV, broad rights-holder voting, or tokenomics. Those topics belong to later milestones or separate governance decisions. + +A separate `SvGovernanceVoter` contract is preferred over adding voter fields to `SvInfo` because it keeps membership and operational identity distinct from voting authority. It also gives Phase 1 a focused lifecycle for bootstrap and rotation. + +Explicit `DsoRules_RequestGovernanceVote` and `DsoRules_CastGovernanceVote` choices are preferred over overloading the operator choices because the eligibility predicate partitions every `ActionRequiringConfirmation` into exactly one path. With strict role split, the represented SV's vote slot can only be written through the path that owns the request's action class, removing ambiguity about which authority just changed a vote. + +The one-vote-per-node model is preserved by continuing to store the vote under the represented SV in `VoteRequest.votes`. The governance voter signs the SV's vote; it does not become a new voting unit. Changing the map key from `Text` to `Party` removes the indirection through `SvInfo.name` at cast time and makes the per-represented-SV slot identity unambiguous. + +`SRARC_OffboardSv` is intentionally included in the Phase 1 allowlist because offboarding is a governance-membership decision rather than a node-operation decision. Review should focus on whether the high-impact path needs extra UI warnings, reason requirements, or tests, not on silently moving it back to the operator-only bucket. + +### CIP-0103 Compatibility + +[CIP-0103][cip-0103] defines the dApp-to-Wallet API. It does not prescribe on-ledger contract patterns, but it does establish that external parties submit via `prepareExecute` with `disclosedContracts`. The contract surface in this CIP is intentionally compatible with that flow: + +- `DsoRules_CastGovernanceVote` is controlled by `vote.castBy` (the governance-voter party) and takes plain contract IDs (`requestCid`, `bindingCid`). +- The binding is observable by the governance voter, so it can be supplied as a disclosed contract by a CIP-0103-conforming Wallet. +- The cast does not require visibility on contracts unique to the SV node, so the governance voter can submit through a participant that is not the SV's participant once the read-side visibility model is settled. + +`Requires: CIP-0103` is intentionally not asserted in the preamble: the on-ledger surface defined here is independently useful and does not depend on CIP-0103 being adopted. The relationship is one of compatibility, not dependence. + +### Alternatives Considered + +- **Store the governance voter on `SvInfo`.** Rejected because it couples governance voting to the operator/member record, broadens the disclosure surface of operator records, and complicates rotation. +- **Treat the governance voter as another SV/operator authority.** Rejected because it would blur voting and automation authority. +- **Encode multiple governance voters per SV at the ledger layer.** Rejected for Phase 1. Voting weight stays at the SV and multi-user organizations are expected to map several users onto the single governance-voter party at the dApp/UI layer rather than via multiple ledger bindings. +- **Use a contract key on `(dso, sv)` for the binding.** Earlier drafts proposed this. Splice maintainers prefer to avoid keys where possible, and the consuming rotation lifecycle already preserves the intended invariant under the recommended workflow. The reference implementation omits the key; whether the invariant should be promoted is left as an open question. +- **Propose-Accept on the binding.** Adds ceremony with no Phase 1 benefit; can be layered on later as a CIP amendment without invalidating the unilateral-declaration semantics. +- **Operator override on non-operational votes.** Earlier drafts allowed this on the shared slot. Rejected on review: operational votes should be cast only by the operator, and non-operational votes should be cast only by governance parties. The strict role split makes the partition unambiguous. +- **Configurable action allowlist.** Rejected: it would let governance voters vote to expand their own authority. The classifier is hardcoded in Daml and can only be extended via a package upgrade. +- **Depend only on transaction history for attribution.** Rejected because the vote record itself should identify whether the operator or governance-voter path cast the current vote. +- **`ClearGovernanceVoter` choice on the binding.** Earlier drafts had it. Removed: leaving the represented SV without a binding has no useful semantics, and "return control to the operator" is expressed cleanly as rotating back to the represented SV. + +## Backwards compatibility + +Existing SVs continue to operate through the current operator path for operational actions. Existing confirmation, execution, close, and automation flows stay in place. + +Two `VoteRequest`/`Vote` shape changes and one choice-eligibility change deserve explicit treatment: + +- **Active-contract shape changes.** `Vote` gains two non-optional fields (`castBy`, `castByRole`), and `VoteRequest.votes` changes key from `Map.Map Text Vote` to `Map.Map Party Vote`. Both are breaking shape changes that Splice package upgrades cannot rewrite in place. The recommended migration path is for the upgrading DSO to drain in-flight `VoteRequest` contracts (close or let expire) before activating the new package, after which every freshly created `VoteRequest` and `Vote` is written under the new shape directly. Where draining is not feasible, the `Vote` attribution fields may be introduced as `Optional` (`optCastBy`, `optCastByRole`) over a migration window with non-optional fields as the steady-state target; the `votes` map key change is harder to phase in and a brief governance-vote freeze is the simpler alternative. The reference implementation uses the post-migration shapes directly because it assumes the drain-and-upgrade path. +- **Operator path eligibility rejection.** `DsoRules_RequestVote` and `DsoRules_CastVote` now reject `isGovernanceVoterAction` constructors. Any caller that was opening or casting on such actions via the operator choices must migrate to `DsoRules_RequestGovernanceVote` / `DsoRules_CastGovernanceVote`. The migration is mechanical, and the onboarding default of self-binding means every existing SV has an available binding without explicit setup. +- **`VoteRequest.votes` read-side traversal.** With the key type now `Party`, frontends or read-side code that walked the map by SV display name must look up by the SV `Party` instead. `Vote.sv` remains the represented SV `Party`; tallying logic does not change. + +The one-vote-per-SV tally is preserved exactly. `DsoRules_CloseVoteRequest` continues to count at most one vote per represented SV regardless of which path wrote it. + +No tokenomics, fees, rewards, or amulet semantics are affected. + +## Reference implementation + +The Daml reference implementation lives in [canton-network/splice#5533](https://github.com/canton-network/splice/pull/5533). Relevant artifacts on the reference branch: + +- `daml/splice-dso-governance/daml/Splice/DSO/GovernanceVoter.daml` +- `daml/splice-dso-governance/daml/Splice/DsoRules.daml` +- `daml/splice-dso-governance-test/daml/Splice/Scripts/TestGovernance.daml` +- `docs/src/sv_operator/sv_governance_voter.rst` + +### Test matrix + +| Concern | Test | +| --- | --- | +| Binding lifecycle (self → delegate → back to self), with single-binding-per-SV asserted at each step | `testSvGovernanceVoterBindingLifecycle` | +| Onboarding default self-binding | `testSvGovernanceVoterBindingLifecycle` | +| `governanceVoter == dso` rejected on create and rotate | `testSvGovernanceVoterBindingLifecycle` | +| Operator-only request only via operator path; governance-voter-only request only via governance-voter path | `testGovernanceVoterCastPath` | +| Operator-only cast only via operator path; governance-voter-only cast only via governance-voter path | `testGovernanceVoterCastPath` | +| Rotation invalidates previous governance voter | `testGovernanceVoterCastPath` | +| Action allowlist coverage across every supported/unsupported constructor | `testGovernanceVoterActionTaxonomy` | +| One vote per represented SV preserved across updates | `testVoteUpdateKeepsOneSlotPerSv` | +| Cast after `castDeadline` rejected on both paths | `testCastDeadlineExpiry` | +| Operator-path attribution pre-validation | `testOperatorCastAttributionGuards` | +| Per-SV cooldown | `testVoteCastingCooldown` | +| Offboarding the represented SV while a governance-voter request is open: subsequent `DsoRules_CastGovernanceVote` fails the "Voter is not an SV" check; the request expires naturally via `DsoRules_CloseVoteRequest`. | covered by combined membership/cast tests | + +All tests in `splice-dso-governance-test-daml/damlTest` pass on the reference branch. + +## Open Review Questions + +- **Single-binding invariant enforcement.** The intended workflow preserves "one active binding per `(dso, sv)`" through the consuming rotation lifecycle and SV onboarding default, but the template permits the represented SV to bare-create additional bindings. Should the invariant be promoted to a contract key, a DSO-owned registry contract, or an explicit duplicate-create guard inside `DsoRules`? Splice maintainers should decide based on local conventions. `testGovernanceVoterDuplicateBindingsAmbiguity` in the reference implementation pins the current last-writer-wins behavior so any future tightening has a concrete baseline. +- **`SvGovernanceVoter` creation guard.** `sv` is the sole signatory, so an arbitrary party cannot create a binding naming someone else's SV party as `sv`. The narrower issue is that any party — including parties that are not in `DsoRules.svs` — can create a binding with itself as `sv`, producing DSO-visible spam contracts. Vote-cast integrity is preserved by the `Map.lookup binding.sv svs` check inside `DsoRules_CastGovernanceVote`, so such bindings cannot influence tallies; the residual risk is purely on the DSO's observed ACS. Should the creation path be moved behind a dedicated `DsoRules_RegisterGovernanceVoter` choice that gates on DSO roster membership, or is the cast-time guard sufficient given that the DSO party already filters its observed ACS on the read side? This is independent of the single-binding question above. +- **`VoteRequest` read/disclosure packaging for non-SV participants.** Should governance voters receive direct observer visibility on open `VoteRequest` contracts, or should Scan/read-API discovery return the proposal details and disclosed-contract material needed for explicit-disclosure submission? External signing on a non-SV participant is not complete until this is settled. +- **`Vote` migration shape.** Reference implementation uses non-optional `castBy`/`castByRole` and assumes the drain-and-upgrade path described in *Backwards compatibility*. If upstream prefers an optional-fields migration step, the steady-state shape should still be non-optional. +- **`SRARC_OffboardSv` inclusion.** Included in the Phase 1 allowlist because offboarding is a governance-membership decision. Review should focus on extra warnings, reason requirements, and tests for the high-impact path rather than silently moving it back to the operator-only bucket. +- **Self-offboarding lockout.** A compromised or unresponsive governance voter for an SV could vote to block an `SRARC_OffboardSv` action that names that SV as its target. Phase 1 deliberately lets each represented SV's slot record a vote of either sign on every governance-voter eligible action, so the contract does not partition self-offboarding any differently from other actions. Should the cast path exclude an SV's binding from voting on an offboarding action that targets that SV? Possible answers: exclude at the cast guard, require an additional operator-side acknowledgment in the close path, or leave it to social/governance process. The reference implementation does none of these. + +## Changelog + +- **2026-05-14:** Initial draft. From 3e5c9cdbc569599c9044062825228bb3a3f0b805 Mon Sep 17 00:00:00 2001 From: pedrodneves Date: Fri, 15 May 2026 16:33:36 +0100 Subject: [PATCH 2/9] Update to CIP-0116 (#211) * Update cip-0116.md Signed-off-by: pedrodneves * small changes --------- Signed-off-by: pedrodneves Co-authored-by: Amanda L Martin --- cip-0116/cip-0116.md | 155 +++++++++++++++++-------------------------- 1 file changed, 61 insertions(+), 94 deletions(-) diff --git a/cip-0116/cip-0116.md b/cip-0116/cip-0116.md index 275b1d1f..10cfdb95 100644 --- a/cip-0116/cip-0116.md +++ b/cip-0116/cip-0116.md @@ -1,6 +1,6 @@
 Number: CIP-0116
-Title: Featured App Staking
+Title: Featured App Locking
 Author(s): W Eric Saraniecki  
 Type: Tokenomics  
 Status: Proposed
@@ -14,128 +14,94 @@ This proposal introduces per-party CC locking requirements for Featured App (FA)
 
 The objective is to:
 - Shift FA governance towards a market-based process
-- Allow for the temporary use of SV-locked CC to bootstrap this process
-
-The proposal is implemented in three stages:
-1. Immediate
-2. Post CIP-0104
-3. Within 90 days of CIP-0104 go-live
+- Prioritize applications that demonstrate committed capital
 
 ## Motivation
 
 Featured App designation currently relies heavily on Foundation governance.
 
 This proposal introduces objective, on-chain criteria to:
-- Reduce reliance on Foundation support
-- Allow all CC holders to participate in FA designation process
 
-Pre CIP-0104 conditions require immediate higher locking amounts due to FA Marker
-mechanics. Following CIP-0104, FA Markers are removed and all FA reward attribution is
-managed at the protocol level, allowing a transition to lower thresholds and increased reliance
-on market signals.
+- Reduce reliance on Foundation prioritization
+- Allow all CC holders to participate in FA designation process
 
 ## Specification
 
 **1. Featured App Requirements**
 
 To qualify for and maintain Featured App status, an application must:
+
   1. Maintain good standing with the Foundation
   2. Satisfy the applicable minimum CC locking requirement per PartyId
+  
 Failure to meet either condition results in loss of Featured App status.
-Locking is required per PartyId.
-
-## 2. Stage 1 — Immediate upon CIP passing
-
-**2.1 Locking Requirement**
-- Minimum: 25,000,000 CC per PartyId
-
-**2.2 Locking Mechanics**
-
-- Locking follows the same rules as Super Validator (SV) locking
-- CC must be held in a segregated, auditable PartyId and reported to the Foundation
-- Only actively locked CC counts toward FA eligibility
-
-**2.3 SV Lock Reuse**
-
-- CC used for SV locking may also be used to satisfy FA locking requirements
-- SV must report to the Foundation which apps they are actively locking on behalf of
-- SVs can use their full locked amount for backing apps
-
-**2.4 Duration**
-
-● This stage remains in effect until activation of CIP-0104
-
-## 3. Stage 2 — Post CIP-0104
 
-Upon activation of CIP-0104, FA locking transitions to a lower-threshold model.
-
-**3.1 Locking Tiers**
-- Tier 1
-  - FA Rewards capped at 80c
-  - Minimum: 1,000,000 CC
-  - Lock duration: 30 days
-- Tier 2
-  - FA Rewards capped at $1.50
-  - Minimum: 5,000,000 CC
-  - Lock duration: 365 days
-
-**3.2 SV Lock Reuse**
-- SV lock reuse remains permitted in this stage
-
-## 4. Stage 3 — Within 90 Days of CIP-0104 Go-Live
-
-SV Locking reuse is sunset 90 days after the activation of CIP-0104.
-Apps will have to find otherwise unencumbered CC to lock for the purpose of gaining or
-maintaining FA status.
-
-## 5. SV Lock Reuse Revocation
-
-If a Super Validator uses SV-locked CC to support Featured Apps, and one of its applications
-loses Featured App status:
-- The Super Validator loses the ability to use SV-locked CC to satisfy FA locking
-requirements for any additional applications going forward
-- Any app which has already gained FA status through the SV’s locked CC will remain in
-good standing
-
-If a Super Validator uses SV-locked CC to support Featured Apps, and two of its applications
-loses Featured App status:
-
-- The Super Validator loses the ability to use SV-locked CC to satisfy FA locking
-requirements for any applications
-- Any app which has already gained FA status through the SV’s locked CC will have 30
-days to find a new source of their lock
-
-If a Super Validator uses SV-locked CC to support Featured Apps, and three of its applications
-loses Featured App status:
-
-- The Super Validator loses the ability to use SV-locked CC to satisfy FA locking
-requirements for any applications
-- Any Featured Apps relying on that SV’s reused CC immediately lose Featured App
-status
-
-The Super Validator may continue to support Featured Apps using non-SV-locked CC
+Locking is required per PartyId.
 
-## 6. Enforcement
+## 2. Locking Requirements
+
+**2.1 CC Requirements**
+
+**Non-Issuer Featured App Parties**
+- **Locked:** 5,000,000 CC per PartyId
+- Follows similar process as SV Locking:
+  
+  Hold funds in a segregated, identifiable PartyId
+    - Inform Foundation
+    -  Only actively locked CC counts toward FA eligibility
+    -  60-day unlock period - vests 1/60 per day
+**Asset Issuer Featured App Parties**
+- **Locked:** 25,000,000 CC per PartyId
+- Follows similar process as SV Locking:
+
+  Hold funds in a segregated, identifiable PartyId
+    - Inform Foundation
+    - Only actively locked CC counts toward FA eligibility
+- 60-day unlock period - vests 1/60 per day
+
+**2.2 New Featured Apps**
+
+- Prior to voting:
+  - FA Applicant identifies PartyID and CC that will be locked
+  - If it meets the threshold, Foundation will order reviews based on the time at
+which assets were pre-positioned
+  - Automating this process will be presented in a future CIP
+- If FA Applicant is approved, they must lock ahead of a vote to feature their party ID
+  - Onchain vote will be gated on FA proving they have locked sufficient token
+- If an application is not approved, CC may be moved immediately
+
+**2.3 Existing Featured Apps**
+- Existing Featured Apps have 30 days from CIP approval to meet their locking
+requirements
+- Failure to meet this requirement results in loss of Featured App status
+- FA status may be regained through a new application process
+
+**2.4 FA Guideline Enforcement**
+- The Foundation will enforce proper PartyId separation as required under FA Guidelines
+- FA Status can be revoked if the locked amount falls below required thresholds or if a
+Featured App does not meet the required lock after introducing activity necessitating a
+move into a higher locking amount.
+
+## 3. Enforcement
 
 - Locking requirements must be satisfied continuously
-- If locking falls below required thresholds Featured App status is immediately removed
-- Super Validator operators are required to implement and maintain a rapid-response
-unfeaturing process
+- If locking falls below required thresholds, Featured App status is immediately removed
 
-This process must:
+Super Validator operators are required to implement and maintain a rapid-response unfeaturing
+process.
 
+This process must:
 - Allow any Super Validator to initiate an unfeature action via a standard proposal
-mechanism on the back of official notice from the Foundation
-- SV Operators must respond within 30 minutes or less from initiation of an unfeature vote
-proposal
+mechanism following official notice from the Foundation
+- Require SV Operators to respond within 30 minutes or less from initiation of an
+unfeature vote proposal
 
-## 7. Governance
+## 4. Governance
 
 The Tokenomics Working Group and Super Validators may propose updates to:
 
 - Locking thresholds
 - Lock durations
-- Lock reuse policies
 
 All updates must:
 
@@ -147,4 +113,5 @@ All updates must:
 This CIP is licensed under CC0-1.0: Creative Commons CC0 1.0 Universal.
 
 ## Changelog
+- 2026-05-15: Updated
 - 2026-05-06: Proposed

From acc628a0eb0403641c8951d159685b54960f7607 Mon Sep 17 00:00:00 2001
From: pedrodneves 
Date: Wed, 20 May 2026 13:44:10 +0100
Subject: [PATCH 3/9] Update cip-0116.md (#213)

Signed-off-by: pedrodneves 
---
 cip-0116/cip-0116.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/cip-0116/cip-0116.md b/cip-0116/cip-0116.md
index 10cfdb95..d87ec9a5 100644
--- a/cip-0116/cip-0116.md
+++ b/cip-0116/cip-0116.md
@@ -108,6 +108,7 @@ All updates must:
 - Be publicly communicated
 - Include an effective date
 - Provide reasonable notice prior to enforcement
+- Go through full governance process and SV vote
 
 ## Copyright
 This CIP is licensed under CC0-1.0: Creative Commons CC0 1.0 Universal.

From 5b7218e4c66761b51e5e2f28192d7ed7c5714873 Mon Sep 17 00:00:00 2001
From: pedrodneves 
Date: Wed, 20 May 2026 15:47:48 +0100
Subject: [PATCH 4/9] CIP-0016 Approval (#214)

* Update cip-0116.md

Signed-off-by: pedrodneves 

* Update README.md

Signed-off-by: pedrodneves 

---------

Signed-off-by: pedrodneves 
---
 README.md            | 2 +-
 cip-0116/cip-0116.md | 4 +++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index 1139e7f0..1a4becb4 100644
--- a/README.md
+++ b/README.md
@@ -109,4 +109,4 @@ Global Synchronizer CIPs
 | [cip-0113](/cip-0113/cip-0113.md) |  | Add Further Asset Management as a Super Validator (Weight 8.0) | Yiannis Varelas | Governance | Approved |
 | [cip-0114](/cip-0114/cip-0114.md) |  | Digital Asset Treasury (DAT) SV Program | Mark Wendland w/ Community proposal | Tokenomics | Approved |
 | [cip-0115](/cip-0115/cip-0115.md) |  | Add Societe Generale as a Super Validator (max weight 8) | Salim Nemouchi | Governance | Approved |
-| [cip-0116](/cip-0116/cip-0116.md) |  | Featured App Staking | Eric Saraniecki | Tokenomics | Proposed |
+| [cip-0116](/cip-0116/cip-0116.md) |  | Featured App Staking | Eric Saraniecki | Tokenomics | Approved |
diff --git a/cip-0116/cip-0116.md b/cip-0116/cip-0116.md
index d87ec9a5..4fc7d786 100644
--- a/cip-0116/cip-0116.md
+++ b/cip-0116/cip-0116.md
@@ -3,8 +3,9 @@ Number: CIP-0116
 Title: Featured App Locking
 Author(s): W Eric Saraniecki  
 Type: Tokenomics  
-Status: Proposed
+Status: Approved
 Created: 2026-01-21
+Approved: 2026-05-20
 License: CC0-1.0
 
@@ -114,5 +115,6 @@ All updates must: This CIP is licensed under CC0-1.0: Creative Commons CC0 1.0 Universal. ## Changelog +- 2026-05-20: Approved - 2026-05-15: Updated - 2026-05-06: Proposed From 21fb776d7635089398917ec8b152f5c2f7f4879f Mon Sep 17 00:00:00 2001 From: Eric Mann Date: Thu, 21 May 2026 14:35:12 -0500 Subject: [PATCH 5/9] Update SV governance voter CIP draft Signed-off-by: Eric Mann --- .../cip-XXXX-SV-Governance-Voter.md | 281 +++++++----------- 1 file changed, 105 insertions(+), 176 deletions(-) diff --git a/cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md b/cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md index c20b27fd..b3ce6fbf 100644 --- a/cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md +++ b/cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md @@ -13,9 +13,9 @@ The current Splice SV governance flow uses the SV operator party as both the node-automation identity and the governance-voting identity. This CIP adds a first-class governance-voter authority path for a Phase 1 subset of non-operational votes. -The intended workflow has one active governance voter per SV, declared through an `SvGovernanceVoter` binding. That voter may open and cast or update the represented SV's vote on explicitly allowlisted non-operational requests. The single-active-binding shape is preserved by the consuming `RotateGovernanceVoter` lifecycle and the self-binding onboarding default; the template itself does not enforce that invariant at the contract level (see *Open Review Questions*). The operator path continues to handle operational requests and rejects governance-voter-eligible actions; the governance-voter path rejects everything else. The vote still counts as the SV's existing vote — it does not create a second voting unit. +Each SV has one active governance voter declared through a DSO-signed `SvGovernanceVoter` binding. SV onboarding creates a self-binding by default, and later changes use the existing confirmation-quorum flow through a new `SRARC_RotateGovernanceVoter` action. The governance voter may open and cast or update the represented SV's vote on explicitly allowlisted non-operational requests. Operational requests remain on the operator path, and each request/cast choice rejects the wrong path. The vote still counts as the SV's existing vote — it does not create a second voting unit. -The on-ledger surface is intentionally compatible with the external-party submission flow defined by [CIP-0103][cip-0103]: the governance-voter cast choice is controlled by the governance-voter party, takes plain contract IDs for the vote request and binding, and the binding is observable by the governance voter so it can be supplied as a disclosed contract. The dApp client, Scan-based discovery, and wallet/signing-provider choices live downstream of this CIP. +The on-ledger surface is intentionally compatible with the external-party submission flow defined by [CIP-0103][cip-0103]: the existing request and cast choices take optional binding arguments, the governance-voter path is controlled by the governance-voter party, and the binding can be sourced through Scan and supplied as a disclosed contract. The dApp client, Scan-based discovery, and wallet/signing-provider choices live downstream of this CIP. [cip-0103]: ../cip-0103/cip-0103.md @@ -31,31 +31,31 @@ This CIP covers the first contract slice needed for a separated SV governance-vo The public review points for this CIP are: -- An SV-declared `SvGovernanceVoter` binding authorizes a governance voter to act only on governance-voter-eligible requests for the represented SV. +- A DSO-signed `SvGovernanceVoter` binding authorizes a governance voter to act only on governance-voter-eligible requests for the represented SV. - `isGovernanceVoterAction` is a hardcoded Phase 1 allowlist; new action constructors remain operator-only until deliberately reviewed. -- The contract change is limited to the adjacent governance-voter module, `DsoRules` request/cast choices, vote attribution fields, and the vote map key. -- Both authority paths write the represented SV's single vote slot; attribution records who signed, not additional weight. +- The contract change is limited to the adjacent governance-voter module, optional governance-voter arguments on existing `DsoRules` request/cast/close choices, vote attribution fields, and binding lifecycle choices. +- Both authority paths write the represented SV's single vote slot; attribution records who signed and which binding was used, not additional weight. - The contract surface is compatible with explicit-disclosure submission by a governance voter and leaves production read/API packaging to downstream review. -- The reference implementation includes Daml tests for binding lifecycle, action taxonomy, strict role split, attribution, cooldown, deadlines, and one-slot tallying. +- The reference implementation includes Daml tests for binding lifecycle, action taxonomy, strict role split, attribution, cooldown, deadlines, stale binding handling, binding garbage collection, and one-slot tallying. ### Affected Contract Surface The affected upstream surface is `daml/splice-dso-governance/daml/Splice/` in the [Splice](https://github.com/canton-network/splice) repository: -- `Splice/DSO/GovernanceVoter.daml` (new module) — `SvGovernanceVoter` template and `RotateGovernanceVoter` choice. -- `Splice/DsoRules.daml` — extended `Vote` record (with `castBy`, `castByRole`), `VoteCastRole` enum, `isGovernanceVoterAction` classifier, new `DsoRules_RequestGovernanceVote` and `DsoRules_CastGovernanceVote` choices, and the eligibility/deadline/attribution guards added to `DsoRules_RequestVote` and `DsoRules_CastVote`. +- `Splice/DSO/GovernanceVoter.daml` (new module) — DSO-signed `SvGovernanceVoter` template and checked-fetch instances for DSO-owned and governance-voter-owned use. +- `Splice/DsoRules.daml` — extended `Vote` record (optional `castBy`, `castByRole`, and `bindingCid`), `VoteCastRole` enum, `isGovernanceVoterAction` classifier, `SRARC_RotateGovernanceVoter` and `DsoRules_RotateGovernanceVoter`, optional governance-voter arguments on `DsoRules_RequestVote` and `DsoRules_CastVote`, stale-binding filtering in `DsoRules_CloseVoteRequest`, and `DsoRules_GarbageCollectSvGovernanceVoters`. The following existing surfaces remain operator-controlled or otherwise stable: -- `DsoRules_RequestVote` — still operator-controlled; now rejects governance-voter-eligible actions and directs callers to `DsoRules_RequestGovernanceVote`. -- `DsoRules_CastVote` — still operator-controlled; now rejects governance-voter-eligible actions, requires the caller-supplied `Vote` to carry consistent operator attribution, and enforces an explicit request-deadline check. -- `DsoRules_ConfirmAction`, `DsoRules_ExecuteConfirmedAction`, `Confirmation`, `DsoRules_CloseVoteRequest`, `VoteRequest.trackingCid` — unchanged. +- `DsoRules_RequestVote` — keeps its existing operator shape when `bindingCid = None`; `bindingCid = Some _` selects the governance-voter path. +- `DsoRules_CastVote` — keeps its existing operator shape when `bindingCid = None` and `castBy = None`; `bindingCid = Some _` and `castBy = Some _` select the governance-voter path. +- `DsoRules_ConfirmAction`, `DsoRules_ExecuteConfirmedAction`, `Confirmation`, `VoteRequest.trackingCid` — unchanged. `DsoRules_CloseVoteRequest` continues to count at most one vote per represented SV using its existing semantics; the role attribution on `Vote` changes accountability, not weight. ### Governance-Voter Binding -The proposal adds a separate authority contract instead of storing governance-voter state on `SvInfo`. `SvInfo` describes SV membership and operational identity. The governance voter is related to the SV but is not the operator identity. +The proposal adds a separate DSO-owned authority contract instead of storing governance-voter state on `SvInfo`. `SvInfo` describes SV membership and operational identity. The governance voter is related to the SV but is not the operator identity. ```daml template SvGovernanceVoter @@ -64,39 +64,24 @@ template SvGovernanceVoter sv : Party governanceVoter : Party where - signatory sv - observer dso, governanceVoter - - ensure - sv /= dso - && governanceVoter /= dso - - choice RotateGovernanceVoter : RotateGovernanceVoterResult - with - newGovernanceVoter : Party - controller sv - do - require "New governance voter must differ" (newGovernanceVoter /= governanceVoter) - require "Governance voter must not be dso" (newGovernanceVoter /= dso) - bindingCid <- create this with governanceVoter = newGovernanceVoter - pure RotateGovernanceVoterResult with .. + signatory dso + observer sv ``` Rules: -- `sv` is the sole signatory because the SV declares who may cast its non-operational vote. The relationship between the SV and the chosen governance voter is treated as offchain trust; Phase 1 does not require on-ledger acceptance by the prospective voter. -- `governanceVoter` is an observer so wallets and external participants can inspect the binding before signing, and so the binding can be supplied as a disclosed contract on a CIP-0103-conforming submission. -- `dso` is an observer so DSO-side workflows and read APIs can discover active bindings without elevating the DSO to a signatory. -- `governanceVoter == sv` is allowed and is the onboarding default. Every SV starts with a self-binding so the represented SV is always covered. -- `governanceVoter == dso` is rejected on both create and rotate; the DSO must never appear as an SV's governance voter. -- There is intentionally **no `Clear` choice**. "Returning control to the operator" is expressed as `RotateGovernanceVoter` back to the represented SV. Without a binding nobody would be authorized to cast on governance-voter actions for the represented SV, which has no useful semantics. -- `RotateGovernanceVoter` is a consuming choice. It does not call `archive self`; Daml archives the consumed binding when the new one is created. The reference implementation also rejects no-op rotations (`newGovernanceVoter /= governanceVoter`). -- There is intentionally **no contract key**. The intended workflow keeps one active binding per `(dso, sv)`, shaped by the consuming rotation lifecycle and the self-binding onboarding default. The template does not prevent the represented SV from bare-creating additional bindings for itself; the cast guard still records one vote per represented SV under last-writer-wins, so the residual risk is observability (cast log ambiguity) rather than tally integrity. Whether the workflow shape should be promoted to a contract-level invariant — via a contract key, a DSO-owned registry, or an explicit duplicate-create guard — is left as an open question for maintainer review (see *Open Review Questions*). -- Because `sv` is the sole signatory, the implicit per-signatory `Archive` choice lets the SV unilaterally archive any of its bindings. That is self-harm only — the SV temporarily loses the ability to cast on governance-voter actions and recovers by creating a new self-binding — and is left for the SV workflow to police rather than enforced at the contract level. +- `dso` is the sole signatory. The represented SV cannot bare-create, rotate, or archive its binding. +- `sv` is an observer so the represented SV can inspect its current binding. +- `governanceVoter` is intentionally not an observer. A governance-voter participant can discover the binding through Scan or receive it as a disclosed contract, without needing to vet the DSO governance DAR for this template. +- `governanceVoter == sv` is allowed and is the onboarding default. `DsoRules_AddSv` atomically creates this self-binding when an SV is onboarded. +- `governanceVoter == dso` is rejected on rotation; the DSO must never appear as an SV's governance voter. +- There is intentionally **no `Clear` choice**. "Returning control to the operator" is expressed as rotating back to the represented SV. Without a binding nobody would be authorized to cast on governance-voter actions for the represented SV. +- There is intentionally **no on-template `Rotate` choice**. Rotation is represented as the operational `SRARC_RotateGovernanceVoter` action and executed by `DsoRules_RotateGovernanceVoter`, which archives the current binding and creates the replacement binding after the standard confirmation-quorum flow. +- There is intentionally **no contract key**. The DSO-owned lifecycle preserves one active binding per represented SV through onboarding and rotation. Cleanup for duplicate or orphaned bindings left by older package versions, offboarding, or development-network re-onboarding is handled by `DsoRules_GarbageCollectSvGovernanceVoters` and SV automation. ### Vote Attribution -`Vote` is extended to carry signer attribution alongside the existing fields. Tallying continues to use `Vote.sv` (the represented SV). +`Vote` is extended to carry signer and binding attribution alongside the existing fields. The new fields are optional and appended for Daml upgrade compatibility. Tallying continues to use `Vote.sv` (the represented SV). ```daml data VoteCastRole @@ -105,18 +90,20 @@ data VoteCastRole deriving (Eq, Show) data Vote = Vote with - sv : Party -- represented SV whose vote slot is updated - castBy : Party -- party that signed the cast - castByRole : VoteCastRole -- authority path that wrote the slot + sv : Party -- represented SV whose vote slot is updated accept : Bool reason : Reason optCastAt : Optional Time + castBy : Optional Party -- party that signed the cast + castByRole : Optional VoteCastRole -- authority path that wrote the slot + bindingCid : Optional (ContractId SvGovernanceVoter) + -- binding used for governance-voter casts deriving (Eq, Show) ``` -Operator votes use `castBy = sv` and `castByRole = VCR_Operator`. Governance-voter votes use `castBy = governanceVoter` and `castByRole = VCR_GovernanceVoter`. +Operator votes use `castBy = Some sv`, `castByRole = Some VCR_Operator`, and `bindingCid = None`. Governance-voter votes use `castBy = Some governanceVoter`, `castByRole = Some VCR_GovernanceVoter`, and `bindingCid = Some bindingCid`. Legacy votes lifted from older packages may carry `None` in the appended attribution fields. -`VoteRequest.votes` is keyed by represented SV `Party` (the reference implementation changes the map from `Map.Map Text Vote` to `Map.Map Party Vote`). Both cast paths write into the same represented-SV slot using `Map.insert binding.sv recordedVote request.votes` (operator path uses `vote.sv`). `castByRole` changes attribution, not voting weight. +`VoteRequest.votes` remains keyed by SV display name `Text` for upgrade compatibility. The represented SV is still recorded in `Vote.sv`, and both cast paths write into the same represented-SV slot. `castByRole` and `bindingCid` change attribution and staleness handling, not voting weight. ### Governance-Voter Action Classifier @@ -134,7 +121,12 @@ isGovernanceVoterAction action = SRARC_UpdateSvRewardWeight _ -> True SRARC_CreateUnallocatedUnclaimedActivityRecord _ -> True SRARC_OffboardSv _ -> True - _ -> False + SRARC_AddSv _ -> False + SRARC_ConfirmSvOnboarding _ -> False + SRARC_CreateExternalPartyAmuletRules _ -> False + SRARC_CreateTransferCommandCounter _ -> False + SRARC_CreateBootstrapExternalPartyConfigStateInstruction _ -> False + SRARC_RotateGovernanceVoter _ -> False ARC_AmuletRules amuletAction -> case amuletAction of CRARC_SetConfig _ -> True @@ -145,147 +137,81 @@ isGovernanceVoterAction action = Eligibility errors on the operator path use: ```text -"Action is governance-voter eligible; use DsoRules_RequestGovernanceVote" -"Action is governance-voter eligible; use DsoRules_CastGovernanceVote" +"Action is governance-voter eligible; pass `bindingCid` to take the governance-voter path" ``` The symmetric errors on the governance-voter path use: ```text -"Action must be governance-voter eligible" -- on request -"Action is not governance-voter eligible; use DsoRules_CastVote" -- on cast +"Action is not governance-voter eligible; omit `bindingCid` for the operator path" ``` These are distinct from binding, authority, and request-state errors surfaced elsewhere in the cast logic. ### Vote Request Creation -The operator-path `DsoRules_RequestVote` choice gains an eligibility rejection so it cannot be used to open requests for governance-voter-eligible actions: +`DsoRules_RequestVote` remains the single request-creation choice. It gains an optional `bindingCid` argument appended at the end for upgrade compatibility: ```daml nonconsuming choice DsoRules_RequestVote : DsoRules_RequestVoteResult with - requester : Party + requester : Party -- represented SV on operator path; governance voter on governance-voter path action : ActionRequiringConfirmation reason : Reason voteRequestTimeout : Optional RelTime targetEffectiveAt : Optional Time + bindingCid : Optional (ContractId SvGovernanceVoter) controller requester do - require "Action is governance-voter eligible; use DsoRules_RequestGovernanceVote" - (not (isGovernanceVoterAction action)) - -- ... existing operator-path behavior ... -``` - -A new symmetric choice handles governance-voter-eligible request creation: - -```daml -nonconsuming choice DsoRules_RequestGovernanceVote : DsoRules_RequestGovernanceVoteResult - with - governanceVoter : Party - bindingCid : ContractId SvGovernanceVoter - action : ActionRequiringConfirmation - reason : Reason - voteRequestTimeout : Optional RelTime - targetEffectiveAt : Optional Time - controller governanceVoter - do - require "Action must be governance-voter eligible" (isGovernanceVoterAction action) - binding <- fetch bindingCid - require "Binding dso must match rules dso" (binding.dso == dso) - require "Caller must match binding governance voter" - (governanceVoter == binding.governanceVoter) - requesterName <- case binding.sv `Map.lookup` svs of - None -> fail "Represented SV is not an SV" - Some info -> pure info.name - -- represented SV is taken from binding.sv; - -- requester remains the represented SV display name for existing outputs; - -- initial vote is recorded against binding.sv with VCR_GovernanceVoter. + case bindingCid of + None -> do + require "Action is governance-voter eligible; pass `bindingCid` to take the governance-voter path" + (not (isGovernanceVoterAction action)) + -- requester is the represented SV; initial vote is VCR_Operator. + Some cid -> do + require "Action is not governance-voter eligible; omit `bindingCid` for the operator path" + (isGovernanceVoterAction action) + svGovernanceVoter <- fetchChecked (ForOwner with dso; owner = requester) cid + -- requester is the governance voter; represented SV is taken from the binding. + -- initial vote is VCR_GovernanceVoter and records bindingCid = Some cid. ``` -The governance voter is the sole creator of a non-operational vote request, consistent with the design intent that operational voting remains an operator concern and non-operational voting belongs to the governance voter (which may be the SV itself under the self-binding default). - -`DsoRules_RequestGovernanceVote` records an auto-accept initial vote for the represented SV (`castBy = binding.governanceVoter`, `castByRole = VCR_GovernanceVoter`, `accept = True`, reason "I accept, as I requested the vote.") mirroring the operator path's convention on `DsoRules_RequestVote`. The initial vote occupies the represented SV's slot in `VoteRequest.votes` and may be updated through `DsoRules_CastGovernanceVote` while the request is still open. - -### Governance-Voter Cast Choice - -The cast choice mirrors the request choice. It takes the same `Vote` record as the operator path so wallets and frontends can share serialization, and the choice itself canonicalizes `sv`, `castBy`, `castByRole`, and `optCastAt` before writing the slot: +The governance voter is the creator of a non-operational vote request, consistent with the design intent that operational voting remains an operator concern and non-operational voting belongs to the governance voter (which may be the SV itself under the self-binding default). -```daml -nonconsuming choice DsoRules_CastGovernanceVote : DsoRules_CastGovernanceVoteResult - with - requestCid : ContractId VoteRequest - bindingCid : ContractId SvGovernanceVoter - vote : Vote - controller vote.castBy - do - requireWellformedVote config vote - binding <- fetch bindingCid - require "Binding dso must match rules dso" (binding.dso == dso) - require "Vote SV must match binding SV" (vote.sv == binding.sv) - require "Vote signer must match binding governance voter" - (vote.castBy == binding.governanceVoter) - require "Vote signer role must be governance voter" - (vote.castByRole == VCR_GovernanceVoter) - -- represented SV must be active - case Map.lookup binding.sv svs of - None -> fail "Voter is not an SV" - Some _ -> pure () - request <- fetchChecked (ForDso with dso) requestCid - require "Action is not governance-voter eligible; use DsoRules_CastVote" - (isGovernanceVoterAction request.action) - now <- getTime - let castDeadline = fromOptional request.voteBefore request.targetEffectiveAt - require "Vote request has expired" (now < castDeadline) - archive requestCid - -- per-represented-SV cooldown using the slot's last cast time - enforceCooldown ... - let recordedVote = vote with - sv = binding.sv - castBy = binding.governanceVoter - castByRole = VCR_GovernanceVoter - optCastAt = Some now - create request with - votes = Map.insert binding.sv recordedVote request.votes - trackingCid = Some (fromOptional requestCid request.trackingCid) -``` +On the governance-voter path, `DsoRules_RequestVote` records an auto-accept initial vote for the represented SV (`castBy = Some governanceVoter`, `castByRole = Some VCR_GovernanceVoter`, `bindingCid = Some cid`, `accept = True`, reason "I accept, as I requested the vote.") mirroring the operator path's convention. The initial vote occupies the represented SV's slot in `VoteRequest.votes` and may be updated through `DsoRules_CastVote` while the request is still open. -The governance-voter path is an explicit choice rather than an overload of the operator choice. Operator and governance-voter responsibilities are partitioned by the eligibility predicate (see *Operator Vote Path* below and *Strict Role Split*). +### Vote Cast Choice -### Operator Vote Path - -`DsoRules_CastVote` continues to work for operational actions. It gains attribution pre-validation, a request-deadline check, and the eligibility rejection that completes the strict role split: +`DsoRules_CastVote` remains the single cast/update choice. It gains optional `bindingCid` and `castBy` arguments appended at the end for upgrade compatibility. Both must be absent on the operator path and present on the governance-voter path: ```daml nonconsuming choice DsoRules_CastVote : DsoRules_CastVoteResult with - requestCid : ContractId VoteRequest - vote : Vote - controller vote.sv + requestCid : ContractId VoteRequest + vote : Vote + bindingCid : Optional (ContractId SvGovernanceVoter) + castBy : Optional Party + controller fromOptional vote.sv castBy do requireWellformedVote config vote - require "Vote castBy must match SV on operator path" - (vote.castBy == vote.sv) - require "Vote castByRole must be VCR_Operator on operator path" - (vote.castByRole == VCR_Operator) - -- ... SV membership check ... - request <- fetchAndArchive (ForDso with dso) requestCid - require "Action is governance-voter eligible; use DsoRules_CastGovernanceVote" - (not (isGovernanceVoterAction request.action)) + -- bindingCid and castBy are both None (operator) or both Some (governance voter). + -- governance-voter path uses fetchChecked (ForOwner with dso; owner = castBy) bindingCid. + request <- fetchChecked (ForDso with dso) requestCid + -- action eligibility, deadline, cooldown, archive, and slot update are shared. now <- getTime let castDeadline = fromOptional request.voteBefore request.targetEffectiveAt require "Vote request has expired" (now < castDeadline) - -- ... per-SV cooldown + slot write ... + -- recordedVote canonicalizes sv, castBy, castByRole, bindingCid, and optCastAt. ``` -Operator-side callers that already construct votes correctly are unaffected. Callers that supplied wrong attribution values were relying on the previous silent server-side overwrite and now see an explicit failure rather than a misattributed vote. +The operator path records `Some VCR_Operator`; the governance-voter path records `Some VCR_GovernanceVoter` and the binding used for the cast. The choice canonicalizes attribution before writing, so caller-supplied attribution metadata is not trusted for authorization. ### Strict Role Split Each action class is partitioned into exactly one cast path: -- `isGovernanceVoterAction request.action == True` → the request is opened by `DsoRules_RequestGovernanceVote` and cast via `DsoRules_CastGovernanceVote`. The operator path rejects. -- `isGovernanceVoterAction request.action == False` → the request is opened by `DsoRules_RequestVote` and cast via `DsoRules_CastVote`. The governance-voter path rejects. +- `isGovernanceVoterAction request.action == True` → request and cast use `DsoRules_RequestVote` / `DsoRules_CastVote` with `bindingCid = Some _` (and `castBy = Some _` on cast). The operator path rejects. +- `isGovernanceVoterAction request.action == False` → request and cast use `DsoRules_RequestVote` / `DsoRules_CastVote` with `bindingCid = None` (and `castBy = None` on cast). The governance-voter path rejects. There is no operator override of a governance-voter vote, and no governance-voter override of an operator vote. The represented SV's vote slot can only be written through the path that owns the request's action class. This is a deliberate change from earlier drafts that allowed operator override on the shared slot. @@ -297,6 +223,7 @@ The SV/operator path remains responsible for operational and automation-oriented - casting/updating votes for operational actions, - confirming actions and executing confirmed actions, - onboarding or activating SV membership, +- rotating governance-voter bindings through `SRARC_RotateGovernanceVoter`, - bootstrapping external-party or transfer infrastructure, - running round lifecycle automation, - ANS payment workflow actions, @@ -315,20 +242,23 @@ The governance voter does not receive general SV authority. It receives only the ### Bootstrap And Lifecycle -Phase 1 onboarding initializes each SV with `governanceVoter == sv` (a self-binding). The SV's existing operator-led workflow keeps working without an explicit setup step; the long-term model with a distinct governance-voter party is reached via `RotateGovernanceVoter`. +Phase 1 onboarding initializes each SV with `governanceVoter == sv` (a self-binding). The SV's existing operator-led workflow keeps working without an explicit setup step; the long-term model with a distinct governance-voter party is reached via the operational `SRARC_RotateGovernanceVoter` action and `DsoRules_RotateGovernanceVoter` choice. -Rotation applies at cast time. If the SV rotates from voter A to voter B while a request is open, voter B can update the represented SV's vote after the rotation; voter A cannot. A vote already cast by voter A remains valid as the represented SV's vote unless voter B updates it through the still-open request. +Rotation applies at cast time and close time. If the DSO rotates an SV from voter A to voter B while a request is open, voter B can update the represented SV's vote after the rotation; voter A cannot. A vote already cast by voter A records the binding it was cast under. When `DsoRules_CloseVoteRequest` is invoked with `currentBindings = Some [...]`, the close logic drops governance-voter-cast votes whose recorded binding is no longer the live binding for the represented SV and reports them in `staleBindingVoters`. `currentBindings = None` preserves pre-staleness behavior for callers that have not adopted the new argument. Returning control to the operator is expressed by rotating back to the represented SV. There is no separate `Clear` operation. +Bindings for removed SVs, and duplicate bindings left by older package versions or development-network re-onboarding, are cleaned up by `DsoRules_GarbageCollectSvGovernanceVoters` and SV automation. + ### One-Vote-Per-Node Semantics This proposal does not change vote weight or tallying. -- `VoteRequest.votes` remains a per-represented-SV slot. The key changes from `Text` (SV display name) to `Party` (the represented SV), removing the dependence on display-name lookup during cast. +- `VoteRequest.votes` remains a per-represented-SV slot keyed by SV display-name `Text` for upgrade compatibility. The represented SV remains available as `Vote.sv`. - Both the operator path and the governance-voter path write the represented SV's existing slot. - Re-casting through either path updates the same slot under the existing request semantics, subject to the per-SV cooldown. - `DsoRules_CloseVoteRequest` continues to count at most one vote per represented SV. +- When supplied with the live binding set, `DsoRules_CloseVoteRequest` drops governance-voter votes cast under stale bindings before tallying. - There is no vote slot keyed by governance-voter party, wallet, user, or participant. ### Visibility And Read Assumptions @@ -337,23 +267,25 @@ The write path is not enough. A governance voter must be able to inspect the pro Phase 1 proposes the following visibility position: -- `SvGovernanceVoter` is visible to the governance voter by observer, so a CIP-0103-conforming Wallet can supply the binding as a disclosed contract. +- `SvGovernanceVoter` is not visible to the governance voter by observer. The governance voter discovers the binding through Scan/read APIs or receives it as a disclosed contract. - Proposal discovery and proposal-detail rendering are served through Scan or an SV-hosted read API rather than by making every proposal contract directly visible for browsing. -- The supported unaffiliated-voter submit path is explicit disclosure: the governance voter submits the target contract IDs together with the disclosed contracts needed to exercise `DsoRules_CastGovernanceVote`. +- The supported unaffiliated-voter submit path is explicit disclosure: the governance voter submits the target contract IDs together with the disclosed contracts needed to exercise `DsoRules_CastVote` on the governance-voter path. - SV-hosted submission or relay remains a valid deployment option, but it is not required by the on-ledger design. -- The remaining production decision is how Scan or an SV-hosted read API packages the proposal details and disclosed-contract material needed for review and submission, similar to the existing `AmuletRules` flow used by validators. +- The remaining production packaging work is for Scan or an SV-hosted read API to return the proposal details, binding information, and disclosed-contract material needed for review and submission, similar to the existing `AmuletRules` flow used by validators. -The exact `VoteRequest` read/disclosure packaging is the main remaining boundary case. This CIP claims compatibility with external participant submission and lists the production decision as a maintainer-owned open question rather than a hidden TODO. +This CIP claims compatibility with external participant submission and leaves the exact read/disclosure API packaging to downstream implementation. ### Security Considerations - The governance-voter path rejects unsupported action constructors by default. - Governance voters cannot exercise `DsoRules_ConfirmAction`, `DsoRules_ExecuteConfirmedAction`, or any operator-only operational choice. +- Governance-voter binding rotation is operational: it uses `SRARC_RotateGovernanceVoter` and the standard confirmation-quorum flow, not unilateral SV action. - Both cast paths reject votes after the request's deadline (`now < castDeadline`, where `castDeadline = fromOptional voteBefore targetEffectiveAt`), matching documented `DsoRules_CloseVoteRequest` semantics. - Both cast paths enforce a per-represented-SV cooldown to rate-limit rapid re-casts. -- The operator path pre-validates `vote.castBy` and `vote.castByRole` before recording, so caller-side attribution bugs surface immediately. -- Binding rotation is checked at cast time, not only at request creation. +- The cast choice canonicalizes `castBy`, `castByRole`, and `bindingCid` before recording. +- Binding rotation is checked at cast time and, when `currentBindings` is supplied, at close time. - Audit records distinguish operator-cast and governance-voter-cast votes via `Vote.castBy` / `Vote.castByRole`. +- `SRARC_OffboardSv` is intentionally not special-cased when the represented SV is the offboarding target. The represented SV's vote remains its vote; changing target-party voting rights is a broader governance-process question outside this authority-splitting CIP. ## Motivation @@ -371,20 +303,20 @@ The proposal keeps the first standards-track change narrow. It separates the gov This CIP does not standardize the standalone governance dApp, wallet/provider selection, deployment packaging, mobile or notification workflows, generalized identity, multiple voters per SV, broad rights-holder voting, or tokenomics. Those topics belong to later milestones or separate governance decisions. -A separate `SvGovernanceVoter` contract is preferred over adding voter fields to `SvInfo` because it keeps membership and operational identity distinct from voting authority. It also gives Phase 1 a focused lifecycle for bootstrap and rotation. +A separate `SvGovernanceVoter` contract is preferred over adding voter fields to `SvInfo` because it keeps membership and operational identity distinct from voting authority. Making the DSO the signatory keeps binding lifecycle under `DsoRules`, where onboarding, confirmation-quorum rotation, stale-binding checks, and cleanup can be enforced consistently. -Explicit `DsoRules_RequestGovernanceVote` and `DsoRules_CastGovernanceVote` choices are preferred over overloading the operator choices because the eligibility predicate partitions every `ActionRequiringConfirmation` into exactly one path. With strict role split, the represented SV's vote slot can only be written through the path that owns the request's action class, removing ambiguity about which authority just changed a vote. +Optional governance-voter arguments on `DsoRules_RequestVote` and `DsoRules_CastVote` are preferred over separate governance-voter choices because they preserve upgrade compatibility for existing choice callers while still making the authority path explicit. The eligibility predicate partitions every `ActionRequiringConfirmation` into exactly one path. With strict role split, the represented SV's vote slot can only be written through the path that owns the request's action class, removing ambiguity about which authority just changed a vote. -The one-vote-per-node model is preserved by continuing to store the vote under the represented SV in `VoteRequest.votes`. The governance voter signs the SV's vote; it does not become a new voting unit. Changing the map key from `Text` to `Party` removes the indirection through `SvInfo.name` at cast time and makes the per-represented-SV slot identity unambiguous. +The one-vote-per-node model is preserved by continuing to store the vote under the represented SV's existing `VoteRequest.votes` slot. The governance voter signs the SV's vote; it does not become a new voting unit. The map key remains `Text` for upgrade compatibility, while `Vote.sv` carries the represented SV party used for tallying and staleness checks. -`SRARC_OffboardSv` is intentionally included in the Phase 1 allowlist because offboarding is a governance-membership decision rather than a node-operation decision. Review should focus on whether the high-impact path needs extra UI warnings, reason requirements, or tests, not on silently moving it back to the operator-only bucket. +`SRARC_OffboardSv` is intentionally included in the Phase 1 allowlist because offboarding is a governance-membership decision rather than a node-operation decision. The high-impact path should have clear UI warnings, reason quality expectations, and tests, but it should not silently move back to the operator-only bucket. This CIP also does not exclude the target SV from voting on its own offboarding; it preserves the current represented-SV voting semantics and treats any target-party voting restriction as a separate governance-process decision. ### CIP-0103 Compatibility [CIP-0103][cip-0103] defines the dApp-to-Wallet API. It does not prescribe on-ledger contract patterns, but it does establish that external parties submit via `prepareExecute` with `disclosedContracts`. The contract surface in this CIP is intentionally compatible with that flow: -- `DsoRules_CastGovernanceVote` is controlled by `vote.castBy` (the governance-voter party) and takes plain contract IDs (`requestCid`, `bindingCid`). -- The binding is observable by the governance voter, so it can be supplied as a disclosed contract by a CIP-0103-conforming Wallet. +- `DsoRules_CastVote` is controlled by `fromOptional vote.sv castBy`; on the governance-voter path, `castBy = Some governanceVoter` and `bindingCid = Some binding`. +- The binding can be sourced through Scan and supplied as a disclosed contract by a CIP-0103-conforming Wallet. - The cast does not require visibility on contracts unique to the SV node, so the governance voter can submit through a participant that is not the SV's participant once the read-side visibility model is settled. `Requires: CIP-0103` is intentionally not asserted in the preamble: the on-ledger surface defined here is independently useful and does not depend on CIP-0103 being adopted. The relationship is one of compatibility, not dependence. @@ -394,7 +326,7 @@ The one-vote-per-node model is preserved by continuing to store the vote under t - **Store the governance voter on `SvInfo`.** Rejected because it couples governance voting to the operator/member record, broadens the disclosure surface of operator records, and complicates rotation. - **Treat the governance voter as another SV/operator authority.** Rejected because it would blur voting and automation authority. - **Encode multiple governance voters per SV at the ledger layer.** Rejected for Phase 1. Voting weight stays at the SV and multi-user organizations are expected to map several users onto the single governance-voter party at the dApp/UI layer rather than via multiple ledger bindings. -- **Use a contract key on `(dso, sv)` for the binding.** Earlier drafts proposed this. Splice maintainers prefer to avoid keys where possible, and the consuming rotation lifecycle already preserves the intended invariant under the recommended workflow. The reference implementation omits the key; whether the invariant should be promoted is left as an open question. +- **Use a contract key on `(dso, sv)` for the binding.** Earlier drafts proposed this. The reference implementation omits the key and instead keeps lifecycle control inside `DsoRules`, with onboarding/rotation preserving the intended invariant and garbage collection handling duplicates from older package versions or development-network re-onboarding. - **Propose-Accept on the binding.** Adds ceremony with no Phase 1 benefit; can be layered on later as a CIP amendment without invalidating the unilateral-declaration semantics. - **Operator override on non-operational votes.** Earlier drafts allowed this on the shared slot. Rejected on review: operational votes should be cast only by the operator, and non-operational votes should be cast only by governance parties. The strict role split makes the partition unambiguous. - **Configurable action allowlist.** Rejected: it would let governance voters vote to expand their own authority. The classifier is hardcoded in Daml and can only be extended via a package upgrade. @@ -405,11 +337,13 @@ The one-vote-per-node model is preserved by continuing to store the vote under t Existing SVs continue to operate through the current operator path for operational actions. Existing confirmation, execution, close, and automation flows stay in place. -Two `VoteRequest`/`Vote` shape changes and one choice-eligibility change deserve explicit treatment: +The implementation is structured to preserve Daml upgrade compatibility: -- **Active-contract shape changes.** `Vote` gains two non-optional fields (`castBy`, `castByRole`), and `VoteRequest.votes` changes key from `Map.Map Text Vote` to `Map.Map Party Vote`. Both are breaking shape changes that Splice package upgrades cannot rewrite in place. The recommended migration path is for the upgrading DSO to drain in-flight `VoteRequest` contracts (close or let expire) before activating the new package, after which every freshly created `VoteRequest` and `Vote` is written under the new shape directly. Where draining is not feasible, the `Vote` attribution fields may be introduced as `Optional` (`optCastBy`, `optCastByRole`) over a migration window with non-optional fields as the steady-state target; the `votes` map key change is harder to phase in and a brief governance-vote freeze is the simpler alternative. The reference implementation uses the post-migration shapes directly because it assumes the drain-and-upgrade path. -- **Operator path eligibility rejection.** `DsoRules_RequestVote` and `DsoRules_CastVote` now reject `isGovernanceVoterAction` constructors. Any caller that was opening or casting on such actions via the operator choices must migrate to `DsoRules_RequestGovernanceVote` / `DsoRules_CastGovernanceVote`. The migration is mechanical, and the onboarding default of self-binding means every existing SV has an available binding without explicit setup. -- **`VoteRequest.votes` read-side traversal.** With the key type now `Party`, frontends or read-side code that walked the map by SV display name must look up by the SV `Party` instead. `Vote.sv` remains the represented SV `Party`; tallying logic does not change. +- **`Vote` attribution.** `Vote` gains optional trailing fields (`castBy`, `castByRole`, and `bindingCid`). Existing contracts lift with `None`; new votes are recorded with `Some` attribution. +- **`VoteRequest.votes` key.** The map remains `Map.Map Text Vote`, preserving existing active-contract shape. `Vote.sv` continues to identify the represented SV party. +- **Choice arguments.** `DsoRules_RequestVote`, `DsoRules_CastVote`, and `DsoRules_CloseVoteRequest` gain optional trailing arguments. Existing callers can continue to use the operator/back-compat path by passing `None`. +- **Close result.** `DsoRules_CloseVoteRequestResult` gains optional trailing `staleBindingVoters`. `None` means no staleness check ran; `Some []` means the check ran and dropped no voters. +- **Operator path eligibility rejection.** `DsoRules_RequestVote` and `DsoRules_CastVote` now reject `isGovernanceVoterAction` constructors when called with `bindingCid = None`. Any caller opening or casting on such actions must pass the governance-voter binding. The one-vote-per-SV tally is preserved exactly. `DsoRules_CloseVoteRequest` continues to count at most one vote per represented SV regardless of which path wrote it. @@ -428,30 +362,25 @@ The Daml reference implementation lives in [canton-network/splice#5533](https:// | Concern | Test | | --- | --- | -| Binding lifecycle (self → delegate → back to self), with single-binding-per-SV asserted at each step | `testSvGovernanceVoterBindingLifecycle` | +| Binding lifecycle (self → delegate → back to self), using confirmation-quorum rotation | `testSvGovernanceVoterBindingLifecycle` | | Onboarding default self-binding | `testSvGovernanceVoterBindingLifecycle` | -| `governanceVoter == dso` rejected on create and rotate | `testSvGovernanceVoterBindingLifecycle` | +| Represented SV cannot bare-create or duplicate its own binding | `testSvGovernanceVoterBindingLifecycle` | +| `governanceVoter == dso` rejected on rotate | `testSvGovernanceVoterBindingLifecycle` | | Operator-only request only via operator path; governance-voter-only request only via governance-voter path | `testGovernanceVoterCastPath` | | Operator-only cast only via operator path; governance-voter-only cast only via governance-voter path | `testGovernanceVoterCastPath` | | Rotation invalidates previous governance voter | `testGovernanceVoterCastPath` | | Action allowlist coverage across every supported/unsupported constructor | `testGovernanceVoterActionTaxonomy` | | One vote per represented SV preserved across updates | `testVoteUpdateKeepsOneSlotPerSv` | | Cast after `castDeadline` rejected on both paths | `testCastDeadlineExpiry` | -| Operator-path attribution pre-validation | `testOperatorCastAttributionGuards` | | Per-SV cooldown | `testVoteCastingCooldown` | -| Offboarding the represented SV while a governance-voter request is open: subsequent `DsoRules_CastGovernanceVote` fails the "Voter is not an SV" check; the request expires naturally via `DsoRules_CloseVoteRequest`. | covered by combined membership/cast tests | +| Governance-voter vote cast under a rotated binding is dropped when close supplies live bindings | `testStaleBindingDropsVote` | +| Close-vote staleness check is opt-in for back-compat callers | `testStalenessCheckOptIn` | +| Duplicate and orphaned governance-voter bindings can be garbage-collected | `testGarbageCollectSvGovernanceVoters` | +| Offboarding the represented SV while a governance-voter request is open: subsequent governance-voter cast fails the "Voter is not an SV" check; the request expires naturally via `DsoRules_CloseVoteRequest`. | covered by combined membership/cast tests | All tests in `splice-dso-governance-test-daml/damlTest` pass on the reference branch. -## Open Review Questions - -- **Single-binding invariant enforcement.** The intended workflow preserves "one active binding per `(dso, sv)`" through the consuming rotation lifecycle and SV onboarding default, but the template permits the represented SV to bare-create additional bindings. Should the invariant be promoted to a contract key, a DSO-owned registry contract, or an explicit duplicate-create guard inside `DsoRules`? Splice maintainers should decide based on local conventions. `testGovernanceVoterDuplicateBindingsAmbiguity` in the reference implementation pins the current last-writer-wins behavior so any future tightening has a concrete baseline. -- **`SvGovernanceVoter` creation guard.** `sv` is the sole signatory, so an arbitrary party cannot create a binding naming someone else's SV party as `sv`. The narrower issue is that any party — including parties that are not in `DsoRules.svs` — can create a binding with itself as `sv`, producing DSO-visible spam contracts. Vote-cast integrity is preserved by the `Map.lookup binding.sv svs` check inside `DsoRules_CastGovernanceVote`, so such bindings cannot influence tallies; the residual risk is purely on the DSO's observed ACS. Should the creation path be moved behind a dedicated `DsoRules_RegisterGovernanceVoter` choice that gates on DSO roster membership, or is the cast-time guard sufficient given that the DSO party already filters its observed ACS on the read side? This is independent of the single-binding question above. -- **`VoteRequest` read/disclosure packaging for non-SV participants.** Should governance voters receive direct observer visibility on open `VoteRequest` contracts, or should Scan/read-API discovery return the proposal details and disclosed-contract material needed for explicit-disclosure submission? External signing on a non-SV participant is not complete until this is settled. -- **`Vote` migration shape.** Reference implementation uses non-optional `castBy`/`castByRole` and assumes the drain-and-upgrade path described in *Backwards compatibility*. If upstream prefers an optional-fields migration step, the steady-state shape should still be non-optional. -- **`SRARC_OffboardSv` inclusion.** Included in the Phase 1 allowlist because offboarding is a governance-membership decision. Review should focus on extra warnings, reason requirements, and tests for the high-impact path rather than silently moving it back to the operator-only bucket. -- **Self-offboarding lockout.** A compromised or unresponsive governance voter for an SV could vote to block an `SRARC_OffboardSv` action that names that SV as its target. Phase 1 deliberately lets each represented SV's slot record a vote of either sign on every governance-voter eligible action, so the contract does not partition self-offboarding any differently from other actions. Should the cast path exclude an SV's binding from voting on an offboarding action that targets that SV? Possible answers: exclude at the cast guard, require an additional operator-side acknowledgment in the close path, or leave it to social/governance process. The reference implementation does none of these. - ## Changelog +- **2026-05-21:** Reconciled draft with updated reference implementation. - **2026-05-14:** Initial draft. From c68396b59e333692831facbd5a198d9e1a6e4337 Mon Sep 17 00:00:00 2001 From: Eric Mann Date: Thu, 21 May 2026 14:42:04 -0500 Subject: [PATCH 6/9] Tighten SV governance voter CIP draft --- .../cip-XXXX-SV-Governance-Voter.md | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md b/cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md index b3ce6fbf..33b6e7d7 100644 --- a/cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md +++ b/cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md @@ -25,17 +25,17 @@ This CIP is licensed under CC0-1.0: [Creative Commons CC0 1.0 Universal](https:/ ## Specification -This CIP covers the first contract slice needed for a separated SV governance-voter workflow: define the governance-voting authority model, classify Phase 1 vote actions, preserve one-vote-per-SV semantics, and submit a Daml reference implementation for maintainer review. The standalone dApp, Scan/read API packaging, wallet integration, deployment packaging, and UX hardening remain downstream work. +This CIP covers the first contract slice needed for a separated SV governance-voter workflow: define the governance-voting authority model, classify Phase 1 vote actions, preserve one-vote-per-SV semantics, and provide a Daml reference implementation. The standalone dApp, Scan/read API packaging, wallet integration, deployment packaging, and UX hardening remain downstream work. -### Review Scope +### Scope -The public review points for this CIP are: +This CIP standardizes the following contract-level behavior: - A DSO-signed `SvGovernanceVoter` binding authorizes a governance voter to act only on governance-voter-eligible requests for the represented SV. -- `isGovernanceVoterAction` is a hardcoded Phase 1 allowlist; new action constructors remain operator-only until deliberately reviewed. +- `isGovernanceVoterAction` is a hardcoded Phase 1 allowlist; new action constructors remain operator-only until explicitly classified. - The contract change is limited to the adjacent governance-voter module, optional governance-voter arguments on existing `DsoRules` request/cast/close choices, vote attribution fields, and binding lifecycle choices. - Both authority paths write the represented SV's single vote slot; attribution records who signed and which binding was used, not additional weight. -- The contract surface is compatible with explicit-disclosure submission by a governance voter and leaves production read/API packaging to downstream review. +- The contract surface is compatible with explicit-disclosure submission by a governance voter; production read/API packaging is downstream implementation work. - The reference implementation includes Daml tests for binding lifecycle, action taxonomy, strict role split, attribution, cooldown, deadlines, stale binding handling, binding garbage collection, and one-slot tallying. ### Affected Contract Surface @@ -55,7 +55,7 @@ The following existing surfaces remain operator-controlled or otherwise stable: ### Governance-Voter Binding -The proposal adds a separate DSO-owned authority contract instead of storing governance-voter state on `SvInfo`. `SvInfo` describes SV membership and operational identity. The governance voter is related to the SV but is not the operator identity. +This CIP adds a separate DSO-owned authority contract instead of storing governance-voter state on `SvInfo`. `SvInfo` describes SV membership and operational identity. The governance voter is related to the SV but is not the operator identity. ```daml template SvGovernanceVoter @@ -252,7 +252,7 @@ Bindings for removed SVs, and duplicate bindings left by older package versions ### One-Vote-Per-Node Semantics -This proposal does not change vote weight or tallying. +This CIP does not change vote weight or tallying. - `VoteRequest.votes` remains a per-represented-SV slot keyed by SV display-name `Text` for upgrade compatibility. The represented SV remains available as `Vote.sv`. - Both the operator path and the governance-voter path write the represented SV's existing slot. @@ -265,15 +265,15 @@ This proposal does not change vote weight or tallying. The write path is not enough. A governance voter must be able to inspect the proposal before signing and must have enough contract visibility on the submitting participant to exercise the ledger choice. -Phase 1 proposes the following visibility position: +Phase 1 uses the following visibility position: - `SvGovernanceVoter` is not visible to the governance voter by observer. The governance voter discovers the binding through Scan/read APIs or receives it as a disclosed contract. - Proposal discovery and proposal-detail rendering are served through Scan or an SV-hosted read API rather than by making every proposal contract directly visible for browsing. - The supported unaffiliated-voter submit path is explicit disclosure: the governance voter submits the target contract IDs together with the disclosed contracts needed to exercise `DsoRules_CastVote` on the governance-voter path. - SV-hosted submission or relay remains a valid deployment option, but it is not required by the on-ledger design. -- The remaining production packaging work is for Scan or an SV-hosted read API to return the proposal details, binding information, and disclosed-contract material needed for review and submission, similar to the existing `AmuletRules` flow used by validators. +- Scan or an SV-hosted read API returns the proposal details, binding information, and disclosed-contract material needed for proposal inspection and submission, similar to the existing `AmuletRules` flow used by validators. -This CIP claims compatibility with external participant submission and leaves the exact read/disclosure API packaging to downstream implementation. +This CIP is compatible with external participant submission and leaves the exact read/disclosure API packaging to downstream implementation. ### Security Considerations @@ -285,7 +285,7 @@ This CIP claims compatibility with external participant submission and leaves th - The cast choice canonicalizes `castBy`, `castByRole`, and `bindingCid` before recording. - Binding rotation is checked at cast time and, when `currentBindings` is supplied, at close time. - Audit records distinguish operator-cast and governance-voter-cast votes via `Vote.castBy` / `Vote.castByRole`. -- `SRARC_OffboardSv` is intentionally not special-cased when the represented SV is the offboarding target. The represented SV's vote remains its vote; changing target-party voting rights is a broader governance-process question outside this authority-splitting CIP. +- `SRARC_OffboardSv` is intentionally not special-cased when the represented SV is the offboarding target. The represented SV's vote remains its vote; changing target-party voting rights is a broader governance-process decision outside this authority-splitting CIP. ## Motivation @@ -299,7 +299,7 @@ This CIP separates governance voting from node operation on the ledger without r ## Rationale -The proposal keeps the first standards-track change narrow. It separates the governance-voting identity from the operator identity without changing voting weight, confirmation, execution, round automation, or broader governance process. +This CIP keeps the first standards-track change narrow. It separates the governance-voting identity from the operator identity without changing voting weight, confirmation, execution, round automation, or broader governance process. This CIP does not standardize the standalone governance dApp, wallet/provider selection, deployment packaging, mobile or notification workflows, generalized identity, multiple voters per SV, broad rights-holder voting, or tokenomics. Those topics belong to later milestones or separate governance decisions. @@ -309,7 +309,7 @@ Optional governance-voter arguments on `DsoRules_RequestVote` and `DsoRules_Cast The one-vote-per-node model is preserved by continuing to store the vote under the represented SV's existing `VoteRequest.votes` slot. The governance voter signs the SV's vote; it does not become a new voting unit. The map key remains `Text` for upgrade compatibility, while `Vote.sv` carries the represented SV party used for tallying and staleness checks. -`SRARC_OffboardSv` is intentionally included in the Phase 1 allowlist because offboarding is a governance-membership decision rather than a node-operation decision. The high-impact path should have clear UI warnings, reason quality expectations, and tests, but it should not silently move back to the operator-only bucket. This CIP also does not exclude the target SV from voting on its own offboarding; it preserves the current represented-SV voting semantics and treats any target-party voting restriction as a separate governance-process decision. +`SRARC_OffboardSv` is intentionally included in the Phase 1 allowlist because offboarding is a governance-membership decision rather than a node-operation decision. The high-impact path is paired with clear UI warnings, reason quality expectations, and tests, but it does not move back to the operator-only bucket. This CIP also does not exclude the target SV from voting on its own offboarding; it preserves the current represented-SV voting semantics and treats any target-party voting restriction as a separate governance-process decision. ### CIP-0103 Compatibility From a4f4f7a46ef94d462596dafe37b4166adad12f2d Mon Sep 17 00:00:00 2001 From: Eric Mann Date: Thu, 21 May 2026 14:44:39 -0500 Subject: [PATCH 7/9] Align governance voter cast example --- .../cip-XXXX-SV-Governance-Voter.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md b/cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md index 33b6e7d7..0121475a 100644 --- a/cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md +++ b/cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md @@ -194,8 +194,17 @@ nonconsuming choice DsoRules_CastVote : DsoRules_CastVoteResult controller fromOptional vote.sv castBy do requireWellformedVote config vote - -- bindingCid and castBy are both None (operator) or both Some (governance voter). - -- governance-voter path uses fetchChecked (ForOwner with dso; owner = castBy) bindingCid. + (effectiveVoteSv, castByParty, castByRoleVal) <- case (bindingCid, castBy) of + (None, None) -> + pure (vote.sv, vote.sv, VCR_Operator) + (Some cid, Some p) -> do + svGovernanceVoter <- fetchChecked (ForOwner with dso; owner = p) cid + require "Vote SV must match binding SV" (vote.sv == svGovernanceVoter.sv) + pure (svGovernanceVoter.sv, p, VCR_GovernanceVoter) + (Some _, None) -> + fail "`castBy` must be present when `bindingCid` is set" + (None, Some _) -> + fail "`bindingCid` must be present when `castBy` is set" request <- fetchChecked (ForDso with dso) requestCid -- action eligibility, deadline, cooldown, archive, and slot update are shared. now <- getTime From 918e75453a4c6a8e503b71e681051ce35798c74a Mon Sep 17 00:00:00 2001 From: Eric Mann Date: Thu, 21 May 2026 15:48:31 -0500 Subject: [PATCH 8/9] Document governance vote classification. --- .../cip-XXXX-SV-Governance-Voter.md | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md b/cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md index 0121475a..8c4c8591 100644 --- a/cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md +++ b/cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md @@ -148,6 +148,37 @@ The symmetric errors on the governance-voter path use: These are distinct from binding, authority, and request-state errors surfaced elsewhere in the cast logic. +### Operational And Non-Operational Vote Classification + +The Phase 1 split follows from the current Splice governance flow: proposal review, vote preparation, signing, and submission are all routed through the SV application and the SV operator identity path. That path is appropriate for node automation and operational workflows, but it also makes policy voting depend on node-operator credentials. The classification below separates actions that express governance intent from actions that operate, onboard, bootstrap, or automate SV infrastructure. + +Governance-voter-eligible actions are those that satisfy all of these conditions: + +1. The action is explicitly allowlisted in `isGovernanceVoterAction`. +2. The action expresses policy, configuration, reward, application-status, activity-accounting, or governance-membership intent. +3. The action does not give the governance voter authority to operate an SV node, confirm or execute actions, onboard SVs, bootstrap external-party infrastructure, run round automation, or control payment workflows. + +The Phase 1 governance-voter allowlist is: + +- `SRARC_GrantFeaturedAppRight`: featured-app status governance. +- `SRARC_RevokeFeaturedAppRight`: featured-app status governance. +- `SRARC_SetConfig`: DSO rules configuration governance. +- `SRARC_UpdateSvRewardWeight`: reward-weight policy governance. +- `SRARC_CreateUnallocatedUnclaimedActivityRecord`: governance-approved activity or reward accounting. +- `SRARC_OffboardSv`: governance-membership decision. +- `CRARC_SetConfig`: Amulet rules configuration governance. + +The following categories remain operator-only in Phase 1: + +- SV onboarding and membership activation, including `SRARC_AddSv` and `SRARC_ConfirmSvOnboarding`. +- Governance-voter binding lifecycle changes, including `SRARC_RotateGovernanceVoter`, because rotation changes who may exercise the represented SV's governance vote and therefore runs through the confirmation-quorum path. +- External-party and infrastructure bootstrap, including `SRARC_CreateExternalPartyAmuletRules`, `SRARC_CreateTransferCommandCounter`, and `SRARC_CreateBootstrapExternalPartyConfigStateInstruction`. +- Round lifecycle automation, including mining-round start/archive actions. +- ANS payment workflow actions. +- Any `ActionRequiringConfirmation` constructor not explicitly listed by `isGovernanceVoterAction`. + +This classification preserves the current one-vote-per-SV governance model while moving only non-operational policy voting onto the governance-voter authority path. + ### Vote Request Creation `DsoRules_RequestVote` remains the single request-creation choice. It gains an optional `bindingCid` argument appended at the end for upgrade compatibility: From fa9db880892052a3f1ebfd0e85e9fedd00ce5449 Mon Sep 17 00:00:00 2001 From: Eric Mann Date: Mon, 8 Jun 2026 18:02:56 -0700 Subject: [PATCH 9/9] Clarify SV governance voter scope. --- .../cip-XXXX-SV-Governance-Voter.md | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md b/cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md index 8c4c8591..252ab809 100644 --- a/cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md +++ b/cip-XXXX-SV-Governance-Voter/cip-XXXX-SV-Governance-Voter.md @@ -11,9 +11,9 @@ ## Abstract -The current Splice SV governance flow uses the SV operator party as both the node-automation identity and the governance-voting identity. This CIP adds a first-class governance-voter authority path for a Phase 1 subset of non-operational votes. +The current Splice SV governance flow uses the SV operator party as both the node-automation identity and the governance-voting identity. This CIP adds a first-class governance-voter authority path for a proposed Phase 1 subset of non-operational votes. -Each SV has one active governance voter declared through a DSO-signed `SvGovernanceVoter` binding. SV onboarding creates a self-binding by default, and later changes use the existing confirmation-quorum flow through a new `SRARC_RotateGovernanceVoter` action. The governance voter may open and cast or update the represented SV's vote on explicitly allowlisted non-operational requests. Operational requests remain on the operator path, and each request/cast choice rejects the wrong path. The vote still counts as the SV's existing vote — it does not create a second voting unit. +Each SV has one active governance voter declared through a DSO-signed `SvGovernanceVoter` binding. SV onboarding creates a self-binding by default, and later changes use the existing confirmation-quorum flow through a new `SRARC_RotateGovernanceVoter` action. The governance voter may open and cast or update the represented SV's vote on non-operational requests in the proposed Phase 1 allowlist. Operational requests remain on the operator path, and each request/cast choice rejects the wrong path. The vote still counts as the SV's existing vote — it does not create a second voting unit. The on-ledger surface is intentionally compatible with the external-party submission flow defined by [CIP-0103][cip-0103]: the existing request and cast choices take optional binding arguments, the governance-voter path is controlled by the governance-voter party, and the binding can be sourced through Scan and supplied as a disclosed contract. The dApp client, Scan-based discovery, and wallet/signing-provider choices live downstream of this CIP. @@ -32,7 +32,7 @@ This CIP covers the first contract slice needed for a separated SV governance-vo This CIP standardizes the following contract-level behavior: - A DSO-signed `SvGovernanceVoter` binding authorizes a governance voter to act only on governance-voter-eligible requests for the represented SV. -- `isGovernanceVoterAction` is a hardcoded Phase 1 allowlist; new action constructors remain operator-only until explicitly classified. +- `isGovernanceVoterAction` is a hardcoded proposed Phase 1 allowlist; new action constructors remain operator-only until explicitly classified through CIP/package review. - The contract change is limited to the adjacent governance-voter module, optional governance-voter arguments on existing `DsoRules` request/cast/close choices, vote attribution fields, and binding lifecycle choices. - Both authority paths write the represented SV's single vote slot; attribution records who signed and which binding was used, not additional weight. - The contract surface is compatible with explicit-disclosure submission by a governance voter; production read/API packaging is downstream implementation work. @@ -107,7 +107,7 @@ Operator votes use `castBy = Some sv`, `castByRole = Some VCR_Operator`, and `bi ### Governance-Voter Action Classifier -The classifier is allowlist-based. New `ActionRequiringConfirmation` constructors do not become governance-voter-eligible by default; the classifier must be extended deliberately. +The classifier is allowlist-based. New `ActionRequiringConfirmation` constructors do not become governance-voter-eligible by default; the classifier must be extended deliberately through CIP/package review. ```daml isGovernanceVoterAction : ActionRequiringConfirmation -> Bool @@ -152,20 +152,22 @@ These are distinct from binding, authority, and request-state errors surfaced el The Phase 1 split follows from the current Splice governance flow: proposal review, vote preparation, signing, and submission are all routed through the SV application and the SV operator identity path. That path is appropriate for node automation and operational workflows, but it also makes policy voting depend on node-operator credentials. The classification below separates actions that express governance intent from actions that operate, onboard, bootstrap, or automate SV infrastructure. -Governance-voter-eligible actions are those that satisfy all of these conditions: +The allowlist is a proposed Phase 1 governance boundary, not a claim that every constructor has an intrinsic or permanent classification. Some actions can have both policy and operational consequences; their inclusion or exclusion should be validated during CIP and maintainer review. + +Governance-voter-eligible actions in this proposed Phase 1 boundary satisfy all of these conditions: 1. The action is explicitly allowlisted in `isGovernanceVoterAction`. 2. The action expresses policy, configuration, reward, application-status, activity-accounting, or governance-membership intent. 3. The action does not give the governance voter authority to operate an SV node, confirm or execute actions, onboard SVs, bootstrap external-party infrastructure, run round automation, or control payment workflows. -The Phase 1 governance-voter allowlist is: +The proposed Phase 1 governance-voter allowlist is: - `SRARC_GrantFeaturedAppRight`: featured-app status governance. - `SRARC_RevokeFeaturedAppRight`: featured-app status governance. - `SRARC_SetConfig`: DSO rules configuration governance. - `SRARC_UpdateSvRewardWeight`: reward-weight policy governance. - `SRARC_CreateUnallocatedUnclaimedActivityRecord`: governance-approved activity or reward accounting. -- `SRARC_OffboardSv`: governance-membership decision. +- `SRARC_OffboardSv`: high-impact governance-membership decision; included for Phase 1 validation. - `CRARC_SetConfig`: Amulet rules configuration governance. The following categories remain operator-only in Phase 1: @@ -177,7 +179,7 @@ The following categories remain operator-only in Phase 1: - ANS payment workflow actions. - Any `ActionRequiringConfirmation` constructor not explicitly listed by `isGovernanceVoterAction`. -This classification preserves the current one-vote-per-SV governance model while moving only non-operational policy voting onto the governance-voter authority path. +This classification preserves the current one-vote-per-SV governance model while moving only the proposed non-operational policy-voting subset onto the governance-voter authority path. ### Vote Request Creation @@ -207,7 +209,7 @@ nonconsuming choice DsoRules_RequestVote : DsoRules_RequestVoteResult -- initial vote is VCR_GovernanceVoter and records bindingCid = Some cid. ``` -The governance voter is the creator of a non-operational vote request, consistent with the design intent that operational voting remains an operator concern and non-operational voting belongs to the governance voter (which may be the SV itself under the self-binding default). +The governance voter is the creator of an allowlisted non-operational vote request, consistent with the design intent that operational voting remains an operator concern and non-operational voting belongs to the governance voter (which may be the SV itself under the self-binding default). This does not broaden request creation for operational actions: `DsoRules_RequestVote` rejects the governance-voter path unless `isGovernanceVoterAction action == True`, and rejects the operator path for governance-voter-eligible actions. On the governance-voter path, `DsoRules_RequestVote` records an auto-accept initial vote for the represented SV (`castBy = Some governanceVoter`, `castByRole = Some VCR_GovernanceVoter`, `bindingCid = Some cid`, `accept = True`, reason "I accept, as I requested the vote.") mirroring the operator path's convention. The initial vote occupies the represented SV's slot in `VoteRequest.votes` and may be updated through `DsoRules_CastVote` while the request is still open. @@ -307,13 +309,13 @@ The write path is not enough. A governance voter must be able to inspect the pro Phase 1 uses the following visibility position: -- `SvGovernanceVoter` is not visible to the governance voter by observer. The governance voter discovers the binding through Scan/read APIs or receives it as a disclosed contract. -- Proposal discovery and proposal-detail rendering are served through Scan or an SV-hosted read API rather than by making every proposal contract directly visible for browsing. -- The supported unaffiliated-voter submit path is explicit disclosure: the governance voter submits the target contract IDs together with the disclosed contracts needed to exercise `DsoRules_CastVote` on the governance-voter path. -- SV-hosted submission or relay remains a valid deployment option, but it is not required by the on-ledger design. -- Scan or an SV-hosted read API returns the proposal details, binding information, and disclosed-contract material needed for proposal inspection and submission, similar to the existing `AmuletRules` flow used by validators. +- `SvGovernanceVoter` is not visible to the governance voter by observer. The governance voter discovers the binding through Scan or receives it as a disclosed contract. +- Proposal discovery and proposal-detail rendering are served through Scan rather than by making every proposal contract directly visible for browsing. +- The expected unaffiliated-voter submit path is explicit disclosure: the governance voter submits the target contract IDs together with the disclosed contracts needed to exercise `DsoRules_CastVote` on the governance-voter path. +- SV-hosted submission or relay remains a valid deployment option, but it is not required by the on-ledger design and is not the discovery model standardized by this CIP. +- Scan returns the proposal details, binding information, and disclosed-contract material needed for proposal inspection and explicit-disclosure submission, similar to the existing `AmuletRules` flow used by validators. -This CIP is compatible with external participant submission and leaves the exact read/disclosure API packaging to downstream implementation. +This CIP is compatible with external participant submission and leaves the exact Scan endpoint packaging to downstream implementation. It does not require governance-voter submission to be fully independent of the SV node, but it also does not preclude independent submission when the needed disclosed contracts are available. ### Security Considerations @@ -335,7 +337,7 @@ The operator party runs or controls the SV node, signs automation commands, and Today an SV-funded organization that wants direct, auditable governance participation must hold node-operator credentials. The status quo also offers no way to distinguish, in a vote record, whether a vote was cast through an operator-automation path or by a human governance representative. -This CIP separates governance voting from node operation on the ledger without redesigning either. The governance voter is a signer for the represented SV's vote on an explicit allowlist of non-operational actions, not a new voting unit. The SV remains the unit of voting weight; the cast simply carries an accountability stamp identifying which party signed it through which authority path. +This CIP separates governance voting from node operation on the ledger without redesigning either. The governance voter is a signer for the represented SV's vote on a proposed explicit allowlist of non-operational actions, not a new voting unit. The SV remains the unit of voting weight; the cast simply carries an accountability stamp identifying which party signed it through which authority path. ## Rationale @@ -349,7 +351,7 @@ Optional governance-voter arguments on `DsoRules_RequestVote` and `DsoRules_Cast The one-vote-per-node model is preserved by continuing to store the vote under the represented SV's existing `VoteRequest.votes` slot. The governance voter signs the SV's vote; it does not become a new voting unit. The map key remains `Text` for upgrade compatibility, while `Vote.sv` carries the represented SV party used for tallying and staleness checks. -`SRARC_OffboardSv` is intentionally included in the Phase 1 allowlist because offboarding is a governance-membership decision rather than a node-operation decision. The high-impact path is paired with clear UI warnings, reason quality expectations, and tests, but it does not move back to the operator-only bucket. This CIP also does not exclude the target SV from voting on its own offboarding; it preserves the current represented-SV voting semantics and treats any target-party voting restriction as a separate governance-process decision. +`SRARC_OffboardSv` is intentionally included in the proposed Phase 1 allowlist because offboarding is a governance-membership decision rather than a node-operation decision. Because it is high impact, that inclusion should be validated during CIP review and paired with clear UI warnings, reason quality expectations, and tests. This CIP also does not exclude the target SV from voting on its own offboarding; it preserves the current represented-SV voting semantics and treats any target-party voting restriction as a separate governance-process decision. ### CIP-0103 Compatibility @@ -357,7 +359,7 @@ The one-vote-per-node model is preserved by continuing to store the vote under t - `DsoRules_CastVote` is controlled by `fromOptional vote.sv castBy`; on the governance-voter path, `castBy = Some governanceVoter` and `bindingCid = Some binding`. - The binding can be sourced through Scan and supplied as a disclosed contract by a CIP-0103-conforming Wallet. -- The cast does not require visibility on contracts unique to the SV node, so the governance voter can submit through a participant that is not the SV's participant once the read-side visibility model is settled. +- The cast does not require visibility on contracts unique to the SV node, so the governance voter can submit through a participant that is not the SV's participant when Scan supplies the needed contract IDs and disclosed contracts. `Requires: CIP-0103` is intentionally not asserted in the preamble: the on-ledger surface defined here is independently useful and does not depend on CIP-0103 being adopted. The relationship is one of compatibility, not dependence.