diff --git a/daml/splice-amulet-test/daml/Splice/Scripts/TestAggregateLocks.daml b/daml/splice-amulet-test/daml/Splice/Scripts/TestAggregateLocks.daml new file mode 100644 index 0000000000..bfb26638d6 --- /dev/null +++ b/daml/splice-amulet-test/daml/Splice/Scripts/TestAggregateLocks.daml @@ -0,0 +1,354 @@ +{-# LANGUAGE ApplicativeDo #-} +module Splice.Scripts.TestAggregateLocks where + +import DA.Time +import Daml.Script +import Splice.Amulet +import Splice.AggregateLock +import Splice.AmuletRules +import Splice.Expiry +import Splice.VestedAmulet +import Splice.Scripts.Util + +import DA.Assert +import DA.Optional +import DA.Functor +import DA.Foldable (mapA_, sequence_) +import qualified DA.Map as M + + +testAggregateLocks : Script () +testAggregateLocks = do + DefaultAppWithUsers{..} <- setupDefaultAppWithUsers + amuletCid <- tap app alice 1000.0 + amuletCid2 <- tap app bob 1000.0 + now <- getTime + + aggLock <- submit (actAs [alice.primaryParty, app.dso]) $ createCmd AggregateLock with + dso = app.dso + key = AggregateLockKey $ alice.primaryParty + vestingSchedule = None + + aggTotal <- submit app.dso $ createCmd AggregateTotal with + dso = app.dso + key = AggregateLockKey $ alice.primaryParty + totalAsOf = 0.0 + asOfTime = now + asOfRound = 0 + lowerBound = True + upperBound = True + + let getTotal = do + [(totalContractId, totalContract)] <- query @AggregateTotal alice.primaryParty + (,) totalContractId . fromSome <$> queryDisclosure app.dso totalContractId + + offer <- submit (actAs alice.primaryParty) $ createCmd OpenDelegationOffer with + owner = alice.primaryParty + lock = aggLock + unlockControllerSets = [[alice.primaryParty]] + substituteControllerSets = [[alice.primaryParty]] + + disclosedOfferAlice <- fromSome <$> queryDisclosure alice.primaryParty offer + disclosedLock <- fromSome <$> queryDisclosure alice.primaryParty aggLock + -- disclosedOfferBob <- fromSome <$> queryDisclosure bob.primaryParty offer + + (AggregateLock_LockHoldingResult lockedAmulet) <- submit alice.primaryParty $ exerciseCmd offer OpenDelegationOffer_LockHolding with + holder = alice.primaryParty + holding = amuletCid + + (AggregateLock_LockHoldingResult lockedAmulet2) <- submit (actAs [bob.primaryParty] <> discloseMany [disclosedOfferAlice, disclosedLock]) $ exerciseCmd offer OpenDelegationOffer_LockHolding with + holder = bob.primaryParty + holding = amuletCid2 + + (total, disclosedTotal) <- getTotal + + debug ("total", total) + + unlockedAmulet <- submit (actAs [bob.primaryParty, alice.primaryParty] <> discloseMany [disclosedLock, disclosedTotal]) $ exerciseCmd lockedAmulet2 AggregateLockedAmulet_Unlock with + amount = 500.0 + authorizers = [ bob.primaryParty, alice.primaryParty ] + lock = aggLock + total + + debug ("Unlocked", unlockedAmulet) + + dsoAggregateAutomation app.dso 0 + + onChainTotals <- query @AggregateTotal app.dso + debug ("On Chain Totals", onChainTotals) + + (total, disclosedTotal) <- getTotal + + let lockedAmulet3 = unlockedAmulet.locked + unlockedAmulet2 <- submit (actAs [bob.primaryParty, alice.primaryParty] <> discloseMany [disclosedLock, disclosedTotal]) $ exerciseCmd lockedAmulet3 AggregateLockedAmulet_Unlock with + amount = 250.0 + authorizers = [ bob.primaryParty, alice.primaryParty ] + lock = aggLock + total + + [(_, onChainTotal)] <- query @AggregateTotal app.dso + + assertEq False onChainTotal.lowerBound + assertEq 1500.0 onChainTotal.totalAsOf + + dsoAggregateAutomation app.dso 1 + + [(_, onChainTotal)] <- query @AggregateTotal app.dso + assertEq 1250.0 onChainTotal.totalAsOf + assertEq True onChainTotal.lowerBound + + pure () + + +dsoAggregateAutomation : Party -> Int -> Script () +dsoAggregateAutomation dso round = do + lockedHoldings <- query @AggregateLockedAmulet dso + let totals = M.fromListWithL (+) $ lockedHoldings <&> \(_, holding) -> + (holding.lockedTo, holding.amulet.amount.initialAmount) + onChainTotals <- query @AggregateTotal dso + let onChainTotalsMap : M.Map AggregateLockKey (Optional (ContractId AggregateTotal)) + onChainTotalsMap = M.fromListWithL (\_ _ -> None) $ onChainTotals <&> \(contractId, total) -> + (total.key, Some contractId) + + now <- getTime + + let updateOneLock : AggregateLockKey -> Decimal -> Optional (ContractId AggregateTotal) -> Optional (Script ()) + updateOneLock key total (Some oldTotalId) = Some $ + submit (actAs dso) $ do + archiveCmd oldTotalId + createCmd AggregateTotal with + totalAsOf = total + asOfTime = now + asOfRound = round + lowerBound = True + upperBound = True + dso + key + pure () + updateOneLock _ _ None = Some $ assertFail "Only one AggregateTotal per key is permitted" + let onChainUpdates = M.merge (\_ _ -> Some $ assertFail "Missing AggregateTotal") (\k -> updateOneLock k (0.0 : Decimal)) updateOneLock totals onChainTotalsMap + + sequence_ onChainUpdates + + pure () + + +-- Test general VestedAmulet unlcok and edge cases +testVestingAmuletUnlock : Script () +testVestingAmuletUnlock = do + DefaultAppWithUsers{..} <- setupDefaultAppWithUsers + now <- getTime + + amuletCid <- tap app alice 3652.5 + Some(amulet) <- queryContractId @Amulet alice.primaryParty amuletCid + (_, openRound) <- getLatestOpenRound app + + let initialVestedAmuletValues : VestedAmulet = VestedAmulet { aggregateLockedAmulet = amulet + , startDate = now + , endDate = (addRelTime now $ (days 365) + (hours 6)) + , withdrawnAmount = 0.0 + , unlockControllerSets = [[alice.primaryParty], [bob.primaryParty]] + } + + vestedAmuletCid <- submit (actAs [alice.primaryParty, app.dso]) $ + createCmd VestedAmulet with + aggregateLockedAmulet = initialVestedAmuletValues.aggregateLockedAmulet + startDate = initialVestedAmuletValues.startDate + endDate = initialVestedAmuletValues.endDate + withdrawnAmount = initialVestedAmuletValues.withdrawnAmount + unlockControllerSets = initialVestedAmuletValues.unlockControllerSets + + -- Immediate withdraw attempt causes transaction to fail due to the current + -- eligible withdraw amount being zero (no time has passed since locking yet) + submitMustFail (actAs alice.primaryParty) $ + exerciseCmd vestedAmuletCid VestedAmulet_WithdrawAvailableUnlockedAmulet with + authorizers = [alice.primaryParty] + openRound = openRound.round + expectedDso = app.dso + + -- Unlocking with the wrong authorizer should fail + submitMustFail (actAs [alice.primaryParty, charlie.primaryParty]) $ + exerciseCmd vestedAmuletCid VestedAmulet_WithdrawAvailableUnlockedAmulet with + authorizers = [charlie.primaryParty] + openRound = openRound.round + expectedDso = app.dso + + let totalTimePassed = days 10 + passTime totalTimePassed + + vestedWithdrawResult <- submit (actAs alice.primaryParty) $ + exerciseCmd vestedAmuletCid VestedAmulet_WithdrawAvailableUnlockedAmulet with + authorizers = [alice.primaryParty] + openRound = openRound.round + expectedDso = app.dso + + let vestedAmuletCid' = fromSome $ mVestedAmuletCid vestedWithdrawResult + unlockedAmulet <- fromSome <$> queryContractId alice.primaryParty (unlockedAmuletCid vestedWithdrawResult) + vestedAmulet <- fromSome <$> queryContractId alice.primaryParty vestedAmuletCid' + + updatedNow <- getTime + let availableAmount = calculateAvailableWithdrawAmount updatedNow initialVestedAmuletValues + + assertEq unlockedAmulet.amount.initialAmount availableAmount + assertEq vestedAmulet.aggregateLockedAmulet.amount.initialAmount $ + initialVestedAmuletValues.aggregateLockedAmulet.amount.initialAmount - availableAmount + + -- Try withdraw again, right after previous withdraw (should fail) + submitMustFail (actAs alice.primaryParty) $ + exerciseCmd vestedAmuletCid' VestedAmulet_WithdrawAvailableUnlockedAmulet with + authorizers = [alice.primaryParty] + openRound = openRound.round + expectedDso = app.dso + + -- Let's try to withdraw right before the full amount is available + passTime ((days 364 + hours 5 + minutes 59 + seconds 59 + milliseconds 59 + microseconds 59) - totalTimePassed) + + vestedWithdrawResult' <- submit (actAs [alice.primaryParty, bob.primaryParty]) $ + exerciseCmd vestedAmuletCid' VestedAmulet_WithdrawAvailableUnlockedAmulet with + authorizers = [bob.primaryParty] + openRound = openRound.round + expectedDso = app.dso + + let vestedAmuletCid'' = fromSome $ mVestedAmuletCid vestedWithdrawResult' + unlockedAmulet' <- fromSome <$> queryContractId alice.primaryParty (unlockedAmuletCid vestedWithdrawResult') + vestedAmulet' <- fromSome <$> queryContractId alice.primaryParty vestedAmuletCid'' + + updatedNow <- getTime + let availableAmount' = calculateAvailableWithdrawAmount updatedNow vestedAmulet + + -- Unlocked amulet and vested amulet should both mathing right + assertEq unlockedAmulet'.amount.initialAmount availableAmount' + assertEq vestedAmulet'.aggregateLockedAmulet.amount.initialAmount $ + vestedAmulet.aggregateLockedAmulet.amount.initialAmount - availableAmount' + + -- Now try to withdraw after the endDate has passed (the full should be withdrawable) + passTime (days 10) + + vestedWithdrawResult'' <- submit (actAs alice.primaryParty) $ + exerciseCmd vestedAmuletCid'' VestedAmulet_WithdrawAvailableUnlockedAmulet with + authorizers = [alice.primaryParty] + openRound = openRound.round + expectedDso = app.dso + + let mVestedAmulet = mVestedAmuletCid vestedWithdrawResult'' + unlockedAmulet'' <- fromSome <$> queryContractId alice.primaryParty (unlockedAmuletCid vestedWithdrawResult'') + + updatedNow' <- getTime + let availableAmount'' = calculateAvailableWithdrawAmount updatedNow' vestedAmulet' + + -- Unlocked amulet should receive last bit, while there should be no more vestedAmulet + assertEq unlockedAmulet''.amount.initialAmount availableAmount'' + assertEq mVestedAmulet None + + -- Make sure all receiving Amulets match up to original Amulet values + assertEq initialVestedAmuletValues.aggregateLockedAmulet.amount.initialAmount $ + unlockedAmulet.amount.initialAmount + + unlockedAmulet'.amount.initialAmount + + unlockedAmulet''.amount.initialAmount + + pure () + +-- Testing being able to use VestedAmulet out of an AggregateLock unlock +testAggregateLockVestingFlow : Script () +testAggregateLockVestingFlow = do + DefaultAppWithUsers{..} <- setupDefaultAppWithUsers + let period = (days 365) + (hours 6) + bobInitialAmount = 1000.0 + + amuletCid <- tap app alice 1000.0 + amuletCid2 <- tap app bob bobInitialAmount + now <- getTime + (_, openRound) <- getLatestOpenRound app + + aggLock <- submit (actAs [alice.primaryParty, app.dso]) $ createCmd AggregateLock with + dso = app.dso + key = AggregateLockKey alice.primaryParty + vestingSchedule = Some $ VestingSchedule with period + + aggTotal <- submit app.dso $ createCmd AggregateTotal with + dso = app.dso + key = AggregateLockKey $ alice.primaryParty + totalAsOf = 0.0 + asOfTime = now + asOfRound = 0 + lowerBound = True + upperBound = True + + let getTotal = do + [(totalContractId, totalContract)] <- query @AggregateTotal alice.primaryParty + (,) totalContractId . fromSome <$> queryDisclosure app.dso totalContractId + + offer <- submit (actAs alice.primaryParty) $ createCmd OpenDelegationOffer with + owner = alice.primaryParty + lock = aggLock + unlockControllerSets = [[alice.primaryParty]] + substituteControllerSets = [[alice.primaryParty]] + + disclosedOfferAlice <- fromSome <$> queryDisclosure alice.primaryParty offer + disclosedLock <- fromSome <$> queryDisclosure alice.primaryParty aggLock + + submit alice.primaryParty $ exerciseCmd offer OpenDelegationOffer_LockHolding with + holder = alice.primaryParty + holding = amuletCid + (AggregateLock_LockHoldingResult lockedAmulet2) <- submit (actAs [bob.primaryParty] <> discloseMany [disclosedOfferAlice, disclosedLock]) $ exerciseCmd offer OpenDelegationOffer_LockHolding with + holder = bob.primaryParty + holding = amuletCid2 + + (total, disclosedTotal) <- getTotal + debug ("total", total) + + unlockedAmulet <- submit (actAs [bob.primaryParty, alice.primaryParty] <> discloseMany [disclosedLock, disclosedTotal]) $ exerciseCmd lockedAmulet2 AggregateLockedAmulet_Unlock with + amount = 500.0 + authorizers = [ bob.primaryParty, alice.primaryParty ] + lock = aggLock + total + debug ("Unlocked", unlockedAmulet) + + -- Once the aggregateLock has unlocked, we can grab the vesting and locked portions + Some(initialVestedAmulet) <- queryContractId bob.primaryParty unlockedAmulet.vesting + Some(lockedAmulet) <- queryContractId @AggregateLockedAmulet bob.primaryParty unlockedAmulet.locked + debug ("vesting", initialVestedAmulet) + debug ("locked", lockedAmulet) + + -- Immediate withdraw attempt causes transaction to fail due to the current + -- eligible withdraw amount being zero (no time has passed since locking yet) + submitMustFail (actAs [bob.primaryParty, alice.primaryParty]) $ + exerciseCmd unlockedAmulet.vesting VestedAmulet_WithdrawAvailableUnlockedAmulet with + authorizers = [bob.primaryParty, alice.primaryParty] + openRound = openRound.round + expectedDso = app.dso + + -- Time needs to pass to be able to withdraw fron the VestedAmulet + passTime (days 10) + + vestedWithdrawResult <- submit (actAs [bob.primaryParty, alice.primaryParty]) $ + exerciseCmd unlockedAmulet.vesting VestedAmulet_WithdrawAvailableUnlockedAmulet with + authorizers = [bob.primaryParty, alice.primaryParty] + openRound = openRound.round + expectedDso = app.dso + debug ("vestedWithdrawResult", vestedWithdrawResult) + + let vestedAmuletCid' = fromSome $ mVestedAmuletCid vestedWithdrawResult + unlockedVestedAmulet <- fromSome <$> queryContractId bob.primaryParty (unlockedAmuletCid vestedWithdrawResult) + vestedAmulet <- fromSome <$> queryContractId bob.primaryParty vestedAmuletCid' + debug ("unlockedVestedAmulet", unlockedVestedAmulet) + debug ("vestedAmulet", vestedAmulet) + + updatedNow <- getTime + let availableAmount = calculateAvailableWithdrawAmount updatedNow initialVestedAmulet + + -- Make sure unlocked/locked vestedAmulet amounts are the expected values + assertEq unlockedVestedAmulet.amount.initialAmount availableAmount + assertEq vestedAmulet.aggregateLockedAmulet.amount.initialAmount $ + initialVestedAmulet.aggregateLockedAmulet.amount.initialAmount - availableAmount + + -- We want to make sure the initial amount is equivalent to + -- all locked/unlocked aggregate/vested amounts + assertEq bobInitialAmount $ + lockedAmulet.amulet.amount.initialAmount + + unlockedVestedAmulet.amount.initialAmount + + vestedAmulet.aggregateLockedAmulet.amount.initialAmount + + dsoAggregateAutomation app.dso 1 + + pure () diff --git a/daml/splice-amulet/daml/Splice/AggregateLock.daml b/daml/splice-amulet/daml/Splice/AggregateLock.daml new file mode 100644 index 0000000000..16119265b2 --- /dev/null +++ b/daml/splice-amulet/daml/Splice/AggregateLock.daml @@ -0,0 +1,199 @@ +module Splice.AggregateLock where + +import DA.Time +import DA.Foldable (forA_) + +import Splice.Amulet +import Splice.Types +import Splice.VestedAmulet + +data AggregateLockKey = AggregateLockKey with + owner : Party + deriving (Eq, Show, Ord) + +data VestingSchedule = VestingSchedule with + period : RelTime -- check this type. + deriving (Eq, Show) + +template AggregateLock + with + dso : Party + key : AggregateLockKey + vestingSchedule : Optional VestingSchedule + where + signatory key.owner, dso + nonconsuming choice AggregateLock_LockHolding : AggregateLock_LockHoldingResult + with + holder : Party + holding : ContractId Amulet + unlockControllerSets : ControllerSets + substituteControllerSets : ControllerSets + controller key.owner, holder + do + holdingContract <- fetch holding + archive holding + AggregateLock_LockHoldingResult <$> create AggregateLockedAmulet with + amulet = holdingContract + lockedTo = key + unlockControllerSets + substituteControllerSets + + choice AggregateLock_AllowImmediateUnlocks : AggregateLock_AllowImmediateUnlocksResult + controller dso + do + AggregateLock_AllowImmediateUnlocksResult <$> create this with + vestingSchedule = None + +data AggregateLock_LockHoldingResult = AggregateLock_LockHoldingResult (ContractId AggregateLockedAmulet) +data AggregateLock_AllowImmediateUnlocksResult = AggregateLock_AllowImmediateUnlocksResult (ContractId AggregateLock) + +data AggregateLockedAmulet_UnlockResult + = AggregateLockedAmulet_UnlockResult_Vesting with + vesting : ContractId VestedAmulet + locked : ContractId AggregateLockedAmulet + | AggregateLockedAmulet_UnlockResult_Immediate with + unlocked : ContractId Amulet + locked : ContractId AggregateLockedAmulet + deriving (Show) + + +template AggregateLockedAmulet + with + amulet : Amulet + lockedTo : AggregateLockKey + unlockControllerSets : ControllerSets + substituteControllerSets : ControllerSets + where + signatory amulet.dso, amulet.owner + + choice AggregateLockedAmulet_Unlock : AggregateLockedAmulet_UnlockResult + with + amount : Decimal + authorizers : [Party] + lock : ContractId AggregateLock + total : ContractId AggregateTotal + controller authorizers + do + assert $ authorizers `elem` unlockControllerSets + + let remainder = amulet.amount.initialAmount - amount -- FIXME: do something about expiring amulet. + lockParameters <- fetch lock + now <- getTime + totalContract <- fetch total + if totalContract.lowerBound + then do + create totalContract with + lowerBound = False + archive total + else return () + case lockParameters.vestingSchedule of + Some schedule -> do + vested <- create VestedAmulet with + unlockControllerSets = unlockControllerSets + startDate = now + endDate = addRelTime now schedule.period + aggregateLockedAmulet = amulet with + amount = amulet.amount with + initialAmount = amount + withdrawnAmount = 0.0 + locked <- create this with + amulet = amulet with + amount = amulet.amount with + initialAmount = remainder + return $ AggregateLockedAmulet_UnlockResult_Vesting vested locked + None -> do + unlocked <- create this.amulet with + amount = this.amulet.amount with + initialAmount = amount + locked <- create this with + amulet = amulet with + amount = amulet.amount with + initialAmount = remainder + return $ AggregateLockedAmulet_UnlockResult_Immediate unlocked locked + +{- + choice AggregateLockedAmulet_Substitute : AggregateLockedAmulet_Substitute + with + amount : Decimal authorizers : [Party] + lock : ContractId AggregateLock + swapWith : Amulet + newUnlockControllerSets : ControllerSets + newSubstituteControllerSets : ControllerSets + controller swapWith.owner :: authorizers + do + assert $ authorizers `elem` substituteControllerSets + let remainder = amulet.amount. - amount + unlocked <- create Amulet with + + + + Nothing -> do + unlocked <- create Amulet with + amount = amulet.amount with + initialAmount = amount + locked <- create this with + amulet = amulet with + amount = amulet.amount with + initialAmount = remainder + return AggregateLockedAmulet_UnlockResult_Immediate unlocked amount +-} + +template AggregateTotal + with + dso : Party + key : AggregateLockKey + totalAsOf : Decimal + asOfTime : Time + asOfRound : Int + lowerBound : Bool + upperBound : Bool + where + signatory dso + observer key.owner + +template AggregateLockDsoAutomation + with + dso : Party + where + signatory dso + nonconsuming choice AggregateLockDsoAutomation_UpdateHolderTotal : AggregateLockDsoAutomation_UpdateHolderTotalResult + with + key : AggregateLockKey + asOfRound : Int + holdings : [ AggregateLockedAmulet ] + controller dso + do + forA_ holdings $ \a -> + assert $ a.lockedTo == key + let totalAsOf = foldl (\t h -> t + h.amulet.amount.initialAmount) 0.0 holdings -- FIXME: use correct amount calculation. + asOfTime <- getTime + create AggregateTotal with + lowerBound = True + upperBound = True + .. + pure AggregateLockDsoAutomation_UpdateHolderTotalResult + +data AggregateLockDsoAutomation_UpdateHolderTotalResult = AggregateLockDsoAutomation_UpdateHolderTotalResult + +template OpenDelegationOffer + with + owner : Party + lock : ContractId AggregateLock + unlockControllerSets : ControllerSets + substituteControllerSets : ControllerSets + where + signatory owner + + nonconsuming choice OpenDelegationOffer_LockHolding : AggregateLock_LockHoldingResult + with + holder : Party + holding : ContractId Amulet + controller holder + do + holdingAmulet <- fetch holding + assert $ holder == holdingAmulet.owner + exercise lock AggregateLock_LockHolding with + unlockControllerSets = (::) holder <$> unlockControllerSets + substituteControllerSets = (::) holder <$> substituteControllerSets + .. + diff --git a/daml/splice-amulet/daml/Splice/Types.daml b/daml/splice-amulet/daml/Splice/Types.daml index 9c976cebd3..906191bfe5 100644 --- a/daml/splice-amulet/daml/Splice/Types.daml +++ b/daml/splice-amulet/daml/Splice/Types.daml @@ -54,3 +54,5 @@ instance HasCheckedFetch TransferInstructionView ForDso where instance HasCheckedFetch AllocationView ForDso where contractGroupId AllocationView {..} = ForDso with dso = allocation.transferLeg.instrumentId.admin + +type ControllerSets = [[Party]] diff --git a/daml/splice-amulet/daml/Splice/VestedAmulet.daml b/daml/splice-amulet/daml/Splice/VestedAmulet.daml new file mode 100644 index 0000000000..32ff214b30 --- /dev/null +++ b/daml/splice-amulet/daml/Splice/VestedAmulet.daml @@ -0,0 +1,106 @@ +module Splice.VestedAmulet where + +import qualified DA.Text as T (implode) +import DA.Time +import DA.Numeric + +import Splice.Amulet +import Splice.Fees +import Splice.Types +import Splice.Util + +data VestedAmulet_WithdrawResult + = VestedAmulet_WithdrawResult + with + unlockedAmuletCid : ContractId Amulet -- ^ References new amulet created from the vested + -- amulet's unlocked withdrawn amount. + mVestedAmuletCid : Optional (ContractId VestedAmulet) -- ^ Optional reference to the vested amulet if any + -- locked aggregate amount remains. None if all of the amount is has already been withdrawn. + deriving (Show, Eq) + +calculateAvailableWithdrawAmount : Time -> VestedAmulet -> Decimal +calculateAvailableWithdrawAmount currentDateTime VestedAmulet{..} = let + totalVestedAmulet : Decimal = castAndRound $ aggregateLockedAmulet.amount.initialAmount + withdrawnAmount + in if currentDateTime >= endDate + then (castAndRound totalVestedAmulet) - withdrawnAmount + else + let totalUnlockPeriod = intToDecimal . convertRelTimeToMicroseconds $ subTime endDate startDate + unlockPeriodElapsed = intToDecimal . convertRelTimeToMicroseconds $ subTime currentDateTime startDate + totalVestedPercentageElapsed : Decimal = div unlockPeriodElapsed totalUnlockPeriod + in (castAndRound $ totalVestedAmulet * totalVestedPercentageElapsed) - withdrawnAmount + +template VestedAmulet + with + aggregateLockedAmulet : Amulet + startDate : Time + endDate : Time + withdrawnAmount : Decimal + unlockControllerSets : ControllerSets + where + signatory aggregateLockedAmulet.dso, aggregateLockedAmulet.owner + + choice VestedAmulet_WithdrawAvailableUnlockedAmulet : VestedAmulet_WithdrawResult + with + authorizers : [Party] + openRound : Round + expectedDso : Party + -- Party that the sender expects to represent the DSO party of the AmuletRules contract they are calling. + -- Must always be set to protect from malicious delegees swapping the AmuletRules contract out for + -- one under their control. + controller authorizers + do + flip require (authorizers `elem` unlockControllerSets) $ + T.implode [ + "Authorizers: '" + , show authorizers + , "', does not match any allowed unlock configurations: '" + , show unlockControllerSets + , "'." + ] + + flip require ((show aggregateLockedAmulet.dso) == (show expectedDso)) $ + T.implode [ + "DSO party expected by caller " + , show expectedDso + , " does not match actual DSO party " + , show aggregateLockedAmulet.dso + ] + + now <- getTime + let currentEligibleWithdrawableAmount : Decimal = calculateAvailableWithdrawAmount now this + + require ("Current eligible withdraw amount for vested amulet is not greater than 0.0. " + <> "currentEligibleWithdrawableAmount came back = '" + <> show currentEligibleWithdrawableAmount + <> "'." + ) + (currentEligibleWithdrawableAmount > 0.0) + + -- Amulet for Unlocked Amount of CC + unlockedAmulet <- create Amulet with + dso = expectedDso + owner = aggregateLockedAmulet.owner + amount = ExpiringAmount with + initialAmount = currentEligibleWithdrawableAmount + createdAt = openRound + ratePerRound = aggregateLockedAmulet.amount.ratePerRound + + let remainingAggregateLockedAmount = aggregateLockedAmulet.amount.initialAmount - currentEligibleWithdrawableAmount + + -- Vested Amulet in case any amount remains locked in aggregateLockedAmulet + vestedAmulet <- if remainingAggregateLockedAmount > 0.0 then do + Some <$> create VestedAmulet with + startDate + endDate + unlockControllerSets = unlockControllerSets + withdrawnAmount = withdrawnAmount + currentEligibleWithdrawableAmount + aggregateLockedAmulet = Amulet with + dso = expectedDso + owner = aggregateLockedAmulet.owner + amount = ExpiringAmount with + initialAmount = remainingAggregateLockedAmount + createdAt = openRound + ratePerRound = aggregateLockedAmulet.amount.ratePerRound + else return None + + return $ VestedAmulet_WithdrawResult unlockedAmulet vestedAmulet