move Proposal impl. to Agora.Proposal.Scripts to avoid cyclic deps

This commit is contained in:
Emily Martins 2022-04-25 22:32:26 +02:00
parent cee9eaec79
commit deaa5cf6f1
6 changed files with 253 additions and 197 deletions

View file

@ -129,6 +129,7 @@ library
Agora.Governor Agora.Governor
Agora.MultiSig Agora.MultiSig
Agora.Proposal Agora.Proposal
Agora.Proposal.Scripts
Agora.Proposal.Time Agora.Proposal.Time
Agora.Record Agora.Record
Agora.SafeMoney Agora.SafeMoney

View file

@ -10,7 +10,6 @@ module Agora.Effect.NoOp (noOpValidator, PNoOp) where
import Control.Applicative (Const) import Control.Applicative (Const)
import Agora.Effect (makeEffect) import Agora.Effect (makeEffect)
import Plutarch (popaque)
import Plutarch.Api.V1 (PValidator) import Plutarch.Api.V1 (PValidator)
import Plutarch.TryFrom (PTryFrom (..)) import Plutarch.TryFrom (PTryFrom (..))
import Plutus.V1.Ledger.Value (CurrencySymbol) import Plutus.V1.Ledger.Value (CurrencySymbol)

View file

@ -19,7 +19,6 @@ import Generics.SOP (Generic, I (I))
import Agora.Effect (makeEffect) import Agora.Effect (makeEffect)
import Agora.Utils (findTxOutByTxOutRef, paddValue, passert) import Agora.Utils (findTxOutByTxOutRef, paddValue, passert)
import Plutarch (popaque)
import Plutarch.Api.V1 ( import Plutarch.Api.V1 (
PCredential (..), PCredential (..),
PTuple, PTuple,
@ -34,7 +33,7 @@ import Plutarch.DataRepr (
PDataFields, PDataFields,
PIsDataReprInstances (..), PIsDataReprInstances (..),
) )
import Plutarch.Lift (PUnsafeLiftDecl (..)) import Plutarch.Lift (PConstantDecl, PUnsafeLiftDecl (..))
import Plutarch.Monadic qualified as P import Plutarch.Monadic qualified as P
import Plutarch.TryFrom (PTryFrom (..)) import Plutarch.TryFrom (PTryFrom (..))
import Plutus.V1.Ledger.Credential (Credential) import Plutus.V1.Ledger.Credential (Credential)
@ -69,10 +68,11 @@ newtype PTreasuryWithdrawalDatum (s :: S)
instance PUnsafeLiftDecl PTreasuryWithdrawalDatum where instance PUnsafeLiftDecl PTreasuryWithdrawalDatum where
type PLifted PTreasuryWithdrawalDatum = TreasuryWithdrawalDatum type PLifted PTreasuryWithdrawalDatum = TreasuryWithdrawalDatum
deriving via deriving via
(DerivePConstantViaData TreasuryWithdrawalDatum PTreasuryWithdrawalDatum) (DerivePConstantViaData TreasuryWithdrawalDatum PTreasuryWithdrawalDatum)
instance instance
(PConstant TreasuryWithdrawalDatum) (PConstantDecl TreasuryWithdrawalDatum)
instance PTryFrom PData PTreasuryWithdrawalDatum where instance PTryFrom PData PTreasuryWithdrawalDatum where
type PTryFromExcess PData PTreasuryWithdrawalDatum = Const () type PTryFromExcess PData PTreasuryWithdrawalDatum = Const ()
@ -99,7 +99,7 @@ treasuryWithdrawalValidator currSymbol = makeEffect currSymbol $
\_cs (datum' :: Term _ PTreasuryWithdrawalDatum) txOutRef' txInfo' -> P.do \_cs (datum' :: Term _ PTreasuryWithdrawalDatum) txOutRef' txInfo' -> P.do
datum <- pletFields @'["receivers", "treasuries"] datum' datum <- pletFields @'["receivers", "treasuries"] datum'
txInfo <- pletFields @'["outputs", "inputs"] txInfo' txInfo <- pletFields @'["outputs", "inputs"] txInfo'
PJust txOut <- pmatch $ findTxOutByTxOutRef # txOutRef' # pfromData txInfo' PJust txOut <- pmatch $ findTxOutByTxOutRef # txOutRef' # pfromData txInfo.inputs
effInput <- pletFields @'["address", "value"] $ txOut effInput <- pletFields @'["address", "value"] $ txOut
outputValues <- outputValues <-
plet $ plet $

View file

@ -26,11 +26,6 @@ module Agora.Proposal (
PProposalVotes (..), PProposalVotes (..),
PProposalId (..), PProposalId (..),
PResultTag (..), PResultTag (..),
-- * Scripts
proposalValidator,
proposalPolicy,
proposalDatumValid,
) where ) where
import GHC.Generics qualified as GHC import GHC.Generics qualified as GHC
@ -38,47 +33,26 @@ import Generics.SOP (Generic, I (I))
import Plutarch.Api.V1 ( import Plutarch.Api.V1 (
PDatumHash, PDatumHash,
PMap, PMap,
PMintingPolicy,
PPubKeyHash, PPubKeyHash,
PScriptContext (PScriptContext),
PScriptPurpose (PMinting, PSpending),
PTxInfo (PTxInfo),
PValidator,
PValidatorHash, PValidatorHash,
mintingPolicySymbol,
mkMintingPolicy,
) )
import Plutarch.DataRepr (
DerivePConstantViaData (..),
PDataFields,
PIsDataReprInstances (PIsDataReprInstances),
)
import Plutus.V1.Ledger.Api (DatumHash, PubKeyHash, ValidatorHash)
import PlutusTx qualified import PlutusTx qualified
import PlutusTx.AssocMap qualified as AssocMap import PlutusTx.AssocMap qualified as AssocMap
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
import Agora.Record (mkRecordConstr, (.&), (.=))
import Agora.SafeMoney (GTTag) import Agora.SafeMoney (GTTag)
import Agora.Utils (
anyOutput,
findTxOutByTxOutRef,
passert,
pnotNull,
psymbolValueOf,
ptokenSpent,
ptxSignedBy,
pvalueSpent,
)
import Control.Arrow (first) import Control.Arrow (first)
import Plutarch.Api.V1.Extra (passetClass, passetClassValueOf) import Plutarch.DataRepr (DerivePConstantViaData (..), PDataFields, PIsDataReprInstances (..))
import Plutarch.Builtin (PBuiltinMap) import Plutarch.Lift (
import Plutarch.Lift (DerivePConstantViaNewtype (..), PConstantDecl, PUnsafeLiftDecl (..)) DerivePConstantViaNewtype (..),
import Plutarch.Monadic qualified as P PConstantDecl,
PUnsafeLiftDecl (..),
)
import Plutarch.SafeMoney (PDiscrete, Tagged) import Plutarch.SafeMoney (PDiscrete, Tagged)
import Plutarch.TryFrom (PTryFrom (PTryFromExcess, ptryFrom')) import Plutarch.TryFrom (PTryFrom (PTryFromExcess, ptryFrom'))
import Plutarch.Unsafe (punsafeCoerce) import Plutarch.Unsafe (punsafeCoerce)
import Plutus.V1.Ledger.Value (AssetClass (AssetClass)) import Plutus.V1.Ledger.Api (DatumHash, PubKeyHash, ValidatorHash)
import Plutus.V1.Ledger.Value (AssetClass)
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Haskell-land -- Haskell-land
@ -395,161 +369,3 @@ data PProposalRedeemer (s :: S)
instance PUnsafeLiftDecl PProposalRedeemer where type PLifted PProposalRedeemer = ProposalRedeemer instance PUnsafeLiftDecl PProposalRedeemer where type PLifted PProposalRedeemer = ProposalRedeemer
deriving via (DerivePConstantViaData ProposalRedeemer PProposalRedeemer) instance (PConstantDecl ProposalRedeemer) deriving via (DerivePConstantViaData ProposalRedeemer PProposalRedeemer) instance (PConstantDecl ProposalRedeemer)
--------------------------------------------------------------------------------
{- | Policy for Proposals.
This needs to perform two checks:
- Governor is happy with mint.
- Exactly 1 token is minted.
NOTE: The governor needs to check that the datum is correct
and sent to the right address.
-}
proposalPolicy :: Proposal -> ClosedTerm PMintingPolicy
proposalPolicy proposal =
plam $ \_redeemer ctx' -> P.do
PScriptContext ctx' <- pmatch ctx'
ctx <- pletFields @'["txInfo", "purpose"] ctx'
PTxInfo txInfo' <- pmatch $ pfromData ctx.txInfo
txInfo <- pletFields @'["inputs", "mint"] txInfo'
PMinting _ownSymbol <- pmatch $ pfromData ctx.purpose
let inputs = txInfo.inputs
mintedValue = pfromData txInfo.mint
AssetClass (govCs, govTn) = proposal.governorSTAssetClass
PMinting ownSymbol' <- pmatch $ pfromData ctx.purpose
let mintedProposalST = passetClassValueOf # mintedValue # (passetClass # (pfield @"_0" # ownSymbol') # pconstant "")
passert "Governance state-thread token must move" $
ptokenSpent
# (passetClass # pconstant govCs # pconstant govTn)
# inputs
passert "Minted exactly one proposal ST" $
mintedProposalST #== 1
popaque (pconstant ())
-- | Validator for Proposals.
proposalValidator :: Proposal -> ClosedTerm PValidator
proposalValidator proposal =
plam $ \datum redeemer ctx' -> P.do
PScriptContext ctx' <- pmatch ctx'
ctx <- pletFields @'["txInfo", "purpose"] ctx'
txInfo <- plet $ pfromData ctx.txInfo
PTxInfo txInfo' <- pmatch txInfo
txInfoF <- pletFields @'["inputs", "mint"] txInfo'
PSpending ((pfield @"_0" #) -> txOutRef) <- pmatch $ pfromData ctx.purpose
PJust txOut <- pmatch $ findTxOutByTxOutRef # txOutRef # txInfoF.inputs
txOutF <- pletFields @'["address", "value"] $ txOut
let proposalDatum :: Term _ PProposalDatum
proposalDatum = pfromData $ punsafeCoerce datum
proposalRedeemer :: Term _ PProposalRedeemer
proposalRedeemer = pfromData $ punsafeCoerce redeemer
proposalF <-
pletFields
@'[ "proposalId"
, "effects"
, "status"
, "cosigners"
, "thresholds"
, "votes"
]
proposalDatum
ownAddress <- plet $ txOutF.address
stCurrencySymbol <- plet $ pconstant $ mintingPolicySymbol $ mkMintingPolicy (proposalPolicy proposal)
valueSpent <- plet $ pvalueSpent # txInfoF.inputs
spentST <- plet $ psymbolValueOf # stCurrencySymbol #$ valueSpent
let AssetClass (stakeSym, stakeTn) = proposal.stakeSTAssetClass
spentStakeST <- plet $ passetClassValueOf # valueSpent # (passetClass # pconstant stakeSym # pconstant stakeTn)
pmatch proposalRedeemer $ \case
PVote _r -> P.do
passert "ST at inputs must be 1" $
spentST #== 1
popaque (pconstant ())
--------------------------------------------------------------------------
PCosign r -> P.do
newSigs <- plet $ pfield @"newCosigners" # r
passert "ST at inputs must be 1" $
spentST #== 1
passert "Signed by all new cosigners" $
pall # plam (\sig -> ptxSignedBy # ctx.txInfo # sig) # newSigs
passert "As many new cosigners as Stake datums" $
spentStakeST #== plength # newSigs
passert "Signatures are correctly added to cosignature list" $
anyOutput @PProposalDatum # ctx.txInfo
#$ plam
$ \newValue address newProposalDatum -> P.do
-- This is a little sad. Can we do better by
-- building a new ProposalDatum and then comparing?
let correctDatum =
pdata newProposalDatum
#== pdata
( mkRecordConstr
PProposalDatum
( #proposalId .= proposalF.proposalId
.& #effects .= proposalF.effects
.& #status .= proposalF.status
.& #cosigners .= pdata (pconcat # newSigs # proposalF.cosigners)
.& #thresholds .= proposalF.thresholds
.& #votes .= proposalF.votes
)
)
foldr1
(#&&)
[ pcon PTrue
, ptraceIfFalse "Datum must be correct" correctDatum
, ptraceIfFalse "Value should be correct" $ pdata txOutF.value #== pdata newValue
, ptraceIfFalse "Must be sent to Proposal's address" $ ownAddress #== pdata address
]
popaque (pconstant ())
--------------------------------------------------------------------------
PUnlock _r -> P.do
passert "ST at inputs must be 1" $
spentST #== 1
popaque (pconstant ())
--------------------------------------------------------------------------
PAdvanceProposal _r -> P.do
passert "ST at inputs must be 1" $
spentST #== 1
popaque (pconstant ())
{- | Check for various invariants a proposal must uphold.
This can be used to check both upopn creation and
upon any following state transitions in the proposal.
-}
proposalDatumValid :: Term s (PProposalDatum :--> PBool)
proposalDatumValid =
phoistAcyclic $
plam $ \datum' -> P.do
datum <- pletFields @'["effects", "cosigners"] $ datum'
let effects :: Term _ (PBuiltinMap PResultTag (PBuiltinMap PValidatorHash PDatumHash))
effects = punsafeCoerce datum.effects
atLeastOneNegativeResult :: Term _ PBool
atLeastOneNegativeResult =
pany # plam (\pair -> pnull #$ pfromData $ psndBuiltin # pair) # effects
foldr1
(#&&)
[ ptraceIfFalse "Proposal has at least one ResultTag has no effects" atLeastOneNegativeResult
, ptraceIfFalse "Proposal has at least one cosigner" $ pnotNull # pfromData datum.cosigners
]

View file

@ -0,0 +1,227 @@
module Agora.Proposal.Scripts (
proposalValidator,
proposalPolicy,
proposalDatumValid,
) where
import Agora.Proposal
import Agora.Record (mkRecordConstr, (.&), (.=))
import Agora.Stake (PStakeDatum)
import Agora.Utils (
anyOutput,
findTxOutByTxOutRef,
passert,
pfindDatum',
pnotNull,
psymbolValueOf,
ptokenSpent,
ptxSignedBy,
pvalueSpent,
)
import Plutarch.Api.V1 (
PDatumHash,
PMaybeData (PDJust, PDNothing),
PMintingPolicy,
PPubKeyHash,
PScriptContext (PScriptContext),
PScriptPurpose (PMinting, PSpending),
PTxInInfo (PTxInInfo),
PTxInfo (PTxInfo),
PTxOut (PTxOut),
PValidator,
PValidatorHash,
mintingPolicySymbol,
mkMintingPolicy,
)
import Plutarch.Api.V1.Extra (passetClass, passetClassValueOf)
import Plutarch.Builtin (PBuiltinMap)
import Plutarch.Monadic qualified as P
import Plutarch.Unsafe (punsafeCoerce)
import Plutus.V1.Ledger.Value (AssetClass (AssetClass))
{- | Policy for Proposals.
This needs to perform two checks:
- Governor is happy with mint.
- Exactly 1 token is minted.
NOTE: The governor needs to check that the datum is correct
and sent to the right address.
-}
proposalPolicy :: Proposal -> ClosedTerm PMintingPolicy
proposalPolicy proposal =
plam $ \_redeemer ctx' -> P.do
PScriptContext ctx' <- pmatch ctx'
ctx <- pletFields @'["txInfo", "purpose"] ctx'
PTxInfo txInfo' <- pmatch $ pfromData ctx.txInfo
txInfo <- pletFields @'["inputs", "mint"] txInfo'
PMinting _ownSymbol <- pmatch $ pfromData ctx.purpose
let inputs = txInfo.inputs
mintedValue = pfromData txInfo.mint
AssetClass (govCs, govTn) = proposal.governorSTAssetClass
PMinting ownSymbol' <- pmatch $ pfromData ctx.purpose
let mintedProposalST = passetClassValueOf # mintedValue # (passetClass # (pfield @"_0" # ownSymbol') # pconstant "")
passert "Governance state-thread token must move" $
ptokenSpent
# (passetClass # pconstant govCs # pconstant govTn)
# inputs
passert "Minted exactly one proposal ST" $
mintedProposalST #== 1
popaque (pconstant ())
-- | Validator for Proposals.
proposalValidator :: Proposal -> ClosedTerm PValidator
proposalValidator proposal =
plam $ \datum redeemer ctx' -> P.do
PScriptContext ctx' <- pmatch ctx'
ctx <- pletFields @'["txInfo", "purpose"] ctx'
txInfo <- plet $ pfromData ctx.txInfo
PTxInfo txInfo' <- pmatch txInfo
txInfoF <- pletFields @'["inputs", "mint"] txInfo'
PSpending ((pfield @"_0" #) -> txOutRef) <- pmatch $ pfromData ctx.purpose
PJust txOut <- pmatch $ findTxOutByTxOutRef # txOutRef # txInfoF.inputs
txOutF <- pletFields @'["address", "value"] $ txOut
let proposalDatum :: Term _ PProposalDatum
proposalDatum = pfromData $ punsafeCoerce datum
proposalRedeemer :: Term _ PProposalRedeemer
proposalRedeemer = pfromData $ punsafeCoerce redeemer
proposalF <-
pletFields
@'[ "proposalId"
, "effects"
, "status"
, "cosigners"
, "thresholds"
, "votes"
]
proposalDatum
ownAddress <- plet $ txOutF.address
stCurrencySymbol <- plet $ pconstant $ mintingPolicySymbol $ mkMintingPolicy (proposalPolicy proposal)
valueSpent <- plet $ pvalueSpent # txInfoF.inputs
spentST <- plet $ psymbolValueOf # stCurrencySymbol #$ valueSpent
let AssetClass (stakeSym, stakeTn) = proposal.stakeSTAssetClass
stakeSTAssetClass <- plet $ passetClass # pconstant stakeSym # pconstant stakeTn
spentStakeST <- plet $ passetClassValueOf # valueSpent # stakeSTAssetClass
pmatch proposalRedeemer $ \case
PVote _r -> P.do
passert "ST at inputs must be 1" $
spentST #== 1
popaque (pconstant ())
--------------------------------------------------------------------------
PCosign r -> P.do
newSigs <- plet $ pfield @"newCosigners" # r
passert "ST at inputs must be 1" $
spentST #== 1
passert "Signed by all new cosigners" $
pall # plam (\sig -> ptxSignedBy # ctx.txInfo # sig) # newSigs
passert "As many new cosigners as Stake datums" $
spentStakeST #== plength # newSigs
let stakeDatumOwnedBy :: Term _ (PPubKeyHash :--> PStakeDatum :--> PBool)
stakeDatumOwnedBy =
phoistAcyclic $
plam $ \pk stakeDatum -> P.do
stakeDatumF <- pletFields @'["owner"] $ pto stakeDatum
stakeDatumF.owner #== pdata pk
-- Does the input have a `Stake` owned by a particular PK?
let isInputStakeOwnedBy :: Term _ (PAsData PPubKeyHash :--> PAsData PTxInInfo :--> PBool)
isInputStakeOwnedBy =
plam $ \ss txInInfo' -> P.do
PTxInInfo ((pfield @"resolved" #) -> txOut) <- pmatch $ pfromData txInInfo'
PTxOut txOut' <- pmatch txOut
txOutF <- pletFields @'["value", "datumHash"] txOut'
outStakeST <- plet $ passetClassValueOf # txOutF.value # stakeSTAssetClass
pmatch txOutF.datumHash $ \case
PDNothing _ -> pcon PFalse
PDJust ((pfield @"_0" #) -> datumHash) ->
pif
(outStakeST #== 1)
-- TODO: use 'ptryFindDatum' instead in the future
( pmatch (pfindDatum' # datumHash # txInfo) $ \case
PNothing -> pcon PFalse
PJust v -> stakeDatumOwnedBy # pfromData ss # pfromData v
)
(pcon PFalse)
passert "All new cosigners are witnessed by their Stake datums" $
pall
# plam (\sig -> pany # (isInputStakeOwnedBy # sig) # txInfoF.inputs)
# newSigs
passert "Signatures are correctly added to cosignature list" $
anyOutput @PProposalDatum # ctx.txInfo
#$ plam
$ \newValue address newProposalDatum -> P.do
let correctDatum =
pdata newProposalDatum
#== pdata
( mkRecordConstr
PProposalDatum
( #proposalId .= proposalF.proposalId
.& #effects .= proposalF.effects
.& #status .= proposalF.status
.& #cosigners .= pdata (pconcat # newSigs # proposalF.cosigners)
.& #thresholds .= proposalF.thresholds
.& #votes .= proposalF.votes
)
)
foldr1
(#&&)
[ pcon PTrue
, ptraceIfFalse "Datum must be correct" correctDatum
, ptraceIfFalse "Value should be correct" $ pdata txOutF.value #== pdata newValue
, ptraceIfFalse "Must be sent to Proposal's address" $ ownAddress #== pdata address
]
popaque (pconstant ())
--------------------------------------------------------------------------
PUnlock _r -> P.do
passert "ST at inputs must be 1" $
spentST #== 1
popaque (pconstant ())
--------------------------------------------------------------------------
PAdvanceProposal _r -> P.do
passert "ST at inputs must be 1" $
spentST #== 1
popaque (pconstant ())
{- | Check for various invariants a proposal must uphold.
This can be used to check both upopn creation and
upon any following state transitions in the proposal.
-}
proposalDatumValid :: Term s (PProposalDatum :--> PBool)
proposalDatumValid =
phoistAcyclic $
plam $ \datum' -> P.do
datum <- pletFields @'["effects", "cosigners"] $ datum'
let effects :: Term _ (PBuiltinMap PResultTag (PBuiltinMap PValidatorHash PDatumHash))
effects = punsafeCoerce datum.effects
atLeastOneNegativeResult :: Term _ PBool
atLeastOneNegativeResult =
pany # plam (\pair -> pnull #$ pfromData $ psndBuiltin # pair) # effects
foldr1
(#&&)
[ ptraceIfFalse "Proposal has at least one ResultTag has no effects" atLeastOneNegativeResult
, ptraceIfFalse "Proposal has at least one cosigner" $ pnotNull # pfromData datum.cosigners
]

View file

@ -11,6 +11,7 @@ module Agora.Utils (
pfind', pfind',
pfindDatum, pfindDatum,
pfindDatum', pfindDatum',
ptryFindDatum,
pvalueSpent, pvalueSpent,
ptxSignedBy, ptxSignedBy,
paddValue, paddValue,
@ -67,6 +68,7 @@ import Plutarch.Api.V1.Value (PValue (PValue))
import Plutarch.Builtin (ppairDataBuiltin) import Plutarch.Builtin (ppairDataBuiltin)
import Plutarch.Internal (punsafeCoerce) import Plutarch.Internal (punsafeCoerce)
import Plutarch.Monadic qualified as P import Plutarch.Monadic qualified as P
import Plutarch.TryFrom (PTryFrom, ptryFrom)
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Validator-level utility functions -- Validator-level utility functions
@ -82,6 +84,17 @@ pfindDatum = phoistAcyclic $
PTxInfo txInfo' <- pmatch txInfo'' PTxInfo txInfo' <- pmatch txInfo''
plookupTuple # datumHash #$ pfield @"datums" # txInfo' plookupTuple # datumHash #$ pfield @"datums" # txInfo'
-- | Find a datum with the given hash, and `ptryFrom` it.
ptryFindDatum :: PTryFrom PData a => Term s (PDatumHash :--> PTxInfo :--> PMaybe a)
ptryFindDatum = phoistAcyclic $
plam $ \datumHash txInfo'' -> P.do
PTxInfo txInfo' <- pmatch txInfo''
pmatch (plookupTuple # datumHash #$ pfield @"datums" # txInfo') $ \case
PNothing -> pcon PNothing
PJust datum -> P.do
(datum', _) <- ptryFrom $ pto datum
pcon (PJust datum')
{- | Find a datum with the given hash. {- | Find a datum with the given hash.
NOTE: this is unsafe in the sense that, if the data layout is wrong, this is UB. NOTE: this is unsafe in the sense that, if the data layout is wrong, this is UB.
-} -}