From 87ff8ba34388901ed77d05f559829953b87b738e Mon Sep 17 00:00:00 2001 From: Emily Martins Date: Sat, 26 Mar 2022 10:37:32 +0100 Subject: [PATCH 01/12] stub out Proposal, Governor. kill hoogle before starting a new one. --- Makefile | 1 + agora.cabal | 3 +- agora/Agora/Governor.hs | 8 ++++++ agora/Agora/Proposal.hs | 62 +++++++++++++++++++++++++++++++++++++++++ agora/Agora/Voting.hs | 3 -- 5 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 agora/Agora/Governor.hs create mode 100644 agora/Agora/Proposal.hs diff --git a/Makefile b/Makefile index a2a17e7..50df7e5 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ usage: @echo " haddock -- Generate Haddock docs for project" hoogle: + pkill hoogle hoogle generate --local=haddock --database=hoo/local.hoo hoogle server --local -p 8081 >> /dev/null & hoogle server --local --database=hoo/local.hoo -p 8082 >> /dev/null & diff --git a/agora.cabal b/agora.cabal index b50a55b..7fdf830 100644 --- a/agora.cabal +++ b/agora.cabal @@ -124,7 +124,8 @@ library Agora.SafeMoney.QQ Agora.Stake Agora.Treasury - Agora.Voting + Agora.Governor + Agora.Proposal other-modules: Agora.Utils diff --git a/agora/Agora/Governor.hs b/agora/Agora/Governor.hs new file mode 100644 index 0000000..5dea361 --- /dev/null +++ b/agora/Agora/Governor.hs @@ -0,0 +1,8 @@ +{- | +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 () where diff --git a/agora/Agora/Proposal.hs b/agora/Agora/Proposal.hs new file mode 100644 index 0000000..f215d30 --- /dev/null +++ b/agora/Agora/Proposal.hs @@ -0,0 +1,62 @@ +{- | +Module : Agora.Proposal +Maintainer : emi@haskell.fyi +Description: Proposal scripts encoding effects that operate on the system. + +Proposal scripts encoding effects that operate on the system. +-} +module Agora.Proposal ( + ProposalDatum (..), + ProposalStatus (..), + ResultTag (..), +) where + +import Plutus.V1.Ledger.Api (DatumHash, PubKeyHash, ValidatorHash) + +-------------------------------------------------------------------------------- + +{- | Encodes a result. Typically, for a Yes/No proposal, we encode it like this: + + "No" ~ EffectTag 0 + "Yes" ~ EffectTag 1 +-} +newtype ResultTag = ResultTag {getResultTag :: Integer} + +{- | The 'status' of the proposal. This is only useful for __actual__ + state transitions, as opposed to time-based 'phases'. + + If the proposal is 'VotingReady', for instance, that doesn't necessarily + mean that voting is possible, as this also requires the timing to be right. +-} +data ProposalStatus + = -- | A draft proposal represents a proposal that has yet to be realized. + -- In effect, this means one which didn't have enough LQ to be a full + -- proposal, and needs cosigners to enable that to happen. This is + -- similar to a "temperature check", but only useful if multiple people + -- want to pool governance tokens together. If the proposal doesn't get to + -- 'VotingReady' on time, the proposal will **never** be able to get + -- voted on. + Draft + | -- | The proposal has/had enough GT cosigned in order to be a fully fledged + -- proposal. This means that once the timing requirements align, + -- proposal will be able to be voted on. + VotingReady + | -- | The proposal has finished for whatever reason. This can mean it's been + -- voted on and completed, but it can also mean the proposal failed due to + -- time constraints or didn't get to 'VotingReady' first. + -- + -- TODO: The owner of the proposal may choose to reclaim their proposal. + Finished + +data ProposalDatum = ProposalDatum + { -- 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 :: [(ResultTag, [(ValidatorHash, DatumHash)])] + -- ^ Effect lookup table. First by result, then by + , status :: ProposalStatus + -- ^ The status the proposal is in. + , proposers :: [PubKeyHash] + -- ^ Who created the proposal initially. + -- We may want to remove this. + } diff --git a/agora/Agora/Voting.hs b/agora/Agora/Voting.hs index 5436960..066956c 100644 --- a/agora/Agora/Voting.hs +++ b/agora/Agora/Voting.hs @@ -6,6 +6,3 @@ Description: Types for votes and vote counting module Agora.Voting ( Vote (..), ) where - --- | Type representing direction of vote. -data Vote = InFavorOf | OpposedTo From 8a98ec9ec3e723fd0dc5986f6e3f9597e043d40a Mon Sep 17 00:00:00 2001 From: Emily Martins Date: Sat, 26 Mar 2022 11:52:52 +0100 Subject: [PATCH 02/12] rework SafeMoney to use tags instead of MoneyClass --- Makefile | 2 +- agora-test/Spec/Sample/Stake.hs | 15 ++++- agora/Agora/SafeMoney.hs | 105 ++++++++++++++------------------ agora/Agora/SafeMoney/QQ.hs | 8 +-- agora/Agora/Stake.hs | 80 ++++++++---------------- 5 files changed, 89 insertions(+), 121 deletions(-) diff --git a/Makefile b/Makefile index 50df7e5..90ae380 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ usage: @echo " haddock -- Generate Haddock docs for project" hoogle: - pkill hoogle + pkill hoogle || true hoogle generate --local=haddock --database=hoo/local.hoo hoogle server --local -p 8081 >> /dev/null & hoogle server --local --database=hoo/local.hoo -p 8082 >> /dev/null & diff --git a/agora-test/Spec/Sample/Stake.hs b/agora-test/Spec/Sample/Stake.hs index 4bb0073..ccb4e52 100644 --- a/agora-test/Spec/Sample/Stake.hs +++ b/agora-test/Spec/Sample/Stake.hs @@ -47,7 +47,7 @@ import Plutus.V1.Ledger.Api ( import Plutus.V1.Ledger.Contexts (TxOut (TxOut), TxOutRef (TxOutRef)) import Plutus.V1.Ledger.Interval qualified as Interval import Plutus.V1.Ledger.Scripts (Validator) -import Plutus.V1.Ledger.Value (TokenName (TokenName)) +import Plutus.V1.Ledger.Value (AssetClass (AssetClass), TokenName (TokenName)) import Plutus.V1.Ledger.Value qualified as Value -------------------------------------------------------------------------------- @@ -59,8 +59,17 @@ import Spec.Util (datumPair, toDatumHash) -------------------------------------------------------------------------------- -- | 'Stake' parameters for 'LQ'. -stake :: Stake LQ -stake = Stake +stake :: Stake +stake = + Stake + { gtClassRef = + AssetClassRef + ( AssetClass + ( "da8c30857834c6ae7203935b89278c532b3995245295456f993e1d24" + , "LQ" + ) + ) + } -- | 'Stake' policy instance. policy :: MintingPolicy diff --git a/agora/Agora/SafeMoney.hs b/agora/Agora/SafeMoney.hs index bec07c5..2809566 100644 --- a/agora/Agora/SafeMoney.hs +++ b/agora/Agora/SafeMoney.hs @@ -7,9 +7,14 @@ Phantom-type protected types for handling money in Plutus. -} module Agora.SafeMoney ( -- * Types - MoneyClass, PDiscrete, + -- * Tags and refs + AssetClassRef (..), + ADATag, + GTTag, + adaRef, + -- * Utility functions paddDiscrete, pgeqDiscrete, @@ -18,24 +23,14 @@ module Agora.SafeMoney ( -- * Conversions pdiscreteValue, pvalueDiscrete, - - -- * Example MoneyClasses - LQ, - ADA, ) where -import Data.Proxy (Proxy (Proxy)) -import Data.String -import GHC.TypeLits ( - KnownSymbol, - Nat, - Symbol, - symbolVal, - ) import Prelude -------------------------------------------------------------------------------- +import Plutus.V1.Ledger.Value (AssetClass (AssetClass)) + import Plutarch.Api.V1 (PValue) import Plutarch.Builtin () import Plutarch.Internal () @@ -43,39 +38,46 @@ import Plutarch.Monadic qualified as P -------------------------------------------------------------------------------- -import Agora.Utils (passetClassValueOf, psingletonValue) +import Agora.Utils ( + passetClassValueOf', + psingletonValue, + ) + +-------------------------------------------------------------------------------- +-- Example tags + +-- | Governance token +data GTTag + +-- | ADA +data ADATag -------------------------------------------------------------------------------- --- | Type-level unique identifier for an 'Plutus.V1.Ledger.Value.AssetClass' -type MoneyClass = - ( -- AssetClass - Symbol - , -- TokenName - Symbol - , -- Decimal places - Nat - ) +-- | A tagged AssetClass. Use to resolve a reference inside of a PDiscrete +data AssetClassRef (tag :: Type) = AssetClassRef {getAssetClass :: AssetClass} --- | A 'PDiscrete' amount of currency tagged on the type level with the 'MoneyClass' it belongs to -newtype PDiscrete (mc :: MoneyClass) (s :: S) +adaRef :: AssetClassRef ADATag +adaRef = AssetClassRef (AssetClass ("", "")) + +newtype PDiscrete (tag :: Type) (s :: S) = PDiscrete (Term s PInteger) - deriving (PlutusType, PIsData, PEq, POrd) via (DerivePNewtype (PDiscrete mc) PInteger) + deriving (PlutusType, PIsData, PEq, POrd) via (DerivePNewtype (PDiscrete tag) PInteger) -- | Check if one 'PDiscrete' is greater than another. -pgeqDiscrete :: forall (mc :: MoneyClass) (s :: S). Term s (PDiscrete mc :--> PDiscrete mc :--> PBool) +pgeqDiscrete :: forall (tag :: Type) (s :: S). Term s (PDiscrete tag :--> PDiscrete tag :--> PBool) pgeqDiscrete = phoistAcyclic $ plam $ \x y -> P.do PDiscrete x' <- pmatch x PDiscrete y' <- pmatch y y' #<= x' --- | Returns a zero-value 'PDiscrete' unit for any 'MoneyClass'. -pzeroDiscrete :: forall (mc :: MoneyClass) (s :: S). Term s (PDiscrete mc) +-- | Returns a zero-value 'PDiscrete' unit for any tag. +pzeroDiscrete :: forall (tag :: Type) (s :: S). Term s (PDiscrete tag) pzeroDiscrete = phoistAcyclic $ pcon (PDiscrete 0) --- | Add two 'PDiscrete' values of the same 'MoneyClass'. -paddDiscrete :: Term s (PDiscrete mc :--> PDiscrete mc :--> PDiscrete mc) +-- | Add two 'PDiscrete' values of the same tag. +paddDiscrete :: forall (tag :: Type) (s :: S). Term s (PDiscrete tag :--> PDiscrete tag :--> PDiscrete tag) paddDiscrete = phoistAcyclic $ -- In the future, this should use plutarch-numeric plam $ \x y -> P.do @@ -83,46 +85,29 @@ paddDiscrete = phoistAcyclic $ PDiscrete y' <- pmatch y pcon (PDiscrete $ x' + y') --- | The MoneyClass of LQ. -type LQ :: MoneyClass -type LQ = '("da8c30857834c6ae7203935b89278c532b3995245295456f993e1d24", "LQ", 6) - --- | The MoneyClass of ADA. -type ADA :: MoneyClass -type ADA = '("", "", 6) - -------------------------------------------------------------------------------- -- | Downcast a `PValue` to a `PDiscrete` unit. pvalueDiscrete :: - forall (moneyClass :: MoneyClass) (ac :: Symbol) (n :: Symbol) (scale :: Nat) s. - ( KnownSymbol ac - , KnownSymbol n - , moneyClass ~ '(ac, n, scale) - ) => - Term s (PValue :--> PDiscrete moneyClass) -pvalueDiscrete = phoistAcyclic $ + forall (tag :: Type) (s :: S). + AssetClassRef tag -> + Term s (PValue :--> PDiscrete tag) +pvalueDiscrete (AssetClassRef ac) = phoistAcyclic $ plam $ \f -> - pcon . PDiscrete $ - passetClassValueOf # pconstant (fromString $ symbolVal $ Proxy @ac) - # pconstant (fromString $ symbolVal $ Proxy @n) - # f + pcon . PDiscrete $ passetClassValueOf' ac # f {- | Get a `PValue` from a `PDiscrete`. __NOTE__: `pdiscreteValue` after `pvalueDiscrete` is not a round-trip. - It filters for a particular 'MoneyClass'. + It filters for a particular tag. -} pdiscreteValue :: - forall (moneyClass :: MoneyClass) (ac :: Symbol) (n :: Symbol) (scale :: Nat) s. - ( KnownSymbol ac - , KnownSymbol n - , moneyClass ~ '(ac, n, scale) - ) => - Term s (PDiscrete moneyClass :--> PValue) -pdiscreteValue = phoistAcyclic $ + forall (tag :: Type) (s :: S). + AssetClassRef tag -> + Term s (PDiscrete tag :--> PValue) +pdiscreteValue (AssetClassRef (AssetClass (cs, tn))) = phoistAcyclic $ plam $ \f -> pmatch f $ \case PDiscrete p -> psingletonValue - # pconstant (fromString $ symbolVal $ Proxy @ac) - # pconstant (fromString $ symbolVal $ Proxy @n) + # pconstant cs + # pconstant tn # p diff --git a/agora/Agora/SafeMoney/QQ.hs b/agora/Agora/SafeMoney/QQ.hs index 3fdf161..00d4b79 100644 --- a/agora/Agora/SafeMoney/QQ.hs +++ b/agora/Agora/SafeMoney/QQ.hs @@ -33,11 +33,11 @@ import Prelude import Plutarch.Internal (punsafeCoerce) -import Agora.SafeMoney (MoneyClass, PDiscrete) +import Agora.SafeMoney (PDiscrete) -------------------------------------------------------------------------------- -{- | Generate 'PDiscrete' values tagged by a particular MoneyClass +{- | Generate 'PDiscrete' values tagged by a particular tag @ [discrete| 123.456 'Agora.SafeMoney.ADA' |] :: 'Term' s ('PDiscrete' 'Agora.SafeMoney.ADA') @@ -46,7 +46,7 @@ import Agora.SafeMoney (MoneyClass, PDiscrete) discrete :: QuasiQuoter discrete = QuasiQuoter discreteExp errorDiscretePat errorDiscreteType errorDiscreteDiscretelaration -discreteConstant :: forall (moneyClass :: MoneyClass) s. Integer -> Term s (PDiscrete moneyClass) +discreteConstant :: forall tag s. Integer -> Term s (PDiscrete tag) discreteConstant n = punsafeCoerce (pconstant n :: Term s PInteger) fixedToInteger :: Integer -> (Integer, Integer) -> Integer @@ -68,7 +68,7 @@ discreteExp s = case parseDiscreteRatioExp s of Just (num, mc) -> do mcName <- lookupTypeName mc >>= \case - Nothing -> fail $ "MoneyClass with the name " <> show mc <> " is not in scope." + Nothing -> fail $ "Type with the name " <> show mc <> " is not in scope." Just v -> pure v reified <- reify mcName case reified of diff --git a/agora/Agora/Stake.hs b/agora/Agora/Stake.hs index 3929449..ee5c52c 100644 --- a/agora/Agora/Stake.hs +++ b/agora/Agora/Stake.hs @@ -20,13 +20,7 @@ module Agora.Stake ( -------------------------------------------------------------------------------- -import Data.Proxy (Proxy (Proxy)) -import Data.String (IsString (fromString)) import GHC.Generics qualified as GHC -import GHC.TypeLits ( - KnownSymbol, - symbolVal, - ) import Generics.SOP (Generic, I (I)) import Prelude @@ -59,7 +53,8 @@ import Plutus.V1.Ledger.Value (AssetClass (AssetClass)) -------------------------------------------------------------------------------- import Agora.SafeMoney ( - MoneyClass, + AssetClassRef (..), + GTTag, PDiscrete, paddDiscrete, pdiscreteValue, @@ -84,12 +79,15 @@ import Agora.Utils ( -------------------------------------------------------------------------------- -- | Parameters for creating Stake scripts. -data Stake (gt :: MoneyClass) = Stake +data Stake = Stake + { gtClassRef :: AssetClassRef GTTag + -- ^ Resolve governance token + } -- | Plutarch-level redeemer for Stake scripts. -data PStakeRedeemer (gt :: MoneyClass) (s :: S) +data PStakeRedeemer (s :: S) = -- | Deposit or withdraw a discrete amount of the staked governance token. - PDepositWithdraw (Term s (PDataRecord '["delta" ':= PDiscrete gt])) + PDepositWithdraw (Term s (PDataRecord '["delta" ':= PDiscrete GTTag])) | -- | Destroy a stake, retrieving its LQ, the minimum ADA and any other assets. PDestroy (Term s (PDataRecord '[])) deriving stock (GHC.Generic) @@ -97,13 +95,7 @@ data PStakeRedeemer (gt :: MoneyClass) (s :: S) deriving anyclass (PIsDataRepr) deriving (PlutusType, PIsData) - via PIsDataReprInstances (PStakeRedeemer gt) - --- FIXME: 'StakeRedeemer' and 'StakeDatum' are stripped of their --- typesafe `PDiscrete` equivalent due to issues with `makeIsDataIndexed` --- when using the kind @gt :: MoneyClass@. This ought to be fixed with --- a future patch in Plutarch upstream. For now, we will deal with lower --- type safety off-chain. + via PIsDataReprInstances PStakeRedeemer -- | Haskell-level redeemer for Stake scripts. data StakeRedeemer @@ -116,16 +108,16 @@ data StakeRedeemer PlutusTx.makeIsDataIndexed ''StakeRedeemer [('DepositWithdraw, 0), ('Destroy, 1)] -- | Plutarch-level datum for Stake scripts. -newtype PStakeDatum (gt :: MoneyClass) (s :: S) = PStakeDatum +newtype PStakeDatum (s :: S) = PStakeDatum { getStakeDatum :: - Term s (PDataRecord '["stakedAmount" ':= PDiscrete gt, "owner" ':= PPubKeyHash]) + Term s (PDataRecord '["stakedAmount" ':= PDiscrete GTTag, "owner" ':= PPubKeyHash]) } deriving stock (GHC.Generic) deriving anyclass (Generic) deriving anyclass (PIsDataRepr) deriving (PlutusType, PIsData, PDataFields) - via (PIsDataReprInstances (PStakeDatum gt)) + via (PIsDataReprInstances PStakeDatum) -- | Haskell-level datum for Stake scripts. data StakeDatum = StakeDatum @@ -154,14 +146,10 @@ PlutusTx.makeIsDataIndexed ''StakeDatum [('StakeDatum, 0)] -- | Policy for Stake state threads. stakePolicy :: - forall (gt :: MoneyClass) ac n scale s. - ( KnownSymbol ac - , KnownSymbol n - , gt ~ '(ac, n, scale) - ) => - Stake gt -> + forall (s :: S). + Stake -> Term s PMintingPolicy -stakePolicy _stake = +stakePolicy stake = plam $ \_redeemer ctx' -> P.do ctx <- pletFields @'["txInfo", "purpose"] ctx' txInfo' <- plet ctx.txInfo @@ -180,7 +168,7 @@ stakePolicy _stake = mintedST #== -1 passert "An unlocked input existed containing an ST" $ - anyInput @(PStakeDatum gt) # pfromData txInfo' + anyInput @PStakeDatum # pfromData txInfo' #$ plam $ \value _ stakeDatum' -> P.do let hasST = psymbolValueOf # ownSymbol # value #== 1 @@ -197,7 +185,7 @@ stakePolicy _stake = mintedST #== 1 passert "A UTXO must exist with the correct output" $ - anyOutput @(PStakeDatum gt) # pfromData txInfo' + anyOutput @PStakeDatum # pfromData txInfo' #$ plam $ \value address stakeDatum' -> P.do let cred = pfield @"credential" # address @@ -220,7 +208,7 @@ stakePolicy _stake = # 1 let expectedValue = paddValue - # (pdiscreteValue # stakeDatum.stakedAmount) + # (pdiscreteValue stake.gtClassRef # stakeDatum.stakedAmount) # stValue let ownerSignsTransaction = ptxSignedBy @@ -234,12 +222,7 @@ stakePolicy _stake = foldr1 (#&&) [ pgeqByClass' (AssetClass ("", "")) # value # expectedValue - , pgeqByClass' - ( AssetClass - ( fromString . symbolVal $ Proxy @ac - , fromString . symbolVal $ Proxy @n - ) - ) + , pgeqByClass' stake.gtClassRef.getAssetClass # value # expectedValue , pgeqByClass @@ -259,12 +242,8 @@ stakePolicy _stake = -- | Validator intended for Stake UTXOs to live in. stakeValidator :: - forall (gt :: MoneyClass) ac n scale s. - ( KnownSymbol ac - , KnownSymbol n - , gt ~ '(ac, n, scale) - ) => - Stake gt -> + forall (s :: S). + Stake -> Term s PValidator stakeValidator stake = plam $ \datum redeemer ctx' -> P.do @@ -273,9 +252,9 @@ stakeValidator stake = txInfo <- pletFields @'["mint", "inputs", "outputs"] txInfo' -- Coercion is safe in that if coercion fails we crash hard. - let stakeRedeemer :: Term _ (PStakeRedeemer gt) + let stakeRedeemer :: Term _ PStakeRedeemer stakeRedeemer = pfromData $ punsafeCoerce redeemer - stakeDatum' :: Term _ (PStakeDatum gt) + stakeDatum' :: Term _ PStakeDatum stakeDatum' = pfromData $ punsafeCoerce datum stakeDatum <- pletFields @'["owner", "stakedAmount"] stakeDatum' @@ -310,7 +289,7 @@ stakeValidator stake = "Owner signs this transaction" ownerSignsTransaction passert "A UTXO must exist with the correct output" $ - anyOutput @(PStakeDatum gt) # txInfo' + anyOutput @PStakeDatum # txInfo' #$ plam $ \value address newStakeDatum' -> P.do newStakeDatum <- pletFields @'["owner", "stakedAmount"] newStakeDatum' @@ -325,7 +304,7 @@ stakeValidator stake = -- do we need to check this, really? pgeqDiscrete # (pfromData newStakeDatum.stakedAmount) # pzeroDiscrete ] - let expectedValue = paddValue # continuingValue # (pdiscreteValue # delta) + let expectedValue = paddValue # continuingValue # (pdiscreteValue stake.gtClassRef # delta) -- TODO: Same as above. This is quite inefficient now, as it does two lookups -- instead of a more efficient single pass, @@ -334,12 +313,7 @@ stakeValidator stake = foldr1 (#&&) [ pgeqByClass' (AssetClass ("", "")) # value # expectedValue - , pgeqByClass' - ( AssetClass - ( fromString . symbolVal $ Proxy @ac - , fromString . symbolVal $ Proxy @n - ) - ) + , pgeqByClass' stake.gtClassRef.getAssetClass # value # expectedValue , pgeqBySymbol @@ -360,7 +334,7 @@ stakeValidator stake = -------------------------------------------------------------------------------- -- | Check whether a Stake is locked. If it is locked, various actions are unavailable. -stakeLocked :: forall (gt :: MoneyClass) s. Term s (PStakeDatum gt :--> PBool) +stakeLocked :: forall (s :: S). Term s (PStakeDatum :--> PBool) stakeLocked = phoistAcyclic $ plam $ \_stakeDatum -> -- TODO: when we extend this to support proposals, this will need to do something From 48541836c7cfc64e7da20922bbc57c15e98599bb Mon Sep 17 00:00:00 2001 From: Emily Martins Date: Sat, 26 Mar 2022 13:01:56 +0100 Subject: [PATCH 03/12] add ToData/FromData instances to `Discrete` --- agora/Agora/SafeMoney.hs | 25 ++++++++++++++++++++++++- agora/Agora/Stake.hs | 2 +- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/agora/Agora/SafeMoney.hs b/agora/Agora/SafeMoney.hs index 2809566..4289c27 100644 --- a/agora/Agora/SafeMoney.hs +++ b/agora/Agora/SafeMoney.hs @@ -29,7 +29,9 @@ import Prelude -------------------------------------------------------------------------------- +import Plutus.V1.Ledger.Api (BuiltinData (..), Data (..)) import Plutus.V1.Ledger.Value (AssetClass (AssetClass)) +import PlutusTx.IsData.Class (FromData (..), ToData (..)) import Plutarch.Api.V1 (PValue) import Plutarch.Builtin () @@ -55,11 +57,32 @@ data ADATag -------------------------------------------------------------------------------- -- | A tagged AssetClass. Use to resolve a reference inside of a PDiscrete -data AssetClassRef (tag :: Type) = AssetClassRef {getAssetClass :: AssetClass} +newtype AssetClassRef (tag :: Type) = AssetClassRef {getAssetClass :: AssetClass} +-- | Resolves ada tags. adaRef :: AssetClassRef ADATag adaRef = AssetClassRef (AssetClass ("", "")) +{- | Represents a single asset in a 'Value' related to a particular 'AssetClass' + through 'AssetClassRef'. +-} +newtype Discrete (tag :: Type) + = Discrete Integer + deriving stock (Show, Eq) + +{- We have to manually write these instances because the `tag` will confuse + `makeIsDataIndexed`. +-} +instance forall tag. FromData (Discrete tag) where + fromBuiltinData (BuiltinData (I x)) = Just (Discrete x) + fromBuiltinData _ = Nothing + +instance forall tag. ToData (Discrete tag) where + toBuiltinData (Discrete x) = BuiltinData (I x) + +{- | Represents a single asset in a 'PValue' related to a particular 'AssetClass' + through 'AssetClassRef'. +-} newtype PDiscrete (tag :: Type) (s :: S) = PDiscrete (Term s PInteger) deriving (PlutusType, PIsData, PEq, POrd) via (DerivePNewtype (PDiscrete tag) PInteger) diff --git a/agora/Agora/Stake.hs b/agora/Agora/Stake.hs index ee5c52c..9096427 100644 --- a/agora/Agora/Stake.hs +++ b/agora/Agora/Stake.hs @@ -79,7 +79,7 @@ import Agora.Utils ( -------------------------------------------------------------------------------- -- | Parameters for creating Stake scripts. -data Stake = Stake +newtype Stake = Stake { gtClassRef :: AssetClassRef GTTag -- ^ Resolve governance token } From 15d25f314ba9c71517337844878acd1f9f1f3b93 Mon Sep 17 00:00:00 2001 From: Emily Martins Date: Sat, 26 Mar 2022 15:08:11 +0100 Subject: [PATCH 04/12] migrate Haskell-level datums to use Discrete --- agora-test/Spec/Sample/Stake.hs | 15 ++---- agora-test/Spec/Stake.hs | 6 ++- agora.cabal | 1 - agora/Agora/Proposal.hs | 5 +- agora/Agora/SafeMoney.hs | 43 +++++++++------ agora/Agora/SafeMoney/QQ.hs | 96 --------------------------------- agora/Agora/Stake.hs | 10 ++-- agora/Agora/Voting.hs | 8 --- 8 files changed, 44 insertions(+), 140 deletions(-) delete mode 100644 agora/Agora/SafeMoney/QQ.hs delete mode 100644 agora/Agora/Voting.hs diff --git a/agora-test/Spec/Sample/Stake.hs b/agora-test/Spec/Sample/Stake.hs index ccb4e52..85b95ac 100644 --- a/agora-test/Spec/Sample/Stake.hs +++ b/agora-test/Spec/Sample/Stake.hs @@ -21,7 +21,6 @@ module Spec.Sample.Stake ( ) where -------------------------------------------------------------------------------- - import Plutarch.Api.V1 ( mintingPolicySymbol, mkMintingPolicy, @@ -144,9 +143,9 @@ stakeCreationUnsigned = -- | Config for creating a ScriptContext that deposits or withdraws. data DepositWithdrawExample = DepositWithdrawExample - { startAmount :: Integer + { startAmount :: Discrete GTTag -- ^ The amount of GT stored before the transaction. - , delta :: Integer + , delta :: Discrete GTTag -- ^ The amount of GT deposited or withdrawn from the Stake. } @@ -169,10 +168,7 @@ stakeDepositWithdraw config = { txOutAddress = Address (ScriptCredential $ validatorHash validator) Nothing , txOutValue = st - <> Value.singleton - "da8c30857834c6ae7203935b89278c532b3995245295456f993e1d24" - "LQ" - stakeBefore.stakedAmount + <> discreteValue stake.gtClassRef stakeBefore.stakedAmount , txOutDatumHash = Just (toDatumHash stakeAfter) } ] @@ -181,10 +177,7 @@ stakeDepositWithdraw config = { txOutAddress = Address (ScriptCredential $ validatorHash validator) Nothing , txOutValue = st - <> Value.singleton - "da8c30857834c6ae7203935b89278c532b3995245295456f993e1d24" - "LQ" - stakeAfter.stakedAmount + <> discreteValue stake.gtClassRef stakeAfter.stakedAmount , txOutDatumHash = Just (toDatumHash stakeAfter) } ] diff --git a/agora-test/Spec/Stake.hs b/agora-test/Spec/Stake.hs index 8064ddf..5ce5d79 100644 --- a/agora-test/Spec/Stake.hs +++ b/agora-test/Spec/Stake.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE QuasiQuotes #-} + {- | Module : Spec.Stake Maintainer : emi@haskell.fyi @@ -57,13 +59,13 @@ tests = "stakeDepositWithdraw withdraw" (stakeValidator Stake.stake) (toDatum $ StakeDatum 100_000 signer) - (toDatum $ DepositWithdraw (negate 100_000)) + (toDatum $ DepositWithdraw $ negate 100_000) (Stake.stakeDepositWithdraw $ DepositWithdrawExample {startAmount = 100_000, delta = negate 100_000}) , validatorFailsWith "stakeDepositWithdraw negative GT" (stakeValidator Stake.stake) (toDatum $ StakeDatum 100_000 signer) - (toDatum $ DepositWithdraw (negate 1_000_000)) + (toDatum $ DepositWithdraw 1_000_000) (Stake.stakeDepositWithdraw $ DepositWithdrawExample {startAmount = 100_000, delta = negate 1_000_000}) ] ] diff --git a/agora.cabal b/agora.cabal index 7fdf830..6d1005a 100644 --- a/agora.cabal +++ b/agora.cabal @@ -121,7 +121,6 @@ library Agora.AuthorityToken Agora.MultiSig Agora.SafeMoney - Agora.SafeMoney.QQ Agora.Stake Agora.Treasury Agora.Governor diff --git a/agora/Agora/Proposal.hs b/agora/Agora/Proposal.hs index f215d30..aa936ae 100644 --- a/agora/Agora/Proposal.hs +++ b/agora/Agora/Proposal.hs @@ -22,8 +22,8 @@ import Plutus.V1.Ledger.Api (DatumHash, PubKeyHash, ValidatorHash) -} newtype ResultTag = ResultTag {getResultTag :: Integer} -{- | The 'status' of the proposal. This is only useful for __actual__ - state transitions, as opposed to time-based 'phases'. +{- | The "status" of the proposal. This is only useful for __actual__ + state transitions, as opposed to time-based "phases". If the proposal is 'VotingReady', for instance, that doesn't necessarily mean that voting is possible, as this also requires the timing to be right. @@ -48,6 +48,7 @@ data ProposalStatus -- TODO: The owner of the proposal may choose to reclaim their proposal. Finished +-- | Haskell-level datum for Proposal scripts. data ProposalDatum = ProposalDatum { -- TODO: could we encode this more efficiently? -- This is shaped this way for future proofing. diff --git a/agora/Agora/SafeMoney.hs b/agora/Agora/SafeMoney.hs index 4289c27..0862cef 100644 --- a/agora/Agora/SafeMoney.hs +++ b/agora/Agora/SafeMoney.hs @@ -7,7 +7,8 @@ Phantom-type protected types for handling money in Plutus. -} module Agora.SafeMoney ( -- * Types - PDiscrete, + PDiscrete (..), + Discrete (..), -- * Tags and refs AssetClassRef (..), @@ -23,15 +24,16 @@ module Agora.SafeMoney ( -- * Conversions pdiscreteValue, pvalueDiscrete, + discreteValue, ) where import Prelude -------------------------------------------------------------------------------- -import Plutus.V1.Ledger.Api (BuiltinData (..), Data (..)) -import Plutus.V1.Ledger.Value (AssetClass (AssetClass)) -import PlutusTx.IsData.Class (FromData (..), ToData (..)) +import Plutus.V1.Ledger.Value (AssetClass (AssetClass), Value) +import Plutus.V1.Ledger.Value qualified as Value +import PlutusTx qualified import Plutarch.Api.V1 (PValue) import Plutarch.Builtin () @@ -63,22 +65,21 @@ newtype AssetClassRef (tag :: Type) = AssetClassRef {getAssetClass :: AssetClass adaRef :: AssetClassRef ADATag adaRef = AssetClassRef (AssetClass ("", "")) -{- | Represents a single asset in a 'Value' related to a particular 'AssetClass' +-- TODO: Currently it's possible to transmute from one discrete to another. +-- How do we prevent this? +-- +-- @ +-- transmute :: forall (a :: Type) (b :: Type). Discrete a -> Discrete b +-- transmute = Discrete . getDiscrete +-- @ + +{- | Represents a single asset in a 'Plutus.V1.Ledger.Value.Value' related to a particular 'AssetClass' through 'AssetClassRef'. -} -newtype Discrete (tag :: Type) - = Discrete Integer +newtype Discrete (tag :: Type) = Discrete {getDiscrete :: Integer} deriving stock (Show, Eq) - -{- We have to manually write these instances because the `tag` will confuse - `makeIsDataIndexed`. --} -instance forall tag. FromData (Discrete tag) where - fromBuiltinData (BuiltinData (I x)) = Just (Discrete x) - fromBuiltinData _ = Nothing - -instance forall tag. ToData (Discrete tag) where - toBuiltinData (Discrete x) = BuiltinData (I x) + deriving newtype (PlutusTx.ToData, PlutusTx.FromData, PlutusTx.UnsafeFromData) + deriving newtype (Num) -- TODO: Use plutarch-numeric {- | Represents a single asset in a 'PValue' related to a particular 'AssetClass' through 'AssetClassRef'. @@ -134,3 +135,11 @@ pdiscreteValue (AssetClassRef (AssetClass (cs, tn))) = phoistAcyclic $ # pconstant cs # pconstant tn # p + +discreteValue :: + forall (tag :: Type). + AssetClassRef tag -> + Discrete tag -> + Value +discreteValue (AssetClassRef (AssetClass (cs, tn))) (Discrete v) = + Value.singleton cs tn v diff --git a/agora/Agora/SafeMoney/QQ.hs b/agora/Agora/SafeMoney/QQ.hs deleted file mode 100644 index 00d4b79..0000000 --- a/agora/Agora/SafeMoney/QQ.hs +++ /dev/null @@ -1,96 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} - -{- | -Module : Agora.SafeMoney.QQ -Maintainer : emi@haskell.fyi -Description: Quasiquoter for SafeMoney types. - -Quasiquoter for SafeMoney types. --} -module Agora.SafeMoney.QQ (discrete) where - -import GHC.Real (Ratio ((:%))) -import Language.Haskell.TH qualified as TH (Type) -import Language.Haskell.TH.Quote (QuasiQuoter (QuasiQuoter)) -import Language.Haskell.TH.Syntax ( - Dec (TySynD), - Exp (AppE, AppTypeE, LitE, VarE), - Info (TyConI), - Lit (IntegerL), - Pat, - Q, - TyLit (NumTyLit, StrTyLit), - Type (AppT, ConT, LitT, PromotedTupleT), - lookupTypeName, - reify, - ) -import Text.ParserCombinators.ReadP (readP_to_S, skipSpaces) -import Text.Read (lexP, readPrec_to_P) -import Text.Read.Lex (Lexeme (Ident, Number), Number, numberToFixed, numberToRational) -import Prelude - --------------------------------------------------------------------------------- - -import Plutarch.Internal (punsafeCoerce) - -import Agora.SafeMoney (PDiscrete) - --------------------------------------------------------------------------------- - -{- | Generate 'PDiscrete' values tagged by a particular tag - -@ - [discrete| 123.456 'Agora.SafeMoney.ADA' |] :: 'Term' s ('PDiscrete' 'Agora.SafeMoney.ADA') -@ --} -discrete :: QuasiQuoter -discrete = QuasiQuoter discreteExp errorDiscretePat errorDiscreteType errorDiscreteDiscretelaration - -discreteConstant :: forall tag s. Integer -> Term s (PDiscrete tag) -discreteConstant n = punsafeCoerce (pconstant n :: Term s PInteger) - -fixedToInteger :: Integer -> (Integer, Integer) -> Integer -fixedToInteger places (i, f) = i * 10 ^ places + f - -safeIntegerUpcast :: Integer -> Number -> Either String Integer -safeIntegerUpcast places num = - case (numberToFixed places num, numberToRational num * 10 ^ places) of - (Just (i, f), _n :% 1) -> - Right $ fixedToInteger places (i, f) - (Just (i, f), _n :% _d) -> - Left $ "Using more than the available decimal places (" <> show places <> "). Would round to " <> show i <> "." <> show f - _ -> Left "Some error occurred while getting number" - -discreteExp :: String -> Q Exp -discreteExp s = case parseDiscreteRatioExp s of - Nothing -> - fail $ "Input malformed. Got: " <> s - Just (num, mc) -> do - mcName <- - lookupTypeName mc >>= \case - Nothing -> fail $ "Type with the name " <> show mc <> " is not in scope." - Just v -> pure v - reified <- reify mcName - case reified of - TyConI (TySynD tyName [] (AppT (AppT (AppT (PromotedTupleT 3) (LitT (StrTyLit _))) (LitT _)) (LitT (NumTyLit n)))) -> - case safeIntegerUpcast n num of - Right i -> - pure $ AppE (AppTypeE (VarE 'discreteConstant) (ConT tyName)) (LitE (IntegerL i)) - Left e -> fail e - ty' -> fail $ "Could not reify type, got: " <> show ty' - -parseDiscreteRatioExp :: String -> Maybe (Number, String) -parseDiscreteRatioExp s = - let p = skipSpaces *> ((,) <$> readPrec_to_P lexP 0 <* skipSpaces <*> readPrec_to_P lexP 0) <* skipSpaces - in case readP_to_S p s of - [((Number n, Ident i), "")] -> Just (n, i) - _ -> Nothing - -errorDiscretePat :: String -> Q Pat -errorDiscretePat _ = fail "Cannot use 'discrete' in a pattern context." - -errorDiscreteType :: String -> Q TH.Type -errorDiscreteType _ = fail "Cannot use 'discrete' in a type context." - -errorDiscreteDiscretelaration :: String -> Q [Dec] -errorDiscreteDiscretelaration _ = fail "Cannot use 'discrete' in a declaration context." diff --git a/agora/Agora/Stake.hs b/agora/Agora/Stake.hs index 9096427..8d3296e 100644 --- a/agora/Agora/Stake.hs +++ b/agora/Agora/Stake.hs @@ -54,6 +54,7 @@ import Plutus.V1.Ledger.Value (AssetClass (AssetClass)) import Agora.SafeMoney ( AssetClassRef (..), + Discrete, GTTag, PDiscrete, paddDiscrete, @@ -66,6 +67,7 @@ import Agora.Utils ( anyOutput, paddValue, passert, + passetClassValueOf', pfindTxInByTxOutRef, pgeqByClass, pgeqByClass', @@ -100,7 +102,7 @@ data PStakeRedeemer (s :: S) -- | Haskell-level redeemer for Stake scripts. data StakeRedeemer = -- | Deposit or withdraw a discrete amount of the staked governance token. - DepositWithdraw Integer + DepositWithdraw (Discrete GTTag) | -- | Destroy a stake, retrieving its LQ, the minimum ADA and any other assets. Destroy deriving stock (Show, GHC.Generic) @@ -121,8 +123,7 @@ newtype PStakeDatum (s :: S) = PStakeDatum -- | Haskell-level datum for Stake scripts. data StakeDatum = StakeDatum - { -- FIXME: This needs to be gt - stakedAmount :: Integer + { stakedAmount :: Discrete GTTag , owner :: PubKeyHash } deriving stock (Show, GHC.Generic) @@ -306,6 +307,9 @@ stakeValidator stake = ] let expectedValue = paddValue # continuingValue # (pdiscreteValue stake.gtClassRef # delta) + ptrace (pshow $ passetClassValueOf' stake.gtClassRef.getAssetClass # value) + ptrace (pshow $ passetClassValueOf' stake.gtClassRef.getAssetClass # expectedValue) + -- TODO: Same as above. This is quite inefficient now, as it does two lookups -- instead of a more efficient single pass, -- but it doesn't really matter for this. At least it's correct. diff --git a/agora/Agora/Voting.hs b/agora/Agora/Voting.hs deleted file mode 100644 index 066956c..0000000 --- a/agora/Agora/Voting.hs +++ /dev/null @@ -1,8 +0,0 @@ -{- | -Module : Agora.Voting -Maintainer : emi@haskell.fyi -Description: Types for votes and vote counting --} -module Agora.Voting ( - Vote (..), -) where From ae5c18aa00af40365138a6ab7f8494b50445fbdf Mon Sep 17 00:00:00 2001 From: Emily Martins Date: Sat, 26 Mar 2022 16:07:21 +0100 Subject: [PATCH 05/12] add vote tally to Proposal datum --- agora.cabal | 1 + agora/Agora/Effect.hs | 8 ++++++ agora/Agora/Proposal.hs | 64 +++++++++++++++++++++++++++++++++++------ 3 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 agora/Agora/Effect.hs diff --git a/agora.cabal b/agora.cabal index 6d1005a..aeb2adb 100644 --- a/agora.cabal +++ b/agora.cabal @@ -122,6 +122,7 @@ library Agora.MultiSig Agora.SafeMoney Agora.Stake + Agora.Effect Agora.Treasury Agora.Governor Agora.Proposal diff --git a/agora/Agora/Effect.hs b/agora/Agora/Effect.hs new file mode 100644 index 0000000..b802044 --- /dev/null +++ b/agora/Agora/Effect.hs @@ -0,0 +1,8 @@ +{- | +Module : Agora.Effect +Maintainer : emi@haskell.fyi +Description: Helpers for constructing effects + +Helpers for constructing effects. +-} +module Agora.Effect () where diff --git a/agora/Agora/Proposal.hs b/agora/Agora/Proposal.hs index aa936ae..8b7d263 100644 --- a/agora/Agora/Proposal.hs +++ b/agora/Agora/Proposal.hs @@ -8,6 +8,8 @@ Proposal scripts encoding effects that operate on the system. module Agora.Proposal ( ProposalDatum (..), ProposalStatus (..), + ProposalThresholds (..), + ProposalVotes (..), ResultTag (..), ) where @@ -15,39 +17,79 @@ import Plutus.V1.Ledger.Api (DatumHash, PubKeyHash, ValidatorHash) -------------------------------------------------------------------------------- +import Agora.SafeMoney (Discrete, GTTag) + +-------------------------------------------------------------------------------- + {- | Encodes a result. Typically, for a Yes/No proposal, we encode it like this: - "No" ~ EffectTag 0 - "Yes" ~ EffectTag 1 +@ +"No" ~ 'ResultTag' 0 +"Yes" ~ 'ResultTag' 1 +@ -} newtype ResultTag = ResultTag {getResultTag :: Integer} -{- | The "status" of the proposal. This is only useful for __actual__ - state transitions, as opposed to time-based "phases". +{- | The "status" of the proposal. This is only useful for state transitions, + as opposed to time-based "phases". If the proposal is 'VotingReady', for instance, that doesn't necessarily mean that voting is possible, as this also requires the timing to be right. -} data ProposalStatus = -- | A draft proposal represents a proposal that has yet to be realized. + -- -- In effect, this means one which didn't have enough LQ to be a full -- proposal, and needs cosigners to enable that to happen. This is -- similar to a "temperature check", but only useful if multiple people -- want to pool governance tokens together. If the proposal doesn't get to - -- 'VotingReady' on time, the proposal will **never** be able to get + -- 'VotingReady' on time, the proposal will __never__ be able to get -- voted on. Draft | -- | The proposal has/had enough GT cosigned in order to be a fully fledged - -- proposal. This means that once the timing requirements align, + -- proposal. + -- + -- This means that once the timing requirements align, -- proposal will be able to be voted on. VotingReady - | -- | The proposal has finished for whatever reason. This can mean it's been - -- voted on and completed, but it can also mean the proposal failed due to - -- time constraints or didn't get to 'VotingReady' first. + | -- | The proposal has finished. + -- + -- This can mean it's been voted on and completed, but it can also mean + -- the proposal failed due to time constraints or didn't + -- get to 'VotingReady' first. -- -- TODO: The owner of the proposal may choose to reclaim their proposal. Finished +{- | The threshold values for various state transitions to happen. + This data is stored centrally (in the Governor) and copied over + to Proposals when they are created. +-} +data ProposalThresholds = ProposalThresholds + { execute :: Discrete GTTag + -- ^ How much GT minimum must a particular 'ResultTag' accumulate to fulfil. + , draft :: Discrete GTTag + -- ^ How much GT required to "create" a proposal. + , vote :: Discrete GTTag + -- ^ How much GT required to allow voting to happen. + -- (i.e. to move into 'VotingReady') + } + +{- | Map which encodes the total tally for each result. + It's important that the 'shape' is consistent with the shape of 'effects'. + + e.g. if the 'effects' field looks like the following: + + @[('ResultTag' 0, []), ('ResultTag' 1, [(vh, dh)])]@ + + Then 'ProposalVotes' need be of the shape: + + @[('ResultTag' 0, n), ('ResultTag' 1, m)]@ +-} +newtype ProposalVotes = ProposalVotes + { getProposalVotes :: [(ResultTag, Integer)] + } + -- | Haskell-level datum for Proposal scripts. data ProposalDatum = ProposalDatum { -- TODO: could we encode this more efficiently? @@ -60,4 +102,8 @@ data ProposalDatum = ProposalDatum , proposers :: [PubKeyHash] -- ^ Who created the proposal initially. -- We may want to remove this. + , thresholds :: ProposalThresholds + -- ^ Thresholds copied over on initialization. + , votes :: ProposalVotes + -- ^ Vote tally on the proposal } From 85344059202d3753a92dc55fc16510b04208dbab Mon Sep 17 00:00:00 2001 From: Emily Martins Date: Mon, 28 Mar 2022 12:20:47 +0200 Subject: [PATCH 06/12] flesh out Governor datum a bit more --- agora/Agora/Governor.hs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/agora/Agora/Governor.hs b/agora/Agora/Governor.hs index 5dea361..b4ad0bc 100644 --- a/agora/Agora/Governor.hs +++ b/agora/Agora/Governor.hs @@ -5,4 +5,24 @@ Description: Governor entity scripts acting as authority of entire system. Governor entity scripts acting as authority of entire system. -} -module Agora.Governor () where +module Agora.Governor (GovernorDatum (..)) where + +import Agora.Proposal (ProposalThresholds) + +data GovernorDatum = GovernorDatum + { proposalThresholds :: ProposalThresholds + -- ^ Gets copied over upon creation of a 'Proposal'. + } + +{- | Redeemer for Governor script. + + The governor has two primary responsibilities: + - The gating of Proposal creation + - The gating of minting authority tokens +-} +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 From 64d006d025aee43589f9e7a7c672fda775c9fc15 Mon Sep 17 00:00:00 2001 From: Emily Martins Date: Mon, 28 Mar 2022 13:12:16 +0200 Subject: [PATCH 07/12] add 'makeEffect' template stub --- agora/Agora/Effect.hs | 31 ++++++++++++++++++++++++++++++- agora/Agora/Governor.hs | 2 +- agora/PPrelude.hs | 5 +++-- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/agora/Agora/Effect.hs b/agora/Agora/Effect.hs index b802044..fccb32e 100644 --- a/agora/Agora/Effect.hs +++ b/agora/Agora/Effect.hs @@ -5,4 +5,33 @@ Description: Helpers for constructing effects Helpers for constructing effects. -} -module Agora.Effect () where +module Agora.Effect (makeEffect) where + +import Plutarch.Api.V1 (PScriptPurpose (PSpending), PTxInfo, PTxOutRef, PValidator) +import Plutarch.Internal (punsafeCoerce) +import Plutarch.Monadic qualified as P + +-------------------------------------------------------------------------------- + +-- | Helper 'template' for creating effect validator. +makeEffect :: + forall (datum :: PType) (s :: S). + PIsData datum => + (Term s datum -> Term s PTxOutRef -> Term s PTxInfo -> Term s POpaque) -> + Term s PValidator +makeEffect f = + plam $ \datum _redeemer ctx' -> P.do + ctx <- pletFields @'["txInfo", "purpose"] ctx' + txInfo' <- plet ctx.txInfo + + let datum' :: Term _ datum + datum' = pfromData $ punsafeCoerce datum + + PSpending txOutRef <- pmatch $ pfromData ctx.purpose + txOutRef' <- plet (pfield @"_0" # txOutRef) + + -- TODO: Here, check that a *single* GAT is burned. + + f datum' txOutRef' txInfo' + +-------------------------------------------------------------------------------- diff --git a/agora/Agora/Governor.hs b/agora/Agora/Governor.hs index b4ad0bc..f44eda1 100644 --- a/agora/Agora/Governor.hs +++ b/agora/Agora/Governor.hs @@ -5,7 +5,7 @@ Description: Governor entity scripts acting as authority of entire system. Governor entity scripts acting as authority of entire system. -} -module Agora.Governor (GovernorDatum (..)) where +module Agora.Governor (GovernorDatum (..), GovernorRedeemer (..)) where import Agora.Proposal (ProposalThresholds) diff --git a/agora/PPrelude.hs b/agora/PPrelude.hs index 8fba4be..3232cf9 100644 --- a/agora/PPrelude.hs +++ b/agora/PPrelude.hs @@ -11,7 +11,8 @@ module PPrelude ( module Plutarch, ) where --- These are not exported by Plutarch.Prelude, for some reason. Maybe we can 'fix' this upstream? -import Plutarch (ClosedTerm, compile) +-- NOTE: These are not exported by Plutarch.Prelude, for some reason. +-- Maybe we can 'fix' this upstream? +import Plutarch (ClosedTerm, POpaque, compile) import Plutarch.Prelude import Prelude From 43f3b5c62a3e4a39133a20d0b7c76ea8fac18a85 Mon Sep 17 00:00:00 2001 From: Emily Martins Date: Mon, 28 Mar 2022 13:34:09 +0200 Subject: [PATCH 08/12] add Plutarch versions of Proposal types for sanity check. --- agora/Agora/Proposal.hs | 91 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 4 deletions(-) diff --git a/agora/Agora/Proposal.hs b/agora/Agora/Proposal.hs index 8b7d263..c9f2daa 100644 --- a/agora/Agora/Proposal.hs +++ b/agora/Agora/Proposal.hs @@ -6,20 +6,38 @@ Description: Proposal scripts encoding effects that operate on the system. Proposal scripts encoding effects that operate on the system. -} module Agora.Proposal ( + -- * Haskell-land ProposalDatum (..), ProposalStatus (..), ProposalThresholds (..), ProposalVotes (..), ResultTag (..), + + -- * Plutarch-land + PProposalDatum (..), + PResultTag (..), ) where +import GHC.Generics qualified as GHC +import Generics.SOP (Generic, I (I)) +import Plutarch.Api.V1 ( + PDatumHash, + PMap, + PPubKeyHash, + PValidatorHash, + ) +import Plutarch.DataRepr ( + PDataFields, + PIsDataReprInstances (PIsDataReprInstances), + ) import Plutus.V1.Ledger.Api (DatumHash, PubKeyHash, ValidatorHash) -------------------------------------------------------------------------------- -import Agora.SafeMoney (Discrete, GTTag) +import Agora.SafeMoney (Discrete, GTTag, PDiscrete) -------------------------------------------------------------------------------- +-- Haskell-land {- | Encodes a result. Typically, for a Yes/No proposal, we encode it like this: @@ -99,11 +117,76 @@ data ProposalDatum = ProposalDatum -- ^ Effect lookup table. First by result, then by , status :: ProposalStatus -- ^ The status the proposal is in. - , proposers :: [PubKeyHash] - -- ^ Who created the proposal initially. - -- We may want to remove this. + , cosigners :: [PubKeyHash] + -- ^ Who created the proposal initially + who cosigned. , thresholds :: ProposalThresholds -- ^ Thresholds copied over on initialization. , votes :: ProposalVotes -- ^ Vote tally on the proposal } + +-------------------------------------------------------------------------------- +-- Plutarch-land + +-- | Plutarch-level version of 'ResultTag'. +newtype PResultTag (s :: S) = PResultTag (Term s PInteger) + deriving (PlutusType, PIsData, PEq, POrd) via (DerivePNewtype PResultTag PInteger) + +-- | Plutarch-level version of 'ProposalStatus'. +data PProposalStatus (s :: S) + = -- TODO: 'PProposalStatus' ought te be encoded as 'PInteger'. + -- e.g. like Tilde used 'pmatchEnum'. + PDraft (Term s (PDataRecord '[])) + | PVotingReady (Term s (PDataRecord '[])) + | PFinished (Term s (PDataRecord '[])) + deriving stock (GHC.Generic) + deriving anyclass (Generic) + deriving anyclass (PIsDataRepr) + deriving + (PlutusType, PIsData) + via PIsDataReprInstances PProposalStatus + +-- | Plutarch-level version of 'ProposalThresholds'. +data PProposalThresholds (s :: S) = PProposalThresholds + { getProposalThresholds :: + Term + s + ( PDataRecord + '[ "execute" ':= PDiscrete GTTag + , "draft" ':= PDiscrete GTTag + , "vote" ':= PDiscrete GTTag + ] + ) + } + deriving stock (GHC.Generic) + deriving anyclass (Generic) + deriving anyclass (PIsDataRepr) + deriving + (PlutusType, PIsData, PDataFields) + via (PIsDataReprInstances PProposalThresholds) + +-- | Plutarch-level version of 'ProposalVotes'. +newtype PProposalVotes (s :: S) + = PProposalVotes (Term s (PMap PResultTag PInteger)) + deriving (PlutusType, PIsData) via (DerivePNewtype PProposalVotes (PMap PResultTag PInteger)) + +-- | Plutarch-level version of 'ProposalDatum'. +newtype PProposalDatum (s :: S) = PProposalDatum + { getProposalDatum :: + Term + s + ( PDataRecord + '[ "effects" ':= PMap PResultTag (PMap PValidatorHash PDatumHash) + , "status" ':= PProposalStatus + , "cosigners" ':= PBuiltinList PPubKeyHash + , "thresholds" ':= PProposalThresholds + , "votes" ':= PProposalVotes + ] + ) + } + deriving stock (GHC.Generic) + deriving anyclass (Generic) + deriving anyclass (PIsDataRepr) + deriving + (PlutusType, PIsData, PDataFields) + via (PIsDataReprInstances PProposalDatum) From 8d50857dfdf4fcf05e6ce13f060371e5c695554d Mon Sep 17 00:00:00 2001 From: Emily Martins Date: Mon, 28 Mar 2022 13:51:30 +0200 Subject: [PATCH 09/12] add IsData instances to ProposalTypes --- agora/Agora/Proposal.hs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/agora/Agora/Proposal.hs b/agora/Agora/Proposal.hs index c9f2daa..596a169 100644 --- a/agora/Agora/Proposal.hs +++ b/agora/Agora/Proposal.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE TemplateHaskell #-} + {- | Module : Agora.Proposal Maintainer : emi@haskell.fyi @@ -31,6 +33,7 @@ import Plutarch.DataRepr ( PIsDataReprInstances (PIsDataReprInstances), ) import Plutus.V1.Ledger.Api (DatumHash, PubKeyHash, ValidatorHash) +import PlutusTx qualified -------------------------------------------------------------------------------- @@ -47,6 +50,7 @@ import Agora.SafeMoney (Discrete, GTTag, PDiscrete) @ -} newtype ResultTag = ResultTag {getResultTag :: Integer} + deriving newtype (PlutusTx.ToData, PlutusTx.FromData, PlutusTx.UnsafeFromData) {- | The "status" of the proposal. This is only useful for state transitions, as opposed to time-based "phases". @@ -79,6 +83,8 @@ data ProposalStatus -- TODO: The owner of the proposal may choose to reclaim their proposal. Finished +PlutusTx.makeIsDataIndexed ''ProposalStatus [('Draft, 0), ('VotingReady, 1), ('Finished, 2)] + {- | The threshold values for various state transitions to happen. This data is stored centrally (in the Governor) and copied over to Proposals when they are created. @@ -93,6 +99,8 @@ data ProposalThresholds = ProposalThresholds -- (i.e. to move into 'VotingReady') } +PlutusTx.makeIsDataIndexed ''ProposalThresholds [('ProposalThresholds, 0)] + {- | Map which encodes the total tally for each result. It's important that the 'shape' is consistent with the shape of 'effects'. @@ -107,6 +115,7 @@ data ProposalThresholds = ProposalThresholds newtype ProposalVotes = ProposalVotes { getProposalVotes :: [(ResultTag, Integer)] } + deriving newtype (PlutusTx.ToData, PlutusTx.FromData, PlutusTx.UnsafeFromData) -- | Haskell-level datum for Proposal scripts. data ProposalDatum = ProposalDatum @@ -125,6 +134,8 @@ data ProposalDatum = ProposalDatum -- ^ Vote tally on the proposal } +PlutusTx.makeIsDataIndexed ''ProposalDatum [('ProposalDatum, 0)] + -------------------------------------------------------------------------------- -- Plutarch-land From cf7f8a67923f563d9bed461341ef2f590062460d Mon Sep 17 00:00:00 2001 From: Emily Martins Date: Mon, 28 Mar 2022 13:55:47 +0200 Subject: [PATCH 10/12] fix `hie.yaml` path for agora main library --- hie.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hie.yaml b/hie.yaml index e1be10a..6020af6 100644 --- a/hie.yaml +++ b/hie.yaml @@ -1,6 +1,6 @@ cradle: cabal: - - path: "./agora-src" + - path: "./agora" component: "lib:agora" - path: "./agora-bench" component: "benchmark:agora-bench" From 107db1303dc8cb2cec7f093095846dde68f57daa Mon Sep 17 00:00:00 2001 From: Emily Martins Date: Mon, 28 Mar 2022 14:59:53 +0200 Subject: [PATCH 11/12] fix docs, hlint errors --- agora-test/Spec.hs | 2 +- agora-test/Spec/Model/MultiSig.hs | 4 ++-- agora-test/Spec/Stake.hs | 2 +- agora/Agora/Effect.hs | 2 +- agora/Agora/Governor.hs | 5 +++-- agora/Agora/MultiSig.hs | 4 ++-- agora/Agora/Proposal.hs | 9 +++++++-- agora/Agora/SafeMoney.hs | 5 +++-- agora/Agora/Utils.hs | 8 ++++---- 9 files changed, 24 insertions(+), 17 deletions(-) diff --git a/agora-test/Spec.hs b/agora-test/Spec.hs index 502cb27..6442ae8 100644 --- a/agora-test/Spec.hs +++ b/agora-test/Spec.hs @@ -11,7 +11,7 @@ import Test.Tasty (defaultMain, testGroup) import Spec.Model.MultiSig qualified as MultiSig import Spec.Stake qualified as Stake --- | The Agora test suite +-- | The Agora test suite. main :: IO () main = defaultMain $ diff --git a/agora-test/Spec/Model/MultiSig.hs b/agora-test/Spec/Model/MultiSig.hs index 15dcfae..47dfda0 100644 --- a/agora-test/Spec/Model/MultiSig.hs +++ b/agora-test/Spec/Model/MultiSig.hs @@ -171,7 +171,7 @@ instance HasScriptRunner MultiSigProp MultiSigModel where (pcon PUnit) perror --- | Consistency tests for the 'HasParameterisedGenerator' instance of 'MultiSigModel' +-- | Consistency tests for the 'HasParameterisedGenerator' instance of 'MultiSigModel'. genTests :: TestTree genTests = testGroup "genTests" $ @@ -182,7 +182,7 @@ genTests = Yes ] --- | Tests for the 'HasScriptRunner' instance of 'MultiSigModel' +-- | Tests for the 'HasScriptRunner' instance of 'MultiSigModel'. plutarchTests :: TestTree plutarchTests = testGroup "plutarchTests" $ diff --git a/agora-test/Spec/Stake.hs b/agora-test/Spec/Stake.hs index 5ce5d79..ccd16e7 100644 --- a/agora-test/Spec/Stake.hs +++ b/agora-test/Spec/Stake.hs @@ -29,7 +29,7 @@ import Spec.Util (policyFailsWith, policySucceedsWith, toDatum, validatorFailsWi -------------------------------------------------------------------------------- --- | Stake tests +-- | Stake tests. tests :: [TestTree] tests = [ testGroup diff --git a/agora/Agora/Effect.hs b/agora/Agora/Effect.hs index fccb32e..82764d2 100644 --- a/agora/Agora/Effect.hs +++ b/agora/Agora/Effect.hs @@ -13,7 +13,7 @@ import Plutarch.Monadic qualified as P -------------------------------------------------------------------------------- --- | Helper 'template' for creating effect validator. +-- | Helper "template" for creating effect validator. makeEffect :: forall (datum :: PType) (s :: S). PIsData datum => diff --git a/agora/Agora/Governor.hs b/agora/Agora/Governor.hs index f44eda1..55b480e 100644 --- a/agora/Agora/Governor.hs +++ b/agora/Agora/Governor.hs @@ -9,9 +9,10 @@ module Agora.Governor (GovernorDatum (..), GovernorRedeemer (..)) where import Agora.Proposal (ProposalThresholds) -data GovernorDatum = GovernorDatum +-- | Datum for the Governor script. +newtype GovernorDatum = GovernorDatum { proposalThresholds :: ProposalThresholds - -- ^ Gets copied over upon creation of a 'Proposal'. + -- ^ Gets copied over upon creation of a 'Agora.Proposal.ProposalDatum'. } {- | Redeemer for Governor script. diff --git a/agora/Agora/MultiSig.hs b/agora/Agora/MultiSig.hs index 6e8270d..93cf3e6 100644 --- a/agora/Agora/MultiSig.hs +++ b/agora/Agora/MultiSig.hs @@ -77,13 +77,13 @@ deriving via (DerivePConstantViaData MultiSig PMultiSig) instance (PConstant Mul -------------------------------------------------------------------------------- --- | Check if a Haskell-level MultiSig signs this transaction +-- | Check if a Haskell-level MultiSig signs this transaction. validatedByMultisig :: MultiSig -> Term s (PTxInfo :--> PBool) validatedByMultisig params = phoistAcyclic $ pvalidatedByMultisig # pconstant params --- | Check if a Plutarch-level MultiSig signs this transaction +-- | Check if a Plutarch-level MultiSig signs this transaction. pvalidatedByMultisig :: Term s (PMultiSig :--> PTxInfo :--> PBool) pvalidatedByMultisig = phoistAcyclic $ diff --git a/agora/Agora/Proposal.hs b/agora/Agora/Proposal.hs index 596a169..0584b99 100644 --- a/agora/Agora/Proposal.hs +++ b/agora/Agora/Proposal.hs @@ -9,6 +9,7 @@ Proposal scripts encoding effects that operate on the system. -} module Agora.Proposal ( -- * Haskell-land + Proposal (..), ProposalDatum (..), ProposalStatus (..), ProposalThresholds (..), @@ -50,6 +51,7 @@ import Agora.SafeMoney (Discrete, GTTag, PDiscrete) @ -} newtype ResultTag = ResultTag {getResultTag :: Integer} + deriving stock (Eq, Show) deriving newtype (PlutusTx.ToData, PlutusTx.FromData, PlutusTx.UnsafeFromData) {- | The "status" of the proposal. This is only useful for state transitions, @@ -102,7 +104,7 @@ data ProposalThresholds = ProposalThresholds PlutusTx.makeIsDataIndexed ''ProposalThresholds [('ProposalThresholds, 0)] {- | Map which encodes the total tally for each result. - It's important that the 'shape' is consistent with the shape of 'effects'. + It's important that the "shape" is consistent with the shape of 'effects'. e.g. if the 'effects' field looks like the following: @@ -136,6 +138,9 @@ data ProposalDatum = ProposalDatum PlutusTx.makeIsDataIndexed ''ProposalDatum [('ProposalDatum, 0)] +-- | Parameters that identify the Proposal validator script. +data Proposal = Proposal + -------------------------------------------------------------------------------- -- Plutarch-land @@ -158,7 +163,7 @@ data PProposalStatus (s :: S) via PIsDataReprInstances PProposalStatus -- | Plutarch-level version of 'ProposalThresholds'. -data PProposalThresholds (s :: S) = PProposalThresholds +newtype PProposalThresholds (s :: S) = PProposalThresholds { getProposalThresholds :: Term s diff --git a/agora/Agora/SafeMoney.hs b/agora/Agora/SafeMoney.hs index 0862cef..d8c3da0 100644 --- a/agora/Agora/SafeMoney.hs +++ b/agora/Agora/SafeMoney.hs @@ -50,10 +50,10 @@ import Agora.Utils ( -------------------------------------------------------------------------------- -- Example tags --- | Governance token +-- | Governance token. data GTTag --- | ADA +-- | ADA. data ADATag -------------------------------------------------------------------------------- @@ -136,6 +136,7 @@ pdiscreteValue (AssetClassRef (AssetClass (cs, tn))) = phoistAcyclic $ # pconstant tn # p +-- | Get a `Value` from a `Discrete`. discreteValue :: forall (tag :: Type). AssetClassRef tag -> diff --git a/agora/Agora/Utils.hs b/agora/Agora/Utils.hs index 9c5224a..2f875b0 100644 --- a/agora/Agora/Utils.hs +++ b/agora/Agora/Utils.hs @@ -183,21 +183,21 @@ passetClassValueOf' :: AssetClass -> Term s (PValue :--> PInteger) passetClassValueOf' (AssetClass (sym, token)) = passetClassValueOf # pconstant sym # pconstant token --- | Return '>=' on two values comparing by only a particular AssetClass +-- | Return '>=' on two values comparing by only a particular AssetClass. pgeqByClass :: Term s (PCurrencySymbol :--> PTokenName :--> PValue :--> PValue :--> PBool) pgeqByClass = phoistAcyclic $ plam $ \cs tn a b -> passetClassValueOf # cs # tn # b #<= passetClassValueOf # cs # tn # a --- | Return '>=' on two values comparing by only a particular CurrencySymbol +-- | Return '>=' on two values comparing by only a particular CurrencySymbol. pgeqBySymbol :: Term s (PCurrencySymbol :--> PValue :--> PValue :--> PBool) pgeqBySymbol = phoistAcyclic $ plam $ \cs a b -> psymbolValueOf # cs # b #<= psymbolValueOf # cs # a --- | Return '>=' on two values comparing by only a particular Haskell-level AssetClass +-- | Return '>=' on two values comparing by only a particular Haskell-level AssetClass. pgeqByClass' :: AssetClass -> Term s (PValue :--> PValue :--> PBool) pgeqByClass' ac = phoistAcyclic $ @@ -233,7 +233,7 @@ pmapUnionWith = phoistAcyclic $ # ys pcon (PMap $ pconcat # ls # rs) --- | Add two 'PValue's together +-- | Add two 'PValue's together. paddValue :: forall s. Term s (PValue :--> PValue :--> PValue) paddValue = phoistAcyclic $ plam $ \a' b' -> P.do From 522051657c0d38988871ac0e6890cc54c35c8643 Mon Sep 17 00:00:00 2001 From: Emily Martins Date: Mon, 28 Mar 2022 18:01:27 +0200 Subject: [PATCH 12/12] apply docs suggestions --- agora.cabal | 1 + agora/Agora/Governor.hs | 14 +++++++++----- agora/Agora/Proposal.hs | 12 ++++++------ agora/Agora/Stake.hs | 2 +- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/agora.cabal b/agora.cabal index c41b6af..3afdca4 100644 --- a/agora.cabal +++ b/agora.cabal @@ -96,6 +96,7 @@ common deps , generics-sop , plutarch , plutarch-extra + , plutarch-numeric , plutus-core , plutus-ledger-api , plutus-tx diff --git a/agora/Agora/Governor.hs b/agora/Agora/Governor.hs index 55b480e..33584e1 100644 --- a/agora/Agora/Governor.hs +++ b/agora/Agora/Governor.hs @@ -5,7 +5,7 @@ Description: Governor entity scripts acting as authority of entire system. Governor entity scripts acting as authority of entire system. -} -module Agora.Governor (GovernorDatum (..), GovernorRedeemer (..)) where +module Agora.Governor (GovernorDatum (..), GovernorRedeemer (..), Governor (..)) where import Agora.Proposal (ProposalThresholds) @@ -15,11 +15,11 @@ newtype GovernorDatum = GovernorDatum -- ^ Gets copied over upon creation of a 'Agora.Proposal.ProposalDatum'. } -{- | Redeemer for Governor script. +{- | Redeemer for Governor script. The governor has two primary + responsibilities: - The governor has two primary responsibilities: - - The gating of Proposal creation - - The gating of minting authority tokens + 1. The gating of Proposal creation. + 2. The gating of minting authority tokens. -} data GovernorRedeemer = -- | Checks that a proposal was created lawfully, and allows it. @@ -27,3 +27,7 @@ data GovernorRedeemer | -- | Checks that a SINGLE proposal finished correctly, -- and allows minting GATs for each effect script. MintGATs + +-- | Parameters for creating Governor scripts. +data Governor + = Governor diff --git a/agora/Agora/Proposal.hs b/agora/Agora/Proposal.hs index 0584b99..ddad144 100644 --- a/agora/Agora/Proposal.hs +++ b/agora/Agora/Proposal.hs @@ -88,12 +88,12 @@ data ProposalStatus PlutusTx.makeIsDataIndexed ''ProposalStatus [('Draft, 0), ('VotingReady, 1), ('Finished, 2)] {- | The threshold values for various state transitions to happen. - This data is stored centrally (in the Governor) and copied over - to Proposals when they are created. + This data is stored centrally (in the 'Agora.Governor.Governor') and copied over + to 'Proposal's when they are created. -} data ProposalThresholds = ProposalThresholds { execute :: Discrete GTTag - -- ^ How much GT minimum must a particular 'ResultTag' accumulate to fulfil. + -- ^ How much GT minimum must a particular 'ResultTag' accumulate for it to pass. , draft :: Discrete GTTag -- ^ How much GT required to "create" a proposal. , vote :: Discrete GTTag @@ -110,7 +110,7 @@ PlutusTx.makeIsDataIndexed ''ProposalThresholds [('ProposalThresholds, 0)] @[('ResultTag' 0, []), ('ResultTag' 1, [(vh, dh)])]@ - Then 'ProposalVotes' need be of the shape: + Then 'ProposalVotes' needs be of the shape: @[('ResultTag' 0, n), ('ResultTag' 1, m)]@ -} @@ -125,11 +125,11 @@ data ProposalDatum = ProposalDatum -- This is shaped this way for future proofing. -- See https://github.com/Liqwid-Labs/agora/issues/39 effects :: [(ResultTag, [(ValidatorHash, DatumHash)])] - -- ^ Effect lookup table. First by result, then by + -- ^ Effect lookup table. First by result, then by effect hash. , status :: ProposalStatus -- ^ The status the proposal is in. , cosigners :: [PubKeyHash] - -- ^ Who created the proposal initially + who cosigned. + -- ^ Who created the proposal initially, and who cosigned it later. , thresholds :: ProposalThresholds -- ^ Thresholds copied over on initialization. , votes :: ProposalVotes diff --git a/agora/Agora/Stake.hs b/agora/Agora/Stake.hs index 8d3296e..67184bf 100644 --- a/agora/Agora/Stake.hs +++ b/agora/Agora/Stake.hs @@ -83,7 +83,7 @@ import Agora.Utils ( -- | Parameters for creating Stake scripts. newtype Stake = Stake { gtClassRef :: AssetClassRef GTTag - -- ^ Resolve governance token + -- ^ Used when inlining the AssetClass of a 'PDiscrete' in the script code. } -- | Plutarch-level redeemer for Stake scripts.