Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
354 changes: 354 additions & 0 deletions daml/splice-amulet-test/daml/Splice/Scripts/TestAggregateLocks.daml
Original file line number Diff line number Diff line change
@@ -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 ()
Loading