prevent minting multiple into one UTxO; regression tests

This commit is contained in:
Hongrui Fang 2022-11-09 17:42:18 +08:00
parent 86bcc78553
commit 1a7d704497
No known key found for this signature in database
GPG key ID: F10AB2CCE24113DD
8 changed files with 215 additions and 123 deletions

View file

@ -36,12 +36,13 @@ module Sample.Proposal.Advance (
mkBadGovernorOutputDatumBundle,
mkUnexpectedOutputStakeBundles,
mkFastforwardToFinishBundles,
mkBadGovernorRedeemerBundle,
) where
import Agora.Governor (
Governor (..),
GovernorDatum (..),
GovernorRedeemer (MintGATs),
GovernorRedeemer (CreateProposal, MintGATs),
)
import Agora.Proposal (
ProposalDatum (..),
@ -85,6 +86,7 @@ import Plutarch.Context (
timeRange,
withDatum,
withInlineDatum,
withRedeemer,
withRef,
withValue,
)
@ -101,6 +103,7 @@ import PlutusLedgerApi.V2 (
TxOutRef (TxOutRef),
ValidatorHash,
)
import PlutusTx qualified
import Sample.Proposal.Shared (
governorTxRef,
proposalTxRef,
@ -165,9 +168,18 @@ data ParameterBundle = ParameterBundle
}
-- | Everything about the generated governor stuff.
newtype GovernorParameters = GovernorParameters
data GovernorParameters = forall
(redeemer :: Type)
(predeemer :: PType).
( PUnsafeLiftDecl predeemer
, PLifted predeemer ~ redeemer
, PIsData predeemer
, PlutusTx.ToData redeemer
) =>
GovernorParameters
{ invalidGovernorOutputDatum :: Bool
-- ^ The output governor datum will be changed.
, governorRedeemer :: redeemer
}
-- | Everything about the generated authority token stuff.
@ -432,7 +444,7 @@ governorRef = TxOutRef governorTxRef 2
governor validator.
-}
mkGovernorBuilder :: forall b. CombinableBuilder b => GovernorParameters -> b
mkGovernorBuilder ps =
mkGovernorBuilder ps@(GovernorParameters _ redeemer) =
let gst = assetClassValue governorAssetClass 1
value = sortValue $ gst <> minAda
in mconcat
@ -442,6 +454,7 @@ mkGovernorBuilder ps =
, withValue value
, withRef governorRef
, withDatum governorInputDatum
, withRedeemer redeemer
]
, output $
mconcat
@ -452,12 +465,6 @@ mkGovernorBuilder ps =
]
]
{- | The proposal redeemer used to spend the governor UTXO, which is always
'MintGATs' in this case.
-}
governorRedeemer :: GovernorRedeemer
governorRedeemer = MintGATs
--------------------------------------------------------------------------------
-- * Authority Token
@ -538,16 +545,19 @@ mkTestTree name pb val =
proposalInputDatum
proposalRedeemer
(spend proposalRef)
governor =
maybe [] singleton $
testValidator
(fromJust val.forGovernorValidator)
"governor"
governorValidator
governorInputDatum
governorRedeemer
(spend governorRef)
<$ pb.governorParameters
( \(GovernorParameters _ governorRedeemer) ->
testValidator
(fromJust val.forGovernorValidator)
"governor"
governorValidator
governorInputDatum
governorRedeemer
(spend governorRef)
)
<$> pb.governorParameters
authority = case pb.authorityTokenParameters of
[] -> []
@ -827,6 +837,7 @@ mkValidToNextStateBundle nCosigners nEffects authScript from =
gov =
GovernorParameters
{ invalidGovernorOutputDatum = False
, governorRedeemer = MintGATs
}
in b
{ governorParameters = Just gov
@ -1066,7 +1077,19 @@ mkBadGovernorOutputDatumBundle nCosigners nEffects =
}
where
template = mkValidFromLockedBundle nCosigners nEffects
gov = GovernorParameters True
gov = GovernorParameters True MintGATs
mkBadGovernorRedeemerBundle ::
Word ->
Word ->
ParameterBundle
mkBadGovernorRedeemerBundle nCosigners nEffects =
template
{ governorParameters = Just gov
}
where
template = mkValidFromLockedBundle nCosigners nEffects
gov = GovernorParameters False CreateProposal
mkFastforwardToFinishBundles ::
Word ->

View file

@ -19,12 +19,17 @@ module Sample.Proposal.Create (
invalidProposalStatusParameters,
fakeSSTParameters,
wrongGovernorRedeemer,
wrongGovernorRedeemer1,
) where
import Agora.Governor (
Governor (..),
GovernorDatum (..),
GovernorRedeemer (CreateProposal, MutateGovernor),
GovernorRedeemer (
CreateProposal,
MintGATs,
MutateGovernor
),
)
import Agora.Proposal (
ProposalDatum (..),
@ -71,6 +76,8 @@ import PlutusLedgerApi.V2 (
Credential (PubKeyCredential),
POSIXTime (POSIXTime),
POSIXTimeRange,
Redeemer (Redeemer),
ToData (toBuiltinData),
TxOutRef (TxOutRef),
always,
)
@ -123,8 +130,8 @@ data Parameters = Parameters
-- ^ The status of the newly created proposal.
, fakeSST :: Bool
-- ^ Whether to use SST that doesn't belong to the stake validator.
, wrongGovernorRedeemer :: Bool
-- ^ Use 'MutateGovernor' as the governor redeemer
, governorRedeemer :: Redeemer
-- ^ The redeemer used to spend the governor.
}
--------------------------------------------------------------------------------
@ -358,7 +365,7 @@ createProposal ps = builder
[ script governorValidatorHash
, withValue governorValue
, withDatum governorInputDatum
, withRedeemer $ mkGovernorRedeemer ps
, withRedeemer ps.governorRedeemer
, withRef governorRef
]
, output $
@ -418,13 +425,6 @@ createProposal ps = builder
stakeRedeemer :: StakeRedeemer
stakeRedeemer = PermitVote
-- | Spend the governor with the 'CreateProposal' redeemer.
mkGovernorRedeemer :: Parameters -> GovernorRedeemer
mkGovernorRedeemer ps =
if ps.wrongGovernorRedeemer
then MutateGovernor
else CreateProposal
-- | Mint the PST with an arbitrary redeemer. Doesn't really matter.
proposalPolicyRedeemer :: ()
proposalPolicyRedeemer = ()
@ -443,7 +443,7 @@ totallyValidParameters =
, timeRangeClosed = True
, proposalStatus = Draft
, fakeSST = False
, wrongGovernorRedeemer = False
, governorRedeemer = Redeemer $ toBuiltinData CreateProposal
}
invalidOutputGovernorDatumParameters :: Parameters
@ -505,7 +505,13 @@ fakeSSTParameters =
wrongGovernorRedeemer :: Parameters
wrongGovernorRedeemer =
totallyValidParameters
{ wrongGovernorRedeemer = True
{ governorRedeemer = Redeemer $ toBuiltinData MintGATs
}
wrongGovernorRedeemer1 :: Parameters
wrongGovernorRedeemer1 =
totallyValidParameters
{ governorRedeemer = Redeemer $ toBuiltinData MutateGovernor
}
--------------------------------------------------------------------------------
@ -540,7 +546,7 @@ mkTestTree
"governor"
governorValidator
governorInputDatum
(mkGovernorRedeemer ps)
ps.governorRedeemer
(spend governorRef)
stakeTest =

View file

@ -97,6 +97,12 @@ specs =
False
False
True
, Create.mkTestTree
"wrong governor redeemer"
Create.wrongGovernorRedeemer1
False
False
True
]
]
, group
@ -353,6 +359,15 @@ specs =
, forGovernorValidator = Just False
, forAuthorityTokenPolicy = Just True
}
, Advance.mkTestTree
"wrong governor redeemer"
(Advance.mkBadGovernorRedeemerBundle cs es)
Advance.Validity
{ forProposalValidator = True
, forStakeValidator = True
, forGovernorValidator = Just False
, forAuthorityTokenPolicy = Just False
}
]
]
, group "unlocking" $

View file

@ -11,6 +11,7 @@ module Agora.AuthorityToken (
singleAuthorityTokenBurned,
) where
import Agora.Governor (PGovernorRedeemer (PMintGATs), presolveGovernorRedeemer)
import Agora.SafeMoney (AuthorityTokenTag, GovernorSTTag)
import Agora.Utils (psymbolValueOfT, ptag, ptoScottEncodingT, puntag)
import Plutarch.Api.V1 (
@ -24,17 +25,14 @@ import Plutarch.Api.V2 (
KeyGuarantees,
PAddress (PAddress),
PMintingPolicy,
PScriptContext (PScriptContext),
PScriptPurpose (PMinting),
PTxInInfo (PTxInInfo),
PTxInfo (PTxInfo),
PTxOut (PTxOut),
)
import Plutarch.Extra.AssetClass (PAssetClassData)
import Plutarch.Extra.Bool (passert)
import "liqwid-plutarch-extra" Plutarch.Extra.List (plookupAssoc)
import Plutarch.Extra.Maybe (pfromJust)
import Plutarch.Extra.ScriptContext (pisTokenSpent)
import Plutarch.Extra.Maybe (passertPJust, pfromJust)
import Plutarch.Extra.Sum (PSum (PSum))
import Plutarch.Extra.Tagged (PTagged)
import "liqwid-plutarch-extra" Plutarch.Extra.TermCont (
@ -149,33 +147,44 @@ singleAuthorityTokenBurned gatCs inputs mint = unTermCont $ do
-}
authorityTokenPolicy :: ClosedTerm (PTagged GovernorSTTag PAssetClassData :--> PMintingPolicy)
authorityTokenPolicy =
plam $ \gstAssetClass _redeemer ctx' ->
pmatch ctx' $ \(PScriptContext ctx') -> unTermCont $ do
ctx <- pletFieldsC @'["txInfo", "purpose"] ctx'
PTxInfo txInfo' <- pmatchC $ pfromData ctx.txInfo
txInfo <- pletFieldsC @'["inputs", "mint", "outputs"] txInfo'
let inputs = txInfo.inputs
govTokenSpent = pisTokenSpent # puntag (ptoScottEncodingT # gstAssetClass) # inputs
plam $ \gstAssetClass _redeemer ctx -> unTermCont $ do
ctxF <- pletFieldsC @'["txInfo", "purpose"] ctx
txInfoF <-
pletFieldsC
@'[ "inputs"
, "mint"
, "outputs"
, "redeemers"
]
ctxF.txInfo
PMinting ownSymbol' <- pmatchC $ pfromData ctx.purpose
PMinting ownSymbol' <- pmatchC $ pfromData ctxF.purpose
let ownSymbol = pfromData $ pfield @"_0" # ownSymbol'
let ownSymbol = pfromData $ pfield @"_0" # ownSymbol'
PPair mintedATs burntATs <-
pmatchC $ pfromJust #$ psymbolValueOf' # ownSymbol # txInfo.mint
PPair mintedATs burntATs <-
pmatchC $ pfromJust #$ psymbolValueOf' # ownSymbol # txInfoF.mint
pure $
popaque $
pif
(0 #< mintedATs)
( unTermCont $ do
pguardC "No GAT burnt" $ 0 #== burntATs
pguardC "Parent token did not move in minting GATs" govTokenSpent
pguardC "All outputs only emit valid GATs" $
pall
# plam
(authorityTokensValidIn # ptag ownSymbol #)
# txInfo.outputs
pure $ pconstant ()
)
(passert "No GAT minted" (0 #== mintedATs) (pconstant ()))
pure $
popaque $
pif
(0 #< mintedATs)
( unTermCont $ do
pguardC "No GAT burnt" $ 0 #== burntATs
let governorRedeemer =
passertPJust
# "GST should move"
#$ presolveGovernorRedeemer
# (ptoScottEncodingT # gstAssetClass)
# pfromData txInfoF.inputs
# txInfoF.redeemers
pguardC "Governor redeemr correct" $
pcon PMintGATs #== governorRedeemer
pguardC "All outputs only emit valid GATs" $
pall
# plam
(authorityTokensValidIn # ptag ownSymbol #)
# txInfoF.outputs
pure $ pconstant ()
)
(passert "No GAT minted" (0 #== mintedATs) (pconstant ()))

View file

@ -21,6 +21,7 @@ module Agora.Governor (
pgetNextProposalId,
getNextProposalId,
pisGovernorDatumValid,
presolveGovernorRedeemer,
) where
import Agora.Aeson.Orphans ()
@ -39,21 +40,33 @@ import Agora.Proposal.Time (
pisMaxTimeRangeWidthValid,
pisProposalTimingConfigValid,
)
import Agora.SafeMoney (GTTag)
import Agora.SafeMoney (GTTag, GovernorSTTag)
import Data.Aeson qualified as Aeson
import Data.Tagged (Tagged)
import Optics.TH (makeFieldLabelsNoPrefix)
import Plutarch.Api.V1.Scripts (PRedeemer)
import Plutarch.Api.V2 (KeyGuarantees (Unsorted), PMap, PScriptPurpose (PSpending), PTxInInfo)
import Plutarch.DataRepr (
DerivePConstantViaData (DerivePConstantViaData),
PDataFields,
)
import Plutarch.Extra.AssetClass (AssetClass)
import Plutarch.Extra.AssetClass (AssetClass, PAssetClass)
import Plutarch.Extra.Bind (PBind ((#>>=)))
import Plutarch.Extra.Field (pletAll)
import Plutarch.Extra.Function (pflip)
import Plutarch.Extra.Functor (PFunctor (pfmap))
import Plutarch.Extra.IsData (
DerivePConstantViaEnum (DerivePConstantEnum),
EnumIsData (EnumIsData),
PlutusTypeEnumData,
)
import "liqwid-plutarch-extra" Plutarch.Extra.List (pfindJust)
import Plutarch.Extra.Maybe (pjust, pnothing)
import Plutarch.Extra.Record (mkRecordConstr, (.=))
import Plutarch.Extra.ScriptContext (ptryFromRedeemer)
import Plutarch.Extra.Tagged (PTagged)
import "liqwid-plutarch-extra" Plutarch.Extra.TermCont (pletFieldsC)
import Plutarch.Extra.Value (passetClassValueOfT)
import Plutarch.Lift (PConstantDecl, PUnsafeLiftDecl (PLifted))
import PlutusLedgerApi.V1 (TxOutRef)
import PlutusTx qualified
@ -285,3 +298,49 @@ pisGovernorDatumValid = phoistAcyclic $
, ptraceIfFalse "time range valid" $
pisMaxTimeRangeWidthValid # datumF.createProposalTimeRangeMaxWidth
]
-- | @since 1.0.0
presolveGovernorRedeemer ::
forall (s :: S).
Term
s
( PTagged GovernorSTTag PAssetClass
:--> PBuiltinList PTxInInfo
:--> PMap 'Unsorted PScriptPurpose PRedeemer
:--> PMaybe PGovernorRedeemer
)
presolveGovernorRedeemer = phoistAcyclic $
plam $ \gstClass inputs redeemers ->
let governorInputRef =
pfindJust
# plam
( flip pletAll $ \inputF ->
let value = pfield @"value" # inputF.resolved
isGovernorInput =
passetClassValueOfT
# gstClass
# value
#== 1
in pif
isGovernorInput
(pjust # inputF.outRef)
pnothing
)
# inputs
governorScriptPurpose =
pfmap
# plam
( \ref ->
mkRecordConstr
PSpending
(#_0 .= ref)
)
# governorInputRef
governorRedeemer =
governorScriptPurpose
#>>= pflip
# ptryFromRedeemer @(PAsData PGovernorRedeemer)
# redeemers
in pfmap # plam pfromData # governorRedeemer

View file

@ -57,8 +57,8 @@ import Plutarch.Extra.AssetClass (PAssetClassData, passetClass)
import Plutarch.Extra.Field (pletAll, pletAllC)
import "liqwid-plutarch-extra" Plutarch.Extra.List (pfindJust, plistEqualsBy, pmapMaybe)
import "liqwid-plutarch-extra" Plutarch.Extra.Map (pkeys, ptryLookup)
import Plutarch.Extra.Maybe (passertPJust, pjust, pmaybe, pmaybeData, pnothing)
import Plutarch.Extra.Ord (psort)
import Plutarch.Extra.Maybe (passertPJust, pjust, pmaybeData, pnothing)
import Plutarch.Extra.Ord (POrdering (..), pcompareBy, pfromOrd, psort)
import Plutarch.Extra.Record (mkRecordConstr, (.&), (.=))
import Plutarch.Extra.ScriptContext (
pfindTxInByTxOutRef,
@ -335,6 +335,8 @@ governorValidator =
----------------------------------------------------------------------------
pstClass <- pletC $ passetClass # pto pstSymbol # pconstant ""
getProposalDatum :: Term _ (PTxOut :--> PMaybe PProposalDatum) <-
pletC $
plam $
@ -342,8 +344,8 @@ governorValidator =
let isProposalUTxO =
txOutF.address
#== pdata proposalValidatorAddress
#&& psymbolValueOfT
# pstSymbol
#&& passetClassValueOf
# pstClass
# txOutF.value
#== 1
@ -388,16 +390,7 @@ governorValidator =
-- Check that exactly one proposal token is being minted.
pguardC "Exactly one proposal token must be minted" $
let vMap = pfromData $ pto txInfoF.mint
tnMap = plookup # puntag pstSymbol # vMap
-- Ada and PST
onlyPST = plength # pto vMap #== 2
onePST =
pmaybe
# pconstant False
# plam (#== AssocMap.psingleton # pconstant "" # 1)
# tnMap
in onlyPST #&& onePST
passetClassValueOf # pstClass # txInfoF.mint #== 1
-- Check that a stake is spent to create the propsal,
-- and the value it contains meets the requirement.
@ -510,14 +503,13 @@ governorValidator =
( \output -> unTermCont $ do
outputF <- pletFieldsC @'["address", "datum", "value"] output
let isAuthorityUTxO =
let atAmount =
psymbolValueOfT
# atSymbol
# outputF.value
#== 1
handleAuthorityUTxO =
unTermCont $ do
do
receiverScriptHash <-
pletC $
passertPJust
@ -556,13 +548,21 @@ governorValidator =
, ptraceIfFalse "Value correctly encodes Auth Check script" valueGATCorrect
]
pure receiverScriptHash
pure $ pjust # receiverScriptHash
pure $
pif
isAuthorityUTxO
(pjust # handleAuthorityUTxO)
pnothing
pmatchC
( pcompareBy
# pfromOrd
# atAmount
# 1
)
>>= \case
-- atAmount == 1
PEQ -> handleAuthorityUTxO
-- atAmount < 1
PLT -> pure pnothing
-- atAmount > 1
PGT -> pure $ ptraceError "More than one GAT in one UTxO"
)
-- The sorted hashes of all the GAT receivers.

View file

@ -10,7 +10,7 @@ module Agora.Proposal.Scripts (
proposalPolicy,
) where
import Agora.Governor (PGovernorRedeemer (PCreateProposal))
import Agora.Governor (PGovernorRedeemer (PCreateProposal), presolveGovernorRedeemer)
import Agora.Proposal (
PProposalDatum (PProposalDatum),
PProposalRedeemer (PAdvanceProposal, PCosign, PUnlockStake, PVote),
@ -70,7 +70,6 @@ import Plutarch.Extra.Record (mkRecordConstr, (.&), (.=))
import Plutarch.Extra.ScriptContext (
pfindTxInByTxOutRef,
ptryFromOutputDatum,
ptryFromRedeemer,
)
import Plutarch.Extra.Sum (PSum (PSum))
import Plutarch.Extra.Tagged (PTagged)
@ -82,7 +81,7 @@ import "liqwid-plutarch-extra" Plutarch.Extra.TermCont (
ptryFromC,
)
import Plutarch.Extra.Traversable (pfoldMap)
import Plutarch.Extra.Value (passetClassValueOfT, psymbolValueOf)
import Plutarch.Extra.Value (psymbolValueOf')
import Plutarch.Unsafe (punsafeCoerce)
{- | Policy for Proposals.
@ -118,44 +117,25 @@ proposalPolicy =
PMinting ((pfield @"_0" #) -> ownSymbol) <- pmatchC $ pfromData ctxF.purpose
let mintedProposalST =
psymbolValueOf
pguardC "Minted exactly one proposal ST"
$ pmatch
( pfromJust
#$ psymbolValueOf'
# ownSymbol
# txInfoF.mint
)
$ \(PPair minted burnt) ->
minted
#== 1
#&& ptraceIfFalse "Burning a proposal is not supported" (burnt #== 0)
pguardC "Minted exactly one proposal ST" $
mintedProposalST #== 1
let governorInputRef =
let governorRedeemer =
passertPJust
# "GST should move"
#$ pfindJust
# plam
( flip pletAll $ \inputF ->
let value = pfield @"value" # inputF.resolved
isGovernorInput =
passetClassValueOfT
# (ptoScottEncodingT # gstAssetClass)
# value
#== 1
in pif
isGovernorInput
(pjust # inputF.outRef)
pnothing
)
#$ presolveGovernorRedeemer
# (ptoScottEncodingT # gstAssetClass)
# pfromData txInfoF.inputs
governorScriptPurpose =
mkRecordConstr
PSpending
(#_0 .= governorInputRef)
governorRedeemer =
pfromData $
pfromJust
#$ ptryFromRedeemer @(PAsData PGovernorRedeemer)
# governorScriptPurpose
# txInfoF.redeemers
# txInfoF.redeemers
pguardC "Govenor redeemer correct" $
pcon PCreateProposal #== governorRedeemer

View file

@ -207,7 +207,7 @@ stakePolicy =
passetClassValueOfT
# (ptoScottEncodingT # gtClass)
# outputF.value
#== (pfromData datumF.stakedAmount)
#== pfromData datumF.stakedAmount
, ptraceIfFalse "Stake Owner should sign the transaction" $
pauthorizedBy
# authorizationContext txInfoF