Skip to content

WIP: cip 0105 externally implemented governance locks#6427

Draft
jonored wants to merge 10 commits into
canton-network:mainfrom
obsidiansystems:jg/cip-0105-externally-implemented-governance-locks
Draft

WIP: cip 0105 externally implemented governance locks#6427
jonored wants to merge 10 commits into
canton-network:mainfrom
obsidiansystems:jg/cip-0105-externally-implemented-governance-locks

Conversation

@jonored

@jonored jonored commented Jul 15, 2026

Copy link
Copy Markdown

WIP code for possible direction for CIP-0105 locking implementation using the existing AmuletAllocationV2 unmodified, for reference only. Do not merge.

Pull Request Checklist

Cluster Testing

  • If a cluster test is required, comment /cluster_test on this PR to request it, and ping someone with access to the DA-internal system to approve it.
  • If an upgrade test is required, comment /upgrade_test on this PR to request it, and ping someone with access to the DA-internal system to approve it.
  • If a hard-migration test is required (from the latest release), comment /hdm_test on this PR to request it, and ping someone with access to the DA-internal system to approve it.
  • If a logical synchronizer upgrade test is required (from canton-3.5), comment /lsu_test on this PR to request it, and ping someone with access to the DA-internal system to approve it.

PR Guidelines

  • Include any change that might be observable by our partners or affect their deployment in the release notes.
  • Specify fixed issues with Fixes #n, and mention issues worked on using #n
  • Include a screenshot for frontend-related PRs - see README or use your favorite screenshot tool

Merge Guidelines

  • Make the git commit message look sensible when squash-merging on GitHub (most likely: just copy your PR description).

-- vesting to be in a separate dar from amulet only needed for SVs and
-- interested observers.

template AggregateLock

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

unclear to me why this is a separate template, ideally this would be done through the withdraw choice. If that is not an option, it seems like this could just be a choice on ExternalPartyAmuletRules

let GovernanceLockedControllers{..} = governanceLockedControllersFromMeta locked.meta ownerParty
require "Unlock controller must be one of the options" $ any (\conj -> all (`elem` authorizers) conj) unlockControllerSets
require "Not allowed to unlock to a different party than locked the funds" $ locked.allocation.authorizer == withdrawTo.allocation.authorizer
if not allowImmediateUnlock then require "Unlock without vesting is only allowed when specifically enabled" False else pure ()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't understand what this field is for. When do you want unlock without vesting?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In (https://docs.google.com/document/d/1VGtgNSNHrHnSLggqxlvhCTFrMPgaeCs2OLEJ8Fq9Ar4/edit?tab=t.0) under the Aggregated Locking Requirements, FR-25 says "It must be possible to require that locked coins only be unlocked through the vesting process." which is phrased in such a way as to suggest that this requirement is an optional possibility.

FR-26 says "It must be possible to permit immediate unlocks of all coins locked to an Aggregate Lock, to support Featured App non-approval; when in this state the vesting process is not required."

I might be misinterpreting how an implementation of that would look (just getting spun up on the details of this project still), but this might be related.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

When you've tried to set up a Featured App, locked the required amulet to satisfy the threshold requirement, and then failed to actually become a Featured App at this time. CIP-0116 says that "if an application is not approved, CC may be moved immediately", and I am defaulting to "implement things in the CIP on-chain if reasoable". No objection to specifying a different process for handling that scenario.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ok that makes sense to handle. But your implementation only seems to handle the case where allowImmediateUnlock = True and otherwise just fail in the require instead of doing vesting?

where
controller authorizers
do
locked <- view <$> fetch lockedCid

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

use the various checked fetch variants, CI is also gonna fail on this

Comment thread daml/splice-amulet/daml/Splice/AggregateLock.daml
instrumentId
meta = emptyMetadata
]
nextIterationFunding = Some $ TM.singleton instrumentId newLockedAmount

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The math doesn't add up for me. locked locks up currentLockedAmount. You are now trying to unlock amount. So newLockedAmount is currentLockedAmount - amount. But then you are trying to create two allocations with nextIterationFunding for that amount?

Comment on lines +67 to +68
lockedCid : ContractId Allocation
withdrawToCid : ContractId Allocation

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you add some comments on how you expect the two allocations to be used? it's not clear to me. Or even better add the test script.


type ControllerSets = [[Party]]

partiesFromText : Text -> Optional [Party]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

there are a bunch of utilities already e.g. parseCommaSeparated, let's reuse those instead of reimplementing half of them.

controller authorizers
do
-- remainingLocked stays in nextIterationFunding
vestingLock <- view <$> fetch vestingLockCid

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

no naked fetches


-- TODO: Probably add valid unlockController set check?
-- TODO: Potentially do case statement with nextIterationFunding optionals
require "There are no remaining funds to withdraw"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

assertion text is the wrong way around, you should state the property that should hold. so "there are remaining funds to withdraw"

authorizers : [Party]
vestingLockCid : ContractId Allocation
where
controller authorizers

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

there are zero checks on authorizers

let GovernanceLockedControllers{..} = governanceLockedControllersFromMeta locked.meta ownerParty
require "Unlock controller must be one of the options" $ any (\conj -> all (`elem` authorizers) conj) unlockControllerSets
require "Not allowed to unlock to a different party than locked the funds" $ locked.allocation.authorizer == withdrawTo.allocation.authorizer
if not allowImmediateUnlock then require "Unlock without vesting is only allowed when specifically enabled" False else pure ()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
if not allowImmediateUnlock then require "Unlock without vesting is only allowed when specifically enabled" False else pure ()
require "Unlock without vesting is only allowed when specifically enabled" allowImmediateUnlock


AmuletRegistryV2.tapFaucet registriesEnv.amuletV2 bob 1800.0

aggLock <- submit (actAs [alice, dso]) $ createCmd AggregateLock with

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

you can't assume that you can submit with both alice and dso as actAs. Getting authorization right is a crucial part in Daml models so let's please not "cheat" here

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

in this case I also don't understand why you even need actAs alice, the only signatory seems to be DSO.

Comment thread daml/splice-amulet-test/daml/Splice/Scripts/TestAggregateLocks.daml
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants