agora/agora/Agora/Governor.hs

497 lines
16 KiB
Haskell

{-# LANGUAGE TemplateHaskell #-}
{- |
Module : Agora.Governor
Maintainer : emi@haskell.fyi
Description: Governor entity scripts acting as authority of entire system.
Governor entity scripts acting as authority of entire system.
-}
module Agora.Governor (
-- * Haskell-land
GovernorDatum (..),
GovernorRedeemer (..),
Governor (..),
governorStateTokenName,
-- * Plutarch-land
PGovernorDatum (..),
PGovernorRedeemer (..),
-- * Scripts
governorPolicy,
governorValidator,
-- * Utilities
governorStateTokenAssetClass,
authorityTokenSymbolFromGovernor,
) where
--------------------------------------------------------------------------------
import GHC.Generics qualified as GHC
import Generics.SOP (Generic, I (I))
--------------------------------------------------------------------------------
import Agora.AuthorityToken (
AuthorityToken (..),
authorityTokenPolicy,
authorityTokensValidIn,
singleAuthorityTokenBurned,
)
import Agora.Proposal (
PProposalDatum,
PProposalId,
PProposalStatus (PDraft),
PProposalThresholds,
PProposalVotes (PProposalVotes),
PResultTag (PResultTag),
Proposal (..),
ProposalId,
ProposalThresholds,
pnextProposalId,
proposalDatumValid,
proposalPolicy,
proposalValidator,
)
import Agora.Utils (
containsSingleCurrencySymbol,
findOutputsToAddress,
hasOnlyOneTokenOfCurrencySymbol,
mustBePDJust,
mustBePJust,
mustFindDatum',
passert,
passetClassValueOf,
passetClassValueOf',
pfindDatum,
pfindTxInByTxOutRef,
pisDJust,
pisUxtoSpent,
pownCurrencySymbol,
psymbolValueOf,
scriptHashFromAddress,
)
--------------------------------------------------------------------------------
import Plutarch (popaque)
import Plutarch.Api.V1 (
PAddress,
PCurrencySymbol,
PDatumHash,
PMaybeData (PDJust),
PMintingPolicy,
PScriptPurpose (PSpending),
PValidator,
PValidatorHash,
PValue,
mintingPolicySymbol,
mkMintingPolicy,
mkValidator,
validatorHash,
)
import Plutarch.Api.V1.Extra (pownMintValue)
import Plutarch.DataRepr (
DerivePConstantViaData (..),
PDataFields,
PIsDataReprInstances (PIsDataReprInstances),
)
import Plutarch.Lift (PUnsafeLiftDecl (..))
import Plutarch.Monadic qualified as P
import Plutarch.Unsafe (punsafeCoerce)
--------------------------------------------------------------------------------
import Plutarch.Builtin (PBuiltinMap, pforgetData)
import Plutarch.Map.Extra (plookup, plookup')
import Plutus.V1.Ledger.Api (
Address (Address),
Credential (ScriptCredential),
CurrencySymbol (..),
MintingPolicy,
TxOutRef,
Validator,
ValidatorHash,
)
import Plutus.V1.Ledger.Value (
AssetClass (..),
TokenName (..),
)
import PlutusTx qualified
--------------------------------------------------------------------------------
-- | Datum for the Governor script.
data GovernorDatum = GovernorDatum
{ proposalThresholds :: ProposalThresholds
-- ^ Gets copied over upon creation of a 'Agora.Proposal.ProposalDatum'.
, nextProposalId :: ProposalId
-- ^ What tag the next proposal will get upon creating.
}
deriving stock (Show, GHC.Generic)
PlutusTx.makeIsDataIndexed ''GovernorDatum [('GovernorDatum, 0)]
{- | Redeemer for Governor script. The governor has two primary
responsibilities:
1. The gating of Proposal creation.
2. The gating of minting authority tokens.
Parameters of the governor can also be mutated by an effect.
-}
data GovernorRedeemer
= -- | Checks that a proposal was created lawfully, and allows it.
CreateProposal
| -- | Checks that a SINGLE proposal finished correctly,
-- and allows minting GATs for each effect script.
MintGATs
| -- | Allows effects to mutate the parameters.
MutateMutateGovernor
deriving stock (Show, GHC.Generic)
PlutusTx.makeIsDataIndexed
''GovernorRedeemer
[ ('CreateProposal, 0)
, ('MintGATs, 1)
, ('MutateMutateGovernor, 2)
]
-- | Parameters for creating Governor scripts.
data Governor = Governor
{ stORef :: TxOutRef
-- ^ An utxo, which will be spent to mint the state token for the governor validator.
}
governorStateTokenName :: TokenName
governorStateTokenName = TokenName ""
--------------------------------------------------------------------------------
-- | Plutarch-level datum for the Governor script.
newtype PGovernorDatum (s :: S) = PGovernorDatum
{ getGovernorDatum ::
Term
s
( PDataRecord
'[ "proposalThresholds" ':= PProposalThresholds
, "nextProposalId" ':= PProposalId
]
)
}
deriving stock (GHC.Generic)
deriving anyclass (Generic)
deriving anyclass (PIsDataRepr)
deriving
(PlutusType, PIsData, PDataFields)
via PIsDataReprInstances PGovernorDatum
instance PUnsafeLiftDecl PGovernorDatum where type PLifted PGovernorDatum = GovernorDatum
deriving via (DerivePConstantViaData GovernorDatum PGovernorDatum) instance (PConstant GovernorDatum)
-- | Plutarch-level version of 'GovernorRedeemer'.
data PGovernorRedeemer (s :: S)
= PCreateProposal (Term s (PDataRecord '[]))
| PMintGATs (Term s (PDataRecord '[]))
| PMutateGovernor (Term s (PDataRecord '[]))
deriving stock (GHC.Generic)
deriving anyclass (Generic)
deriving anyclass (PIsDataRepr)
deriving
(PlutusType, PIsData)
via PIsDataReprInstances PGovernorRedeemer
instance PUnsafeLiftDecl PGovernorRedeemer where type PLifted PGovernorRedeemer = GovernorRedeemer
deriving via (DerivePConstantViaData GovernorRedeemer PGovernorRedeemer) instance (PConstant GovernorRedeemer)
--------------------------------------------------------------------------------
{- | Policy for Governors.
This policy mints a state token for the 'governorValidator'.
It will check:
- The utxo specified in the Governor parameter is spent.
- Only one token is minted.
- Ensure the token name is "".
-}
governorPolicy :: Governor -> ClosedTerm PMintingPolicy
governorPolicy params =
plam $ \_ ctx' -> P.do
ctx <- pletFields @'["txInfo", "purpose"] ctx'
let oref = pconstant params.stORef
ownSymbol = pownCurrencySymbol # ctx'
mintValue <- plet $ pownMintValue # ctx'
passert "Referenced utxo should be spent" $ pisUxtoSpent # oref # ctx.txInfo
passert "Exactly one token should be minted" $
psymbolValueOf # ownSymbol # mintValue #== 1
#&& passetClassValueOf # ownSymbol # pconstant governorStateTokenName # mintValue #== 1
passert "Nothing is minted other than the state token" $
containsSingleCurrencySymbol # mintValue
popaque (pconstant ())
{- Validator for Governors.
A state token, minted by 'governorPolicy' is used to identify the datum utxo.
No matter what redeemer it receives, it will always check:
- The utxo which has the state token must be spent.
- The state token always stays at the script address.
- The utxo which holds the state token, has a well well-formed 'GovernorDatum' datum.
For 'CreateProposal' redeemer, it will check:
- Exactly one proposal state token is minted.
- Exactly one utxo should be sent to the proposal validator.
- The utxo must contain the proposal state token.
- The datum of said utxo must be correct.
- Proposal id in the governor datum must be advanced.
For 'MintGATs' redeemer, it will check:
- State datum is not changed.
- Exactly one proposal is being processed.
- Mint one GAT for every effect.
- The GATs is properly tagged. (Should we do this?)
- The GATs are sent to the appropraite effects. (Should we do this?)
For 'MutateGovernor', it will check:
- A GAT is burnt.
- Said GAT must be tagged by the effect that is spending it.
-}
governorValidator :: Governor -> ClosedTerm PValidator
governorValidator params =
plam $ \datum' redeemer' ctx' -> P.do
-- TODO: use ptryFrom
redeemer <- pmatch $ pfromData @PGovernorRedeemer $ punsafeCoerce redeemer'
ctx <- pletFields @'["txInfo", "purpose"] ctx'
txInfo' <- plet $ pfromData $ ctx.txInfo
txInfo <- pletFields @'["mint", "inputs", "outputs"] txInfo'
PSpending ((pfield @"_0" #) -> txOutRef') <- pmatch $ pfromData ctx.purpose
let txOutRef = pfromData txOutRef'
PJust ((pfield @"resolved" #) -> ownInput') <- pmatch $ pfindTxInByTxOutRef # txOutRef # txInfo'
ownInput <- pletFields @'["address", "value"] ownInput'
let selfAddress = pfromData $ ownInput.address
-- TODO: use ptryFrom
let oldParams' = pfromData @PGovernorDatum $ punsafeCoerce datum'
oldParams <- pletFields @'["proposalThresholds", "nextProposalId"] oldParams'
let ownInputDatumNFTAmount = stateTokenValueOf # ownInput.value
passert "Own input should have exactly one state token" $
ownInputDatumNFTAmount #== 1
ownOutputs <- plet $ findOutputsToAddress # txInfo' # selfAddress
passert "Exactly one utxo should be sent to the governor" $
plength # ownOutputs #== 1
ownOutput <- pletFields @'["value", "datumHash"] $ phead # ownOutputs
let ownOuputDatumNFTAmount = stateTokenValueOf # ownOutput.value
passert "State token should stay at governor's address" $
ownOuputDatumNFTAmount #== 1
passert "Output utxo to governor should have datum" $
pisDJust # ownOutput.datumHash
-- TODO: use `PTryFrom` and reject bad datum
newDatum' <- plet $ mustFindDatum' @PGovernorDatum # ownOutput.datumHash # ctx.txInfo
newParams <- pletFields @'["proposalThresholds", "nextProposalId"] newDatum'
case redeemer of
PCreateProposal _ -> P.do
passert "Proposal id should be advanced by 1" $
pnextProposalId # oldParams.nextProposalId #== newParams.nextProposalId
passert "Exactly one proposal token must be minted" $
hasOnlyOneTokenOfCurrencySymbol # pProposalSym # txInfo.mint
outputs <- plet $ findOutputsToAddress # ctx.txInfo # pconstant proposalValidatorAddress
passert "Exactly one utxo should be sent to the proposal validator" $
plength # outputs #== 1
output <- pletFields @'["value", "datumHash"] $ phead # outputs
passert "The proposal state token must be sent to the proposal validator" $
psymbolValueOf # pProposalSym # output.value #== 1
passert "The utxo paid to the proposal validator must have datum" $
pisDJust # output.datumHash
let proposalDatum' =
mustFindDatum' @PProposalDatum
# output.datumHash
# ctx.txInfo
proposalParams <-
pletFields
@'["id", "status", "cosigners", "thresholds", "votes"]
proposalDatum'
passert "Invalid proposal id in proposal parameters" $
proposalParams.id #== oldParams.nextProposalId
passert "Invalid thresholds in proposal parameters" $
proposalParams.thresholds #== oldParams.proposalThresholds
-- passert "Initial proposal votes should be empty" $
-- pnull #$ pto $ pto $ pfromData proposalParams.votes
-- passert "Initial proposal status should be Draft" $ P.do
-- s <- pmatch $ proposalParams.status
-- case s of
-- PDraft _ -> pconstant True
-- _ -> pconstant False
passert "Proposal datum must be valid" $
proposalDatumValid # proposalDatum'
-- TODO: proposal impl not done yet
ptraceError "Not implemented yet"
PMintGATs _ -> P.do
passert "Datum should not be changed" $
-- FIXME: There should be a better way to do this
(pforgetData $ pdata newDatum') #== datum'
-- TODO: any need to check the proposal datum here?
inputsWithProposalStateToken <-
plet $
pfilter
# ( plam $ \(((pfield @"value" #) . (pfield @"resolved" #)) -> value) ->
0 #< psymbolValueOf # pProposalSym # value
)
#$ pfromData txInfo.inputs
passert "One proposal at a time" $
plength # inputsWithProposalStateToken #== 1
proposalInputTxOut <-
pletFields @'["address", "value", "datumHash"] $
pfield @"resolved" #$ phead # inputsWithProposalStateToken
proposalDatum' <- plet $ mustFindDatum' @PProposalDatum # proposalInputTxOut.datumHash # txInfo'
passert "Proposal datum must be valid" $
proposalDatumValid # proposalDatum'
proposalDatum <- pletFields @'["id", "effects", "status", "cosigners", "thresholds", "votes"] proposalDatum'
PProposalVotes votes' <- pmatch $ pfromData proposalDatum.votes
votes <- plet votes'
let yesVotes = plookup' # pyesResultTag # votes
noVotes = plookup' # pnoResultTag # votes
-- TODO: check thresholds here
finalResultTag = pif (yesVotes #< noVotes) pnoResultTag pyesResultTag
effects <- plet $ plookup' # finalResultTag #$ proposalDatum.effects
gatCount <- plet $ plength #$ pto $ pto effects
passert "Required amount of GATs should be minted" $
psymbolValueOf # pProposalSym # txInfo.mint #== gatCount
passert "No token should be minted other than GAT" $
containsSingleCurrencySymbol # txInfo.mint
outputsWithGAT <-
plet $
pfilter
# ( plam $ \((pfield @"value" #) -> value) ->
0 #< psymbolValueOf # pGATSym # value
)
#$ pfromData txInfo.outputs
passert "Minted GAT amount should equal to amount of output GAT" $
plength # outputsWithGAT #== gatCount
popaque $
pfoldr
# ( plam $ \(pfromData -> output') _ -> P.do
output <- pletFields @'["address", "datumHash"] $ output'
let scriptHash =
mustBePJust # "GAT receiver is not a script"
#$ scriptHashFromAddress # output.address
datumHash =
mustBePDJust # "Output to effect should have datum"
#$ output.datumHash
expectedDatumHash =
mustBePJust # "Receiver is not in effect list"
#$ plookup # scriptHash # effects
passert "GAT must be tagged by the effect hash" $ authorityTokensValidIn # pGATSym # output'
passert "Unexpected datum" $ datumHash #== expectedDatumHash
(pconstant ())
)
# (pconstant ())
# outputsWithGAT
-- TODO: check proposal votes and timing
-- TODO: waiting for impl of proposal
PMutateGovernor _ -> P.do
passert "No token should be burnt other than GAT" $
containsSingleCurrencySymbol # txInfo.mint
popaque $ singleAuthorityTokenBurned pGATSym ctx.txInfo txInfo.mint
where
stateTokenAssetClass :: AssetClass
stateTokenAssetClass = governorStateTokenAssetClass params
proposalParams :: Proposal
proposalParams =
Proposal
{ governorSTAssetClass = stateTokenAssetClass
}
proposalSymbol :: CurrencySymbol
proposalSymbol = mintingPolicySymbol policy
where
policy :: MintingPolicy
policy = mkMintingPolicy $ proposalPolicy proposalParams
pProposalSym :: Term s PCurrencySymbol
pProposalSym = phoistAcyclic $ pconstant proposalSymbol
proposalValidatorAddress :: Address
proposalValidatorAddress = Address (ScriptCredential hash) Nothing
where
hash :: ValidatorHash
hash = validatorHash validator
validator :: Validator
validator = mkValidator $ proposalValidator proposalParams
stateTokenValueOf :: Term s (PValue :--> PInteger)
stateTokenValueOf = passetClassValueOf' stateTokenAssetClass
pGATSym :: Term s PCurrencySymbol
pGATSym = phoistAcyclic $ pconstant $ authorityTokenSymbolFromGovernor params
pyesResultTag :: Term s PResultTag
pyesResultTag = phoistAcyclic $ pcon $ PResultTag $ pconstant 1
pnoResultTag :: Term s PResultTag
pnoResultTag = phoistAcyclic $ pcon $ PResultTag $ pconstant 0
--------------------------------------------------------------------------------
governorStateTokenAssetClass :: Governor -> AssetClass
governorStateTokenAssetClass gov = AssetClass (symbol, governorStateTokenName)
where
policy :: MintingPolicy
policy = mkMintingPolicy $ governorPolicy gov
symbol :: CurrencySymbol
symbol = mintingPolicySymbol policy
authorityTokenSymbolFromGovernor :: Governor -> CurrencySymbol
authorityTokenSymbolFromGovernor gov = mintingPolicySymbol policy
where
params = AuthorityToken $ governorStateTokenAssetClass gov
policy = mkMintingPolicy $ authorityTokenPolicy params