587 lines
21 KiB
Haskell
587 lines
21 KiB
Haskell
{-# LANGUAGE TemplateHaskell #-}
|
|
|
|
{- |
|
|
Module : Agora.Proposal
|
|
Maintainer : emi@haskell.fyi
|
|
Description: Proposal scripts encoding effects that operate on the system.
|
|
|
|
Proposal scripts encoding effects that operate on the system.
|
|
-}
|
|
module Agora.Proposal (
|
|
-- * Haskell-land
|
|
Proposal (..),
|
|
ProposalDatum (..),
|
|
ProposalRedeemer (..),
|
|
ProposalStatus (..),
|
|
ProposalThresholds (..),
|
|
ProposalVotes (..),
|
|
ProposalId (..),
|
|
ResultTag (..),
|
|
emptyVotesFor,
|
|
|
|
-- * Plutarch-land
|
|
PProposalDatum (..),
|
|
PProposalRedeemer (..),
|
|
PProposalStatus (..),
|
|
PProposalThresholds (..),
|
|
PProposalVotes (..),
|
|
PProposalId (..),
|
|
PResultTag (..),
|
|
|
|
-- * Plutarch helpers
|
|
proposalDatumValid,
|
|
pemptyVotesFor,
|
|
pwinner,
|
|
pneutralOption,
|
|
pretractVotes,
|
|
) where
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
import Control.Applicative (Const)
|
|
import Control.Arrow (first)
|
|
import Data.Tagged (Tagged)
|
|
import GHC.Generics qualified as GHC
|
|
import Generics.SOP (Generic, I (I))
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
import PlutusLedgerApi.V1 (DatumHash, PubKeyHash, ValidatorHash)
|
|
import PlutusLedgerApi.V1.Value (AssetClass)
|
|
import PlutusTx qualified
|
|
import PlutusTx.AssocMap qualified as AssocMap
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
import Plutarch.Api.V1 (
|
|
KeyGuarantees (Unsorted),
|
|
PDatumHash,
|
|
PMap,
|
|
PPubKeyHash,
|
|
PValidatorHash,
|
|
)
|
|
import Plutarch.DataRepr (DerivePConstantViaData (..), PDataFields, PIsDataReprInstances (..))
|
|
import Plutarch.Extra.List (pnotNull)
|
|
import Plutarch.Extra.Map qualified as PM
|
|
import Plutarch.Extra.Map.Unsorted qualified as PUM
|
|
import Plutarch.Extra.TermCont (pguardC, pletC)
|
|
import Plutarch.Lift (
|
|
DerivePConstantViaNewtype (..),
|
|
PConstantDecl,
|
|
PUnsafeLiftDecl (..),
|
|
)
|
|
import Plutarch.SafeMoney (PDiscrete)
|
|
import Plutarch.TryFrom (PTryFrom (PTryFromExcess, ptryFrom'))
|
|
import Plutarch.Unsafe (punsafeCoerce)
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
import Agora.Proposal.Time (PProposalStartingTime, PProposalTimingConfig, ProposalStartingTime, ProposalTimingConfig)
|
|
import Agora.SafeMoney (GTTag)
|
|
import Agora.Utils (mustBePJust)
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Haskell-land
|
|
|
|
{- | Identifies a Proposal, issued upon creation of a proposal. In practice,
|
|
this number starts at zero, and increments by one for each proposal.
|
|
The 100th proposal will be @'ProposalId' 99@. This counter lives
|
|
in the 'Agora.Governor.Governor'. See 'Agora.Governor.nextProposalId', and
|
|
'Agora.Governor.pgetNextProposalId'.
|
|
-}
|
|
newtype ProposalId = ProposalId {proposalTag :: Integer}
|
|
deriving newtype (PlutusTx.ToData, PlutusTx.FromData, PlutusTx.UnsafeFromData)
|
|
deriving stock (Eq, Show, GHC.Generic)
|
|
|
|
{- | Encodes a result. Typically, for a Yes/No proposal, we encode it like this:
|
|
|
|
@
|
|
"No" ~ 'ResultTag' 0
|
|
"Yes" ~ 'ResultTag' 1
|
|
@
|
|
-}
|
|
newtype ResultTag = ResultTag {getResultTag :: Integer}
|
|
deriving stock (Eq, Show, Ord, GHC.Generic)
|
|
deriving newtype (PlutusTx.ToData, PlutusTx.FromData, PlutusTx.UnsafeFromData)
|
|
|
|
{- | The "status" of the proposal. This is only useful for state transitions that
|
|
need to happen as a result of a transaction as opposed to time-based "periods".
|
|
|
|
See the note on wording & the state machine in the tech-design.
|
|
|
|
If the proposal is 'VotingReady', for instance, that doesn't necessarily
|
|
mean that voting is possible, as this also requires the timing to be right.
|
|
-}
|
|
data ProposalStatus
|
|
= -- | A draft proposal represents a proposal that has yet to be realized.
|
|
--
|
|
-- In effect, this means one which didn't have enough LQ to be a full
|
|
-- proposal, and needs cosigners to enable that to happen. This is
|
|
-- similar to a "temperature check", but only useful if multiple people
|
|
-- want to pool governance tokens together. If the proposal doesn't get to
|
|
-- 'VotingReady' on time, the proposal will __never__ be able to get
|
|
-- voted on.
|
|
Draft
|
|
| -- | The proposal has/had enough GT cosigned in order to be a fully fledged
|
|
-- proposal.
|
|
--
|
|
-- This means that once the timing requirements align,
|
|
-- proposal will be able to be voted on.
|
|
VotingReady
|
|
| -- | The proposal has been voted on, and the votes have been locked
|
|
-- permanently. The proposal now goes into a locking time after the
|
|
-- normal voting time. After this, it's possible to execute the proposal.
|
|
Locked
|
|
| -- | The proposal has finished.
|
|
--
|
|
-- This can mean it's been voted on and completed, but it can also mean
|
|
-- the proposal failed due to time constraints or didn't
|
|
-- get to 'VotingReady' first.
|
|
--
|
|
-- At this stage, the 'votes' field of 'ProposalDatum' is frozen.
|
|
--
|
|
-- See 'AdvanceProposal' for documentation on state transitions.
|
|
--
|
|
-- TODO: The owner of the proposal may choose to reclaim their proposal.
|
|
Finished
|
|
deriving stock (Eq, Show, GHC.Generic)
|
|
|
|
PlutusTx.makeIsDataIndexed ''ProposalStatus [('Draft, 0), ('VotingReady, 1), ('Locked, 2), ('Finished, 3)]
|
|
|
|
{- | The threshold values for various state transitions to happen.
|
|
This data is stored centrally (in the 'Agora.Governor.Governor') and copied over
|
|
to 'Proposal's when they are created.
|
|
-}
|
|
data ProposalThresholds = ProposalThresholds
|
|
{ execute :: Tagged GTTag Integer
|
|
-- ^ How much GT minimum must a particular 'ResultTag' accumulate for it to pass.
|
|
, create :: Tagged GTTag Integer
|
|
-- ^ How much GT required to "create" a proposal.
|
|
--
|
|
-- It is recommended this be a high enough amount, in order to prevent DOS from bad
|
|
-- actors.
|
|
, vote :: Tagged GTTag Integer
|
|
-- ^ How much GT required to allow voting to happen.
|
|
-- (i.e. to move into 'VotingReady')
|
|
}
|
|
deriving stock (Eq, Show, GHC.Generic)
|
|
|
|
PlutusTx.makeIsDataIndexed ''ProposalThresholds [('ProposalThresholds, 0)]
|
|
|
|
{- | Map which encodes the total tally for each result.
|
|
It's important that the "shape" is consistent with the shape of 'effects'.
|
|
|
|
e.g. if the 'effects' field looks like the following:
|
|
|
|
@[('ResultTag' 0, []), ('ResultTag' 1, [(vh, dh)])]@
|
|
|
|
Then 'ProposalVotes' needs be of the shape:
|
|
|
|
@[('ResultTag' 0, n), ('ResultTag' 1, m)]@
|
|
-}
|
|
newtype ProposalVotes = ProposalVotes
|
|
{ getProposalVotes :: AssocMap.Map ResultTag Integer
|
|
}
|
|
deriving newtype (PlutusTx.ToData, PlutusTx.FromData, PlutusTx.UnsafeFromData)
|
|
deriving stock (Eq, Show, GHC.Generic)
|
|
|
|
-- | Create a 'ProposalVotes' that has the same shape as the 'effects' field.
|
|
emptyVotesFor :: forall a. AssocMap.Map ResultTag a -> ProposalVotes
|
|
emptyVotesFor = ProposalVotes . AssocMap.mapWithKey (const . const 0)
|
|
|
|
-- | Haskell-level datum for Proposal scripts.
|
|
data ProposalDatum = ProposalDatum
|
|
{ proposalId :: ProposalId
|
|
-- ^ Identification of the proposal.
|
|
-- TODO: could we encode this more efficiently?
|
|
-- This is shaped this way for future proofing.
|
|
-- See https://github.com/Liqwid-Labs/agora/issues/39
|
|
, effects :: AssocMap.Map ResultTag (AssocMap.Map ValidatorHash DatumHash)
|
|
-- ^ Effect lookup table. First by result, then by effect hash.
|
|
, status :: ProposalStatus
|
|
-- ^ The status the proposal is in.
|
|
, cosigners :: [PubKeyHash]
|
|
-- ^ Who created the proposal initially, and who cosigned it later.
|
|
, thresholds :: ProposalThresholds
|
|
-- ^ Thresholds copied over on initialization.
|
|
, votes :: ProposalVotes
|
|
-- ^ Vote tally on the proposal
|
|
, timingConfig :: ProposalTimingConfig
|
|
-- ^ Timing configuration copied over on initialization.
|
|
, startingTime :: ProposalStartingTime
|
|
-- ^ The time upon the creation of the proposal.
|
|
}
|
|
deriving stock (Eq, Show, GHC.Generic)
|
|
|
|
PlutusTx.makeIsDataIndexed ''ProposalDatum [('ProposalDatum, 0)]
|
|
|
|
-- | Haskell-level redeemer for Proposal scripts.
|
|
data ProposalRedeemer
|
|
= -- | Cast one or more votes towards a particular 'ResultTag'.
|
|
Vote ResultTag
|
|
| -- | Add one or more public keys to the cosignature list.
|
|
-- Must be signed by those cosigning.
|
|
--
|
|
-- This is particularly used in the 'Draft' 'ProposalStatus',
|
|
-- where matching 'Agora.Stake.Stake's can be called to advance the proposal,
|
|
-- provided enough GT is shared among them.
|
|
Cosign [PubKeyHash]
|
|
| -- | Allow unlocking one or more stakes with votes towards particular 'ResultTag'.
|
|
Unlock ResultTag
|
|
| -- | Advance the proposal, performing the required checks for whether that is legal.
|
|
--
|
|
-- These are roughly the checks for each possible transition:
|
|
--
|
|
-- === @'Draft' -> 'VotingReady'@:
|
|
--
|
|
-- 1. The sum of all of the cosigner's GT is larger than the 'startVoting' field of 'ProposalThresholds'.
|
|
-- 2. The proposal's current time ensures 'isDraftPeriod'.
|
|
--
|
|
-- === @'VotingReady' -> 'Locked'@:
|
|
--
|
|
-- 1. The sum of all votes is larger than 'countVoting'.
|
|
-- 2. The winning 'ResultTag' has more votes than all other 'ResultTag's.
|
|
-- 3. The proposal's current time ensures 'isVotingPeriod'.
|
|
--
|
|
-- === @'Locked' -> 'Finished'@:
|
|
--
|
|
-- 1. The proposal's current time ensures 'isExecutionPeriod'.
|
|
-- 2. The transaction mints the GATs to the receiving effects.
|
|
--
|
|
-- === @* -> 'Finished'@:
|
|
--
|
|
-- If the proposal has run out of time for the current 'ProposalStatus', it will always be possible
|
|
-- to transition into 'Finished' status, because it has expired (and failed).
|
|
AdvanceProposal
|
|
deriving stock (Eq, Show, GHC.Generic)
|
|
|
|
PlutusTx.makeIsDataIndexed
|
|
''ProposalRedeemer
|
|
[ ('Vote, 0)
|
|
, ('Cosign, 1)
|
|
, ('Unlock, 2)
|
|
, ('AdvanceProposal, 3)
|
|
]
|
|
|
|
-- | Parameters that identify the Proposal validator script.
|
|
data Proposal = Proposal
|
|
{ governorSTAssetClass :: AssetClass
|
|
, stakeSTAssetClass :: AssetClass
|
|
, maximumCosigners :: Integer
|
|
-- ^ Arbitrary limit for maximum amount of cosigners on a proposal.
|
|
}
|
|
deriving stock (Show, Eq, GHC.Generic)
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Plutarch-land
|
|
|
|
-- | Plutarch-level version of 'ResultTag'.
|
|
newtype PResultTag (s :: S) = PResultTag (Term s PInteger)
|
|
deriving (PlutusType, PIsData, PEq, POrd) via (DerivePNewtype PResultTag PInteger)
|
|
|
|
instance PUnsafeLiftDecl PResultTag where type PLifted PResultTag = ResultTag
|
|
deriving via
|
|
(DerivePConstantViaNewtype ResultTag PResultTag PInteger)
|
|
instance
|
|
(PConstantDecl ResultTag)
|
|
|
|
-- FIXME: This instance and the one below, for 'PProposalId', should be derived.
|
|
-- Soon this will be possible through 'DerivePNewtype'.
|
|
instance PTryFrom PData (PAsData PResultTag) where
|
|
type PTryFromExcess PData (PAsData PResultTag) = PTryFromExcess PData (PAsData PInteger)
|
|
ptryFrom' d k =
|
|
ptryFrom' @_ @(PAsData PInteger) d $
|
|
-- JUSTIFICATION:
|
|
-- We are coercing from @PAsData PInteger@ to @PAsData PResultTag@.
|
|
-- Since 'PResultTag' is a simple newtype, their shape is the same.
|
|
|
|
-- JUSTIFICATION:
|
|
-- We are coercing from @PAsData PInteger@ to @PAsData PResultTag@.
|
|
-- Since 'PResultTag' is a simple newtype, their shape is the same.
|
|
k . first punsafeCoerce
|
|
|
|
-- | Plutarch-level version of 'PProposalId'.
|
|
newtype PProposalId (s :: S) = PProposalId (Term s PInteger)
|
|
deriving (PlutusType, PIsData, PEq, POrd) via (DerivePNewtype PProposalId PInteger)
|
|
|
|
instance PTryFrom PData (PAsData PProposalId) where
|
|
type PTryFromExcess PData (PAsData PProposalId) = PTryFromExcess PData (PAsData PInteger)
|
|
ptryFrom' d k =
|
|
ptryFrom' @_ @(PAsData PInteger) d $
|
|
-- JUSTIFICATION:
|
|
-- We are coercing from @PAsData PInteger@ to @PAsData PProposalId@.
|
|
-- Since 'PProposalId' is a simple newtype, their shape is the same.
|
|
|
|
-- JUSTIFICATION:
|
|
-- We are coercing from @PAsData PInteger@ to @PAsData PProposalId@.
|
|
-- Since 'PProposalId' is a simple newtype, their shape is the same.
|
|
k . first punsafeCoerce
|
|
|
|
instance PUnsafeLiftDecl PProposalId where type PLifted PProposalId = ProposalId
|
|
deriving via
|
|
(DerivePConstantViaNewtype ProposalId PProposalId PInteger)
|
|
instance
|
|
(PConstantDecl ProposalId)
|
|
|
|
-- | Plutarch-level version of 'ProposalStatus'.
|
|
data PProposalStatus (s :: S)
|
|
= -- TODO: 'PProposalStatus' ought te be encoded as 'PInteger'.
|
|
-- e.g. like Tilde used 'pmatchEnum'.
|
|
PDraft (Term s (PDataRecord '[]))
|
|
| PVotingReady (Term s (PDataRecord '[]))
|
|
| PLocked (Term s (PDataRecord '[]))
|
|
| PFinished (Term s (PDataRecord '[]))
|
|
deriving stock (GHC.Generic)
|
|
deriving anyclass (Generic)
|
|
deriving anyclass (PIsDataRepr)
|
|
deriving
|
|
(PlutusType, PIsData, PEq)
|
|
via PIsDataReprInstances PProposalStatus
|
|
|
|
instance PUnsafeLiftDecl PProposalStatus where type PLifted PProposalStatus = ProposalStatus
|
|
deriving via (DerivePConstantViaData ProposalStatus PProposalStatus) instance (PConstantDecl ProposalStatus)
|
|
|
|
-- | Plutarch-level version of 'ProposalThresholds'.
|
|
newtype PProposalThresholds (s :: S) = PProposalThresholds
|
|
{ getProposalThresholds ::
|
|
Term
|
|
s
|
|
( PDataRecord
|
|
'[ "execute" ':= PDiscrete GTTag
|
|
, "create" ':= PDiscrete GTTag
|
|
, "vote" ':= PDiscrete GTTag
|
|
]
|
|
)
|
|
}
|
|
deriving stock (GHC.Generic)
|
|
deriving anyclass (Generic)
|
|
deriving anyclass (PIsDataRepr)
|
|
deriving
|
|
(PlutusType, PIsData, PDataFields)
|
|
via (PIsDataReprInstances PProposalThresholds)
|
|
|
|
instance PUnsafeLiftDecl PProposalThresholds where type PLifted PProposalThresholds = ProposalThresholds
|
|
deriving via (DerivePConstantViaData ProposalThresholds PProposalThresholds) instance (PConstantDecl ProposalThresholds)
|
|
|
|
-- | Plutarch-level version of 'ProposalVotes'.
|
|
newtype PProposalVotes (s :: S)
|
|
= PProposalVotes (Term s (PMap 'Unsorted PResultTag PInteger))
|
|
deriving (PlutusType, PIsData) via (DerivePNewtype PProposalVotes (PMap 'Unsorted PResultTag PInteger))
|
|
|
|
-- | Retract votes given the option and the amount of votes.
|
|
pretractVotes :: Term s (PResultTag :--> PInteger :--> PProposalVotes :--> PProposalVotes)
|
|
pretractVotes = phoistAcyclic $
|
|
plam $ \rt count votes ->
|
|
let voteMap :: Term _ (PMap 'Unsorted PResultTag PInteger)
|
|
voteMap = pto votes
|
|
in pcon $
|
|
PProposalVotes $
|
|
PM.pupdate
|
|
# plam
|
|
( \oldCount -> unTermCont $ do
|
|
newCount <- pletC $ oldCount - count
|
|
pguardC "Resulting vote count greater or equal to 0" $ 0 #<= newCount
|
|
pure $ pcon $ PJust newCount
|
|
)
|
|
# rt
|
|
# voteMap
|
|
|
|
instance PUnsafeLiftDecl PProposalVotes where type PLifted PProposalVotes = ProposalVotes
|
|
deriving via
|
|
(DerivePConstantViaNewtype ProposalVotes PProposalVotes (PMap 'Unsorted PResultTag PInteger))
|
|
instance
|
|
(PConstantDecl ProposalVotes)
|
|
|
|
-- | Plutarch-level version of 'emptyVotesFor'.
|
|
pemptyVotesFor :: forall s a. (PIsData a) => Term s (PMap 'Unsorted PResultTag a :--> PProposalVotes)
|
|
pemptyVotesFor =
|
|
phoistAcyclic $
|
|
plam
|
|
( \m ->
|
|
pcon $
|
|
PProposalVotes $ PM.pmap # plam (const $ pconstant 0) # m
|
|
)
|
|
|
|
-- | Plutarch-level version of 'ProposalDatum'.
|
|
newtype PProposalDatum (s :: S) = PProposalDatum
|
|
{ getProposalDatum ::
|
|
Term
|
|
s
|
|
( PDataRecord
|
|
'[ "proposalId" ':= PProposalId
|
|
, "effects" ':= PMap 'Unsorted PResultTag (PMap 'Unsorted PValidatorHash PDatumHash)
|
|
, "status" ':= PProposalStatus
|
|
, "cosigners" ':= PBuiltinList (PAsData PPubKeyHash)
|
|
, "thresholds" ':= PProposalThresholds
|
|
, "votes" ':= PProposalVotes
|
|
, "timingConfig" ':= PProposalTimingConfig
|
|
, "startingTime" ':= PProposalStartingTime
|
|
]
|
|
)
|
|
}
|
|
deriving stock (GHC.Generic)
|
|
deriving anyclass (Generic)
|
|
deriving anyclass (PIsDataRepr)
|
|
deriving
|
|
(PlutusType, PIsData, PDataFields, PEq)
|
|
via (PIsDataReprInstances PProposalDatum)
|
|
|
|
-- TODO: Derive this.
|
|
instance PTryFrom PData (PAsData PProposalDatum) where
|
|
type PTryFromExcess PData (PAsData PProposalDatum) = Const ()
|
|
ptryFrom' d k =
|
|
k (punsafeCoerce d, ())
|
|
|
|
instance PUnsafeLiftDecl PProposalDatum where type PLifted PProposalDatum = ProposalDatum
|
|
deriving via (DerivePConstantViaData ProposalDatum PProposalDatum) instance (PConstantDecl ProposalDatum)
|
|
|
|
-- | Plutarch-level version of 'ProposalRedeemer'.
|
|
data PProposalRedeemer (s :: S)
|
|
= PVote (Term s (PDataRecord '["resultTag" ':= PResultTag]))
|
|
| PCosign (Term s (PDataRecord '["newCosigners" ':= PBuiltinList (PAsData PPubKeyHash)]))
|
|
| PUnlock (Term s (PDataRecord '["resultTag" ':= PResultTag]))
|
|
| PAdvanceProposal (Term s (PDataRecord '[]))
|
|
deriving stock (GHC.Generic)
|
|
deriving anyclass (Generic)
|
|
deriving anyclass (PIsDataRepr)
|
|
deriving
|
|
(PlutusType, PIsData)
|
|
via PIsDataReprInstances PProposalRedeemer
|
|
|
|
-- See below.
|
|
instance PTryFrom PData (PAsData PProposalRedeemer) where
|
|
type PTryFromExcess PData (PAsData PProposalRedeemer) = Const ()
|
|
ptryFrom' d k =
|
|
k (punsafeCoerce d, ())
|
|
|
|
-- TODO: Waiting on PTryFrom for 'PPubKeyHash'
|
|
-- deriving via
|
|
-- PAsData (PIsDataReprInstances PProposalRedeemer)
|
|
-- instance
|
|
-- PTryFrom PData (PAsData PProposalRedeemer)
|
|
|
|
instance PUnsafeLiftDecl PProposalRedeemer where type PLifted PProposalRedeemer = ProposalRedeemer
|
|
deriving via (DerivePConstantViaData ProposalRedeemer PProposalRedeemer) instance (PConstantDecl ProposalRedeemer)
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
{- | Check for various invariants a proposal must uphold.
|
|
This can be used to check both upon creation and
|
|
upon any following state transitions in the proposal.
|
|
-}
|
|
proposalDatumValid :: Proposal -> Term s (Agora.Proposal.PProposalDatum :--> PBool)
|
|
proposalDatumValid proposal =
|
|
phoistAcyclic $
|
|
plam $ \datum' -> unTermCont $ do
|
|
datum <- tcont $ pletFields @'["effects", "cosigners", "votes"] $ datum'
|
|
|
|
let atLeastOneNegativeResult =
|
|
pany
|
|
# phoistAcyclic
|
|
(plam $ \m -> pnull #$ pto $ pfromData $ psndBuiltin # m)
|
|
#$ pto
|
|
$ pfromData datum.effects
|
|
|
|
pure $
|
|
foldr1
|
|
(#&&)
|
|
[ ptraceIfFalse "Proposal has at least one ResultTag has no effects" atLeastOneNegativeResult
|
|
, ptraceIfFalse "Proposal has at least one cosigner" $ pnotNull # pfromData datum.cosigners
|
|
, ptraceIfFalse "Proposal has fewer cosigners than the limit" $ plength # pfromData datum.cosigners #<= pconstant proposal.maximumCosigners
|
|
, ptraceIfFalse "Proposal votes and effects are compatible with each other" $ PUM.pkeysEqual # datum.effects # pto (pfromData datum.votes)
|
|
]
|
|
|
|
{- | Find the winner result tag, given the votes, the quorum the "neutral" result tag.
|
|
|
|
The winner should be unambiguous, meaning that if two options have the same highest votes,
|
|
the "neutral" option will be the winner.
|
|
-}
|
|
pwinner ::
|
|
Term
|
|
s
|
|
( PProposalVotes
|
|
:--> PInteger
|
|
:--> PResultTag
|
|
:--> PResultTag
|
|
)
|
|
pwinner = phoistAcyclic $
|
|
plam $ \votes quorum neutral -> unTermCont $ do
|
|
winner <- pletC $ phighestVotes # votes
|
|
winnerResultTag <- pletC $ pfromData $ pfstBuiltin # winner
|
|
highestVotes <- pletC $ pfromData $ psndBuiltin # winner
|
|
|
|
let l :: Term _ (PBuiltinList _)
|
|
l = pto $ pto votes
|
|
|
|
f ::
|
|
Term
|
|
_
|
|
( PBuiltinPair (PAsData PResultTag) (PAsData PInteger)
|
|
:--> PInteger
|
|
:--> PInteger
|
|
)
|
|
f = plam $ \(pfromData . (psndBuiltin #) -> thisVotes) i ->
|
|
pif
|
|
(thisVotes #== highestVotes)
|
|
(i + 1)
|
|
i
|
|
|
|
noDuplicateHighestVotes =
|
|
ptraceIfFalse "Ambiguous winner" $
|
|
pfoldr # f # 0 # l #== 1
|
|
|
|
exceedQuorum =
|
|
ptraceIfFalse "Highest vote count should exceed the minimum threshold" $
|
|
quorum #< highestVotes
|
|
|
|
pure $
|
|
pif
|
|
(noDuplicateHighestVotes #&& exceedQuorum)
|
|
winnerResultTag
|
|
neutral
|
|
|
|
-- | Find the winning outcome (and the corresponding vote count) given the votes.
|
|
phighestVotes ::
|
|
Term
|
|
s
|
|
( PProposalVotes
|
|
:--> PBuiltinPair (PAsData PResultTag) (PAsData PInteger)
|
|
)
|
|
phighestVotes = phoistAcyclic $
|
|
plam $ \votes ->
|
|
let l :: Term _ (PBuiltinList _)
|
|
l = pto $ pto votes
|
|
|
|
f ::
|
|
Term
|
|
_
|
|
( PBuiltinPair (PAsData PResultTag) (PAsData PInteger)
|
|
:--> PBuiltinPair (PAsData PResultTag) (PAsData PInteger)
|
|
:--> PBuiltinPair (PAsData PResultTag) (PAsData PInteger)
|
|
)
|
|
f = phoistAcyclic $
|
|
plam $ \this last ->
|
|
let lastVotes = pfromData $ psndBuiltin # last
|
|
thisVotes = pfromData $ psndBuiltin # this
|
|
in pif (lastVotes #< thisVotes) this last
|
|
in pfoldr # f # (phead # l) # l
|
|
|
|
-- | Find the "neutral" option (a dummy outcome with no effect) given the effects.
|
|
pneutralOption ::
|
|
Term
|
|
s
|
|
( PMap 'Unsorted PResultTag (PMap 'Unsorted PValidatorHash PDatumHash)
|
|
:--> PResultTag
|
|
)
|
|
pneutralOption = phoistAcyclic $
|
|
plam $ \effects ->
|
|
let l :: Term _ (PBuiltinList (PBuiltinPair (PAsData PResultTag) _))
|
|
l = pto effects
|
|
|
|
f :: Term _ (PBuiltinPair (PAsData PResultTag) (PAsData (PMap 'Unsorted _ _)) :--> PBool)
|
|
f = phoistAcyclic $
|
|
plam $ \((pfromData . (psndBuiltin #) -> el)) ->
|
|
let el' :: Term _ (PBuiltinList _)
|
|
el' = pto el
|
|
in pnull # el'
|
|
in pfromData $ pfstBuiltin #$ mustBePJust # "No neutral option" #$ pfind # f # l
|