diff --git a/daml/splice-amulet-test/daml/Splice/Scripts/TestAmuletRulesTransfer.daml b/daml/splice-amulet-test/daml/Splice/Scripts/TestAmuletRulesTransfer.daml index 50d504a95b..e3066d2b1d 100644 --- a/daml/splice-amulet-test/daml/Splice/Scripts/TestAmuletRulesTransfer.daml +++ b/daml/splice-amulet-test/daml/Splice/Scripts/TestAmuletRulesTransfer.daml @@ -3,6 +3,7 @@ module Splice.Scripts.TestAmuletRulesTransfer where +import DA.Action (replicateA_) import DA.Assert import DA.Foldable (forA_) import DA.List @@ -110,6 +111,7 @@ testUsageFees = do inputUnclaimedActivityRecordAmount = Some 0.0 inputValidatorFaucetAmount = Some 0.0 inputSvRewardAmount = 0.0 + inputDevelopmentFundAmount = Some 0.0 inputAmuletAmount = 100.0 balanceChanges = Map.empty holdingFees = 0.0 -- no holding fees charged on transfer @@ -574,5 +576,66 @@ testUnclaimedActivityRecordTransferInput = do (_, openRound) <- getLatestActiveOpenRound app let createFee = openRound.transferConfigUsd.createFee.fee * openRound.amuletPrice transferResult.summary.senderChangeAmount === amountToMint - createFee + transferResult.summary.inputUnclaimedActivityRecordAmount === Some amountToMint + + pure () + +testInputDevelopmentFundCoupon : Script () +testInputDevelopmentFundCoupon = do + defaultAppWithUsers@DefaultAppWithUsers{..} <- setupDefaultAppWithUsers + baseConfig <- getAmuletConfig app + -- Replace the default AmuletConfig with a 0.05% allocation for the development fund. + -- This configuration applies to the next open round. + replaceDevelopmentFundConfig app baseConfig (Some alice.primaryParty) (Some 0.05) + + -- Run 3 issuances for the already open rounds without the development fund configuration. + -- No development fund coupons are minted. + replicateA_ 3 $ runNextIssuance app + -- Mint one development fund coupon per issuance (two total). + runNextIssuance app + runNextIssuance app + context <- getPaymentTransferContext app alice + [(cid1, coupon1), (cid2, coupon2)] <- query @UnclaimedDevelopmentFundCoupon app.dso + let totalAmount = coupon1.amount + coupon2.amount + + -- Allocate a DevelopmentFundCoupon + now <- getTime + let expiresAt = addRelTime now (days 2) + AmuletRules_AllocateDevelopmentFundCouponResult + { developmentFundCouponCid, optUnclaimedDevelopmentFundCouponCid = None } <- + allocateDevelopmentFundCoupon app alice.primaryParty bob.primaryParty totalAmount expiresAt [cid1, cid2] + + -- Unhappy - expiresAt has been reached + setTime $ addRelTime expiresAt (minutes 1) + submitMultiMustFail [bob.primaryParty] [app.dso] $ + exerciseCmd context.amuletRules AmuletRules_Transfer with + transfer = Transfer with + sender = bob.primaryParty + provider = bob.primaryParty + inputs = [InputDevelopmentFundCoupon developmentFundCouponCid] + outputs = [] + beneficiaries = None + context = context.context + expectedDso = Some app.dso + setTime now + + -- Happy + transferResult <- + checkTransferMetadata app TxKind_MergeSplit bob.primaryParty $ + checkBalanceChanges defaultAppWithUsers $ + submitMulti [bob.primaryParty] [app.dso] $ + exerciseCmd context.amuletRules AmuletRules_Transfer with + transfer = Transfer with + sender = bob.primaryParty + provider = bob.primaryParty + inputs = [InputDevelopmentFundCoupon developmentFundCouponCid] + outputs = [] + beneficiaries = None + context = context.context + expectedDso = Some app.dso + (_, openRound) <- getLatestActiveOpenRound app + let createFee = openRound.transferConfigUsd.createFee.fee * openRound.amuletPrice + transferResult.summary.senderChangeAmount === totalAmount - createFee + transferResult.summary.inputDevelopmentFundAmount === Some totalAmount pure () diff --git a/daml/splice-amulet-test/daml/Splice/Scripts/TestDesignExample.daml b/daml/splice-amulet-test/daml/Splice/Scripts/TestDesignExample.daml index 05ba573799..ccf1084c82 100644 --- a/daml/splice-amulet-test/daml/Splice/Scripts/TestDesignExample.daml +++ b/daml/splice-amulet-test/daml/Splice/Scripts/TestDesignExample.daml @@ -158,6 +158,7 @@ test_designExample= do inputUnclaimedActivityRecordAmount = Some 0.0 inputValidatorFaucetAmount = Some 0.0 inputSvRewardAmount = 0.0 + inputDevelopmentFundAmount = Some 0.0 inputAmuletAmount = refreshAmuletAmount + lockedAmuletAmount balanceChanges = Map.empty holdingFees diff --git a/daml/splice-amulet-test/daml/Splice/Scripts/TestDevelopmentFundCoupon.daml b/daml/splice-amulet-test/daml/Splice/Scripts/TestDevelopmentFundCoupon.daml new file mode 100644 index 0000000000..8b080c437e --- /dev/null +++ b/daml/splice-amulet-test/daml/Splice/Scripts/TestDevelopmentFundCoupon.daml @@ -0,0 +1,190 @@ +-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +module Splice.Scripts.TestDevelopmentFundCoupon where + +import DA.Action (replicateA_) +import DA.Assert +import DA.Time +import Daml.Script + +import Splice.Amulet +import Splice.AmuletRules +import Splice.Scripts.Util + +testAllocateDevelopmentFundCoupon : Script () +testAllocateDevelopmentFundCoupon = do + DefaultAppWithUsers{..} <- setupDefaultAppWithUsers + -- Replace the default AmuletConfig with a 0.05% allocation for the development fund. + -- No Fund manager configured. + -- This configuration applies to the next open round. + baseConfig <- getAmuletConfig app + replaceDevelopmentFundConfig app baseConfig None (Some 0.05) + + -- Run 3 issuances for the already open rounds without the development fund configuration. + -- No development fund coupons are minted. + replicateA_ 3 $ runNextIssuance app + -- Mint one development fund coupon per issuance (two total). + runNextIssuance app + runNextIssuance app + now <- getTime + [(cid1, coupon1), (cid2, coupon2)] <- query @UnclaimedDevelopmentFundCoupon app.dso + let + totalAmount = coupon1.amount + coupon2.amount + unclaimedDevelopmentFundCouponCids = [cid1, cid2] + expiresAt = addRelTime now (days 2) + firstAllocationAmount = 10.0 + secondAllocationAmount = totalAmount - firstAllocationAmount + + -- Unhappy - DevelopmentFundCoupon cannot be allocated without a Development Fund manager configured + allocateDevelopmentFundCouponMustFail app alice.primaryParty bob.primaryParty firstAllocationAmount + expiresAt unclaimedDevelopmentFundCouponCids + + -- Set 5% and Alice as the fund manager + replaceDevelopmentFundConfig app baseConfig (Some alice.primaryParty) (Some 0.05) + + -- Unhappy - expiresAt is in the past + let invalidExpiresAt = addRelTime now (minutes (-1)) + allocateDevelopmentFundCouponMustFail app alice.primaryParty bob.primaryParty firstAllocationAmount + invalidExpiresAt unclaimedDevelopmentFundCouponCids + + -- Unhappy - controller must be the configured Development Fund manager (alice) + allocateDevelopmentFundCouponMustFail app bob.primaryParty bob.primaryParty firstAllocationAmount + expiresAt unclaimedDevelopmentFundCouponCids + + -- Unhappy - insufficient amount to cover the requested allocation + allocateDevelopmentFundCouponMustFail app alice.primaryParty bob.primaryParty (totalAmount + 1.0) + expiresAt unclaimedDevelopmentFundCouponCids + + -- Happy - Development Fund coupon allocated with UnclaimedDevelopmentFundCoupon leftover + AmuletRules_AllocateDevelopmentFundCouponResult + { developmentFundCouponCid, optUnclaimedDevelopmentFundCouponCid = Some leftOverUnclaimedDevelopmentFundCouponCid } <- + allocateDevelopmentFundCoupon app alice.primaryParty bob.primaryParty firstAllocationAmount + expiresAt unclaimedDevelopmentFundCouponCids + Some developmentFundCoupon <- queryContractId bob.primaryParty developmentFundCouponCid + developmentFundCoupon === DevelopmentFundCoupon with + dso = app.dso + beneficiary = bob.primaryParty + fundManager = alice.primaryParty + amount = firstAllocationAmount + expiresAt + + -- Happy - Development Fund coupon allocated with no UnclaimedDevelopmentFundCoupon leftover + AmuletRules_AllocateDevelopmentFundCouponResult { developmentFundCouponCid, optUnclaimedDevelopmentFundCouponCid = None } <- + allocateDevelopmentFundCoupon app alice.primaryParty bob.primaryParty secondAllocationAmount + expiresAt [leftOverUnclaimedDevelopmentFundCouponCid] + Some developmentFundCoupon <- queryContractId bob.primaryParty developmentFundCouponCid + developmentFundCoupon === DevelopmentFundCoupon with + dso = app.dso + beneficiary = bob.primaryParty + fundManager = alice.primaryParty + amount = secondAllocationAmount + expiresAt + + pure () + +testWithdrawalOfDevelopmentFundCoupon : Script () +testWithdrawalOfDevelopmentFundCoupon = do + DefaultAppWithUsers{..} <- setupDefaultAppWithUsers + now <- getTime + let + amount = 42.0 + expiresAt = addRelTime now (days 1) + fundManager = alice.primaryParty + beneficiary = bob.primaryParty + reason = "Some reason" + + developmentFundCouponCid <- createDevelopmentFundCoupon app fundManager beneficiary amount expiresAt + + -- Unhappy - expiresAt has been reached + setTime $ addRelTime expiresAt (minutes 1) + withdrawDevelopmentFundCouponMustFail fundManager reason developmentFundCouponCid + + setTime now + + -- Unhappy - invalid controller + withdrawDevelopmentFundCouponMustFail app.dso reason developmentFundCouponCid + withdrawDevelopmentFundCouponMustFail beneficiary reason developmentFundCouponCid + + -- Happy + unclaimedDevelopmentFundCouponCid <- withdrawDevelopmentFundCoupon fundManager reason developmentFundCouponCid + Some unclaimedDevelopmentFundCoupon <- queryContractId app.dso unclaimedDevelopmentFundCouponCid + unclaimedDevelopmentFundCoupon === UnclaimedDevelopmentFundCoupon with + dso = app.dso + amount + + pure () + +testExpiryOfDevelopmentFundCoupon : Script () +testExpiryOfDevelopmentFundCoupon = do + DefaultAppWithUsers{..} <- setupDefaultAppWithUsers + now <- getTime + let + amount = 42.0 + expiresAt = addRelTime now (days 1) + fundManager = alice.primaryParty + beneficiary = bob.primaryParty + + developmentFundCouponCid <- createDevelopmentFundCoupon app fundManager beneficiary amount expiresAt + + -- Unhappy - expiresAt has not been reached + expiresDevelopmentFundCouponMustFail app.dso developmentFundCouponCid + + setTime $ addRelTime expiresAt (minutes 1) + + -- -- Unhappy - invalid controller + expiresDevelopmentFundCouponMustFail fundManager developmentFundCouponCid + expiresDevelopmentFundCouponMustFail beneficiary developmentFundCouponCid + + -- Happy + unclaimedDevelopmentFundCouponCid <- expiresDevelopmentFundCoupon app.dso developmentFundCouponCid + Some unclaimedDevelopmentFundCoupon <- queryContractId app.dso unclaimedDevelopmentFundCouponCid + unclaimedDevelopmentFundCoupon === UnclaimedDevelopmentFundCoupon with + dso = app.dso + amount + + pure () + + +allocateDevelopmentFundCouponMustFail + : AmuletApp -> Party -> Party -> Decimal -> Time -> [ContractId UnclaimedDevelopmentFundCoupon] + -> Script () +allocateDevelopmentFundCouponMustFail app fundManager beneficiary amount expiresAt unclaimedDevelopmentFundCouponCids = do + [(amuletRulesCid, _)] <- query @AmuletRules app.dso + submitMultiMustFail [fundManager] [app.dso] do + exerciseCmd amuletRulesCid AmuletRules_AllocateDevelopmentFundCoupon with + unclaimedDevelopmentFundCouponCids + beneficiary + amount + expiresAt + fundManager + +createDevelopmentFundCoupon : AmuletApp -> Party -> Party -> Decimal -> Time -> Script (ContractId DevelopmentFundCoupon) +createDevelopmentFundCoupon app fundManager beneficiary amount expiresAt = + submit app.dso do + createCmd DevelopmentFundCoupon with + dso = app.dso + beneficiary + fundManager + amount + expiresAt + +withdrawDevelopmentFundCoupon : Party -> Text -> ContractId DevelopmentFundCoupon -> Script (ContractId UnclaimedDevelopmentFundCoupon) +withdrawDevelopmentFundCoupon fundManager reason developmentFundCouponCid = + (.unclaimedDevelopmentFundCouponCid) <$> submit fundManager do + exerciseCmd developmentFundCouponCid DevelopmentFundCoupon_Withdraw with reason + +withdrawDevelopmentFundCouponMustFail : Party -> Text -> ContractId DevelopmentFundCoupon -> Script () +withdrawDevelopmentFundCouponMustFail actor reason developmentFundCouponCid = + submitMustFail actor do + exerciseCmd developmentFundCouponCid DevelopmentFundCoupon_Withdraw with reason + +expiresDevelopmentFundCoupon : Party -> ContractId DevelopmentFundCoupon -> Script (ContractId UnclaimedDevelopmentFundCoupon) +expiresDevelopmentFundCoupon dso developmentFundCouponCid = + (.unclaimedDevelopmentFundCouponCid) <$> submit dso do + exerciseCmd developmentFundCouponCid DevelopmentFundCoupon_DsoExpire + +expiresDevelopmentFundCouponMustFail : Party -> ContractId DevelopmentFundCoupon -> Script () +expiresDevelopmentFundCouponMustFail actor developmentFundCouponCid = + submitMustFail actor do + exerciseCmd developmentFundCouponCid DevelopmentFundCoupon_DsoExpire diff --git a/daml/splice-amulet-test/daml/Splice/Scripts/TestLockAndAmuletExpiry.daml b/daml/splice-amulet-test/daml/Splice/Scripts/TestLockAndAmuletExpiry.daml index f39dae024a..9d25a4e883 100644 --- a/daml/splice-amulet-test/daml/Splice/Scripts/TestLockAndAmuletExpiry.daml +++ b/daml/splice-amulet-test/daml/Splice/Scripts/TestLockAndAmuletExpiry.daml @@ -33,6 +33,7 @@ scaleAmuletConfig amuletPrice config = AmuletConfig with packageConfig = config.packageConfig transferPreapprovalFee = fmap (/ amuletPrice) config.transferPreapprovalFee featuredAppActivityMarkerAmount = fmap (/ amuletPrice) config.featuredAppActivityMarkerAmount + optDevelopmentFundManager = config.optDevelopmentFundManager test : Script () test = script do diff --git a/daml/splice-amulet-test/daml/Splice/Scripts/UnitTests/Issuance.daml b/daml/splice-amulet-test/daml/Splice/Scripts/UnitTests/Issuance.daml index e32b7b3edb..5c391e4665 100644 --- a/daml/splice-amulet-test/daml/Splice/Scripts/UnitTests/Issuance.daml +++ b/daml/splice-amulet-test/daml/Splice/Scripts/UnitTests/Issuance.daml @@ -21,6 +21,7 @@ tickDuration = defaultAmuletConfig.tickDuration amuletPrice : Decimal amuletPrice = 0.005 + -- Issuance curve retrieval --------------------------- @@ -66,6 +67,16 @@ expectedParameters_E1_0_0p5 = IssuingRoundParameters with unclaimedValidatorRewards = 7551.7503805175 unclaimedAppRewards = 68395.2511415525 unclaimedSvRewards = 0.0000000001 + amuletsToIssueToDevelopmentFund = 0.0 + +expectedParameters_E1_0_0p5_DevFund5 : IssuingRoundParameters +expectedParameters_E1_0_0p5_DevFund5 = + expectedParameters_E1_0_0p5 with + issuancePerSvRewardCoupon = 5783.8660578387 -- decreased by ~5 % (expected linear scaling with 5 % fund allocation) + unclaimedValidatorRewards = 5649.1628614916 -- decreased by ~25 % (validator tranche near cap: small pool cut sharply reduces unclaimed remainder) + unclaimedAppRewards = 62687.4885844749 -- decreased by ~8.3 % (app tranche less constrained; closer to linear scaling) + unclaimedSvRewards = 0.0 -- decreased ~5 % (below rounding threshold) + amuletsToIssueToDevelopmentFund = 38051.7503805175 expectedParameters_E1_0p5_1p5 : IssuingRoundParameters expectedParameters_E1_0p5_1p5 = IssuingRoundParameters with @@ -77,6 +88,16 @@ expectedParameters_E1_0p5_1p5 = IssuingRoundParameters with unclaimedValidatorRewards = 15162.1004566210 unclaimedAppRewards = 106447.0015220700 unclaimedSvRewards = 0.000000004 + amuletsToIssueToDevelopmentFund = 0.0 + +expectedParameters_E1_0p5_1p5_DevFund5 : IssuingRoundParameters +expectedParameters_E1_0p5_1p5_DevFund5 = + expectedParameters_E1_0p5_1p5 with + issuancePerSvRewardCoupon = 1735.1598173516 -- decreased by ~5 % (expected linear scaling with 5 % fund allocation) + unclaimedValidatorRewards = 12878.9954337899 -- decreased by ~15 % (validator tranche near cap: small pool cut sharply reduces unclaimed remainder) + unclaimedAppRewards = 98836.6514459665 -- decreased by ~10 % (app tranche less constrained; closer to linear scaling) + unclaimedSvRewards = 0.0 -- decreased ~5 % (below rounding threshold) + amuletsToIssueToDevelopmentFund = 19025.8751902588 expectedParameters_E1_1p5_5 : IssuingRoundParameters expectedParameters_E1_1p5_5 = IssuingRoundParameters with @@ -88,6 +109,16 @@ expectedParameters_E1_1p5_5 = IssuingRoundParameters with unclaimedValidatorRewards = 3746.5753424658 unclaimedAppRewards = 72200.4261796042 unclaimedSvRewards = 0.0 + amuletsToIssueToDevelopmentFund = 0.0 + +expectedParameters_E1_1p5_5_DevFund5 : IssuingRoundParameters +expectedParameters_E1_1p5_5_DevFund5 = + expectedParameters_E1_1p5_5 with + issuancePerSvRewardCoupon = 361.4916286149 -- decreased by ~5 % (expected linear scaling with 5 % fund allocation) + unclaimedValidatorRewards = 2034.2465753425 -- decreased by ~45 % (validator tranche strongly cap-bound; small pool accentuates drop) + unclaimedAppRewards = 66302.4048706240 -- decreased by ~8.2 % (app tranche less constrained; close to linear scaling) + unclaimedSvRewards = 0.0000000016 -- minor rounding drift (below significance threshold) + amuletsToIssueToDevelopmentFund = 9512.9375951294 expectedParameters_E1_5_10 : IssuingRoundParameters expectedParameters_E1_5_10 = IssuingRoundParameters with @@ -99,6 +130,17 @@ expectedParameters_E1_5_10 = IssuingRoundParameters with unclaimedValidatorRewards = 0.0000000017 unclaimedAppRewards = 19879.2694063927 unclaimedSvRewards = 0.0 + amuletsToIssueToDevelopmentFund = 0.0 + +expectedParameters_E1_5_10_DevFund5 : IssuingRoundParameters +expectedParameters_E1_5_10_DevFund5 = + expectedParameters_E1_5_10 with + issuancePerValidatorFaucetCoupon = 339.5662100457 -- decreased by ~5.6 % (slightly above 5 % due to faucet cap effects) + issuancePerSvRewardCoupon = 90.3729071537 -- decreased by ~5 % (expected linear scaling) + unclaimedValidatorRewards = 0.0 -- minor rounding drift (below significance threshold) + unclaimedAppRewards = 16597.3059360731 -- decreased by ~16.5 % (cap constraints amplify reduction) + unclaimedSvRewards = 0.0000000029 -- minor rounding drift (below significance threshold) + amuletsToIssueToDevelopmentFund = 4756.4687975647 expectedParameters_E1_10plus : IssuingRoundParameters expectedParameters_E1_10plus = IssuingRoundParameters with @@ -110,23 +152,45 @@ expectedParameters_E1_10plus = IssuingRoundParameters with unclaimedValidatorRewards = 0.0 unclaimedAppRewards = 0.0000000152 unclaimedSvRewards = 0.0000000023 + amuletsToIssueToDevelopmentFund = 0.0 + +expectedParameters_E1_10plus_DevFund5 : IssuingRoundParameters +expectedParameters_E1_10plus_DevFund5 = + expectedParameters_E1_10plus with + issuancePerFeaturedAppRewardCoupon = 70.3246004566 -- decreased by ~6 % (slightly above 5 % since both featured and unfeatured tranches are below cap) + issuancePerValidatorFaucetCoupon = 140.7458143075 -- decreased by ~6.3 % (faucet below cap, reduced pool scales slightly more than linearly) + issuancePerSvRewardCoupon = 22.5932267884 -- decreased by ~5 % (expected linear scaling) + unclaimedAppRewards = 0.0000000084 -- decreased ~5 % (below rounding threshold) + unclaimedSvRewards = 0.0000000033 -- minor rounding drift (below significance threshold) + amuletsToIssueToDevelopmentFund = 2378.2343987823 + + testE1 : Script () testE1 = script do validateOpenMiningRoundSummary summaryExample1 + -- 0% Development Fund expectedParameters_E1_0_0p5 === computeIssuingRoundParameters tickDuration amuletPrice issuanceConfig_0_0p5 summaryExample1 expectedParameters_E1_0p5_1p5 === computeIssuingRoundParameters tickDuration amuletPrice issuanceConfig_0p5_1p5 summaryExample1 expectedParameters_E1_1p5_5 === computeIssuingRoundParameters tickDuration amuletPrice issuanceConfig_1p5_5 summaryExample1 expectedParameters_E1_5_10 === computeIssuingRoundParameters tickDuration amuletPrice issuanceConfig_5_10 summaryExample1 expectedParameters_E1_10plus === computeIssuingRoundParameters tickDuration amuletPrice issuanceConfig_10plus summaryExample1 + -- 5% Development Fund + -- Most parameters decrease by ~5%; larger deviations occur when issuance caps bind or rounding applies. + expectedParameters_E1_0_0p5_DevFund5 === computeIssuingRoundParameters tickDuration amuletPrice (withDevFund5 issuanceConfig_0_0p5) summaryExample1 + expectedParameters_E1_0p5_1p5_DevFund5 === computeIssuingRoundParameters tickDuration amuletPrice (withDevFund5 issuanceConfig_0p5_1p5) summaryExample1 + expectedParameters_E1_1p5_5_DevFund5 === computeIssuingRoundParameters tickDuration amuletPrice (withDevFund5 issuanceConfig_1p5_5) summaryExample1 + expectedParameters_E1_5_10_DevFund5 === computeIssuingRoundParameters tickDuration amuletPrice (withDevFund5 issuanceConfig_5_10) summaryExample1 + expectedParameters_E1_10plus_DevFund5 === computeIssuingRoundParameters tickDuration amuletPrice (withDevFund5 issuanceConfig_10plus) summaryExample1 + -- Example 2: all zeros ----------------------- -summaryExamplNoActivity : OpenMiningRoundSummary -summaryExamplNoActivity = OpenMiningRoundSummary with +summaryExampleNoActivity : OpenMiningRoundSummary +summaryExampleNoActivity = OpenMiningRoundSummary with totalValidatorRewardCoupons = 0.0 totalFeaturedAppRewardCoupons = 0.0 totalUnfeaturedAppRewardCoupons = 0.0 @@ -143,12 +207,28 @@ expectedParameters_NoActivity_0_0p5 = IssuingRoundParameters with unclaimedValidatorRewards = 38051.7503805175 unclaimedAppRewards = 114155.2511415525 unclaimedSvRewards = 608828.0060882801 + amuletsToIssueToDevelopmentFund = 0.0 + +expectedParameters_NoActivity_0_0p5_DevFund5 : IssuingRoundParameters +expectedParameters_NoActivity_0_0p5_DevFund5 = + expectedParameters_NoActivity_0_0p5 with + issuancePerSvRewardCoupon = 578386.6057838661 -- decreased by ~5 % (expected linear scaling; no coupons active) + unclaimedValidatorRewards = 36149.1628614916 -- decreased by ~5 % (fund allocation directly reduces total issuance) + unclaimedAppRewards = 108447.4885844749 -- decreased by ~5 % (expected linear scaling) + unclaimedSvRewards = 578386.6057838661 -- decreased by ~5 % (identical scaling since all rewards remain unclaimed) + amuletsToIssueToDevelopmentFund = 38051.7503805175 + testNoActivity : Script () testNoActivity = script do - validateOpenMiningRoundSummary summaryExamplNoActivity + validateOpenMiningRoundSummary summaryExampleNoActivity + + -- 0% Development Fund + expectedParameters_NoActivity_0_0p5 === computeIssuingRoundParameters tickDuration amuletPrice issuanceConfig_0_0p5 summaryExampleNoActivity - expectedParameters_NoActivity_0_0p5 === computeIssuingRoundParameters tickDuration amuletPrice issuanceConfig_0_0p5 summaryExamplNoActivity + -- 5% Development Fund + -- Most parameters decrease by ~5%; larger deviations occur when issuance caps bind or rounding applies. + expectedParameters_NoActivity_0_0p5_DevFund5 === computeIssuingRoundParameters tickDuration amuletPrice (withDevFund5 issuanceConfig_0_0p5) summaryExampleNoActivity -- Example 3: low activity @@ -173,18 +253,31 @@ expectedParameters_E3_0_0p5 = IssuingRoundParameters with unclaimedValidatorRewards = 9511.7503805175 unclaimedAppRewards = 104095.2511415525 unclaimedSvRewards = 0.0000000001 + amuletsToIssueToDevelopmentFund = 0.0 + +expectedParameters_E3_0_0p5_DevFund5 : IssuingRoundParameters +expectedParameters_E3_0_0p5_DevFund5 = + expectedParameters_E3_0_0p5 with + issuancePerSvRewardCoupon = 289193.302891933 -- decreased by ~5 % (expected linear scaling; SV tranche not capped) + unclaimedValidatorRewards = 7609.1628614916 -- decreased by ~20 % (validator tranche near cap, amplifying reduction) + unclaimedAppRewards = 98387.4885844749 -- decreased by ~5.5 % (mostly linear scaling) + amuletsToIssueToDevelopmentFund = 38051.7503805175 testE4 : Script () testE4 = script do validateOpenMiningRoundSummary summaryExample3 + -- 0% Development Fund expectedParameters_E3_0_0p5 === computeIssuingRoundParameters tickDuration amuletPrice issuanceConfig_0_0p5 summaryExample3 + -- 5% Development Fund + -- Most parameters decrease by ~5%; larger deviations occur when issuance caps bind or rounding applies. + expectedParameters_E3_0_0p5_DevFund5 === computeIssuingRoundParameters tickDuration amuletPrice (withDevFund5 issuanceConfig_0_0p5) summaryExample3 + -- Example 4: millions of SV reward weight ------------------------------------------- - summaryExampleLargeSvRewardWeight : OpenMiningRoundSummary summaryExampleLargeSvRewardWeight = OpenMiningRoundSummary with totalValidatorRewardCoupons = 0.0 @@ -203,13 +296,56 @@ expectedParameters_LargeSvRewardWeight_10plus = IssuingRoundParameters with unclaimedAppRewards = 35673.5159817352 unclaimedValidatorRewards = 9512.9375951294 unclaimedSvRewards = 0.0 + amuletsToIssueToDevelopmentFund = 0.0 + +expectedParameters_LargeSvRewardWeight_10plus_DevFund5 : IssuingRoundParameters +expectedParameters_LargeSvRewardWeight_10plus_DevFund5 = + expectedParameters_LargeSvRewardWeight_10plus with + issuancePerSvRewardCoupon = 0.0009413844 -- decreased by ~5 % (expected linear scaling; SV weight dominates distribution) + unclaimedValidatorRewards = 9037.2907153729 -- decreased by ~5 % (linear with total issuance) + unclaimedAppRewards = 33889.8401826484 -- decreased by ~5 % (expected linear scaling) + unclaimedSvRewards = 0.0001188433 -- minor rounding drift (below significance threshold) + amuletsToIssueToDevelopmentFund = 2378.2343987823 testLargeSvRewardWeight : Script () testLargeSvRewardWeight = script do - validateOpenMiningRoundSummary summaryExamplNoActivity + validateOpenMiningRoundSummary summaryExampleNoActivity + + -- 0% Development Fund + expectedParameters_LargeSvRewardWeight_10plus === + computeIssuingRoundParameters tickDuration amuletPrice issuanceConfig_10plus summaryExampleLargeSvRewardWeight + + -- 5% Development Fund + -- Most parameters decrease by ~5%; larger deviations occur when issuance caps bind or rounding applies. + expectedParameters_LargeSvRewardWeight_10plus_DevFund5 === + computeIssuingRoundParameters tickDuration amuletPrice (withDevFund5 issuanceConfig_10plus) summaryExampleLargeSvRewardWeight + - let actual = computeIssuingRoundParameters tickDuration amuletPrice issuanceConfig_10plus summaryExampleLargeSvRewardWeight - expectedParameters_LargeSvRewardWeight_10plus === actual +-- Example 5: Development Fund receives the full issuance +---------------------------------------------------------- + +expectedParameters_AllIssuanceForFund : IssuingRoundParameters +expectedParameters_AllIssuanceForFund = IssuingRoundParameters with + issuancePerValidatorRewardCoupon = 0.0 + issuancePerFeaturedAppRewardCoupon = 0.0 + issuancePerUnfeaturedAppRewardCoupon = 0.0 + issuancePerValidatorFaucetCoupon = 0.0 + issuancePerSvRewardCoupon = 0.0 + unclaimedAppRewards = 0.0 + unclaimedValidatorRewards = 0.0 + unclaimedSvRewards = 0.0 + amuletsToIssueToDevelopmentFund = 761035.0076103501 + +testAllIssuanceForFund : Script () +testAllIssuanceForFund = script do + validateOpenMiningRoundSummary summaryExample3 + -- 100% Development Fund + -- All issuance goes to the Development Fund; all reward-related fields are zero. + let issuanceConfig_0_0p5_fundOne = issuanceConfig_0_0p5 with optDevelopmentFundPercentage = Some 1.0 + expectedParameters_AllIssuanceForFund === computeIssuingRoundParameters tickDuration amuletPrice issuanceConfig_0_0p5_fundOne summaryExample1 +-- Utility: apply 5% Development Fund allocation +withDevFund5 : IssuanceConfig -> IssuanceConfig +withDevFund5 config = config with optDevelopmentFundPercentage = Some 0.05 diff --git a/daml/splice-amulet-test/daml/Splice/Scripts/Util.daml b/daml/splice-amulet-test/daml/Splice/Scripts/Util.daml index f7b47d47d6..4bd9daeb71 100644 --- a/daml/splice-amulet-test/daml/Splice/Scripts/Util.daml +++ b/daml/splice-amulet-test/daml/Splice/Scripts/Util.daml @@ -39,7 +39,7 @@ import Splice.Testing.Registries.AmuletRegistry.Parameters import Splice.Util -- Bootstrapping Amulet -------------------- +------------------------ -- | A type to hold the off-ledger information required to interact with the Amulet app. @@ -85,6 +85,19 @@ genericSetupApp dsoPrefix = do return app +-- Replacing AmuletConfig +-------------------------- + + +replaceDevelopmentFundConfig : AmuletApp -> AmuletConfig Unit.USD -> Optional Party -> Optional Decimal -> Script () +replaceDevelopmentFundConfig app baseConfig optDevelopmentFundManager optDevelopmentFundPercentage = do + setAmuletConfig app baseConfig baseConfig with + optDevelopmentFundManager + issuanceCurve = baseConfig.issuanceCurve with + initialValue = baseConfig.issuanceCurve.initialValue with + optDevelopmentFundPercentage + + -- AmuletApp users -------------- @@ -646,6 +659,25 @@ getAmuletConfig app = do now <- getTime pure $ getValueAsOf now amuletRules.configSchedule +setAmuletConfig : AmuletApp -> AmuletConfig Unit.USD -> AmuletConfig Unit.USD -> Script () +setAmuletConfig app baseConfig newConfig = do + Some (amuletRulesCid, _) <- queryAmuletRulesByKey app.dso + void $ submit app.dso $ + exerciseCmd amuletRulesCid AmuletRules_SetConfig with + newConfig + baseConfig + +allocateDevelopmentFundCoupon + : AmuletApp -> Party -> Party -> Decimal -> Time -> [ContractId UnclaimedDevelopmentFundCoupon] + -> Script AmuletRules_AllocateDevelopmentFundCouponResult +allocateDevelopmentFundCoupon app fundManager beneficiary amount expiresAt unclaimedDevelopmentFundCouponCids = do + submitExerciseAmuletRulesByKey app [fundManager] [] AmuletRules_AllocateDevelopmentFundCoupon with + unclaimedDevelopmentFundCouponCids + beneficiary + amount + expiresAt + fundManager + -- Metadata verification ------------------------ @@ -693,13 +725,16 @@ genericCheckTxMetadata extractMeta extractSummary app expectedKind sender body = mint <- case extractSummary result of None -> pure 0.0 Some summary -> do - let inputUnclaimedActivityRecordAmount = fromOptional 0.0 summary.inputUnclaimedActivityRecordAmount + let + inputUnclaimedActivityRecordAmount = fromOptional 0.0 summary.inputUnclaimedActivityRecordAmount + inputDevelopmentFundAmountAmount = fromOptional 0.0 summary.inputDevelopmentFundAmount expectUnlessZero svRewardAmountMetaKey summary.inputSvRewardAmount expectUnlessZero appRewardAmountMetaKey summary.inputAppRewardAmount expectUnlessZero validatorRewardAmountMetaKey summary.inputValidatorRewardAmount expectUnlessZero unclaimedActivityRecordAmountMetaKey inputUnclaimedActivityRecordAmount + expectUnlessZero developmentFundAmountMetaKey inputDevelopmentFundAmountAmount pure $ summary.inputAppRewardAmount + summary.inputValidatorRewardAmount + summary.inputSvRewardAmount + - inputUnclaimedActivityRecordAmount + inputUnclaimedActivityRecordAmount + inputDevelopmentFundAmountAmount let expectedBurn = totalHoldingsBefore + mint - totalHoldingsAfter -- mints are inferred, and show here as a negative burn if (expectedBurn < 0.0) diff --git a/daml/splice-amulet/daml/Splice/Amulet.daml b/daml/splice-amulet/daml/Splice/Amulet.daml index d3366d4190..07f6c0c072 100644 --- a/daml/splice-amulet/daml/Splice/Amulet.daml +++ b/daml/splice-amulet/daml/Splice/Amulet.daml @@ -81,9 +81,15 @@ data SvRewardCoupon_ArchiveAsBeneficiaryResult = SvRewardCoupon_ArchiveAsBenefic data UnclaimedActivityRecord_ArchiveAsBeneficiaryResult = UnclaimedActivityRecord_ArchiveAsBeneficiaryResult -data UnclaimedActivityRecord_DsoExpireResult = UnclaimedActivityRecord_DsoExpireResult with +data UnclaimedActivityRecord_DsoExpireResult = UnclaimedActivityRecord_DsoExpireResult with unclaimedRewardCid : ContractId UnclaimedReward +data DevelopmentFundCoupon_WithdrawResult = DevelopmentFundCoupon_WithdrawResult with + unclaimedDevelopmentFundCouponCid : ContractId UnclaimedDevelopmentFundCoupon + +data DevelopmentFundCoupon_DsoExpireResult = DevelopmentFundCoupon_DsoExpireResult with + unclaimedDevelopmentFundCouponCid : ContractId UnclaimedDevelopmentFundCoupon + -- | A amulet, which can be locked and whose amount expires over time. -- -- The expiry serves to charge an inactivity fee, and thereby ensures that the @@ -383,6 +389,55 @@ template SvRewardCoupon with do return SvRewardCoupon_ArchiveAsBeneficiaryResult +-- | A coupon recording an emission for the Development Fund from CIP-0082 that +-- was not yet assigned to a specific beneficiary. +template UnclaimedDevelopmentFundCoupon + with + dso : Party + amount : Decimal -- ^ The total amount of `Amulet` to mint on collection. + where + signatory dso + ensure amount > 0.0 + +-- | A coupon recording an emission for the Development Fund under CIP-0082, +-- which can be collected by the designated beneficiary. +template DevelopmentFundCoupon + with + dso : Party + beneficiary : Party -- ^ The owner of the `Amulet` to be minted. + fundManager : Party + -- ^ The party that executed the assignment of the coupon to the beneficiary + -- so they can mint from the development fund. + amount : Decimal -- ^ The total amount of `Amulet` to mint on collection. + expiresAt : Time -- ^ Until when the minting can be completed. + where + ensure amount > 0.0 + + signatory dso + + -- The beneficiary is an observer of the coupon, as they need to be able to claim it. + observer beneficiary + + -- The fundManager is an observer so they can see both the transaction creating and the one archiving the coupon. + observer fundManager + + choice DevelopmentFundCoupon_Withdraw : DevelopmentFundCoupon_WithdrawResult + with + reason : Text -- ^ Reason for withdrawing the coupon. + controller fundManager + do + assertWithinDeadline "DevelopmentFundCoupon.expiresAt" expiresAt + unclaimedDevelopmentFundCouponCid <- create UnclaimedDevelopmentFundCoupon with dso; amount + return DevelopmentFundCoupon_WithdrawResult with unclaimedDevelopmentFundCouponCid + + choice DevelopmentFundCoupon_DsoExpire : DevelopmentFundCoupon_DsoExpireResult + controller dso + do + assertDeadlineExceeded "DevelopmentFundCoupon.expiresAt" expiresAt + unclaimedDevelopmentFundCouponCid <- create UnclaimedDevelopmentFundCoupon with dso; amount + pure DevelopmentFundCoupon_DsoExpireResult with unclaimedDevelopmentFundCouponCid + + -- | Rewards that have not been claimed and are thus at the disposal of the foundation. template UnclaimedReward with dso : Party @@ -392,29 +447,29 @@ template UnclaimedReward with signatory dso --- | A record of activity that can be minted by the beneficiary. --- Note that these do not come out of the per-round issuance but are instead created by burning --- UnclaimedRewardCoupon as defined through a vote by the SVs. That's also why expiry is a separate +-- | A record of activity that can be minted by the beneficiary. +-- Note that these do not come out of the per-round issuance but are instead created by burning +-- UnclaimedRewardCoupon as defined through a vote by the SVs. That's also why expiry is a separate -- time-based expiry instead of being tied to a round like the other activity records. template UnclaimedActivityRecord with dso : Party beneficiary : Party -- ^ The owner of the `Amulet` to be minted. 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. - where + reason : Text -- ^ A reason to mint the `Amulet`. + expiresAt : Time -- ^ Selected timestamp defining the lifetime of the contract. + where signatory dso observer beneficiary ensure amount > 0.0 choice UnclaimedActivityRecord_DsoExpire : UnclaimedActivityRecord_DsoExpireResult controller dso - do + do assertDeadlineExceeded "UnclaimedActivityRecord.expiresAt" expiresAt unclaimedRewardCid <- create UnclaimedReward with dso; amount pure UnclaimedActivityRecord_DsoExpireResult with unclaimedRewardCid - + requireAmuletExpiredForAllOpenRounds : ContractId OpenMiningRound -> Amulet -> Update () requireAmuletExpiredForAllOpenRounds roundCid amulet = do @@ -456,4 +511,13 @@ instance HasCheckedFetch FeaturedAppActivityMarker ForDso where contractGroupId FeaturedAppActivityMarker {..} = ForDso with dso instance HasCheckedFetch UnclaimedActivityRecord ForOwner where - contractGroupId UnclaimedActivityRecord{..} = ForOwner with dso; owner = beneficiary \ No newline at end of file + contractGroupId UnclaimedActivityRecord{..} = ForOwner with dso; owner = beneficiary + +instance HasCheckedFetch UnclaimedDevelopmentFundCoupon ForDso where + contractGroupId UnclaimedDevelopmentFundCoupon{..} = ForDso with dso + +instance HasCheckedFetch DevelopmentFundCoupon ForOwner where + contractGroupId DevelopmentFundCoupon{..} = ForOwner with dso; owner = beneficiary + +instance HasCheckedFetch DevelopmentFundCoupon ForDso where + contractGroupId DevelopmentFundCoupon{..} = ForDso with dso diff --git a/daml/splice-amulet/daml/Splice/Amulet/TokenApiUtils.daml b/daml/splice-amulet/daml/Splice/Amulet/TokenApiUtils.daml index 1b8f738c20..bd339412e3 100644 --- a/daml/splice-amulet/daml/Splice/Amulet/TokenApiUtils.daml +++ b/daml/splice-amulet/daml/Splice/Amulet/TokenApiUtils.daml @@ -43,7 +43,7 @@ nonZeroMetadata k n m -- | Add an metadata entry for an optional value if it is non-zero number. optionalNonZeroMetadata : (Eq a, Additive a, Show a) => Text -> Optional a -> TextMap Text -> TextMap Text -optionalNonZeroMetadata k optN m = +optionalNonZeroMetadata k optN m = case optN of None -> m Some n -> nonZeroMetadata k n m @@ -75,6 +75,9 @@ appRewardBeneficiariesMetaKey = amuletPrefix <> "app-reward-beneficiaries" appRewardBeneficiaryWeightsMetaKey : Text appRewardBeneficiaryWeightsMetaKey = amuletPrefix <> "app-reward-beneficiary-weights" +developmentFundAmountMetaKey : Text +developmentFundAmountMetaKey = amuletPrefix <> "development-fund" + -- Splice API Metadata keys --------------------------- diff --git a/daml/splice-amulet/daml/Splice/AmuletConfig.daml b/daml/splice-amulet/daml/Splice/AmuletConfig.daml index abb4dc9af0..c5c9f1bf4d 100644 --- a/daml/splice-amulet/daml/Splice/AmuletConfig.daml +++ b/daml/splice-amulet/daml/Splice/AmuletConfig.daml @@ -45,6 +45,8 @@ data AmuletConfig unit = AmuletConfig with -- that should be used for command submissions. transferPreapprovalFee : Optional Decimal -- ^ Fee for keeping a transfer pre-approval around. featuredAppActivityMarkerAmount : Optional Decimal -- ^ $-amount used for the conversion from FeaturedAppActivityMarker -> AppRewardCoupon + optDevelopmentFundManager : Optional Party + -- ^ Party authorized to manage and allocate minting rights from the Development Fund. deriving (Eq, Show) -- $1/year specified as a daily rate @@ -120,6 +122,7 @@ instance Patchable (AmuletConfig USD) where packageConfig = patch new.packageConfig base.packageConfig current.packageConfig transferPreapprovalFee = patch new.transferPreapprovalFee base.transferPreapprovalFee current.transferPreapprovalFee featuredAppActivityMarkerAmount = patch new.featuredAppActivityMarkerAmount base.featuredAppActivityMarkerAmount current.featuredAppActivityMarkerAmount + optDevelopmentFundManager = patch new.optDevelopmentFundManager base.optDevelopmentFundManager current.optDevelopmentFundManager instance Patchable (TransferConfig USD) where patch new base current = TransferConfig with diff --git a/daml/splice-amulet/daml/Splice/AmuletRules.daml b/daml/splice-amulet/daml/Splice/AmuletRules.daml index 392e25fa9f..edaef635c1 100644 --- a/daml/splice-amulet/daml/Splice/AmuletRules.daml +++ b/daml/splice-amulet/daml/Splice/AmuletRules.daml @@ -1,6 +1,7 @@ -- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -- SPDX-License-Identifier: Apache-2.0 +{-# LANGUAGE MultiWayIf #-} module Splice.AmuletRules where import Prelude hiding (forA) @@ -64,6 +65,7 @@ data AmuletRules_AdvanceOpenMiningRoundsResult = AmuletRules_AdvanceOpenMiningRo data AmuletRules_MiningRound_StartIssuingResult = AmuletRules_MiningRound_StartIssuingResult with issuingRoundCid : ContractId IssuingMiningRound + unclaimedDevelopmentFundCouponCid : Optional (ContractId UnclaimedDevelopmentFundCoupon) data AmuletRules_MiningRound_CloseResult = AmuletRules_MiningRound_CloseResult with closedRoundCid : ContractId ClosedMiningRound @@ -76,6 +78,13 @@ data AmuletRules_ClaimExpiredRewardsResult = AmuletRules_ClaimExpiredRewardsResu data AmuletRules_MergeUnclaimedRewardsResult = AmuletRules_MergeUnclaimedRewardsResult with unclaimedRewardCid : ContractId UnclaimedReward +data AmuletRules_MergeUnclaimedDevelopmentFundCouponsResult = AmuletRules_MergeUnclaimedDevelopmentFundCouponsResult with + unclaimedDevelopmentFundCouponCid : ContractId UnclaimedDevelopmentFundCoupon + +data AmuletRules_AllocateDevelopmentFundCouponResult = AmuletRules_AllocateDevelopmentFundCouponResult with + developmentFundCouponCid : ContractId DevelopmentFundCoupon + optUnclaimedDevelopmentFundCouponCid : Optional (ContractId UnclaimedDevelopmentFundCoupon) + data AmuletRules_SetConfigResult = AmuletRules_SetConfigResult with newAmuletRules : ContractId AmuletRules @@ -450,6 +459,15 @@ template AmuletRules dso amount = totalUnclaimedRewards + -- record unclaimed development fund coupon contract + unclaimedDevelopmentFundCouponCid <- + if params.amuletsToIssueToDevelopmentFund > 0.0 + then + Some <$> create UnclaimedDevelopmentFundCoupon with + dso + amount = params.amuletsToIssueToDevelopmentFund + else pure None + -- create issuing round now <- getTime let tickDuration = miningRound.tickDuration @@ -579,6 +597,66 @@ template AmuletRules return AmuletRules_MergeUnclaimedRewardsResult with .. + -- Batch merge of unclaimed development fund coupons + nonconsuming choice AmuletRules_MergeUnclaimedDevelopmentFundCoupons : AmuletRules_MergeUnclaimedDevelopmentFundCouponsResult + with + unclaimedDevelopmentFundCouponCids : [ContractId UnclaimedDevelopmentFundCoupon] + controller dso + do + require "More than one unclaimed development fund coupon contract" (length unclaimedDevelopmentFundCouponCids > 1) + + -- archive all given coupons + archivedAmounts <- forA unclaimedDevelopmentFundCouponCids $ \unclaimedDevelopmentFundCouponCid -> do + unclaimedDevelopmentFundCoupon <- fetchAndArchive (ForDso with dso) unclaimedDevelopmentFundCouponCid + pure unclaimedDevelopmentFundCoupon.amount + + -- create a new unclaimed development fund coupon over the total + unclaimedDevelopmentFundCouponCid <- create UnclaimedDevelopmentFundCoupon with + dso + amount = sum archivedAmounts + + return AmuletRules_MergeUnclaimedDevelopmentFundCouponsResult with .. + + -- Allows the Development Fund manager to allocate a specified amount from a + -- collection of UnclaimedDevelopmentFundCoupons to a beneficiary, generating a + -- DevelopmentFundCoupon and producing a leftover unclaimed coupon if applicable. + nonconsuming choice AmuletRules_AllocateDevelopmentFundCoupon : AmuletRules_AllocateDevelopmentFundCouponResult + with + unclaimedDevelopmentFundCouponCids : [ContractId UnclaimedDevelopmentFundCoupon] + beneficiary : Party + amount : Decimal + expiresAt : Time + fundManager : Party + controller fundManager + do + -- Verify expiry + assertWithinDeadline "DevelopmentFundCoupon.expiresAt" expiresAt + + -- Verify fundManager + configUsd <- getValueAsOfLedgerTime configSchedule + case configUsd.optDevelopmentFundManager of + None -> abort "DevelopmentFundCoupon cannot be allocated without a Development Fund manager configured" + Some developmentFundManager -> + require ("controller is the configured Development Fund manager: " <> show developmentFundManager) $ + developmentFundManager == fundManager + + -- Verify the amount and create a leftover UnclaimedDevelopmentFundCoupon if needed + totalUnclaimedDevelopmentFundCoupons <- sum <$> forA unclaimedDevelopmentFundCouponCids \cid -> do + coupon <- fetchAndArchive (ForDso with dso) cid + pure coupon.amount + let leftover = totalUnclaimedDevelopmentFundCoupons - amount + optUnclaimedDevelopmentFundCouponCid <- + if | leftover < 0.0 -> abort $ "insufficient amount to cover the requested allocation: " <> show (negate leftover) + | leftover == 0.0 -> pure None + | otherwise -> Some <$> create UnclaimedDevelopmentFundCoupon with dso; amount = leftover + + -- record development fund coupon contract + developmentFundCouponCid <- create DevelopmentFundCoupon with dso; beneficiary; fundManager; amount; expiresAt + + return AmuletRules_AllocateDevelopmentFundCouponResult with + developmentFundCouponCid + optUnclaimedDevelopmentFundCouponCid + -- This allows fetchByKey-style fetches if you have readAs -- but not actAs claims. nonconsuming choice AmuletRules_Fetch: AmuletRules @@ -717,7 +795,7 @@ data RewardsIssuanceConfig = RewardsIssuanceConfig with -- | Execute a transfer. executeTransfer : RewardsIssuanceConfig -> TransferContext -> Party -> Transfer -> Update TransferResult -executeTransfer config context dso t = do +executeTransfer config context dso t = do -- compute summaries csum <- summarizeAndValidateContext context dso t isum <- summarizeAndConsumeInputs csum dso t.sender t.inputs @@ -749,6 +827,7 @@ executeTransfer config context dso t = do nonZeroMetadata appRewardAmountMetaKey summary.inputAppRewardAmount $ nonZeroMetadata validatorRewardAmountMetaKey summary.inputValidatorRewardAmount $ optionalNonZeroMetadata unclaimedActivityRecordAmountMetaKey summary.inputUnclaimedActivityRecordAmount $ + optionalNonZeroMetadata developmentFundAmountMetaKey summary.inputDevelopmentFundAmount $ TextMap.empty return TransferResult with @@ -778,6 +857,9 @@ data TransferInputsSummary = TransferInputsSummary with totalUnclaimedActivityRecordAmount : Optional Decimal -- ^ Note: Made optional as the addition of this field is checked by the upgrade checker -- on package upload because `TransferInputsSummary` is serializable. + totalDevelopmentFundAmount : Optional Decimal + -- ^ Note: Same rationale as above — made optional to ensure compatibility with + -- the upgrade checker on package upload because `TransferInputsSummary` is serializable. deriving (Eq, Show) type TransferOutputsSummary = [PreprocessedTransferOutput] @@ -836,6 +918,7 @@ summarizeAndConsumeInputs csum dso sender inps = do foldlA (summarizeAndConsumeInput csum.openRound.round) initialSummary inps where forOwner = ForOwner with dso; owner = sender + forDso = ForDso with dso initialSummary = TransferInputsSummary with totalAmuletAmount = 0.0 @@ -847,6 +930,7 @@ summarizeAndConsumeInputs csum dso sender inps = do amountArchivedAsOfRoundZero = 0.0 changeToHoldingFeesRate = 0.0 totalUnclaimedActivityRecordAmount = Some 0.0 + totalDevelopmentFundAmount = Some 0.0 summarizeAndConsumeInput _round s (InputAmulet amuletCid) = do amulet <- fetchAndArchive forOwner amuletCid @@ -863,6 +947,7 @@ summarizeAndConsumeInputs csum dso sender inps = do totalHoldingFees = s.totalHoldingFees amountArchivedAsOfRoundZero = s.amountArchivedAsOfRoundZero + getValueAsOfRound0 amulet.amount changeToHoldingFeesRate = s.changeToHoldingFeesRate - amulet.amount.ratePerRound.rate + totalDevelopmentFundAmount = s.totalDevelopmentFundAmount summarizeAndConsumeInput _round s (InputAppRewardCoupon couponCid) = do coupon <- fetchAndArchive forOwner couponCid @@ -881,10 +966,11 @@ summarizeAndConsumeInputs csum dso sender inps = do totalHoldingFees = s.totalHoldingFees amountArchivedAsOfRoundZero = s.amountArchivedAsOfRoundZero changeToHoldingFeesRate = s.changeToHoldingFeesRate + totalDevelopmentFundAmount = s.totalDevelopmentFundAmount summarizeAndConsumeInput _round s (InputValidatorRewardCoupon couponCid) = do -- we must and do use the validator right to archive the coupon of the user - coupon <- fetchButArchiveLater (ForDso with dso) couponCid + coupon <- fetchButArchiveLater forDso couponCid do rightCid <- getValidatorRight csum coupon.user exercise couponCid ValidatorRewardCoupon_ArchiveAsValidator with @@ -902,6 +988,7 @@ summarizeAndConsumeInputs csum dso sender inps = do totalHoldingFees = s.totalHoldingFees amountArchivedAsOfRoundZero = s.amountArchivedAsOfRoundZero changeToHoldingFeesRate = s.changeToHoldingFeesRate + totalDevelopmentFundAmount = s.totalDevelopmentFundAmount summarizeAndConsumeInput _round s (InputSvRewardCoupon couponCid) = do -- we use the SvRewardCoupon_ArchiveAsBeneficiary choice to signal the archival of the coupon @@ -920,6 +1007,7 @@ summarizeAndConsumeInputs csum dso sender inps = do totalHoldingFees = s.totalHoldingFees amountArchivedAsOfRoundZero = s.amountArchivedAsOfRoundZero changeToHoldingFeesRate = s.changeToHoldingFeesRate + totalDevelopmentFundAmount = s.totalDevelopmentFundAmount summarizeAndConsumeInput _round s (InputValidatorLivenessActivityRecord recordCid) = do record <- fetchAndArchive forOwner recordCid @@ -936,6 +1024,7 @@ summarizeAndConsumeInputs csum dso sender inps = do totalHoldingFees = s.totalHoldingFees amountArchivedAsOfRoundZero = s.amountArchivedAsOfRoundZero changeToHoldingFeesRate = s.changeToHoldingFeesRate + totalDevelopmentFundAmount = s.totalDevelopmentFundAmount summarizeAndConsumeInput _round s (ExtTransferInput _dummyUnitField optInputValidatorFaucetCoupon) = do optional (pure s) (summarizeAndConsumeValidatorFaucetInput s) optInputValidatorFaucetCoupon @@ -954,6 +1043,22 @@ summarizeAndConsumeInputs csum dso sender inps = do totalHoldingFees = s.totalHoldingFees amountArchivedAsOfRoundZero = s.amountArchivedAsOfRoundZero changeToHoldingFeesRate = s.changeToHoldingFeesRate + totalDevelopmentFundAmount = s.totalDevelopmentFundAmount + + summarizeAndConsumeInput _round s (InputDevelopmentFundCoupon couponCid) = do + coupon <- fetchAndArchive forOwner couponCid + assertWithinDeadline "DevelopmentFundCoupon.expiresAt" coupon.expiresAt + return TransferInputsSummary with + totalAmuletAmount = s.totalAmuletAmount + totalAppRewardAmount = s.totalAppRewardAmount + totalValidatorRewardAmount = s.totalValidatorRewardAmount + totalUnclaimedActivityRecordAmount = s.totalUnclaimedActivityRecordAmount + totalValidatorFaucetAmount = s.totalValidatorFaucetAmount + totalSvRewardAmount = s.totalSvRewardAmount + totalHoldingFees = s.totalHoldingFees + amountArchivedAsOfRoundZero = s.amountArchivedAsOfRoundZero + changeToHoldingFeesRate = s.changeToHoldingFeesRate + totalDevelopmentFundAmount = (+ coupon.amount) <$> s.totalDevelopmentFundAmount summarizeAndConsumeValidatorFaucetInput s couponCid = do coupon <- fetchAndArchive forOwner couponCid @@ -970,6 +1075,7 @@ summarizeAndConsumeInputs csum dso sender inps = do totalHoldingFees = s.totalHoldingFees amountArchivedAsOfRoundZero = s.amountArchivedAsOfRoundZero changeToHoldingFeesRate = s.changeToHoldingFeesRate + totalDevelopmentFundAmount = s.totalDevelopmentFundAmount -- | Deduplicate lock-holders to store them and charge for them at most once dedupOutputLockHolders : TransferOutput -> TransferOutput @@ -1029,6 +1135,7 @@ summarizeTransfer sender openRound transferConfigAmulet inp preprocessedOutputs + fromOptional 0.0 inp.totalUnclaimedActivityRecordAmount + inp.totalValidatorFaucetAmount + inp.totalSvRewardAmount + + fromOptional 0.0 inp.totalDevelopmentFundAmount - totalOutputAmount - sum outputFees senderChangeFee = min transferConfigAmulet.createFee.fee leftOverAmount senderChangeAmount = leftOverAmount - senderChangeFee @@ -1054,6 +1161,7 @@ summarizeTransfer sender openRound transferConfigAmulet inp preprocessedOutputs inputUnclaimedActivityRecordAmount = inp.totalUnclaimedActivityRecordAmount inputValidatorFaucetAmount = Some inp.totalValidatorFaucetAmount inputSvRewardAmount = inp.totalSvRewardAmount + inputDevelopmentFundAmount = inp.totalDevelopmentFundAmount holdingFees = inp.totalHoldingFees outputFees senderChangeFee @@ -1245,6 +1353,7 @@ data TransferInput -- ^ Added in CIP-3. Optional validator faucet coupon input into this transfer. | InputValidatorLivenessActivityRecord (ContractId ValidatorLivenessActivityRecord) | InputUnclaimedActivityRecord (ContractId UnclaimedActivityRecord) + | InputDevelopmentFundCoupon (ContractId DevelopmentFundCoupon) deriving (Eq, Ord, Show) -- | Smart constructor for inputing validator faucet coupons into a transfer. @@ -1348,6 +1457,9 @@ data TransferSummary = TransferSummary with inputUnclaimedActivityRecordAmount : Optional Decimal -- ^ Total amount of unclaimed activity record issuance input into this transfer. -- Note: Made optional as the addition of this field is checked by the upgrade checker. + inputDevelopmentFundAmount : Optional Decimal + -- ^ Total amount of development fund coupon issuance input into this transfer. + -- Note: Made optional as the addition of this field is checked by the upgrade checker. deriving (Show, Eq) data BalanceChange = BalanceChange with diff --git a/daml/splice-amulet/daml/Splice/Issuance.daml b/daml/splice-amulet/daml/Splice/Issuance.daml index 91da289785..62fba77c48 100644 --- a/daml/splice-amulet/daml/Splice/Issuance.daml +++ b/daml/splice-amulet/daml/Splice/Issuance.daml @@ -25,6 +25,8 @@ data IssuanceConfig = IssuanceConfig with optValidatorFaucetCap : Optional Decimal -- ^ Maximal amount in $ for the per-validator issuance of validator faucet coupons; -- Introduced as part of CIP-3. Defaults to 2.85 USD. + optDevelopmentFundPercentage : Optional Decimal + -- ^ Percentage of each mint emission allocated to the Development Fund under CIP-0082. deriving (Eq, Show) -- | Getter with the right default value for the validator faucet cap. @@ -45,6 +47,7 @@ validIssuanceConfig this@IssuanceConfig{..} = && featuredAppRewardCap >= 0.0 && unfeaturedAppRewardCap >= 0.0 && getValidatorFaucetCap this >= 0.0 + && optional True (\pct -> pct >= 0.0 && pct <= 1.0) optDevelopmentFundPercentage -- computation of issuance per round @@ -73,6 +76,7 @@ data IssuingRoundParameters = IssuingRoundParameters with unclaimedValidatorRewards : Decimal unclaimedSvRewards : Decimal -- ^ Can be non-zero due to rounding, or no SV having had the chance to claim their coupons. issuancePerValidatorFaucetCoupon : Decimal + amuletsToIssueToDevelopmentFund : Decimal deriving (Eq, Show) validateOpenMiningRoundSummary : CanAssert m => OpenMiningRoundSummary -> m () @@ -94,9 +98,14 @@ computeIssuingRoundParameters tickDuration amuletPrice config summary = unclaimedAppRewards = featuredAppIssuance.unclaimedRewards unclaimedSvRewards issuancePerValidatorFaucetCoupon = validatorFaucetIssuance.issuancePerCoupon + amuletsToIssueToDevelopmentFund where + developmentFundPercentage = fromOptional 0.0 config.optDevelopmentFundPercentage + amuletsToIssueToSvs = - amuletsToIssueInRound - validatorRewardIssuance.rewardsToIssue - unfeaturedAppIssuance.rewardsToIssue + adjustedAmuletsToIssueInRound + - validatorRewardIssuance.rewardsToIssue + - unfeaturedAppIssuance.rewardsToIssue issuancePerSvRewardCoupon = if summary.totalSvRewardWeight == 0 @@ -114,8 +123,11 @@ computeIssuingRoundParameters tickDuration amuletPrice config summary = intToDecimal (convertRelTimeToMicroseconds tickDuration) amuletsToIssueInRound = config.amuletToIssuePerYear / roundsPerYear + amuletsToIssueToDevelopmentFund = amuletsToIssueInRound * developmentFundPercentage + adjustedAmuletsToIssueInRound = amuletsToIssueInRound - amuletsToIssueToDevelopmentFund + validatorRewardIssuance = computeIssuanceTranche - (amuletsToIssueInRound * config.validatorRewardPercentage) + (adjustedAmuletsToIssueInRound * config.validatorRewardPercentage) config.validatorRewardCap summary.totalValidatorRewardCoupons @@ -126,7 +138,7 @@ computeIssuingRoundParameters tickDuration amuletPrice config summary = (intToDecimal $ getTotalValidatorFaucetCoupons summary) unfeaturedAppIssuance = computeIssuanceTranche - (amuletsToIssueInRound * config.appRewardPercentage) + (adjustedAmuletsToIssueInRound * config.appRewardPercentage) config.unfeaturedAppRewardCap (summary.totalFeaturedAppRewardCoupons + summary.totalUnfeaturedAppRewardCoupons) @@ -179,3 +191,4 @@ instance Patchable IssuanceConfig where featuredAppRewardCap = patch new.featuredAppRewardCap base.featuredAppRewardCap current.featuredAppRewardCap unfeaturedAppRewardCap = patch new.unfeaturedAppRewardCap base.unfeaturedAppRewardCap current.unfeaturedAppRewardCap optValidatorFaucetCap = patch new.optValidatorFaucetCap base.optValidatorFaucetCap current.optValidatorFaucetCap + optDevelopmentFundPercentage = patch new.optDevelopmentFundPercentage base.optDevelopmentFundPercentage current.optDevelopmentFundPercentage diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestUtils.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestUtils.daml index 17abe747c3..8187ce50ad 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestUtils.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestUtils.daml @@ -9,12 +9,14 @@ import DA.Assert import DA.Foldable (forA_) import DA.List import qualified DA.Map as Map +import DA.Optional (fromOptional) import qualified DA.Set as Set import qualified DA.Text as T import Daml.Script import DA.Time import Splice.Amulet +import Splice.AmuletConfig (AmuletConfig(..), USD) import Splice.AmuletRules import Splice.Issuance import Splice.Round @@ -38,24 +40,26 @@ bpsMultiplier : Int bpsMultiplier = 10000 initMainNet : Script (AmuletApp, Party, (Party, Party, Party, Party)) -initMainNet = initDecentralizedSynchronizer False +initMainNet = initDecentralizedSynchronizer False None initMainNetWithAmuletPrice : Decimal -> Script (AmuletApp, Party, (Party, Party, Party, Party)) -initMainNetWithAmuletPrice = initDecentralizedSynchronizerWithAmuletPrice False 0 +initMainNetWithAmuletPrice amuletPrice = initDecentralizedSynchronizerWithAmuletPrice False 0 amuletPrice None initDevNet : Script (AmuletApp, Party, (Party, Party, Party, Party)) -initDevNet = initDecentralizedSynchronizer True +initDevNet = initDecentralizedSynchronizer True None +initDevNetWithAmuletConfig : AmuletConfig USD -> Script (AmuletApp, Party, (Party, Party, Party, Party)) +initDevNetWithAmuletConfig amuletConfig = initDecentralizedSynchronizer True (Some amuletConfig) -initDecentralizedSynchronizer : Bool -> Script (AmuletApp, Party, (Party, Party, Party, Party)) -initDecentralizedSynchronizer isDevNet = initDecentralizedSynchronizerWithAmuletPrice isDevNet 0 1.0 +initDecentralizedSynchronizer : Bool -> Optional (AmuletConfig USD) -> Script (AmuletApp, Party, (Party, Party, Party, Party)) +initDecentralizedSynchronizer isDevNet optAmuletConfig = initDecentralizedSynchronizerWithAmuletPrice isDevNet 0 1.0 optAmuletConfig initDecentralizedSynchronizerWithNonZeroRound : Bool -> Int -> Script (AmuletApp, Party, (Party, Party, Party, Party)) -initDecentralizedSynchronizerWithNonZeroRound isDevNet initialRound = initDecentralizedSynchronizerWithAmuletPrice isDevNet initialRound 1.0 +initDecentralizedSynchronizerWithNonZeroRound isDevNet initialRound = initDecentralizedSynchronizerWithAmuletPrice isDevNet initialRound 1.0 None -initDecentralizedSynchronizerWithAmuletPrice : Bool -> Int -> Decimal -> Script (AmuletApp, Party, (Party, Party, Party, Party)) -initDecentralizedSynchronizerWithAmuletPrice isDevNet initialRound amuletPrice = do +initDecentralizedSynchronizerWithAmuletPrice : Bool -> Int -> Decimal -> Optional (AmuletConfig USD) -> Script (AmuletApp, Party, (Party, Party, Party, Party)) +initDecentralizedSynchronizerWithAmuletPrice isDevNet initialRound amuletPrice optAmuletConfig = do [sv1, sv2, sv3, sv4] <- forA ["sv1", "sv2", "sv3", "sv4"] allocateParty dso <- allocateParty "dso-party" @@ -82,7 +86,7 @@ initDecentralizedSynchronizerWithAmuletPrice isDevNet initialRound amuletPrice = decentralizedSynchronizer = initialDsoDecentralizedSynchronizerConfig nextScheduledSynchronizerUpgrade = None voteCooldownTime = None -- use default value of 1 minute - let amuletConfig = defaultAmuletConfig + let amuletConfig = fromOptional defaultAmuletConfig optAmuletConfig let ansRulesConfig = defaultAnsRulesConfig now <- getTime diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestDecentralizedAutomation.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestDecentralizedAutomation.daml index 5d08b8d107..19d7f69613 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestDecentralizedAutomation.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestDecentralizedAutomation.daml @@ -3,6 +3,7 @@ module Splice.Scripts.TestDecentralizedAutomation where +import DA.Action (replicateA_) import DA.Assert import DA.Foldable (forA_) import DA.List() @@ -26,6 +27,7 @@ import Splice.Scripts.DsoTestUtils import Splice.Ans import Splice.Scripts.AnsRulesParameters import Splice.Scripts.TestTransferPreapproval +import Splice.Testing.Registries.AmuletRegistry.Parameters import Splice.Wallet.Subscriptions @@ -53,6 +55,95 @@ testUnclaimedRewardsMerging = do pure () +-- Development Fund +-------------------- + +testUnclaimedDevelopmentFundCouponsMerging : Script () +testUnclaimedDevelopmentFundCouponsMerging = do + let + -- 5% Development Fund + amuletConfig = + defaultAmuletConfig with + issuanceCurve = defaultAmuletConfig.issuanceCurve with + initialValue = defaultAmuletConfig.issuanceCurve.initialValue with + optDevelopmentFundPercentage = Some 0.05 + (app, _, (sv1, _, _, _)) <- initDevNetWithAmuletConfig amuletConfig + + -- Mint 5 unclaimed development fund coupons + replicateA_ 5 $ runNextIssuanceD app 1.0 + + [(amuletRulesCid, _)] <- query @AmuletRules app.dso + unclaimedDevelopmentFundCouponCids@(cid1 :: _) <- fmap fst <$> query @UnclaimedDevelopmentFundCoupon app.dso + length unclaimedDevelopmentFundCouponCids === 5 + + -- Unhappy path - requires more than one development fund coupon contract. + dsoDelegateSubmitsMustFail app $ \cid -> exerciseCmd cid $ + DsoRules_MergeUnclaimedDevelopmentFundCoupons with + amuletRulesCid + choiceArg = AmuletRules_MergeUnclaimedDevelopmentFundCoupons with + unclaimedDevelopmentFundCouponCids = [cid1] + sv = sv1 + + -- Happy path + dsoDelegateSubmits app $ \cid -> exerciseCmd cid $ + DsoRules_MergeUnclaimedDevelopmentFundCoupons with + amuletRulesCid + choiceArg = AmuletRules_MergeUnclaimedDevelopmentFundCoupons with + unclaimedDevelopmentFundCouponCids + sv = sv1 + + unclaimedDevelopmentFundCoupons <- query @UnclaimedDevelopmentFundCoupon app.dso + length unclaimedDevelopmentFundCoupons === 1 + + pure () + +testDevelopmentFundCouponExpiry : Script () +testDevelopmentFundCouponExpiry = do + fundManager <- allocateParty "FundManager" + let + -- 5% Development Fund + amuletConfig = + defaultAmuletConfig with + optDevelopmentFundManager = Some fundManager + issuanceCurve = defaultAmuletConfig.issuanceCurve with + initialValue = defaultAmuletConfig.issuanceCurve.initialValue with + optDevelopmentFundPercentage = Some 0.05 + (app, _, (sv1, _, _, _)) <- initDevNetWithAmuletConfig amuletConfig + [(dsoRulesCid, _)] <- query @DsoRules app.dso + + -- Mint 1 unclaimed development fund coupon + runNextIssuanceD app 1.0 + [(unclaimedDevelopmentFundCouponCid, unclaimedDevelopmentFundCoupon)] <- query @UnclaimedDevelopmentFundCoupon app.dso + + -- Allocate a development fund coupon + now <- getTime + let + expiresAt = addRelTime now (hours 1) + amount = unclaimedDevelopmentFundCoupon.amount + alice <- setupUser app "alice" app.dso + developmentFundCouponCid <- (.developmentFundCouponCid) <$> + allocateDevelopmentFundCoupon app fundManager alice.primaryParty amount expiresAt [unclaimedDevelopmentFundCouponCid] + + -- Unhappy - expiresAt has not been reached + submitMultiMustFail [sv1] [app.dso] do + exerciseCmd dsoRulesCid DsoRules_ExpireDevelopmentFundCoupon with + developmentFundCouponCid + sv = sv1 + + -- Happy + setTime $ addRelTime expiresAt (minutes 1) + unclaimedDevelopmentFundCouponCid <- (.unclaimedDevelopmentFundCouponCid) <$> + submitMulti [sv1] [app.dso] do + exerciseCmd dsoRulesCid DsoRules_ExpireDevelopmentFundCoupon with + developmentFundCouponCid + sv = sv1 + Some unclaimedDevelopmentFundCoupon <- queryContractId app.dso unclaimedDevelopmentFundCouponCid + unclaimedDevelopmentFundCoupon === UnclaimedDevelopmentFundCoupon with + dso = app.dso + amount + + pure () + -- Testing confirmations ------------------------ diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestGovernance.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestGovernance.daml index c71be96ff0..da4495b672 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestGovernance.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestGovernance.daml @@ -730,6 +730,7 @@ testAmuletRulesTickDurationChange = do testAmuletRulesConfigChange : Script () testAmuletRulesConfigChange = do (app, dso, (sv1, sv2, sv3, _)) <- initMainNet + let Some fundManager = partyFromText "FundManager" [(dsoRulesCid, _)] <- query @DsoRules dso @@ -766,6 +767,7 @@ testAmuletRulesConfigChange = do amuletToIssuePerYear = 40e9 validatorRewardPercentage = 0.05 appRewardPercentage = 0.15 + optDevelopmentFundPercentage = Some 0.05 let defaultIssuanceCurve2 = Schedule with initialValue = issuanceConfig_0p5_1p5_2 @@ -778,6 +780,7 @@ testAmuletRulesConfigChange = do transferConfig = defaultTransferConfig2 issuanceCurve = defaultIssuanceCurve2 tickDuration = seconds 200 + optDevelopmentFundManager = Some fundManager -- second config that changes the other half of the parameters let newConfig2 = amuletRules.configSchedule.initialValue with diff --git a/daml/splice-dso-governance/daml/Splice/DsoRules.daml b/daml/splice-dso-governance/daml/Splice/DsoRules.daml index 71990a32ea..fefb3336f1 100644 --- a/daml/splice-dso-governance/daml/Splice/DsoRules.daml +++ b/daml/splice-dso-governance/daml/Splice/DsoRules.daml @@ -253,6 +253,12 @@ data DsoRules_ClaimExpiredRewardsResult = DsoRules_ClaimExpiredRewardsResult wit data DsoRules_MergeUnclaimedRewardsResult = DsoRules_MergeUnclaimedRewardsResult with unclaimedReward: ContractId UnclaimedReward +data DsoRules_MergeUnclaimedDevelopmentFundCouponsResult = DsoRules_MergeUnclaimedDevelopmentFundCouponsResult with + result : AmuletRules_MergeUnclaimedDevelopmentFundCouponsResult + +data DsoRules_ExpireDevelopmentFundCouponResult = DsoRules_ExpireDevelopmentFundCouponResult with + unclaimedDevelopmentFundCouponCid : ContractId UnclaimedDevelopmentFundCoupon + data DsoRules_MiningRound_CloseResult = DsoRules_MiningRound_CloseResult with closedRound : ContractId ClosedMiningRound @@ -1318,6 +1324,29 @@ template DsoRules with return DsoRules_MergeUnclaimedRewardsResult with unclaimedReward = result.unclaimedRewardCid + -- Batch merge of of development fund coupons + nonconsuming choice DsoRules_MergeUnclaimedDevelopmentFundCoupons : DsoRules_MergeUnclaimedDevelopmentFundCouponsResult + with + amuletRulesCid : ContractId AmuletRules + choiceArg : AmuletRules_MergeUnclaimedDevelopmentFundCoupons + sv : Party + controller sv + do + _ <- getAndValidateSvParty this (Some sv) + result <- exercise amuletRulesCid choiceArg + return DsoRules_MergeUnclaimedDevelopmentFundCouponsResult with result + + nonconsuming choice DsoRules_ExpireDevelopmentFundCoupon : DsoRules_ExpireDevelopmentFundCouponResult + with + developmentFundCouponCid : ContractId DevelopmentFundCoupon + sv : Party + controller sv + do + _ <- getAndValidateSvParty this (Some sv) + DevelopmentFundCoupon_DsoExpireResult unclaimedDevelopmentFundCouponCid <- + exercise developmentFundCouponCid DevelopmentFundCoupon_DsoExpire + pure $ DsoRules_ExpireDevelopmentFundCouponResult with unclaimedDevelopmentFundCouponCid + nonconsuming choice DsoRules_MiningRound_Close : DsoRules_MiningRound_CloseResult with amuletRulesCid : ContractId AmuletRules diff --git a/daml/splice-util/daml/Splice/Util.daml b/daml/splice-util/daml/Splice/Util.daml index 80ee7987cb..aa9a17a0cd 100644 --- a/daml/splice-util/daml/Splice/Util.daml +++ b/daml/splice-util/daml/Splice/Util.daml @@ -206,6 +206,9 @@ instance Patchable RelTime where instance Patchable Time where patch = patchScalar +instance Patchable Party where + patch = patchScalar + mapDifference : Ord k => Map k a -> Map k a -> Map k k mapDifference = Map.merge (\_ _ -> None) (\k _ -> Some k) (\_ _ _ -> None) diff --git a/token-standard/splice-token-standard-test/daml/Splice/Testing/Registries/AmuletRegistry/Parameters.daml b/token-standard/splice-token-standard-test/daml/Splice/Testing/Registries/AmuletRegistry/Parameters.daml index a806f1c567..59dbee421a 100644 --- a/token-standard/splice-token-standard-test/daml/Splice/Testing/Registries/AmuletRegistry/Parameters.daml +++ b/token-standard/splice-token-standard-test/daml/Splice/Testing/Registries/AmuletRegistry/Parameters.daml @@ -88,6 +88,8 @@ defaultAmuletConfig = AmuletConfig with -- Amount of the AppRewardCoupon contract that a FeaturedAppActivityMarker is converted to. featuredAppActivityMarkerAmount = Some defaultFeaturedAppActivityMarkerAmount + optDevelopmentFundManager = None + -- | Default configuration schedule with single current amulet config defaultAmuletConfigSchedule : Schedule Time (AmuletConfig USD) defaultAmuletConfigSchedule = Schedule with @@ -131,6 +133,7 @@ issuanceConfig_10plus = IssuanceConfig with featuredAppRewardCap = 100.0 unfeaturedAppRewardCap = 0.6 optValidatorFaucetCap = None -- We use the default of 2.85 USD introduced in the upgrade for CIP-3 + optDevelopmentFundPercentage = None defaultIssuanceCurve : Schedule RelTime IssuanceConfig