refactor and add more tests for unlocking stakes

This commit is contained in:
Hongrui Fang 2022-07-08 23:54:21 +08:00
parent b76b5c6ec6
commit 5eee3127b2
3 changed files with 591 additions and 213 deletions

View file

@ -1,12 +1,19 @@
module Sample.Proposal.UnlockStake (
unlockStake,
StakeRole (..),
UnlockStakeParameters (..),
votesTemplate,
emptyEffectFor,
mkProposalInputDatum,
mkStakeInputDatum,
mkProposalValidatorTestCase,
Parameters (..),
unlockStake,
mkTestTree,
mkVoterRetractVotesWhileVotingParameters,
mkVoterCreatorRetractVotesWhileVotingParameters,
mkCreatorRemoveCreatorLocksWhenFinishedParameters,
mkVoterCreatorRemoveAllLocksWhenFinishedParameters,
mkVoterUnlockStakeAfterVotingParameters,
mkVoterCreatorRemoveVoteLocksWhenLockedParameters,
mkRetractVotesWhileNotVoting,
mkUnockIrrelevantStakeParameters,
mkRemoveCreatorLockBeforeFinishedParameters,
mkRetractVotesWithCreatorStakeParamaters,
mkAlterStakeParameters,
) where
--------------------------------------------------------------------------------
@ -21,9 +28,8 @@ import Agora.Proposal (
)
import Agora.Proposal.Scripts (proposalValidator)
import Agora.Proposal.Time (ProposalStartingTime (ProposalStartingTime))
import Agora.Stake (ProposalLock (..), Stake (..), StakeDatum (..))
import Control.Monad (join)
import Data.Coerce (coerce)
import Agora.Stake (ProposalLock (..), Stake (..), StakeDatum (..), StakeRedeemer (RetractVotes))
import Agora.Stake.Scripts (stakeValidator)
import Data.Default.Class (Default (def))
import Data.Tagged (Tagged (..), untag)
import Plutarch.Context (
@ -32,14 +38,15 @@ import Plutarch.Context (
input,
output,
script,
signedWith,
txId,
withDatum,
withRefIndex,
withTxId,
withOutRef,
withValue,
)
import PlutusLedgerApi.V1 (
DatumHash,
PubKeyHash,
ScriptContext (..),
ScriptPurpose (Spending),
TxInfo (..),
@ -48,7 +55,7 @@ import PlutusLedgerApi.V1 (
)
import PlutusLedgerApi.V1.Value qualified as Value
import PlutusTx.AssocMap qualified as AssocMap
import Sample.Proposal.Shared (proposalTxRef, stakeTxRef, testFunc)
import Sample.Proposal.Shared (stakeTxRef, testFunc)
import Sample.Shared (
minAda,
proposalPolicySymbol,
@ -59,7 +66,7 @@ import Sample.Shared (
stakeValidatorHash,
)
import Sample.Shared qualified as Shared
import Test.Specification (SpecificationTree)
import Test.Specification (SpecificationTree, group)
import Test.Util (sortValue, updateMap)
--------------------------------------------------------------------------------
@ -82,104 +89,130 @@ emptyEffectFor (ProposalVotes vs) =
map (,AssocMap.empty) (AssocMap.keys vs)
-- | The default vote option that will be used by functions in this module.
defaultVoteFor :: ResultTag
defaultVoteFor = ResultTag 0
defVoteFor :: ResultTag
defVoteFor = ResultTag 0
-- | The default number of GTs the stake will have.
defaultStakedGTs :: Tagged _ Integer
defaultStakedGTs = Tagged 100000
defStakedGTs :: Tagged _ Integer
defStakedGTs = 100000
{- | If 'Parameters.alterOutputStake' is set to true, the
'StakeDatum.stakedAmount' will be set to this.
-}
alteredStakedGTs :: Tagged _ Integer
alteredStakedGTs = 100
-- | Default owner of the stakes.
defOwner :: PubKeyHash
defOwner = signer
-- | How a stake has been used on a particular proposal.
data StakeRole
= -- | The stake was spent to vote for a paraticular option.
Voter
| -- | The stake was used to created the proposal.
| -- | The stake was used to create the proposal.
Creator
| -- | The stake was used to both create and vote for the proposal.
Both
| -- | The stake has nothing to do with the proposal.
Irrelevant
deriving stock (Bounded, Enum, Show)
-- | Parameters for creating a 'TxOut' that unlocks a stake.
data UnlockStakeParameters = UnlockStakeParameters
data Parameters = Parameters
{ proposalCount :: Integer
-- ^ The number of proposals in the 'TxOut'.
, stakeUsage :: StakeRole
, stakeRole :: StakeRole
-- ^ The role of the stake we're unlocking.
, retractVotes :: Bool
-- ^ Whether to retract votes or not.
, removeVoterLock :: Bool
-- ^ Remove the voter locks from the input stake.
, removeCreatorLock :: Bool
-- ^ Remove the creator locks from the input stake.
, proposalStatus :: ProposalStatus
-- ^ The state of all the proposals.
, alterOutputStake :: Bool
}
instance Show UnlockStakeParameters where
show p =
let role = case p.stakeUsage of
Voter -> "voter"
Creator -> "creator"
_ -> "irrelevant stake"
-- | Iterate over the proposal id of every proposal, given the number of proposals.
forEachProposalId :: Parameters -> (ProposalId -> a) -> [a]
forEachProposalId ps = forEachProposalId' ps.proposalCount
where
forEachProposalId' :: Integer -> (ProposalId -> a) -> [a]
forEachProposalId' 0 _ = error "zero proposal"
forEachProposalId' n f = f . ProposalId <$> [0 .. n - 1]
action =
if p.retractVotes
then "unlock stake + retract votes"
else "unlock stake"
-- | Create locks for the input stake given the parameters.
mkInputStakeLocks :: Parameters -> [ProposalLock]
mkInputStakeLocks ps = mconcat $ forEachProposalId ps $ mkStakeLocksFor ps.stakeRole
where
mkStakeLocksFor :: StakeRole -> ProposalId -> [ProposalLock]
mkStakeLocksFor sr pid =
let voted = [Voted pid defVoteFor]
created = [Created pid]
in case sr of
Voter -> voted
Creator -> created
Both -> voted <> created
_ -> []
while = show p.proposalStatus
-- | Create locks for the output stake by removing locks from the input locks.
mkOutputStakeLocks :: Parameters -> [ProposalLock]
mkOutputStakeLocks ps =
filter
( \lock -> not $ case lock of
Voted _ _ -> ps.removeVoterLock
Created _ -> ps.removeCreatorLock
)
inputLocks
where
inputLocks = mkInputStakeLocks ps
proposalInfo = mconcat [show p.proposalCount, " proposals"]
in mconcat [proposalInfo, ", ", role, ", ", action, ", ", while]
-- | Create the stake input datum given the parameters.
mkStakeInputDatum :: Parameters -> StakeDatum
mkStakeInputDatum ps =
StakeDatum
{ stakedAmount = defStakedGTs
, owner = defOwner
, lockedBy = mkInputStakeLocks ps
}
-- | Create stake output datum given the parameters.
mkStakeOutputDatum :: Parameters -> StakeDatum
mkStakeOutputDatum ps =
let template = mkStakeInputDatum ps
stakedAmount' =
if ps.alterOutputStake
then alteredStakedGTs
else defStakedGTs
in template
{ stakedAmount = stakedAmount'
, lockedBy = mkOutputStakeLocks ps
}
-- | Generate some input proposals and their corresponding output proposals.
mkProposals :: UnlockStakeParameters -> [(ProposalDatum, ProposalDatum)]
mkProposals p = forEachProposalId p.proposalCount $ mkProposalDatumPair p
-- | Iterate over the proposal id of every proposal, given the number of proposals.
forEachProposalId :: Integer -> (ProposalId -> a) -> [a]
forEachProposalId 0 _ = error "zero proposal"
forEachProposalId n f = f . ProposalId <$> [0 .. n - 1]
-- | Create the input stake and its corresponding output stake.
mkStakeDatumPair :: UnlockStakeParameters -> (StakeDatum, StakeDatum)
mkStakeDatumPair c =
let output =
StakeDatum
{ stakedAmount = defaultStakedGTs
, owner = signer
, lockedBy = []
}
inputLocks = join $ forEachProposalId c.proposalCount (mkStakeLocks c.stakeUsage)
input = output {lockedBy = inputLocks}
in (input, output)
where
mkStakeLocks :: StakeRole -> ProposalId -> [ProposalLock]
mkStakeLocks Voter pid = [Voted pid defaultVoteFor]
mkStakeLocks Creator pid = [Created pid]
mkStakeLocks _ _ = []
mkProposals :: Parameters -> [(ProposalDatum, ProposalDatum)]
mkProposals ps = forEachProposalId ps $ mkProposalDatumPair ps
-- | Create the input proposal datum.
mkProposalInputDatum :: UnlockStakeParameters -> ProposalId -> ProposalDatum
mkProposalInputDatum :: Parameters -> ProposalId -> ProposalDatum
mkProposalInputDatum p pid = fst $ mkProposalDatumPair p pid
-- | Create the input stake datum.
mkStakeInputDatum :: UnlockStakeParameters -> StakeDatum
mkStakeInputDatum = fst . mkStakeDatumPair
-- | Create a input proposal and its corresponding output proposal.
mkProposalDatumPair ::
UnlockStakeParameters ->
Parameters ->
ProposalId ->
(ProposalDatum, ProposalDatum)
mkProposalDatumPair params pid =
let owner = signer
inputVotes = mkInputVotes params.stakeUsage $ untag defaultStakedGTs
let inputVotes = mkInputVotes params.stakeRole $ untag defStakedGTs
input =
ProposalDatum
{ proposalId = pid
, effects = emptyEffectFor votesTemplate
, status = params.proposalStatus
, cosigners = [owner]
, cosigners = [defOwner]
, thresholds = def
, votes = inputVotes
, timingConfig = def
@ -198,62 +231,62 @@ mkProposalDatumPair params pid =
-- The staked amount/votes.
Integer ->
ProposalVotes
mkInputVotes Voter vc =
ProposalVotes $
updateMap (Just . const vc) defaultVoteFor $
getProposalVotes votesTemplate
mkInputVotes Creator _ =
ProposalVotes $
updateMap (Just . const 1000) defaultVoteFor $
updateMap (Just . const 1000) defVoteFor $
getProposalVotes votesTemplate
mkInputVotes Irrelevant _ = votesTemplate
mkInputVotes _ vc =
ProposalVotes $
updateMap (Just . const vc) defVoteFor $
getProposalVotes votesTemplate
mkInputVotes _ _ = votesTemplate
-- | Create a 'TxInfo' that tries to unlock a stake.
unlockStake :: UnlockStakeParameters -> TxInfo
unlockStake p =
unlockStake :: Parameters -> TxInfo
unlockStake ps =
let pst = Value.singleton proposalPolicySymbol "" 1
sst = Value.assetClassValue stakeAssetClass 1
pIODatums = mkProposals p
(sInDatum, sOutDatum) = mkStakeDatumPair p
pIODatums = mkProposals ps
proposals =
foldMap
( \(i, o) ->
( \((i, o), idx) ->
mconcat
@BaseBuilder
[ input $
script proposalValidatorHash
. withValue pst
. withDatum i
. withTxId proposalTxRef
. withRefIndex (coerce i.proposalId + 2)
. withOutRef (mkProposalRef idx)
, output $
script proposalValidatorHash
. withValue (sortValue $ pst <> minAda)
. withDatum o
]
)
pIODatums
(zip pIODatums [0 ..])
stakeValue =
sortValue $
mconcat
[ Value.assetClassValue
(untag stake.gtClassRef)
(untag defaultStakedGTs)
(untag defStakedGTs)
, sst
, minAda
]
sInDatum = mkStakeInputDatum ps
sOutDatum = mkStakeOutputDatum ps
stakes =
mconcat @BaseBuilder
[ input $
script stakeValidatorHash
. withValue stakeValue
. withDatum sInDatum
. withTxId stakeTxRef
. withRefIndex 1
. withOutRef stakeRef
, output $
script stakeValidatorHash
. withValue stakeValue
@ -265,23 +298,243 @@ unlockStake p =
[ txId "388bc0b897b3dadcd479da4c88291de4113a50b72ddbed001faf7fc03f11bc52"
, proposals
, stakes
, signedWith defOwner
]
in buildTxInfoUnsafe builder
-- | Create a test case that tests the proposal validator's @'Unlock' _@ redeemer.
mkProposalValidatorTestCase :: UnlockStakeParameters -> Bool -> SpecificationTree
mkProposalValidatorTestCase p shouldSucceed =
let datum = mkProposalInputDatum p $ ProposalId 0
redeemer = Unlock
name = show p
scriptContext =
ScriptContext
(unlockStake p)
(Spending (TxOutRef proposalTxRef 2))
in testFunc
shouldSucceed
name
(proposalValidator Shared.proposal)
datum
redeemer
scriptContext
-- | Reference to the stake UTXO.
stakeRef :: TxOutRef
stakeRef = TxOutRef stakeTxRef 1
-- | Generate the reference to a proposal UTXOs, given the index of the proposal.
mkProposalRef :: Int -> TxOutRef
mkProposalRef offset = TxOutRef stakeTxRef $ 2 + fromIntegral offset
-- | Proposal redeemer used by 'mkTestTree', in this case it's always 'Unlock'.
proposalRedeemer :: ProposalRedeemer
proposalRedeemer = Unlock
-- | Stake redeemer used by 'mkTestTree', in this case it's always 'RetractVotes'.
stakeRedeemer :: StakeRedeemer
stakeRedeemer = RetractVotes
--------------------------------------------------------------------------------
{- | Legal parameters that retract votes while the proposals is in 'VotingReady'
state, and also remove voter locks from the stake, which is
used to vote on the proposals.
-}
mkVoterRetractVotesWhileVotingParameters :: Integer -> Parameters
mkVoterRetractVotesWhileVotingParameters nProposals =
Parameters
{ proposalCount = nProposals
, stakeRole = Voter
, retractVotes = True
, removeVoterLock = True
, removeCreatorLock = False
, proposalStatus = VotingReady
, alterOutputStake = False
}
{- | Legal parameters that retract votes while the proposals is in 'VotingReady'
state, and also remove voter locks from the stake, which is
used to both create and vote on the proposals.
-}
mkVoterCreatorRetractVotesWhileVotingParameters :: Integer -> Parameters
mkVoterCreatorRetractVotesWhileVotingParameters nProposals =
Parameters
{ proposalCount = nProposals
, stakeRole = Both
, retractVotes = True
, removeVoterLock = True
, removeCreatorLock = False
, proposalStatus = VotingReady
, alterOutputStake = False
}
{- | Legal parameters that remove creator locks from the stake while the
proposals is in 'Finished' state. The stake was only used for creating
the proposals.
-}
mkCreatorRemoveCreatorLocksWhenFinishedParameters :: Integer -> Parameters
mkCreatorRemoveCreatorLocksWhenFinishedParameters nProposals =
Parameters
{ proposalCount = nProposals
, stakeRole = Creator
, retractVotes = False
, removeVoterLock = False
, removeCreatorLock = True
, proposalStatus = Finished
, alterOutputStake = False
}
{- | Legal parameters that remove voter and creator locks from the stake while
the proposals is in 'Finished' state. The stake was used for creating
and voting on the proposals.
-}
mkVoterCreatorRemoveAllLocksWhenFinishedParameters :: Integer -> Parameters
mkVoterCreatorRemoveAllLocksWhenFinishedParameters nProposals =
Parameters
{ proposalCount = nProposals
, stakeRole = Both
, retractVotes = False
, removeVoterLock = True
, removeCreatorLock = True
, proposalStatus = Finished
, alterOutputStake = False
}
{- Legal parameters that remove voter locks from the stake after the voting
phrase. The stake was used only for voting on the proposals.
-}
mkVoterUnlockStakeAfterVotingParameters :: Integer -> [Parameters]
mkVoterUnlockStakeAfterVotingParameters nProposals =
map
( \st ->
Parameters
{ proposalCount = nProposals
, stakeRole = Voter
, retractVotes = False
, removeVoterLock = True
, removeCreatorLock = False
, proposalStatus = st
, alterOutputStake = False
}
)
[Locked, Finished]
{- Legal parameters that remove voter locks whenproposals are in phrase.
The stake was used for crating and voting on the proposals.
-}
mkVoterCreatorRemoveVoteLocksWhenLockedParameters :: Integer -> Parameters
mkVoterCreatorRemoveVoteLocksWhenLockedParameters nProposals =
Parameters
{ proposalCount = nProposals
, stakeRole = Both
, retractVotes = False
, removeVoterLock = True
, removeCreatorLock = False
, proposalStatus = Locked
, alterOutputStake = False
}
{- | Illegal parameters that retract votes when the proposals are not in voting
phrase.
-}
mkRetractVotesWhileNotVoting :: Integer -> [Parameters]
mkRetractVotesWhileNotVoting nProposals = do
role <- enumFrom Voter
status <- [Draft, Locked, Finished]
pure $
Parameters
{ proposalCount = nProposals
, stakeRole = role
, retractVotes = True
, removeVoterLock = True
, removeCreatorLock = False
, proposalStatus = status
, alterOutputStake = False
}
{- | Illegal parameter that try to unlock a stake that has nothing to do with
the proposals.
-}
mkUnockIrrelevantStakeParameters :: Integer -> [Parameters]
mkUnockIrrelevantStakeParameters nProposals = do
status <- [Draft, VotingReady, Locked, Finished]
retractVotes <- [True, False]
pure $
Parameters
{ proposalCount = nProposals
, stakeRole = Irrelevant
, retractVotes = retractVotes
, removeVoterLock = True
, removeCreatorLock = True
, proposalStatus = status
, alterOutputStake = False
}
{- | Illegal parameters that remove the creator locks before the proposals are
'Finished'.
-}
mkRemoveCreatorLockBeforeFinishedParameters :: Integer -> [Parameters]
mkRemoveCreatorLockBeforeFinishedParameters nProposals = do
status <- [Draft, VotingReady, Locked]
pure $
Parameters
{ proposalCount = nProposals
, stakeRole = Creator
, retractVotes = False
, removeVoterLock = False
, removeCreatorLock = True
, proposalStatus = status
, alterOutputStake = False
}
{- | Illegal parameters that try to retract votes with a stake that was only used
for creating the proposals.
-}
mkRetractVotesWithCreatorStakeParamaters :: Integer -> Parameters
mkRetractVotesWithCreatorStakeParamaters nProposals =
Parameters
{ proposalCount = nProposals
, stakeRole = Creator
, retractVotes = True
, removeVoterLock = True
, removeCreatorLock = True
, proposalStatus = VotingReady
, alterOutputStake = False
}
{- | Illegal parameters that try to change the 'StakeDatum.stakedAmount' field of
the output stake datum.
-}
mkAlterStakeParameters :: Integer -> [Parameters]
mkAlterStakeParameters nProposals = do
role <- enumFrom Voter
status <- [Draft, Locked, Finished]
pure $
Parameters
{ proposalCount = nProposals
, stakeRole = role
, retractVotes = True
, removeVoterLock = True
, removeCreatorLock = False
, proposalStatus = status
, alterOutputStake = True
}
--------------------------------------------------------------------------------
{- | Create a test tree that runs both the stake validator and the proposal
validator.
-}
mkTestTree :: String -> Parameters -> Bool -> SpecificationTree
mkTestTree name ps isValid = group name [stake, proposal]
where
txInfo = unlockStake ps
stake =
testFunc
(not ps.alterOutputStake)
"stake"
(stakeValidator Shared.stake)
(mkStakeInputDatum ps)
stakeRedeemer
(ScriptContext txInfo (Spending stakeRef))
proposal =
let idx = 0
pid = ProposalId $ fromIntegral idx
ref = mkProposalRef idx
in testFunc
isValid
"propsoal"
(proposalValidator Shared.proposal)
(mkProposalInputDatum ps pid)
proposalRedeemer
(ScriptContext txInfo (Spending ref))