check stake locks in stake validator

This commit is contained in:
Hongrui Fang 2022-09-26 20:17:41 +08:00
parent 1bc60a48e5
commit 9b858079dd
4 changed files with 157 additions and 69 deletions

View file

@ -53,7 +53,7 @@ specs =
Create.addInvalidLocksParameters Create.addInvalidLocksParameters
True True
False False
True False
, Create.mkTestTree , Create.mkTestTree
"has reached maximum proposals limit" "has reached maximum proposals limit"
Create.exceedMaximumProposalsParameters Create.exceedMaximumProposalsParameters

View file

@ -45,6 +45,7 @@ module Agora.Stake (
import Agora.Proposal ( import Agora.Proposal (
PProposalId, PProposalId,
PProposalRedeemer, PProposalRedeemer,
PProposalStatus,
PResultTag, PResultTag,
ProposalId, ProposalId,
ResultTag, ResultTag,
@ -252,6 +253,8 @@ newtype PStakeDatum (s :: S) = PStakeDatum
PEq PEq
, -- | @since 1.0.0 , -- | @since 1.0.0
PDataFields PDataFields
, -- | @since 1.0.0
PShow
) )
instance DerivePlutusType PStakeDatum where instance DerivePlutusType PStakeDatum where
@ -503,9 +506,13 @@ instance DerivePlutusType PStakeRedeemerContext where
-} -}
data PProposalContext (s :: S) data PProposalContext (s :: S)
= -- | A proposal is spent. = -- | A proposal is spent.
PWithProposalRedeemer (Term s PProposalRedeemer) PSpendProposal
(Term s PProposalId)
(Term s PProposalStatus)
(Term s PProposalRedeemer)
| -- | A new proposal is created. | -- | A new proposal is created.
PNewProposal PNewProposal
(Term s PProposalId)
| -- | No proposal is spent or created. | -- | No proposal is spent or created.
PNoProposal PNoProposal
deriving stock deriving stock

View file

@ -14,12 +14,17 @@ module Agora.Stake.Redeemers (
pdepositWithdraw, pdepositWithdraw,
) where ) where
import Agora.Proposal (PProposalRedeemer (PUnlock, PVote)) import Agora.Proposal (
PProposalId,
PProposalRedeemer (PUnlock, PVote),
ProposalStatus (Finished),
)
import Agora.Stake ( import Agora.Stake (
PProposalContext ( PProposalContext (
PNewProposal, PNewProposal,
PWithProposalRedeemer PSpendProposal
), ),
PProposalLock (PCreated, PVoted),
PSigContext (owner, signedBy), PSigContext (owner, signedBy),
PSignedBy ( PSignedBy (
PSignedByDelegate, PSignedByDelegate,
@ -93,26 +98,39 @@ pisSignedBy = phoistAcyclic $
-- | Return true if only the @lockedBy@ field of the stake datum is updated. -- | Return true if only the @lockedBy@ field of the stake datum is updated.
ponlyLocksUpdated :: ponlyLocksUpdated ::
forall (s :: S). forall (s :: S).
Term s (PStakeRedeemerHandlerContext :--> PBool) Term
s
( ( PBuiltinList (PAsData PProposalLock)
:--> PBuiltinList (PAsData PProposalLock)
)
:--> PStakeRedeemerHandlerContext
:--> PBool
)
ponlyLocksUpdated = phoistAcyclic $ ponlyLocksUpdated = phoistAcyclic $
plam $ \f ->
pbatchUpdateInputs #$ plam $ \i o -> pbatchUpdateInputs #$ plam $ \i o ->
pletAll i $ \iF -> pletAll i $ \iF ->
let newLocks = pfield @"lockedBy" # o let newLocks = f # pfromData iF.lockedBy
in mkRecordConstr
expected =
mkRecordConstr
PStakeDatum PStakeDatum
( #stakedAmount .= iF.stakedAmount ( #stakedAmount .= iF.stakedAmount
.& #owner .= iF.owner .& #owner .= iF.owner
.& #delegatedTo .= iF.delegatedTo .& #delegatedTo .= iF.delegatedTo
.& #lockedBy .= newLocks .& #lockedBy .= pdata newLocks
) )
#== o in expected #== o
-- | Validation logic shared between 'ppermitVote' and 'retractVote'. -- | Validation logic shared between 'ppermitVote' and 'retractVote'.
pvoteHelper :: pvoteHelper ::
forall (s :: S). forall (s :: S).
Term Term
s s
( (PProposalContext :--> PBool) ( ( PProposalContext
:--> PBuiltinList (PAsData PProposalLock)
:--> PBuiltinList (PAsData PProposalLock)
)
:--> PStakeRedeemerHandler :--> PStakeRedeemerHandler
) )
pvoteHelper = phoistAcyclic $ pvoteHelper = phoistAcyclic $
@ -125,14 +143,21 @@ pvoteHelper = phoistAcyclic $
-- 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" $
valProposalCtx # ctxF.proposalContext
pguardC "Correct outputs" $ pguardC "Correct outputs" $
ponlyLocksUpdated # ctx ponlyLocksUpdated # (valProposalCtx # ctxF.proposalContext) # ctx
pure $ pconstant () pure $ pconstant ()
paddNewLock ::
forall (s :: S).
Term
s
( PProposalLock
:--> PBuiltinList (PAsData PProposalLock)
:--> PBuiltinList (PAsData PProposalLock)
)
paddNewLock = phoistAcyclic $ plam $ \newLock -> pcons # pdata newLock
{- | Default implementation of 'Agora.Stake.PermitVote'. {- | Default implementation of 'Agora.Stake.PermitVote'.
@since 1.0.0 @since 1.0.0
@ -141,11 +166,41 @@ ppermitVote :: forall (s :: S). Term s PStakeRedeemerHandler
ppermitVote = pvoteHelper #$ phoistAcyclic $ ppermitVote = pvoteHelper #$ phoistAcyclic $
plam $ plam $
flip pmatch $ \case flip pmatch $ \case
PWithProposalRedeemer r -> pmatch r $ \case PSpendProposal pid _ r -> pmatch r $ \case
PVote _ -> pconstant True PVote ((pfromData . (pfield @"resultTag" #)) -> voteFor) ->
_ -> ptrace "Expected Vote" $ pconstant False let newLock =
PNewProposal -> pconstant True mkRecordConstr
_ -> pconstant False PVoted
( #votedOn .= pdata pid
.& #votedFor .= pdata voteFor
)
in paddNewLock # newLock
_ -> ptraceError "Expected Vote"
PNewProposal pid ->
let newLock =
mkRecordConstr
PCreated
( #created .= pdata pid
)
in paddNewLock # newLock
_ -> ptraceError "Expected proposal"
premoveLocks ::
forall (s :: S).
Term
s
( PProposalId :--> PBool
:--> PBuiltinList (PAsData PProposalLock)
:--> PBuiltinList (PAsData PProposalLock)
)
premoveLocks = phoistAcyclic $
plam $ \pid rc ->
pfilter
# plam
( \(pfromData -> l) -> pnot #$ pmatch l $ \case
PCreated ((pfield @"created" #) -> pid') -> rc #&& pid' #== pid
PVoted ((pfield @"votedOn" #) -> pid') -> pid' #== pid
)
{- | Default implementation of 'Agora.Stake.RetractVotes'. {- | Default implementation of 'Agora.Stake.RetractVotes'.
@ -155,10 +210,13 @@ pretractVote :: forall (s :: S). Term s PStakeRedeemerHandler
pretractVote = pvoteHelper #$ phoistAcyclic $ pretractVote = pvoteHelper #$ phoistAcyclic $
plam $ plam $
flip pmatch $ \case flip pmatch $ \case
PWithProposalRedeemer r -> pmatch r $ \case PSpendProposal pid s r -> pmatch r $ \case
PUnlock _ -> pconstant True PUnlock _ ->
_ -> ptrace "Expected Unlock" $ pconstant False let allowRemovingCreatorLock =
_ -> pconstant False s #== pconstant Finished
in premoveLocks # pid # allowRemovingCreatorLock
_ -> ptraceError "Expected unlock"
_ -> ptraceError "Expected spending proposal"
-- | Validation logic shared by 'pdelegateTo' and 'pclearDelegate'. -- | Validation logic shared by 'pdelegateTo' and 'pclearDelegate'.
pdelegateHelper :: pdelegateHelper ::

View file

@ -12,7 +12,7 @@ module Agora.Stake.Scripts (
) where ) where
import Agora.Credential (authorizationContext, pauthorizedBy) import Agora.Credential (authorizationContext, pauthorizedBy)
import Agora.Proposal (PProposalRedeemer) import Agora.Proposal (PProposalDatum, PProposalRedeemer)
import Agora.SafeMoney (GTTag) import Agora.SafeMoney (GTTag)
import Agora.Scripts ( import Agora.Scripts (
AgoraScripts, AgoraScripts,
@ -23,7 +23,7 @@ import Agora.Stake (
PProposalContext ( PProposalContext (
PNewProposal, PNewProposal,
PNoProposal, PNoProposal,
PWithProposalRedeemer PSpendProposal
), ),
PSigContext (PSigContext), PSigContext (PSigContext),
PSignedBy ( PSignedBy (
@ -73,10 +73,8 @@ import Plutarch.Api.V2 (
AmountGuarantees, AmountGuarantees,
PMintingPolicy, PMintingPolicy,
PScriptPurpose (PMinting, PSpending), PScriptPurpose (PMinting, PSpending),
PTxInInfo,
PTxInfo, PTxInfo,
PTxOut, PTxOut,
PTxOutRef,
PValidator, PValidator,
) )
import Plutarch.Extra.AssetClass ( import Plutarch.Extra.AssetClass (
@ -84,14 +82,14 @@ import Plutarch.Extra.AssetClass (
passetClassValueOf, passetClassValueOf,
pvalueOf, pvalueOf,
) )
import Plutarch.Extra.Bind (PBind ((#>>=)))
import Plutarch.Extra.Category (PSemigroupoid ((#>>>))) import Plutarch.Extra.Category (PSemigroupoid ((#>>>)))
import Plutarch.Extra.Field (pletAll)
import Plutarch.Extra.Functor (PFunctor (pfmap)) import Plutarch.Extra.Functor (PFunctor (pfmap))
import "liqwid-plutarch-extra" Plutarch.Extra.List (pfindJust, pmapMaybe) import "liqwid-plutarch-extra" Plutarch.Extra.List (pfindJust, pmapMaybe)
import Plutarch.Extra.Maybe ( import Plutarch.Extra.Maybe (
passertPJust, passertPJust,
pfromMaybe,
pjust, pjust,
pmaybe,
pmaybeData, pmaybeData,
pnothing, pnothing,
) )
@ -402,9 +400,9 @@ mkStakeValidator
pguardC "No new SST minted" $ pguardC "No new SST minted" $
foldl1 foldl1
(#||) (#||)
[ ptraceIfFalse "All stakes burnt" $ [ ptraceIfTrue "All stakes burnt" $
mintedST #< 0 #&& pnull # stakeOutputDatums mintedST #< 0 #&& pnull # stakeOutputDatums
, ptraceIfFalse "Nothing burnt" $ , ptraceIfTrue "Nothing burnt" $
mintedST #== 0 mintedST #== 0
] ]
@ -420,16 +418,37 @@ mkStakeValidator
# pconstant propCs # pconstant propCs
# pconstant propTn # pconstant propTn
getProposalDatum <- pletC $
plam $
flip pletAll $ \txOutF ->
let isProposalUTxO =
passetClassValueOf
# txOutF.value
# proposalSTClass #== 1
proposalDatum =
pfromData $
pfromOutputDatum @(PAsData PProposalDatum)
# txOutF.datum
# txInfoF.datums
in pif isProposalUTxO (pjust # proposalDatum) pnothing
let pstMinted = let pstMinted =
passetClassValueOf # txInfoF.mint # proposalSTClass #== 1 passetClassValueOf # txInfoF.mint # proposalSTClass #== 1
proposalContext <- newProposalContext =
pletC $ pcon $
let convertRedeemer = plam $ \(pto -> dt) -> PNewProposal $
ptryFrom @PProposalRedeemer dt fst pfield @"proposalId"
#$ passertPJust # "Proposal output should present"
#$ pfindJust # getProposalDatum # pfromData txInfoF.outputs
findRedeemer = plam $ \ref -> spendProposalContext =
plookup let getProposalRedeemer = plam $ \ref ->
flip (ptryFrom @PProposalRedeemer) fst $
pto $
passertPJust
# "Malformed script context: propsoal input not found in redeemer map"
#$ plookup
# pcon # pcon
( PSpending $ ( PSpending $
pdcons @_0 pdcons @_0
@ -438,24 +457,28 @@ mkStakeValidator
) )
# txInfoF.redeemers # txInfoF.redeemers
f :: Term _ (PTxInInfo :--> PMaybe PTxOutRef) getContext = plam $
f = plam $ \inInfo -> flip pletAll $ \inInfoF ->
let value = pfield @"value" #$ pfield @"resolved" # inInfo pfmap
ref = pfield @"outRef" # inInfo
in pif
(passetClassValueOf # value # proposalSTClass #== 1)
(pjust # ref)
pnothing
proposalRef = pfindJust # f # txInfoF.inputs
in pif pstMinted (pcon PNewProposal) $
pmaybe
# pcon PNoProposal
# plam # plam
( \((convertRedeemer #) -> proposalRedeemer) -> ( \proposalDatum ->
pcon $ PWithProposalRedeemer proposalRedeemer let id = pfield @"proposalId" # proposalDatum
status = pfield @"status" # proposalDatum
redeemer = getProposalRedeemer # inInfoF.outRef
in pcon $ PSpendProposal id status redeemer
) )
#$ proposalRef #>>= findRedeemer #$ getProposalDatum
# pfromData inInfoF.resolved
in pfindJust # getContext # pfromData txInfoF.inputs
noProposalContext = pcon PNoProposal
proposalContext <-
pletC $
pif
pstMinted
newProposalContext
(pfromMaybe # noProposalContext # spendProposalContext)
-------------------------------------------------------------------------- --------------------------------------------------------------------------