Merge pull request #61 from Liqwid-Labs/connor/governor

Implementation of the governor components
This commit is contained in:
方泓睿 2022-05-17 20:52:58 +08:00 committed by GitHub
commit 72d615bf0a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 2114 additions and 132 deletions

View file

@ -1,7 +1,7 @@
# This really ought to be `/usr/bin/env bash`, but nix flakes don't like that.
SHELL := /bin/sh
.PHONY: hoogle format haddock usage tag lint ps_bridge
.PHONY: hoogle format haddock usage tag format_nix format_haskell format_check lint ps_bridge
usage:
@echo "usage: make <command> [OPTIONS]"
@ -11,6 +11,9 @@ usage:
@echo " format -- Format the project"
@echo " haddock -- Generate Haddock docs for project"
@echo " tag -- Generate CTAGS and ETAGS files for project"
@echo " format_haskell -- Format haskell stuff, including source code and cabal files"
@echo " format_nix -- Format *.nix files only"
@echo " format_check -- Check if all haskell stuff have been formatted correctly"
@echo " lint -- Get hlint suggestions for project"
@echo " ps_bridge -- Generate purescript bridge files"

View file

@ -4,6 +4,7 @@ module Agora.Governor where
import Prelude
import Agora.Proposal (ProposalId, ProposalThresholds)
import Agora.SafeMoney (GTTag)
import Data.Bounded.Generic (genericBottom, genericTop)
import Data.Enum (class Enum)
import Data.Enum.Generic (genericPred, genericSucc)
@ -13,6 +14,10 @@ import Data.Lens.Iso.Newtype (_Newtype)
import Data.Lens.Record (prop)
import Data.Maybe (Maybe(..))
import Data.Newtype (class Newtype)
import Data.Tagged (Tagged)
import GHC.Num.Integer (Integer)
import Plutus.V1.Ledger.Tx (TxOutRef)
import Plutus.V1.Ledger.Value (AssetClass)
import Type.Proxy (Proxy(Proxy))
newtype GovernorDatum = GovernorDatum
@ -34,6 +39,7 @@ _GovernorDatum = _Newtype
data GovernorRedeemer
= CreateProposal
| MintGATs
| MutateGovernor
derive instance Generic GovernorRedeemer _
@ -57,21 +63,24 @@ _MintGATs = prism' (const MintGATs) case _ of
MintGATs -> Just unit
_ -> Nothing
_MutateGovernor :: Prism' GovernorRedeemer Unit
_MutateGovernor = prism' (const MutateGovernor) case _ of
MutateGovernor -> Just unit
_ -> Nothing
--------------------------------------------------------------------------------
data Governor = Governor
newtype Governor = Governor
{ gstOutRef :: TxOutRef
, gtClassRef :: Tagged GTTag AssetClass
, maximumCosigners :: Integer
}
derive instance Generic Governor _
instance Enum Governor where
succ = genericSucc
pred = genericPred
instance Bounded Governor where
bottom = genericBottom
top = genericTop
derive instance Newtype Governor _
--------------------------------------------------------------------------------
_Governor :: Iso' Governor Unit
_Governor = iso (const unit) (const Governor)
_Governor :: Iso' Governor {gstOutRef :: TxOutRef, gtClassRef :: Tagged GTTag AssetClass, maximumCosigners :: Integer}
_Governor = _Newtype

View file

@ -14,7 +14,6 @@ import Data.Lens.Record (prop)
import Data.Maybe (Maybe(..))
import Data.Newtype (class Newtype)
import Data.Tagged (Tagged)
import Data.Tuple (Tuple)
import GHC.Num.Integer (Integer)
import Plutus.V1.Ledger.Crypto (PubKeyHash)
import Plutus.V1.Ledger.Scripts (DatumHash, ValidatorHash)
@ -120,7 +119,7 @@ _ProposalVotes = _Newtype
newtype ProposalDatum = ProposalDatum
{ proposalId :: ProposalId
, effects :: Map ResultTag (Array (Tuple ValidatorHash DatumHash))
, effects :: Map ResultTag (Map ValidatorHash DatumHash)
, status :: ProposalStatus
, cosigners :: Array PubKeyHash
, thresholds :: ProposalThresholds
@ -133,7 +132,7 @@ derive instance Newtype ProposalDatum _
--------------------------------------------------------------------------------
_ProposalDatum :: Iso' ProposalDatum {proposalId :: ProposalId, effects :: Map ResultTag (Array (Tuple ValidatorHash DatumHash)), status :: ProposalStatus, cosigners :: Array PubKeyHash, thresholds :: ProposalThresholds, votes :: ProposalVotes}
_ProposalDatum :: Iso' ProposalDatum {proposalId :: ProposalId, effects :: Map ResultTag (Map ValidatorHash DatumHash), status :: ProposalStatus, cosigners :: Array PubKeyHash, thresholds :: ProposalThresholds, votes :: ProposalVotes}
_ProposalDatum = _Newtype
--------------------------------------------------------------------------------

View file

@ -0,0 +1,619 @@
{- |
Module : Spec.Sample.Governor
Maintainer : connor@mlabs.city
Description: Sample based testing for Governor utxos
This module tests primarily the happy path for Governor interactions
-}
module Sample.Governor (
createProposal,
mutateState,
mintGATs,
mintGST,
) where
--------------------------------------------------------------------------------
import Plutarch.Api.V1 (mkValidator, validatorHash)
import Plutarch.SafeMoney.Tagged
--------------------------------------------------------------------------------
import Plutus.V1.Ledger.Address (scriptHashAddress)
import Plutus.V1.Ledger.Api (
Address (..),
Credential (ScriptCredential),
Datum (..),
ScriptContext (..),
ScriptPurpose (Minting, Spending),
ToData (toBuiltinData),
TokenName (..),
TxInInfo (TxInInfo),
TxInfo (..),
TxOut (..),
TxOutRef (..),
Validator,
ValidatorHash (..),
)
import Plutus.V1.Ledger.Interval qualified as Interval
import Plutus.V1.Ledger.Scripts (unitDatum)
import Plutus.V1.Ledger.Value (
AssetClass (..),
)
import Plutus.V1.Ledger.Value qualified as Value
import PlutusTx.AssocMap qualified as AssocMap
--------------------------------------------------------------------------------
import Agora.Effect.NoOp (noOpValidator)
import Agora.Governor (GovernorDatum (..), getNextProposalId)
import Agora.Proposal (
ProposalDatum (..),
ProposalId (..),
ProposalStatus (..),
ProposalVotes (..),
ResultTag (..),
emptyVotesFor,
)
import Agora.Proposal qualified as P
import Agora.Stake (
ProposalLock (..),
Stake (..),
StakeDatum (..),
)
--------------------------------------------------------------------------------
import Sample.Shared (
authorityTokenSymbol,
defaultProposalThresholds,
govAssetClass,
govSymbol,
govValidatorAddress,
gstUTXORef,
minAda,
proposalPolicySymbol,
proposalValidatorAddress,
signer,
signer2,
stake,
stakeAddress,
stakeAssetClass,
)
import Test.Util (datumPair, toDatumHash)
--------------------------------------------------------------------------------
{- | A valid 'ScriptContext' for minting GST.
- Only the minting policy will be ran in the transaction.
- An arbitrary UTXO is spent to create the token.
- We call this the "witness" UTXO.
- This UTXO is referenced in the 'Agora.Governor.Governor' parameter
- The minting policy should only be ran once its life time,
cause the GST cannot be minted twice or burnt.
- The output UTXO must carry a valid 'GovernorDatum'.
- It's worth noticing that the transaction should send the GST to the governor validator,
but unfortunately we can't check it in the policy. The GST will stay at the address of
the governor validator forever once the token is under control of the said validator.
TODO: tag the output UTXO with the target address.
-}
mintGST :: ScriptContext
mintGST =
let gst = Value.assetClassValue govAssetClass 1
---
governorOutputDatum' :: GovernorDatum
governorOutputDatum' =
GovernorDatum
{ proposalThresholds = defaultProposalThresholds
, nextProposalId = ProposalId 0
}
governorOutputDatum :: Datum
governorOutputDatum = Datum $ toBuiltinData governorOutputDatum'
governorOutput :: TxOut
governorOutput =
TxOut
{ txOutAddress = govValidatorAddress
, txOutValue = gst <> minAda
, txOutDatumHash = Just $ toDatumHash governorOutputDatum
}
---
witness :: ValidatorHash
witness = "a926a9a72a0963f428e3252caa8354e655603996fb8892d6b8323fd072345924"
witnessAddress :: Address
witnessAddress = Address (ScriptCredential witness) Nothing
---
-- The witness UTXO must be consumed.
witnessInput :: TxOut
witnessInput =
TxOut
{ txOutAddress = witnessAddress
, txOutValue = mempty
, txOutDatumHash = Nothing
}
initialSpend :: TxInInfo
initialSpend = TxInInfo gstUTXORef witnessInput
in ScriptContext
{ scriptContextTxInfo =
TxInfo
{ txInfoInputs =
[ initialSpend
]
, txInfoOutputs = [governorOutput]
, -- Some ada to cover the transaction fee
txInfoFee = Value.singleton "" "" 2
, -- Exactly one GST is minted
txInfoMint = gst
, txInfoDCert = []
, txInfoWdrl = []
, txInfoValidRange = Interval.always
, txInfoSignatories = [signer]
, txInfoData = [datumPair governorOutputDatum]
, txInfoId = "90906d3e6b4d6dec2e747dcdd9617940ea8358164c7244694cfa39dec18bd9d4"
}
, scriptContextPurpose = Minting govSymbol
}
{- | A valid script context to create a proposal.
Three component will run in the transaction:
TODO: mention redeemers
- Governor validator
- Stake validator
- Proposal policy
The components will ensure:
- The governor state UTXO is spent
- A new UTXO is paid back to governor validator, which carries the GST.
- The proposal id in the state datum is advanced.
- A new UTXO is sent to the proposal validator
- The UTXO contains a newly minted proposal state token.
- It also carries a legal proposal state datum, whose status is set to 'Agora.Proposal.Draft'.
- A stake is spent to create a proposal
- The stake owner must sign the transaction.
- The output stake must paid back to the stake validator.
- The output stake is locked by the newly created proposal.
-}
createProposal :: ScriptContext
createProposal =
let pst = Value.singleton proposalPolicySymbol "" 1
gst = Value.assetClassValue govAssetClass 1
sst = Value.assetClassValue stakeAssetClass 1
stackedGTs = 424242424242
thisProposalId = ProposalId 0
---
governorInputDatum' :: GovernorDatum
governorInputDatum' =
GovernorDatum
{ proposalThresholds = defaultProposalThresholds
, nextProposalId = thisProposalId
}
governorInputDatum :: Datum
governorInputDatum = Datum $ toBuiltinData governorInputDatum'
governorInput :: TxOut
governorInput =
TxOut
{ txOutAddress = govValidatorAddress
, txOutValue = gst
, txOutDatumHash = Just $ toDatumHash governorInputDatum
}
---
effects =
AssocMap.fromList
[ (ResultTag 0, AssocMap.empty)
, (ResultTag 1, AssocMap.empty)
]
proposalDatum :: Datum
proposalDatum =
Datum
( toBuiltinData $
ProposalDatum
{ P.proposalId = ProposalId 0
, effects = effects
, status = Draft
, cosigners = [signer]
, thresholds = defaultProposalThresholds
, votes = emptyVotesFor effects
}
)
proposalOutput :: TxOut
proposalOutput =
TxOut
{ txOutAddress = proposalValidatorAddress
, txOutValue = pst <> minAda
, txOutDatumHash = Just (toDatumHash proposalDatum)
}
---
stakeInputDatum' :: StakeDatum
stakeInputDatum' =
StakeDatum
{ stakedAmount = Tagged stackedGTs
, owner = signer
, lockedBy = []
}
stakeInputDatum :: Datum
stakeInputDatum = Datum $ toBuiltinData stakeInputDatum'
stakeInput :: TxOut
stakeInput =
TxOut
{ txOutAddress = stakeAddress
, txOutValue = sst <> Value.assetClassValue (untag stake.gtClassRef) stackedGTs
, txOutDatumHash = Just (toDatumHash stakeInputDatum)
}
---
governorOutputDatum' :: GovernorDatum
governorOutputDatum' = governorInputDatum' {nextProposalId = getNextProposalId thisProposalId}
governorOutputDatum :: Datum
governorOutputDatum = Datum $ toBuiltinData governorOutputDatum'
governorOutput :: TxOut
governorOutput =
governorInput
{ txOutDatumHash = Just $ toDatumHash governorOutputDatum
, txOutValue = gst <> minAda
}
---
proposalLocks :: [ProposalLock]
proposalLocks =
[ ProposalLock (ResultTag 0) thisProposalId
, ProposalLock (ResultTag 1) thisProposalId
]
stakeOutputDatum' :: StakeDatum
stakeOutputDatum' = stakeInputDatum' {lockedBy = proposalLocks}
stakeOutputDatum :: Datum
stakeOutputDatum = Datum $ toBuiltinData stakeOutputDatum'
stakeOutput :: TxOut
stakeOutput =
stakeInput
{ txOutDatumHash = Just $ toDatumHash stakeOutputDatum
, txOutValue = sst <> Value.assetClassValue (untag stake.gtClassRef) stackedGTs <> minAda
}
---
ownInputRef :: TxOutRef
ownInputRef = TxOutRef "4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865" 1
in ScriptContext
{ scriptContextTxInfo =
TxInfo
{ txInfoInputs =
[ TxInInfo
ownInputRef
governorInput
, TxInInfo
(TxOutRef "4262bbd0b3fc926b74eaa8abab5def6ce5e6b94f19cf221c02a16e7da8cd470f" 1)
stakeInput
]
, txInfoOutputs = [proposalOutput, governorOutput, stakeOutput]
, txInfoFee = Value.singleton "" "" 2
, txInfoMint = pst
, txInfoDCert = []
, txInfoWdrl = []
, txInfoValidRange = Interval.always
, txInfoSignatories = [signer]
, txInfoData =
datumPair
<$> [ governorInputDatum
, governorOutputDatum
, proposalDatum
, stakeInputDatum
, stakeOutputDatum
]
, txInfoId = "1ffb9669335c908d9a4774a4bf7aa7bfafec91d015249b4138bc83fde4a3330a"
}
, scriptContextPurpose = Spending ownInputRef
}
{- This script context should be a valid transaction for minting authority for the effect scrips.
The following components will run:
- Governor validator
- Authority policy
- Proposal validator
There should be only one proposal the transaction.
The validity of the proposal will be checked:
- It's in 'Agora.Proposal.Locked' state.
- It has a 'winner' effect group, meaning that the votes meet the requirements.
The system will ensure that for every effect scrips in said effect group,
a newly minted GAT is sent to the corresponding effect, and properly tagged.
-}
mintGATs :: ScriptContext
mintGATs =
let pst = Value.singleton proposalPolicySymbol "" 1
gst = Value.assetClassValue govAssetClass 1
gat = Value.assetClassValue atAssetClass 1
---
mockEffect :: Validator
mockEffect = mkValidator $ noOpValidator ""
mockEffectHash :: ValidatorHash
mockEffectHash = validatorHash mockEffect
mockEffectAddress :: Address
mockEffectAddress = scriptHashAddress mockEffectHash
mockEffectOutputDatum :: Datum
mockEffectOutputDatum = unitDatum
atTokenName :: TokenName
atTokenName = TokenName hash
where
ValidatorHash hash = mockEffectHash
atAssetClass :: AssetClass
atAssetClass = AssetClass (authorityTokenSymbol, atTokenName)
---
governorInputDatum' :: GovernorDatum
governorInputDatum' =
GovernorDatum
{ proposalThresholds = defaultProposalThresholds
, nextProposalId = ProposalId 5
}
governorInputDatum :: Datum
governorInputDatum = Datum $ toBuiltinData governorInputDatum'
governorInput :: TxOut
governorInput =
TxOut
{ txOutAddress = govValidatorAddress
, txOutValue = gst
, txOutDatumHash = Just $ toDatumHash governorInputDatum
}
---
effects =
AssocMap.fromList
[ (ResultTag 0, AssocMap.empty)
, (ResultTag 1, AssocMap.singleton mockEffectHash $ toDatumHash mockEffectOutputDatum)
]
proposalVotes :: ProposalVotes
proposalVotes =
ProposalVotes $
AssocMap.fromList
[ (ResultTag 0, 100)
, (ResultTag 1, 2000) -- The winner
]
proposalInputDatum' :: ProposalDatum
proposalInputDatum' =
ProposalDatum
{ P.proposalId = ProposalId 0
, effects = effects
, status = Locked
, cosigners = [signer, signer2]
, thresholds = defaultProposalThresholds
, votes = proposalVotes
}
proposalInputDatum :: Datum
proposalInputDatum = Datum $ toBuiltinData proposalInputDatum'
proposalInput :: TxOut
proposalInput =
TxOut
{ txOutAddress = proposalValidatorAddress
, txOutValue = pst
, txOutDatumHash = Just (toDatumHash proposalInputDatum)
}
---
governorOutputDatum' :: GovernorDatum
governorOutputDatum' = governorInputDatum'
governorOutputDatum :: Datum
governorOutputDatum = Datum $ toBuiltinData governorOutputDatum'
governorOutput :: TxOut
governorOutput =
governorInput
{ txOutDatumHash = Just $ toDatumHash governorOutputDatum
, txOutValue = gst <> minAda
}
---
proposalOutputDatum' :: ProposalDatum
proposalOutputDatum' = proposalInputDatum' {status = Finished}
proposalOutputDatum :: Datum
proposalOutputDatum = Datum $ toBuiltinData proposalOutputDatum'
proposalOutput :: TxOut
proposalOutput =
proposalInput
{ txOutDatumHash = Just $ toDatumHash proposalOutputDatum
, txOutValue = pst <> minAda
}
--
mockEffectOutput :: TxOut
mockEffectOutput =
TxOut
{ txOutAddress = mockEffectAddress
, txOutDatumHash = Just $ toDatumHash mockEffectOutputDatum
, txOutValue = gat <> minAda
}
--
ownInputRef :: TxOutRef
ownInputRef = TxOutRef "4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865" 1
in ScriptContext
{ scriptContextTxInfo =
TxInfo
{ txInfoInputs =
[ TxInInfo ownInputRef governorInput
, TxInInfo
(TxOutRef "11b2162f267614b803761032b6333040fc61478ae788c088614ee9487ab0c1b7" 1)
proposalInput
]
, txInfoOutputs =
[ governorOutput
, proposalOutput
, mockEffectOutput
]
, txInfoFee = Value.singleton "" "" 2
, txInfoMint = gat
, txInfoDCert = []
, txInfoWdrl = []
, txInfoValidRange = Interval.always
, txInfoSignatories = [signer, signer2]
, txInfoData =
datumPair
<$> [ governorInputDatum
, governorOutputDatum
, proposalInputDatum
, proposalOutputDatum
, mockEffectOutputDatum
]
, txInfoId = "ff755f613c1f7487dfbf231325c67f481f7a97e9faf4d8b09ad41176fd65cbe7"
}
, scriptContextPurpose = Spending ownInputRef
}
{- | A valid script context for changing the state datum of the governor.
In this case, the following components will run:
* Governor validator
* Effect script
The effect script should carry an valid tagged authority token,
and said token will be burnt in the transaction. We use 'noOpValidator'
here as a mock effect, so no actual change is done to the governor state.
TODO: use 'mutateGovernorEffect' as the mock effect in the future.
The governor will ensure the new governor state is valid.
-}
mutateState :: ScriptContext
mutateState =
let gst = Value.assetClassValue govAssetClass 1
gat = Value.assetClassValue atAssetClass 1
burntGAT = Value.assetClassValue atAssetClass (-1)
---
-- TODO: Use the *real* effect, see https://github.com/Liqwid-Labs/agora/pull/62
mockEffect :: Validator
mockEffect = mkValidator $ noOpValidator ""
mockEffectHash :: ValidatorHash
mockEffectHash = validatorHash mockEffect
mockEffectAddress :: Address
mockEffectAddress = scriptHashAddress mockEffectHash
atTokenName :: TokenName
atTokenName = TokenName hash
where
ValidatorHash hash = mockEffectHash
atAssetClass :: AssetClass
atAssetClass = AssetClass (authorityTokenSymbol, atTokenName)
--
mockEffectInputDatum :: Datum
mockEffectInputDatum = unitDatum
mockEffectInput :: TxOut
mockEffectInput =
TxOut
{ txOutAddress = mockEffectAddress
, txOutValue = gat -- Will be burnt
, txOutDatumHash = Just $ toDatumHash mockEffectInputDatum
}
--
mockEffectOutputDatum :: Datum
mockEffectOutputDatum = mockEffectInputDatum
mockEffectOutput :: TxOut
mockEffectOutput =
mockEffectInput
{ txOutValue = minAda
, txOutDatumHash = Just $ toDatumHash mockEffectOutputDatum
}
--
governorInputDatum' :: GovernorDatum
governorInputDatum' =
GovernorDatum
{ proposalThresholds = defaultProposalThresholds
, nextProposalId = ProposalId 5
}
governorInputDatum :: Datum
governorInputDatum = Datum $ toBuiltinData governorInputDatum'
governorInput :: TxOut
governorInput =
TxOut
{ txOutAddress = govValidatorAddress
, txOutValue = gst
, txOutDatumHash = Just $ toDatumHash governorInputDatum
}
--
governorOutputDatum' :: GovernorDatum
governorOutputDatum' = governorInputDatum'
governorOutputDatum :: Datum
governorOutputDatum = Datum $ toBuiltinData governorOutputDatum'
governorOutput :: TxOut
governorOutput =
governorInput
{ txOutDatumHash = Just $ toDatumHash governorOutputDatum
, txOutValue = gst <> minAda
}
--
ownInputRef :: TxOutRef
ownInputRef = TxOutRef "f867238a04597c99a0b9858746557d305025cca3b9f78ea14d5c88c4cfcf58ff" 1
in ScriptContext
{ scriptContextTxInfo =
TxInfo
{ txInfoInputs =
[ TxInInfo ownInputRef governorInput
, TxInInfo
(TxOutRef "ecff06d7cf99089294569cc8b92609e44927278f9901730715d14634fbc10089" 1)
mockEffectInput
]
, txInfoOutputs =
[ governorOutput
, mockEffectOutput
]
, txInfoFee = Value.singleton "" "" 2
, txInfoMint = burntGAT
, txInfoDCert = []
, txInfoWdrl = []
, txInfoValidRange = Interval.always
, txInfoSignatories = [signer]
, txInfoData =
datumPair
<$> [ governorInputDatum
, governorOutputDatum
, mockEffectInputDatum
, mockEffectOutputDatum
]
, txInfoId = "9a12a605086a9f866731869a42d0558036fc739c74fea3849aa41562c015aaf9"
}
, scriptContextPurpose = Spending ownInputRef
}

View file

@ -60,8 +60,8 @@ proposalCreation =
let st = Value.singleton proposalPolicySymbol "" 1 -- Proposal ST
effects =
AssocMap.fromList
[ (ResultTag 0, [])
, (ResultTag 1, [])
[ (ResultTag 0, AssocMap.empty)
, (ResultTag 1, AssocMap.empty)
]
proposalDatum :: Datum
proposalDatum =
@ -155,8 +155,8 @@ cosignProposal newSigners =
let st = Value.singleton proposalPolicySymbol "" 1 -- Proposal ST
effects =
AssocMap.fromList
[ (ResultTag 0, [])
, (ResultTag 1, [])
[ (ResultTag 0, AssocMap.empty)
, (ResultTag 1, AssocMap.empty)
]
proposalBefore :: ProposalDatum
proposalBefore =
@ -193,7 +193,7 @@ cosignProposal newSigners =
mconcat
[ Value.singleton "" "" 10_000_000
, Value.assetClassValue (untag stake.gtClassRef) 50_000_000
, Value.singleton stakeSymbol "" 1
, Value.assetClassValue stakeAssetClass 1
]
, txOutDatumHash = Just (toDatumHash stakeDatum)
}
@ -214,7 +214,7 @@ cosignProposal newSigners =
mconcat
[ Value.singleton "" "" 10_000_000
, Value.assetClassValue (untag stake.gtClassRef) 50_000_000
, Value.singleton stakeSymbol "" 1
, Value.assetClassValue stakeAssetClass 1
]
, txOutDatumHash = Just (toDatumHash stakeDatum)
}

View file

@ -15,15 +15,20 @@ module Sample.Shared (
-- ** Stake
stake,
stakeSymbol,
stakeAssetClass,
stakeValidatorHash,
stakeAddress,
stakeSymbol,
-- ** Governor
governor,
govPolicy,
govValidator,
govSymbol,
govAssetClass,
govValidatorAddress,
govValidatorHash,
gstUTXORef,
-- ** Proposal
defaultProposalThresholds,
@ -32,6 +37,10 @@ module Sample.Shared (
proposalValidatorHash,
proposalValidatorAddress,
-- ** Authority
authorityToken,
authorityTokenSymbol,
-- ** Treasury
treasuryOut,
gatTn,
@ -41,22 +50,31 @@ module Sample.Shared (
wrongEffHash,
) where
import Agora.AuthorityToken
import Agora.Effect.NoOp (noOpValidator)
import Agora.Governor (
Governor (Governor),
)
import Agora.Governor.Scripts (
authorityTokenFromGovernor,
authorityTokenSymbolFromGovernor,
governorPolicy,
governorSTAssetClassFromGovernor,
governorValidator,
governorValidatorHash,
proposalFromGovernor,
proposalSTSymbolFromGovernor,
proposalValidatorHashFromGovernor,
stakeFromGovernor,
stakeSTAssetClassFromGovernor,
stakeSTSymbolFromGovernor,
stakeValidatorHashFromGovernor,
)
import Agora.Proposal (
Proposal (..),
ProposalThresholds (..),
)
import Agora.Proposal.Scripts (
proposalPolicy,
proposalValidator,
)
import Agora.Stake (Stake (..))
import Agora.Stake.Scripts (stakePolicy, stakeValidator)
import Agora.Treasury (treasuryValidator)
import Agora.Utils (validatorHashToTokenName)
import Plutarch.Api.V1 (
@ -73,38 +91,46 @@ import Plutus.V1.Ledger.Api (
CurrencySymbol,
MintingPolicy (..),
PubKeyHash,
TxOutRef (TxOutRef),
Value,
)
import Plutus.V1.Ledger.Contexts (
TxOut (..),
)
import Plutus.V1.Ledger.Scripts (Validator, ValidatorHash (..))
import Plutus.V1.Ledger.Value (TokenName, Value)
import Plutus.V1.Ledger.Value (AssetClass, TokenName)
import Plutus.V1.Ledger.Value qualified as Value
--------------------------------------------------------------------------------
stake :: Stake
stake =
Stake
{ gtClassRef =
Tagged $
Value.assetClass
"da8c30857834c6ae7203935b89278c532b3995245295456f993e1d24"
"LQ"
, proposalSTClass = Value.assetClass proposalPolicySymbol ""
}
stake = stakeFromGovernor governor
stakeSymbol :: CurrencySymbol
stakeSymbol = mintingPolicySymbol $ mkMintingPolicy $ stakePolicy stake.gtClassRef
stakeSymbol = stakeSTSymbolFromGovernor governor
stakeAssetClass :: AssetClass
stakeAssetClass = stakeSTAssetClassFromGovernor governor
stakeValidatorHash :: ValidatorHash
stakeValidatorHash = validatorHash $ mkValidator (stakeValidator stake)
stakeValidatorHash = stakeValidatorHashFromGovernor governor
stakeAddress :: Address
stakeAddress = Address (ScriptCredential stakeValidatorHash) Nothing
gstUTXORef :: TxOutRef
gstUTXORef = TxOutRef "f28cd7145c24e66fd5bcd2796837aeb19a48a2656e7833c88c62a2d0450bd00d" 0
governor :: Governor
governor = Governor
governor = Governor oref gt mc
where
oref = gstUTXORef
gt =
Tagged $
Value.assetClass
"da8c30857834c6ae7203935b89278c532b3995245295456f993e1d24"
"LQ"
mc = 6
govPolicy :: MintingPolicy
govPolicy = mkMintingPolicy (governorPolicy governor)
@ -115,16 +141,20 @@ govValidator = mkValidator (governorValidator governor)
govSymbol :: CurrencySymbol
govSymbol = mintingPolicySymbol govPolicy
govAssetClass :: AssetClass
govAssetClass = governorSTAssetClassFromGovernor governor
govValidatorHash :: ValidatorHash
govValidatorHash = governorValidatorHash governor
govValidatorAddress :: Address
govValidatorAddress = scriptHashAddress govValidatorHash
proposal :: Proposal
proposal =
Proposal
{ governorSTAssetClass = Value.assetClass govSymbol ""
, stakeSTAssetClass = Value.assetClass stakeSymbol ""
, maximumCosigners = 6
}
proposal = proposalFromGovernor governor
proposalPolicySymbol :: CurrencySymbol
proposalPolicySymbol = mintingPolicySymbol $ mkMintingPolicy (proposalPolicy proposal)
proposalPolicySymbol = proposalSTSymbolFromGovernor governor
-- | A sample 'PubKeyHash'.
signer :: PubKeyHash
@ -135,7 +165,7 @@ signer2 :: PubKeyHash
signer2 = "8a30896c4fd5e79843e4ca1bd2cdbaa36f8c0bc3be74012141420192"
proposalValidatorHash :: ValidatorHash
proposalValidatorHash = validatorHash (mkValidator $ proposalValidator proposal)
proposalValidatorHash = proposalValidatorHashFromGovernor governor
proposalValidatorAddress :: Address
proposalValidatorAddress = scriptHashAddress proposalValidatorHash
@ -148,6 +178,12 @@ defaultProposalThresholds =
, startVoting = Tagged 10
}
authorityToken :: AuthorityToken
authorityToken = authorityTokenFromGovernor governor
authorityTokenSymbol :: CurrencySymbol
authorityTokenSymbol = authorityTokenSymbolFromGovernor governor
------------------------------------------------------------------
treasuryOut :: TxOut

View file

@ -7,6 +7,7 @@ This module tests primarily the happy path for Stake creation
-}
module Sample.Stake (
stake,
stakeAssetClass,
stakeSymbol,
validatorHashTN,
signer,
@ -60,7 +61,7 @@ validatorHashTN = let ValidatorHash vh = validatorHash (mkValidator $ stakeValid
-- | This script context should be a valid transaction.
stakeCreation :: ScriptContext
stakeCreation =
let st = Value.singleton stakeSymbol validatorHashTN 1 -- Stake ST
let st = Value.assetClassValue stakeAssetClass 1 -- Stake ST
datum :: Datum
datum = Datum (toBuiltinData $ StakeDatum 424242424242 signer [])
in ScriptContext
@ -120,7 +121,7 @@ data DepositWithdrawExample = DepositWithdrawExample
-- | Create a ScriptContext that deposits or withdraws, given the config for it.
stakeDepositWithdraw :: DepositWithdrawExample -> ScriptContext
stakeDepositWithdraw config =
let st = Value.singleton stakeSymbol validatorHashTN 1 -- Stake ST
let st = Value.assetClassValue stakeAssetClass 1 -- Stake ST
stakeBefore :: StakeDatum
stakeBefore = StakeDatum config.startAmount signer []

View file

@ -8,6 +8,7 @@ import Test.Tasty (defaultMain, testGroup)
import Spec.AuthorityToken qualified as AuthorityToken
import Spec.Effect.TreasuryWithdrawal qualified as TreasuryWithdrawal
import Spec.Governor qualified as Governor
import Spec.Model.MultiSig qualified as MultiSig
import Spec.Proposal qualified as Proposal
import Spec.Stake qualified as Stake
@ -42,7 +43,10 @@ main =
"AuthorityToken tests"
AuthorityToken.tests
, testGroup
"Utility functions tests"
"Governor tests"
Governor.tests
, testGroup
"Utility tests"
Utils.tests
, testGroup
"Multisig tests"

View file

@ -0,0 +1,57 @@
{- |
Module : Spec.Governor
Maintainer : connor@mlabs.city
Description: Tests for Agora governor.
Thie module exports `tests`, a list of `TestTree`s, which ensure
that Agora's governor component workds as intended.
Tests should pass when the validator or policy is given one of the
valid script contexts, which are defined in 'Agora.Sample.Governor'.
TODO: Add negative test cases, see [#76](https://github.com/Liqwid-Labs/agora/issues/76).
-}
module Spec.Governor (tests) where
import Agora.Governor (GovernorDatum (..), GovernorRedeemer (..))
import Agora.Governor.Scripts (governorPolicy, governorValidator)
import Agora.Proposal (ProposalId (..))
import Sample.Governor (createProposal, mintGATs, mintGST, mutateState)
import Sample.Shared qualified as Shared
import Test.Tasty (TestTree, testGroup)
import Test.Util (policySucceedsWith, validatorSucceedsWith)
--------------------------------------------------------------------------------
tests :: [TestTree]
tests =
[ testGroup
"policy"
[ policySucceedsWith
"GST minting"
(governorPolicy Shared.governor)
()
mintGST
]
, testGroup
"validator"
[ validatorSucceedsWith
"proposal creation"
(governorValidator Shared.governor)
(GovernorDatum Shared.defaultProposalThresholds (ProposalId 0))
CreateProposal
createProposal
, validatorSucceedsWith
"GATs minting"
(governorValidator Shared.governor)
(GovernorDatum Shared.defaultProposalThresholds (ProposalId 5))
MintGATs
mintGATs
, validatorSucceedsWith
"mutate governor state"
(governorValidator Shared.governor)
(GovernorDatum Shared.defaultProposalThresholds (ProposalId 5))
MutateGovernor
mutateState
]
]

View file

@ -12,6 +12,7 @@ module Spec.Proposal (tests) where
--------------------------------------------------------------------------------
import Agora.Proposal (
Proposal (..),
ProposalDatum (ProposalDatum),
ProposalId (ProposalId),
ProposalRedeemer (Cosign),
@ -49,7 +50,7 @@ tests =
"policy"
[ policySucceedsWith
"proposalCreation"
(proposalPolicy Shared.proposal)
(proposalPolicy Shared.proposal.governorSTAssetClass)
()
Proposal.proposalCreation
]
@ -64,8 +65,8 @@ tests =
{ proposalId = ProposalId 0
, effects =
AssocMap.fromList
[ (ResultTag 0, [])
, (ResultTag 1, [])
[ (ResultTag 0, AssocMap.empty)
, (ResultTag 1, AssocMap.empty)
]
, status = Draft
, cosigners = [signer]
@ -73,8 +74,8 @@ tests =
, votes =
emptyVotesFor $
AssocMap.fromList
[ (ResultTag 0, [])
, (ResultTag 1, [])
[ (ResultTag 0, AssocMap.empty)
, (ResultTag 1, AssocMap.empty)
]
}
)

View file

@ -7,7 +7,138 @@ Tests for utility functions in 'Agora.Utils'.
-}
module Spec.Utils (tests) where
--------------------------------------------------------------------------------
import Agora.Utils (phalve, pisUniq, pmergeBy, pmsort, pnubSort)
--------------------------------------------------------------------------------
import Data.List (nub, sort)
import Data.Set as S
--------------------------------------------------------------------------------
import Test.Tasty (TestTree)
import Test.Tasty.QuickCheck (testProperty)
--------------------------------------------------------------------------------
tests :: [TestTree]
tests = []
tests =
[ testProperty "'pmsort' sorts a list properly" prop_msortCorrect
, testProperty "'pmerge' merges two sorted lists into one sorted list" prop_mergeCorrect
, testProperty "'phalve' splits a list in half as expected" prop_halveCorrect
, testProperty "'pnubSort' sorts a list and remove duplicate elements" prop_nubSortProperly
, testProperty "'pisUniq' can tell whether all elements in a list are unique" prop_uniqueList
]
--------------------------------------------------------------------------------
-- | Yield true if 'Agora.Utils.pmsort' sorts a given list correctly.
prop_msortCorrect :: [Integer] -> Bool
prop_msortCorrect l = sorted == expected
where
-- Expected sorted list, using 'Data.List.sort'.
expected :: [Integer]
expected = sort l
--
psorted :: Term _ (PBuiltinList PInteger)
psorted = pmsort # pconstant l
sorted :: [Integer]
sorted = plift psorted
-- | Yield true if 'Agora.Utils.pmerge' merges two list into a ordered list correctly.
prop_mergeCorrect :: [Integer] -> [Integer] -> Bool
prop_mergeCorrect a b = merged == expected
where
-- Sorted list a and b
sa = sort a
sb = sort b
-- Merge two lists which are assumed to be ordered.
merge :: [Integer] -> [Integer] -> [Integer]
merge xs [] = xs
merge [] ys = ys
merge sx@(x : xs) sy@(y : ys)
| x <= y = x : merge xs sy
| otherwise = y : merge sx ys
expected :: [Integer]
expected = merge sa sb
--
pmerged :: Term _ (PBuiltinList PInteger)
pmerged = pmergeBy # plam (#<) # pconstant sa # pconstant sb
merged :: [Integer]
merged = plift pmerged
{- | Yield true if Plutarch level 'Agora.Utils.phalve' splits a given list
as its Haskell level counterpart does.
-}
prop_halveCorrect :: [Integer] -> Bool
prop_halveCorrect l = halved == expected
where
-- Halve a list.
halve :: [Integer] -> ([Integer], [Integer])
halve xs = go xs xs
where
go xs [] = ([], xs)
go (x : xs) [_] = ([x], xs)
go (x : xs) (_ : _ : ys) =
let (first, last) =
go xs ys
in (x : first, last)
go [] _ = ([], [])
expected :: ([Integer], [Integer])
expected = halve l
--
phalved :: Term _ (PPair (PBuiltinList PInteger) (PBuiltinList PInteger))
phalved = phalve # pconstant l
halved :: ([Integer], [Integer])
halved =
let f = plift $ pmatch phalved $ \(PPair x _) -> x
s = plift $ pmatch phalved $ \(PPair _ x) -> x
in (f, s)
{- | Yield true if 'Agora.Utils.pnubSort' sorts and removes
duplicate elements from a given list.
-}
prop_nubSortProperly :: [Integer] -> Bool
prop_nubSortProperly l = nubbed == expected
where
-- Sort and list and then nub it.
expected :: [Integer]
expected = nub $ sort l
--
pnubbed :: Term _ (PBuiltinList PInteger)
pnubbed = pnubSort # pconstant l
nubbed :: [Integer]
nubbed = plift pnubbed
{- | Yield true if 'Agora.Utils.isUnique' can correctly determine
whether a given list only contains unique elements or not.
-}
prop_uniqueList :: [Integer] -> Bool
prop_uniqueList l = isUnique == expected
where
-- Convert input list to a set.
-- If the set's size equals to list's size,
-- the list only contains unique elements.
expected :: Bool
expected = S.size (S.fromList l) == length l
--
isUnique = plift $ pisUniq # pconstant l

View file

@ -119,6 +119,7 @@ common test-deps
, tasty
, tasty-hedgehog
, tasty-hunit
, tasty-quickcheck
common exe-opts
ghc-options: -threaded -rtsopts -with-rtsopts=-N -O0
@ -131,6 +132,7 @@ library
Agora.Effect.NoOp
Agora.Effect.TreasuryWithdrawal
Agora.Governor
Agora.Governor.Scripts
Agora.MultiSig
Agora.Proposal
Agora.Proposal.Scripts
@ -162,12 +164,10 @@ library agora-testlib
library agora-sample
import: lang, deps, test-deps
build-depends:
, agora
, agora-testlib
build-depends: agora-testlib
exposed-modules:
Sample.Effect.TreasuryWithdrawal
Sample.Governor
Sample.Proposal
Sample.Shared
Sample.Stake
@ -183,6 +183,7 @@ test-suite agora-test
other-modules:
Spec.AuthorityToken
Spec.Effect.TreasuryWithdrawal
Spec.Governor
Spec.Model.MultiSig
Spec.Proposal
Spec.Stake
@ -190,7 +191,6 @@ test-suite agora-test
Spec.Utils
build-depends:
, agora
, agora-sample
, agora-testlib

View file

@ -17,6 +17,7 @@ import Plutarch.Api.V1 (
PAddress (..),
PCredential (..),
PCurrencySymbol (..),
PMintingPolicy,
PScriptContext (..),
PScriptPurpose (..),
PTxInInfo (PTxInInfo),
@ -127,9 +128,7 @@ singleAuthorityTokenBurned gatCs txInfo mint = unTermCont $ do
]
-- | Policy given 'AuthorityToken' params.
authorityTokenPolicy ::
AuthorityToken ->
Term s (PData :--> PScriptContext :--> PUnit)
authorityTokenPolicy :: AuthorityToken -> ClosedTerm PMintingPolicy
authorityTokenPolicy params =
plam $ \_redeemer ctx' ->
pmatch ctx' $ \(PScriptContext ctx') -> unTermCont $ do
@ -156,7 +155,6 @@ authorityTokenPolicy params =
authorityTokensValidIn
# ownSymbol
# txOut
pure $ pconstant ()
pure $ popaque $ pconstant ()
)
(pconstant ())
(popaque $ pconstant ())

View file

@ -2,7 +2,7 @@
{- |
Module : Agora.Governor
Maintainer : emi@haskell.fyi
Maintainer : connor@mlabs.city
Description: Governor entity scripts acting as authority of entire system.
Governor entity scripts acting as authority of entire system.
@ -14,17 +14,52 @@ module Agora.Governor (
Governor (..),
-- * Plutarch-land
PGovernorDatum (..),
PGovernorRedeemer (..),
-- * Scripts
governorPolicy,
governorValidator,
-- * Utilities
pgetNextProposalId,
getNextProposalId,
governorDatumValid,
) where
import Agora.Proposal (ProposalId, ProposalThresholds)
--------------------------------------------------------------------------------
import Control.Applicative (Const)
import GHC.Generics qualified as GHC
import Plutarch.Api.V1 (PMintingPolicy, PValidator)
import Generics.SOP (Generic, I (I))
--------------------------------------------------------------------------------
import Agora.Proposal (
PProposalId (..),
PProposalThresholds (..),
ProposalId (ProposalId),
ProposalThresholds,
)
import Agora.SafeMoney (GTTag)
import Agora.Utils (tclet)
--------------------------------------------------------------------------------
import Plutarch.DataRepr (
DerivePConstantViaData (..),
PDataFields,
PIsDataReprInstances (PIsDataReprInstances),
)
import Plutarch.Lift (PConstantDecl, PUnsafeLiftDecl (..))
import Plutarch.SafeMoney (Tagged (..), puntag)
import Plutarch.TryFrom (PTryFrom (..))
import Plutarch.Unsafe (punsafeCoerce)
--------------------------------------------------------------------------------
import Plutus.V1.Ledger.Api (TxOutRef)
import Plutus.V1.Ledger.Value (AssetClass (..))
import PlutusTx qualified
--------------------------------------------------------------------------------
-- | Datum for the Governor script.
data GovernorDatum = GovernorDatum
{ proposalThresholds :: ProposalThresholds
@ -32,7 +67,7 @@ data GovernorDatum = GovernorDatum
, nextProposalId :: ProposalId
-- ^ What tag the next proposal will get upon creating.
}
deriving stock (GHC.Generic)
deriving stock (Show, GHC.Generic)
PlutusTx.makeIsDataIndexed ''GovernorDatum [('GovernorDatum, 0)]
@ -41,6 +76,8 @@ PlutusTx.makeIsDataIndexed ''GovernorDatum [('GovernorDatum, 0)]
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.
@ -48,23 +85,106 @@ data GovernorRedeemer
| -- | Checks that a SINGLE proposal finished correctly,
-- and allows minting GATs for each effect script.
MintGATs
deriving stock (GHC.Generic)
| -- | Allows effects to mutate the parameters.
MutateGovernor
deriving stock (Show, GHC.Generic)
PlutusTx.makeIsDataIndexed ''GovernorRedeemer [('CreateProposal, 0), ('MintGATs, 1)]
PlutusTx.makeIsDataIndexed
''GovernorRedeemer
[ ('CreateProposal, 0)
, ('MintGATs, 1)
, ('MutateGovernor, 2)
]
-- | Parameters for creating Governor scripts.
data Governor
= Governor
data Governor = Governor
{ gstOutRef :: TxOutRef
-- ^ Referenced utxo will be spent to mint the GST.
, gtClassRef :: Tagged GTTag AssetClass
-- ^ Governance token of the system.
, maximumCosigners :: Integer
-- ^ Arbitrary limit for maximum amount of cosigners on a proposal.
-- See `Agora.Proposal.proposalDatumValid`.
}
deriving stock (GHC.Generic)
--------------------------------------------------------------------------------
-- | Policy for Governors.
governorPolicy :: Governor -> ClosedTerm PMintingPolicy
governorPolicy _ =
plam $ \_redeemer _ctx' -> popaque (pconstant ())
-- | 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, PEq)
via PIsDataReprInstances PGovernorDatum
-- | Validator for Governors.
governorValidator :: Governor -> ClosedTerm PValidator
governorValidator _ =
plam $ \_datum _redeemer _ctx' -> popaque (pconstant ())
instance PUnsafeLiftDecl PGovernorDatum where type PLifted PGovernorDatum = GovernorDatum
deriving via (DerivePConstantViaData GovernorDatum PGovernorDatum) instance (PConstantDecl GovernorDatum)
-- FIXME: derive this via 'PIsDataReprInstances'
-- Blocked by: PProposalThresholds
instance PTryFrom PData (PAsData PGovernorDatum) where
type PTryFromExcess PData (PAsData PGovernorDatum) = Const ()
ptryFrom' d k = k (punsafeCoerce d, ())
-- | 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 (PConstantDecl GovernorRedeemer)
deriving via PAsData (PIsDataReprInstances PGovernorRedeemer) instance PTryFrom PData (PAsData PGovernorRedeemer)
--------------------------------------------------------------------------------
-- | Plutrach version of 'getNextProposalId'.
pgetNextProposalId :: Term s (PProposalId :--> PProposalId)
pgetNextProposalId = phoistAcyclic $ plam $ \(pto -> pid) -> pcon $ PProposalId $ pid + 1
-- | Get next proposal id.
getNextProposalId :: ProposalId -> ProposalId
getNextProposalId (ProposalId pid) = ProposalId $ pid + 1
--------------------------------------------------------------------------------
governorDatumValid :: Term s (PGovernorDatum :--> PBool)
governorDatumValid = phoistAcyclic $
plam $ \datum -> unTermCont $ do
thresholds <-
tcont $
pletFields @'["execute", "draft", "vote"] $
pfield @"proposalThresholds" # datum
execute <- tclet $ puntag thresholds.execute
draft <- tclet $ puntag thresholds.draft
vote <- tclet $ puntag thresholds.vote
pure $
foldr1
(#&&)
[ ptraceIfFalse "Execute threshold is less than or equal to" $ 0 #<= execute
, ptraceIfFalse "Draft threshold is less than or equal to " $ 0 #<= draft
, ptraceIfFalse "Vote threshold is less than or equal to " $ 0 #<= vote
, ptraceIfFalse "Draft threshold is less than vote threshold" $ draft #<= vote
, ptraceIfFalse "Execute threshold is less than vote threshold" $ vote #< execute
]

View file

@ -0,0 +1,822 @@
{- |
Module : Agora.Governor.Scripts
Maintainer : connor@mlabs.city
Description: Plutus scripts for Governors.
Plutus scripts for Governors.
-}
module Agora.Governor.Scripts (
-- * GST
-- $gst
-- * Scripts
governorPolicy,
governorValidator,
-- * Bridges
governorSTSymbolFromGovernor,
governorSTAssetClassFromGovernor,
proposalSTAssetClassFromGovernor,
stakeSTSymbolFromGovernor,
stakeFromGovernor,
stakeValidatorHashFromGovernor,
proposalFromGovernor,
proposalValidatorHashFromGovernor,
proposalSTSymbolFromGovernor,
stakeSTAssetClassFromGovernor,
governorValidatorHash,
authorityTokenFromGovernor,
authorityTokenSymbolFromGovernor,
) where
--------------------------------------------------------------------------------
import Agora.AuthorityToken (
AuthorityToken (..),
authorityTokenPolicy,
authorityTokensValidIn,
singleAuthorityTokenBurned,
)
import Agora.Governor (
Governor (gstOutRef, gtClassRef, maximumCosigners),
PGovernorDatum (PGovernorDatum),
PGovernorRedeemer (PCreateProposal, PMintGATs, PMutateGovernor),
governorDatumValid,
pgetNextProposalId,
)
import Agora.Proposal (
PProposalDatum (..),
PProposalId (..),
PProposalStatus (PFinished),
PResultTag,
Proposal (..),
ProposalStatus (Draft, Locked),
proposalDatumValid,
)
import Agora.Proposal.Scripts (
proposalPolicy,
proposalValidator,
)
import Agora.Record
import Agora.SafeMoney (GTTag)
import Agora.Stake (
PProposalLock (..),
PStakeDatum (..),
Stake (..),
)
import Agora.Stake.Scripts (
stakePolicy,
stakeValidator,
)
import Agora.Utils (
findOutputsToAddress,
hasOnlyOneTokenOfCurrencySymbol,
mustBePDJust,
mustBePJust,
mustFindDatum',
pfindTxInByTxOutRef,
pisDJust,
pisJust,
pisUTXOSpent,
psymbolValueOf,
ptryFindDatum,
ptxSignedBy,
pvalueSpent,
scriptHashFromAddress,
tcassert,
tclet,
tcmatch,
validatorHashToAddress,
validatorHashToTokenName,
)
--------------------------------------------------------------------------------
import Plutarch.Api.V1 (
PAddress,
PCurrencySymbol,
PDatumHash,
PMap,
PMintingPolicy,
PScriptPurpose (PMinting, PSpending),
PTxOut,
PValidator,
PValidatorHash,
PValue,
mintingPolicySymbol,
mkMintingPolicy,
mkValidator,
validatorHash,
)
import Plutarch.Api.V1.Extra (
passetClass,
passetClassValueOf,
)
import Plutarch.Map.Extra (
pkeys,
plookup,
plookup',
)
import Plutarch.SafeMoney (
PDiscrete,
puntag,
pvalueDiscrete',
)
import Plutarch.TryFrom (ptryFrom)
--------------------------------------------------------------------------------
import Plutus.V1.Ledger.Api (
CurrencySymbol (..),
MintingPolicy,
)
import Plutus.V1.Ledger.Scripts (ValidatorHash (..))
import Plutus.V1.Ledger.Value (
AssetClass (..),
)
--------------------------------------------------------------------------------
{- $gst
Governance state token, aka. GST, is an NFT that identifies a UTXO that
carries the state datum of the Governance script.
This token is minted by a one-shot monetary policy 'governorPolicy',
meaning that the token has guaranteed uniqueness.
The 'governorValidator' ensures that exactly one GST stays
at the address of itself forever.
-}
--------------------------------------------------------------------------------
{- | Policy for minting GSTs.
This policy perform the following checks:
- The UTXO referenced in the parameter is spent in the transaction.
- Exactly one GST is minted.
- Ensure the token name is empty.
- Said UTXO should carry a valid 'Agora.Governor.GovernorDatum'.
NOTE: It's user's responsibility to make sure the token is sent to the corresponding governor validator.
We /can't/ really check this in the policy, otherwise we create a cyclic reference issue.
-}
governorPolicy :: Governor -> ClosedTerm PMintingPolicy
governorPolicy gov =
plam $ \_ ctx' -> unTermCont $ do
let oref = pconstant gov.gstOutRef
PMinting ((pfield @"_0" #) -> ownSymbol) <- tcmatch (pfromData $ pfield @"purpose" # ctx')
let ownAssetClass = passetClass # ownSymbol # pconstant ""
txInfo = pfromData $ pfield @"txInfo" # ctx'
txInfoF <- tcont $ pletFields @'["mint", "inputs", "outputs", "datums"] txInfo
tcassert "Referenced utxo should be spent" $
pisUTXOSpent # oref # txInfoF.inputs
tcassert "Exactly one token should be minted" $
psymbolValueOf # ownSymbol # txInfoF.mint #== 1
#&& passetClassValueOf # txInfoF.mint # ownAssetClass #== 1
govOutput <-
tclet $
mustBePJust
# "Governor output not found"
#$ pfind
# plam
( \((pfield @"value" #) . pfromData -> value) ->
psymbolValueOf # ownSymbol # value #== 1
)
# pfromData txInfoF.outputs
let datumHash = pfield @"datumHash" # pfromData govOutput
datum = mustFindDatum' @PGovernorDatum # datumHash # txInfoF.datums
pure $ popaque $ governorDatumValid # datum
{- | Validator for Governors.
== Common checks
The validator always ensures:
- The UTXO which holds the GST must be spent.
- The GST always stays at the validator's address.
- The new state UTXO has a valid datum of type 'GovernorDatum'.
== Creating a Proposal
When the redeemer is 'CreateProposal', the script will check:
- For governor's state datum:
* 'nextProposalId' is advanced.
* Nothing is changed other that that.
- Exactly one stake (the "input stake") must be provided in the input:
* At least 'Agora.Stake.stackedAmount' of GT must be spent in the transaction.
* The transaction must be signed by the stake owner.
- Exactly one new proposal state token is minted.
- An UTXO which holds the newly minted proposal state token is sent to the proposal validator.
This UTXO must have a valid datum of type 'Agora.Proposal.ProposalDatum', the datum must:
* Copy its id and thresholds from the governor's state.
* Have status set to 'Proposal.Draft'.
* Have zero votes.
* Have exactly one cosigner - the stake owner
- An UTXO which holds the stake state token is sent back to the stake validator.
This UTXO must have a valid datum of type 'Agora.Stake.StakeDatum':
* The 'Agora.Stake.stakedAmount' and 'Agora.Stake.owner' should not be changed,
comparing to the input stake.
* The new proposal locks must be appended to the 'Agora.Stake.lockedBy'.
== Minting GATs
When the redeemer is 'MintGATs', the script will check:
- Governor's state is not changed.
- Exactly only one proposal is in the inputs. Let's call this the /input proposal/.
- The proposal is in the 'Proposal.Executable' state.
NOTE: The input proposal is found by looking for the UTXO with a proposal state token in the inputs.
=== Effect Group Selection
Currently a proposal can have two or more than two options to vote on,
meaning that it can contains two or more effect groups,
according to [#39](https://github.com/Liqwid-Labs/agora/issues/39).
Either way, the shapes of 'Proposal.votes' and 'Proposal.effects' should be the same.
This is checked by 'Proposal.proposalDatumValid'.
The script will look at the the 'Proposal.votes' to determine which group has the highest votes,
said group shoud be executed.
During the process, minimum votes requirement will also be enforced.
Next, the script will:
- Ensure that for every effect in the said effect group,
exactly one valid GAT is minted and sent to the effect.
- The amount of GAT minted in the transaction should be equal to the number of effects.
- A new UTXO is sent to the proposal validator, this UTXO should:
* Include the one proposal state token.
* Have a valid datum of type 'Proposal.ProposalDatum'.
This datum should be as same as the one of the input proposal,
except its status should be 'Proposal.Finished'.
== Changing the State
Redeemer 'MutateGovernor' allows the state datum to be changed by an external effect.
In this case, the script will check
- Exactly one GAT is burnt in the transaction.
- Said GAT is tagged by the effect.
-}
governorValidator :: Governor -> ClosedTerm PValidator
governorValidator gov =
plam $ \datum' redeemer' ctx' -> unTermCont $ do
(pfromData -> redeemer, _) <- tcont $ ptryFrom redeemer'
ctxF <- tcont $ pletFields @'["txInfo", "purpose"] ctx'
txInfo' <- tclet $ pfromData $ ctxF.txInfo
txInfoF <- tcont $ pletFields @'["mint", "inputs", "outputs", "datums", "signatories"] txInfo'
PSpending (pfromData . (pfield @"_0" #) -> ownInputRef) <- tcmatch $ pfromData ctxF.purpose
((pfield @"resolved" #) -> ownInput) <-
tclet $
mustBePJust # "Own input not found"
#$ pfindTxInByTxOutRef # ownInputRef # txInfoF.inputs
ownInputF <- tcont $ pletFields @'["address", "value"] ownInput
let ownAddress = pfromData $ ownInputF.address
(pfromData -> (oldGovernorDatum :: Term _ PGovernorDatum), _) <- tcont $ ptryFrom datum'
oldGovernorDatumF <- tcont $ pletFields @'["proposalThresholds", "nextProposalId"] oldGovernorDatum
-- Check that GST will be returned to the governor.
let ownInputGSTAmount = psymbolValueOf # pgstSymbol # ownInputF.value
tcassert "Own input should have exactly one state token" $
ownInputGSTAmount #== 1
ownOutputs <- tclet $ findOutputsToAddress # txInfoF.outputs # ownAddress
tcassert "Exactly one utxo should be sent to the governor" $
plength # ownOutputs #== 1
ownOutput <- tcont $ pletFields @'["value", "datumHash"] $ phead # ownOutputs
let ownOuputGSTAmount = psymbolValueOf # pgstSymbol # ownOutput.value
tcassert "State token should stay at governor's address" $
ownOuputGSTAmount #== 1
-- Check that own output have datum of type 'GovernorDatum'.
let outputGovernorStateDatumHash =
mustBePDJust # "Governor output doesn't have datum" # ownOutput.datumHash
newGovernorDatum <-
tclet $
pfromData $
mustBePJust # "Ouput governor state datum not found"
#$ ptryFindDatum # outputGovernorStateDatumHash # txInfoF.datums
tcassert "New datum is not valid" $ governorDatumValid # newGovernorDatum
pure $
pmatch redeemer $ \case
PCreateProposal _ -> unTermCont $ do
-- Check that the transaction advances proposal id.
let expectedNextProposalId = pgetNextProposalId # oldGovernorDatumF.nextProposalId
expectedNewDatum =
mkRecordConstr
PGovernorDatum
( #proposalThresholds .= oldGovernorDatumF.proposalThresholds
.& #nextProposalId .= pdata expectedNextProposalId
)
tcassert "Unexpected governor state datum" $
newGovernorDatum #== expectedNewDatum
-- Check that exactly one proposal token is being minted.
tcassert "Exactly one proposal token must be minted" $
hasOnlyOneTokenOfCurrencySymbol # ppstSymbol # txInfoF.mint
-- Check that a stake is spent to create the propsal,
-- and the value it contains meets the requirement.
stakeInput <-
tclet $
mustBePJust # "Stake input not found" #$ pfind
# phoistAcyclic
( plam $
\((pfield @"resolved" #) -> txOut') -> unTermCont $ do
txOut <- tcont $ pletFields @'["address", "value"] txOut'
pure $
txOut.address #== pdata pstakeValidatorAddress
#&& psymbolValueOf # psstSymbol # txOut.value #== 1
)
# pfromData txInfoF.inputs
stakeInputF <- tcont $ pletFields @'["datumHash", "value"] $ pfield @"resolved" # stakeInput
tcassert "Stake input doesn't have datum" $
pisDJust # stakeInputF.datumHash
let stakeInputDatum = mustFindDatum' @PStakeDatum # stakeInputF.datumHash # txInfoF.datums
stakeInputDatumF <-
tcont $ pletFields @["stakedAmount", "owner", "lockedBy"] stakeInputDatum
tcassert "Required amount of stake GTs should be presented" $
stakeInputDatumF.stakedAmount #== (pgtValueOf # stakeInputF.value)
-- TODO: Is this required?
tcassert "Tx should be signed by the stake owner" $
ptxSignedBy # txInfoF.signatories # stakeInputDatumF.owner
-- Check that the newly minted PST is sent to the proposal validator,
-- and the datum it carries is legal.
outputsToProposalValidatorWithStateToken <-
tclet $
pfilter
# phoistAcyclic
( plam $
\txOut' -> unTermCont $ do
txOut <- tcont $ pletFields @'["address", "value"] txOut'
pure $
txOut.address #== pdata pproposalValidatorAddress
#&& psymbolValueOf # ppstSymbol # txOut.value #== 1
)
# pfromData txInfoF.outputs
tcassert "Exactly one UTXO with proposal state token should be sent to the proposal validator" $
plength # outputsToProposalValidatorWithStateToken #== 1
outputDatumHash <- tclet $ pfield @"datumHash" #$ phead # outputsToProposalValidatorWithStateToken
tcassert "The utxo paid to the proposal validator must have datum" $
pisDJust # outputDatumHash
proposalOutputDatum' <-
tclet $
mustFindDatum' @PProposalDatum
# outputDatumHash
# txInfoF.datums
tcassert "Proposal datum must be valid" $
proposalDatumValid' # proposalOutputDatum'
proposalOutputDatum <-
tcont $
pletFields
@'["proposalId", "status", "cosigners", "thresholds", "votes"]
proposalOutputDatum'
-- Id and thresholds should be copied from the old governor state datum.
tcassert "Invalid proposal id in proposal datum" $
proposalOutputDatum.proposalId #== oldGovernorDatumF.nextProposalId
tcassert "Invalid thresholds in proposal datum" $
proposalOutputDatum.thresholds #== oldGovernorDatumF.proposalThresholds
-- The proposal at this point should be in draft state.
tcassert "Proposal state should be draft" $
proposalOutputDatum.status #== pconstantData Draft
tcassert "Proposal should have only one cosigner" $
plength # pfromData proposalOutputDatum.cosigners #== 1
let cosigner = phead # pfromData proposalOutputDatum.cosigners
tcassert "Cosigner should be the stake owner" $
pdata stakeInputDatumF.owner #== cosigner
-- Check the output stake has been proposly updated.
stakeOutput <-
tclet $
mustBePJust
# "Stake output not found"
#$ pfind
# phoistAcyclic
( plam $
\txOut' -> unTermCont $ do
txOut <- tcont $ pletFields @'["address", "value"] txOut'
pure $
txOut.address #== pdata pstakeValidatorAddress
#&& psymbolValueOf # psstSymbol # txOut.value #== 1
)
# pfromData txInfoF.outputs
stakeOutputF <- tcont $ pletFields @'["datumHash", "value"] $ stakeOutput
tcassert "Staked GTs should be sent back to stake validator" $
stakeInputDatumF.stakedAmount #== (pgtValueOf # stakeOutputF.value)
let stakeOutputDatumHash = mustBePDJust # "Stake output should have datum" # stakeOutputF.datumHash
stakeOutputDatum =
mustBePJust # "Stake output not found" #$ ptryFindDatum # stakeOutputDatumHash # txInfoF.datums
-- The stake should be locked by the newly created proposal.
let possibleVoteResults = pkeys #$ pto $ pfromData proposalOutputDatum.votes
mkProposalLock :: Term _ (PProposalId :--> PAsData PResultTag :--> PAsData PProposalLock)
mkProposalLock =
phoistAcyclic $
plam
( \pid rt' ->
pdata $
mkRecordConstr
PProposalLock
( #vote .= rt' .& #proposalTag .= pdata pid
)
)
-- Append new locks to existing locks
expectedProposalLocks =
pconcat # stakeInputDatumF.lockedBy
#$ pmap # (mkProposalLock # proposalOutputDatum.proposalId) # possibleVoteResults
expectedStakeOutputDatum =
pdata $
mkRecordConstr
PStakeDatum
( #stakedAmount .= stakeInputDatumF.stakedAmount
.& #owner .= stakeInputDatumF.owner
.& #lockedBy .= pdata expectedProposalLocks
)
tcassert "Unexpected stake output datum" $ expectedStakeOutputDatum #== stakeOutputDatum
pure $ popaque $ pconstant ()
--------------------------------------------------------------------------
PMintGATs _ -> unTermCont $ do
tcassert "Governor state should not be changed" $ newGovernorDatum #== oldGovernorDatum
-- Filter out proposal inputs and ouputs using PST and the address of proposal validator.
tcassert "The governor can only process one proposal at a time" $
(psymbolValueOf # ppstSymbol #$ pvalueSpent # txInfoF.inputs) #== 1
proposalInputF <-
tcont $
pletFields @'["datumHash"] $
pfield @"resolved"
#$ pfromData
$ mustBePJust
# "Proposal input not found"
#$ pfind
# plam
( \((pfield @"resolved" #) -> txOut) -> unTermCont $ do
txOutF <- tcont $ pletFields @'["address", "value"] txOut
pure $
psymbolValueOf # ppstSymbol # txOutF.value #== 1
#&& txOutF.address #== pdata pproposalValidatorAddress
)
# pfromData txInfoF.inputs
proposalOutputF <-
tcont $
pletFields @'["datumHash"] $
mustBePJust # "Proposal output not found"
#$ pfind
# plam
( \txOut -> unTermCont $ do
txOutF <- tcont $ pletFields @'["address", "value"] txOut
pure $
psymbolValueOf # ppstSymbol # txOutF.value #== 1
#&& txOutF.address #== pdata pproposalValidatorAddress
)
# pfromData txInfoF.outputs
proposalInputDatum <-
tclet $
mustFindDatum' @PProposalDatum
# proposalInputF.datumHash
# txInfoF.datums
proposalOutputDatum <-
tclet $
mustFindDatum' @PProposalDatum
# proposalOutputF.datumHash
# txInfoF.datums
tcassert "Proposal datum must be valid" $
proposalDatumValid' # proposalInputDatum
#&& proposalDatumValid' # proposalOutputDatum
proposalInputDatumF <-
tcont $
pletFields @'["proposalId", "effects", "status", "cosigners", "thresholds", "votes"]
proposalInputDatum
-- Check that the proposal state is advanced so that a proposal cannot be executed twice.
tcassert "Proposal must be in locked(executable) state in order to execute effects" $
proposalInputDatumF.status #== pconstantData Locked
let expectedOutputProposalDatum =
mkRecordConstr
PProposalDatum
( #proposalId .= proposalInputDatumF.proposalId
.& #effects .= proposalInputDatumF.effects
.& #status .= pdata (pcon $ PFinished pdnil)
.& #cosigners .= proposalInputDatumF.cosigners
.& #thresholds .= proposalInputDatumF.thresholds
.& #votes .= proposalInputDatumF.votes
)
tcassert "Unexpected output proposal datum" $
pdata proposalOutputDatum #== pdata expectedOutputProposalDatum
-- TODO: anything else to check here?
-- Find the highest votes and the corresponding tag.
let highestVoteFolder =
phoistAcyclic $
plam
( \pair last' ->
pif
(pisJust # last')
( unTermCont $ do
PJust last <- tcmatch last'
let lastHighestVote = pfromData $ psndBuiltin # last
thisVote = pfromData $ psndBuiltin # pair
pure $ pif (lastHighestVote #< thisVote) (pcon $ PJust pair) last'
)
(pcon $ PJust pair)
)
votesList = pto $ pto $ pfromData proposalInputDatumF.votes
maybeWinner =
pfoldr # highestVoteFolder # pcon PNothing # votesList
winner <- tclet $ mustBePJust # "No winning outcome" # maybeWinner
let highestVote = pfromData $ psndBuiltin # winner
minimumVotes = puntag $ pfromData $ pfield @"execute" # proposalInputDatumF.thresholds
tcassert "Higgest vote doesn't meet the minimum requirement" $ minimumVotes #<= highestVote
let finalResultTag = pfromData $ pfstBuiltin # winner
-- The effects of the winner outcome.
effectGroup <- tclet $ plookup' # finalResultTag #$ proposalInputDatumF.effects
gatCount <- tclet $ plength #$ pto $ pto effectGroup
tcassert "Required amount of GATs should be minted" $
psymbolValueOf # patSymbol # txInfoF.mint #== gatCount
-- Ensure that every GAT goes to one of the effects in the winner effect group.
outputsWithGAT <-
tclet $
pfilter
# phoistAcyclic
( plam
( \((pfield @"value" #) -> value) ->
0 #< psymbolValueOf # patSymbol # value
)
)
# pfromData txInfoF.outputs
tcassert "Output GATs is more than minted GATs" $
plength # outputsWithGAT #== gatCount
let gatOutputValidator' :: Term s (PMap PValidatorHash PDatumHash :--> PAsData PTxOut :--> PBool)
gatOutputValidator' =
phoistAcyclic $
plam
( \effects (pfromData -> output') -> unTermCont $ do
output <- tcont $ 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 the effect list"
#$ plookup # scriptHash # effects
pure $
foldr1
(#&&)
[ ptraceIfFalse "GAT must be tagged by the effect hash" $ authorityTokensValidIn # patSymbol # output'
, ptraceIfFalse "Unexpected datum" $ datumHash #== expectedDatumHash
]
)
gatOutputValidator = gatOutputValidator' # effectGroup
pure $
popaque $
pfoldr
# plam
( \txOut r ->
let value = pfield @"value" # txOut
atValue = psymbolValueOf # patSymbol # value
in pif (atValue #== 0) r $
pif (atValue #== 1) (r #&& gatOutputValidator # txOut) $ pconstant False
)
# pconstant True
# pfromData txInfoF.outputs
--------------------------------------------------------------------------
PMutateGovernor _ -> unTermCont $ do
-- Check that a GAT is burnt.
pure $ popaque $ singleAuthorityTokenBurned patSymbol ctxF.txInfo txInfoF.mint
where
-- Get th amount of governance tokens in a value.
pgtValueOf :: Term s (PValue :--> PDiscrete GTTag)
pgtValueOf = phoistAcyclic $ pvalueDiscrete' gov.gtClassRef
-- The currency symbol of authority token.
patSymbol :: Term s PCurrencySymbol
patSymbol = phoistAcyclic $ pconstant $ authorityTokenSymbolFromGovernor gov
-- The currency symbol of the proposal state token.
ppstSymbol :: Term s PCurrencySymbol
ppstSymbol =
let AssetClass (sym, _) = proposalSTAssetClassFromGovernor gov
in phoistAcyclic $ pconstant sym
-- Is a proposal state datum valid?
proposalDatumValid' :: Term s (PProposalDatum :--> PBool)
proposalDatumValid' =
let params = proposalFromGovernor gov
in phoistAcyclic $ proposalDatumValid params
-- The address of the proposal validator.
pproposalValidatorAddress :: Term s PAddress
pproposalValidatorAddress =
let vh = proposalValidatorHashFromGovernor gov
in phoistAcyclic $ pconstant $ validatorHashToAddress vh
-- The address of the stake validator.
pstakeValidatorAddress :: Term s PAddress
pstakeValidatorAddress =
let vh = stakeValidatorHashFromGovernor gov
in phoistAcyclic $ pconstant $ validatorHashToAddress vh
-- The currency symbol of the stake state token.
psstSymbol :: Term s PCurrencySymbol
psstSymbol =
let sym = stakeSTSymbolFromGovernor gov
in phoistAcyclic $ pconstant sym
-- The currency symbol of the governor state token.
pgstSymbol :: Term s PCurrencySymbol
pgstSymbol =
let sym = governorSTSymbolFromGovernor gov
in phoistAcyclic $ pconstant sym
--------------------------------------------------------------------------------
-- | Get the 'CurrencySymbol' of GST.
governorSTSymbolFromGovernor :: Governor -> CurrencySymbol
governorSTSymbolFromGovernor gov = mintingPolicySymbol policy
where
policy :: MintingPolicy
policy = mkMintingPolicy $ governorPolicy gov
-- | Get the 'AssetClass' of GST.
governorSTAssetClassFromGovernor :: Governor -> AssetClass
governorSTAssetClassFromGovernor gov = AssetClass (symbol, "")
where
symbol :: CurrencySymbol
symbol = governorSTSymbolFromGovernor gov
-- | Get the 'CurrencySymbol' of the proposal state token.
proposalSTSymbolFromGovernor :: Governor -> CurrencySymbol
proposalSTSymbolFromGovernor gov = symbol
where
gstAC = governorSTAssetClassFromGovernor gov
policy = mkMintingPolicy $ proposalPolicy gstAC
symbol = mintingPolicySymbol policy
-- | Get the 'AssetClass' of the proposal state token.
proposalSTAssetClassFromGovernor :: Governor -> AssetClass
proposalSTAssetClassFromGovernor gov = AssetClass (symbol, "")
where
symbol = proposalSTSymbolFromGovernor gov
-- | Get the 'CurrencySymbol' of the stake token/
stakeSTSymbolFromGovernor :: Governor -> CurrencySymbol
stakeSTSymbolFromGovernor gov = mintingPolicySymbol policy
where
policy = mkMintingPolicy $ stakePolicy gov.gtClassRef
{- | Get the 'AssetClass' of the stake token.
Note that the token is tagged with the hash of the stake validator.
See 'Agora.Stake.Script.stakePolicy'.
-}
stakeSTAssetClassFromGovernor :: Governor -> AssetClass
stakeSTAssetClassFromGovernor gov = AssetClass (symbol, tokenName)
where
symbol = stakeSTSymbolFromGovernor gov
-- Tag with the address where the token is being sent to.
tokenName = validatorHashToTokenName $ stakeValidatorHashFromGovernor gov
-- | Get the 'Stake' parameter, given the 'Governor' parameter.
stakeFromGovernor :: Governor -> Stake
stakeFromGovernor gov =
Stake gov.gtClassRef $
proposalSTAssetClassFromGovernor gov
-- | Get the hash of 'Agora.Stake.Script.stakePolicy'.
stakeValidatorHashFromGovernor :: Governor -> ValidatorHash
stakeValidatorHashFromGovernor gov = validatorHash validator
where
params = stakeFromGovernor gov
validator = mkValidator $ stakeValidator params
-- | Get the 'Proposal' parameter, given the 'Governor' parameter.
proposalFromGovernor :: Governor -> Proposal
proposalFromGovernor gov = Proposal gstAC sstAC mc
where
gstAC = governorSTAssetClassFromGovernor gov
mc = gov.maximumCosigners
sstAC = stakeSTAssetClassFromGovernor gov
-- | Get the hash of 'Agora.Proposal.proposalPolicy'.
proposalValidatorHashFromGovernor :: Governor -> ValidatorHash
proposalValidatorHashFromGovernor gov = validatorHash validator
where
params = proposalFromGovernor gov
validator = mkValidator $ proposalValidator params
-- | Get the hash of 'Agora.Proposal.proposalValidator'.
governorValidatorHash :: Governor -> ValidatorHash
governorValidatorHash gov = validatorHash validator
where
validator = mkValidator $ governorValidator gov
-- | Get the 'AuthorityToken' parameter given the 'Governor' parameter.
authorityTokenFromGovernor :: Governor -> AuthorityToken
authorityTokenFromGovernor gov = AuthorityToken $ governorSTAssetClassFromGovernor gov
-- | Get the 'CurrencySymbol' of the authority token.
authorityTokenSymbolFromGovernor :: Governor -> CurrencySymbol
authorityTokenSymbolFromGovernor gov = mintingPolicySymbol policy
where
policy = mkMintingPolicy $ authorityTokenPolicy params
params = authorityTokenFromGovernor gov

View file

@ -48,7 +48,6 @@ import Agora.SafeMoney (GTTag)
import Agora.Utils (pkeysEqual, pnotNull)
import Control.Applicative (Const)
import Control.Arrow (first)
import Plutarch.Builtin (PBuiltinMap)
import Plutarch.DataRepr (DerivePConstantViaData (..), PDataFields, PIsDataReprInstances (..))
import Plutarch.Lift (
DerivePConstantViaNewtype (..),
@ -174,10 +173,10 @@ emptyVotesFor = ProposalVotes . AssocMap.mapWithKey (const . const 0)
data ProposalDatum = ProposalDatum
{ proposalId :: ProposalId
-- ^ Identification of the proposal.
, -- TODO: could we encode this more efficiently?
-- TODO: could we encode this more efficiently?
-- This is shaped this way for future proofing.
-- See https://github.com/Liqwid-Labs/agora/issues/39
effects :: AssocMap.Map ResultTag [(ValidatorHash, DatumHash)]
, effects :: AssocMap.Map ResultTag (AssocMap.Map ValidatorHash DatumHash)
-- ^ Effect lookup table. First by result, then by effect hash.
, status :: ProposalStatus
-- ^ The status the proposal is in.
@ -414,17 +413,12 @@ proposalDatumValid proposal =
plam $ \datum' -> unTermCont $ do
datum <- tcont $ pletFields @'["effects", "cosigners", "votes"] $ datum'
let effects :: Term _ (PBuiltinMap Agora.Proposal.PResultTag (PBuiltinMap Plutarch.Api.V1.PValidatorHash Plutarch.Api.V1.PDatumHash))
effects =
-- JUSTIFICATION:
-- @datum.effects : PMap PResultTag (PMap PValidatorHash PDatumHash)@
-- @PMap PResultTag (PMap PValidatorHash PDatumHash)@ is equivalent to
-- @PBuiltinMap PResultTag (PBuiltinMap Plutarch.Api.V1.PValidatorHash Plutarch.Api.V1.PDatumHash)@
punsafeCoerce datum.effects
atLeastOneNegativeResult :: Term _ PBool
atLeastOneNegativeResult =
pany # plam (\pair -> pnull #$ pfromData $ psndBuiltin # pair) # effects
let atLeastOneNegativeResult =
pany
# phoistAcyclic
(plam $ \m -> pnull #$ pto $ pfromData $ psndBuiltin # m)
#$ pto
$ pfromData datum.effects
pure $
foldr1

View file

@ -21,7 +21,7 @@ import Agora.Utils (
anyOutput,
findTxOutByTxOutRef,
getMintingPolicySymbol,
pisUniq,
pisUniqBy,
psymbolValueOf,
ptokenSpent,
ptxSignedBy,
@ -59,8 +59,11 @@ import Plutus.V1.Ledger.Value (AssetClass (AssetClass))
- This policy cannot be burned.
-}
proposalPolicy :: Proposal -> ClosedTerm PMintingPolicy
proposalPolicy proposal =
proposalPolicy ::
-- | The assetclass of GST, see 'Agora.Governor.Scripts.governorPolicy'.
AssetClass ->
ClosedTerm PMintingPolicy
proposalPolicy (AssetClass (govCs, govTn)) =
plam $ \_redeemer ctx' -> unTermCont $ do
PScriptContext ctx' <- tcmatch ctx'
ctx <- tcont $ pletFields @'["txInfo", "purpose"] ctx'
@ -70,7 +73,6 @@ proposalPolicy proposal =
let inputs = txInfo.inputs
mintedValue = pfromData txInfo.mint
AssetClass (govCs, govTn) = proposal.governorSTAssetClass
PMinting ownSymbol' <- tcmatch $ pfromData ctx.purpose
let mintedProposalST =
@ -147,9 +149,10 @@ proposalValidator proposal =
ownAddress <- tclet $ txOutF.address
let stCurrencySymbol =
pconstant $ getMintingPolicySymbol (proposalPolicy proposal)
pconstant $ getMintingPolicySymbol (proposalPolicy proposal.governorSTAssetClass)
valueSpent <- tclet $ pvalueSpent # txInfoF.inputs
spentST <- tclet $ psymbolValueOf # stCurrencySymbol #$ valueSpent
let AssetClass (stakeSym, stakeTn) = proposal.stakeSTAssetClass
stakeSTAssetClass <-
tclet $ passetClass # pconstant stakeSym # pconstant stakeTn
@ -168,7 +171,10 @@ proposalValidator proposal =
newSigs <- tclet $ pfield @"newCosigners" # r
tcassert "Cosigners are unique" $
pisUniq # newSigs
pisUniqBy
# phoistAcyclic (plam (#==))
# phoistAcyclic (plam $ \(pfromData -> x) (pfromData -> y) -> x #< y)
# newSigs
tcassert "Signed by all new cosigners" $
pall # signedBy # newSigs

View file

@ -32,8 +32,14 @@ module Agora.Utils (
pisJust,
ptokenSpent,
pkeysEqual,
pnub,
pnubSortBy,
pisUniq,
pisUniqBy,
pisDJust,
pisUTXOSpent,
pmsortBy,
pmsort,
pnubSort,
-- * Functions which should (probably) not be upstreamed
anyOutput,
@ -46,11 +52,20 @@ module Agora.Utils (
validatorHashToTokenName,
pvalidatorHashToTokenName,
getMintingPolicySymbol,
hasOnlyOneTokenOfCurrencySymbol,
mustFindDatum',
mustBePJust,
mustBePDJust,
validatorHashToAddress,
pmergeBy,
phalve,
) where
--------------------------------------------------------------------------------
import Plutus.V1.Ledger.Api (
Address (..),
Credential (..),
CurrencySymbol,
TokenName (..),
ValidatorHash (..),
@ -83,7 +98,7 @@ import Plutarch.Api.V1 (
import Plutarch.Api.V1.AssocMap (PMap (PMap))
import Plutarch.Api.V1.Extra (PAssetClass, passetClassValueOf, pvalueOf)
import Plutarch.Api.V1.Value (PValue (PValue))
import Plutarch.Builtin (ppairDataBuiltin)
import Plutarch.Builtin (pforgetData, ppairDataBuiltin)
import Plutarch.Map.Extra (pkeys)
import Plutarch.Reducible (Reducible (Reduce))
import Plutarch.TryFrom (PTryFrom (PTryFromExcess), ptryFrom)
@ -346,38 +361,159 @@ ptokenSpent =
{- | True if both maps have exactly the same keys.
Using @'#=='@ is not sufficient, because keys returned are not ordered.
-}
pkeysEqual :: forall (s :: S) k a b. Term s (PMap k a :--> PMap k b :--> PBool)
pkeysEqual :: (POrd k, PIsData k) => forall (s :: S) a b. Term s (PMap k a :--> PMap k b :--> PBool)
pkeysEqual = phoistAcyclic $
plam $ \p q -> unTermCont $ do
pks <- tclet $ pkeys # p
qks <- tclet $ pkeys # q
pure $
pall # plam (\pk -> pelem # pk # qks) # pks
#&& pall # plam (\qk -> pelem # qk # pks) # qks
pif
(plength # pks #== plength # qks)
( unTermCont $ do
let comp = phoistAcyclic $ plam $ \(pfromData -> x) (pfromData -> y) -> x #< y
spks = pmsortBy # comp # pks
sqks = pmsortBy # comp # qks
-- | / O(n^2) /. Clear out duplicates in a list. The order is not preserved.
pnub :: forall list a (s :: S). (PEq a, PIsListLike list a) => Term s (list a :--> list a)
pnub =
phoistAcyclic $
precList
( \self x xs ->
pif
(pnot #$ pelem # x # xs)
(pcons # x # (self # xs))
(self # xs)
)
(const pnil)
pure $ plistEquals # spks # sqks
)
(pcon PFalse)
-- | / O(n^2) /. Check if a list contains no duplicates.
pisUniq :: forall list a (s :: S). (PEq a, PIsListLike list a) => Term s (list a :--> PBool)
pisUniq =
phoistAcyclic $
precList
( \self x xs ->
(pnot #$ pelem # x # xs)
#&& (self # xs)
-- | / O(nlogn) /. Sort and remove dupicate elements in a list.
pnubSortBy ::
forall list a (s :: S).
(PIsListLike list a) =>
Term s ((a :--> a :--> PBool) :--> (a :--> a :--> PBool) :--> list a :--> list a)
pnubSortBy = phoistAcyclic $
plam $ \eq comp l -> pif (pnull # l) l $
unTermCont $ do
sl <- tclet $ pmsortBy # comp # l
let x = phead # sl
xs = ptail # sl
return $ pgo # eq # x # xs
where
pgo = phoistAcyclic pfix #$ plam pgo'
pgo' self eq seen l =
pif (pnull # l) (psingleton # seen) $
unTermCont $ do
x <- tclet $ phead # l
xs <- tclet $ ptail # l
return $
pif
(eq # x # seen)
(self # eq # seen # xs)
(pcons # seen #$ self # eq # x # xs)
-- | Special version of 'pnubSortBy', which requires elements have 'POrd'.
pnubSort ::
forall list a (s :: S).
(PIsListLike list a, POrd a) =>
Term s (list a :--> list a)
pnubSort = phoistAcyclic $ pnubSortBy # eq # comp
where
eq = phoistAcyclic $ plam (#==)
comp = phoistAcyclic $ plam (#<)
-- | / O(nlogn) /. Check if a list contains no duplicates.
pisUniqBy ::
forall list a (s :: S).
(PIsListLike list a) =>
Term s ((a :--> a :--> PBool) :--> (a :--> a :--> PBool) :--> list a :--> PBool)
pisUniqBy = phoistAcyclic $
plam $ \eq comp xs ->
let nubbed = pnubSortBy # eq # comp # xs
in plength # xs #== plength # nubbed
-- | A special case of 'pisUniqBy' which requires elements have 'POrd' instance.
pisUniq :: forall list a (s :: S). (POrd a, PIsListLike list a) => Term s (list a :--> PBool)
pisUniq = phoistAcyclic $ pisUniqBy # eq # comp
where
eq = phoistAcyclic $ plam (#==)
comp = phoistAcyclic $ plam (#<)
-- | Yield True if a given PMaybeData is of form @'PDJust' _@.
pisDJust :: Term s (PMaybeData a :--> PBool)
pisDJust = phoistAcyclic $
plam $ \x ->
pmatch
x
( \case
PDJust _ -> pconstant True
_ -> pconstant False
)
(const $ pcon PTrue)
-- | Determines if a given UTXO is spent.
pisUTXOSpent :: Term s (PTxOutRef :--> PBuiltinList (PAsData PTxInInfo) :--> PBool)
pisUTXOSpent = phoistAcyclic $
plam $ \oref inputs -> P.do
pisJust #$ pfindTxInByTxOutRef # oref # inputs
-- | / O(n) /. Merge two lists which are assumed to be ordered, given a custom comparator.
pmergeBy :: (PIsListLike l a) => Term s ((a :--> a :--> PBool) :--> l a :--> l a :--> l a)
pmergeBy = phoistAcyclic $ pfix #$ plam pmergeBy'
where
pmergeBy' self comp a b =
pif (pnull # a) b $
pif (pnull # b) a $
unTermCont $ do
ah <- tclet $ phead # a
at <- tclet $ ptail # a
bh <- tclet $ phead # b
bt <- tclet $ ptail # b
pure $
pif
(comp # ah # bh)
(pcons # ah #$ self # comp # at # b)
(pcons # bh #$ self # comp # a # bt)
{- | / O(nlogn) /. Merge sort, bottom-up version, given a custom comparator.
Elements are arranged from lowest to highest,
keeping duplicates in the order they appeared in the input.
-}
pmsortBy :: (PIsListLike l a) => Term s ((a :--> a :--> PBool) :--> l a :--> l a)
pmsortBy = phoistAcyclic $ pfix #$ plam pmsortBy'
where
pmsortBy' self comp xs = pif (pnull # xs) pnil $
pif (pnull #$ ptail # xs) xs $
pmatch (phalve # xs) $ \(PPair fh sh) ->
let sfh = self # comp # fh
ssh = self # comp # sh
in pmergeBy # comp # sfh # ssh
-- | A special case of 'pmsortBy' which requires elements have 'POrd' instance.
pmsort :: (POrd a, PIsListLike l a) => Term s (l a :--> l a)
pmsort = phoistAcyclic $ pmsortBy # comp
where
comp = phoistAcyclic $ plam (#<)
-- | Split a list in half.
phalve :: (PIsListLike l a) => Term s (l a :--> PPair (l a) (l a))
phalve = phoistAcyclic $ plam $ \l -> go # l # l
where
go = phoistAcyclic $ pfix #$ plam go'
go' self xs ys =
pif
(pnull # ys)
(pcon $ PPair pnil xs)
( unTermCont $ do
yt <- tclet $ ptail # ys
xh <- tclet $ phead # xs
xt <- tclet $ ptail # xs
pure $
pif (pnull # yt) (pcon $ PPair (psingleton # xh) xt) $
unTermCont $ do
yt' <- tclet $ ptail # yt
pure $
pmatch (self # xt # yt') $ \(PPair first last) ->
pcon $ PPair (pcons # xh # first) last
)
--------------------------------------------------------------------------------
{- Functions which should (probably) not be upstreamed
@ -514,3 +650,49 @@ pvalidatorHashToTokenName vh = pcon (PTokenName (pto vh))
-- | Get the CurrencySymbol of a PMintingPolicy.
getMintingPolicySymbol :: ClosedTerm PMintingPolicy -> CurrencySymbol
getMintingPolicySymbol v = mintingPolicySymbol $ mkMintingPolicy v
-- | The entire value only contains one token of the given currency symbol.
hasOnlyOneTokenOfCurrencySymbol :: Term s (PCurrencySymbol :--> PValue :--> PBool)
hasOnlyOneTokenOfCurrencySymbol = phoistAcyclic $
plam $ \cs vs -> P.do
psymbolValueOf # cs # vs #== 1
#&& (plength #$ pto $ pto $ pto vs) #== 1
-- | Find datum given a maybe datum hash
mustFindDatum' ::
forall (datum :: PType).
(PIsData datum, PTryFrom PData (PAsData datum)) =>
forall s.
Term
s
( PMaybeData PDatumHash
:--> PBuiltinList (PAsData (PTuple PDatumHash PDatum))
:--> datum
)
mustFindDatum' = phoistAcyclic $
plam $ \mdh datums -> unTermCont $ do
let dh = mustBePDJust # "Given TxOut dones't have a datum" # mdh
dt = mustBePJust # "Datum not found in the transaction" #$ plookupTuple # dh # datums
(d, _) <- tcont $ ptryFrom $ pforgetData $ pdata dt
pure $ pfromData d
{- | Extract the value stored in a PMaybe container.
If there's no value, throw an error with the given message.
-}
mustBePJust :: forall a s. Term s (PString :--> PMaybe a :--> a)
mustBePJust = phoistAcyclic $
plam $ \emsg mv' -> pmatch mv' $ \case
PJust v -> v
_ -> ptraceError emsg
{- | Extract the value stored in a PMaybeData container.
If there's no value, throw an error with the given message.
-}
mustBePDJust :: forall a s. (PIsData a) => Term s (PString :--> PMaybeData a :--> a)
mustBePDJust = phoistAcyclic $
plam $ \emsg mv' -> pmatch mv' $ \case
PDJust ((pfield @"_0" #) -> v) -> v
_ -> ptraceError emsg
validatorHashToAddress :: ValidatorHash -> Address
validatorHashToAddress vh = Address (ScriptCredential vh) Nothing