allow spending more than one stakes in one tx

This commit is contained in:
Hongrui Fang 2022-09-23 14:41:58 +08:00
parent 8e91a7ef2d
commit 1bc60a48e5
6 changed files with 397 additions and 325 deletions

View file

@ -138,7 +138,6 @@ stakeDepositWithdraw config =
mconcat mconcat
[ txId "0b2086cbf8b6900f8cb65e012de4516cb66b5cb08a9aaba12a8b88be" [ txId "0b2086cbf8b6900f8cb65e012de4516cb66b5cb08a9aaba12a8b88be"
, signedWith signer , signedWith signer
, mint st
, input $ , input $
mconcat mconcat
[ script stakeValidatorHash [ script stakeValidatorHash
@ -147,7 +146,7 @@ stakeDepositWithdraw config =
st st
<> Value.assetClassValue (untag governor.gtClassRef) (fromDiscrete stakeBefore.stakedAmount) <> Value.assetClassValue (untag governor.gtClassRef) (fromDiscrete stakeBefore.stakedAmount)
) )
, withDatum stakeAfter , withDatum stakeBefore
, withRef stakeRef , withRef stakeRef
] ]
, output $ , output $

View file

@ -21,8 +21,7 @@ module Agora.Stake (
PStakeRole (..), PStakeRole (..),
-- * Validation context -- * Validation context
PStakeInputContext (..), PSignedBy (..),
PStakeOutputContext (..),
PSigContext (..), PSigContext (..),
PStakeRedeemerContext (..), PStakeRedeemerContext (..),
PStakeRedeemerHandlerContext (..), PStakeRedeemerHandlerContext (..),
@ -43,14 +42,18 @@ module Agora.Stake (
runStakeRedeemerHandler, runStakeRedeemerHandler,
) where ) where
import Agora.Proposal (PProposalId, PProposalRedeemer, PResultTag, ProposalId, ResultTag) import Agora.Proposal (
PProposalId,
PProposalRedeemer,
PResultTag,
ProposalId,
ResultTag,
)
import Agora.SafeMoney (GTTag) import Agora.SafeMoney (GTTag)
import Data.Tagged (Tagged) import Data.Tagged (Tagged)
import Generics.SOP qualified as SOP import Generics.SOP qualified as SOP
import Plutarch.Api.V1 (KeyGuarantees (Sorted), PCredential) import Plutarch.Api.V1 (PCredential)
import Plutarch.Api.V1.Value (PValue)
import Plutarch.Api.V2 ( import Plutarch.Api.V2 (
AmountGuarantees (Positive),
PMaybeData, PMaybeData,
PTxInfo, PTxInfo,
) )
@ -58,7 +61,6 @@ import Plutarch.DataRepr (
DerivePConstantViaData (DerivePConstantViaData), DerivePConstantViaData (DerivePConstantViaData),
PDataFields, PDataFields,
) )
import Plutarch.Extra.AssetClass (PAssetClass)
import Plutarch.Extra.Field (pletAll) import Plutarch.Extra.Field (pletAll)
import Plutarch.Extra.IsData ( import Plutarch.Extra.IsData (
DerivePConstantViaDataList (DerivePConstantViaDataList), DerivePConstantViaDataList (DerivePConstantViaDataList),
@ -429,61 +431,11 @@ instance DerivePlutusType PStakeRole where
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
{- | Represent the stake being spent.
@since 1.0.0
-}
data PStakeInputContext (s :: S) = PStakeInput
{ ownInputDatum :: Term s PStakeDatum
-- ^ The stake datum of said stake.
, ownInputValue :: Term s (PValue 'Sorted 'Positive)
-- ^ The value carried by the stake UTxO.
}
deriving stock
( -- | @since 1.0.0
Generic
)
deriving anyclass
( -- | @since 1.0.0
PlutusType
)
-- | @since 1.0.0
instance DerivePlutusType PStakeInputContext where
type DPTStrat _ = PlutusTypeScott
{- | Where the stake will go?
@since 1.0.0
-}
data PStakeOutputContext (s :: S)
= -- | The output stake is owned by the stake validator.
PStakeOutput
{ ownOutputDatum :: Term s PStakeDatum
-- ^ The stake datum of the output stake.
, ownOutputValue :: Term s (PValue 'Sorted 'Positive)
-- ^ The value carried by the stake output UTxO.
}
| -- | The stake is burnt in the transaction.
PStakeBurnt
deriving stock
( -- | @since 1.0.0
Generic
)
deriving anyclass
( -- | @since 1.0.0
PlutusType
)
-- | @since 1.0.0
instance DerivePlutusType PStakeOutputContext where
type DPTStrat _ = PlutusTypeScott
{- | Who authorizes the transaction? {- | Who authorizes the transaction?
@since 1.0.0 @since 1.0.0
-} -}
data PSigContext (s :: S) data PSignedBy (s :: S)
= -- | The stake owner authorized the transaction. = -- | The stake owner authorized the transaction.
PSignedByOwner PSignedByOwner
| -- | The delegate authorized the transaction. | -- | The delegate authorized the transaction.
@ -499,6 +451,25 @@ data PSigContext (s :: S)
PlutusType PlutusType
) )
-- | @since 1.0.0
instance DerivePlutusType PSignedBy where
type DPTStrat _ = PlutusTypeScott
-- | @since 1.0.0
data PSigContext (s :: S) = PSigContext
{ owner :: Term s PCredential
, delegate :: Term s (PMaybeData (PAsData PCredential))
, signedBy :: Term s PSignedBy
}
deriving stock
( -- | @since 1.0.0
Generic
)
deriving anyclass
( -- | @since 1.0.0
PlutusType
)
-- | @since 1.0.0 -- | @since 1.0.0
instance DerivePlutusType PSigContext where instance DerivePlutusType PSigContext where
type DPTStrat _ = PlutusTypeScott type DPTStrat _ = PlutusTypeScott
@ -555,12 +526,11 @@ instance DerivePlutusType PProposalContext where
@1.0.0 @1.0.0
-} -}
data PStakeRedeemerHandlerContext (s :: S) = PStakeRedeemerHandlerContext data PStakeRedeemerHandlerContext (s :: S) = PStakeRedeemerHandlerContext
{ stakeInput :: Term s PStakeInputContext { stakeInputDatums :: Term s (PBuiltinList PStakeDatum)
, stakeOutput :: Term s PStakeOutputContext , stakeOutputDatums :: Term s (PBuiltinList PStakeDatum)
, redeemerContext :: Term s PStakeRedeemerContext , redeemerContext :: Term s PStakeRedeemerContext
, sigContext :: Term s PSigContext , sigContext :: Term s PSigContext
, proposalContext :: Term s PProposalContext , proposalContext :: Term s PProposalContext
, gtAssetClass :: Term s PAssetClass
, extraTxContext :: Term s PTxInfo , extraTxContext :: Term s PTxInfo
} }
deriving stock deriving stock
@ -589,9 +559,13 @@ type PStakeRedeemerHandler = PStakeRedeemerHandlerContext :--> PUnit
@since 1.0.0 @since 1.0.0
-} -}
newtype PStakeRedeemerHandlerTerm = PStakeRedeemerHandlerTerm (ClosedTerm PStakeRedeemerHandler) newtype PStakeRedeemerHandlerTerm
= PStakeRedeemerHandlerTerm
(ClosedTerm PStakeRedeemerHandler)
runStakeRedeemerHandler :: PStakeRedeemerHandlerTerm -> ClosedTerm PStakeRedeemerHandler runStakeRedeemerHandler ::
PStakeRedeemerHandlerTerm ->
ClosedTerm PStakeRedeemerHandler
runStakeRedeemerHandler (PStakeRedeemerHandlerTerm t) = t runStakeRedeemerHandler (PStakeRedeemerHandlerTerm t) = t
{- | A collection of stake redeemer handlers for each stake redeemers. {- | A collection of stake redeemer handlers for each stake redeemers.
@ -666,7 +640,14 @@ pisIrrelevant = phoistAcyclic $
@since 0.2.0 @since 0.2.0
-} -}
pgetStakeRole :: forall (s :: S). Term s (PProposalId :--> PBuiltinList (PAsData PProposalLock) :--> PStakeRole) pgetStakeRole ::
forall (s :: S).
Term
s
( PProposalId
:--> PBuiltinList (PAsData PProposalLock)
:--> PStakeRole
)
pgetStakeRole = phoistAcyclic $ pgetStakeRole = phoistAcyclic $
plam $ \pid locks -> plam $ \pid locks ->
pfoldl pfoldl
@ -688,7 +669,14 @@ pgetStakeRole = phoistAcyclic $
# pcon PIrrelevant # pcon PIrrelevant
# locks # locks
where where
pcombineStakeRole :: forall (s :: S). Term s (PStakeRole :--> PStakeRole :--> PStakeRole) pcombineStakeRole ::
forall (s :: S).
Term
s
( PStakeRole
:--> PStakeRole
:--> PStakeRole
)
pcombineStakeRole = phoistAcyclic $ pcombineStakeRole = phoistAcyclic $
plam $ \x y -> plam $ \x y ->
let cannotCombine = ptraceError "duplicate roles" let cannotCombine = ptraceError "duplicate roles"

View file

@ -16,75 +16,96 @@ module Agora.Stake.Redeemers (
import Agora.Proposal (PProposalRedeemer (PUnlock, PVote)) import Agora.Proposal (PProposalRedeemer (PUnlock, PVote))
import Agora.Stake ( import Agora.Stake (
PProposalContext (PNewProposal, PWithProposalRedeemer), PProposalContext (
PSigContext (PSignedByOwner, PUnknownSig), PNewProposal,
PWithProposalRedeemer
),
PSigContext (owner, signedBy),
PSignedBy (
PSignedByDelegate,
PSignedByOwner,
PUnknownSig
),
PStakeDatum (PStakeDatum), PStakeDatum (PStakeDatum),
PStakeInputContext (PStakeInput), PStakeRedeemerContext (
PStakeOutputContext (PStakeBurnt, PStakeOutput), PDepositWithdrawDelta,
PStakeRedeemerContext (PDepositWithdrawDelta, PNoMetadata, PSetDelegateTo), PNoMetadata,
PSetDelegateTo
),
PStakeRedeemerHandler, PStakeRedeemerHandler,
PStakeRedeemerHandlerContext (..), PStakeRedeemerHandlerContext (
proposalContext,
redeemerContext,
sigContext,
stakeInputDatums,
stakeOutputDatums
),
pstakeLocked, pstakeLocked,
) )
import Agora.Utils (pdeleteBy, pfromSingleton)
import Plutarch.Api.V1.Address (PCredential) import Plutarch.Api.V1.Address (PCredential)
import Plutarch.Api.V1.Value (AmountGuarantees (Positive), PValue)
import Plutarch.Api.V2 (PMaybeData) import Plutarch.Api.V2 (PMaybeData)
import Plutarch.Extra.Field (pletAllC) import Plutarch.Extra.Field (pletAll, pletAllC)
import Plutarch.Extra.Maybe (pdjust, pdnothing, pmaybeData) import Plutarch.Extra.Maybe (pdjust, pdnothing, pmaybeData)
import Plutarch.Extra.Record (mkRecordConstr, (.&), (.=)) import Plutarch.Extra.Record (mkRecordConstr, (.&), (.=))
import "liqwid-plutarch-extra" Plutarch.Extra.TermCont (pguardC, pletC, pmatchC) import "liqwid-plutarch-extra" Plutarch.Extra.TermCont (pguardC, pletC, pmatchC)
import Plutarch.Extra.Value (pgeqByClass, pgeqByClass')
import Plutarch.Numeric.Additive (AdditiveMonoid (zero), AdditiveSemigroup ((+))) import Plutarch.Numeric.Additive (AdditiveMonoid (zero), AdditiveSemigroup ((+)))
import Plutarch.SafeMoney (pdiscreteValue)
import PlutusLedgerApi.V1.Value (AssetClass (..))
import Prelude hiding (Num ((+))) import Prelude hiding (Num ((+)))
-- | Return true if stake input and output carries the same value. pbatchUpdateInputs ::
pownOutputValueUnchanged ::
forall (s :: S). forall (s :: S).
Term s (PStakeRedeemerHandlerContext :--> PBool) Term
pownOutputValueUnchanged = phoistAcyclic $ s
plam $ ( (PStakeDatum :--> PStakeDatum :--> PBool)
flip pmatch $ \ctxF -> unTermCont $ do :--> PStakeRedeemerHandlerContext
PStakeInput _ inVal <- pmatchC ctxF.stakeInput :--> PBool
PStakeOutput _ outVal <- pmatchC ctxF.stakeOutput )
pbatchUpdateInputs = phoistAcyclic $
plam $ \f -> flip pmatch $ \ctxF ->
pnull #$ pfoldr
# (pdeleteBy # f)
# ctxF.stakeOutputDatums
# ctxF.stakeInputDatums
pure $ inVal #== outVal pgetSignedBy ::
forall (s :: S).
Term
s
(PStakeRedeemerHandlerContext :--> PSignedBy)
pgetSignedBy = phoistAcyclic $
plam $ \ctx -> unTermCont $ do
ctxF <- pmatchC ctx
sctxF <- pmatchC ctxF.sigContext
pure sctxF.signedBy
pisSignedBy ::
forall (s :: S).
Term
s
(PBool :--> PBool :--> PStakeRedeemerHandlerContext :--> PBool)
pisSignedBy = phoistAcyclic $
plam $ \byOwner byDelegate ctx ->
pmatch (pgetSignedBy # ctx) $ \case
PSignedByOwner -> byOwner
PSignedByDelegate -> byDelegate
PUnknownSig -> pconstant False
-- | Return true if only the @lockedBy@ field of the stake datum is updated. -- | Return true if only the @lockedBy@ field of the stake datum is updated.
ponlyLocksUpdated :: ponlyLocksUpdated ::
forall (s :: S). forall (s :: S).
Term s (PStakeRedeemerHandlerContext :--> PBool) Term s (PStakeRedeemerHandlerContext :--> PBool)
ponlyLocksUpdated = phoistAcyclic $ ponlyLocksUpdated = phoistAcyclic $
plam $ pbatchUpdateInputs #$ plam $ \i o ->
flip pmatch $ \ctxF -> unTermCont $ do pletAll i $ \iF ->
PStakeInput inDat _ <- pmatchC ctxF.stakeInput let newLocks = pfield @"lockedBy" # o
PStakeOutput outDat _ <- pmatchC ctxF.stakeOutput in mkRecordConstr
inDatF <- pletAllC inDat
let onlyLocksUpdated =
let templateStakeDatum =
mkRecordConstr
PStakeDatum PStakeDatum
( #stakedAmount .= inDatF.stakedAmount ( #stakedAmount .= iF.stakedAmount
.& #owner .= inDatF.owner .& #owner .= iF.owner
.& #delegatedTo .= inDatF.delegatedTo .& #delegatedTo .= iF.delegatedTo
.& #lockedBy .= pfield @"lockedBy" # outDat .& #lockedBy .= newLocks
) )
in outDat #== templateStakeDatum #== o
pure onlyLocksUpdated
-- | Return true if the transaction is signed by the owner of the stake.
psignedByOwner ::
forall (s :: S).
Term s (PStakeRedeemerHandlerContext :--> PBool)
psignedByOwner = phoistAcyclic $
plam $
flip pmatch $ \ctxF -> pmatch ctxF.sigContext $ \case
PSignedByOwner -> pconstant True
_ -> pconstant False
-- | Validation logic shared between 'ppermitVote' and 'retractVote'. -- | Validation logic shared between 'ppermitVote' and 'retractVote'.
pvoteHelper :: pvoteHelper ::
@ -99,9 +120,7 @@ pvoteHelper = phoistAcyclic $
ctxF <- pmatchC ctx ctxF <- pmatchC ctx
pguardC "Owner or delegate signs this transaction" $ pguardC "Owner or delegate signs this transaction" $
pmatch ctxF.sigContext $ \case pisSignedBy # pconstant True # pconstant True # ctx
PUnknownSig -> pconstant False
_ -> pconstant True
-- This puts trust into the Proposal. The Proposal must necessarily check -- This puts trust into the Proposal. The Proposal must necessarily check
-- that this is not abused. -- that this is not abused.
@ -109,14 +128,8 @@ pvoteHelper = phoistAcyclic $
pguardC "Proposal ST spent" $ pguardC "Proposal ST spent" $
valProposalCtx # ctxF.proposalContext valProposalCtx # ctxF.proposalContext
pguardC "A UTXO must exist with the correct output" $ pguardC "Correct outputs" $
let valueCorrect = pownOutputValueUnchanged # ctx ponlyLocksUpdated # ctx
outputDatumCorrect = ponlyLocksUpdated # ctx
in foldl1
(#&&)
[ ptraceIfFalse "valueCorrect" valueCorrect
, ptraceIfFalse "datumCorrect" outputDatumCorrect
]
pure $ pconstant () pure $ pconstant ()
@ -158,38 +171,33 @@ pdelegateHelper ::
pdelegateHelper = phoistAcyclic $ pdelegateHelper = phoistAcyclic $
plam $ \f ctx -> unTermCont $ do plam $ \f ctx -> unTermCont $ do
ctxF <- pmatchC ctx ctxF <- pmatchC ctx
sigCtxF <- pmatchC ctxF.sigContext
pguardC "Owner signs this transaction" $ psignedByOwner # ctx pguardC "Owner signs this transaction" $
pisSignedBy # pconstant True # pconstant False # ctx
PStakeInput inpDat _ <- pmatchC ctxF.stakeInput let newDelegate = f # ctxF.redeemerContext
PStakeOutput outDat _ <- pmatchC ctxF.stakeOutput
inpDatF <- pletAllC inpDat
let maybePkh = f # ctxF.redeemerContext
pguardC "Cannot delegate to the owner" $ pguardC "Cannot delegate to the owner" $
pmaybeData pmaybeData
# pcon PTrue # pcon PTrue
# plam (\pkh -> pnot #$ inpDatF.owner #== pkh) # plam (\pkh -> pnot #$ sigCtxF.owner #== pfromData pkh)
# maybePkh # newDelegate
pguardC "A UTXO must exist with the correct output" $ pguardC "Correct outputs" $
let correctOutputDatum = pbatchUpdateInputs
outDat # plam
#== mkRecordConstr ( \i o -> pletAll i $ \iF ->
mkRecordConstr
PStakeDatum PStakeDatum
( #stakedAmount .= inpDatF.stakedAmount ( #stakedAmount .= iF.stakedAmount
.& #owner .= inpDatF.owner .& #owner .= iF.owner
.& #delegatedTo .= pdata maybePkh .& #delegatedTo .= pdata newDelegate
.& #lockedBy .= inpDatF.lockedBy .& #lockedBy .= iF.lockedBy
) )
valueCorrect = pownOutputValueUnchanged # ctx #== o
in foldl1 )
(#&&) # ctx
[ ptraceIfFalse "valueCorrect" valueCorrect
, ptraceIfFalse "datumCorrect" correctOutputDatum
]
pure $ pconstant () pure $ pconstant ()
@ -224,13 +232,11 @@ pdestroy = phoistAcyclic $
plam $ \ctx -> unTermCont $ do plam $ \ctx -> unTermCont $ do
ctxF <- pmatchC ctx ctxF <- pmatchC ctx
PStakeInput inpDat _ <- pmatchC ctxF.stakeInput
PStakeBurnt <- pmatchC ctxF.stakeOutput
pguardC "Owner signs this transaction" $ pguardC "Owner signs this transaction" $
psignedByOwner # ctx pisSignedBy # pconstant True # pconstant False # ctx
pguardC "Stake unlocked" $ pnot #$ pstakeLocked # inpDat pguardC "Stake unlocked" $
pnot #$ pany # pstakeLocked # ctxF.stakeInputDatums
pure $ pconstant () pure $ pconstant ()
@ -243,61 +249,43 @@ pdepositWithdraw = phoistAcyclic $
plam $ \ctx -> unTermCont $ do plam $ \ctx -> unTermCont $ do
ctxF <- pmatchC ctx ctxF <- pmatchC ctx
PStakeInput inpDat inpVal <- pmatchC ctxF.stakeInput pguardC "Owner signs this transaction" $
PStakeOutput outDat outVal <- pmatchC ctxF.stakeOutput pisSignedBy # pconstant True # pconstant False # ctx
pguardC "Stake unlocked" $ pnot #$ pstakeLocked # inpDat ----------------------------------------------------------------------------
pguardC "Owner signs this transaction" $ psignedByOwner # ctx stakeInputDatum <-
pletC $
ptrace "Single stake input" $
pfromSingleton # ctxF.stakeInputDatums
stakeInputDatumF <- pletAllC stakeInputDatum
let stakeOutputDatum =
ptrace "Single stake output" $
pfromSingleton # ctxF.stakeOutputDatums
----------------------------------------------------------------------------
pguardC "Stake unlocked" $
pnot #$ pstakeLocked # stakeInputDatum
----------------------------------------------------------------------------
pguardC
"A UTXO must exist with the correct output"
$ unTermCont $ do
inpDatF <- pletAllC inpDat
PDepositWithdrawDelta delta <- pmatchC ctxF.redeemerContext PDepositWithdrawDelta delta <- pmatchC ctxF.redeemerContext
let oldStakedAmount = pfromData $ inpDatF.stakedAmount newStakedAmount <- pletC $ stakeInputDatumF.stakedAmount + delta
newStakedAmount <- pletC $ oldStakedAmount + delta pguardC "Non-negative staked amount" $ zero #<= newStakedAmount
pguardC "New staked amount should be greater than or equal to 0" $
zero #<= newStakedAmount
let expectedDatum = let expectedDatum =
mkRecordConstr mkRecordConstr
PStakeDatum PStakeDatum
( #stakedAmount .= pdata newStakedAmount ( #stakedAmount .= pdata newStakedAmount
.& #owner .= inpDatF.owner .& #owner .= stakeInputDatumF.owner
.& #delegatedTo .= inpDatF.delegatedTo .& #delegatedTo .= stakeInputDatumF.delegatedTo
.& #lockedBy .= inpDatF.lockedBy .& #lockedBy .= stakeInputDatumF.lockedBy
) )
datumCorrect = outDat #== expectedDatum
let valueDelta :: Term _ (PValue _ 'Positive) pguardC "Valid output datum" $ expectedDatum #== stakeOutputDatum
valueDelta = pdiscreteValue # ctxF.gtAssetClass # delta
expectedValue =
inpVal <> valueDelta
gtAssetClassF <- pletAllC ctxF.gtAssetClass
let valueCorrect =
foldr1
(#&&)
[ pgeqByClass' (AssetClass ("", ""))
# outVal
# expectedValue
, pgeqByClass
# gtAssetClassF.currencySymbol
# gtAssetClassF.tokenName
# outVal
# expectedValue
]
--
pure $
foldl1
(#&&)
[ ptraceIfFalse "valueCorrect" valueCorrect
, ptraceIfFalse "datumCorrect" datumCorrect
]
pure $ pconstant () pure $ pconstant ()

View file

@ -25,15 +25,21 @@ import Agora.Stake (
PNoProposal, PNoProposal,
PWithProposalRedeemer PWithProposalRedeemer
), ),
PSigContext ( PSigContext (PSigContext),
PSignedBy (
PSignedByDelegate, PSignedByDelegate,
PSignedByOwner, PSignedByOwner,
PUnknownSig PUnknownSig
), ),
PStakeDatum, PStakeDatum,
PStakeInputContext (PStakeInput), PStakeRedeemer (
PStakeOutputContext (PStakeBurnt, PStakeOutput), PClearDelegate,
PStakeRedeemer (PClearDelegate, PDelegateTo, PDepositWithdraw, PDestroy, PPermitVote, PRetractVotes), PDelegateTo,
PDepositWithdraw,
PDestroy,
PPermitVote,
PRetractVotes
),
PStakeRedeemerContext ( PStakeRedeemerContext (
PDepositWithdrawDelta, PDepositWithdrawDelta,
PNoMetadata, PNoMetadata,
@ -57,15 +63,19 @@ import Agora.Stake.Redeemers (
) )
import Data.Tagged (Tagged (Tagged)) import Data.Tagged (Tagged (Tagged))
import Plutarch.Api.V1 ( import Plutarch.Api.V1 (
KeyGuarantees (Sorted),
PCredential (PPubKeyCredential, PScriptCredential), PCredential (PPubKeyCredential, PScriptCredential),
PTokenName, PTokenName,
) )
import Plutarch.Api.V1.AssocMap (plookup) import Plutarch.Api.V1.AssocMap (plookup)
import Plutarch.Api.V1.Value (PValue)
import Plutarch.Api.V2 ( import Plutarch.Api.V2 (
AmountGuarantees,
PMintingPolicy, PMintingPolicy,
PScriptPurpose (PMinting, PSpending), PScriptPurpose (PMinting, PSpending),
PTxInInfo, PTxInInfo,
PTxInfo, PTxInfo,
PTxOut,
PTxOutRef, PTxOutRef,
PValidator, PValidator,
) )
@ -75,8 +85,9 @@ import Plutarch.Extra.AssetClass (
pvalueOf, pvalueOf,
) )
import Plutarch.Extra.Bind (PBind ((#>>=))) import Plutarch.Extra.Bind (PBind ((#>>=)))
import Plutarch.Extra.Field (pletAllC) import Plutarch.Extra.Category (PSemigroupoid ((#>>>)))
import "liqwid-plutarch-extra" Plutarch.Extra.List (pfindJust) import Plutarch.Extra.Functor (PFunctor (pfmap))
import "liqwid-plutarch-extra" Plutarch.Extra.List (pfindJust, pmapMaybe)
import Plutarch.Extra.Maybe ( import Plutarch.Extra.Maybe (
passertPJust, passertPJust,
pjust, pjust,
@ -100,6 +111,7 @@ import Plutarch.Extra.Value (
psymbolValueOf, psymbolValueOf,
) )
import Plutarch.SafeMoney ( import Plutarch.SafeMoney (
pvalueDiscrete,
pvalueDiscrete', pvalueDiscrete',
) )
import Plutarch.Unsafe (punsafeCoerce) import Plutarch.Unsafe (punsafeCoerce)
@ -235,8 +247,14 @@ mkStakeValidator
impl impl
as as
(Tagged (AssetClass (gtSym, gtTn))) = (Tagged (AssetClass (gtSym, gtTn))) =
plam $ \datum redeemer ctx -> unTermCont $ do plam $ \_datum redeemer ctx -> unTermCont $ do
gtAssetClass <- pletC $ passetClass # pconstant gtSym # pconstant gtTn let sstValueOf ::
( forall (ag :: AmountGuarantees) (s :: S).
Term s (PValue 'Sorted ag :--> PInteger)
)
sstValueOf =
phoistAcyclic $
psymbolValueOf # pconstant (stakeSTSymbol as)
-------------------------------------------------------------------------- --------------------------------------------------------------------------
@ -257,118 +275,138 @@ mkStakeValidator
-------------------------------------------------------------------------- --------------------------------------------------------------------------
-- Assemble the stake input context.
stakeInputDatum <- pfromData . fst <$> ptryFromC datum
stakeInputDatumF <- pletAllC $ pto stakeInputDatum
PSpending stakeInputRef <- pmatchC $ pfromData ctxF.purpose PSpending stakeInputRef <- pmatchC $ pfromData ctxF.purpose
-- The UTxO we are validating, which is also the input stake. let validatedInput =
stakeInput <-
pletC $
pfield @"resolved" pfield @"resolved"
#$ passertPJust # "Malformed script context: own input not found" #$ passertPJust
# "Malformed script context: validated input not found"
#$ pfindTxInByTxOutRef #$ pfindTxInByTxOutRef
# (pfield @"_0" # stakeInputRef) # (pfield @"_0" # stakeInputRef)
# txInfoF.inputs # txInfoF.inputs
stakeInputF <- pletFieldsC @'["address", "value"] stakeInput stakeValidatorAddress = pfield @"address" # validatedInput
stakeInputContext <-
pletC $
pcon $
PStakeInput
stakeInputDatum
stakeInputF.value
-------------------------------------------------------------------------- --------------------------------------------------------------------------
-- Assemble the signature context. getStakeDatum :: Term _ (PTxOut :--> PMaybe PStakeDatum) <-
pletC $
plam $ \txOut -> unTermCont $ do
txOutF <- pletFieldsC @'["value", "datum", "address"] txOut
signedBy <- pletC $ pauthorizedBy # authorizationContext txInfoF let isStakeUTxO =
foldl1
(#&&)
[ ptraceIfFalse "Carries SST" $
sstValueOf # txOutF.value #== 1
, ptraceIfFalse "Owned by stake validator" $
txOutF.address #== stakeValidatorAddress
]
let ownerSignsTransaction = signedBy # stakeInputDatumF.owner datum =
ptrace "Resolve stake datum" $
pfromData $
pfromOutputDatum @(PAsData PStakeDatum)
# txOutF.datum
# txInfoF.datums
pure $ pif isStakeUTxO (pjust # datum) pnothing
--------------------------------------------------------------------------
stakeInputDatums <-
pletC $
pmapMaybe
# ((pfield @"resolved") #>>> getStakeDatum)
# pfromData txInfoF.inputs
--------------------------------------------------------------------------
firstStakeInputDatumF <-
pletFieldsC @'["owner", "delegatedTo"] $
phead # stakeInputDatums
restOfStakeInputDatums <- pletC $ ptail # stakeInputDatums
pguardC "All input stakes have the same owner or delegate" $
let allHaveSameOwner =
pall
# ( (pfield @"owner")
#>>> plam (#== firstStakeInputDatumF.owner)
)
# restOfStakeInputDatums
allHaveSameDelegate =
pall
# ( (pfield @"delegatedTo")
#>>> plam (#== firstStakeInputDatumF.delegatedTo)
)
# restOfStakeInputDatums
in allHaveSameOwner #|| allHaveSameDelegate
authorizedBy <- pletC $ pauthorizedBy # authorizationContext txInfoF
let ownerSignsTransaction = authorizedBy # firstStakeInputDatumF.owner
delegateSignsTransaction = delegateSignsTransaction =
pmaybeData pmaybeData
# pconstant False # pconstant False
# plam ((signedBy #) . pfromData) # plam ((authorizedBy #) . pfromData)
# pfromData stakeInputDatumF.delegatedTo # pfromData firstStakeInputDatumF.delegatedTo
signedBy =
pif
ownerSignsTransaction
(pcon PSignedByOwner)
$ pif
delegateSignsTransaction
(pcon PSignedByDelegate)
$ pcon PUnknownSig
sigContext <- sigContext <-
pletC $ pletC $
pif ownerSignsTransaction (pcon PSignedByOwner) $
pif delegateSignsTransaction (pcon PSignedByDelegate) $
pcon PUnknownSig
--------------------------------------------------------------------------
stCurrencySymbol <- pletC $ pconstant $ stakeSTSymbol as
mintedST <- pletC $ psymbolValueOf # stCurrencySymbol # txInfoF.mint
valueSpent <- pletC $ pvalueSpent # txInfoF.inputs
spentST <- pletC $ psymbolValueOf # stCurrencySymbol #$ valueSpent
-- The stake validator can only handle one stake in one transaction.
pguardC "ST at inputs must be 1" $
spentST #== 1
let oneStakeBurnt =
ptraceIfFalse "Exactly one stake st burnt" $
mintedST #== (-1)
--------------------------------------------------------------------------
-- Assemble the stake output context.
let -- Look for the output stake.
stakeOutput =
pfindJust
# plam
( \output -> unTermCont $ do
outputF <-
pletFieldsC @'["address", "value", "datum"]
output
let isStakeOutput =
-- The stake should be owned by the stake validator.
outputF.address #== stakeInputF.address
#&&
-- The stake UTxO carries the state thread token.
psymbolValueOf
# stCurrencySymbol
# outputF.value #== 1
stakeOutputDatum =
pfromOutputDatum
# outputF.datum
# txInfoF.datums
context =
pcon $ pcon $
PStakeOutput PSigContext
(pfromData stakeOutputDatum) firstStakeInputDatumF.owner
outputF.value firstStakeInputDatumF.delegatedTo
signedBy
pure $ --------------------------------------------------------------------------
pif
isStakeOutput let gtAssetClass = passetClass # pconstant gtSym # pconstant gtTn
(pjust # context)
pnothing stakeOutputDatums <-
pletC $
pmapMaybe
# plam
( \output ->
let validateGT = plam $ \stakeDatum ->
let expected = pfield @"stakedAmount" # stakeDatum
actual =
pvalueDiscrete
# gtAssetClass
# (pfield @"value" # output)
in pif
(expected #== actual)
stakeDatum
(ptraceError "Unmatched GT value")
in pfmap
# validateGT
# (getStakeDatum # output)
) )
# pfromData txInfoF.outputs # pfromData txInfoF.outputs
stakeOutputContext <- --------------------------------------------------------------------------
pletC $
pmatch stakeOutput $ \case
-- Stake output found.
PJust stakeOutput' -> stakeOutput'
-- Stake output not found, meaning the input stake should be burnt.
PNothing -> unTermCont $ do
pguardC "One stake should be burnt" oneStakeBurnt
pure $ pcon PStakeBurnt mintedST <- pletC $ sstValueOf # txInfoF.mint
pguardC "No new SST minted" $
foldl1
(#||)
[ ptraceIfFalse "All stakes burnt" $
mintedST #< 0 #&& pnull # stakeOutputDatums
, ptraceIfFalse "Nothing burnt" $
mintedST #== 0
]
-------------------------------------------------------------------------- --------------------------------------------------------------------------
@ -427,12 +465,11 @@ mkStakeValidator
plam $ \redeemerContext -> plam $ \redeemerContext ->
pcon $ pcon $
PStakeRedeemerHandlerContext PStakeRedeemerHandlerContext
stakeInputContext stakeInputDatums
stakeOutputContext stakeOutputDatums
redeemerContext redeemerContext
sigContext sigContext
proposalContext proposalContext
gtAssetClass
txInfo txInfo
noMetadataContext <- noMetadataContext <-

View file

@ -22,13 +22,15 @@ module Agora.Utils (
pstringIntercalate, pstringIntercalate,
punwords, punwords,
pcurrentTimeDuration, pcurrentTimeDuration,
pdelete,
pdeleteBy,
pisSingleton,
pfromSingleton,
) where ) where
import Plutarch.Api.V1 (PPOSIXTime, PTokenName, PValidatorHash) import Plutarch.Api.V1 (PPOSIXTime, PTokenName, PValidatorHash)
import Plutarch.Api.V2 (PScriptHash) import Plutarch.Api.V2 (PScriptHash)
import "liqwid-plutarch-extra" Plutarch.Extra.TermCont (pmatchC)
import Plutarch.Extra.Time (PCurrentTime (PCurrentTime)) import Plutarch.Extra.Time (PCurrentTime (PCurrentTime))
import Plutarch.List (puncons)
import Plutarch.Unsafe (punsafeCoerce) import Plutarch.Unsafe (punsafeCoerce)
import PlutusLedgerApi.V2 ( import PlutusLedgerApi.V2 (
Address (Address), Address (Address),
@ -57,8 +59,8 @@ validatorHashToTokenName (ValidatorHash hash) = TokenName hash
@since 1.0.0 @since 1.0.0
-} -}
pvalidatorHashToTokenName :: forall (s :: S). Term s PValidatorHash -> Term s PTokenName pvalidatorHashToTokenName :: forall (s :: S). Term s (PValidatorHash :--> PTokenName)
pvalidatorHashToTokenName = punsafeCoerce pvalidatorHashToTokenName = phoistAcyclic $ plam punsafeCoerce
{- | Safely convert a 'PScriptHash' into a 'PTokenName'. This can be useful for tagging {- | Safely convert a 'PScriptHash' into a 'PTokenName'. This can be useful for tagging
tokens for extra safety. tokens for extra safety.
@ -145,21 +147,23 @@ plistEqualsBy ::
(b :: PType) (b :: PType)
(s :: S). (s :: S).
(PIsListLike list1 a, PIsListLike list2 b) => (PIsListLike list1 a, PIsListLike list2 b) =>
Term s ((a :--> b :--> PBool) :--> list1 a :--> (list2 b :--> PBool)) Term s ((a :--> b :--> PBool) :--> list1 a :--> list2 b :--> PBool)
plistEqualsBy = phoistAcyclic $ pfix # go plistEqualsBy = phoistAcyclic $
where plam $ \eq -> pfix #$ plam $ \self l1 l2 ->
go = plam $ \self eq l1 l2 -> unTermCont $ do pelimList
l1' <- pmatchC $ puncons # l1 ( \x xs ->
l2' <- pmatchC $ puncons # l2 pelimList
( \y ys ->
case (l1', l2') of -- Avoid comparison if two lists have different length.
(PJust l1'', PJust l2'') -> do self # xs # ys #&& eq # x # y
(PPair h1 t1) <- pmatchC l1'' )
(PPair h2 t2) <- pmatchC l2'' -- l2 is empty, but l1 is not.
(pconstant False)
pure $ eq # h1 # h2 #&& self # eq # t1 # t2 l2
(PNothing, PNothing) -> pure $ pconstant True )
_ -> pure $ pconstant False -- l1 is empty, so l2 should be empty as well.
(pnull # l2)
l1
-- | @since 1.0.0 -- | @since 1.0.0
pstringIntercalate :: pstringIntercalate ::
@ -190,3 +194,59 @@ pcurrentTimeDuration = phoistAcyclic $
plam $ plam $
flip pmatch $ flip pmatch $
\(PCurrentTime lb ub) -> ub - lb \(PCurrentTime lb ub) -> ub - lb
{- | / O(n) /. Remove the first occurance of a value from the given list.
@since 1.0.0
-}
pdelete ::
forall (a :: PType) (list :: PType -> PType) (s :: S).
(PEq a, PIsListLike list a) =>
Term s (a :--> list a :--> list a)
pdelete = phoistAcyclic $ pdeleteBy # plam (#==)
-- | @since 1.0.0
pdeleteBy ::
forall (a :: PType) (list :: PType -> PType) (s :: S).
(PIsListLike list a) =>
Term s ((a :--> a :--> PBool) :--> a :--> list a :--> list a)
pdeleteBy = phoistAcyclic $
plam $ \f' x -> plet (f' # x) $ \f ->
precList
( \self h t ->
pif
(f # h)
t
(pcons # h #$ self # t)
)
(const pnil)
{- | / O(1) /.Return true if the given list has only one element.
@since 1.0.0
-}
pisSingleton ::
forall (a :: PType) (list :: PType -> PType) (s :: S).
(PIsListLike list a) =>
Term s (list a :--> PBool)
pisSingleton =
phoistAcyclic $
precList
(\_ _ t -> pnull # t)
(const $ pconstant False)
-- | @since 1.0.0
pfromSingleton ::
forall (a :: PType) (list :: PType -> PType) (s :: S).
(PIsListLike list a) =>
Term s (list a :--> a)
pfromSingleton =
phoistAcyclic $
precList
( \_ h t ->
pif
(pnull # t)
h
(ptraceError "More than one element")
)
(const $ ptraceError "Empty list")

6
flake.lock generated
View file

@ -11979,17 +11979,17 @@
"plutarch": "plutarch_15" "plutarch": "plutarch_15"
}, },
"locked": { "locked": {
"lastModified": 1664028810, "lastModified": 1664220695,
"narHash": "sha256-thMEO1P/ciHjnMFyL0bla781TG5C/nB5EEtebb3Boik=", "narHash": "sha256-thMEO1P/ciHjnMFyL0bla781TG5C/nB5EEtebb3Boik=",
"owner": "Liqwid-Labs", "owner": "Liqwid-Labs",
"repo": "plutarch-script-export", "repo": "plutarch-script-export",
"rev": "4f0da58ba67cdcfe5c7d97e6e27dc00dfb71e657", "rev": "eba175e63516a4fed43ceab1826ea6522f28dd0f",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "Liqwid-Labs", "owner": "Liqwid-Labs",
"ref": "main",
"repo": "plutarch-script-export", "repo": "plutarch-script-export",
"rev": "4f0da58ba67cdcfe5c7d97e6e27dc00dfb71e657",
"type": "github" "type": "github"
} }
}, },