diff --git a/daml/dars.lock b/daml/dars.lock index a639c0ca9a..611b4aef7e 100644 --- a/daml/dars.lock +++ b/daml/dars.lock @@ -71,6 +71,7 @@ splice-dso-governance 0.1.24 4974c654485d4ecaa6b5caf8ef3c2679efa8195c4b50d4965a8 splice-dso-governance 0.1.25 b41ffa8aadafc8ae78eeb39b201eca9bc96470267d36553badd5df2a0587b16f splice-dso-governance 0.1.26 d65173dabe1b5e7278ffb66c55defb123f72df18218c7e317bcc4f703f08a84e splice-dso-governance 0.1.27 ab73d34dddab433044f560603f2822e5b26d9af98bd6ea8a0e9a25c5f717ab9a +splice-dso-governance 0.1.28 f70766cc94255f385f0c6e12c7635ef1adfeecde30de873db646db5a2cf8e94b splice-dso-governance 0.1.3 b0ae3cc03e418790305a3c15f761fe495572de5827f8d322fb8b96996b783c13 splice-dso-governance 0.1.4 dc24fd18b4d151cd1e0ff6bfb7438bafb2f50fe076d0f16f50565e60b153a0be splice-dso-governance 0.1.5 9e3ca1d22ad495dfabf3d61acae3dc1a7718f527f02092280b58cf69edfdc84c @@ -78,7 +79,7 @@ splice-dso-governance 0.1.6 4e7653cfbf7ca249de4507aca9cd3b91060e5489042a522c589d splice-dso-governance 0.1.7 d406eba1132d464605f4dae3edf8cf5ecbbb34bd8edef0e047e7e526d328718c splice-dso-governance 0.1.8 1790a114f83d5f290261fae1e7e46fba75a861a3dd603c6b4ef6b67b49053948 splice-dso-governance 0.1.9 9ee83bfd872f91e659b8a8439c5b4eaf240bcf6f19698f884d7d7993ab48c401 -splice-dso-governance-test 0.1.33 bb97b29d63309dad1c7d9af45a5f425e4fae18ddc83f5c20755e21f4d21bcd26 +splice-dso-governance-test 0.1.34 0ba60677a1ce30ffb2ff86e89485bad6925c0541d5e6977f791bff403464b2da splice-token-standard-test 1.0.13 a556574314ab5ecbfa04b04a6b6c9259cf90388461fe307ba712257ad5993a6b splice-token-test-dummy-holding 0.0.1 1cd171c6c42ab46dc9cf12d80c6111369e00cea5cdf054924b4f26ce94b1ef5b splice-token-test-dummy-holding 0.0.2 4f40fb033ef3db89623642c1b494e846097fa32af138b3864a63aa15937a323d diff --git a/daml/dars/splice-dso-governance-0.1.28.dar b/daml/dars/splice-dso-governance-0.1.28.dar new file mode 100644 index 0000000000..6420f8ab53 Binary files /dev/null and b/daml/dars/splice-dso-governance-0.1.28.dar differ diff --git a/daml/splice-dso-governance-test/daml.yaml b/daml/splice-dso-governance-test/daml.yaml index 2babc8a99a..7b40dabed3 100644 --- a/daml/splice-dso-governance-test/daml.yaml +++ b/daml/splice-dso-governance-test/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.4.11 name: splice-dso-governance-test source: daml -version: 0.1.33 +version: 0.1.34 dependencies: - daml-prim - daml-stdlib diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestGovernance.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestGovernance.daml index cabb7c45c3..a43520b682 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestGovernance.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestGovernance.daml @@ -85,6 +85,128 @@ testSvGovernanceVoterBindingLifecycle = do None <- queryContractId dso rotateResult.bindingCid pure () +-- | Tests governance-voter casting while preserving represented-SV vote slots. +testGovernanceVoterCastPath : Script () +testGovernanceVoterCastPath = do + (_app, dso, (sv1, sv2, _sv3, _sv4)) <- initMainNet + governanceVoter1 <- allocateParty "cast-governance-voter-1" + governanceVoter2 <- allocateParty "cast-governance-voter-2" + wrongVoter <- allocateParty "wrong-cast-governance-voter" + newSv <- allocateParty "cast-new-sv" + + [(dsoRulesCid, _)] <- query @DsoRules dso + [(_, openRound), _, _] <- sortOn (._2.round) <$> query @OpenMiningRound dso + bindingCid <- submit sv1 $ createCmd SvGovernanceVoter with + dso + sv = sv1 + governanceVoter = governanceVoter1 + + let governanceVote signer accept body = Vote with + sv = sv1 + castBy = signer + castByRole = VCR_GovernanceVoter + accept + reason = Reason with url = ""; body + optCastAt = None + let operatorVote accept body = Vote with + sv = sv1 + castBy = sv1 + castByRole = VCR_Operator + accept + reason = Reason with url = ""; body + optCastAt = None + let requestOffboardSv = submit (actAs sv1 <> readAs dso) $ exerciseCmd dsoRulesCid DsoRules_RequestVote with + requester = sv1 + action = ARC_DsoRules with + dsoAction = SRARC_OffboardSv DsoRules_OffboardSv with sv = sv2 + reason = Reason with url = ""; body = "cast path" + targetEffectiveAt = None + voteRequestTimeout = Some (days 7) + + requestResult <- requestOffboardSv + passTime (minutes 1) + castResult <- submit (actAs governanceVoter1 <> readAs dso) $ exerciseCmd dsoRulesCid DsoRules_CastGovernanceVote with + requestCid = requestResult.voteRequest + bindingCid + vote = governanceVote governanceVoter1 False "governance voter update" + Some request <- queryContractId dso castResult.voteRequest + Map.size request.votes === 1 + case Map.lookup request.requester request.votes of + None -> fail "represented SV vote missing" + Some vote -> do + vote.sv === sv1 + vote.castBy === governanceVoter1 + vote.castByRole === VCR_GovernanceVoter + vote.accept === False + + passTime (minutes 1) + operatorResult <- submit (actAs sv1 <> readAs dso) $ exerciseCmd dsoRulesCid DsoRules_CastVote with + requestCid = castResult.voteRequest + vote = operatorVote True "operator overwrite" + Some request <- queryContractId dso operatorResult.voteRequest + case Map.lookup request.requester request.votes of + None -> fail "operator vote missing" + Some vote -> do + vote.castBy === sv1 + vote.castByRole === VCR_Operator + vote.accept === True + + passTime (minutes 1) + castResult <- submit (actAs governanceVoter1 <> readAs dso) $ exerciseCmd dsoRulesCid DsoRules_CastGovernanceVote with + requestCid = operatorResult.voteRequest + bindingCid + vote = governanceVote governanceVoter1 False "governance voter overwrite" + Some request <- queryContractId dso castResult.voteRequest + case Map.lookup request.requester request.votes of + None -> fail "governance-voter vote missing" + Some vote -> do + vote.castBy === governanceVoter1 + vote.castByRole === VCR_GovernanceVoter + vote.accept === False + + unsupportedRequest <- submit (actAs sv1 <> readAs dso) $ exerciseCmd dsoRulesCid DsoRules_RequestVote with + requester = sv1 + action = ARC_DsoRules with + dsoAction = SRARC_AddSv DsoRules_AddSv with + newSvParty = newSv + newSvName = "cast-new-sv" + newSvRewardWeight = 1 + newSvParticipantId = "cast-new-sv-participant" + joinedAsOfRound = openRound.round + reason = Reason with url = ""; body = "unsupported action" + targetEffectiveAt = None + voteRequestTimeout = Some (days 7) + submitMustFail (actAs governanceVoter1 <> readAs dso) $ exerciseCmd dsoRulesCid DsoRules_CastGovernanceVote with + requestCid = unsupportedRequest.voteRequest + bindingCid + vote = governanceVote governanceVoter1 False "unsupported" + + wrongVoterRequest <- requestOffboardSv + submitMustFail (actAs wrongVoter <> readAs dso) $ exerciseCmd dsoRulesCid DsoRules_CastGovernanceVote with + requestCid = wrongVoterRequest.voteRequest + bindingCid + vote = governanceVote wrongVoter False "wrong voter" + + rotateResult <- submit sv1 $ exerciseCmd bindingCid RotateGovernanceVoter with + newGovernanceVoter = governanceVoter2 + rotatedRequest <- requestOffboardSv + submitMustFail (actAs governanceVoter1 <> readAs dso) $ exerciseCmd dsoRulesCid DsoRules_CastGovernanceVote with + requestCid = rotatedRequest.voteRequest + bindingCid = rotateResult.bindingCid + vote = governanceVote governanceVoter1 False "rotated old voter" + passTime (minutes 1) + _ <- submit (actAs governanceVoter2 <> readAs dso) $ exerciseCmd dsoRulesCid DsoRules_CastGovernanceVote with + requestCid = rotatedRequest.voteRequest + bindingCid = rotateResult.bindingCid + vote = governanceVote governanceVoter2 False "rotated new voter" + + _ <- submit sv1 $ exerciseCmd rotateResult.bindingCid ClearGovernanceVoter + clearedRequest <- requestOffboardSv + submitMustFail (actAs governanceVoter2 <> readAs dso) $ exerciseCmd dsoRulesCid DsoRules_CastGovernanceVote with + requestCid = clearedRequest.voteRequest + bindingCid = rotateResult.bindingCid + vote = governanceVote governanceVoter2 False "cleared binding" + -- | Tests the Phase 1 governance-voter action taxonomy. testGovernanceVoterActionTaxonomy : Script () testGovernanceVoterActionTaxonomy = do diff --git a/daml/splice-dso-governance/daml.yaml b/daml/splice-dso-governance/daml.yaml index cf92914f32..02f6682cb8 100644 --- a/daml/splice-dso-governance/daml.yaml +++ b/daml/splice-dso-governance/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.4.11 name: splice-dso-governance source: daml -version: 0.1.27 +version: 0.1.28 dependencies: - daml-prim - daml-stdlib diff --git a/daml/splice-dso-governance/daml/Splice/DsoRules.daml b/daml/splice-dso-governance/daml/Splice/DsoRules.daml index 4cedc722c9..8335d8a5f6 100644 --- a/daml/splice-dso-governance/daml/Splice/DsoRules.daml +++ b/daml/splice-dso-governance/daml/Splice/DsoRules.daml @@ -32,6 +32,7 @@ import Splice.Ans import Splice.SvOnboarding import Splice.DSO.AmuletPrice import Splice.DSO.DecentralizedSynchronizer +import Splice.DSO.GovernanceVoter import Splice.DSO.SvState import Splice.Schedule import Splice.Util @@ -209,6 +210,9 @@ data DsoRules_RequestVoteResult = DsoRules_RequestVoteResult with data DsoRules_CastVoteResult = DsoRules_CastVoteResult with voteRequest : ContractId VoteRequest +data DsoRules_CastGovernanceVoteResult = DsoRules_CastGovernanceVoteResult with + voteRequest : ContractId VoteRequest + data DsoRules_UpdateAmuletPriceVoteResult = DsoRules_UpdateAmuletPriceVoteResult with amuletPriceVote : ContractId AmuletPriceVote @@ -820,6 +824,44 @@ template DsoRules with trackingCid = Some (fromOptional requestCid request.trackingCid) return DsoRules_CastVoteResult with .. + -- Note that this choice can be used to both cast the initial governance-voter + -- vote, and update a vote for the represented SV. + nonconsuming choice DsoRules_CastGovernanceVote : DsoRules_CastGovernanceVoteResult + with + requestCid : ContractId VoteRequest + bindingCid : ContractId SvGovernanceVoter + vote : Vote + controller vote.castBy + do + -- validate vote parameters + 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) + voterName <- case Map.lookup binding.sv svs of + None -> fail "Voter is not an SV" + Some info -> pure info.name + -- validate and archive request + request <- fetchAndArchive (ForDso with dso) requestCid + require "Action is not governance-voter eligible" (isGovernanceVoterAction request.action) + -- rate limit casting of votes by the same represented SV to avoid blocking progress + enforceCooldown ("voteCooldownTime for " <> partyToText binding.sv) (getVoteCooldownTime config) $ do + pastVote <- Map.lookup voterName request.votes + pastVote.optCastAt + -- store vote in the represented SV's vote slot + now <- getTime + let recordedVote = vote with + sv = binding.sv + castBy = binding.governanceVoter + castByRole = VCR_GovernanceVoter + optCastAt = Some now + voteRequest <- create request with + votes = Map.insert voterName recordedVote request.votes + trackingCid = Some (fromOptional requestCid request.trackingCid) + return DsoRules_CastGovernanceVoteResult with .. + -- We expect the TxHistory log of the SV app to store the choice argument together -- with its result for historical reference. -- diff --git a/docs/src/sv_operator/sv_governance_voter.rst b/docs/src/sv_operator/sv_governance_voter.rst index c6c1f88604..66f53fd979 100644 --- a/docs/src/sv_operator/sv_governance_voter.rst +++ b/docs/src/sv_operator/sv_governance_voter.rst @@ -25,6 +25,10 @@ keeps that invariant outside the template key space so reviewers can decide whether workflow validation is sufficient or whether a separate registry pattern is needed before an upstream design is finalized. +Governance-voter vote submission fetches the binding by contract ID, validates +the signer against the binding, checks the action allowlist, and writes the vote +into the represented SV's vote slot. + The first contract slice uses a hardcoded Daml allowlist for governance-voter eligible actions. New ``ActionRequiringConfirmation`` constructors are rejected by default until reviewed and added deliberately. The proposed allowlist is