diff --git a/agora-specs/Property/Governor.hs b/agora-specs/Property/Governor.hs index c4922a8..7d45d99 100644 --- a/agora-specs/Property/Governor.hs +++ b/agora-specs/Property/Governor.hs @@ -106,7 +106,7 @@ governorDatumValidProperty = thres <- genProposalThresholds c let timing = ProposalTimingConfig 0 0 0 0 - return $ GovernorDatum thres (ProposalId 0) timing (MaxTimeRangeWidth 0) + return $ GovernorDatum thres (ProposalId 0) timing (MaxTimeRangeWidth 0) 3 where taggedInteger p = Tagged <$> chooseInteger p genProposalThresholds :: GovernorDatumCases -> Gen ProposalThresholds @@ -181,6 +181,7 @@ governorMintingProperty = , nextProposalId = ProposalId 0 , proposalTimings = def , createProposalTimeRangeMaxWidth = def + , maximumProposalsPerStake = 3 } gen :: GovernorPolicyCases -> Gen ScriptContext diff --git a/agora-specs/Sample/Effect/GovernorMutation.hs b/agora-specs/Sample/Effect/GovernorMutation.hs index 7394b70..bc3967a 100644 --- a/agora-specs/Sample/Effect/GovernorMutation.hs +++ b/agora-specs/Sample/Effect/GovernorMutation.hs @@ -106,6 +106,7 @@ mkEffectTxInfo newGovDatum = , nextProposalId = ProposalId 0 , proposalTimings = def , createProposalTimeRangeMaxWidth = def + , maximumProposalsPerStake = 3 } governorInputDatum :: Datum governorInputDatum = Datum $ toBuiltinData governorInputDatum' @@ -168,6 +169,7 @@ validNewGovernorDatum = , nextProposalId = ProposalId 42 , proposalTimings = def , createProposalTimeRangeMaxWidth = def + , maximumProposalsPerStake = 3 } invalidNewGovernorDatum :: GovernorDatum @@ -180,4 +182,5 @@ invalidNewGovernorDatum = , nextProposalId = ProposalId 42 , proposalTimings = def , createProposalTimeRangeMaxWidth = def + , maximumProposalsPerStake = 3 } diff --git a/agora-specs/Sample/Governor.hs b/agora-specs/Sample/Governor.hs index 1641640..44d0105 100644 --- a/agora-specs/Sample/Governor.hs +++ b/agora-specs/Sample/Governor.hs @@ -1,5 +1,5 @@ {- | -Module : Spec.Sample.Governor +Module : Sample.Governor Maintainer : connor@mlabs.city Description: Sample based testing for Governor utxos @@ -119,6 +119,7 @@ mintGST = , nextProposalId = ProposalId 0 , proposalTimings = def , createProposalTimeRangeMaxWidth = def + , maximumProposalsPerStake = 3 } witness :: ValidatorHash @@ -183,6 +184,7 @@ createProposal = , nextProposalId = thisProposalId , proposalTimings = def , createProposalTimeRangeMaxWidth = def + , maximumProposalsPerStake = 3 } effects = @@ -216,8 +218,7 @@ createProposal = proposalLocks :: [ProposalLock] proposalLocks = - [ ProposalLock (ResultTag 0) thisProposalId - , ProposalLock (ResultTag 1) thisProposalId + [ Created thisProposalId ] stakeOutputDatum :: StakeDatum stakeOutputDatum = stakeInputDatum {lockedBy = proposalLocks} @@ -304,6 +305,7 @@ mintGATs = , nextProposalId = ProposalId 5 , proposalTimings = def , createProposalTimeRangeMaxWidth = def + , maximumProposalsPerStake = 3 } effects = @@ -423,6 +425,7 @@ mutateState = , nextProposalId = ProposalId 5 , proposalTimings = def , createProposalTimeRangeMaxWidth = def + , maximumProposalsPerStake = 3 } governorOutputDatum :: GovernorDatum diff --git a/agora-specs/Sample/Proposal.hs b/agora-specs/Sample/Proposal.hs deleted file mode 100644 index 6d2d6a9..0000000 --- a/agora-specs/Sample/Proposal.hs +++ /dev/null @@ -1,114 +0,0 @@ -{- | -Module : Sample.Proposal -Maintainer : emi@haskell.fyi -Description: Sample based testing for Proposal utxos - -This module tests primarily the happy path for Proposal interactions --} -module Sample.Proposal ( - -- * Script contexts - proposalCreation, -) where - -import Agora.Governor (GovernorDatum (..)) -import Agora.Proposal ( - Proposal (..), - ProposalDatum (..), - ProposalId (..), - ProposalStatus (..), - ResultTag (..), - emptyVotesFor, - ) -import Data.Default.Class (Default (def)) -import Plutarch.Context ( - MintingBuilder, - buildMintingUnsafe, - input, - mint, - output, - script, - signedWith, - txId, - withDatum, - withTxId, - withValue, - ) -import PlutusLedgerApi.V1 ( - ScriptContext (..), - ) -import PlutusLedgerApi.V1.Value qualified as Value ( - assetClassValue, - singleton, - ) -import PlutusTx.AssocMap qualified as AssocMap -import Sample.Shared ( - govValidatorHash, - proposal, - proposalPolicySymbol, - proposalStartingTimeFromTimeRange, - proposalValidatorHash, - signer, - ) -import Test.Util ( - closedBoundedInterval, - ) - -proposalCreation :: ScriptContext -proposalCreation = - let st = Value.singleton proposalPolicySymbol "" 1 -- Proposal ST - effects = - AssocMap.fromList - [ (ResultTag 0, AssocMap.empty) - , (ResultTag 1, AssocMap.empty) - ] - proposalDatum :: ProposalDatum - proposalDatum = - ProposalDatum - { proposalId = ProposalId 0 - , effects = effects - , status = Draft - , cosigners = [signer] - , thresholds = def - , votes = emptyVotesFor effects - , timingConfig = def - , startingTime = proposalStartingTimeFromTimeRange validTimeRange - } - - govBefore :: GovernorDatum - govBefore = - GovernorDatum - { proposalThresholds = def - , nextProposalId = ProposalId 0 - , proposalTimings = def - , createProposalTimeRangeMaxWidth = def - } - - govAfter :: GovernorDatum - govAfter = govBefore {nextProposalId = ProposalId 1} - - validTimeRange = closedBoundedInterval 10 15 - - builder :: MintingBuilder - builder = - mconcat - [ txId "0b2086cbf8b6900f8cb65e012de4516cb66b5cb08a9aaba12a8b88be" - , signedWith signer - , mint st - , input $ - script govValidatorHash - . withValue (Value.assetClassValue proposal.governorSTAssetClass 1) - . withDatum govBefore - . withTxId "0b2086cbf8b6900f8cb65e012de4516cb66b5cb08a9aaba12a8b88be" - , output $ - script proposalValidatorHash - . withValue (st <> Value.singleton "" "" 10_000_000) - . withDatum proposalDatum - , output $ - script govValidatorHash - . withValue - ( Value.assetClassValue proposal.governorSTAssetClass 1 - <> Value.singleton "" "" 10_000_000 - ) - . withDatum govAfter - ] - in buildMintingUnsafe builder diff --git a/agora-specs/Sample/Proposal/Advance.hs b/agora-specs/Sample/Proposal/Advance.hs index ea3aaed..73c2ab6 100644 --- a/agora-specs/Sample/Proposal/Advance.hs +++ b/agora-specs/Sample/Proposal/Advance.hs @@ -1,3 +1,10 @@ +{- | +Module : Sample.Proposal.Advance +Maintainer : connor@mlabs.city +Description: Generate sample data for testing the functionalities of advancing proposals + +Sample and utilities for testing the functionalities of advancing proposals. +-} module Sample.Proposal.Advance ( advanceToNextStateInTimeParameters, advanceToFailedStateDueToTimeoutParameters, @@ -31,7 +38,7 @@ import Agora.Proposal.Time ( ) import Agora.SafeMoney (GTTag) import Agora.Stake ( - ProposalLock (ProposalLock), + ProposalLock (..), Stake (gtClassRef), StakeDatum (..), StakeRedeemer (WitnessStake), @@ -69,7 +76,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 (proposalTxRef, stakeTxRef) import Sample.Shared ( minAda, proposalPolicySymbol, @@ -79,7 +86,7 @@ import Sample.Shared ( stakeValidatorHash, ) import Sample.Shared qualified as Shared -import Test.Specification (SpecificationTree, group) +import Test.Specification (SpecificationTree, group, testValidator) import Test.Util (closedBoundedInterval, pubKeyHashes, sortValue, updateMap) -- | Parameters for state transition of proposals. @@ -99,19 +106,24 @@ data Parameters = Parameters , stakeCount :: Integer -- ^ The number of stakes. , signByAllCosigners :: Bool + -- ^ Whether the transaction is signed by all the cosigners. , perStakeGTs :: Tagged GTTag Integer + -- ^ The staked amount of each stake. } --- +-- | Reference to the proposal UTXO. proposalRef :: TxOutRef proposalRef = TxOutRef proposalTxRef 1 +-- | Create the reference to a particular stake UTXO. mkStakeRef :: Int -> TxOutRef mkStakeRef = TxOutRef stakeTxRef . (+ 2) . fromIntegral --- +-- | Default effects of the propsoal. defEffects :: AssocMap.Map ResultTag (AssocMap.Map ValidatorHash DatumHash) defEffects = AssocMap.fromList @@ -119,14 +131,19 @@ defEffects = , (ResultTag 1, AssocMap.empty) ] +-- | Empty votes for the default effects. emptyVotes :: ProposalVotes emptyVotes = emptyVotesFor defEffects +{- | The default proposal statring time, which doesn't really matter in this + case. +-} proposalStartingTime :: POSIXTime proposalStartingTime = 0 --- +-- | Create the input proposal datum given the parameters. mkProposalInputDatum :: Parameters -> ProposalDatum mkProposalInputDatum ps = ProposalDatum @@ -140,6 +157,7 @@ mkProposalInputDatum ps = , startingTime = ProposalStartingTime proposalStartingTime } +-- | Create the input stake datums given the parameters. mkStakeInputDatums :: Parameters -> [StakeDatum] mkStakeInputDatums ps = map @@ -154,28 +172,37 @@ mkStakeInputDatums ps = where existingLocks :: [ProposalLock] existingLocks = - [ ProposalLock (ResultTag 0) (ProposalId 0) - , ProposalLock (ResultTag 2) (ProposalId 1) + [ Voted (ProposalId 0) (ResultTag 0) + , Voted (ProposalId 1) (ResultTag 2) ] --- +-- | Script purpose of the proposal validator. proposalScriptPurpose :: ScriptPurpose proposalScriptPurpose = Spending proposalRef +-- | Script purpose of the stake validator, given which stake we want to spend. mkStakeScriptPurpose :: Int -> ScriptPurpose mkStakeScriptPurpose = Spending . mkStakeRef --- +{- | The propsoal redeemer used to spend the proposal UTXO, which is always + 'AdvanceProposal' in this case. +-} proposalRedeemer :: ProposalRedeemer proposalRedeemer = AdvanceProposal +{- | The propsoal redeemer used to spend the stake UTXO, which is always + 'WitnessStake' in this case. +-} stakeRedeemer :: StakeRedeemer stakeRedeemer = WitnessStake --- +-- | Create some valid stake owners. mkStakeOwners :: Parameters -> [PubKeyHash] mkStakeOwners ps = sort $ @@ -276,6 +303,9 @@ advance ps = --- +{- | Given the proposal status, create a time range that is in time for + advacing to the next state. +-} mkInTimeTimeRange :: ProposalStatus -> POSIXTimeRange mkInTimeTimeRange advanceFrom = case advanceFrom of @@ -315,6 +345,9 @@ mkInTimeTimeRange advanceFrom = ) Finished -> error "Cannot advance 'Finished' proposal" +{- | Given the proposal status, create a time range that is too time for + advacing to the next state. +-} mkTooLateTimeRange :: ProposalStatus -> POSIXTimeRange mkTooLateTimeRange advanceFrom = case advanceFrom of @@ -363,6 +396,7 @@ mkTooLateTimeRange advanceFrom = --- +-- | Next state of the given proposal status. getNextState :: ProposalStatus -> ProposalStatus getNextState = \case Draft -> VotingReady @@ -475,6 +509,9 @@ invalidOutputStakeParameters nCosigners = --- +{- | Create a test tree that runs the stake validator and proposal validator to + test the advancing functionalities. +-} mkTestTree :: String -> Parameters -> Bool -> SpecificationTree mkTestTree name ps isValidForProposalValidator = group name [proposal, stake] where @@ -482,7 +519,7 @@ mkTestTree name ps isValidForProposalValidator = group name [proposal, stake] proposal = let proposalInputDatum = mkProposalInputDatum ps - in testFunc + in testValidator isValidForProposalValidator "propsoal" (proposalValidator Shared.proposal) @@ -497,7 +534,7 @@ mkTestTree name ps isValidForProposalValidator = group name [proposal, stake] let idx = 0 stakeInputDatum = mkStakeInputDatums ps !! idx isValid = not $ ps.alterOutputStakes - in testFunc + in testValidator isValid "stake" (stakeValidator Shared.stake) diff --git a/agora-specs/Sample/Proposal/Cosign.hs b/agora-specs/Sample/Proposal/Cosign.hs index f04535a..f9a3865 100644 --- a/agora-specs/Sample/Proposal/Cosign.hs +++ b/agora-specs/Sample/Proposal/Cosign.hs @@ -1,3 +1,10 @@ +{- | +Module : Sample.Proposal.Cosign +Maintainer : connor@mlabs.city +Description: Generate sample data for testing the functionalities of cosigning proposals + +Sample and utilities for testing the functionalities of cosigning proposals. +-} module Sample.Proposal.Cosign ( Parameters (..), validCosignNParameters, @@ -57,7 +64,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 (proposalTxRef, stakeTxRef) import Sample.Shared ( minAda, proposalPolicySymbol, @@ -71,6 +78,7 @@ import Sample.Shared qualified as Shared import Test.Specification ( SpecificationTree, group, + testValidator, ) import Test.Util (closedBoundedInterval, pubKeyHashes, sortValue) @@ -317,7 +325,7 @@ mkTestTree name ps isValid = group name [proposal, stake] proposal = let proposalInputDatum = mkProposalInputDatum ps - in testFunc + in testValidator isValid "propsoal" (proposalValidator Shared.proposal) @@ -332,7 +340,7 @@ mkTestTree name ps isValid = group name [proposal, stake] let idx = 0 stakeInputDatum = mkStakeInputDatums ps !! idx isValid = not ps.alterOutputStakes - in testFunc + in testValidator isValid "stake" (stakeValidator Shared.stake) diff --git a/agora-specs/Sample/Proposal/Create.hs b/agora-specs/Sample/Proposal/Create.hs new file mode 100644 index 0000000..3556256 --- /dev/null +++ b/agora-specs/Sample/Proposal/Create.hs @@ -0,0 +1,461 @@ +{- | +Module : Sample.Proposal.Create +Maintainer : connor@mlabs.city +Description: Generate sample data for testing the functionalities of creating proposals + +Sample and utilities for testing the functionalities of creating proposals. +-} +module Sample.Proposal.Create ( + Parameters (..), + mkTestTree, + totallyValidParameters, + invalidOutputGovernorDatumParameters, + useStakeOwnBySomeoneElseParameters, + invalidOutputStakeParameters, + addInvalidLocksParameters, + exceedMaximumProposalsParameters, + timeRangeNotTightParameters, + timeRangeNotClosedParameters, + invalidProposalStatusParameters, +) where + +import Agora.Governor ( + GovernorDatum (..), + GovernorRedeemer (CreateProposal), + ) +import Agora.Governor.Scripts (governorValidator) +import Agora.Proposal ( + Proposal (governorSTAssetClass), + ProposalDatum (..), + ProposalId (ProposalId), + ProposalStatus (..), + ResultTag (ResultTag), + emptyVotesFor, + ) +import Agora.Proposal.Scripts (proposalPolicy) +import Agora.Proposal.Time (MaxTimeRangeWidth (MaxTimeRangeWidth), ProposalStartingTime (..)) +import Agora.Stake ( + ProposalLock (..), + Stake (gtClassRef), + StakeDatum (..), + StakeRedeemer (PermitVote), + ) +import Agora.Stake.Scripts (stakeValidator) +import Data.Coerce (coerce) +import Data.Default (Default (def)) +import Data.Tagged (Tagged, untag) +import Plutarch.Context ( + BaseBuilder, + buildTxInfoUnsafe, + input, + mint, + output, + script, + signedWith, + timeRange, + txId, + withDatum, + withOutRef, + withValue, + ) +import PlutusLedgerApi.V1 ( + DatumHash, + POSIXTime (POSIXTime), + POSIXTimeRange, + PubKeyHash, + ScriptContext (ScriptContext), + ScriptPurpose (Minting, Spending), + TxInfo, + TxOutRef (TxOutRef), + ValidatorHash, + always, + ) +import PlutusLedgerApi.V1.Value qualified as Value +import PlutusTx.AssocMap qualified as AssocMap +import Sample.Proposal.Shared (stakeTxRef) +import Sample.Shared ( + govValidatorHash, + minAda, + proposal, + proposalPolicySymbol, + proposalStartingTimeFromTimeRange, + proposalValidatorHash, + signer, + signer2, + stake, + stakeAssetClass, + stakeValidatorHash, + ) +import Sample.Shared qualified as Shared +import Test.Specification (SpecificationTree, group, testPolicy, testValidator) +import Test.Util (closedBoundedInterval, sortValue) + +-- | Parameters for creating a proposal. +data Parameters = Parameters + { advanceNextProposalId :: Bool + -- ^ Whether to advance 'GovernorDatum.nextProposalId'. + , createdMoreThanMaximumProposals :: Bool + -- ^ Try creating more than maximum amount of proposals. + , stakeOwnerSignsTheTransaction :: Bool + -- ^ Should the stake owner sign the transaction? + , invalidNewLocks :: Bool + -- ^ Place invalid new locks on the output stake. + , alterOutputStakeOwner :: Bool + -- ^ Whether to change the 'owner' field of the output stake datum. + , timeRangeTightEnough :: Bool + -- ^ Is 'TxInfo.validTimeRange' tight enough? + , timeRangeClosed :: Bool + -- ^ Is 'TxInfo.validTimeRange' closed? + , proposalStatus :: ProposalStatus + -- ^ The status of the newly created proposal. + } + +-------------------------------------------------------------------------------- + +-- | See 'GovernorDatum.maximumProposalsPerStake'. +maxProposalPerStake :: Integer +maxProposalPerStake = 3 + +-- | The id of the proposal we are creating. +thisProposalId :: ProposalId +thisProposalId = ProposalId 25 + +-- | The arbitrary staked amount. Doesn;t really matter in this case. +stakedGTs :: Tagged _ Integer +stakedGTs = 5 + +-- | The owner of the stake. +stakeOwner :: PubKeyHash +stakeOwner = signer + +{- | The invalid stake owner. If the 'alterOutputStakeOwner' is set to true, + the output stake owner will be set to this. +-} +alteredStakeOwner :: PubKeyHash +alteredStakeOwner = signer2 + +-- | Locks the stake that the input stake already has. +defLocks :: [ProposalLock] +defLocks = [Created (ProposalId 0)] + +-- | The effect of the newly created proposal. +defEffects :: AssocMap.Map ResultTag (AssocMap.Map ValidatorHash DatumHash) +defEffects = + AssocMap.fromList + [ (ResultTag 0, AssocMap.empty) + , (ResultTag 1, AssocMap.empty) + ] + +-------------------------------------------------------------------------------- + +-- | The governor input datum. +governorInputDatum :: GovernorDatum +governorInputDatum = + GovernorDatum + { proposalThresholds = def + , nextProposalId = thisProposalId + , proposalTimings = def + , createProposalTimeRangeMaxWidth = def + , maximumProposalsPerStake = maxProposalPerStake + } + +-- | Create governor output datum given the parameters. +mkGovernorOutputDatum :: Parameters -> GovernorDatum +mkGovernorOutputDatum ps = + let nextPid = + if ps.advanceNextProposalId + then ProposalId $ coerce thisProposalId + 1 + else thisProposalId + in GovernorDatum + { proposalThresholds = def + , nextProposalId = nextPid + , proposalTimings = def + , createProposalTimeRangeMaxWidth = def + , maximumProposalsPerStake = maxProposalPerStake + } + +-------------------------------------------------------------------------------- + +-- | Create the stake input datum given the parameters. +mkStakeInputDatum :: Parameters -> StakeDatum +mkStakeInputDatum ps = + let locks = + if ps.createdMoreThanMaximumProposals + then + Created . ProposalId + <$> take + (fromInteger maxProposalPerStake) + [1 ..] + else defLocks + in StakeDatum + { stakedAmount = stakedGTs + , owner = stakeOwner + , lockedBy = locks + } + +-- | Create the stake output datum given the parameters. +mkStakeOutputDatum :: Parameters -> StakeDatum +mkStakeOutputDatum ps = + let inputDatum = mkStakeInputDatum ps + newLocks = + if ps.invalidNewLocks + then + [ Voted thisProposalId (ResultTag 0) + , Voted thisProposalId (ResultTag 1) + ] + else [Created thisProposalId] + locks = newLocks <> inputDatum.lockedBy + newOwner = mkOwner ps + in inputDatum + { owner = newOwner + , lockedBy = locks + } + +-------------------------------------------------------------------------------- + +{- | Create the proposal datum for the newly created proposal, given the + parameters. +-} +mkProposalOutputDatum :: Parameters -> ProposalDatum +mkProposalOutputDatum ps = + ProposalDatum + { proposalId = thisProposalId + , effects = defEffects + , status = ps.proposalStatus + , cosigners = [mkOwner ps] + , thresholds = def + , votes = emptyVotesFor defEffects + , timingConfig = def + , startingTime = mkProposalStartingTime ps + } + +-------------------------------------------------------------------------------- + +-- | Create time range for 'TxInfo.validTimeRange'. +mkTimeRange :: Parameters -> POSIXTimeRange +mkTimeRange ps = + if ps.timeRangeClosed + then + let s = 0 + di :: POSIXTime = coerce (def @MaxTimeRangeWidth) + o = if ps.timeRangeTightEnough then (-1) else 1 + in closedBoundedInterval s $ o + di + else always + +-- | Get the starting time of the propsoal. +mkProposalStartingTime :: Parameters -> ProposalStartingTime +mkProposalStartingTime ps = + if ps.timeRangeClosed + then proposalStartingTimeFromTimeRange $ mkTimeRange ps + else ProposalStartingTime 0 + +-- | Who should be the 'owner' of the output stake. +mkOwner :: Parameters -> PubKeyHash +mkOwner ps = + if ps.alterOutputStakeOwner + then alteredStakeOwner + else stakeOwner + +-------------------------------------------------------------------------------- + +-- | Reference to the input stake UTXO. +stakeRef :: TxOutRef +stakeRef = TxOutRef stakeTxRef 1 + +-- | Reference to the input governor UTXO. +governorRef :: TxOutRef +governorRef = TxOutRef "0b2086cbf8b6900f8cb65e012de4516cb66b5cb08a9aaba12a8b88be" 3 + +-------------------------------------------------------------------------------- + +-- | Create a 'TxInfo' that spends a stake to create a new proposal. +createProposal :: Parameters -> TxInfo +createProposal ps = buildTxInfoUnsafe builder + where + pst = Value.singleton proposalPolicySymbol "" 1 + sst = Value.assetClassValue stakeAssetClass 1 + gst = Value.assetClassValue proposal.governorSTAssetClass 1 + + --- + + governorValue = sortValue $ gst <> minAda + stakeValue = + sortValue $ + sortValue $ + sst + <> Value.assetClassValue (untag stake.gtClassRef) (untag stakedGTs) + <> minAda + proposalValue = sortValue $ pst <> minAda + + --- + + withSig = + if ps.stakeOwnerSignsTheTransaction + then signedWith stakeOwner + else mempty + + --- + + builder :: BaseBuilder + builder = + mconcat + [ txId "0b2086cbf8b6900f8cb65e012de4516cb66b5cb08a9aaba12a8b88be" + , --- + withSig + , --- + mint pst + , --- + timeRange $ mkTimeRange ps + , input $ + script govValidatorHash + . withValue governorValue + . withDatum governorInputDatum + . withOutRef governorRef + , output $ + script govValidatorHash + . withValue governorValue + . withDatum (mkGovernorOutputDatum ps) + , --- + input $ + script stakeValidatorHash + . withValue stakeValue + . withDatum (mkStakeInputDatum ps) + . withOutRef stakeRef + , output $ + script stakeValidatorHash + . withValue stakeValue + . withDatum (mkStakeOutputDatum ps) + , --- + output $ + script proposalValidatorHash + . withValue proposalValue + . withDatum (mkProposalOutputDatum ps) + ] + +-------------------------------------------------------------------------------- + +-- | Spend the stake with the 'PermitVote' redeemer. +stakeRedeemer :: StakeRedeemer +stakeRedeemer = PermitVote + +-- | Spend the governor with the 'CreateProposal' redeemer. +governorRedeemer :: GovernorRedeemer +governorRedeemer = CreateProposal + +-- | Mint the PST with an arbitrary redeemer. Doesn't really matter. +proposalPolicyRedeemer :: () +proposalPolicyRedeemer = () + +-------------------------------------------------------------------------------- + +totallyValidParameters :: Parameters +totallyValidParameters = + Parameters + { advanceNextProposalId = True + , createdMoreThanMaximumProposals = False + , stakeOwnerSignsTheTransaction = True + , invalidNewLocks = False + , alterOutputStakeOwner = False + , timeRangeTightEnough = True + , timeRangeClosed = True + , proposalStatus = Draft + } + +invalidOutputGovernorDatumParameters :: Parameters +invalidOutputGovernorDatumParameters = + totallyValidParameters + { advanceNextProposalId = False + } + +useStakeOwnBySomeoneElseParameters :: Parameters +useStakeOwnBySomeoneElseParameters = + totallyValidParameters + { stakeOwnerSignsTheTransaction = False + } + +invalidOutputStakeParameters :: Parameters +invalidOutputStakeParameters = + totallyValidParameters + { alterOutputStakeOwner = True + } + +addInvalidLocksParameters :: Parameters +addInvalidLocksParameters = + totallyValidParameters + { invalidNewLocks = True + } + +exceedMaximumProposalsParameters :: Parameters +exceedMaximumProposalsParameters = + totallyValidParameters + { createdMoreThanMaximumProposals = True + } + +timeRangeNotTightParameters :: Parameters +timeRangeNotTightParameters = + totallyValidParameters + { timeRangeTightEnough = False + } + +timeRangeNotClosedParameters :: Parameters +timeRangeNotClosedParameters = + totallyValidParameters + { timeRangeClosed = False + } + +invalidProposalStatusParameters :: [Parameters] +invalidProposalStatusParameters = + map + ( \st -> + totallyValidParameters {proposalStatus = st} + ) + [VotingReady, Locked, Finished] + +-------------------------------------------------------------------------------- + +{- | Create a test tree that runs the propsoal minting policy, the governor + validator and the stake validator to test the functionalities of creting + proposals +-} +mkTestTree :: String -> Parameters -> Bool -> Bool -> Bool -> SpecificationTree +mkTestTree + name + ps + validForProposalPolicy + validForGovernorValidator + validForStakeValidator = + group name [proposalTest, governorTest, stakeTest] + where + txInfo = createProposal ps + + proposalTest = + testPolicy + validForProposalPolicy + "proposal" + (proposalPolicy Shared.proposal.governorSTAssetClass) + proposalPolicyRedeemer + (ScriptContext txInfo (Minting proposalPolicySymbol)) + + governorTest = + testValidator + validForGovernorValidator + "governor" + (governorValidator Shared.governor) + governorInputDatum + governorRedeemer + ( ScriptContext + txInfo + (Spending governorRef) + ) + + stakeTest = + testValidator + validForStakeValidator + "stake" + (stakeValidator Shared.stake) + (mkStakeInputDatum ps) + stakeRedeemer + ( ScriptContext + txInfo + (Spending stakeRef) + ) diff --git a/agora-specs/Sample/Proposal/Shared.hs b/agora-specs/Sample/Proposal/Shared.hs index 17028ee..0082f68 100644 --- a/agora-specs/Sample/Proposal/Shared.hs +++ b/agora-specs/Sample/Proposal/Shared.hs @@ -1,13 +1,13 @@ -module Sample.Proposal.Shared (proposalTxRef, stakeTxRef, testFunc) where +{- | +Module : Sample.Proposal.Shared +Maintainer : connor@mlabs.city +Description: Shared constants for propsoal samples -import Plutarch.Api.V1 (PValidator) -import Plutarch.Lift (PUnsafeLiftDecl (..)) -import PlutusLedgerApi.V1 (ScriptContext, ToData, TxId) -import Test.Specification ( - SpecificationTree, - validatorFailsWith, - validatorSucceedsWith, - ) +Shared constants for propsoal samples. +-} +module Sample.Proposal.Shared (proposalTxRef, stakeTxRef) where + +import PlutusLedgerApi.V1 (TxId) -- | 'TxId' of all the propsoal inputs in the samples. proposalTxRef :: TxId @@ -16,24 +16,3 @@ proposalTxRef = "0b2086cbf8b6900f8cb65e012de4516cb66b5cb08a9aaba12a8b88be" -- | 'TxId' of all the stake inputs in the samples. stakeTxRef :: TxId stakeTxRef = "0ca36f3a357bc69579ab2531aecd1e7d3714d993c7820f40b864be15" - --- | Get the test function given whether a test case is valid. -testFunc :: - forall {datum :: PType} {redeemer :: PType}. - ( PUnsafeLiftDecl datum - , PUnsafeLiftDecl redeemer - , ToData (PLifted datum) - , ToData (PLifted redeemer) - ) => - -- | Should the validator pass? - Bool -> - String -> - ClosedTerm PValidator -> - PLifted datum -> - PLifted redeemer -> - ScriptContext -> - SpecificationTree -testFunc isValid = - if isValid - then validatorSucceedsWith - else validatorFailsWith diff --git a/agora-specs/Sample/Proposal/UnlockStake.hs b/agora-specs/Sample/Proposal/UnlockStake.hs index da8b4e5..20b1be5 100644 --- a/agora-specs/Sample/Proposal/UnlockStake.hs +++ b/agora-specs/Sample/Proposal/UnlockStake.hs @@ -1,12 +1,26 @@ +{- | +Module : Sample.Proposal.UnlockStake +Maintainer : connor@mlabs.city +Description: Generate sample data for testing the functionalities of unlocking stake and retracting votes + +Sample and utilities for testing the functionalities of unlocking stake and retracting votes +-} 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 +35,8 @@ import Agora.Proposal ( ) import Agora.Proposal.Scripts (proposalValidator) import Agora.Proposal.Time (ProposalStartingTime (ProposalStartingTime)) -import Agora.Stake (ProposalLock (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 +45,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 +62,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) import Sample.Shared ( minAda, proposalPolicySymbol, @@ -59,7 +73,7 @@ import Sample.Shared ( stakeValidatorHash, ) import Sample.Shared qualified as Shared -import Test.Specification (SpecificationTree) +import Test.Specification (SpecificationTree, group, testValidator) import Test.Util (sortValue, updateMap) -------------------------------------------------------------------------------- @@ -82,106 +96,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 = [ProposalLock defaultVoteFor pid] - mkStakeLocks Creator pid = - map (`ProposalLock` pid) $ - AssocMap.keys $ getProposalVotes votesTemplate - 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 @@ -200,62 +238,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 @@ -267,23 +305,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 (ResultTag 0) - 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 = + testValidator + (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 testValidator + isValid + "propsoal" + (proposalValidator Shared.proposal) + (mkProposalInputDatum ps pid) + proposalRedeemer + (ScriptContext txInfo (Spending ref)) diff --git a/agora-specs/Sample/Proposal/Vote.hs b/agora-specs/Sample/Proposal/Vote.hs index f06ac7e..36ed93d 100644 --- a/agora-specs/Sample/Proposal/Vote.hs +++ b/agora-specs/Sample/Proposal/Vote.hs @@ -1,3 +1,10 @@ +{- | +Module : Sample.Proposal.Vote +Maintainer : connor@mlabs.city +Description: Generate sample data for testing the functionalities of voting on proposals. + +Sample and utilities for testing the functionalities of voting on proposals. +-} module Sample.Proposal.Vote ( validVoteParameters, mkTestTree, @@ -17,7 +24,7 @@ import Agora.Proposal.Time ( ProposalTimingConfig (draftTime, votingTime), ) import Agora.Stake ( - ProposalLock (ProposalLock), + ProposalLock (..), Stake (gtClassRef), StakeDatum (..), StakeRedeemer (PermitVote), @@ -47,7 +54,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 (proposalTxRef, stakeTxRef) import Sample.Shared ( minAda, proposalPolicySymbol, @@ -61,13 +68,16 @@ import Sample.Shared qualified as Shared import Test.Specification ( SpecificationTree, group, + testValidator, validatorSucceedsWith, ) import Test.Util (closedBoundedInterval, sortValue, updateMap) +-- | Reference to the proposal UTXO. proposalRef :: TxOutRef proposalRef = TxOutRef proposalTxRef 0 +-- | Reference to the stake UTXO. stakeRef :: TxOutRef stakeRef = TxOutRef stakeTxRef 1 @@ -79,9 +89,11 @@ data Parameters = Parameters -- ^ The count of votes. } +-- | The public key hash of the stake owner. stakeOwner :: PubKeyHash stakeOwner = signer +-- | The votes of the input proposals. initialVotes :: AssocMap.Map ResultTag Integer initialVotes = AssocMap.fromList @@ -89,6 +101,7 @@ initialVotes = , (ResultTag 1, 4242) ] +-- | The input proposal datum. proposalInputDatum :: ProposalDatum proposalInputDatum = ProposalDatum @@ -106,12 +119,16 @@ proposalInputDatum = , startingTime = ProposalStartingTime 0 } +-- | The locks of the input stake. existingLocks :: [ProposalLock] existingLocks = - [ ProposalLock (ResultTag 0) (ProposalId 0) - , ProposalLock (ResultTag 2) (ProposalId 1) + [ Voted (ProposalId 0) (ResultTag 0) + , Voted (ProposalId 1) (ResultTag 2) ] +{- | Set the 'StakeDatum.stakedAmount' according to the number of votes being + casted. +-} mkStakeInputDatum :: Parameters -> StakeDatum mkStakeInputDatum params = StakeDatum @@ -120,14 +137,19 @@ mkStakeInputDatum params = , lockedBy = existingLocks } +-- | Create the proposal redeemer. In this case @'Vote' _@ will always be used. mkProposalRedeemer :: Parameters -> ProposalRedeemer mkProposalRedeemer = Vote . voteFor +-- | Place new proposal locks on the stake. mkNewLock :: Parameters -> ProposalLock -mkNewLock ps = ProposalLock ps.voteFor proposalInputDatum.proposalId +mkNewLock = Voted proposalInputDatum.proposalId . voteFor -mkStakeRedeemer :: Parameters -> StakeRedeemer -mkStakeRedeemer = PermitVote . mkNewLock +{- | The stake redeemer that is used in 'mkTestTree'. In this case it'll always be + 'PermitVote'. +-} +stakeRedeemer :: StakeRedeemer +stakeRedeemer = PermitVote -- | Create a valid transaction that votes on a propsal, given the parameters. vote :: Parameters -> TxInfo @@ -210,6 +232,7 @@ vote params = --- +-- | Valida parameters that vote on the proposal. validVoteParameters :: Parameters validVoteParameters = Parameters @@ -219,13 +242,16 @@ validVoteParameters = --- +{- | Create a test tree that runs the stake validator and proposal validator to + test the voting functionalities. +-} mkTestTree :: String -> Parameters -> Bool -> SpecificationTree mkTestTree name ps isValid = group name [proposal, stake] where txInfo = vote ps proposal = - testFunc + testValidator isValid "propsoal" (proposalValidator Shared.proposal) @@ -242,7 +268,7 @@ mkTestTree name ps isValid = group name [proposal, stake] "stake" (stakeValidator Shared.stake) stakeInputDatum - (mkStakeRedeemer ps) + stakeRedeemer ( ScriptContext txInfo (Spending stakeRef) diff --git a/agora-specs/Spec/Effect/GovernorMutation.hs b/agora-specs/Spec/Effect/GovernorMutation.hs index 5bc7b87..ebcf120 100644 --- a/agora-specs/Spec/Effect/GovernorMutation.hs +++ b/agora-specs/Spec/Effect/GovernorMutation.hs @@ -38,6 +38,7 @@ specs = (ProposalId 0) def def + 3 ) MutateGovernor ( ScriptContext @@ -60,6 +61,7 @@ specs = (ProposalId 0) def def + 3 ) MutateGovernor ( ScriptContext diff --git a/agora-specs/Spec/Governor.hs b/agora-specs/Spec/Governor.hs index b488928..cb905de 100644 --- a/agora-specs/Spec/Governor.hs +++ b/agora-specs/Spec/Governor.hs @@ -47,6 +47,7 @@ specs = (ProposalId 0) def def + 3 ) CreateProposal createProposal @@ -58,6 +59,7 @@ specs = (ProposalId 5) def def + 3 ) MintGATs mintGATs @@ -69,6 +71,7 @@ specs = (ProposalId 5) def def + 3 ) MutateGovernor mutateState diff --git a/agora-specs/Spec/Proposal.hs b/agora-specs/Spec/Proposal.hs index a3c54df..9ef2416 100644 --- a/agora-specs/Spec/Proposal.hs +++ b/agora-specs/Spec/Proposal.hs @@ -7,33 +7,83 @@ Tests for Proposal policy and validator -} module Spec.Proposal (specs) where -import Agora.Proposal ( - Proposal (..), - ProposalStatus (..), - ) -import Agora.Proposal.Scripts (proposalPolicy) -import Sample.Proposal qualified as Proposal import Sample.Proposal.Advance qualified as Advance import Sample.Proposal.Cosign qualified as Cosign +import Sample.Proposal.Create qualified as Create import Sample.Proposal.UnlockStake qualified as UnlockStake import Sample.Proposal.Vote qualified as Vote -import Sample.Shared qualified as Shared (proposal) import Test.Specification ( SpecificationTree, group, - policySucceedsWith, ) -- | Stake specs. specs :: [SpecificationTree] specs = [ group - "policy" - [ policySucceedsWith - "proposalCreation" - (proposalPolicy Shared.proposal.governorSTAssetClass) - () - Proposal.proposalCreation + "policy (proposal creation)" + [ Create.mkTestTree + "legal" + Create.totallyValidParameters + True + True + True + , group + "illegal" + [ Create.mkTestTree + "invalid next proposal id" + Create.invalidOutputGovernorDatumParameters + True + False + True + , Create.mkTestTree + "use other's stake" + Create.useStakeOwnBySomeoneElseParameters + True + False + False + , Create.mkTestTree + "altered stake" + Create.invalidOutputStakeParameters + True + False + False + , Create.mkTestTree + "invalid stake locks" + Create.addInvalidLocksParameters + True + False + True + , Create.mkTestTree + "has reached maximum proposals limit" + Create.exceedMaximumProposalsParameters + True + False + True + , Create.mkTestTree + "loose time range" + Create.timeRangeNotTightParameters + True + False + True + , Create.mkTestTree + "open time range" + Create.timeRangeNotClosedParameters + True + False + True + , group "invalid proposal status" $ + map + ( \ps -> + Create.mkTestTree + (show ps.proposalStatus) + ps + True + False + True + ) + Create.invalidProposalStatusParameters + ] ] , group "validator" @@ -166,110 +216,92 @@ specs = pure $ Advance.mkTestTree name ps False ] in [draftGroup, legalGroup, illegalGroup] - , group "unlocking" $ do - proposalCount <- [1, 42] + , group "unlocking" $ + let proposalCountCases = [1, 5, 10, 42] - let legalGroup = group "legal" $ do - let voterRetractVotesAndUnlockStakeWhileVoting = - UnlockStake.mkProposalValidatorTestCase - UnlockStake.UnlockStakeParameters - { UnlockStake.proposalCount = proposalCount - , UnlockStake.stakeUsage = UnlockStake.Voter - , UnlockStake.retractVotes = True - , UnlockStake.proposalStatus = VotingReady - } - True - creatorUnlockStakeWhileFinished = - UnlockStake.mkProposalValidatorTestCase - UnlockStake.UnlockStakeParameters - { UnlockStake.proposalCount = proposalCount - , UnlockStake.stakeUsage = UnlockStake.Creator - , UnlockStake.retractVotes = False - , UnlockStake.proposalStatus = Finished - } - True + mkSubgroupName nProposals = "with " <> show nProposals <> " proposals" - let voterUnlockStakeAfterVoting = group "voter unlocks stake after voting" $ do - status <- [Finished, Locked] - - pure $ - UnlockStake.mkProposalValidatorTestCase - UnlockStake.UnlockStakeParameters - { UnlockStake.proposalCount = proposalCount - , UnlockStake.stakeUsage = UnlockStake.Voter - , UnlockStake.retractVotes = False - , UnlockStake.proposalStatus = status - } - True - - [ voterRetractVotesAndUnlockStakeWhileVoting - , creatorUnlockStakeWhileFinished - , voterUnlockStakeAfterVoting + mkLegalGroup nProposals = + group + (mkSubgroupName nProposals) + [ UnlockStake.mkTestTree + "voter: retract votes while voting" + (UnlockStake.mkVoterRetractVotesWhileVotingParameters nProposals) + True + , UnlockStake.mkTestTree + "voter/creator: retract votes while voting" + (UnlockStake.mkVoterCreatorRetractVotesWhileVotingParameters nProposals) + True + , UnlockStake.mkTestTree + "creator: remove creator locks when finished" + (UnlockStake.mkCreatorRemoveCreatorLocksWhenFinishedParameters nProposals) + True + , UnlockStake.mkTestTree + "voter/creator: remove all locks when finished" + (UnlockStake.mkVoterCreatorRemoveAllLocksWhenFinishedParameters nProposals) + True + , group "voter: unlock after voting" $ + map + ( \ps -> + let name = show ps.proposalStatus + in UnlockStake.mkTestTree name ps True + ) + (UnlockStake.mkVoterUnlockStakeAfterVotingParameters nProposals) + , UnlockStake.mkTestTree + "voter/creator: remove vote locks when locked" + (UnlockStake.mkVoterCreatorRemoveVoteLocksWhenLockedParameters nProposals) + True ] - let illegalGroup = group "illegal" $ do - let retractsVotesWhileNotVotingReady = - group "voter retracts votes while not voting" $ do - status <- [Draft, Locked, Finished] - - pure $ - UnlockStake.mkProposalValidatorTestCase - UnlockStake.UnlockStakeParameters - { UnlockStake.proposalCount = proposalCount - , UnlockStake.stakeUsage = UnlockStake.Voter - , UnlockStake.retractVotes = True - , UnlockStake.proposalStatus = status - } - False - - unlockIrrelevantStake = - group "unlock an irrelevant stake" $ do - status <- [Draft, VotingReady, Locked, Finished] - shouldRetractVotes <- [True, False] - - pure $ - UnlockStake.mkProposalValidatorTestCase - UnlockStake.UnlockStakeParameters - { UnlockStake.proposalCount = proposalCount - , UnlockStake.stakeUsage = UnlockStake.Irrelevant - , UnlockStake.retractVotes = shouldRetractVotes - , UnlockStake.proposalStatus = status - } - False - - unlockCreatorStakeBeforeFinished = - group "unlock creator stake before finished" $ do - status <- [Draft, VotingReady, Locked] - - pure $ - UnlockStake.mkProposalValidatorTestCase - UnlockStake.UnlockStakeParameters - { UnlockStake.proposalCount = proposalCount - , UnlockStake.stakeUsage = UnlockStake.Creator - , UnlockStake.retractVotes = False - , UnlockStake.proposalStatus = status - } - False - retractVotesWithCreatorStake = - group "creator stake retracts votes" $ do - status <- [Draft, VotingReady, Locked, Finished] - - pure $ - UnlockStake.mkProposalValidatorTestCase - UnlockStake.UnlockStakeParameters - { UnlockStake.proposalCount = proposalCount - , UnlockStake.stakeUsage = UnlockStake.Creator - , UnlockStake.retractVotes = True - , UnlockStake.proposalStatus = status - } - False - - [ retractsVotesWhileNotVotingReady - , unlockIrrelevantStake - , unlockCreatorStakeBeforeFinished - , retractVotesWithCreatorStake + mkIllegalGroup nProposals = + group + (mkSubgroupName nProposals) + [ group "retract votes while not voting" $ + map + ( \ps -> + let name = + "role: " <> show ps.stakeRole + <> ", status: " + <> show ps.proposalStatus + in UnlockStake.mkTestTree name ps False + ) + (UnlockStake.mkRetractVotesWhileNotVoting nProposals) + , group "unlock an irrelevant stake" $ + map + ( \ps -> + let name = + "status: " <> show ps.proposalStatus + <> "retract votes: " + <> show ps.retractVotes + in UnlockStake.mkTestTree name ps False + ) + (UnlockStake.mkUnockIrrelevantStakeParameters nProposals) + , group "remove creator too early" $ + map + ( \ps -> + let name = + "status: " <> show ps.proposalStatus + in UnlockStake.mkTestTree name ps False + ) + (UnlockStake.mkRemoveCreatorLockBeforeFinishedParameters nProposals) + , UnlockStake.mkTestTree + "creator: retract votes" + (UnlockStake.mkRetractVotesWithCreatorStakeParamaters nProposals) + False + , group "alter output stake datum" $ + map + ( \ps -> + let name = + "role: " <> show ps.stakeRole + <> ", status: " + <> show ps.proposalStatus + in UnlockStake.mkTestTree name ps False + ) + (UnlockStake.mkAlterStakeParameters nProposals) ] - [legalGroup, illegalGroup] + legalGroup = group "legal" $ map mkLegalGroup proposalCountCases + illegalGroup = group "illegal" $ map mkIllegalGroup proposalCountCases + in [legalGroup, illegalGroup] ] ] diff --git a/agora-testlib/Test/Specification.hs b/agora-testlib/Test/Specification.hs index e228a68..2594b01 100644 --- a/agora-testlib/Test/Specification.hs +++ b/agora-testlib/Test/Specification.hs @@ -42,6 +42,8 @@ module Test.Specification ( validatorFailsWith, effectSucceedsWith, effectFailsWith, + testValidator, + testPolicy, -- * Converters toTestTree, @@ -253,3 +255,37 @@ effectFailsWith :: ScriptContext -> SpecificationTree effectFailsWith tag eff datum = validatorFailsWith tag eff datum () + +testValidator :: + ( PLift datum + , PlutusTx.ToData (PLifted datum) + , PLift redeemer + , PlutusTx.ToData (PLifted redeemer) + ) => + -- | Should the validator pass? + Bool -> + String -> + ClosedTerm PValidator -> + PLifted datum -> + PLifted redeemer -> + ScriptContext -> + SpecificationTree +testValidator isValid = + if isValid + then validatorSucceedsWith + else validatorFailsWith + +testPolicy :: + ( PLift redeemer + , PlutusTx.ToData (PLifted redeemer) + ) => + Bool -> + String -> + ClosedTerm PMintingPolicy -> + PLifted redeemer -> + ScriptContext -> + SpecificationTree +testPolicy isValid = + if isValid + then policySucceedsWith + else policyFailsWith diff --git a/agora-testlib/Test/Util.hs b/agora-testlib/Test/Util.hs index b610dd7..3fffdc2 100644 --- a/agora-testlib/Test/Util.hs +++ b/agora-testlib/Test/Util.hs @@ -95,12 +95,16 @@ updateMap f k = -------------------------------------------------------------------------------- +-- | Sort the given 'AssocMap.Map' by keys in ascending order. sortMap :: forall k v. Ord k => AssocMap.Map k v -> AssocMap.Map k v sortMap = AssocMap.fromList . sortOn fst . AssocMap.toList +{- | Sort the given 'Value' in ascending order. Some plutarch functions that + work with plutarch's 'Sorted' 'PMap' require this to work correctly. +-} sortValue :: Value -> Value sortValue = Value diff --git a/agora.cabal b/agora.cabal index 08f9e99..cb0283c 100644 --- a/agora.cabal +++ b/agora.cabal @@ -188,9 +188,9 @@ library agora-specs Sample.Effect.GovernorMutation Sample.Effect.TreasuryWithdrawal Sample.Governor - Sample.Proposal Sample.Proposal.Advance Sample.Proposal.Cosign + Sample.Proposal.Create Sample.Proposal.Shared Sample.Proposal.UnlockStake Sample.Proposal.Vote diff --git a/agora/Agora/Governor.hs b/agora/Agora/Governor.hs index 6a6771c..7378fe7 100644 --- a/agora/Agora/Governor.hs +++ b/agora/Agora/Governor.hs @@ -73,6 +73,9 @@ data GovernorDatum = GovernorDatum -- Will get copied over upon the creation of proposals. , createProposalTimeRangeMaxWidth :: MaxTimeRangeWidth -- ^ The maximum valid duration of a transaction that creats a proposal. + , maximumProposalsPerStake :: Integer + -- ^ The maximum number of unfinished proposals that a stake is allowed to be + -- associated to. } deriving stock (Show, GHC.Generic) @@ -149,6 +152,7 @@ newtype PGovernorDatum (s :: S) = PGovernorDatum , "nextProposalId" ':= PProposalId , "proposalTimings" ':= PProposalTimingConfig , "createProposalTimeRangeMaxWidth" ':= PMaxTimeRangeWidth + , "maximumProposalsPerStake" ':= PInteger ] ) } diff --git a/agora/Agora/Governor/Scripts.hs b/agora/Agora/Governor/Scripts.hs index 2b5533e..6f8c778 100644 --- a/agora/Agora/Governor/Scripts.hs +++ b/agora/Agora/Governor/Scripts.hs @@ -46,8 +46,6 @@ import Agora.Governor ( ) import Agora.Proposal ( PProposalDatum (..), - PProposalId (..), - PResultTag, Proposal (..), ProposalStatus (Draft, Finished, Locked), pemptyVotesFor, @@ -65,6 +63,7 @@ import Agora.Stake ( PProposalLock (..), PStakeDatum (..), Stake (..), + pnumCreatedProposals, ) import Agora.Stake.Scripts ( stakePolicy, @@ -108,7 +107,6 @@ import Plutarch.Api.V1.ScriptContext (pfindTxInByTxOutRef, pisUTXOSpent, ptryFin import "liqwid-plutarch-extra" Plutarch.Api.V1.Value (psymbolValueOf) import Plutarch.Extra.IsData (pmatchEnumFromData) import Plutarch.Extra.Map ( - pkeys, plookup, plookup', ) @@ -300,6 +298,7 @@ governorValidator gov = , "nextProposalId" , "proposalTimings" , "createProposalTimeRangeMaxWidth" + , "maximumProposalsPerStake" ] oldGovernorDatum @@ -341,6 +340,8 @@ governorValidator gov = .& #proposalTimings .= oldGovernorDatumF.proposalTimings .& #createProposalTimeRangeMaxWidth .= oldGovernorDatumF.createProposalTimeRangeMaxWidth + .& #maximumProposalsPerStake + .= oldGovernorDatumF.maximumProposalsPerStake ) pguardC "Unexpected governor state datum" $ newGovernorDatum #== expectedNewDatum @@ -377,6 +378,10 @@ governorValidator gov = stakeInputDatumF <- pletFieldsC @["stakedAmount", "owner", "lockedBy"] stakeInputDatum + pguardC "Proposals created by the stake must not exceed the number stored in the governor." $ + pnumCreatedProposals # stakeInputDatumF.lockedBy + #< oldGovernorDatumF.maximumProposalsPerStake + pguardC "Required amount of stake GTs should be presented" $ stakeInputDatumF.stakedAmount #== (pgtValueOf # stakeInputF.value) @@ -479,25 +484,14 @@ governorValidator gov = mustBePJust # "Stake output not found" #$ ptryFindDatum # stakeOutputDatumHash # txInfoF.datums -- The stake should be locked by the newly created proposal. - - let possibleVoteResults = pkeys #$ pto $ pfromData proposalOutputDatum.votes - - mkProposalLock :: Term _ (PProposalId :--> PAsData PResultTag :--> PAsData PProposalLock) - mkProposalLock = - phoistAcyclic $ - plam - ( \pid rt' -> - pdata $ - mkRecordConstr - PProposalLock - ( #vote .= rt' .& #proposalTag .= pdata pid - ) - ) + let newLock = + mkRecordConstr + PCreated + ( #created .= oldGovernorDatumF.nextProposalId + ) -- Append new locks to existing locks - expectedProposalLocks = - pconcat # stakeInputDatumF.lockedBy - #$ pmap # (mkProposalLock # proposalOutputDatum.proposalId) # possibleVoteResults + expectedProposalLocks = pcons # pdata newLock # stakeInputDatumF.lockedBy expectedStakeOutputDatum = pdata $ diff --git a/agora/Agora/Proposal.hs b/agora/Agora/Proposal.hs index 922c3ac..45758d4 100644 --- a/agora/Agora/Proposal.hs +++ b/agora/Agora/Proposal.hs @@ -68,6 +68,7 @@ import Plutarch.Lift ( PUnsafeLiftDecl (..), ) import Plutarch.SafeMoney (PDiscrete) +import Plutarch.Show (PShow (..)) import PlutusLedgerApi.V1 (DatumHash, PubKeyHash, ValidatorHash) import PlutusLedgerApi.V1.Value (AssetClass) import PlutusTx qualified @@ -328,7 +329,7 @@ data ProposalRedeemer -- This list should be sorted in ascending order. Cosign [PubKeyHash] | -- | Allow unlocking one or more stakes with votes towards particular 'ResultTag'. - Unlock ResultTag + Unlock | -- | Advance the proposal, performing the required checks for whether that is legal. -- -- These are roughly the checks for each possible transition: @@ -408,6 +409,8 @@ newtype PResultTag (s :: S) = PResultTag (Term s PInteger) PEq , -- | @since 0.1.0 POrd + , -- | @since 0.2.0 + PShow ) via (DerivePNewtype PResultTag PInteger) @@ -440,6 +443,8 @@ newtype PProposalId (s :: S) = PProposalId (Term s PInteger) PEq , -- | @since 0.1.0 POrd + , -- | @since 0.2.0 + PShow ) via (DerivePNewtype PProposalId PInteger) @@ -665,7 +670,7 @@ deriving via (DerivePConstantViaDataList ProposalDatum PProposalDatum) instance data PProposalRedeemer (s :: S) = PVote (Term s (PDataRecord '["resultTag" ':= PResultTag])) | PCosign (Term s (PDataRecord '["newCosigners" ':= PBuiltinList (PAsData PPubKeyHash)])) - | PUnlock (Term s (PDataRecord '["resultTag" ':= PResultTag])) + | PUnlock (Term s (PDataRecord '[])) | PAdvanceProposal (Term s (PDataRecord '[])) deriving stock ( -- | @since 0.1.0 diff --git a/agora/Agora/Proposal/Scripts.hs b/agora/Agora/Proposal/Scripts.hs index 85ec3f1..e6424b2 100644 --- a/agora/Agora/Proposal/Scripts.hs +++ b/agora/Agora/Proposal/Scripts.hs @@ -29,8 +29,12 @@ import Agora.Proposal.Time ( import Agora.Stake ( PProposalLock (..), PStakeDatum (..), - PStakeUsage (..), - pgetStakeUsage, + pextractVoteOption, + pgetStakeRole, + pisCreator, + pisIrrelevant, + pisPureCreator, + pisVoter, ) import Agora.Utils ( getMintingPolicySymbol, @@ -469,12 +473,7 @@ proposalValidator proposal = -- Ensure that no lock with the current proposal id has been put on the stake. pguardC "Same stake shouldn't vote on the same proposal twice" $ - pnot #$ pany - # plam - ( \((pfield @"proposalTag" #) . pfromData -> pid) -> - pid #== proposalF.proposalId - ) - # pfromData stakeInF.lockedBy + pnot #$ pisVoter #$ pgetStakeRole # proposalF.proposalId # pfromData stakeInF.lockedBy let -- The amount of new votes should be the 'stakedAmount'. -- Update the vote counter of the proposal, and leave other stuff as is. @@ -510,9 +509,9 @@ proposalValidator proposal = let newProposalLock = mkRecordConstr - PProposalLock - ( #vote .= pdata voteFor - .& #proposalTag .= proposalF.proposalId + PVoted + ( #votedOn .= proposalF.proposalId + .& #votedFor .= pdata voteFor ) -- Prepend the new lock to existing locks expectedProposalLocks = @@ -533,30 +532,12 @@ proposalValidator proposal = ---------------------------------------------------------------------- - PUnlock r -> withSingleStake $ \stakeInF stakeOut _ -> do - -- At draft stage, the votes should be empty. - pguardC "Shouldn't retract votes from a draft proposal" $ - pnot #$ currentStatus #== pconstant Draft + PUnlock _ -> withSingleStake $ \stakeInF stakeOut _ -> do + stakeRole <- pletC $ pgetStakeRole # proposalF.proposalId # stakeInF.lockedBy - -- This is the vote option we're retracting from. - retractFrom <- pletC $ pfield @"resultTag" # r + pguardC "Stake input should be relevant" $ + pnot #$ pisIrrelevant # stakeRole - -- Determine if the input stake is actually locked by this proposal. - stakeUsage <- pletC $ pgetStakeUsage # stakeInF.lockedBy # proposalF.proposalId - - pguardC "Stake input relevant" $ - pmatch stakeUsage $ \case - PDidNothing -> - ptraceIfFalse "Stake should be relevant" $ - pconstant False - PCreated -> - ptraceIfFalse "Removing creator's locks means status is Finished" $ - currentStatus #== pconstant Finished - PVotedFor rt -> - ptraceIfFalse "Result tag should match the one given in the redeemer" $ - rt #== retractFrom - - -- The count of removing votes is equal to the 'stakeAmount' of input stake. retractCount <- pletC $ pmatch stakeInF.stakedAmount $ \(PDiscrete v) -> pextract # v @@ -564,13 +545,34 @@ proposalValidator proposal = -- The votes can only change when the proposal still allows voting. let shouldUpdateVotes = currentStatus #== pconstant VotingReady - #&& pnot # (pcon PCreated #== stakeUsage) + #&& pnot # (pisPureCreator # stakeRole) + + allowRemovingCreatorLock = + currentStatus #== pconstant Finished + + isCreator = pisCreator # stakeRole + + -- If the stake has been used for creating the proposal, + -- the creator lock can only be removed when the proposal + -- is finished. + -- + -- In other cases, all the locks related to this + -- proposal should be removed. + validateOutputLocks = plam $ \locks -> + plet + ( pgetStakeRole # proposalF.proposalId # locks + ) + $ \newStakeRole -> + pif + (isCreator #&& pnot # allowRemovingCreatorLock) + (pisPureCreator # newStakeRole) + (pisIrrelevant # newStakeRole) pguardC "Proposal output correct" $ pif shouldUpdateVotes ( let -- Remove votes and leave other parts of the proposal as it. - expectedVotes = pretractVotes # retractFrom # retractCount # proposalF.votes + expectedVotes = pretractVotes # (pextractVoteOption # stakeRole) # retractCount # proposalF.votes expectedProposalOut = mkRecordConstr @@ -598,15 +600,14 @@ proposalValidator proposal = PStakeDatum ( #stakedAmount .= stakeInF.stakedAmount .& #owner .= stakeInF.owner - .& #lockedBy .= stakeOutputLocks + .& #lockedBy .= pdata stakeOutputLocks ) pguardC "Only locks updated in the output stake" $ templateStakeOut #== stakeOut pguardC "All relevant locks removed from the stake" $ - pgetStakeUsage # pfromData stakeOutputLocks - # proposalF.proposalId #== pcon PDidNothing + validateOutputLocks # stakeOutputLocks pure $ pconstant () diff --git a/agora/Agora/Stake.hs b/agora/Agora/Stake.hs index b5f5e16..cb14229 100644 --- a/agora/Agora/Stake.hs +++ b/agora/Agora/Stake.hs @@ -18,11 +18,17 @@ module Agora.Stake ( PStakeDatum (..), PStakeRedeemer (..), PProposalLock (..), - PStakeUsage (..), + PStakeRole (..), -- * Utility functions - stakeLocked, - pgetStakeUsage, + pstakeLocked, + pnumCreatedProposals, + pextractVoteOption, + pgetStakeRole, + pisVoter, + pisCreator, + pisPureCreator, + pisIrrelevant, ) where import Agora.Plutarch.Orphans () @@ -43,14 +49,15 @@ import Plutarch.Extra.IsData ( DerivePConstantViaDataList (..), ProductIsData (ProductIsData), ) -import Plutarch.Extra.List (pmapMaybe, pnotNull) +import Plutarch.Extra.List (pnotNull) import Plutarch.Extra.Other (DerivePNewtype' (..)) -import Plutarch.Extra.TermCont (pletFieldsC) import Plutarch.Lift (PConstantDecl, PUnsafeLiftDecl (..)) import Plutarch.SafeMoney (PDiscrete) +import Plutarch.Show (PShow (..)) import PlutusLedgerApi.V1 (PubKeyHash) import PlutusLedgerApi.V1.Value (AssetClass) import PlutusTx qualified +import Prelude ((+)) import Prelude hiding (Num (..)) -------------------------------------------------------------------------------- @@ -69,8 +76,7 @@ data Stake = Stake GHC.Generic ) -{- | A lock placed on a Stake datum in order to prevent - depositing and withdrawing when votes are in place. +{- | Locks that are stored in the stake datums for various purposes. NOTE: Due to retracting votes always being possible, this lock will only lock with contention on the proposal. @@ -97,30 +103,45 @@ data Stake = Stake @since 0.1.0 -} -data ProposalLock = ProposalLock - { vote :: ResultTag - -- ^ What was voted on. This allows retracting votes to - -- undo their vote. - , proposalId :: ProposalId - -- ^ Identifies the proposal. See 'ProposalId' for further - -- comments on its significance. - } +data ProposalLock + = -- | The stake was used to create a proposal. + -- + -- This kind of lock is placed upon the creation of a proposal, in order + -- to limit creation of proposals per stake. + -- + -- See also: https://github.com/Liqwid-Labs/agora/issues/68 + -- + -- @since 0.2.0 + Created + ProposalId + -- ^ The identifier of the proposal. + | -- | The stake was used to vote on a proposal. + -- + -- This kind of lock is placed while voting on a propsoal, in order to + -- prevent depositing and withdrawing when votes are in place. + -- + -- @since 0.2.0 + Voted + ProposalId + -- ^ The identifier of the proposal. + ResultTag + -- ^ The option which was voted on. This allows votes to be retracted. deriving stock ( -- | @since 0.1.0 Show , -- | @since 0.1.0 GHC.Generic ) - deriving anyclass (Generic) - deriving + deriving anyclass ( -- | @since 0.1.0 - PlutusTx.ToData - , -- | @since 0.1.0 - PlutusTx.FromData - , -- | @since 0.1.0 - PlutusTx.UnsafeFromData + Generic ) - via (ProductIsData ProposalLock) + +PlutusTx.makeIsDataIndexed + ''ProposalLock + [ ('Created, 0) + , ('Voted, 1) + ] {- | Haskell-level redeemer for Stake scripts. @@ -138,12 +159,12 @@ data StakeRedeemer -- This needs to be done in sync with casting a vote, otherwise -- it's possible for a lock to be permanently placed on the stake, -- and then the funds are lost. - PermitVote ProposalLock + PermitVote | -- | Retract a vote, removing it from the 'lockedBy' field. See 'ProposalLock'. -- This action checks for permission of the 'Agora.Proposal.Proposal'. Finished proposals are -- always allowed to have votes retracted and won't affect the Proposal datum, -- allowing 'Stake's to be unlocked. - RetractVotes [ProposalLock] + RetractVotes | -- | The owner can consume stake if nothing is changed about it. -- If the proposal token moves, this is equivalent to the owner consuming it. WitnessStake @@ -165,7 +186,7 @@ PlutusTx.makeIsDataIndexed data StakeDatum = StakeDatum { stakedAmount :: Tagged GTTag Integer -- ^ Tracks the amount of governance token staked in the datum. - -- This also acts as the voting weight for 'Agora.Proposal.Proposal's. + -- This also acts as the voting weight for 'Agora.Proposal.Proposal's. , owner :: PubKeyHash -- ^ The hash of the public key this stake belongs to. -- @@ -173,7 +194,7 @@ data StakeDatum = StakeDatum -- https://github.com/Liqwid-Labs/agora/issues/45 , lockedBy :: [ProposalLock] -- ^ The current proposals locking this stake. This field must be empty - -- for the stake to be usable for deposits and withdrawals. + -- for the stake to be usable for deposits and withdrawals. } deriving stock (Show, GHC.Generic) deriving anyclass (Generic) @@ -227,13 +248,20 @@ newtype PStakeDatum (s :: S) = PStakeDatum via (DerivePNewtype' PStakeDatum) -- | @since 0.1.0 -instance Plutarch.Lift.PUnsafeLiftDecl PStakeDatum where type PLifted PStakeDatum = StakeDatum +instance Plutarch.Lift.PUnsafeLiftDecl PStakeDatum where + type PLifted PStakeDatum = StakeDatum -- | @since 0.1.0 -deriving via (DerivePConstantViaDataList StakeDatum PStakeDatum) instance (Plutarch.Lift.PConstantDecl StakeDatum) +deriving via + (DerivePConstantViaDataList StakeDatum PStakeDatum) + instance + (Plutarch.Lift.PConstantDecl StakeDatum) -- | @since 0.1.0 -deriving via PAsData (DerivePNewtype' PStakeDatum) instance PTryFrom PData (PAsData PStakeDatum) +deriving via + PAsData (DerivePNewtype' PStakeDatum) + instance + PTryFrom PData (PAsData PStakeDatum) {- | Plutarch-level redeemer for Stake scripts. @@ -244,8 +272,8 @@ data PStakeRedeemer (s :: S) PDepositWithdraw (Term s (PDataRecord '["delta" ':= PDiscrete GTTag])) | -- | Destroy a stake, retrieving its LQ, the minimum ADA and any other assets. PDestroy (Term s (PDataRecord '[])) - | PPermitVote (Term s (PDataRecord '["lock" ':= PProposalLock])) - | PRetractVotes (Term s (PDataRecord '["locks" ':= PBuiltinList (PAsData PProposalLock)])) + | PPermitVote (Term s (PDataRecord '[])) + | PRetractVotes (Term s (PDataRecord '[])) | PWitnessStake (Term s (PDataRecord '[])) deriving stock ( -- | @since 0.1.0 @@ -267,65 +295,37 @@ data PStakeRedeemer (s :: S) ) via PIsDataReprInstances PStakeRedeemer +-- | @since 0.1.0 deriving via PAsData (PIsDataReprInstances PStakeRedeemer) instance PTryFrom PData (PAsData PStakeRedeemer) -instance Plutarch.Lift.PUnsafeLiftDecl PStakeRedeemer where type PLifted PStakeRedeemer = StakeRedeemer -deriving via (DerivePConstantViaData StakeRedeemer PStakeRedeemer) instance (Plutarch.Lift.PConstantDecl StakeRedeemer) +-- | @since 0.1.0 +instance Plutarch.Lift.PUnsafeLiftDecl PStakeRedeemer where + type PLifted PStakeRedeemer = StakeRedeemer + +-- | @since 0.1.0 +deriving via + (DerivePConstantViaData StakeRedeemer PStakeRedeemer) + instance + (Plutarch.Lift.PConstantDecl StakeRedeemer) {- | Plutarch-level version of 'ProposalLock'. - @since 0.1.0 + @since 0.2.0 -} -newtype PProposalLock (s :: S) = PProposalLock - { getProposalLock :: - Term - s - ( PDataRecord - '[ "vote" ':= PResultTag - , "proposalTag" ':= PProposalId - ] - ) - } - deriving stock (GHC.Generic) - deriving anyclass (Generic) - deriving anyclass (PIsDataRepr) - deriving - (PlutusType, PIsData, PDataFields, PEq) - via (DerivePNewtype' PProposalLock) - -deriving via - PAsData (DerivePNewtype' PProposalLock) - instance - PTryFrom PData (PAsData PProposalLock) - -instance Plutarch.Lift.PUnsafeLiftDecl PProposalLock where type PLifted PProposalLock = ProposalLock -deriving via (DerivePConstantViaDataList ProposalLock PProposalLock) instance (Plutarch.Lift.PConstantDecl ProposalLock) - --------------------------------------------------------------------------------- - -{- | Check whether a Stake is locked. If it is locked, various actions are unavailable. - - @since 0.1.0 --} -stakeLocked :: forall (s :: S). Term s (PStakeDatum :--> PBool) -stakeLocked = phoistAcyclic $ - plam $ \stakeDatum -> - let locks :: Term _ (PBuiltinList (PAsData PProposalLock)) - locks = pfield @"lockedBy" # stakeDatum - in pnotNull # locks - -{- | Represent the usage of a stake on a particular proposal. - A stake can be used to either create or vote on a proposal. - - @since 0.1.0 --} -data PStakeUsage (s :: S) - = PVotedFor (Term s PResultTag) - | PCreated - | PDidNothing +data PProposalLock (s :: S) + = PCreated (Term s (PDataRecord '["created" ':= PProposalId])) + | PVoted + ( Term + s + ( PDataRecord + '[ "votedOn" ':= PProposalId + , "votedFor" ':= PResultTag + ] + ) + ) deriving stock ( -- | @since 0.1.0 GHC.Generic @@ -334,51 +334,212 @@ data PStakeUsage (s :: S) ( -- | @since 0.1.0 Generic , -- | @since 0.1.0 + HasDatatypeInfo + ) + deriving anyclass + ( -- | @since 0.1.0 + PIsDataRepr + ) + deriving + ( -- | @since 0.1.0 PlutusType , -- | @since 0.1.0 - HasDatatypeInfo + PIsData , -- | @since 0.1.0 PEq ) + via (PIsDataReprInstances PProposalLock) -{- | / O(n) /.Return the usage of a stake on a particular proposal, - given the 'lockedBy' field of a stake and the target proposal. +-- | @since 0.1.0 +deriving via + PAsData (PIsDataReprInstances PProposalLock) + instance + PTryFrom PData (PAsData PProposalLock) - @since 0.1.0 +-- | @since 0.1.0 +instance Plutarch.Lift.PUnsafeLiftDecl PProposalLock where + type PLifted PProposalLock = ProposalLock + +-- | @since 0.1.0 +deriving via + (DerivePConstantViaData ProposalLock PProposalLock) + instance + (Plutarch.Lift.PConstantDecl ProposalLock) + +-- | @since 0.2.0 +instance PShow PProposalLock where + pshow' :: Bool -> Term s PProposalLock -> Term s PString + pshow' True x = "(" <> pshow' False x <> ")" + pshow' False lock = pmatch lock $ \case + PCreated ((pfield @"created" #) -> pid) -> "PCreated " <> pshow' True pid + PVoted x -> pletFields @'["votedOn", "votedFor"] x $ \xF -> + "PVoted " <> pshow' True xF.votedOn <> " " <> pshow' True xF.votedFor + +-------------------------------------------------------------------------------- + +{- | Check whether a Stake is locked. If it is locked, various actions are unavailable. + + @since 0.2.0 -} -pgetStakeUsage :: - Term - _ - ( PBuiltinList (PAsData PProposalLock) - :--> PProposalId - :--> PStakeUsage +pstakeLocked :: forall (s :: S). Term s (PStakeDatum :--> PBool) +pstakeLocked = phoistAcyclic $ + plam $ \stakeDatum -> + let locks :: Term _ (PBuiltinList (PAsData PProposalLock)) + locks = pfield @"lockedBy" # stakeDatum + in pnotNull # locks + +{- | Get the number of *alive* proposals that were created by the given stake. + + @since 0.2.0 +-} +pnumCreatedProposals :: Term s (PBuiltinList (PAsData PProposalLock) :--> PInteger) +pnumCreatedProposals = + phoistAcyclic $ + plam $ \l -> + pfoldl + # phoistAcyclic + ( plam + ( \c (pfromData -> lock) -> + c + + pmatch + lock + ( \case + PCreated _ -> 1 + _ -> 0 + ) + ) + ) + # 0 + # l + +{- | The role of a stake for a particular proposal. Scott-encoded. + + @since 0.2.0 +-} +data PStakeRole (s :: S) + = -- | The stake was used to vote on the proposal. + PVoter + (Term s PResultTag) + -- ^ The option which was voted for. + | -- | The stake was used to create the propsoal. + PCreator + | -- | The stake was used to both create and vote on the proposal. + PBoth + (Term s PResultTag) + -- ^ The option which was voted for. + | -- | The stake has nothing to do with the given propsoal. + PIrrelevant + deriving stock + ( -- | @since 0.2.0 + GHC.Generic + ) + deriving anyclass + ( -- | @since 0.2.0 + Generic + , -- | @since 0.2.0 + PlutusType + , -- | @since 0.2.0 + HasDatatypeInfo + , -- | @since 0.2.0 + PEq ) -pgetStakeUsage = phoistAcyclic $ - plam $ \locks pid -> - let -- All locks from the given proposal. - filteredLocks = - pmapMaybe - # plam - ( \lock'@(pfromData -> lock) -> unTermCont $ do - lockF <- pletFieldsC @'["proposalTag"] lock - pure $ +{- | Retutn true if the stake was used to voted on the proposal. + + @since 0.2.0 +-} +pisVoter :: Term s (PStakeRole :--> PBool) +pisVoter = phoistAcyclic $ + plam $ \sr -> pmatch sr $ \case + PVoter _ -> pconstant True + PBoth _ -> pconstant True + _ -> pconstant False + +{- | Retutn true if the stake was used to create the proposal. + + @since 0.2.0 +-} +pisCreator :: Term s (PStakeRole :--> PBool) +pisCreator = phoistAcyclic $ + plam $ \sr -> pmatch sr $ \case + PCreator -> pconstant True + PBoth _ -> pconstant True + _ -> pconstant False + +{- | Retutn true if the stake was used to create the proposal, but not vote on + the proposal. + + @since 0.2.0 +-} +pisPureCreator :: Term s (PStakeRole :--> PBool) +pisPureCreator = phoistAcyclic $ + plam $ \sr -> pmatch sr $ \case + PCreator -> pconstant True + _ -> pconstant False + +{- | Return true if the stake isn't related to the proposal. + + @since 0.2.0 +-} +pisIrrelevant :: Term s (PStakeRole :--> PBool) +pisIrrelevant = phoistAcyclic $ + plam $ \sr -> pmatch sr $ \case + PIrrelevant -> pconstant True + _ -> pconstant False + +{- | Get the role of a stake for the proposal specified by the poroposal id, + given the 'StakeDatum.lockedBy' field of the stake. + + Note that the list of locks is cosidered valid only if it contains at most + two locks from the given proposal: one voter lock and one creator lock. + + @since 0.2.0 +-} +pgetStakeRole :: Term s (PProposalId :--> PBuiltinList (PAsData PProposalLock) :--> PStakeRole) +pgetStakeRole = phoistAcyclic $ + plam $ \pid locks -> + pfoldl + # plam + ( \role (pfromData -> lock) -> + let thisRole = pmatch lock $ \case + PCreated ((pfield @"created" #) -> pid') -> pif - (lockF.proposalTag #== pid) - (pcon $ PJust lock') - (pcon PNothing) - ) - # locks + (pid' #== pid) + (pcon PCreator) + (pcon PIrrelevant) + PVoted lock' -> pletFields @'["votedOn", "votedFor"] lock' $ \lockF -> + pif + (lockF.votedOn #== pid) + (pcon $ PVoter lockF.votedFor) + (pcon PIrrelevant) + in pcombineStakeRole # thisRole # role + ) + # pcon PIrrelevant + # locks + where + pcombineStakeRole :: Term s (PStakeRole :--> PStakeRole :--> PStakeRole) + pcombineStakeRole = phoistAcyclic $ + plam $ \x y -> + let cannotCombine = ptraceError "duplicate roles" + in pmatch x $ \case + PVoter r -> pmatch y $ \case + PCreator -> pcon $ PBoth r + PIrrelevant -> x + _ -> cannotCombine + PCreator -> pmatch y $ \case + PVoter r -> pcon $ PBoth r + PIrrelevant -> x + _ -> cannotCombine + PBoth _ -> cannotCombine + PIrrelevant -> y - lockCount' = plength # filteredLocks - in plet lockCount' $ \lockCount -> - pif (lockCount #== 0) (pcon PDidNothing) $ - pif - (lockCount #== 1) - ( pcon $ - PVotedFor $ - pfromData $ - pfield @"vote" #$ phead # filteredLocks - ) - -- Note: see the implementation of the governor. - (pcon PCreated) +{- | Get the outcome that was voted for. + + @since 0.2.0 +-} +pextractVoteOption :: Term s (PStakeRole :--> PResultTag) +pextractVoteOption = phoistAcyclic $ + plam $ \sr -> pmatch sr $ \case + PVoter r -> r + PBoth r -> r + _ -> ptraceError "not voter" diff --git a/agora/Agora/Stake/Scripts.hs b/agora/Agora/Stake/Scripts.hs index f84ec33..50a3ddf 100644 --- a/agora/Agora/Stake/Scripts.hs +++ b/agora/Agora/Stake/Scripts.hs @@ -18,7 +18,7 @@ import Agora.Stake ( ), Stake (gtClassRef, proposalSTClass), StakeRedeemer (WitnessStake), - stakeLocked, + pstakeLocked, ) import Agora.Utils ( mustBePJust, @@ -109,7 +109,7 @@ stakePolicy gtClassRef = pif (psymbolValueOf # ownSymbol # txOutF.value #== 1) ( let datum = mustFindDatum' @PStakeDatum # txOutF.datumHash # txInfoF.datums - in pnot # (stakeLocked # datum) + in pnot # (pstakeLocked # datum) ) (pconstant False) ) @@ -263,7 +263,7 @@ stakeValidator stake = spentST <- pletC $ psymbolValueOf # stCurrencySymbol #$ valueSpent -- Is the stake currently locked? - stakeIsLocked <- pletC $ stakeLocked # stakeDatum' + stakeIsLocked <- pletC $ pstakeLocked # stakeDatum' pure $ pmatch stakeRedeemer $ \case @@ -287,7 +287,7 @@ stakeValidator stake = proposalSTClass = passetClass # pconstant propCs # pconstant propTn spentProposalST = passetClassValueOf # valueSpent # proposalSTClass - proposalTokenMoved <- pletC $ spentProposalST #== 1 + proposalTokenMoved <- pletC $ 1 #<= spentProposalST -- Filter out own outputs using own address and ST. ownOutputs <- @@ -371,9 +371,20 @@ stakeValidator stake = pletC $ pdata resolvedF.value #== pdata ownOutputValue + onlyLocksUpdated <- + pletC $ + let templateStakeDatum = + mkRecordConstr + PStakeDatum + ( #stakedAmount .= stakeDatum.stakedAmount + .& #owner .= stakeDatum.owner + .& #lockedBy .= pfield @"lockedBy" # stakeOut + ) + in stakeOut #== templateStakeDatum + pure $ pmatch stakeRedeemer $ \case - PRetractVotes l -> unTermCont $ do + PRetractVotes _ -> unTermCont $ do pguardC "Owner signs this transaction" ownerSignsTransaction @@ -383,18 +394,8 @@ stakeValidator stake = pguardC "Proposal ST spent" proposalTokenMoved pguardC "A UTXO must exist with the correct output" $ - let expectedLocks = pfield @"locks" # l - - expectedDatum = - mkRecordConstr - PStakeDatum - ( #stakedAmount .= stakeDatum.stakedAmount - .& #owner .= stakeDatum.owner - .& #lockedBy .= expectedLocks - ) - - valueCorrect = ownOutputValueUnchanged - outputDatumCorrect = stakeOut #== expectedDatum + let valueCorrect = ownOutputValueUnchanged + outputDatumCorrect = onlyLocksUpdated in foldl1 (#&&) [ ptraceIfFalse "valueCorrect" valueCorrect @@ -405,34 +406,21 @@ stakeValidator stake = ------------------------------------------------------------ - PPermitVote l -> unTermCont $ do + PPermitVote _ -> unTermCont $ do pguardC "Owner signs this transaction" ownerSignsTransaction + let proposalTokenMinted = + passetClassValueOf # txInfoF.mint # proposalSTClass #== 1 + -- This puts trust into the Proposal. The Proposal must necessarily check -- that this is not abused. - pguardC "Proposal ST spent" proposalTokenMoved - - -- Update the stake datum, but only the 'lockedBy' field. - - let -- We actually don't know whether the given lock is valid or not. - -- This is checked in the proposal validator. - newLock = pfield @"lock" # l - -- Prepend the new lock to the existing locks. - expectedLocks = pcons # newLock # stakeDatum.lockedBy - - expectedDatum <- - pletC $ - mkRecordConstr - PStakeDatum - ( #stakedAmount .= stakeDatum.stakedAmount - .& #owner .= stakeDatum.owner - .& #lockedBy .= pdata expectedLocks - ) + pguardC "Proposal ST spent or minted" $ + proposalTokenMoved #|| proposalTokenMinted pguardC "A UTXO must exist with the correct output" $ - let correctOutputDatum = stakeOut #== expectedDatum + let correctOutputDatum = onlyLocksUpdated valueCorrect = ownOutputValueUnchanged in foldl1 (#&&) diff --git a/bench.csv b/bench.csv index 7e8faf7..8dd401c 100644 --- a/bench.csv +++ b/bench.csv @@ -2,69 +2,233 @@ name,cpu,mem,size Agora/Effects/Treasury Withdrawal Effect/effect/Simple,333327612,830203,3674 Agora/Effects/Treasury Withdrawal Effect/effect/Simple with multiple treasuries ,492387542,1197315,3986 Agora/Effects/Treasury Withdrawal Effect/effect/Mixed Assets,456007605,1104500,3859 -Agora/Effects/Governor Mutation Effect/validator/valid new governor datum/governor validator should pass,87839169,243032,8561 -Agora/Effects/Governor Mutation Effect/validator/valid new governor datum/effect validator should pass,106082031,292993,3609 -Agora/Stake/policy/stakeCreation,50939580,148729,2387 -Agora/Stake/validator/stakeDepositWithdraw deposit,150745141,416137,4995 -Agora/Stake/validator/stakeDepositWithdraw withdraw,150745141,416137,4983 -Agora/Proposal/policy/proposalCreation,23140177,69194,1515 -Agora/Proposal/validator/cosignature/legal/with 1 cosigners/propsoal,235408912,657765,8097 -Agora/Proposal/validator/cosignature/legal/with 1 cosigners/stake,125665131,316762,5462 -Agora/Proposal/validator/cosignature/legal/with 5 cosigners/propsoal,680441047,1897008,10727 -Agora/Proposal/validator/cosignature/legal/with 5 cosigners/stake,576106975,1490610,7972 -Agora/Proposal/validator/cosignature/legal/with 10 cosigners/propsoal,1351073436,3706315,14015 -Agora/Proposal/validator/cosignature/legal/with 10 cosigners/stake,1148637636,2982695,11109 -Agora/Proposal/validator/cosignature/illegal/duplicate cosigners/stake,125665131,316762,5462 -Agora/Proposal/validator/cosignature/illegal/proposal status not Draft/with 1 cosigners/status: VotingReady/stake,125665131,316762,5462 -Agora/Proposal/validator/cosignature/illegal/proposal status not Draft/with 1 cosigners/status: Locked/stake,125665131,316762,5462 -Agora/Proposal/validator/cosignature/illegal/proposal status not Draft/with 1 cosigners/status: Finished/stake,125665131,316762,5462 -Agora/Proposal/validator/cosignature/illegal/proposal status not Draft/with 5 cosigners/status: VotingReady/stake,576106975,1490610,7972 -Agora/Proposal/validator/cosignature/illegal/proposal status not Draft/with 5 cosigners/status: Locked/stake,576106975,1490610,7972 -Agora/Proposal/validator/cosignature/illegal/proposal status not Draft/with 5 cosigners/status: Finished/stake,576106975,1490610,7972 -Agora/Proposal/validator/cosignature/illegal/proposal status not Draft/with 10 cosigners/status: VotingReady/stake,1148637636,2982695,11109 -Agora/Proposal/validator/cosignature/illegal/proposal status not Draft/with 10 cosigners/status: Locked/stake,1148637636,2982695,11109 -Agora/Proposal/validator/cosignature/illegal/proposal status not Draft/with 10 cosigners/status: Finished/stake,1148637636,2982695,11109 -Agora/Proposal/validator/voting/legal/propsoal,246896882,688919,8069 -Agora/Proposal/validator/voting/legal/stake,141234659,368136,5481 -Agora/Proposal/validator/advancing/from draft/with 1 cosigner(s)/legal/to next state/propsoal,222376736,631090,8052 -Agora/Proposal/validator/advancing/from draft/with 1 cosigner(s)/legal/to next state/stake,125665131,316762,5459 -Agora/Proposal/validator/advancing/from draft/with 1 cosigner(s)/legal/to failed state/propsoal,217322044,620369,8054 -Agora/Proposal/validator/advancing/from draft/with 1 cosigner(s)/legal/to failed state/stake,125665131,316762,5461 -Agora/Proposal/validator/advancing/from draft/with 1 cosigner(s)/illegal/insufficient cosigns/stake,118020743,304972,5389 -Agora/Proposal/validator/advancing/from draft/with 5 cosigner(s)/legal/to next state/propsoal,614587307,1766683,10875 -Agora/Proposal/validator/advancing/from draft/with 5 cosigner(s)/legal/to next state/stake,548619697,1410656,8162 -Agora/Proposal/validator/advancing/from draft/with 5 cosigner(s)/legal/to failed state/propsoal,239691392,682433,8415 -Agora/Proposal/validator/advancing/from draft/with 5 cosigner(s)/legal/to failed state/stake,125665131,316762,5702 -Agora/Proposal/validator/advancing/from draft/with 5 cosigner(s)/illegal/insufficient cosigns/stake,446252055,1166812,7811 -Agora/Proposal/validator/advancing/from draft/with 10 cosigner(s)/legal/to next state/propsoal,1196289192,3454898,14404 -Agora/Proposal/validator/advancing/from draft/with 10 cosigner(s)/legal/to next state/stake,1201388310,3136405,11540 -Agora/Proposal/validator/advancing/from draft/with 10 cosigner(s)/legal/to failed state/propsoal,267653077,760013,8868 -Agora/Proposal/validator/advancing/from draft/with 10 cosigner(s)/legal/to failed state/stake,125665131,316762,6004 -Agora/Proposal/validator/advancing/from draft/with 10 cosigner(s)/illegal/insufficient cosigns/stake,1025012867,2732083,10837 -Agora/Proposal/validator/advancing/legal/advance to next state/from: VotingReady/propsoal,250229153,709227,8061 -Agora/Proposal/validator/advancing/legal/advance to next state/from: VotingReady/stake,125665131,316762,5466 -Agora/Proposal/validator/advancing/legal/advance to next state/from: Locked/propsoal,238990044,676396,8061 -Agora/Proposal/validator/advancing/legal/advance to next state/from: Locked/stake,125665131,316762,5466 -Agora/Proposal/validator/advancing/legal/advance to failed state/from: VotingReady/propsoal,236162599,670386,8055 -Agora/Proposal/validator/advancing/legal/advance to failed state/from: VotingReady/stake,125665131,316762,5462 -Agora/Proposal/validator/advancing/legal/advance to failed state/from: Locked/propsoal,237293577,672790,8055 -Agora/Proposal/validator/advancing/legal/advance to failed state/from: Locked/stake,125665131,316762,5462 -Agora/Proposal/validator/advancing/illegal/insufficient votes/stake,125665131,316762,5462 -Agora/Proposal/validator/advancing/illegal/initial state is Finished/stake,125665131,316762,5454 -"Agora/Proposal/validator/unlocking/legal/1 proposals, voter, unlock stake + retract votes, VotingReady",245855872,689807,8029 -"Agora/Proposal/validator/unlocking/legal/1 proposals, creator, unlock stake, Finished",215131610,613807,8031 -"Agora/Proposal/validator/unlocking/legal/voter unlocks stake after voting/1 proposals, voter, unlock stake, Finished",212428891,605718,8033 -"Agora/Proposal/validator/unlocking/legal/voter unlocks stake after voting/1 proposals, voter, unlock stake, Locked",212428891,605718,8033 -"Agora/Proposal/validator/unlocking/legal/42 proposals, voter, unlock stake + retract votes, VotingReady",1775520444,5200586,29137 -"Agora/Proposal/validator/unlocking/legal/42 proposals, creator, unlock stake, Finished",1448162043,4319059,29321 -"Agora/Proposal/validator/unlocking/legal/voter unlocks stake after voting/42 proposals, voter, unlock stake, Finished",1340521669,3979526,29305 -"Agora/Proposal/validator/unlocking/legal/voter unlocks stake after voting/42 proposals, voter, unlock stake, Locked",1340521669,3979526,29305 +Agora/Effects/Governor Mutation Effect/validator/valid new governor datum/governor validator should pass,88940927,246756,8891 +Agora/Effects/Governor Mutation Effect/validator/valid new governor datum/effect validator should pass,107090537,296185,3627 +Agora/Stake/policy/stakeCreation,51008580,149029,2522 +Agora/Stake/validator/stakeDepositWithdraw deposit,183506412,498838,4745 +Agora/Stake/validator/stakeDepositWithdraw withdraw,183506412,498838,4733 +Agora/Proposal/policy (proposal creation)/legal/proposal,34975627,103548,2117 +Agora/Proposal/policy (proposal creation)/legal/governor,327971301,871386,9370 +Agora/Proposal/policy (proposal creation)/legal/stake,152415805,398403,5404 +Agora/Proposal/policy (proposal creation)/illegal/invalid next proposal id/proposal,34975627,103548,2117 +Agora/Proposal/policy (proposal creation)/illegal/invalid next proposal id/stake,152415805,398403,5404 +Agora/Proposal/policy (proposal creation)/illegal/use other's stake/proposal,34975627,103548,2086 +Agora/Proposal/policy (proposal creation)/illegal/altered stake/proposal,34975627,103548,2117 +Agora/Proposal/policy (proposal creation)/illegal/invalid stake locks/proposal,34975627,103548,2125 +Agora/Proposal/policy (proposal creation)/illegal/invalid stake locks/stake,157849465,413053,5412 +Agora/Proposal/policy (proposal creation)/illegal/has reached maximum proposals limit/proposal,34975627,103548,2137 +Agora/Proposal/policy (proposal creation)/illegal/has reached maximum proposals limit/stake,158878297,416511,5434 +Agora/Proposal/policy (proposal creation)/illegal/loose time range/proposal,34975627,103548,2117 +Agora/Proposal/policy (proposal creation)/illegal/loose time range/stake,152415805,398403,5404 +Agora/Proposal/policy (proposal creation)/illegal/open time range/proposal,34975627,103548,2113 +Agora/Proposal/policy (proposal creation)/illegal/open time range/stake,152415805,398403,5400 +Agora/Proposal/policy (proposal creation)/illegal/invalid proposal status/VotingReady/proposal,34975627,103548,2117 +Agora/Proposal/policy (proposal creation)/illegal/invalid proposal status/VotingReady/stake,152415805,398403,5404 +Agora/Proposal/policy (proposal creation)/illegal/invalid proposal status/Locked/proposal,34975627,103548,2117 +Agora/Proposal/policy (proposal creation)/illegal/invalid proposal status/Locked/stake,152415805,398403,5404 +Agora/Proposal/policy (proposal creation)/illegal/invalid proposal status/Finished/proposal,34975627,103548,2117 +Agora/Proposal/policy (proposal creation)/illegal/invalid proposal status/Finished/stake,152415805,398403,5404 +Agora/Proposal/validator/cosignature/legal/with 1 cosigners/propsoal,232230876,649441,8097 +Agora/Proposal/validator/cosignature/legal/with 1 cosigners/stake,122255811,317464,5213 +Agora/Proposal/validator/cosignature/legal/with 5 cosigners/propsoal,662894867,1848188,10727 +Agora/Proposal/validator/cosignature/legal/with 5 cosigners/stake,553050999,1459872,7723 +Agora/Proposal/validator/cosignature/legal/with 10 cosigners/propsoal,1315567076,3606875,14015 +Agora/Proposal/validator/cosignature/legal/with 10 cosigners/stake,1101023340,2912657,10860 +Agora/Proposal/validator/cosignature/illegal/duplicate cosigners/stake,122255811,317464,5213 +Agora/Proposal/validator/cosignature/illegal/proposal status not Draft/with 1 cosigners/status: VotingReady/stake,122255811,317464,5213 +Agora/Proposal/validator/cosignature/illegal/proposal status not Draft/with 1 cosigners/status: Locked/stake,122255811,317464,5213 +Agora/Proposal/validator/cosignature/illegal/proposal status not Draft/with 1 cosigners/status: Finished/stake,122255811,317464,5213 +Agora/Proposal/validator/cosignature/illegal/proposal status not Draft/with 5 cosigners/status: VotingReady/stake,553050999,1459872,7723 +Agora/Proposal/validator/cosignature/illegal/proposal status not Draft/with 5 cosigners/status: Locked/stake,553050999,1459872,7723 +Agora/Proposal/validator/cosignature/illegal/proposal status not Draft/with 5 cosigners/status: Finished/stake,553050999,1459872,7723 +Agora/Proposal/validator/cosignature/illegal/proposal status not Draft/with 10 cosigners/status: VotingReady/stake,1101023340,2912657,10860 +Agora/Proposal/validator/cosignature/illegal/proposal status not Draft/with 10 cosigners/status: Locked/stake,1101023340,2912657,10860 +Agora/Proposal/validator/cosignature/illegal/proposal status not Draft/with 10 cosigners/status: Finished/stake,1101023340,2912657,10860 +Agora/Proposal/validator/voting/legal/propsoal,253541830,711367,8079 +Agora/Proposal/validator/voting/legal/stake,139493011,366217,5239 +Agora/Proposal/validator/advancing/from draft/with 1 cosigner(s)/legal/to next state/propsoal,222392288,630302,8060 +Agora/Proposal/validator/advancing/from draft/with 1 cosigner(s)/legal/to next state/stake,122255811,317464,5222 +Agora/Proposal/validator/advancing/from draft/with 1 cosigner(s)/legal/to failed state/propsoal,217337596,619581,8062 +Agora/Proposal/validator/advancing/from draft/with 1 cosigner(s)/legal/to failed state/stake,122255811,317464,5224 +Agora/Proposal/validator/advancing/from draft/with 1 cosigner(s)/illegal/insufficient cosigns/stake,117978393,308804,5152 +Agora/Proposal/validator/advancing/from draft/with 5 cosigner(s)/legal/to next state/propsoal,602596705,1722841,10915 +Agora/Proposal/validator/advancing/from draft/with 5 cosigner(s)/legal/to next state/stake,499559031,1304096,7957 +Agora/Proposal/validator/advancing/from draft/with 5 cosigner(s)/legal/to failed state/propsoal,239706944,681645,8423 +Agora/Proposal/validator/advancing/from draft/with 5 cosigner(s)/legal/to failed state/stake,122255811,317464,5465 +Agora/Proposal/validator/advancing/from draft/with 5 cosigner(s)/illegal/insufficient cosigns/stake,446950999,1172710,7606 +Agora/Proposal/validator/advancing/from draft/with 10 cosigner(s)/legal/to next state/propsoal,1183788938,3402248,14484 +Agora/Proposal/validator/advancing/from draft/with 10 cosigner(s)/legal/to next state/stake,1129251912,2994677,11375 +Agora/Proposal/validator/advancing/from draft/with 10 cosigner(s)/legal/to failed state/propsoal,267668629,759225,8876 +Agora/Proposal/validator/advancing/from draft/with 10 cosigner(s)/legal/to failed state/stake,122255811,317464,5767 +Agora/Proposal/validator/advancing/from draft/with 10 cosigner(s)/illegal/insufficient cosigns/stake,1013080113,2701103,10673 +Agora/Proposal/validator/advancing/legal/advance to next state/from: VotingReady/propsoal,253438293,715975,8069 +Agora/Proposal/validator/advancing/legal/advance to next state/from: VotingReady/stake,122255811,317464,5229 +Agora/Proposal/validator/advancing/legal/advance to next state/from: Locked/propsoal,242199184,683144,8069 +Agora/Proposal/validator/advancing/legal/advance to next state/from: Locked/stake,122255811,317464,5229 +Agora/Proposal/validator/advancing/legal/advance to failed state/from: VotingReady/propsoal,239371739,677134,8063 +Agora/Proposal/validator/advancing/legal/advance to failed state/from: VotingReady/stake,122255811,317464,5225 +Agora/Proposal/validator/advancing/legal/advance to failed state/from: Locked/propsoal,240502717,679538,8063 +Agora/Proposal/validator/advancing/legal/advance to failed state/from: Locked/stake,122255811,317464,5225 +Agora/Proposal/validator/advancing/illegal/insufficient votes/stake,122255811,317464,5225 +Agora/Proposal/validator/advancing/illegal/initial state is Finished/stake,122255811,317464,5217 +Agora/Proposal/validator/unlocking/legal/with 1 proposals/voter: retract votes while voting/stake,128424052,334368,5219 +Agora/Proposal/validator/unlocking/legal/with 1 proposals/voter: retract votes while voting/propsoal,236436652,664524,8060 +Agora/Proposal/validator/unlocking/legal/with 1 proposals/voter/creator: retract votes while voting/stake,131655298,343422,5235 +Agora/Proposal/validator/unlocking/legal/with 1 proposals/voter/creator: retract votes while voting/propsoal,249908859,704466,8071 +Agora/Proposal/validator/unlocking/legal/with 1 proposals/creator: remove creator locks when finished/stake,128424052,334368,5217 +Agora/Proposal/validator/unlocking/legal/with 1 proposals/creator: remove creator locks when finished/propsoal,204228787,584379,8057 +Agora/Proposal/validator/unlocking/legal/with 1 proposals/voter/creator: remove all locks when finished/stake,128424052,334368,5233 +Agora/Proposal/validator/unlocking/legal/with 1 proposals/voter/creator: remove all locks when finished/propsoal,212663044,609283,8069 +Agora/Proposal/validator/unlocking/legal/with 1 proposals/voter: unlock after voting/Locked/stake,128424052,334368,5223 +Agora/Proposal/validator/unlocking/legal/with 1 proposals/voter: unlock after voting/Locked/propsoal,205544939,588263,8064 +Agora/Proposal/validator/unlocking/legal/with 1 proposals/voter: unlock after voting/Finished/stake,128424052,334368,5223 +Agora/Proposal/validator/unlocking/legal/with 1 proposals/voter: unlock after voting/Finished/propsoal,205544939,588263,8064 +Agora/Proposal/validator/unlocking/legal/with 1 proposals/voter/creator: remove vote locks when locked/stake,131655298,343422,5239 +Agora/Proposal/validator/unlocking/legal/with 1 proposals/voter/creator: remove vote locks when locked/propsoal,219548722,629597,8075 +Agora/Proposal/validator/unlocking/legal/with 5 proposals/voter: retract votes while voting/stake,279213296,711004,7303 +Agora/Proposal/validator/unlocking/legal/with 5 proposals/voter: retract votes while voting/propsoal,379077900,1077188,10120 +Agora/Proposal/validator/unlocking/legal/with 5 proposals/voter/creator: retract votes while voting/stake,295369526,756274,7380 +Agora/Proposal/validator/unlocking/legal/with 5 proposals/voter/creator: retract votes while voting/propsoal,444606435,1268442,10172 +Agora/Proposal/validator/unlocking/legal/with 5 proposals/creator: remove creator locks when finished/stake,279213296,711004,7293 +Agora/Proposal/validator/unlocking/legal/with 5 proposals/creator: remove creator locks when finished/propsoal,312309599,902235,10113 +Agora/Proposal/validator/unlocking/legal/with 5 proposals/voter/creator: remove all locks when finished/stake,279213296,711004,7374 +Agora/Proposal/validator/unlocking/legal/with 5 proposals/voter/creator: remove all locks when finished/propsoal,350698580,1013987,10166 +Agora/Proposal/validator/unlocking/legal/with 5 proposals/voter: unlock after voting/Locked/stake,279213296,711004,7324 +Agora/Proposal/validator/unlocking/legal/with 5 proposals/voter: unlock after voting/Locked/propsoal,317778859,917311,10141 +Agora/Proposal/validator/unlocking/legal/with 5 proposals/voter: unlock after voting/Finished/stake,279213296,711004,7324 +Agora/Proposal/validator/unlocking/legal/with 5 proposals/voter: unlock after voting/Finished/propsoal,317778859,917311,10141 +Agora/Proposal/validator/unlocking/legal/with 5 proposals/voter/creator: remove vote locks when locked/stake,295369526,756274,7400 +Agora/Proposal/validator/unlocking/legal/with 5 proposals/voter/creator: remove vote locks when locked/propsoal,383838970,1109957,10192 +Agora/Proposal/validator/unlocking/legal/with 10 proposals/voter: retract votes while voting/stake,467699851,1181799,9909 +Agora/Proposal/validator/unlocking/legal/with 10 proposals/voter: retract votes while voting/propsoal,557379460,1593018,12696 +Agora/Proposal/validator/unlocking/legal/with 10 proposals/voter/creator: retract votes while voting/stake,500012311,1272339,10060 +Agora/Proposal/validator/unlocking/legal/with 10 proposals/voter/creator: retract votes while voting/propsoal,687978405,1973412,12797 +Agora/Proposal/validator/unlocking/legal/with 10 proposals/creator: remove creator locks when finished/stake,467699851,1181799,9888 +Agora/Proposal/validator/unlocking/legal/with 10 proposals/creator: remove creator locks when finished/propsoal,447410614,1299555,12683 +Agora/Proposal/validator/unlocking/legal/with 10 proposals/voter/creator: remove all locks when finished/stake,467699851,1181799,10049 +Agora/Proposal/validator/unlocking/legal/with 10 proposals/voter/creator: remove all locks when finished/propsoal,523243000,1519867,12786 +Agora/Proposal/validator/unlocking/legal/with 10 proposals/voter: unlock after voting/Locked/stake,467699851,1181799,9949 +Agora/Proposal/validator/unlocking/legal/with 10 proposals/voter: unlock after voting/Locked/propsoal,458071259,1328621,12736 +Agora/Proposal/validator/unlocking/legal/with 10 proposals/voter: unlock after voting/Finished/stake,467699851,1181799,9949 +Agora/Proposal/validator/unlocking/legal/with 10 proposals/voter: unlock after voting/Finished/propsoal,458071259,1328621,12736 +Agora/Proposal/validator/unlocking/legal/with 10 proposals/voter/creator: remove vote locks when locked/stake,500012311,1272339,10100 +Agora/Proposal/validator/unlocking/legal/with 10 proposals/voter/creator: remove vote locks when locked/propsoal,589201780,1710407,12837 +Agora/Proposal/validator/unlocking/legal/with 42 proposals/voter: retract votes while voting/stake,1674013803,4194887,26674 +Agora/Proposal/validator/unlocking/legal/with 42 proposals/voter: retract votes while voting/propsoal,1698509444,4894330,29250 +Agora/Proposal/validator/unlocking/legal/with 42 proposals/voter/creator: retract votes while voting/stake,1809726135,4575155,27362 +Agora/Proposal/validator/unlocking/legal/with 42 proposals/voter/creator: retract votes while voting/propsoal,2245559013,6485220,29709 +Agora/Proposal/validator/unlocking/legal/with 42 proposals/creator: remove creator locks when finished/stake,1674013803,4194887,26590 +Agora/Proposal/validator/unlocking/legal/with 42 proposals/creator: remove creator locks when finished/propsoal,1312057110,3842403,29206 +Agora/Proposal/validator/unlocking/legal/with 42 proposals/voter/creator: remove all locks when finished/stake,1674013803,4194887,27301 +Agora/Proposal/validator/unlocking/legal/with 42 proposals/voter/creator: remove all locks when finished/propsoal,1627527288,4757499,29648 +Agora/Proposal/validator/unlocking/legal/with 42 proposals/voter: unlock after voting/Locked/stake,1674013803,4194887,26843 +Agora/Proposal/validator/unlocking/legal/with 42 proposals/voter: unlock after voting/Locked/propsoal,1355942619,3961005,29419 +Agora/Proposal/validator/unlocking/legal/with 42 proposals/voter: unlock after voting/Finished/stake,1674013803,4194887,26843 +Agora/Proposal/validator/unlocking/legal/with 42 proposals/voter: unlock after voting/Finished/propsoal,1355942619,3961005,29419 +Agora/Proposal/validator/unlocking/legal/with 42 proposals/voter/creator: remove vote locks when locked/stake,1809726135,4575155,27531 +Agora/Proposal/validator/unlocking/legal/with 42 proposals/voter/creator: remove vote locks when locked/propsoal,1903523764,5553287,29878 +"Agora/Proposal/validator/unlocking/illegal/with 1 proposals/retract votes while not voting/role: Voter, status: Draft/stake",128424052,334368,5219 +"Agora/Proposal/validator/unlocking/illegal/with 1 proposals/retract votes while not voting/role: Voter, status: Locked/stake",128424052,334368,5219 +"Agora/Proposal/validator/unlocking/illegal/with 1 proposals/retract votes while not voting/role: Voter, status: Finished/stake",128424052,334368,5219 +"Agora/Proposal/validator/unlocking/illegal/with 1 proposals/retract votes while not voting/role: Creator, status: Draft/stake",124053466,322518,5221 +"Agora/Proposal/validator/unlocking/illegal/with 1 proposals/retract votes while not voting/role: Creator, status: Locked/stake",124053466,322518,5221 +"Agora/Proposal/validator/unlocking/illegal/with 1 proposals/retract votes while not voting/role: Creator, status: Finished/stake",124053466,322518,5221 +"Agora/Proposal/validator/unlocking/illegal/with 1 proposals/retract votes while not voting/role: Both, status: Draft/stake",131655298,343422,5235 +"Agora/Proposal/validator/unlocking/illegal/with 1 proposals/retract votes while not voting/role: Both, status: Locked/stake",131655298,343422,5235 +"Agora/Proposal/validator/unlocking/illegal/with 1 proposals/retract votes while not voting/role: Both, status: Finished/stake",131655298,343422,5235 +"Agora/Proposal/validator/unlocking/illegal/with 1 proposals/retract votes while not voting/role: Irrelevant, status: Draft/stake",120822220,313464,5201 +"Agora/Proposal/validator/unlocking/illegal/with 1 proposals/retract votes while not voting/role: Irrelevant, status: Locked/stake",120822220,313464,5201 +"Agora/Proposal/validator/unlocking/illegal/with 1 proposals/retract votes while not voting/role: Irrelevant, status: Finished/stake",120822220,313464,5201 +Agora/Proposal/validator/unlocking/illegal/with 1 proposals/unlock an irrelevant stake/status: Draftretract votes: True/stake,120822220,313464,5201 +Agora/Proposal/validator/unlocking/illegal/with 1 proposals/unlock an irrelevant stake/status: Draftretract votes: False/stake,120822220,313464,5201 +Agora/Proposal/validator/unlocking/illegal/with 1 proposals/unlock an irrelevant stake/status: VotingReadyretract votes: True/stake,120822220,313464,5201 +Agora/Proposal/validator/unlocking/illegal/with 1 proposals/unlock an irrelevant stake/status: VotingReadyretract votes: False/stake,120822220,313464,5201 +Agora/Proposal/validator/unlocking/illegal/with 1 proposals/unlock an irrelevant stake/status: Lockedretract votes: True/stake,120822220,313464,5201 +Agora/Proposal/validator/unlocking/illegal/with 1 proposals/unlock an irrelevant stake/status: Lockedretract votes: False/stake,120822220,313464,5201 +Agora/Proposal/validator/unlocking/illegal/with 1 proposals/unlock an irrelevant stake/status: Finishedretract votes: True/stake,120822220,313464,5201 +Agora/Proposal/validator/unlocking/illegal/with 1 proposals/unlock an irrelevant stake/status: Finishedretract votes: False/stake,120822220,313464,5201 +Agora/Proposal/validator/unlocking/illegal/with 1 proposals/remove creator too early/status: Draft/stake,128424052,334368,5217 +Agora/Proposal/validator/unlocking/illegal/with 1 proposals/remove creator too early/status: VotingReady/stake,128424052,334368,5217 +Agora/Proposal/validator/unlocking/illegal/with 1 proposals/remove creator too early/status: Locked/stake,128424052,334368,5217 +Agora/Proposal/validator/unlocking/illegal/with 1 proposals/creator: retract votes/stake,128424052,334368,5215 +"Agora/Proposal/validator/unlocking/illegal/with 5 proposals/retract votes while not voting/role: Voter, status: Draft/stake",279213296,711004,7303 +"Agora/Proposal/validator/unlocking/illegal/with 5 proposals/retract votes while not voting/role: Voter, status: Locked/stake",279213296,711004,7303 +"Agora/Proposal/validator/unlocking/illegal/with 5 proposals/retract votes while not voting/role: Voter, status: Finished/stake",279213296,711004,7303 +"Agora/Proposal/validator/unlocking/illegal/with 5 proposals/retract votes while not voting/role: Creator, status: Draft/stake",272564030,693562,7309 +"Agora/Proposal/validator/unlocking/illegal/with 5 proposals/retract votes while not voting/role: Creator, status: Locked/stake",272564030,693562,7309 +"Agora/Proposal/validator/unlocking/illegal/with 5 proposals/retract votes while not voting/role: Creator, status: Finished/stake",272564030,693562,7309 +"Agora/Proposal/validator/unlocking/illegal/with 5 proposals/retract votes while not voting/role: Both, status: Draft/stake",295369526,756274,7380 +"Agora/Proposal/validator/unlocking/illegal/with 5 proposals/retract votes while not voting/role: Both, status: Locked/stake",295369526,756274,7380 +"Agora/Proposal/validator/unlocking/illegal/with 5 proposals/retract votes while not voting/role: Both, status: Finished/stake",295369526,756274,7380 +"Agora/Proposal/validator/unlocking/illegal/with 5 proposals/retract votes while not voting/role: Irrelevant, status: Draft/stake",256407800,648292,7221 +"Agora/Proposal/validator/unlocking/illegal/with 5 proposals/retract votes while not voting/role: Irrelevant, status: Locked/stake",256407800,648292,7221 +"Agora/Proposal/validator/unlocking/illegal/with 5 proposals/retract votes while not voting/role: Irrelevant, status: Finished/stake",256407800,648292,7221 +Agora/Proposal/validator/unlocking/illegal/with 5 proposals/unlock an irrelevant stake/status: Draftretract votes: True/stake,256407800,648292,7221 +Agora/Proposal/validator/unlocking/illegal/with 5 proposals/unlock an irrelevant stake/status: Draftretract votes: False/stake,256407800,648292,7221 +Agora/Proposal/validator/unlocking/illegal/with 5 proposals/unlock an irrelevant stake/status: VotingReadyretract votes: True/stake,256407800,648292,7221 +Agora/Proposal/validator/unlocking/illegal/with 5 proposals/unlock an irrelevant stake/status: VotingReadyretract votes: False/stake,256407800,648292,7221 +Agora/Proposal/validator/unlocking/illegal/with 5 proposals/unlock an irrelevant stake/status: Lockedretract votes: True/stake,256407800,648292,7221 +Agora/Proposal/validator/unlocking/illegal/with 5 proposals/unlock an irrelevant stake/status: Lockedretract votes: False/stake,256407800,648292,7221 +Agora/Proposal/validator/unlocking/illegal/with 5 proposals/unlock an irrelevant stake/status: Finishedretract votes: True/stake,256407800,648292,7221 +Agora/Proposal/validator/unlocking/illegal/with 5 proposals/unlock an irrelevant stake/status: Finishedretract votes: False/stake,256407800,648292,7221 +Agora/Proposal/validator/unlocking/illegal/with 5 proposals/remove creator too early/status: Draft/stake,279213296,711004,7293 +Agora/Proposal/validator/unlocking/illegal/with 5 proposals/remove creator too early/status: VotingReady/stake,279213296,711004,7293 +Agora/Proposal/validator/unlocking/illegal/with 5 proposals/remove creator too early/status: Locked/stake,279213296,711004,7293 +Agora/Proposal/validator/unlocking/illegal/with 5 proposals/creator: retract votes/stake,279213296,711004,7283 +"Agora/Proposal/validator/unlocking/illegal/with 10 proposals/retract votes while not voting/role: Voter, status: Draft/stake",467699851,1181799,9909 +"Agora/Proposal/validator/unlocking/illegal/with 10 proposals/retract votes while not voting/role: Voter, status: Locked/stake",467699851,1181799,9909 +"Agora/Proposal/validator/unlocking/illegal/with 10 proposals/retract votes while not voting/role: Voter, status: Finished/stake",467699851,1181799,9909 +"Agora/Proposal/validator/unlocking/illegal/with 10 proposals/retract votes while not voting/role: Creator, status: Draft/stake",458202235,1157367,9920 +"Agora/Proposal/validator/unlocking/illegal/with 10 proposals/retract votes while not voting/role: Creator, status: Locked/stake",458202235,1157367,9920 +"Agora/Proposal/validator/unlocking/illegal/with 10 proposals/retract votes while not voting/role: Creator, status: Finished/stake",458202235,1157367,9920 +"Agora/Proposal/validator/unlocking/illegal/with 10 proposals/retract votes while not voting/role: Both, status: Draft/stake",500012311,1272339,10060 +"Agora/Proposal/validator/unlocking/illegal/with 10 proposals/retract votes while not voting/role: Both, status: Locked/stake",500012311,1272339,10060 +"Agora/Proposal/validator/unlocking/illegal/with 10 proposals/retract votes while not voting/role: Both, status: Finished/stake",500012311,1272339,10060 +"Agora/Proposal/validator/unlocking/illegal/with 10 proposals/retract votes while not voting/role: Irrelevant, status: Draft/stake",425889775,1066827,9746 +"Agora/Proposal/validator/unlocking/illegal/with 10 proposals/retract votes while not voting/role: Irrelevant, status: Locked/stake",425889775,1066827,9746 +"Agora/Proposal/validator/unlocking/illegal/with 10 proposals/retract votes while not voting/role: Irrelevant, status: Finished/stake",425889775,1066827,9746 +Agora/Proposal/validator/unlocking/illegal/with 10 proposals/unlock an irrelevant stake/status: Draftretract votes: True/stake,425889775,1066827,9746 +Agora/Proposal/validator/unlocking/illegal/with 10 proposals/unlock an irrelevant stake/status: Draftretract votes: False/stake,425889775,1066827,9746 +Agora/Proposal/validator/unlocking/illegal/with 10 proposals/unlock an irrelevant stake/status: VotingReadyretract votes: True/stake,425889775,1066827,9746 +Agora/Proposal/validator/unlocking/illegal/with 10 proposals/unlock an irrelevant stake/status: VotingReadyretract votes: False/stake,425889775,1066827,9746 +Agora/Proposal/validator/unlocking/illegal/with 10 proposals/unlock an irrelevant stake/status: Lockedretract votes: True/stake,425889775,1066827,9746 +Agora/Proposal/validator/unlocking/illegal/with 10 proposals/unlock an irrelevant stake/status: Lockedretract votes: False/stake,425889775,1066827,9746 +Agora/Proposal/validator/unlocking/illegal/with 10 proposals/unlock an irrelevant stake/status: Finishedretract votes: True/stake,425889775,1066827,9746 +Agora/Proposal/validator/unlocking/illegal/with 10 proposals/unlock an irrelevant stake/status: Finishedretract votes: False/stake,425889775,1066827,9746 +Agora/Proposal/validator/unlocking/illegal/with 10 proposals/remove creator too early/status: Draft/stake,467699851,1181799,9888 +Agora/Proposal/validator/unlocking/illegal/with 10 proposals/remove creator too early/status: VotingReady/stake,467699851,1181799,9888 +Agora/Proposal/validator/unlocking/illegal/with 10 proposals/remove creator too early/status: Locked/stake,467699851,1181799,9888 +Agora/Proposal/validator/unlocking/illegal/with 10 proposals/creator: retract votes/stake,467699851,1181799,9868 +"Agora/Proposal/validator/unlocking/illegal/with 42 proposals/retract votes while not voting/role: Voter, status: Draft/stake",1674013803,4194887,26674 +"Agora/Proposal/validator/unlocking/illegal/with 42 proposals/retract votes while not voting/role: Voter, status: Locked/stake",1674013803,4194887,26674 +"Agora/Proposal/validator/unlocking/illegal/with 42 proposals/retract votes while not voting/role: Voter, status: Finished/stake",1674013803,4194887,26674 +"Agora/Proposal/validator/unlocking/illegal/with 42 proposals/retract votes while not voting/role: Creator, status: Draft/stake",1646286747,4125719,26736 +"Agora/Proposal/validator/unlocking/illegal/with 42 proposals/retract votes while not voting/role: Creator, status: Locked/stake",1646286747,4125719,26736 +"Agora/Proposal/validator/unlocking/illegal/with 42 proposals/retract votes while not voting/role: Creator, status: Finished/stake",1646286747,4125719,26736 +"Agora/Proposal/validator/unlocking/illegal/with 42 proposals/retract votes while not voting/role: Both, status: Draft/stake",1809726135,4575155,27362 +"Agora/Proposal/validator/unlocking/illegal/with 42 proposals/retract votes while not voting/role: Both, status: Locked/stake",1809726135,4575155,27362 +"Agora/Proposal/validator/unlocking/illegal/with 42 proposals/retract votes while not voting/role: Both, status: Finished/stake",1809726135,4575155,27362 +"Agora/Proposal/validator/unlocking/illegal/with 42 proposals/retract votes while not voting/role: Irrelevant, status: Draft/stake",1510574415,3745451,25961 +"Agora/Proposal/validator/unlocking/illegal/with 42 proposals/retract votes while not voting/role: Irrelevant, status: Locked/stake",1510574415,3745451,25961 +"Agora/Proposal/validator/unlocking/illegal/with 42 proposals/retract votes while not voting/role: Irrelevant, status: Finished/stake",1510574415,3745451,25961 +Agora/Proposal/validator/unlocking/illegal/with 42 proposals/unlock an irrelevant stake/status: Draftretract votes: True/stake,1510574415,3745451,25961 +Agora/Proposal/validator/unlocking/illegal/with 42 proposals/unlock an irrelevant stake/status: Draftretract votes: False/stake,1510574415,3745451,25961 +Agora/Proposal/validator/unlocking/illegal/with 42 proposals/unlock an irrelevant stake/status: VotingReadyretract votes: True/stake,1510574415,3745451,25961 +Agora/Proposal/validator/unlocking/illegal/with 42 proposals/unlock an irrelevant stake/status: VotingReadyretract votes: False/stake,1510574415,3745451,25961 +Agora/Proposal/validator/unlocking/illegal/with 42 proposals/unlock an irrelevant stake/status: Lockedretract votes: True/stake,1510574415,3745451,25961 +Agora/Proposal/validator/unlocking/illegal/with 42 proposals/unlock an irrelevant stake/status: Lockedretract votes: False/stake,1510574415,3745451,25961 +Agora/Proposal/validator/unlocking/illegal/with 42 proposals/unlock an irrelevant stake/status: Finishedretract votes: True/stake,1510574415,3745451,25961 +Agora/Proposal/validator/unlocking/illegal/with 42 proposals/unlock an irrelevant stake/status: Finishedretract votes: False/stake,1510574415,3745451,25961 +Agora/Proposal/validator/unlocking/illegal/with 42 proposals/remove creator too early/status: Draft/stake,1674013803,4194887,26590 +Agora/Proposal/validator/unlocking/illegal/with 42 proposals/remove creator too early/status: VotingReady/stake,1674013803,4194887,26590 +Agora/Proposal/validator/unlocking/illegal/with 42 proposals/remove creator too early/status: Locked/stake,1674013803,4194887,26590 +Agora/Proposal/validator/unlocking/illegal/with 42 proposals/creator: retract votes/stake,1674013803,4194887,26506 Agora/AuthorityToken/singleAuthorityTokenBurned/Correct simple,21017788,55883,806 Agora/AuthorityToken/singleAuthorityTokenBurned/Correct many inputs,33204186,88241,900 Agora/Treasury/Validator/Positive/Allows for effect changes,31556709,81546,1452 Agora/AuthorityToken/singleAuthorityTokenBurned/Correct simple,21017788,55883,806 Agora/AuthorityToken/singleAuthorityTokenBurned/Correct many inputs,33204186,88241,900 -Agora/Governor/policy/GST minting,51007235,144191,2034 -Agora/Governor/validator/proposal creation,309689999,834675,9064 -Agora/Governor/validator/GATs minting,418560845,1137908,9187 -Agora/Governor/validator/mutate governor state,88986020,248491,8662 +Agora/Governor/policy/GST minting,51480023,145787,2048 +Agora/Governor/validator/proposal creation,303114849,813451,9390 +Agora/Governor/validator/GATs minting,422654153,1147158,9516 +Agora/Governor/validator/mutate governor state,90087778,252215,8991