implement new stake locking mechanism

This commit is contained in:
Hongrui Fang 2022-07-08 20:37:14 +08:00
parent 79563c8d64
commit 96a50419d1
No known key found for this signature in database
GPG key ID: 1C4711FFF64C0254
6 changed files with 369 additions and 211 deletions

View file

@ -73,6 +73,8 @@ data GovernorDatum = GovernorDatum
-- Will get copied over upon the creation of proposals. -- Will get copied over upon the creation of proposals.
, createProposalTimeRangeMaxWidth :: MaxTimeRangeWidth , createProposalTimeRangeMaxWidth :: MaxTimeRangeWidth
-- ^ The maximum valid duration of a transaction that creats a proposal. -- ^ The maximum valid duration of a transaction that creats a proposal.
, maximumProposalsPerStake :: Integer
-- ^ The maximum number of *alive* proposals which were careated by a stake.
} }
deriving stock (Show, GHC.Generic) deriving stock (Show, GHC.Generic)
@ -149,6 +151,7 @@ newtype PGovernorDatum (s :: S) = PGovernorDatum
, "nextProposalId" ':= PProposalId , "nextProposalId" ':= PProposalId
, "proposalTimings" ':= PProposalTimingConfig , "proposalTimings" ':= PProposalTimingConfig
, "createProposalTimeRangeMaxWidth" ':= PMaxTimeRangeWidth , "createProposalTimeRangeMaxWidth" ':= PMaxTimeRangeWidth
, "maximumProposalsPerStake" ':= PInteger
] ]
) )
} }

View file

@ -46,8 +46,6 @@ import Agora.Governor (
) )
import Agora.Proposal ( import Agora.Proposal (
PProposalDatum (..), PProposalDatum (..),
PProposalId (..),
PResultTag,
Proposal (..), Proposal (..),
ProposalStatus (Draft, Finished, Locked), ProposalStatus (Draft, Finished, Locked),
pemptyVotesFor, pemptyVotesFor,
@ -65,6 +63,7 @@ import Agora.Stake (
PProposalLock (..), PProposalLock (..),
PStakeDatum (..), PStakeDatum (..),
Stake (..), Stake (..),
pnumCreatedProposals,
) )
import Agora.Stake.Scripts ( import Agora.Stake.Scripts (
stakePolicy, stakePolicy,
@ -108,7 +107,6 @@ import Plutarch.Api.V1.ScriptContext (pfindTxInByTxOutRef, pisUTXOSpent, ptryFin
import "liqwid-plutarch-extra" Plutarch.Api.V1.Value (psymbolValueOf) import "liqwid-plutarch-extra" Plutarch.Api.V1.Value (psymbolValueOf)
import Plutarch.Extra.IsData (pmatchEnumFromData) import Plutarch.Extra.IsData (pmatchEnumFromData)
import Plutarch.Extra.Map ( import Plutarch.Extra.Map (
pkeys,
plookup, plookup,
plookup', plookup',
) )
@ -300,6 +298,7 @@ governorValidator gov =
, "nextProposalId" , "nextProposalId"
, "proposalTimings" , "proposalTimings"
, "createProposalTimeRangeMaxWidth" , "createProposalTimeRangeMaxWidth"
, "maximumProposalsPerStake"
] ]
oldGovernorDatum oldGovernorDatum
@ -341,6 +340,8 @@ governorValidator gov =
.& #proposalTimings .= oldGovernorDatumF.proposalTimings .& #proposalTimings .= oldGovernorDatumF.proposalTimings
.& #createProposalTimeRangeMaxWidth .& #createProposalTimeRangeMaxWidth
.= oldGovernorDatumF.createProposalTimeRangeMaxWidth .= oldGovernorDatumF.createProposalTimeRangeMaxWidth
.& #maximumProposalsPerStake
.= oldGovernorDatumF.maximumProposalsPerStake
) )
pguardC "Unexpected governor state datum" $ pguardC "Unexpected governor state datum" $
newGovernorDatum #== expectedNewDatum newGovernorDatum #== expectedNewDatum
@ -377,6 +378,10 @@ governorValidator gov =
stakeInputDatumF <- stakeInputDatumF <-
pletFieldsC @["stakedAmount", "owner", "lockedBy"] stakeInputDatum pletFieldsC @["stakedAmount", "owner", "lockedBy"] stakeInputDatum
pguardC "Didn't created too many proposals" $
pnumCreatedProposals # stakeInputDatumF.lockedBy
#< oldGovernorDatumF.maximumProposalsPerStake
pguardC "Required amount of stake GTs should be presented" $ pguardC "Required amount of stake GTs should be presented" $
stakeInputDatumF.stakedAmount #== (pgtValueOf # stakeInputF.value) stakeInputDatumF.stakedAmount #== (pgtValueOf # stakeInputF.value)
@ -479,25 +484,14 @@ governorValidator gov =
mustBePJust # "Stake output not found" #$ ptryFindDatum # stakeOutputDatumHash # txInfoF.datums mustBePJust # "Stake output not found" #$ ptryFindDatum # stakeOutputDatumHash # txInfoF.datums
-- The stake should be locked by the newly created proposal. -- The stake should be locked by the newly created proposal.
let newLock =
let possibleVoteResults = pkeys #$ pto $ pfromData proposalOutputDatum.votes mkRecordConstr
PCreated
mkProposalLock :: Term _ (PProposalId :--> PAsData PResultTag :--> PAsData PProposalLock) ( #created .= oldGovernorDatumF.nextProposalId
mkProposalLock = )
phoistAcyclic $
plam
( \pid rt' ->
pdata $
mkRecordConstr
PProposalLock
( #vote .= rt' .& #proposalTag .= pdata pid
)
)
-- Append new locks to existing locks -- Append new locks to existing locks
expectedProposalLocks = expectedProposalLocks = pcons # pdata newLock # stakeInputDatumF.lockedBy
pconcat # stakeInputDatumF.lockedBy
#$ pmap # (mkProposalLock # proposalOutputDatum.proposalId) # possibleVoteResults
expectedStakeOutputDatum = expectedStakeOutputDatum =
pdata $ pdata $

View file

@ -68,6 +68,7 @@ import Plutarch.Lift (
PUnsafeLiftDecl (..), PUnsafeLiftDecl (..),
) )
import Plutarch.SafeMoney (PDiscrete) import Plutarch.SafeMoney (PDiscrete)
import Plutarch.Show (PShow (..))
import PlutusLedgerApi.V1 (DatumHash, PubKeyHash, ValidatorHash) import PlutusLedgerApi.V1 (DatumHash, PubKeyHash, ValidatorHash)
import PlutusLedgerApi.V1.Value (AssetClass) import PlutusLedgerApi.V1.Value (AssetClass)
import PlutusTx qualified import PlutusTx qualified
@ -328,7 +329,7 @@ data ProposalRedeemer
-- This list should be sorted in ascending order. -- This list should be sorted in ascending order.
Cosign [PubKeyHash] Cosign [PubKeyHash]
| -- | Allow unlocking one or more stakes with votes towards particular 'ResultTag'. | -- | 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. | -- | Advance the proposal, performing the required checks for whether that is legal.
-- --
-- These are roughly the checks for each possible transition: -- These are roughly the checks for each possible transition:
@ -426,6 +427,11 @@ deriving via
instance instance
PTryFrom PData (PAsData PResultTag) PTryFrom PData (PAsData PResultTag)
-- | @since 0.2.0
instance PShow PResultTag where
pshow' :: Bool -> Term s PResultTag -> Term s PString
pshow' _ x = pshow @PInteger $ pto x
{- | Plutarch-level version of 'PProposalId'. {- | Plutarch-level version of 'PProposalId'.
@since 0.1.0 @since 0.1.0
@ -458,6 +464,11 @@ deriving via
instance instance
(PConstantDecl ProposalId) (PConstantDecl ProposalId)
-- | @since 0.2.0
instance PShow PProposalId where
pshow' :: Bool -> Term s PProposalId -> Term s PString
pshow' _ x = pshow @PInteger $ pto x
{- | Plutarch-level version of 'ProposalStatus'. {- | Plutarch-level version of 'ProposalStatus'.
@since 0.1.0 @since 0.1.0
@ -665,7 +676,7 @@ deriving via (DerivePConstantViaDataList ProposalDatum PProposalDatum) instance
data PProposalRedeemer (s :: S) data PProposalRedeemer (s :: S)
= PVote (Term s (PDataRecord '["resultTag" ':= PResultTag])) = PVote (Term s (PDataRecord '["resultTag" ':= PResultTag]))
| PCosign (Term s (PDataRecord '["newCosigners" ':= PBuiltinList (PAsData PPubKeyHash)])) | PCosign (Term s (PDataRecord '["newCosigners" ':= PBuiltinList (PAsData PPubKeyHash)]))
| PUnlock (Term s (PDataRecord '["resultTag" ':= PResultTag])) | PUnlock (Term s (PDataRecord '[]))
| PAdvanceProposal (Term s (PDataRecord '[])) | PAdvanceProposal (Term s (PDataRecord '[]))
deriving stock deriving stock
( -- | @since 0.1.0 ( -- | @since 0.1.0

View file

@ -29,8 +29,12 @@ import Agora.Proposal.Time (
import Agora.Stake ( import Agora.Stake (
PProposalLock (..), PProposalLock (..),
PStakeDatum (..), PStakeDatum (..),
PStakeUsage (..), pextractVoteOption,
pgetStakeUsage, pgetStakeRole,
pisCreator,
pisIrrelevant,
pisPureCreator,
pisVoter,
) )
import Agora.Utils ( import Agora.Utils (
getMintingPolicySymbol, getMintingPolicySymbol,
@ -469,12 +473,7 @@ proposalValidator proposal =
-- Ensure that no lock with the current proposal id has been put on the stake. -- 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" $ pguardC "Same stake shouldn't vote on the same proposal twice" $
pnot #$ pany pnot #$ pisVoter #$ pgetStakeRole # proposalF.proposalId # pfromData stakeInF.lockedBy
# plam
( \((pfield @"proposalTag" #) . pfromData -> pid) ->
pid #== proposalF.proposalId
)
# pfromData stakeInF.lockedBy
let -- The amount of new votes should be the 'stakedAmount'. let -- The amount of new votes should be the 'stakedAmount'.
-- Update the vote counter of the proposal, and leave other stuff as is. -- Update the vote counter of the proposal, and leave other stuff as is.
@ -510,9 +509,9 @@ proposalValidator proposal =
let newProposalLock = let newProposalLock =
mkRecordConstr mkRecordConstr
PProposalLock PVoted
( #vote .= pdata voteFor ( #votedOn .= proposalF.proposalId
.& #proposalTag .= proposalF.proposalId .& #votedFor .= pdata voteFor
) )
-- Prepend the new lock to existing locks -- Prepend the new lock to existing locks
expectedProposalLocks = expectedProposalLocks =
@ -533,30 +532,16 @@ proposalValidator proposal =
---------------------------------------------------------------------- ----------------------------------------------------------------------
PUnlock r -> withSingleStake $ \stakeInF stakeOut _ -> do PUnlock _ -> withSingleStake $ \stakeInF stakeOut _ -> do
-- At draft stage, the votes should be empty. -- At draft stage, the votes should be empty.
pguardC "Shouldn't retract votes from a draft proposal" $ pguardC "Shouldn't retract votes from a draft proposal" $
pnot #$ currentStatus #== pconstant Draft pnot #$ currentStatus #== pconstant Draft
-- This is the vote option we're retracting from. stakeRole <- pletC $ pgetStakeRole # proposalF.proposalId # stakeInF.lockedBy
retractFrom <- pletC $ pfield @"resultTag" # r
-- Determine if the input stake is actually locked by this proposal. pguardC "Stake input should be relevant" $
stakeUsage <- pletC $ pgetStakeUsage # stakeInF.lockedBy # proposalF.proposalId pnot #$ pisIrrelevant # stakeRole
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 <- retractCount <-
pletC $ pletC $
pmatch stakeInF.stakedAmount $ \(PDiscrete v) -> pextract # v pmatch stakeInF.stakedAmount $ \(PDiscrete v) -> pextract # v
@ -564,13 +549,28 @@ proposalValidator proposal =
-- The votes can only change when the proposal still allows voting. -- The votes can only change when the proposal still allows voting.
let shouldUpdateVotes = let shouldUpdateVotes =
currentStatus #== pconstant VotingReady currentStatus #== pconstant VotingReady
#&& pnot # (pcon PCreated #== stakeUsage) #&& pnot # (pisPureCreator # stakeRole)
allowRemovingCreatorLock =
currentStatus #== pconstant Finished
isCreator = pisCreator # stakeRole
validateOutputLocks = plam $ \locks ->
plet
( pgetStakeRole # proposalF.proposalId # locks
)
$ \newStakeRole ->
pif
(isCreator #&& pnot # allowRemovingCreatorLock)
(pisPureCreator # newStakeRole)
(pisIrrelevant # newStakeRole)
pguardC "Proposal output correct" $ pguardC "Proposal output correct" $
pif pif
shouldUpdateVotes shouldUpdateVotes
( let -- Remove votes and leave other parts of the proposal as it. ( 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 = expectedProposalOut =
mkRecordConstr mkRecordConstr
@ -598,15 +598,14 @@ proposalValidator proposal =
PStakeDatum PStakeDatum
( #stakedAmount .= stakeInF.stakedAmount ( #stakedAmount .= stakeInF.stakedAmount
.& #owner .= stakeInF.owner .& #owner .= stakeInF.owner
.& #lockedBy .= stakeOutputLocks .& #lockedBy .= pdata stakeOutputLocks
) )
pguardC "Only locks updated in the output stake" $ pguardC "Only locks updated in the output stake" $
templateStakeOut #== stakeOut templateStakeOut #== stakeOut
pguardC "All relevant locks removed from the stake" $ pguardC "All relevant locks removed from the stake" $
pgetStakeUsage # pfromData stakeOutputLocks validateOutputLocks # stakeOutputLocks
# proposalF.proposalId #== pcon PDidNothing
pure $ pconstant () pure $ pconstant ()

View file

@ -18,11 +18,17 @@ module Agora.Stake (
PStakeDatum (..), PStakeDatum (..),
PStakeRedeemer (..), PStakeRedeemer (..),
PProposalLock (..), PProposalLock (..),
PStakeUsage (..), PStakeRole (..),
-- * Utility functions -- * Utility functions
stakeLocked, pstakeLocked,
pgetStakeUsage, pnumCreatedProposals,
pextractVoteOption,
pgetStakeRole,
pisVoter,
pisCreator,
pisPureCreator,
pisIrrelevant,
) where ) where
import Agora.Plutarch.Orphans () import Agora.Plutarch.Orphans ()
@ -43,14 +49,15 @@ import Plutarch.Extra.IsData (
DerivePConstantViaDataList (..), DerivePConstantViaDataList (..),
ProductIsData (ProductIsData), ProductIsData (ProductIsData),
) )
import Plutarch.Extra.List (pmapMaybe, pnotNull) import Plutarch.Extra.List (pnotNull)
import Plutarch.Extra.Other (DerivePNewtype' (..)) import Plutarch.Extra.Other (DerivePNewtype' (..))
import Plutarch.Extra.TermCont (pletFieldsC)
import Plutarch.Lift (PConstantDecl, PUnsafeLiftDecl (..)) import Plutarch.Lift (PConstantDecl, PUnsafeLiftDecl (..))
import Plutarch.SafeMoney (PDiscrete) import Plutarch.SafeMoney (PDiscrete)
import Plutarch.Show (PShow (..))
import PlutusLedgerApi.V1 (PubKeyHash) import PlutusLedgerApi.V1 (PubKeyHash)
import PlutusLedgerApi.V1.Value (AssetClass) import PlutusLedgerApi.V1.Value (AssetClass)
import PlutusTx qualified import PlutusTx qualified
import Prelude ((+))
import Prelude hiding (Num (..)) import Prelude hiding (Num (..))
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@ -69,8 +76,7 @@ data Stake = Stake
GHC.Generic GHC.Generic
) )
{- | A lock placed on a Stake datum in order to prevent {- | Locks that are stored in the stake datums for various purposes.
depositing and withdrawing when votes are in place.
NOTE: Due to retracting votes always being possible, NOTE: Due to retracting votes always being possible,
this lock will only lock with contention on the proposal. this lock will only lock with contention on the proposal.
@ -97,30 +103,47 @@ data Stake = Stake
@since 0.1.0 @since 0.1.0
-} -}
data ProposalLock = ProposalLock data ProposalLock
{ vote :: ResultTag = -- | The stake was used to create a proposal.
-- ^ What was voted on. This allows retracting votes to --
-- undo their vote. -- This kind of lock is placed upon the creation of a proposal, in order
, proposalId :: ProposalId -- to limit creation of proposals per stake.
-- ^ Identifies the proposal. See 'ProposalId' for further --
-- comments on its significance. -- See also: https://github.com/Liqwid-Labs/agora/issues/68
} --
-- @since 0.2.0
Created
{ craeted :: 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
{ votedOn :: ProposalId
-- ^ The identifier of the proposal.
, votedFor :: ResultTag
-- ^ The option which was voted on. This allows votes to be retracted.
}
deriving stock deriving stock
( -- | @since 0.1.0 ( -- | @since 0.1.0
Show Show
, -- | @since 0.1.0 , -- | @since 0.1.0
GHC.Generic GHC.Generic
) )
deriving anyclass (Generic) deriving anyclass
deriving
( -- | @since 0.1.0 ( -- | @since 0.1.0
PlutusTx.ToData Generic
, -- | @since 0.1.0
PlutusTx.FromData
, -- | @since 0.1.0
PlutusTx.UnsafeFromData
) )
via (ProductIsData ProposalLock)
PlutusTx.makeIsDataIndexed
''ProposalLock
[ ('Created, 0)
, ('Voted, 1)
]
{- | Haskell-level redeemer for Stake scripts. {- | Haskell-level redeemer for Stake scripts.
@ -138,12 +161,12 @@ data StakeRedeemer
-- This needs to be done in sync with casting a vote, otherwise -- 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, -- it's possible for a lock to be permanently placed on the stake,
-- and then the funds are lost. -- and then the funds are lost.
PermitVote ProposalLock PermitVote
| -- | Retract a vote, removing it from the 'lockedBy' field. See 'ProposalLock'. | -- | Retract a vote, removing it from the 'lockedBy' field. See 'ProposalLock'.
-- This action checks for permission of the 'Agora.Proposal.Proposal'. Finished proposals are -- 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, -- always allowed to have votes retracted and won't affect the Proposal datum,
-- allowing 'Stake's to be unlocked. -- allowing 'Stake's to be unlocked.
RetractVotes [ProposalLock] RetractVotes
| -- | The owner can consume stake if nothing is changed about it. | -- | The owner can consume stake if nothing is changed about it.
-- If the proposal token moves, this is equivalent to the owner consuming it. -- If the proposal token moves, this is equivalent to the owner consuming it.
WitnessStake WitnessStake
@ -165,7 +188,7 @@ PlutusTx.makeIsDataIndexed
data StakeDatum = StakeDatum data StakeDatum = StakeDatum
{ stakedAmount :: Tagged GTTag Integer { stakedAmount :: Tagged GTTag Integer
-- ^ Tracks the amount of governance token staked in the datum. -- ^ 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 , owner :: PubKeyHash
-- ^ The hash of the public key this stake belongs to. -- ^ The hash of the public key this stake belongs to.
-- --
@ -173,7 +196,7 @@ data StakeDatum = StakeDatum
-- https://github.com/Liqwid-Labs/agora/issues/45 -- https://github.com/Liqwid-Labs/agora/issues/45
, lockedBy :: [ProposalLock] , lockedBy :: [ProposalLock]
-- ^ The current proposals locking this stake. This field must be empty -- ^ 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 stock (Show, GHC.Generic)
deriving anyclass (Generic) deriving anyclass (Generic)
@ -227,13 +250,20 @@ newtype PStakeDatum (s :: S) = PStakeDatum
via (DerivePNewtype' PStakeDatum) via (DerivePNewtype' PStakeDatum)
-- | @since 0.1.0 -- | @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 -- | @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 -- | @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. {- | Plutarch-level redeemer for Stake scripts.
@ -244,8 +274,8 @@ data PStakeRedeemer (s :: S)
PDepositWithdraw (Term s (PDataRecord '["delta" ':= PDiscrete GTTag])) PDepositWithdraw (Term s (PDataRecord '["delta" ':= PDiscrete GTTag]))
| -- | Destroy a stake, retrieving its LQ, the minimum ADA and any other assets. | -- | Destroy a stake, retrieving its LQ, the minimum ADA and any other assets.
PDestroy (Term s (PDataRecord '[])) PDestroy (Term s (PDataRecord '[]))
| PPermitVote (Term s (PDataRecord '["lock" ':= PProposalLock])) | PPermitVote (Term s (PDataRecord '[]))
| PRetractVotes (Term s (PDataRecord '["locks" ':= PBuiltinList (PAsData PProposalLock)])) | PRetractVotes (Term s (PDataRecord '[]))
| PWitnessStake (Term s (PDataRecord '[])) | PWitnessStake (Term s (PDataRecord '[]))
deriving stock deriving stock
( -- | @since 0.1.0 ( -- | @since 0.1.0
@ -267,65 +297,37 @@ data PStakeRedeemer (s :: S)
) )
via PIsDataReprInstances PStakeRedeemer via PIsDataReprInstances PStakeRedeemer
-- | @since 0.1.0
deriving via deriving via
PAsData (PIsDataReprInstances PStakeRedeemer) PAsData (PIsDataReprInstances PStakeRedeemer)
instance instance
PTryFrom PData (PAsData PStakeRedeemer) PTryFrom PData (PAsData PStakeRedeemer)
instance Plutarch.Lift.PUnsafeLiftDecl PStakeRedeemer where type PLifted PStakeRedeemer = StakeRedeemer -- | @since 0.1.0
deriving via (DerivePConstantViaData StakeRedeemer PStakeRedeemer) instance (Plutarch.Lift.PConstantDecl StakeRedeemer) 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'. {- | Plutarch-level version of 'ProposalLock'.
@since 0.1.0 @since 0.2.0
-} -}
newtype PProposalLock (s :: S) = PProposalLock data PProposalLock (s :: S)
{ getProposalLock :: = PCreated (Term s (PDataRecord '["created" ':= PProposalId]))
Term | PVoted
s ( Term
( PDataRecord s
'[ "vote" ':= PResultTag ( PDataRecord
, "proposalTag" ':= PProposalId '[ "votedOn" ':= PProposalId
] , "votedFor" ':= PResultTag
) ]
} )
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
deriving stock deriving stock
( -- | @since 0.1.0 ( -- | @since 0.1.0
GHC.Generic GHC.Generic
@ -334,51 +336,212 @@ data PStakeUsage (s :: S)
( -- | @since 0.1.0 ( -- | @since 0.1.0
Generic Generic
, -- | @since 0.1.0 , -- | @since 0.1.0
HasDatatypeInfo
)
deriving anyclass
( -- | @since 0.1.0
PIsDataRepr
)
deriving
( -- | @since 0.1.0
PlutusType PlutusType
, -- | @since 0.1.0 , -- | @since 0.1.0
HasDatatypeInfo PIsData
, -- | @since 0.1.0 , -- | @since 0.1.0
PEq PEq
) )
via (PIsDataReprInstances PProposalLock)
{- | / O(n) /.Return the usage of a stake on a particular proposal, -- | @since 0.1.0
given the 'lockedBy' field of a stake and the target proposal. 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 _ = "(..)"
pshow' False lock = pmatch lock $ \case
PCreated ((pfield @"created" #) -> pid) -> "Created " <> pshow pid
PVoted x -> pletFields @'["votedOn", "votedFor"] x $ \xF ->
"Voted on " <> pshow xF.votedOn <> " for " <> pshow xF.votedFor
--------------------------------------------------------------------------------
{- | Check whether a Stake is locked. If it is locked, various actions are unavailable.
@since 0.2.0
-} -}
pgetStakeUsage :: pstakeLocked :: forall (s :: S). Term s (PStakeDatum :--> PBool)
Term pstakeLocked = phoistAcyclic $
_ plam $ \stakeDatum ->
( PBuiltinList (PAsData PProposalLock) let locks :: Term _ (PBuiltinList (PAsData PProposalLock))
:--> PProposalId locks = pfield @"lockedBy" # stakeDatum
:--> PStakeUsage 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 pif
(lockF.proposalTag #== pid) (pid' #== pid)
(pcon $ PJust lock') (pcon PCreator)
(pcon PNothing) (pcon PIrrelevant)
) PVoted lock' -> pletFields @'["votedOn", "votedFor"] lock' $ \lockF ->
# locks 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 {- | Get the outcome that was voted for.
in plet lockCount' $ \lockCount ->
pif (lockCount #== 0) (pcon PDidNothing) $ @since 0.2.0
pif -}
(lockCount #== 1) pextractVoteOption :: Term s (PStakeRole :--> PResultTag)
( pcon $ pextractVoteOption = phoistAcyclic $
PVotedFor $ plam $ \sr -> pmatch sr $ \case
pfromData $ PVoter r -> r
pfield @"vote" #$ phead # filteredLocks PBoth r -> r
) _ -> ptraceError "not voter"
-- Note: see the implementation of the governor.
(pcon PCreated)

View file

@ -18,7 +18,7 @@ import Agora.Stake (
), ),
Stake (gtClassRef, proposalSTClass), Stake (gtClassRef, proposalSTClass),
StakeRedeemer (WitnessStake), StakeRedeemer (WitnessStake),
stakeLocked, pstakeLocked,
) )
import Agora.Utils ( import Agora.Utils (
mustBePJust, mustBePJust,
@ -109,7 +109,7 @@ stakePolicy gtClassRef =
pif pif
(psymbolValueOf # ownSymbol # txOutF.value #== 1) (psymbolValueOf # ownSymbol # txOutF.value #== 1)
( let datum = mustFindDatum' @PStakeDatum # txOutF.datumHash # txInfoF.datums ( let datum = mustFindDatum' @PStakeDatum # txOutF.datumHash # txInfoF.datums
in pnot # (stakeLocked # datum) in pnot # (pstakeLocked # datum)
) )
(pconstant False) (pconstant False)
) )
@ -263,7 +263,7 @@ stakeValidator stake =
spentST <- pletC $ psymbolValueOf # stCurrencySymbol #$ valueSpent spentST <- pletC $ psymbolValueOf # stCurrencySymbol #$ valueSpent
-- Is the stake currently locked? -- Is the stake currently locked?
stakeIsLocked <- pletC $ stakeLocked # stakeDatum' stakeIsLocked <- pletC $ pstakeLocked # stakeDatum'
pure $ pure $
pmatch stakeRedeemer $ \case pmatch stakeRedeemer $ \case
@ -287,7 +287,7 @@ stakeValidator stake =
proposalSTClass = passetClass # pconstant propCs # pconstant propTn proposalSTClass = passetClass # pconstant propCs # pconstant propTn
spentProposalST = passetClassValueOf # valueSpent # proposalSTClass spentProposalST = passetClassValueOf # valueSpent # proposalSTClass
proposalTokenMoved <- pletC $ spentProposalST #== 1 proposalTokenMoved <- pletC $ 1 #<= spentProposalST
-- Filter out own outputs using own address and ST. -- Filter out own outputs using own address and ST.
ownOutputs <- ownOutputs <-
@ -371,9 +371,20 @@ stakeValidator stake =
pletC $ pletC $
pdata resolvedF.value #== pdata ownOutputValue 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 $ pure $
pmatch stakeRedeemer $ \case pmatch stakeRedeemer $ \case
PRetractVotes l -> unTermCont $ do PRetractVotes _ -> unTermCont $ do
pguardC pguardC
"Owner signs this transaction" "Owner signs this transaction"
ownerSignsTransaction ownerSignsTransaction
@ -383,18 +394,8 @@ stakeValidator stake =
pguardC "Proposal ST spent" proposalTokenMoved pguardC "Proposal ST spent" proposalTokenMoved
pguardC "A UTXO must exist with the correct output" $ pguardC "A UTXO must exist with the correct output" $
let expectedLocks = pfield @"locks" # l let valueCorrect = ownOutputValueUnchanged
outputDatumCorrect = onlyLocksUpdated
expectedDatum =
mkRecordConstr
PStakeDatum
( #stakedAmount .= stakeDatum.stakedAmount
.& #owner .= stakeDatum.owner
.& #lockedBy .= expectedLocks
)
valueCorrect = ownOutputValueUnchanged
outputDatumCorrect = stakeOut #== expectedDatum
in foldl1 in foldl1
(#&&) (#&&)
[ ptraceIfFalse "valueCorrect" valueCorrect [ ptraceIfFalse "valueCorrect" valueCorrect
@ -405,34 +406,21 @@ stakeValidator stake =
------------------------------------------------------------ ------------------------------------------------------------
PPermitVote l -> unTermCont $ do PPermitVote _ -> unTermCont $ do
pguardC pguardC
"Owner signs this transaction" "Owner signs this transaction"
ownerSignsTransaction ownerSignsTransaction
let proposalTokenMinted =
passetClassValueOf # txInfoF.mint # proposalSTClass #== 1
-- This puts trust into the Proposal. The Proposal must necessarily check -- This puts trust into the Proposal. The Proposal must necessarily check
-- that this is not abused. -- that this is not abused.
pguardC "Proposal ST spent" proposalTokenMoved pguardC "Proposal ST spent or minted" $
proposalTokenMoved #|| proposalTokenMinted
-- 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 "A UTXO must exist with the correct output" $ pguardC "A UTXO must exist with the correct output" $
let correctOutputDatum = stakeOut #== expectedDatum let correctOutputDatum = onlyLocksUpdated
valueCorrect = ownOutputValueUnchanged valueCorrect = ownOutputValueUnchanged
in foldl1 in foldl1
(#&&) (#&&)