Skip to content

cip-0105 lifetime rewards tracking - #7

Draft
dbzguy56 wants to merge 11 commits into
jg/cip-0105-externally-implemented-governance-locksfrom
dpak/cip-0105-lifetime-rewards-tracking
Draft

cip-0105 lifetime rewards tracking#7
dbzguy56 wants to merge 11 commits into
jg/cip-0105-externally-implemented-governance-locksfrom
dpak/cip-0105-lifetime-rewards-tracking

Conversation

@dbzguy56

@dbzguy56 dbzguy56 commented Aug 3, 2026

Copy link
Copy Markdown

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).

-- compute balance change
miningRound <- getIssuingMiningRound csum coupon.round
let currentSvRewardCouponAmount = intToDecimal coupon.weight * miningRound.issuancePerSvRewardCoupon
svRightOwnerName <- grabSvRightOwnerName coupon.svRightOwnerName

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

NB: Currently this fails on None, having thought some more about it, I think the correct thing here may be to instead skip the receipt production on any rewards which don't have this attribution, rather than requiring those coupons to be updated elsewhere before they can be consumed. Unless someone has a better idea than me of how to do it, I think it'll be annoying to automate the attribution of pre-switchover coupons and tedious for everyone involved to do it manually. Instead, we can consider minting of any coupon without a rights holder name as properly belonging to Phase 1 totals that the SvRewardState values will get initialized to.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yes, this should not fail. Just skip generating the receipt.

summarizeAndConsumeInput _round s (InputUnclaimedActivityRecord unclaimedActivityRecordCid) = do
unclaimedActivityRecord <- fetchAndArchive forOwner unclaimedActivityRecordCid
assertWithinDeadline "UnclaimedActivityRecord.expiresAt" unclaimedActivityRecord.expiresAt
svRightOwnerName <- grabSvRightOwnerName unclaimedActivityRecord.svRightOwnerName

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

(see other comment about failure vs. skipping)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Let's also do an additional cleanup: when an SV rights owner name is specified, then we should report the rewards as SV rewards and not as unclaimed activity records. This way non-milestone payments out of the unclaimed activity records are not reported as SV rewards.

amount : Decimal -- ^ The amount of `Amulet` to be minted
reason : Text -- ^ A reason to mint the `Amulet`
expiresAt : Time -- ^ Selected timestamp defining the lifetime of the UnclaimedActivityRecord contract.
svRightOwnerName: Optional Text -- ^ SV right owner whose lifetime rewards increased.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unless this name is already what Moritz' PR is using (and maybe even then?) could we make it svRightsHolderName? Would be nice to have consistent terminology with what we've been using in the spec.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It is already used in Moritz' PR. It is also consistent with the use of 'owner' in the token standard where a Holding has an owner an not a holder. That said: your proposal feels more like proper English, but I believe switching to that name is no longer possible.

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.

Kept as is 🫡

@meiersi-da meiersi-da left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks. This goes in the right direction. One thing that is missing is the conditional population of that field as part of reward coupon creation; and full end-to-end tests for this flow, and the flow via unclaimed activity record.

beneficiary = beneficiary.primaryParty
weight
round = Round 0
svRightOwnerName = Some "provider1"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note: I'd probably have gone for the simpler option of just using svRightOwnerName = None for all the existing tests, as that corresponds to tests for code running in Phase 1.

It's not a problem that you adjusted it here, but it's just to point out a simpler option.

beneficiary : Party -- ^ The beneficiary allowed to receive the reward.
round : Round
weight : Int -- ^ Coupons receive a share of the SV issuance proportional to their weight.
svRightOwnerName: Optional Text -- ^ SV right owner whose lifetime rewards increased.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
svRightOwnerName: Optional Text -- ^ SV right owner whose lifetime rewards increased.
svRightOwnerName: Optional Text -- ^ SV right owner whose rewards this coupons represents.

@@ -498,6 +498,7 @@ template SvRewardCoupon with

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
sv : Party -- ^ The node operator party identifying the SV node that initiated the coupon creation.

amount : Decimal -- ^ The amount of `Amulet` to be minted.
reason : Text -- ^ A reason to mint the `Amulet`.
expiresAt : Time -- ^ Selected timestamp defining the lifetime of the contract.
svRightOwnerName: Optional Text -- ^ SV right owner whose lifetime rewards increased.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
svRightOwnerName: Optional Text -- ^ SV right owner whose lifetime rewards increased.
svRightOwnerName : Optional Text -- ^ SV right owner that is reward by this coupon; and whose lifetime rewards thus increase when minting it.

dso : Party
amount : Decimal
beneficiary : Party
svRightOwnerName: Text -- ^ SV right owner whose lifetime rewards increased.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Please add comment to the template and all its fields as done for (most) of the other templates in this file.

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.

Should be done, lmk how that looks 🫡

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See my comments above. Please compare to your formulation to understand their motivation; and how to improve your comments.

-- the upgrade checker on package upload because `TransferInputsSummary` is serializable.
mintedRewardMap : Optional (Map Party Decimal)
-- ^ Note: This is a map from the beneficiaries of unclaimed activity records, and reward coupons to
mintedRewardMap : Optional (Map Text Decimal)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
mintedRewardMap : Optional (Map Text Decimal)
mintedSvRewardMap : Optional (Map Text Decimal)

pull through that refinement in the other functions

-- compute balance change
miningRound <- getIssuingMiningRound csum coupon.round
let currentSvRewardCouponAmount = intToDecimal coupon.weight * miningRound.issuancePerSvRewardCoupon
svRightOwnerName <- grabSvRightOwnerName coupon.svRightOwnerName

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yes, this should not fail. Just skip generating the receipt.

summarizeAndConsumeInput _round s (InputUnclaimedActivityRecord unclaimedActivityRecordCid) = do
unclaimedActivityRecord <- fetchAndArchive forOwner unclaimedActivityRecordCid
assertWithinDeadline "UnclaimedActivityRecord.expiresAt" unclaimedActivityRecord.expiresAt
svRightOwnerName <- grabSvRightOwnerName unclaimedActivityRecord.svRightOwnerName

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Let's also do an additional cleanup: when an SV rights owner name is specified, then we should report the rewards as SV rewards and not as unclaimed activity records. This way non-milestone payments out of the unclaimed activity records are not reported as SV rewards.

beneficiary
weight
round = round._2.round
svRightOwnerName = Some

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

typo?

amount : Decimal -- ^ The amount of `Amulet` to be minted
reason : Text -- ^ A reason to mint the `Amulet`
expiresAt : Time -- ^ Selected timestamp defining the lifetime of the UnclaimedActivityRecord contract.
svRightOwnerName: Optional Text -- ^ SV right owner whose lifetime rewards increased.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It is already used in Moritz' PR. It is also consistent with the use of 'owner' in the token standard where a Holding has an owner an not a holder. That said: your proposal feels more like proper English, but I believe switching to that name is no longer possible.

@meiersi-da meiersi-da left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks. This is now looking good modulo the two small comments; and missing tests.

Comment thread daml/splice-amulet/daml/Splice/Amulet.daml
addMintedReward beneficiary amount (Some m) = Some $ Map.insertWith (+) beneficiary amount m
addMintedReward : Text -> Decimal -> Optional (Map Text Decimal) -> Optional (Map Text Decimal)
addMintedReward svRightOwnerName amount None = Some $ Map.singleton svRightOwnerName amount
addMintedReward svRightOwnerName amount (Some m) = Some $ Map.insertWith (+) svRightOwnerName amount m

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
addMintedReward svRightOwnerName amount (Some m) = Some $ Map.insertWith (+) svRightOwnerName amount m
addMintedSvReward svRightOwnerName amount (Some m) = Some $ Map.insertWith (+) svRightOwnerName amount m

@meiersi-da meiersi-da left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks.

pure UnclaimedActivityRecord_DsoExpireResult with unclaimedRewardCid


-- | A record that tracks the lifetime rewards accumulated by a SV right owner.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
-- | A record that tracks the lifetime rewards accumulated by a SV right owner.
-- | A record that tracks an SV rights owner's rewards minted from their weight.



-- | A record that tracks the lifetime rewards accumulated by a SV right owner.
-- This record should be created when minting from `SvRewardCoupon` or `UnclaimedActvityRecord` inputs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
-- This record should be created when minting from `SvRewardCoupon` or `UnclaimedActvityRecord` inputs.

references of the contexts where some type or function is used usually age badly

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.

Ah good point

dso : Party
amount : Decimal
beneficiary : Party
amount : Decimal -- ^ The amount of lifetime rewards an SV right owner has accumulated.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
amount : Decimal -- ^ The amount of lifetime rewards an SV right owner has accumulated.
amount : Decimal -- ^ The amount of SV rewards minted

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.

Oops mine was definitely not correct

dso : Party
amount : Decimal
beneficiary : Party
svRightOwnerName: Text -- ^ SV right owner whose lifetime rewards increased.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See my comments above. Please compare to your formulation to understand their motivation; and how to improve your comments.

Comment thread daml/splice-amulet/daml/Splice/Amulet.daml
Comment on lines +65 to +71
require ("SvRewardMintReceipt svRightOwnerName: '"
<> r.svRightOwnerName
<> "' should match SvRewardState's svName: '"
<> svName
<> "'."
)
(svName == r.svRightOwnerName)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

use TokenStandard.Utils.requireMatchExpected that has logic to create good error message for this.

do
-- FIXME: We would like to do the following but would result in a cyclic import
-- _ <- getAndValidateSvNodeOperatorParty this (Some sv)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

require that there is at least one receipt

Comment on lines +80 to +83
totalLifetimeRewardAmount =
case this.totalLifetimeRewardAmount of
None -> Some receiptTotal
Some t -> Some (t + receiptTotal)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

define:

getLifetimeRewardTotal : SvRewardState -> Decimal
getLifetimeRewardTotal state = fromOptional 0.0 state.lifetimeRewardTotal

which will become handy for the reward coupon creation as well; and use

Suggested change
totalLifetimeRewardAmount =
case this.totalLifetimeRewardAmount of
None -> Some receiptTotal
Some t -> Some (t + receiptTotal)
totalLifetimeRewardAmount = Some $ getLifetimeRewardTotal this + receiptTotal

This way we also ensure that we encode the default only once.

dso : Party
svName : Text -- ^ sv right owner name
state : RewardState
totalLifetimeRewardAmount : Optional Decimal -- ^ Tracks the total lifetime rewards of an SV rights owner from Phase 2 onward.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
totalLifetimeRewardAmount : Optional Decimal -- ^ Tracks the total lifetime rewards of an SV rights owner from Phase 2 onward.
lifetimeRewardTotal : Optional Decimal
-- ^ Total amount of SV rewards minted for this SV rights owner.

Comment on lines +72 to +74
return r

let receiptTotal = sum [r.amount | r <- receipts]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
return r
let receiptTotal = sum [r.amount | r <- receipts]
return r.amount
let receiptTotal = sum receiptAmounts

you might then even inline the total computation into the one computation that uses it

@meiersi-da meiersi-da left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks. See my comments for another round of feedback.



-- | A record that tracks an SV rights owner's rewards minted from their weight.
-- This record should be created when minting an SV's reward coupon or if there are any coupons that go unclaimed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
-- This record should be created when minting an SV's reward coupon or if there are any coupons that go unclaimed.
-- This record should be created when minting an SV's reward coupon.

or if there are any coupons that go unclaimed.

What do you mean by that?

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.

I was trying to describe the case for when it goes the InputUnclaimedActivityRecord route, but I guess it goes to the SV's reward amount regardless, so maybe yeah that comment portion isn't needed.

-- NOTE: WIP
test_Lifetime_SvRewards_Tracking : Script ()
test_Lifetime_SvRewards_Tracking = do
(app, (dso), (sv1, sv2, _, _)) <- initDecentralizedSynchronizerWithNonZeroRound True 34 True

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why this non-standard bootstrapping? Not that it's problem, but it is surprising.

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.

Sorry, I'm not following what you mean by this. I saw other examples of initDecentralizedSynchronizerWithNonZeroRound being used like this, should I be using it differently if that's what you mean?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

usuually we just use initMainNet. If you look at the other cases in this file, then these are tests that intentionally choose a non-zero initial round.

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.

Ah I see, I think I was looking at the onLedgerSvRightOwners potion at that time and just grabbed that, but I do see that it's also set to true on initMainNet. Also wasn't sure about the isDevNet portion at the time. Will change this now

Comment on lines +388 to +389
require (show receipt.svRightOwnerName <> " balance: expected " <> show amount <> " == " <> show receipt.amount)
(receipt.amount == amount)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
require (show receipt.svRightOwnerName <> " balance: expected " <> show amount <> " == " <> show receipt.amount)
(receipt.amount == amount)
unless (receipt.amount == amount) $ fail $ show receipt.svRightOwnerName <> " balance: expected amount " <> show amount <> " /= actual amount" <> show receipt.amount)

This structure is often easier to use; and seems to get better auto-completions from Co-pilot.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

just a suggestion

(receipt.amount == amount)
let checkLifetimeRewardTotal dso sv amount = do
(name, _) <- getSvInfoByParty app sv
-- TODO: Double check this is correct (we get two reward states under sv1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

So you are saying you see two of these states? That sounds like a bug. Do you know where they stem from?

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.

Yeah that was a comment I had left for myself on Friday todo monday, we weren't archiving the old SvRewardState under the DsoRules_MergeSvRewardMintReceipts here


[(_, round)] <- queryFilter @IssuingMiningRound app.dso (\r -> r.round == round0._2.round)
-- let sv1Weight = intToDecimal $ bpsMultiplier * 10 - 1
-- TODO: Better way to get weight? Weight is 100000, a little unsure how we came to this number

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

weights are ints in basis points. Weight 10 is the weight of an SV that is qualified as Tier 1. 10 * 10e4 = 100e3.

[(_, round)] <- queryFilter @IssuingMiningRound app.dso (\r -> r.round == round0._2.round)
-- let sv1Weight = intToDecimal $ bpsMultiplier * 10 - 1
-- TODO: Better way to get weight? Weight is 100000, a little unsure how we came to this number
let sv1Balance0 = round.issuancePerSvRewardCoupon * 100000.0 - createFee

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

createFees are 0.0 since https://github.com/canton-foundation/cips/blob/main/cip-0078/cip-0078.md

no need to track them in your test script

beneficiary
weight
round = openRound.round
svRightOwnerName = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Given that we are not tracking lifetime rewards here, let's add a TODO to ensure that this choice cannot be called after the activation of on-ledger SV locking.

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.

Done above at beginning of funciton

do
_ <- getAndValidateSvNodeOperatorParty this (Some svOperator)

require "There must be at least receiptCid in order merge sv reward mint receipts" $

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
require "There must be at least receiptCid in order merge sv reward mint receipts" $
require "There must be at least one receipt" $

no need to repeat the context that's given by the choice name.

@dbzguy56 dbzguy56 changed the title WIP Start adding SvRightOwnerName fields to the appropriate places cip-0105 lifetime rewards tracking Aug 26, 2026
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.

3 participants