From 534fecdd6852fe448ef4559bae7823fe5eb02f3a Mon Sep 17 00:00:00 2001 From: "Jonathan D.K. Gibbons" Date: Mon, 17 Aug 2026 14:20:15 +0000 Subject: [PATCH 1/5] Substitution core, with virtual settlement. --- .../daml/Splice/AggregateLock.daml | 256 +++++++++++++++++- .../daml/Splice/ExternalPartyAmuletRules.daml | 59 +++- 2 files changed, 306 insertions(+), 9 deletions(-) diff --git a/daml/splice-amulet/daml/Splice/AggregateLock.daml b/daml/splice-amulet/daml/Splice/AggregateLock.daml index 8a796b0aa7..4126588c6d 100644 --- a/daml/splice-amulet/daml/Splice/AggregateLock.daml +++ b/daml/splice-amulet/daml/Splice/AggregateLock.daml @@ -1,17 +1,23 @@ -{-# LANGUAGE AllowAmbiguousTypes #-} module Splice.AggregateLock where import Splice.Amulet.TokenApiUtils +import Splice.AmuletRules +import Splice.Amulet.TwoStepTransfer +import Splice.AmuletAllocation as AmuletAllocationV1 +import Splice.Api.Token.HoldingV1 as V1 import Splice.Api.Token.HoldingV2 as V2 import Splice.Api.Token.AllocationV2 as V2 import Splice.Api.Token.AllocationInstructionV2 as V2 import Splice.Api.Token.MetadataV1 -import Splice.TokenStandard.Utils (maxTime, regularAccountOwner, isValidAllocationSpecificationV2) -import Splice.TokenStandard.Utils.Internal.Allocations (settlementFactoryV2_settleBatchDefaultImplNoSelf) +import Splice.TokenStandard.Utils hiding (require) -- (maxTime, regularAccountOwner, isValidAllocationSpecificationV2) +import Splice.TokenStandard.Utils.Internal.Allocations -- (settlementFactoryV2_settleBatchDefaultImplNoSelf) import Splice.TokenStandard.Utils.Internal.Conversions (timeFromMeta, encodeTime) import Splice.Util +import Splice.Amulet + import DA.Action hiding (mapA) +import DA.Assert (assertDeadlineExceeded, assertWithinDeadline) import DA.Either import DA.Optional import qualified DA.Set as S @@ -21,7 +27,7 @@ import qualified DA.Map as Map import qualified DA.TextMap as TM import DA.Time import DA.Traversable (mapA) -import DA.Foldable (concat) +import DA.Foldable (concat, forA_) import Prelude hiding (mapA, concat) -- Lock management @@ -40,11 +46,17 @@ data GovernanceLock data GovernanceLockOperation = GovernanceLockOperation_Allocate GovernanceLock | GovernanceLockOperation_TopUp TopUpAllocation + | GovernanceLockOperation_Substitute SubstituteAllocation data TopUpAllocation = TopUpAllocation with allocationCid : ContractId V2.Allocation +data SubstituteAllocation = + SubstituteAllocation with + substituteCid : ContractId V2.Allocation + topupCid : Optional (ContractId V2.Allocation) + data AggregatedLock = AggregatedLock with lockBeneficiary : Text @@ -61,6 +73,18 @@ data VestingLock = deriving (Eq, Show) +data GovernanceLockKey + = GLK_SVLocked with + beneficiary : Text + | GLK_Vesting with + startDate : Time + endDate : Time + deriving (Ord, Eq) + +governanceLockKey : GovernanceLock -> GovernanceLockKey +governanceLockKey (GovernanceLock_SVLocked (AggregatedLock with lockBeneficiary)) = GLK_SVLocked lockBeneficiary +governanceLockKey (GovernanceLock_VestingLocked (VestingLock with startDate; endDate)) = GLK_Vesting with startDate; endDate + getBeneficiaryPartyOptional : Party -> GovernanceLock -> ExtraArgs -> Update (Optional Party) getBeneficiaryPartyOptional dso (GovernanceLock_SVLocked lock) extraArgs | Some authRightCid <- authRightCidOpt = do @@ -94,7 +118,6 @@ effectiveAtKey = "cip-105/effectiveAt" topUpAllocationMetadataKey : Text topUpAllocationMetadataKey = "cip-105/topup-allocation-cid" - unlockControllerKey : Text unlockControllerKey = "cip-105/unlockControllers" withdrawControllerKey : Text @@ -102,6 +125,8 @@ withdrawControllerKey = "cip-105/withdrawControllers" substituteControllerKey : Text substituteControllerKey = "cip-105/substituteControllers" +substituteAllocationContextKey : Text +substituteAllocationContextKey = "cip-105/topup-allocation-cid" governanceMetadataKeys : Metadata -> S.Set Text governanceMetadataKeys meta = S.fromList $ fst <$> TM.toList ( TM.filterWithKey ( \_ a -> "cip-105/" `T.isPrefixOf` a ) meta.values ) @@ -173,11 +198,14 @@ parseAndValidateGovernanceLockOperation fromAllocate dso alloc = do pure . Some $ GovernanceLockOperation_Allocate lock None -> do let eitherCid = lookupFromContext @(ContractId V2.Allocation) alloc.extraArgs.context topUpAllocationMetadataKey - case eitherCid of + let substituteCid = lookupFromContext @(ContractId V2.Allocation) alloc.extraArgs.context substituteAllocationContextKey + case (,) <$> eitherCid <*> substituteCid of Left t -> assertFail t - Right None -> pure None - Right (Some topUpAllocationCid) -> + Right (None, None) -> pure None + Right (Some topUpAllocationCid, None) -> pure . Some . GovernanceLockOperation_TopUp $ TopUpAllocation topUpAllocationCid + Right (optTopUpAllocationCid, Some substituteCid) -> + pure . Some . GovernanceLockOperation_Substitute $ SubstituteAllocation substituteCid optTopUpAllocationCid governanceLockProposal_rules (GovernanceLock_VestingLocked _) = assertFail "Creating vesting locks from an allocation instruction is not permitted" @@ -462,3 +490,215 @@ instance HasCheckedFetch SvRightOwnerName ForSv where dso svName = rightOwnerName + + + + +-- | Trimmed down version of amulet_allocateionFactoryV2_allocateImplCore specialized to to governance AmuletAllocationV2 instances. +governance_allocateImplCore + : Party + -> V2.AllocationFactory_Allocate + -> Decimal + -> Update (Optional (ContractId LockedAmulet), [ContractId V1.Holding], Time, Metadata) +governance_allocateImplCore dso arg fundingAmount = do + let (V2.AllocationFactory_Allocate with settlement, allocation, requestedAt, inputHoldingCids, extraArgs, actors) = arg + + -- check actors + ensureBasicAccount "allocation.authorizer" allocation.authorizer + let authorizer = accountPrincipal dso allocation.authorizer + checkActors actors [[authorizer]] + + -- == validate each field of the requested allocation + + -- settlement.executors: no check + -- settlement.id: no check + -- settlement.cid: no check + -- settlement.meta: no check + + -- allocation.settlementDeadline: + whenSome allocation.settlementDeadline \d -> do + assertWithinDeadline "Allocation.settlementDeadline" d + + -- allocation.admin: + requireMatchExpected ("allocation.admin", allocation.admin) dso + -- allocation.authorizer: checked above as part of actors + -- allocation.transferLegSides: + require "governance locks must not specify transferLegSides" $ null allocation.transferLegSides + require "governance locks must specify nextIterationFunding" $ isSome allocation.nextIterationFunding + + -- allocation.nextIterationFunding: validated `amulet_allocationFactoryV2_allocateImpl` + -- allocation.committed: nothing to validate + -- allocation.meta: check no redundant account information + ensureNoAccountMeta "allocation" "authorizer" allocation.meta + + -- arg.requestedAt (of the allocation instruction itself): + assertDeadlineExceeded "requestedAt" requestedAt + -- arg.inputHoldingCids: validated by prepareTwoStepTransfer if funidng is required + -- arg.extraArgs: no check other than what's done when retrieving reference data + -- arg.actors: checked above + + -- compute the allocation expiry time + -- (_, configState) <- getExternalPartyConfigStateFromChoiceContext dso arg.extraArgs.context + -- let configAmulet = transferConfigAmuletFromExternalPartyConfigState configState + expiresAt <- pure maxComparableTime -- computeAllocationExpiry configAmulet (isGovernanceAllocationArgument arg) requestedAt allocation.settlementDeadline + + -- create locked amulet if required + if fundingAmount <= 0.0 + then do + -- return input cids as change, so they can be reused on chaining + return (None, map downcast inputHoldingCids, expiresAt, emptyMetadata) + else do + + -- We are using an unfeatured payment context, as the locking to self should not be featured. + paymentContext <- unfeaturedPaymentContextFromChoiceContext dso extraArgs.context + + require "At least one input holding must be provided for allocations that require funding" (not (null inputHoldingCids)) + -- lock the funds + let twoStepTransfer = TwoStepTransfer with + dso + sender = authorizer + amount = fundingAmount + lockContext = AmuletAllocationV1.mkAllocationLockContext (downcast settlement) + transferBefore = expiresAt + transferBeforeDeadline = "allocation.expiresAt" + -- Minor hack: we use the authorizer for receiver and provider as these fields are irrlevant + -- for preparing the two-step transfer . + receiver = authorizer + provider = authorizer + allowFeaturing = False + (la, cids, meta) <- prepareTwoStepTransfer twoStepTransfer arg.requestedAt (map downcast inputHoldingCids) paymentContext + return (Some la, cids, expiresAt, meta) + + +-- Execution primitives; a version of settleBatch that can operate on "virtual" allocations and is restricted from modifying net account holdings. + + +data AbstractGovernanceAllocation = AbstractGovernanceAllocation with + allocView : V2.AllocationView + lock : GovernanceLock + +data VirtualFinalizedAllocation = VirtualFinalizedAllocation with + allocation : AbstractGovernanceAllocation + extraTransferLegSides : [ TransferLegSide ] + nextIterationFunding : Decimal + +settleVirtualLock + : AbstractGovernanceAllocation + -> [TransferLegSide] + -> Decimal + -> ExtraArgs + -> Update AllocationResult +settleVirtualLock alloc transferLegSides nextIterationFunding extraArgs = do + requireUnique "transferLegSides (id + side)" (map (\leg -> (leg.transferLegId, leg.side)) transferLegSides) + + case netAmount > 0.0 of + True -> do + lockedAmuletCid <- create LockedAmulet with + lock = TimeLock with + holders = [authorizer, allocation.admin] + expiresAt = newExpiresAt + optContext = Some $ AmuletAllocationV1.mkAllocationLockContext (downcast settlement) + amulet = Amulet with + dso = allocation.admin + owner = authorizer + amount = expiringAmount configAmulet.holdingFee outputFundingAmount configState.holdingFeesOpenRoundNumber + nextIterationAllocationCid <- Some <$> create AmuletAllocationV2 with + lockedAmulet + settlement + allocation = this.allocation with + transferLegSides = [] + nextIterationFunding = arg.nextIterationFunding + expiresAt = newExpiresAt + numIterations = numIterations + 1 + createdAt + governanceLock + pure V2.AllocationResult with + output = V2.AllocationResult_Settled with + nextIterationAllocationCid + + pure undefined + +governanceLocks_settleBatchWithVirtualAllocations + : Party + -- ^ Admin of the allocations and the settlement factory + -> [VirtualFinalizedAllocation] -- ([V2.TransferLegSide], V2.AllocationView, GovernanceLock)] + -- ^ "virtual" finalized allocations. Allocations with output amounts are created, and allocations with input funding imply allocation of holdings. + -> V2.SettlementFactory_SettleBatch + -> Update V2.SettlementFactory_SettleBatchResult +governanceLocks_settleBatchWithVirtualAllocations admin virtualAllocations arg = do + let V2.SettlementFactory_SettleBatch {..} = arg + checkActors actors [settlement.executors] + + let virtualAuthorizedLegs = [ (alloc.allocation.allocView.allocation.authorizer, transferLegSide) + | alloc <- virtualAllocations, transferLegSide <- alloc.extraTransferLegSides ] + -- validate call to factory and retrieve context + allocations <- fetchAndValidateAllocationsWithExtraLegs admin virtualAuthorizedLegs arg + + -- settle all allocations + allocationSettleResults <- forA allocations $ \(finalizedAlloc, allocView) -> do + let settleArg = V2.Allocation_Settle with + -- Use default controllers as the actors + actors = allocView.allocation.admin :: allocView.settlement.executors + extraTransferLegSides = finalizedAlloc.extraTransferLegSides + nextIterationFunding = finalizedAlloc.nextIterationFunding + extraArgs + exercise finalizedAlloc.allocationCid settleArg with + + virtualAllocationSettleResults <- forA virtualAllocations $ \alloc -> do + settleVirtualLock alloc.allocation alloc.extraTransferLegSides alloc.nextIterationFunding extraArgs + + -- return result + pure V2.SettlementFactory_SettleBatchResult with + allocationSettleResults = virtualAllocationSettleResults <> allocationSettleResults + meta = emptyMetadata + +-- | Core logic to validate the allocations for a batch settlement. +-- Used in `settlementFactoryV2_settleBatchDefaultImpl` and exposed as a standalone function to +-- be reused by factories that need custom settlement execution logic. +fetchAndValidateAllocationsWithExtraLegs + : Party -> [(V2.Account, TransferLegSide)] -> V2.SettlementFactory_SettleBatch + -> Update [(V2.FinalizedAllocation, V2.AllocationView)] +fetchAndValidateAllocationsWithExtraLegs admin virtualAllocations V2.SettlementFactory_SettleBatch { settlement, transferLegs, allocations } = do + requireUnique "SettleBatch.transferLegsIds" (map (.transferLegId) transferLegs) + forA_ transferLegs $ \leg -> do + require' ("transferLeg.amount", leg.amount) isGreaterR ("zero", 0.0) + + -- fetch and validate all allocations + allocations <- forA allocations $ \finalizedAlloc -> do + allocation0 <- fetch finalizedAlloc.allocationCid + let allocationView = view allocation0 + let allocation = allocationView.allocation + + requireMatchExpected ("allocation.settlement", allocationView.settlement) settlement + requireMatchExpected ("allocation.admin", allocation.admin) admin + -- validate finalized allocation and record extra legs + validateNextIterationArgs "finalizedAllocation" (isSome allocation.nextIterationFunding) + finalizedAlloc.extraTransferLegSides allocation.nextIterationFunding + let adjustedAllocView = allocationView with + allocation = allocation with + transferLegSides = allocation.transferLegSides ++ finalizedAlloc.extraTransferLegSides + + pure (finalizedAlloc, adjustedAllocView) + + -- ensure there are no duplicate authorizations, even when looking just at the transfer-leg ids + let allocatedAuthorizations = do + (_, allocationView) <- allocations + leg <- allocationView.allocation.transferLegSides + pure (allocationView.allocation.authorizer, leg) + requireUnique "transferLegIds of allocated authorizations" + [ (authorizer, leg.transferLegId, leg.side) | (authorizer, leg) <- allocatedAuthorizations ] + + -- check that exactly the required allocations are present + let requiredAuthorizations = S.fromList $ concatMap transferLegSidesWithAuthorizer transferLegs + let allocatedAuthorizationsSet = S.fromList $ allocatedAuthorizations <> virtualAllocations + let superfluousAuthorizations = allocatedAuthorizationsSet `S.difference` requiredAuthorizations + let missingAuthorizations = requiredAuthorizations `S.difference` allocatedAuthorizationsSet + + require' ("missing authorizations", missingAuthorizations) isEqualR ("empty set", S.empty) + require' ("superfluous authorizations", superfluousAuthorizations) isEqualR ("empty set", S.empty) + + require "governance lock actions must preserve account holding totals" $ + all (\leg -> leg.sender == leg.receiver) transferLegs + + pure allocations + diff --git a/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml b/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml index d4268a673e..227e84e7ba 100644 --- a/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml +++ b/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml @@ -626,6 +626,11 @@ template GovernanceLockProposal with admin : Party requestedAt : Time + -- requiredAuthorizers : ControllerSpecification + + substituteCid : Optional (ContractId AmuletAllocationV2) + topupCid : Optional (ContractId AmuletAllocationV2) + -- Cached for the observer; not used for authorization. svParty : Optional Party where @@ -678,7 +683,8 @@ governanceLockProposal_acceptImpl this self V2.AllocationInstruction_Accept {act checkIsBeneficiary this.admin this.proposedLock extraArgs actors - let V2.AllocationInstructionView { .. } = view $ toInterface @V2.AllocationInstruction this +--governanceLockProposal_execute : GovernanceLockProposal -> ContractId V2.AllocationInstruction -> Update V2.AllocationInstructionResult +-- let V2.AllocationInstructionView { .. } = view $ toInterface @V2.AllocationInstruction this let arg = V2.AllocationFactory_Allocate with settlement @@ -688,6 +694,24 @@ governanceLockProposal_acceptImpl this self V2.AllocationInstruction_Accept {act extraArgs actors = [ this.allocationAuthorizer ] + topUpAlloc : AmuletAllocationV2 <- fetchButArchiveLater (ForDso with dso) $ fromInterfaceContractId topUpAllocationCid + + -- unlock amulet to get holdings - unlockAmuletAllocationV2 + newHoldingsTm <- unlockAmuletAllocationV2 topUpAlloc extraArgs + newHoldingsSm <- unlockAmuletAllocationV2 substituteAlloc extraArgs + let newHoldingsToAdd = concat $ catOptionals $ + [ TextMap.lookup amuletInstrumentIdName newHoldingsTm + , TextMap.lookup amuletInstrumentIdName newHoldingsSm + ] + + -- unlock amulet to get holdings - unlockAmuletAllocationV2 + newHoldingsTm <- unlockAmuletAllocationV2 topUpAlloc extraArgs + let newHoldingsToAdd = fromOptional [] $ TextMap.lookup amuletInstrumentIdName newHoldingsTm + + -- amulet_allocationFactoryV2_allocateImplCore is the only reason any of this needs to be in this file. + (lockedAmulet, senderChangeCids, expiresAt, meta) <- + amulet_allocationFactoryV2_allocateImplCore (ExternalPartyAmuletRules this.admin) arg this.allocationAmount + -- amulet_allocationFactoryV2_allocateImplCore is the only reason any of this needs to be in this file. (lockedAmulet, senderChangeCids, expiresAt, meta) <- amulet_allocationFactoryV2_allocateImplCore (ExternalPartyAmuletRules this.admin) arg this.allocationAmount @@ -722,6 +746,8 @@ governanceAllocateImpl arg@V2.AllocationFactory_Allocate{..} = do proposedLock allocationAuthorizer = regularAccountOwner allocation.authorizer allocationAmount + topupCid = None + substituteCid = None admin = allocation.admin requestedAt svParty = None -- Could take cip-105/svAuthRightCid and cache here for observer, or could use explicit disclosure and an API lookup for accept. @@ -729,6 +755,7 @@ governanceAllocateImpl arg@V2.AllocationFactory_Allocate{..} = do authorizerChangeCids = TextMap.empty output = V2.AllocationInstructionResult_Pending with allocationInstructionCid meta = emptyMetadata + GovernanceLockOperation_Substitute op -> governanceAllocateSubstitutionImpl arg op allocationAmount GovernanceLockOperation_TopUp (TopUpAllocation topUpAllocationCid) -> do -- Grab top-up-alloc -- fetchButarchiveLater old @@ -780,3 +807,33 @@ governanceAllocateImpl arg@V2.AllocationFactory_Allocate{..} = do authorizerChangeCids = TextMap.fromList [(amuletInstrumentIdName, map upcast senderChangeCids)] output = V2.AllocationInstructionResult_Completed with allocationCid meta + +governanceAllocateSubstitutionImpl : V2.AllocationFactory_Allocate -> SubstituteAllocation -> Decimal -> Update V2.AllocationInstructionResult +governanceAllocateSubstitutionImpl arg@V2.AllocationFactory_Allocate {..} SubstituteAllocation { substituteCid, topupCid } allocationAmount = do + substituted <- fetchChecked (ForDso arg.allocation.admin) (fromInterfaceContractId @AmuletAllocationV2 substituteCid) + proposedLock <- requireSome "allocation to substitute must be a governance locked allocation" substituted.governanceLock + topupOpt <- Trav.forA topupCid $ \topupCid -> do + topup <- fetchChecked (ForDso arg.allocation.admin) (fromInterfaceContractId @AmuletAllocationV2 topupCid) + require "governance locks must match on beneficiary or vesting terms when substituting with topup" $ + Some (governanceLockKey proposedLock) == (governanceLockKey <$> topup.governanceLock) + pure topup + + svParty <- getBeneficiaryPartyOptional arg.allocation.admin proposedLock extraArgs + allocationInstructionCid <- fmap toInterfaceContractId $ create $ GovernanceLockProposal with + originalInstructionCid = None + availableActions = Map.fromList [] -- Can't actually keep the SvRightHolder name and party in sync for the view. + inputHoldingCids + -- Minimal representation + proposedLock + allocationAuthorizer = regularAccountOwner allocation.authorizer + allocationAmount + admin = allocation.admin + requestedAt + svParty = None -- Could take cip-105/svAuthRightCid and cache here for observer, or could use explicit disclosure and an API lookup for accept. + topupCid = fromInterfaceContractId <$> topupCid + substituteCid = Some $ fromInterfaceContractId substituteCid + pure V2.AllocationInstructionResult with + authorizerChangeCids = TextMap.empty + output = V2.AllocationInstructionResult_Pending with allocationInstructionCid + meta = emptyMetadata + From d0c185d6dd7360ccda94373cc760363be6aa892e Mon Sep 17 00:00:00 2001 From: "Jonathan D.K. Gibbons" Date: Mon, 17 Aug 2026 20:21:56 +0000 Subject: [PATCH 2/5] Substitution, simple form. --- .../daml/Splice/AggregateLock.daml | 17 +- .../GovernanceSubstitutionProposal.daml | 158 ++++++++++++++++++ 2 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 daml/splice-amulet/daml/Splice/GovernanceSubstitutionProposal.daml diff --git a/daml/splice-amulet/daml/Splice/AggregateLock.daml b/daml/splice-amulet/daml/Splice/AggregateLock.daml index 4126588c6d..83edbafe69 100644 --- a/daml/splice-amulet/daml/Splice/AggregateLock.daml +++ b/daml/splice-amulet/daml/Splice/AggregateLock.daml @@ -41,6 +41,7 @@ class GovernanceAllocation a where data GovernanceLock = GovernanceLock_SVLocked AggregatedLock | GovernanceLock_VestingLocked VestingLock + | GovernanceLock_SubstituteProposal SubstitutionProposal deriving (Eq, Show) data GovernanceLockOperation @@ -52,6 +53,13 @@ data TopUpAllocation = TopUpAllocation with allocationCid : ContractId V2.Allocation +data SubstitutionProposal + = Substitute_SV AggregatedLock + | Substitute_Vesting VestingLock + +governanceLockForSubstitution Substitute_SV al = GovernanceLock_SVLocked al +governanceLockForSubstitution Substitute_Vesting vl = GovernanceLock_VestingLocked vl + data SubstituteAllocation = SubstituteAllocation with substituteCid : ContractId V2.Allocation @@ -72,6 +80,9 @@ data VestingLock = controllers : GovernanceLockControllers deriving (Eq, Show) +lockControllersFromGovernanceLock (GovernanceLock_SVLocked (AggregatedLock with controllers)) = Some controllers +lockControllersFromGovernanceLock (GovernanceLock_VestingLocked (VestingLock with controllers)) = Some controllers +lockControllersFromGovernanceLock (GovernanceLock_SubstituteProposal _) = None data GovernanceLockKey = GLK_SVLocked with @@ -84,6 +95,8 @@ data GovernanceLockKey governanceLockKey : GovernanceLock -> GovernanceLockKey governanceLockKey (GovernanceLock_SVLocked (AggregatedLock with lockBeneficiary)) = GLK_SVLocked lockBeneficiary governanceLockKey (GovernanceLock_VestingLocked (VestingLock with startDate; endDate)) = GLK_Vesting with startDate; endDate +governanceLockKey (GovernanceLock_SubstituteProposal (Substitute_SV (AggregatedLock with lockBeneficiary))) = GLK_SVLocked lockBeneficiary +governanceLockKey (GovernanceLock_SubstituteProposal (Substitute_Vesting (VestingLock with startDate; endDate))) = GLK_Vesting with startDate; endDate getBeneficiaryPartyOptional : Party -> GovernanceLock -> ExtraArgs -> Update (Optional Party) getBeneficiaryPartyOptional dso (GovernanceLock_SVLocked lock) extraArgs @@ -548,7 +561,7 @@ governance_allocateImplCore dso arg fundingAmount = do -- return input cids as change, so they can be reused on chaining return (None, map downcast inputHoldingCids, expiresAt, emptyMetadata) else do - + -- We are using an unfeatured payment context, as the locking to self should not be featured. paymentContext <- unfeaturedPaymentContextFromChoiceContext dso extraArgs.context @@ -612,7 +625,7 @@ settleVirtualLock alloc transferLegSides nextIterationFunding extraArgs = do numIterations = numIterations + 1 createdAt governanceLock - pure V2.AllocationResult with + pure V2.AllocationResult with output = V2.AllocationResult_Settled with nextIterationAllocationCid diff --git a/daml/splice-amulet/daml/Splice/GovernanceSubstitutionProposal.daml b/daml/splice-amulet/daml/Splice/GovernanceSubstitutionProposal.daml new file mode 100644 index 0000000000..25b365a896 --- /dev/null +++ b/daml/splice-amulet/daml/Splice/GovernanceSubstitutionProposal.daml @@ -0,0 +1,158 @@ +module Splice.GovernanceSubstitutionProposal where + +import Splice.AmuletAllocationV2 +import Splice.Amulet.TokenApiUtils +import Splice.AmuletRules +import Splice.Amulet.TwoStepTransfer +import Splice.AmuletAllocation as AmuletAllocationV1 +import Splice.Api.Token.HoldingV1 as V1 +import Splice.Api.Token.HoldingV2 as V2 +import Splice.Api.Token.AllocationV2 as V2 +import Splice.Api.Token.AllocationInstructionV2 as V2 +import Splice.Api.Token.MetadataV1 +import Splice.TokenStandard.Utils hiding (require) -- (maxTime, regularAccountOwner, isValidAllocationSpecificationV2) +import Splice.TokenStandard.Utils.Internal.Allocations -- (settlementFactoryV2_settleBatchDefaultImplNoSelf) +import Splice.TokenStandard.Utils.Internal.Conversions (timeFromMeta, encodeTime) +import Splice.Util + +import Splice.Amulet + +import DA.Action hiding (mapA) +import DA.Assert (assertDeadlineExceeded, assertWithinDeadline) +import DA.Either +import DA.Optional +import qualified DA.Set as S +import DA.Text as T +import DA.List hiding (concat) +import qualified DA.Map as Map +import qualified DA.TextMap as TM +import DA.Time +import DA.Traversable (mapA) +import DA.Foldable (concat, forA_) +import Prelude hiding (mapA, concat) + + +template GovernanceLockSubstitutionProposal with + admin : Party + proposedBy : Party + proposedTo : Party + substituteWithCid : ContractId AmuletAllocationV2 + topUpAllocationCid : Optional (ContractId AmuletAllocationV2) + where + signatory admin, proposedBy + interface instance V2.AllocationInstruction for GovernanceLockSubstitutionProposal where + view = V2.AllocationInstructionView with + originalInstructionCid + settlement = V2.SettlementInfo with + executors = [ admin ] + id = "cip-105/Substitution" + cid = None + meta = emptyMetadata + allocation = V2.AllocationSpecification with + admin + authorizer = basicAccount allocationAuthorizer + transferLegSides = [] + settlementDeadline = Some maxComparableTime + nextIterationFunding = Some $ TextMap.singleton amuletInstrumentIdName allocationAmount + committed = True -- Revisit for FA locks + meta = governanceLockToMeta proposedLock + requestedAt + inputHoldingCids + availableActions = Map.fromList + [ (V2.AIA_Withdraw, [[allocationAuthorizer]]) + , (V2.AIA_Accept, [optionalToList svParty]) + ] + expiresAt = None + meta = emptyMetadata + + allocationInstruction_withdrawExtraObservers _ = observer this + allocationInstruction_acceptExtraObservers _ = observer this + allocationInstruction_withdrawImpl self arg = do + require "the offering party must be the party to authorize withdraw" $ arg.actors == [ proposedBy ] + archive self + allocationInstruction_acceptImpl self arg = do + substitutedCid <- arg.extraArgs.context `getFromContextU` "cip-105/substituteCid" -- Possibly extend later to allow more than one unlocking CID; does not change offer model. + substitutedAllocation <- fetchChecked (ForDso admin) substitutedCid + require "substituted-for allocation must be owned by the specified party" $ substitutedAllocation.authorizer.owner == proposedTo + let substituteControllers = fromOptional [[proposedTo]] $ (substitutingAllocation.governanceLock >>= lockControllersFromGovernanceLock >>= substitute) + checkControllerSpecification TM.empty arg.actors substituteControllers + -- We have auth from every required party at this point, move on to checking constraints. + + substituteWithAllocation <- fetchChecked (ForDso admin) substituteWithCid + require "substituted-by allocation must be owned by the proposing party" $ substituteWithAllocation.authorizer.owner == proposedTo + + -- Using amount becuase the settlement legs below will all have this amount. nextIterationFunding maps will vary. + let Some [(_, amount)] = TM.toList substituteWithAllocation.allocation.nextIterationFunding + + require "lock subjects must match between substituted allocations" $ + (governanceLockKey <$> substituteWithAllocation.governanceLock) == (governanceLockKey <$> substitutedAllocation.governanceLock) + + -- Update the proposing party's allocations + case topUpAllocationCid of + None -> do -- No allocation to top up, so we can just convert the input allocation to the new lock. + archive substituteWithCid + create substituteWithAllocation with + committed = True + governanceLock = governanceLockForSubstitution <$> substituteWithAllocation.governanceLock + Some topUpAllocationCid -> do + topUpAllocation <- fetchChecked (ForDso admin) substituteWithCid + require "top up allocation must match the substituting lock" + (governanceLockKey <$> topUpAllocation.governanceLock) == (governanceLockKey <$> substitutedAllocation.governanceLock) + let Some [(_, topupInitialAmount)] = TM.toList substituteWithAllocation.allocation.nextIterationFunding + let topUpResultingAmount = topupInitialAmount + amount + settlementFactoryV2_settleBatchDefaultImplNoSelf (\_ _ -> pure arg.extraArgs) admin $ SettlementFactory_SettleBatch with + settlement = topUpAllocation.settlement + actors = [ admin ] + extraArgs = arg.extraArgs + transferLegs = + [ TransferLeg with + transferLegId + sender = basicAccount $ proposedBy + receiver = basicAccount $ proposedBy + amount + instrumentId + meta = emptyMetadata + ] + allocations = + [ FinalizedAllocation with + allocationCid = toInterfaceContractId substituteWithCid + extraTransferLegSides = + [ TransferLegSide with + transferLegId + side = SenderSide + otherside = basicAccount $ proposedBy + amount + instrumentId + meta = emptyMetadata + ] + nextIterationFunding = newLockedNextIterationFunding + , FinalizedAllocation with + allocationCid = toInterfaceContractId topUpAllocationCid + extraTransferLegSides = + [ TransferLegSide with + transferLegId + side = ReceiverSide + otherside = alloc.allocation.authorizer + amount + instrumentId + meta = emptyMetadata + ] + nextIterationFunding = Some $ TM.singleton amuletInstrumentIdName topUpResultingAmount + ] + + -- And update the acceptor's holding to reflect the released funds. + let Some [(_, acceptingAmount)] = TM.toList substituteWithAllocation.allocation.nextIterationFunding + let newAccepterAmount = acceptingAmount - amount + let nextIterationFunding = if acceptingAmount == amount then None else Some TM.singleton amuletInstrumentIdName newAccepterAmount + settleBatchResult <- settlementFactoryV2_settleBatchDefaultImplNoSelf (\_ _ -> pure arg.extraArgs) admin $ SettlementFactory_SettleBatch with + settlement = topUpAllocation.settlement + actors = [ admin ] + extraArgs = arg.extraArgs + transferLegs = [] + allocations = + [ FinalizedAllocation with + allocationCid = toInterfaceContractId substituteWithCid + extraTransferLegSides = [] + nextIterationFunding + ] + pure $ head settleBatchResult.allocationSettleResults From dd202ba4b78e1fb563b84486ede309763946487c Mon Sep 17 00:00:00 2001 From: "Jonathan D.K. Gibbons" Date: Mon, 17 Aug 2026 20:31:30 +0000 Subject: [PATCH 3/5] Remove governance-specific settlement functions from WIP. --- .../daml/Splice/AggregateLock.daml | 212 ------------------ 1 file changed, 212 deletions(-) diff --git a/daml/splice-amulet/daml/Splice/AggregateLock.daml b/daml/splice-amulet/daml/Splice/AggregateLock.daml index 83edbafe69..f11cc565e6 100644 --- a/daml/splice-amulet/daml/Splice/AggregateLock.daml +++ b/daml/splice-amulet/daml/Splice/AggregateLock.daml @@ -503,215 +503,3 @@ instance HasCheckedFetch SvRightOwnerName ForSv where dso svName = rightOwnerName - - - - --- | Trimmed down version of amulet_allocateionFactoryV2_allocateImplCore specialized to to governance AmuletAllocationV2 instances. -governance_allocateImplCore - : Party - -> V2.AllocationFactory_Allocate - -> Decimal - -> Update (Optional (ContractId LockedAmulet), [ContractId V1.Holding], Time, Metadata) -governance_allocateImplCore dso arg fundingAmount = do - let (V2.AllocationFactory_Allocate with settlement, allocation, requestedAt, inputHoldingCids, extraArgs, actors) = arg - - -- check actors - ensureBasicAccount "allocation.authorizer" allocation.authorizer - let authorizer = accountPrincipal dso allocation.authorizer - checkActors actors [[authorizer]] - - -- == validate each field of the requested allocation - - -- settlement.executors: no check - -- settlement.id: no check - -- settlement.cid: no check - -- settlement.meta: no check - - -- allocation.settlementDeadline: - whenSome allocation.settlementDeadline \d -> do - assertWithinDeadline "Allocation.settlementDeadline" d - - -- allocation.admin: - requireMatchExpected ("allocation.admin", allocation.admin) dso - -- allocation.authorizer: checked above as part of actors - -- allocation.transferLegSides: - require "governance locks must not specify transferLegSides" $ null allocation.transferLegSides - require "governance locks must specify nextIterationFunding" $ isSome allocation.nextIterationFunding - - -- allocation.nextIterationFunding: validated `amulet_allocationFactoryV2_allocateImpl` - -- allocation.committed: nothing to validate - -- allocation.meta: check no redundant account information - ensureNoAccountMeta "allocation" "authorizer" allocation.meta - - -- arg.requestedAt (of the allocation instruction itself): - assertDeadlineExceeded "requestedAt" requestedAt - -- arg.inputHoldingCids: validated by prepareTwoStepTransfer if funidng is required - -- arg.extraArgs: no check other than what's done when retrieving reference data - -- arg.actors: checked above - - -- compute the allocation expiry time - -- (_, configState) <- getExternalPartyConfigStateFromChoiceContext dso arg.extraArgs.context - -- let configAmulet = transferConfigAmuletFromExternalPartyConfigState configState - expiresAt <- pure maxComparableTime -- computeAllocationExpiry configAmulet (isGovernanceAllocationArgument arg) requestedAt allocation.settlementDeadline - - -- create locked amulet if required - if fundingAmount <= 0.0 - then do - -- return input cids as change, so they can be reused on chaining - return (None, map downcast inputHoldingCids, expiresAt, emptyMetadata) - else do - - -- We are using an unfeatured payment context, as the locking to self should not be featured. - paymentContext <- unfeaturedPaymentContextFromChoiceContext dso extraArgs.context - - require "At least one input holding must be provided for allocations that require funding" (not (null inputHoldingCids)) - -- lock the funds - let twoStepTransfer = TwoStepTransfer with - dso - sender = authorizer - amount = fundingAmount - lockContext = AmuletAllocationV1.mkAllocationLockContext (downcast settlement) - transferBefore = expiresAt - transferBeforeDeadline = "allocation.expiresAt" - -- Minor hack: we use the authorizer for receiver and provider as these fields are irrlevant - -- for preparing the two-step transfer . - receiver = authorizer - provider = authorizer - allowFeaturing = False - (la, cids, meta) <- prepareTwoStepTransfer twoStepTransfer arg.requestedAt (map downcast inputHoldingCids) paymentContext - return (Some la, cids, expiresAt, meta) - - --- Execution primitives; a version of settleBatch that can operate on "virtual" allocations and is restricted from modifying net account holdings. - - -data AbstractGovernanceAllocation = AbstractGovernanceAllocation with - allocView : V2.AllocationView - lock : GovernanceLock - -data VirtualFinalizedAllocation = VirtualFinalizedAllocation with - allocation : AbstractGovernanceAllocation - extraTransferLegSides : [ TransferLegSide ] - nextIterationFunding : Decimal - -settleVirtualLock - : AbstractGovernanceAllocation - -> [TransferLegSide] - -> Decimal - -> ExtraArgs - -> Update AllocationResult -settleVirtualLock alloc transferLegSides nextIterationFunding extraArgs = do - requireUnique "transferLegSides (id + side)" (map (\leg -> (leg.transferLegId, leg.side)) transferLegSides) - - case netAmount > 0.0 of - True -> do - lockedAmuletCid <- create LockedAmulet with - lock = TimeLock with - holders = [authorizer, allocation.admin] - expiresAt = newExpiresAt - optContext = Some $ AmuletAllocationV1.mkAllocationLockContext (downcast settlement) - amulet = Amulet with - dso = allocation.admin - owner = authorizer - amount = expiringAmount configAmulet.holdingFee outputFundingAmount configState.holdingFeesOpenRoundNumber - nextIterationAllocationCid <- Some <$> create AmuletAllocationV2 with - lockedAmulet - settlement - allocation = this.allocation with - transferLegSides = [] - nextIterationFunding = arg.nextIterationFunding - expiresAt = newExpiresAt - numIterations = numIterations + 1 - createdAt - governanceLock - pure V2.AllocationResult with - output = V2.AllocationResult_Settled with - nextIterationAllocationCid - - pure undefined - -governanceLocks_settleBatchWithVirtualAllocations - : Party - -- ^ Admin of the allocations and the settlement factory - -> [VirtualFinalizedAllocation] -- ([V2.TransferLegSide], V2.AllocationView, GovernanceLock)] - -- ^ "virtual" finalized allocations. Allocations with output amounts are created, and allocations with input funding imply allocation of holdings. - -> V2.SettlementFactory_SettleBatch - -> Update V2.SettlementFactory_SettleBatchResult -governanceLocks_settleBatchWithVirtualAllocations admin virtualAllocations arg = do - let V2.SettlementFactory_SettleBatch {..} = arg - checkActors actors [settlement.executors] - - let virtualAuthorizedLegs = [ (alloc.allocation.allocView.allocation.authorizer, transferLegSide) - | alloc <- virtualAllocations, transferLegSide <- alloc.extraTransferLegSides ] - -- validate call to factory and retrieve context - allocations <- fetchAndValidateAllocationsWithExtraLegs admin virtualAuthorizedLegs arg - - -- settle all allocations - allocationSettleResults <- forA allocations $ \(finalizedAlloc, allocView) -> do - let settleArg = V2.Allocation_Settle with - -- Use default controllers as the actors - actors = allocView.allocation.admin :: allocView.settlement.executors - extraTransferLegSides = finalizedAlloc.extraTransferLegSides - nextIterationFunding = finalizedAlloc.nextIterationFunding - extraArgs - exercise finalizedAlloc.allocationCid settleArg with - - virtualAllocationSettleResults <- forA virtualAllocations $ \alloc -> do - settleVirtualLock alloc.allocation alloc.extraTransferLegSides alloc.nextIterationFunding extraArgs - - -- return result - pure V2.SettlementFactory_SettleBatchResult with - allocationSettleResults = virtualAllocationSettleResults <> allocationSettleResults - meta = emptyMetadata - --- | Core logic to validate the allocations for a batch settlement. --- Used in `settlementFactoryV2_settleBatchDefaultImpl` and exposed as a standalone function to --- be reused by factories that need custom settlement execution logic. -fetchAndValidateAllocationsWithExtraLegs - : Party -> [(V2.Account, TransferLegSide)] -> V2.SettlementFactory_SettleBatch - -> Update [(V2.FinalizedAllocation, V2.AllocationView)] -fetchAndValidateAllocationsWithExtraLegs admin virtualAllocations V2.SettlementFactory_SettleBatch { settlement, transferLegs, allocations } = do - requireUnique "SettleBatch.transferLegsIds" (map (.transferLegId) transferLegs) - forA_ transferLegs $ \leg -> do - require' ("transferLeg.amount", leg.amount) isGreaterR ("zero", 0.0) - - -- fetch and validate all allocations - allocations <- forA allocations $ \finalizedAlloc -> do - allocation0 <- fetch finalizedAlloc.allocationCid - let allocationView = view allocation0 - let allocation = allocationView.allocation - - requireMatchExpected ("allocation.settlement", allocationView.settlement) settlement - requireMatchExpected ("allocation.admin", allocation.admin) admin - -- validate finalized allocation and record extra legs - validateNextIterationArgs "finalizedAllocation" (isSome allocation.nextIterationFunding) - finalizedAlloc.extraTransferLegSides allocation.nextIterationFunding - let adjustedAllocView = allocationView with - allocation = allocation with - transferLegSides = allocation.transferLegSides ++ finalizedAlloc.extraTransferLegSides - - pure (finalizedAlloc, adjustedAllocView) - - -- ensure there are no duplicate authorizations, even when looking just at the transfer-leg ids - let allocatedAuthorizations = do - (_, allocationView) <- allocations - leg <- allocationView.allocation.transferLegSides - pure (allocationView.allocation.authorizer, leg) - requireUnique "transferLegIds of allocated authorizations" - [ (authorizer, leg.transferLegId, leg.side) | (authorizer, leg) <- allocatedAuthorizations ] - - -- check that exactly the required allocations are present - let requiredAuthorizations = S.fromList $ concatMap transferLegSidesWithAuthorizer transferLegs - let allocatedAuthorizationsSet = S.fromList $ allocatedAuthorizations <> virtualAllocations - let superfluousAuthorizations = allocatedAuthorizationsSet `S.difference` requiredAuthorizations - let missingAuthorizations = requiredAuthorizations `S.difference` allocatedAuthorizationsSet - - require' ("missing authorizations", missingAuthorizations) isEqualR ("empty set", S.empty) - require' ("superfluous authorizations", superfluousAuthorizations) isEqualR ("empty set", S.empty) - - require "governance lock actions must preserve account holding totals" $ - all (\leg -> leg.sender == leg.receiver) transferLegs - - pure allocations - From 317c228ef4b5cfa52e395f127df0369ae084ddeb Mon Sep 17 00:00:00 2001 From: "Jonathan D.K. Gibbons" Date: Mon, 17 Aug 2026 20:40:10 +0000 Subject: [PATCH 4/5] Remove more remnants of combined propose/accept/substitute flow changes. --- .../daml/Splice/ExternalPartyAmuletRules.daml | 60 +------------------ 1 file changed, 2 insertions(+), 58 deletions(-) diff --git a/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml b/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml index 227e84e7ba..1d93908ca6 100644 --- a/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml +++ b/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml @@ -626,11 +626,6 @@ template GovernanceLockProposal with admin : Party requestedAt : Time - -- requiredAuthorizers : ControllerSpecification - - substituteCid : Optional (ContractId AmuletAllocationV2) - topupCid : Optional (ContractId AmuletAllocationV2) - -- Cached for the observer; not used for authorization. svParty : Optional Party where @@ -683,8 +678,7 @@ governanceLockProposal_acceptImpl this self V2.AllocationInstruction_Accept {act checkIsBeneficiary this.admin this.proposedLock extraArgs actors ---governanceLockProposal_execute : GovernanceLockProposal -> ContractId V2.AllocationInstruction -> Update V2.AllocationInstructionResult --- let V2.AllocationInstructionView { .. } = view $ toInterface @V2.AllocationInstruction this + let V2.AllocationInstructionView { .. } = view $ toInterface @V2.AllocationInstruction this let arg = V2.AllocationFactory_Allocate with settlement @@ -694,24 +688,6 @@ governanceLockProposal_acceptImpl this self V2.AllocationInstruction_Accept {act extraArgs actors = [ this.allocationAuthorizer ] - topUpAlloc : AmuletAllocationV2 <- fetchButArchiveLater (ForDso with dso) $ fromInterfaceContractId topUpAllocationCid - - -- unlock amulet to get holdings - unlockAmuletAllocationV2 - newHoldingsTm <- unlockAmuletAllocationV2 topUpAlloc extraArgs - newHoldingsSm <- unlockAmuletAllocationV2 substituteAlloc extraArgs - let newHoldingsToAdd = concat $ catOptionals $ - [ TextMap.lookup amuletInstrumentIdName newHoldingsTm - , TextMap.lookup amuletInstrumentIdName newHoldingsSm - ] - - -- unlock amulet to get holdings - unlockAmuletAllocationV2 - newHoldingsTm <- unlockAmuletAllocationV2 topUpAlloc extraArgs - let newHoldingsToAdd = fromOptional [] $ TextMap.lookup amuletInstrumentIdName newHoldingsTm - - -- amulet_allocationFactoryV2_allocateImplCore is the only reason any of this needs to be in this file. - (lockedAmulet, senderChangeCids, expiresAt, meta) <- - amulet_allocationFactoryV2_allocateImplCore (ExternalPartyAmuletRules this.admin) arg this.allocationAmount - -- amulet_allocationFactoryV2_allocateImplCore is the only reason any of this needs to be in this file. (lockedAmulet, senderChangeCids, expiresAt, meta) <- amulet_allocationFactoryV2_allocateImplCore (ExternalPartyAmuletRules this.admin) arg this.allocationAmount @@ -746,8 +722,6 @@ governanceAllocateImpl arg@V2.AllocationFactory_Allocate{..} = do proposedLock allocationAuthorizer = regularAccountOwner allocation.authorizer allocationAmount - topupCid = None - substituteCid = None admin = allocation.admin requestedAt svParty = None -- Could take cip-105/svAuthRightCid and cache here for observer, or could use explicit disclosure and an API lookup for accept. @@ -755,7 +729,7 @@ governanceAllocateImpl arg@V2.AllocationFactory_Allocate{..} = do authorizerChangeCids = TextMap.empty output = V2.AllocationInstructionResult_Pending with allocationInstructionCid meta = emptyMetadata - GovernanceLockOperation_Substitute op -> governanceAllocateSubstitutionImpl arg op allocationAmount + GovernanceLockOperation_Substitute op -> undefined -- FIXME: governanceAllocateSubstitutionImpl arg op allocationAmount GovernanceLockOperation_TopUp (TopUpAllocation topUpAllocationCid) -> do -- Grab top-up-alloc -- fetchButarchiveLater old @@ -807,33 +781,3 @@ governanceAllocateImpl arg@V2.AllocationFactory_Allocate{..} = do authorizerChangeCids = TextMap.fromList [(amuletInstrumentIdName, map upcast senderChangeCids)] output = V2.AllocationInstructionResult_Completed with allocationCid meta - -governanceAllocateSubstitutionImpl : V2.AllocationFactory_Allocate -> SubstituteAllocation -> Decimal -> Update V2.AllocationInstructionResult -governanceAllocateSubstitutionImpl arg@V2.AllocationFactory_Allocate {..} SubstituteAllocation { substituteCid, topupCid } allocationAmount = do - substituted <- fetchChecked (ForDso arg.allocation.admin) (fromInterfaceContractId @AmuletAllocationV2 substituteCid) - proposedLock <- requireSome "allocation to substitute must be a governance locked allocation" substituted.governanceLock - topupOpt <- Trav.forA topupCid $ \topupCid -> do - topup <- fetchChecked (ForDso arg.allocation.admin) (fromInterfaceContractId @AmuletAllocationV2 topupCid) - require "governance locks must match on beneficiary or vesting terms when substituting with topup" $ - Some (governanceLockKey proposedLock) == (governanceLockKey <$> topup.governanceLock) - pure topup - - svParty <- getBeneficiaryPartyOptional arg.allocation.admin proposedLock extraArgs - allocationInstructionCid <- fmap toInterfaceContractId $ create $ GovernanceLockProposal with - originalInstructionCid = None - availableActions = Map.fromList [] -- Can't actually keep the SvRightHolder name and party in sync for the view. - inputHoldingCids - -- Minimal representation - proposedLock - allocationAuthorizer = regularAccountOwner allocation.authorizer - allocationAmount - admin = allocation.admin - requestedAt - svParty = None -- Could take cip-105/svAuthRightCid and cache here for observer, or could use explicit disclosure and an API lookup for accept. - topupCid = fromInterfaceContractId <$> topupCid - substituteCid = Some $ fromInterfaceContractId substituteCid - pure V2.AllocationInstructionResult with - authorizerChangeCids = TextMap.empty - output = V2.AllocationInstructionResult_Pending with allocationInstructionCid - meta = emptyMetadata - From 163b310a0575e0d4f8369a4738b0f54565b003db Mon Sep 17 00:00:00 2001 From: "Jonathan D.K. Gibbons" Date: Tue, 18 Aug 2026 08:16:38 -0400 Subject: [PATCH 5/5] Update daml/splice-amulet/daml/Splice/GovernanceSubstitutionProposal.daml Co-authored-by: Simon Meier Signed-off-by: Jonathan D.K. Gibbons --- .../daml/Splice/GovernanceSubstitutionProposal.daml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daml/splice-amulet/daml/Splice/GovernanceSubstitutionProposal.daml b/daml/splice-amulet/daml/Splice/GovernanceSubstitutionProposal.daml index 25b365a896..3f3c4c7ce9 100644 --- a/daml/splice-amulet/daml/Splice/GovernanceSubstitutionProposal.daml +++ b/daml/splice-amulet/daml/Splice/GovernanceSubstitutionProposal.daml @@ -45,7 +45,7 @@ template GovernanceLockSubstitutionProposal with originalInstructionCid settlement = V2.SettlementInfo with executors = [ admin ] - id = "cip-105/Substitution" + id = "cip-105/substitution" cid = None meta = emptyMetadata allocation = V2.AllocationSpecification with