{- | Module : Agora.Stake Maintainer : emi@haskell.fyi Description: Vote-lockable stake UTXOs holding GT. Vote-lockable stake UTXOs holding GT. -} module Agora.Stake ( PStakeDatum (..), PStakeAction (..), Stake (..), stakePolicy, stakeValidator, stakeLocked, ) where -------------------------------------------------------------------------------- import GHC.Generics qualified as GHC import GHC.TypeLits ( KnownSymbol, ) import Generics.SOP (Generic, I (I)) import Prelude -------------------------------------------------------------------------------- import Plutarch (popaque) import Plutarch.Api.V1 ( PCredential (PPubKeyCredential, PScriptCredential), PMintingPolicy, PPubKeyHash, PScriptPurpose (PMinting, PSpending), PValidator, mintingPolicySymbol, mkMintingPolicy, ) import Plutarch.DataRepr ( PDataFields, PIsDataReprInstances (PIsDataReprInstances), ) import Plutarch.Internal (punsafeCoerce) import Plutarch.Monadic qualified as P -------------------------------------------------------------------------------- import Agora.SafeMoney ( MoneyClass, PDiscrete, paddDiscrete, pdiscreteValue, ) import Agora.Utils ( anyInput, anyOutput, paddValue, passert, pfindTxInByTxOutRef, psingletonValue, psymbolValueOf, ptxSignedBy, pvalueSpent, ) -------------------------------------------------------------------------------- -- | Parameters for creating Stake scripts. data Stake (gt :: MoneyClass) = Stake -- | Plutarch-level redeemer for Stake scripts. data PStakeAction (gt :: MoneyClass) (s :: S) = -- | Deposit or withdraw a discrete amount of the staked governance token. PDepositWithdraw (Term s (PDataRecord '["delta" ':= PDiscrete gt])) | -- | Destroy a stake, retrieving its LQ, the minimum ADA and any other assets. PDestroy (Term s (PDataRecord '[])) deriving stock (GHC.Generic) deriving anyclass (Generic) deriving anyclass (PIsDataRepr) deriving (PlutusType, PIsData) via PIsDataReprInstances (PStakeAction gt) -- | Plutarch-level datum for Stake scripts. newtype PStakeDatum (gt :: MoneyClass) (s :: S) = PStakeDatum { getStakeDatum :: Term s (PDataRecord '["stakedAmount" ':= PDiscrete gt, "owner" ':= PPubKeyHash]) } deriving stock (GHC.Generic) deriving anyclass (Generic) deriving anyclass (PIsDataRepr) deriving (PlutusType, PIsData, PDataFields) via (PIsDataReprInstances (PStakeDatum gt)) -------------------------------------------------------------------------------- {- What this Policy does For minting: Check that exactly one state thread is minted Check that an output exists with a state thread and a valid datum Check that no state thread is an input assert TokenName == ValidatorHash of the script that we pay to For burning: Check that exactly one state thread is burned Check that datum at state thread is valid and not locked -} -------------------------------------------------------------------------------- -- | Policy for Stake state threads. stakePolicy :: forall (gt :: MoneyClass) ac n scale s. ( KnownSymbol ac , KnownSymbol n , gt ~ '(ac, n, scale) ) => Stake gt -> Term s PMintingPolicy stakePolicy _stake = plam $ \_redeemer ctx' -> P.do ctx <- pletFields @'["txInfo", "purpose"] ctx' txInfo' <- plet ctx.txInfo txInfo <- pletFields @'["mint", "inputs", "outputs"] txInfo' PMinting ownSymbol' <- pmatch $ pfromData ctx.purpose ownSymbol <- plet $ pfield @"_0" # ownSymbol' spentST <- plet $ psymbolValueOf # ownSymbol #$ pvalueSpent # pfromData txInfo' mintedST <- plet $ psymbolValueOf # ownSymbol # txInfo.mint let burning = P.do passert "ST at inputs must be 1" $ spentST #== 1 passert "ST burned" $ mintedST #== -1 passert "An unlocked input existed containing an ST" $ anyInput @(PStakeDatum gt) # pfromData txInfo' #$ plam $ \value _ stakeDatum' -> P.do let hasST = psymbolValueOf # ownSymbol # value #== 1 let unlocked = pnot # (stakeLocked # stakeDatum') hasST #&& unlocked popaque (pconstant ()) let minting = P.do passert "ST at inputs must be 0" $ spentST #== 0 passert "Minted ST must be exactly 1" $ mintedST #== 1 passert "A UTXO must exist with the correct output" $ anyOutput @(PStakeDatum gt) # pfromData txInfo' #$ plam $ \value address stakeDatum' -> P.do let cred = pfield @"credential" # address pmatch cred $ \case -- Should pay to a script address PPubKeyCredential _ -> pcon PFalse PScriptCredential validatorHash' -> P.do validatorHash <- pletFields @'["_0"] validatorHash' stakeDatum <- pletFields @'["owner", "stakedAmount"] stakeDatum' let stValue = psingletonValue # ownSymbol -- This coerce is safe because the structure -- of PValidatorHash is the same as PTokenName. # punsafeCoerce validatorHash._0 # 1 let expectedValue = paddValue # (pdiscreteValue # stakeDatum.stakedAmount) # stValue let ownerSignsTransaction = ptxSignedBy # ctx.txInfo # stakeDatum.owner -- TODO: Needs to be >=, rather than == let valueCorrect = pdata value #== pdata expectedValue ownerSignsTransaction #&& valueCorrect popaque (pconstant ()) pif (0 #< mintedST) minting burning -------------------------------------------------------------------------------- -- | 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 -> Term s PValidator stakeValidator stake = plam $ \datum redeemer ctx' -> P.do ctx <- pletFields @'["txInfo", "purpose"] ctx' txInfo' <- plet ctx.txInfo txInfo <- pletFields @'["mint", "inputs", "outputs"] txInfo' -- Coercion is safe in that if coercion fails we crash hard. let stakeAction :: Term _ (PStakeAction gt) stakeAction = pfromData $ punsafeCoerce redeemer stakeDatum' :: Term _ (PStakeDatum gt) stakeDatum' = pfromData $ punsafeCoerce datum stakeDatum <- pletFields @'["owner", "stakedAmount"] stakeDatum' PSpending txOutRef <- pmatch $ pfromData ctx.purpose PJust txInInfo <- pmatch $ pfindTxInByTxOutRef # (pfield @"_0" # txOutRef) # txInfo' ownAddress <- plet $ pfield @"address" #$ pfield @"resolved" # txInInfo let continuingValue = pfield @"value" #$ pfield @"resolved" # txInInfo stCurrencySymbol <- plet $ pconstant $ mintingPolicySymbol $ mkMintingPolicy (stakePolicy stake) mintedST <- plet $ psymbolValueOf # stCurrencySymbol # txInfo.mint spentST <- plet $ psymbolValueOf # stCurrencySymbol #$ pvalueSpent # txInfo' pmatch stakeAction $ \case PDestroy _ -> P.do passert "ST at inputs must be 1" $ spentST #== 1 passert "Should burn ST" $ mintedST #== -1 passert "Stake unlocked" $ pnot #$ stakeLocked # stakeDatum' popaque (pconstant ()) PDepositWithdraw r -> P.do passert "ST at inputs must be 1" $ spentST #== 1 passert "Stake unlocked" $ pnot #$ stakeLocked # stakeDatum' passert "A UTXO must exist with the correct output" $ anyOutput @(PStakeDatum gt) # txInfo' #$ plam $ \value address newStakeDatum' -> P.do newStakeDatum <- pletFields @'["owner", "stakedAmount"] newStakeDatum' delta <- plet $ pfield @"delta" # r let isScriptAddress = pdata address #== ownAddress let correctOutputDatum = stakeDatum.owner #== newStakeDatum.owner #&& (paddDiscrete # stakeDatum.stakedAmount # delta) #== newStakeDatum.stakedAmount let expectedValue = paddValue # continuingValue # (pdiscreteValue # delta) -- TODO: As above, needs to be >=, rather than == let correctValue = pdata value #== pdata expectedValue isScriptAddress #&& correctOutputDatum #&& correctValue popaque (pconstant ()) -------------------------------------------------------------------------------- -- | 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 = phoistAcyclic $ plam $ \_stakeDatum -> -- TODO: when we extend this to support proposals, this will need to do something pcon PFalse