cip-0105 lifetime rewards tracking - #7
Conversation
| -- compute balance change | ||
| miningRound <- getIssuingMiningRound csum coupon.round | ||
| let currentSvRewardCouponAmount = intToDecimal coupon.weight * miningRound.issuancePerSvRewardCoupon | ||
| svRightOwnerName <- grabSvRightOwnerName coupon.svRightOwnerName |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
(see other comment about failure vs. skipping)
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
| 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 | |||
There was a problem hiding this comment.
| 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. |
There was a problem hiding this comment.
| 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. |
There was a problem hiding this comment.
Please add comment to the template and all its fields as done for (most) of the other templates in this file.
There was a problem hiding this comment.
Should be done, lmk how that looks 🫡
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
| 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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
| 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. |
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
Thanks. This is now looking good modulo the two small comments; and missing tests.
| 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 |
There was a problem hiding this comment.
| addMintedReward svRightOwnerName amount (Some m) = Some $ Map.insertWith (+) svRightOwnerName amount m | |
| addMintedSvReward svRightOwnerName amount (Some m) = Some $ Map.insertWith (+) svRightOwnerName amount m |
| pure UnclaimedActivityRecord_DsoExpireResult with unclaimedRewardCid | ||
|
|
||
|
|
||
| -- | A record that tracks the lifetime rewards accumulated by a SV right owner. |
There was a problem hiding this comment.
| -- | 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. |
There was a problem hiding this comment.
| -- 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
| dso : Party | ||
| amount : Decimal | ||
| beneficiary : Party | ||
| amount : Decimal -- ^ The amount of lifetime rewards an SV right owner has accumulated. |
There was a problem hiding this comment.
| amount : Decimal -- ^ The amount of lifetime rewards an SV right owner has accumulated. | |
| amount : Decimal -- ^ The amount of SV rewards minted |
There was a problem hiding this comment.
Oops mine was definitely not correct
| dso : Party | ||
| amount : Decimal | ||
| beneficiary : Party | ||
| svRightOwnerName: Text -- ^ SV right owner whose lifetime rewards increased. |
There was a problem hiding this comment.
See my comments above. Please compare to your formulation to understand their motivation; and how to improve your comments.
| require ("SvRewardMintReceipt svRightOwnerName: '" | ||
| <> r.svRightOwnerName | ||
| <> "' should match SvRewardState's svName: '" | ||
| <> svName | ||
| <> "'." | ||
| ) | ||
| (svName == r.svRightOwnerName) |
There was a problem hiding this comment.
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) | ||
|
|
There was a problem hiding this comment.
require that there is at least one receipt
| totalLifetimeRewardAmount = | ||
| case this.totalLifetimeRewardAmount of | ||
| None -> Some receiptTotal | ||
| Some t -> Some (t + receiptTotal) |
There was a problem hiding this comment.
define:
getLifetimeRewardTotal : SvRewardState -> Decimal
getLifetimeRewardTotal state = fromOptional 0.0 state.lifetimeRewardTotal
which will become handy for the reward coupon creation as well; and use
| 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. |
There was a problem hiding this comment.
| 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. |
| return r | ||
|
|
||
| let receiptTotal = sum [r.amount | r <- receipts] |
There was a problem hiding this comment.
| 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
left a comment
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
| -- 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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Why this non-standard bootstrapping? Not that it's problem, but it is surprising.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
| require (show receipt.svRightOwnerName <> " balance: expected " <> show amount <> " == " <> show receipt.amount) | ||
| (receipt.amount == amount) |
There was a problem hiding this comment.
| 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.
| (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) |
There was a problem hiding this comment.
So you are saying you see two of these states? That sounds like a bug. Do you know where they stem from?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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" $ |
There was a problem hiding this comment.
| 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.
Pull Request Checklist
Cluster Testing
/cluster_teston this PR to request it, and ping someone with access to the DA-internal system to approve it./upgrade_teston this PR to request it, and ping someone with access to the DA-internal system to approve it./hdm_teston this PR to request it, and ping someone with access to the DA-internal system to approve it./lsu_teston this PR to request it, and ping someone with access to the DA-internal system to approve it.PR Guidelines
Fixes #n, and mention issues worked on using#nMerge Guidelines