agora/agora/Agora/Stake.hs

384 lines
11 KiB
Haskell

{-# LANGUAGE TemplateHaskell #-}
{- |
Module : Agora.Stake
Maintainer : emi@haskell.fyi
Description: Vote-lockable stake UTXOs holding GT.
Vote-lockable stake UTXOs holding GT.
-}
module Agora.Stake (
-- * Haskell-land
StakeDatum (..),
StakeRedeemer (..),
Stake (..),
ProposalLock (..),
-- * Plutarch-land
PStakeDatum (..),
PStakeRedeemer (..),
PProposalLock (..),
PStakeUsage (..),
-- * Utility functions
stakeLocked,
pgetStakeUsage,
) where
import Agora.Plutarch.Orphans ()
import Agora.Proposal (PProposalId, PResultTag, ProposalId (..), ResultTag (..))
import Agora.SafeMoney (GTTag)
import Data.Tagged (Tagged (..))
import GHC.Generics qualified as GHC
import Generics.SOP (Generic, HasDatatypeInfo, I (I))
import Plutarch.Api.V1 (
PPubKeyHash,
)
import Plutarch.DataRepr (
DerivePConstantViaData (..),
PDataFields,
PIsDataReprInstances (PIsDataReprInstances),
)
import Plutarch.Extra.IsData (
DerivePConstantViaDataList (..),
ProductIsData (ProductIsData),
)
import Plutarch.Extra.List (pmapMaybe, pnotNull)
import Plutarch.Extra.Other (DerivePNewtype' (..))
import Plutarch.Extra.TermCont (pletFieldsC)
import Plutarch.Lift (PConstantDecl, PUnsafeLiftDecl (..))
import Plutarch.SafeMoney (PDiscrete)
import PlutusLedgerApi.V1 (PubKeyHash)
import PlutusLedgerApi.V1.Value (AssetClass)
import PlutusTx qualified
import Prelude hiding (Num (..))
--------------------------------------------------------------------------------
{- | Parameters for creating Stake scripts.
@since 0.1.0
-}
data Stake = Stake
{ gtClassRef :: Tagged GTTag AssetClass
-- ^ Used when inlining the AssetClass of a 'PDiscrete' in the script code.
, proposalSTClass :: AssetClass
}
deriving stock
( -- | @since 0.1.0
GHC.Generic
)
{- | A lock placed on a Stake datum in order to prevent
depositing and withdrawing when votes are in place.
NOTE: Due to retracting votes always being possible,
this lock will only lock with contention on the proposal.
FIXME: Contention on Proposals could create contention
on voting which in turn creates contention on stakers.
Vaguely this is the dependency graph for this locking
interaction. Both the stake validator and the proposal
validator are only able to check for one another through
the datum belonging to the ST:
@
┌─────────────────┐ ┌────────────────────┐
│ Stake Validator ├─┐ │ Proposal Validator │
└────────┬────────┘ │ └──────┬─────┬───────┘
│ │ │ │
│ ┌─┼────────┘ │
▼ │ │ ▼
┌──────────────┐ │ │ ┌─────────────────┐
│ Stake Policy │◄─┘ └►│ Proposal Policy │
└──────────────┘ └─────────────────┘
@
@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.
}
deriving stock
( -- | @since 0.1.0
Show
, -- | @since 0.1.0
GHC.Generic
)
deriving anyclass (Generic)
deriving
( -- | @since 0.1.0
PlutusTx.ToData
, -- | @since 0.1.0
PlutusTx.FromData
, -- | @since 0.1.0
PlutusTx.UnsafeFromData
)
via (ProductIsData ProposalLock)
{- | Haskell-level redeemer for Stake scripts.
@since 0.1.0
-}
data StakeRedeemer
= -- | Deposit or withdraw a discrete amount of the staked governance token.
-- Stake must be unlocked.
DepositWithdraw (Tagged GTTag Integer)
| -- | Destroy a stake, retrieving its LQ, the minimum ADA and any other assets.
-- Stake must be unlocked.
Destroy
| -- | Permit a Vote to be added onto a 'Agora.Proposal.Proposal'.
-- This also adds a lock to the 'lockedBy' field. See 'ProposalLock'.
-- 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
| -- | 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]
| -- | 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
deriving stock (Show, GHC.Generic)
PlutusTx.makeIsDataIndexed
''StakeRedeemer
[ ('DepositWithdraw, 0)
, ('Destroy, 1)
, ('PermitVote, 2)
, ('RetractVotes, 3)
, ('WitnessStake, 4)
]
{- | Haskell-level datum for Stake scripts.
@since 0.1.0
-}
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.
, owner :: PubKeyHash
-- ^ The hash of the public key this stake belongs to.
--
-- TODO Support for MultiSig/Scripts is tracked here:
-- 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.
}
deriving stock (Show, GHC.Generic)
deriving anyclass (Generic)
deriving
( -- | @since 0.1.0
PlutusTx.ToData
, -- | @since 0.1.0
PlutusTx.FromData
)
via (ProductIsData StakeDatum)
--------------------------------------------------------------------------------
{- | Plutarch-level datum for Stake scripts.
@since 0.1.0
-}
newtype PStakeDatum (s :: S) = PStakeDatum
{ getStakeDatum ::
Term
s
( PDataRecord
'[ "stakedAmount" ':= PDiscrete GTTag
, "owner" ':= PPubKeyHash
, "lockedBy" ':= PBuiltinList (PAsData PProposalLock)
]
)
}
deriving stock
( -- | @since 0.1.0
GHC.Generic
)
deriving anyclass
( -- | @since 0.1.0
Generic
)
deriving anyclass
( -- | @since 0.1.0
PIsDataRepr
)
deriving
( -- | @since 0.1.0
PlutusType
, -- | @since 0.1.0
PIsData
, -- | @since 0.1.0
PDataFields
, -- | @since 0.1.0
PEq
)
via (DerivePNewtype' PStakeDatum)
-- | @since 0.1.0
instance Plutarch.Lift.PUnsafeLiftDecl PStakeDatum where type PLifted PStakeDatum = StakeDatum
-- | @since 0.1.0
deriving via (DerivePConstantViaDataList StakeDatum PStakeDatum) instance (Plutarch.Lift.PConstantDecl StakeDatum)
-- | @since 0.1.0
deriving via PAsData (DerivePNewtype' PStakeDatum) instance PTryFrom PData (PAsData PStakeDatum)
{- | Plutarch-level redeemer for Stake scripts.
@since 0.1.0
-}
data PStakeRedeemer (s :: S)
= -- | Deposit or withdraw a discrete amount of the staked governance token.
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)]))
| PWitnessStake (Term s (PDataRecord '[]))
deriving stock
( -- | @since 0.1.0
GHC.Generic
)
deriving anyclass
( -- | @since 0.1.0
Generic
)
deriving anyclass
( -- | @since 0.1.0
PIsDataRepr
)
deriving
( -- | @since 0.1.0
PlutusType
, -- | @since 0.1.0
PIsData
)
via PIsDataReprInstances PStakeRedeemer
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)
{- | Plutarch-level version of 'ProposalLock'.
@since 0.1.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
deriving stock
( -- | @since 0.1.0
GHC.Generic
)
deriving anyclass
( -- | @since 0.1.0
Generic
, -- | @since 0.1.0
PlutusType
, -- | @since 0.1.0
HasDatatypeInfo
, -- | @since 0.1.0
PEq
)
{- | / 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
-}
pgetStakeUsage ::
Term
_
( PBuiltinList (PAsData PProposalLock)
:--> PProposalId
:--> PStakeUsage
)
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 $
pif
(lockF.proposalTag #== pid)
(pcon $ PJust lock')
(pcon PNothing)
)
# locks
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)