stake validator basics

This commit is contained in:
Emily Martins 2022-02-24 00:20:57 +01:00
parent b48d797747
commit 3a1fba39b9
4 changed files with 192 additions and 102 deletions

View file

@ -25,4 +25,5 @@ benchmarks =
"full_scripts" "full_scripts"
[ bench "authorityTokenPolicy" $ Agora.authorityTokenPolicy (Agora.AuthorityToken (Value.assetClass "" "")) [ bench "authorityTokenPolicy" $ Agora.authorityTokenPolicy (Agora.AuthorityToken (Value.assetClass "" ""))
, bench "stakePolicy" $ Agora.stakePolicy (Agora.Stake @Agora.LQ) , bench "stakePolicy" $ Agora.stakePolicy (Agora.Stake @Agora.LQ)
, bench "stakeValidator" $ Agora.stakeValidator (Agora.Stake @Agora.LQ)
] ]

View file

@ -25,15 +25,10 @@ import Prelude
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
import Plutus.V1.Ledger.Value (AssetClass (..))
import Plutus.V1.Ledger.Value qualified as Ledger
--------------------------------------------------------------------------------
import Plutarch.Api.V1 import Plutarch.Api.V1
import Plutarch.Builtin import Plutarch.Builtin
import Plutarch.Internal import Plutarch.Internal
import Plutarch.Prelude import Plutarch.Monadic qualified as P
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@ -55,26 +50,15 @@ newtype Discrete (mc :: MoneyClass) (s :: S)
= Discrete (Term s PInteger) = Discrete (Term s PInteger)
deriving (PlutusType, PIsData, PEq, POrd) via (DerivePNewtype (Discrete mc) PInteger) deriving (PlutusType, PIsData, PEq, POrd) via (DerivePNewtype (Discrete mc) PInteger)
instance Num (Term s (Discrete mc)) where -- In the future, this should use plutarch-numeric
(+) x y = pcon $
Discrete . unTermCont $ do -- | Add two `Discrete` values of the same MoneyClass
Discrete x' <- tcont $ pmatch x paddDiscrete :: Term s (Discrete mc :--> Discrete mc :--> Discrete mc)
Discrete y' <- tcont $ pmatch y paddDiscrete = phoistAcyclic $
pure (x' + y') plam $ \x y -> P.do
abs x = pcon $ Discrete x' <- pmatch x
Discrete . unTermCont $ do Discrete y' <- pmatch y
Discrete x' <- tcont $ pmatch x pcon (Discrete $ x' + y')
pure (abs x')
negate x = pcon $
Discrete . unTermCont $ do
Discrete x' <- tcont $ pmatch x
pure (negate x')
(*) x y = pcon $
Discrete . unTermCont $ do
Discrete x' <- tcont $ pmatch x
Discrete y' <- tcont $ pmatch y
pure (x' * y')
fromInteger = error "Tried to `fromInteger` for a Discrete type. use `discrete` quasiquoter instead."
(^*) :: Term s (Discrete mc) -> Term s PInteger -> Term s (Discrete mc) (^*) :: Term s (Discrete mc) -> Term s PInteger -> Term s (Discrete mc)
(^*) x y = pcon $ (^*) x y = pcon $

View file

@ -1,10 +1,11 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE PolyKinds #-} {-# LANGUAGE PolyKinds #-}
{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE UndecidableInstances #-}
-- | Vote-lockable stake UTXOs holding GT -- | Vote-lockable stake UTXOs holding GT
module Agora.Stake ( module Agora.Stake (
StakeDatum (..), PStakeDatum (..),
StakeAction (..), PStakeAction (..),
Stake (..), Stake (..),
stakePolicy, stakePolicy,
stakeValidator, stakeValidator,
@ -22,6 +23,7 @@ import Prelude
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
import Plutarch (popaque)
import Plutarch.Api.V1 import Plutarch.Api.V1
import Plutarch.DataRepr ( import Plutarch.DataRepr (
PDataFields, PDataFields,
@ -39,19 +41,19 @@ import Agora.Utils
data Stake (gt :: MoneyClass) = Stake data Stake (gt :: MoneyClass) = Stake
data StakeAction (gt :: MoneyClass) (s :: S) data PStakeAction (gt :: MoneyClass) (s :: S)
= -- | Deposit or withdraw a discrete amount of the staked governance token = -- | Deposit or withdraw a discrete amount of the staked governance token
DepositWithdraw (Term s (PDataRecord '["delta" ':= Discrete gt])) PDepositWithdraw (Term s (PDataRecord '["delta" ':= Discrete gt]))
| -- | 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
Destroy (Term s (PDataRecord '[])) PDestroy (Term s (PDataRecord '[]))
deriving stock (GHC.Generic) deriving stock (GHC.Generic)
deriving anyclass (Generic) deriving anyclass (Generic)
deriving anyclass (PIsDataRepr) deriving anyclass (PIsDataRepr)
deriving deriving
(PlutusType, PIsData) (PlutusType, PIsData)
via PIsDataReprInstances (StakeAction gt) via PIsDataReprInstances (PStakeAction gt)
newtype StakeDatum (gt :: MoneyClass) (s :: S) = StakeDatum newtype PStakeDatum (gt :: MoneyClass) (s :: S) = PStakeDatum
{ getStakeDatum :: { getStakeDatum ::
( Term ( Term
s s
@ -67,51 +69,7 @@ newtype StakeDatum (gt :: MoneyClass) (s :: S) = StakeDatum
deriving anyclass (PIsDataRepr) deriving anyclass (PIsDataRepr)
deriving deriving
(PlutusType, PIsData, PDataFields) (PlutusType, PIsData, PDataFields)
via (PIsDataReprInstances (StakeDatum gt)) via (PIsDataReprInstances (PStakeDatum gt))
-- | Check if any output matches the predicate
anyOutput ::
forall (datum :: PType) s.
( PIsData datum
) =>
Term s (PTxInfo :--> (PValue :--> PAddress :--> datum :--> PBool) :--> PBool)
anyOutput = phoistAcyclic $
plam $ \txInfo' predicate -> P.do
txInfo <- pletFields @'["outputs"] txInfo'
pany
# ( plam $ \txOut'' -> P.do
PTxOut txOut' <- pmatch (pfromData txOut'')
txOut <- pletFields @'["value", "datumHash", "address"] txOut'
PDJust dh <- pmatch txOut.datumHash
pmatch (pfindDatum' @datum # (pfield @"_0" # dh) # txInfo') $ \case
PJust datum -> P.do
predicate # txOut.value # txOut.address # pfromData datum
PNothing -> pcon PFalse
)
# pfromData txInfo.outputs
-- | Check if any (resolved) input matches the predicate
anyInput ::
forall (datum :: PType) s.
( PIsData datum
) =>
Term s (PTxInfo :--> (PValue :--> PAddress :--> datum :--> PBool) :--> PBool)
anyInput = phoistAcyclic $
plam $ \txInfo' predicate -> P.do
txInfo <- pletFields @'["inputs"] txInfo'
pany
# ( plam $ \txInInfo'' -> P.do
PTxInInfo txInInfo' <- pmatch (pfromData txInInfo'')
let txOut'' = pfield @"resolved" # txInInfo'
PTxOut txOut' <- pmatch (pfromData txOut'')
txOut <- pletFields @'["value", "datumHash", "address"] txOut'
PDJust dh <- pmatch txOut.datumHash
pmatch (pfindDatum' @datum # (pfield @"_0" # dh) # txInfo') $ \case
PJust datum -> P.do
predicate # txOut.value # txOut.address # pfromData datum
PNothing -> pcon PFalse
)
# pfromData txInfo.inputs
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- --
@ -135,7 +93,7 @@ stakePolicy ::
, gt ~ '(ac, n, scale) , gt ~ '(ac, n, scale)
) => ) =>
Stake gt -> Stake gt ->
Term s (PData :--> PAsData PScriptContext :--> PUnit) Term s PMintingPolicy
stakePolicy _stake = stakePolicy _stake =
plam $ \_redeemer ctx' -> P.do plam $ \_redeemer ctx' -> P.do
ctx <- pletFields @'["txInfo", "purpose"] ctx' ctx <- pletFields @'["txInfo", "purpose"] ctx'
@ -144,36 +102,35 @@ stakePolicy _stake =
PMinting ownSymbol' <- pmatch $ pfromData ctx.purpose PMinting ownSymbol' <- pmatch $ pfromData ctx.purpose
ownSymbol <- plet $ pfield @"_0" # ownSymbol' ownSymbol <- plet $ pfield @"_0" # ownSymbol'
valueSpent <- plet $ pvalueSpent # pfromData txInfo' spentST <- plet $ psymbolValueOf # ownSymbol #$ pvalueSpent # pfromData txInfo'
mintedST <- plet $ psymbolValueOf # ownSymbol # txInfo.mint mintedST <- plet $ psymbolValueOf # ownSymbol # txInfo.mint
-- inputST <- plet $ stOf # (pvalueSpent # pfromData txInfo')
let burning = P.do let burning = P.do
passert "ST at inputs must be 1" $ passert "ST at inputs must be 1" $
psymbolValueOf # ownSymbol # valueSpent #== 1 spentST #== 1
passert "ST burned" $ passert "ST burned" $
mintedST #== -1 mintedST #== -1
passert "An unlocked input existed containing an ST" $ passert "An unlocked input existed containing an ST" $
anyInput @(StakeDatum gt) # pfromData txInfo' anyInput @(PStakeDatum gt) # pfromData txInfo'
#$ plam #$ plam
$ \value _ stakeDatum' -> P.do $ \value _ stakeDatum' -> P.do
let hasST = psymbolValueOf # ownSymbol # value #== 1 let hasST = psymbolValueOf # ownSymbol # value #== 1
let unlocked = pnot # (stakeLocked # stakeDatum') let unlocked = pnot # (stakeLocked # stakeDatum')
hasST #&& unlocked hasST #&& unlocked
pconstant () popaque (pconstant ())
let minting = P.do let minting = P.do
passert "ST at inputs must be 0" $ passert "ST at inputs must be 0" $
psymbolValueOf # ownSymbol # valueSpent #== 0 spentST #== 0
passert "Minted ST must be exactly 1" $ passert "Minted ST must be exactly 1" $
mintedST #== 1 mintedST #== 1
passert "A UTXO must exist with the correct output" $ passert "A UTXO must exist with the correct output" $
anyOutput @(StakeDatum gt) # pfromData txInfo' anyOutput @(PStakeDatum gt) # pfromData txInfo'
#$ plam #$ plam
$ \value address stakeDatum' -> P.do $ \value address stakeDatum' -> P.do
let cred = pfield @"credential" # address let cred = pfield @"credential" # address
@ -200,17 +157,10 @@ stakePolicy _stake =
let valueCorrect = pdata value #== pdata expectedValue let valueCorrect = pdata value #== pdata expectedValue
ownerSignsTransaction #&& valueCorrect ownerSignsTransaction #&& valueCorrect
pconstant () popaque (pconstant ())
pif (0 #< mintedST) minting burning pif (0 #< mintedST) minting burning
-- | Check whether a Stake is locked. If it is locked, various actions are unavailable.
stakeLocked :: forall (gt :: MoneyClass) s. Term s (StakeDatum gt :--> PBool)
stakeLocked = phoistAcyclic $
plam $ \_stakeDatum ->
-- TODO: when we extend this to support proposals, this will need to do something
pcon PFalse
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
stakeValidator :: stakeValidator ::
forall (gt :: MoneyClass) ac n scale s. forall (gt :: MoneyClass) ac n scale s.
@ -219,11 +169,62 @@ stakeValidator ::
, gt ~ '(ac, n, scale) , gt ~ '(ac, n, scale)
) => ) =>
Stake gt -> Stake gt ->
Term s (PData :--> PData :--> PAsData PScriptContext :--> PUnit) Term s PValidator
stakeValidator _stake = stakeValidator stake =
plam $ \datum redeemer ctx' -> P.do plam $ \datum redeemer ctx' -> P.do
_ctx <- pletFields @'["txInfo", "purpose"] ctx' ctx <- pletFields @'["txInfo", "purpose"] ctx'
let _stakeAction = punsafeCoerce redeemer :: Term s (StakeAction gt) txInfo' <- plet ctx.txInfo
_stakeDatum <- pletFields @'["owner"] (punsafeCoerce datum :: Term s (StakeDatum gt)) txInfo <- pletFields @'["mint", "inputs", "outputs"] txInfo'
let stakeAction = punsafeCoerce redeemer :: Term s (PStakeAction gt)
let stakeDatum' = punsafeCoerce datum :: Term s (PStakeDatum gt)
stakeDatum <- pletFields @'["owner", "stakedAmount"] stakeDatum'
pconstant () PSpending txOutRef <- pmatch $ pfromData ctx.purpose
PJust txInInfo <- pmatch $ pfindTxInByTxOutRef # (pfield @"_0" # txOutRef) # txInfo'
ownAddress <- plet $ pfield @"address" #$ pfield @"resolved" # txInInfo
let continuingValue = pfield @"value" #$ pfield @"resolved" # txInInfo
stCurrencySymbol <- plet $ pconstant $ mintingPolicySymbol $ mkMintingPolicy (stakePolicy stake)
mintedST <- plet $ psymbolValueOf # stCurrencySymbol # txInfo.mint
spentST <- plet $ psymbolValueOf # stCurrencySymbol #$ pvalueSpent # txInfo'
pmatch stakeAction $ \case
PDestroy _ -> P.do
passert "ST at inputs must be 1" $
spentST #== 1
passert "Should burn ST" $
mintedST #== -1
passert "Stake unlocked" $
pnot #$ stakeLocked # stakeDatum'
popaque (pconstant ())
PDepositWithdraw r -> P.do
passert "ST at inputs must be 1" $
spentST #== 1
passert "Stake unlocked" $
pnot #$ stakeLocked # stakeDatum'
passert "A UTXO must exist with the correct output" $
anyOutput @(PStakeDatum gt) # txInfo'
#$ plam
$ \value address newStakeDatum' -> P.do
newStakeDatum <- pletFields @'["owner", "stakedAmount"] newStakeDatum'
delta <- plet $ pfield @"delta" # r
let isScriptAddress = pdata address #== ownAddress
let correctOutputDatum =
stakeDatum.owner #== newStakeDatum.owner
#&& (paddDiscrete # stakeDatum.stakedAmount # delta) #== newStakeDatum.stakedAmount
let expectedValue = paddValue # continuingValue # (discreteValue # delta)
-- TODO: As above, needs to be >=, rather than ==
let correctValue = pdata value #== pdata expectedValue
isScriptAddress #&& correctOutputDatum #&& correctValue
popaque (pconstant ())
--------------------------------------------------------------------------------
-- | Check whether a Stake is locked. If it is locked, various actions are unavailable.
stakeLocked :: forall (gt :: MoneyClass) s. Term s (PStakeDatum gt :--> PBool)
stakeLocked = phoistAcyclic $
plam $ \_stakeDatum ->
-- TODO: when we extend this to support proposals, this will need to do something
pcon PFalse

View file

@ -1,5 +1,25 @@
-- | Plutarch utility functions that should be upstreamed or don't belong anywhere else -- | Plutarch utility functions that should be upstreamed or don't belong anywhere else
module Agora.Utils (module Agora.Utils) where module Agora.Utils (
-- * Validator-level utility functions
passert,
pfind',
pfindDatum,
pfindDatum',
pvalueSpent,
ptxSignedBy,
paddValue,
plookup,
pfromMaybe,
psymbolValueOf,
passetClassValueOf,
passetClassValueOf',
pfindTxInByTxOutRef,
pfindMap,
-- * Functions which should (probably) not be upstreamed
anyOutput,
anyInput,
) where
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@ -8,16 +28,19 @@ import Plutus.V1.Ledger.Value (AssetClass (..))
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
import Plutarch.Api.V1 ( import Plutarch.Api.V1 (
PAddress,
PCurrencySymbol, PCurrencySymbol,
PDatum, PDatum,
PDatumHash, PDatumHash,
PMap (PMap), PMap (PMap),
PMaybeData (PDJust),
PPubKeyHash, PPubKeyHash,
PTokenName, PTokenName,
PTuple, PTuple,
PTxInInfo (PTxInInfo), PTxInInfo (PTxInInfo),
PTxInfo (PTxInfo), PTxInfo (PTxInfo),
PTxOut (PTxOut), PTxOut (PTxOut),
PTxOutRef,
PValue (PValue), PValue (PValue),
) )
import Plutarch.Builtin (ppairDataBuiltin) import Plutarch.Builtin (ppairDataBuiltin)
@ -25,6 +48,7 @@ import Plutarch.Internal (punsafeCoerce)
import Plutarch.Monadic qualified as P import Plutarch.Monadic qualified as P
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Validator-level utility functions
-- | Assert a particular bool, trace on falsehood. Use in monadic context -- | Assert a particular bool, trace on falsehood. Use in monadic context
passert :: Term s PString -> Term s PBool -> Term s k -> Term s k passert :: Term s PString -> Term s PBool -> Term s k -> Term s k
@ -58,6 +82,22 @@ pfind' p =
(\self x xs -> pif (p x) (pcon (PJust x)) (self # xs)) (\self x xs -> pif (p x) (pcon (PJust x)) (self # xs))
(const $ pcon PNothing) (const $ pcon PNothing)
-- | Get the first element that maps to a PJust in a list
pfindMap ::
PIsListLike list a =>
Term s ((a :--> PMaybe b) :--> list a :--> PMaybe b)
pfindMap =
phoistAcyclic $
plam $ \p ->
precList
( \self x xs ->
-- In the future, this should use `pmatchSum`, I believe?
pmatch (p # x) $ \case
PNothing -> self # xs
PJust v -> pcon (PJust v)
)
(const $ pcon PNothing)
-- | Find the value for a given key in an assoclist -- | Find the value for a given key in an assoclist
plookup :: plookup ::
(PEq a, PIsListLike list (PBuiltinPair a b)) => (PEq a, PIsListLike list (PBuiltinPair a b)) =>
@ -106,6 +146,7 @@ psymbolValueOf =
PMap m <- pmatch (pfromData m') PMap m <- pmatch (pfromData m')
pfoldr # (plam $ \x v -> (pfromData $ psndBuiltin # x) + v) # 0 # m pfoldr # (plam $ \x v -> (pfromData $ psndBuiltin # x) + v) # 0 # m
-- | Extract amount from PValue belonging to a Plutarch-level asset class
passetClassValueOf :: passetClassValueOf ::
Term s (PCurrencySymbol :--> PTokenName :--> PValue :--> PInteger) Term s (PCurrencySymbol :--> PTokenName :--> PValue :--> PInteger)
passetClassValueOf = passetClassValueOf =
@ -136,7 +177,7 @@ pmapUnionWith = phoistAcyclic $
pf <- plet $ pfstBuiltin # p pf <- plet $ pfstBuiltin # p
ps <- plet $ psndBuiltin # p ps <- plet $ psndBuiltin # p
pmatch (plookup # pf # ys) $ \case pmatch (plookup # pf # ys) $ \case
PJust v -> P.do PJust v ->
-- Data conversions here are silly, aren't they? -- Data conversions here are silly, aren't they?
ppairDataBuiltin # pf # (pdata (f # pfromData ps # pfromData v)) ppairDataBuiltin # pf # (pdata (f # pfromData ps # pfromData v))
PNothing -> p PNothing -> p
@ -177,3 +218,66 @@ pvalueSpent = phoistAcyclic $
) )
# pconstant mempty # pconstant mempty
# (pfield @"inputs" # txInfo) # (pfield @"inputs" # txInfo)
-- | Find the TxInInfo by a TxOutRef
pfindTxInByTxOutRef :: Term s (PTxOutRef :--> PTxInfo :--> PMaybe PTxInInfo)
pfindTxInByTxOutRef = phoistAcyclic $
plam $ \txOutRef txInfo' ->
pmatch txInfo' $ \(PTxInfo txInfo) ->
pfindMap
# ( plam $ \txInInfo' ->
plet (pfromData txInInfo') $ \r ->
pmatch r $ \(PTxInInfo txInInfo) ->
pif
(pdata txOutRef #== pfield @"outRef" # txInInfo)
(pcon (PJust r))
(pcon PNothing)
)
#$ (pfield @"inputs" # txInfo)
--------------------------------------------------------------------------------
-- Functions which should (probably) not be upstreamed
-- | Check if any output matches the predicate
anyOutput ::
forall (datum :: PType) s.
( PIsData datum
) =>
Term s (PTxInfo :--> (PValue :--> PAddress :--> datum :--> PBool) :--> PBool)
anyOutput = phoistAcyclic $
plam $ \txInfo' predicate -> P.do
txInfo <- pletFields @'["outputs"] txInfo'
pany
# ( plam $ \txOut'' -> P.do
PTxOut txOut' <- pmatch (pfromData txOut'')
txOut <- pletFields @'["value", "datumHash", "address"] txOut'
PDJust dh <- pmatch txOut.datumHash
pmatch (pfindDatum' @datum # (pfield @"_0" # dh) # txInfo') $ \case
PJust datum -> P.do
predicate # txOut.value # txOut.address # pfromData datum
PNothing -> pcon PFalse
)
# pfromData txInfo.outputs
-- | Check if any (resolved) input matches the predicate
anyInput ::
forall (datum :: PType) s.
( PIsData datum
) =>
Term s (PTxInfo :--> (PValue :--> PAddress :--> datum :--> PBool) :--> PBool)
anyInput = phoistAcyclic $
plam $ \txInfo' predicate -> P.do
txInfo <- pletFields @'["inputs"] txInfo'
pany
# ( plam $ \txInInfo'' -> P.do
PTxInInfo txInInfo' <- pmatch (pfromData txInInfo'')
let txOut'' = pfield @"resolved" # txInInfo'
PTxOut txOut' <- pmatch (pfromData txOut'')
txOut <- pletFields @'["value", "datumHash", "address"] txOut'
PDJust dh <- pmatch txOut.datumHash
pmatch (pfindDatum' @datum # (pfield @"_0" # dh) # txInfo') $ \case
PJust datum -> P.do
predicate # txOut.value # txOut.address # pfromData datum
PNothing -> pcon PFalse
)
# pfromData txInfo.inputs