-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix critical exploit #202
Fix critical exploit #202
Conversation
Minting a GAT would require the GST to be moved, thus requiring a valid proposal. Can you give us a minimum example of how the attack will work? |
As a malicious user, I create a proposal with multiple non-malicious but malformed effects (ie the effects don't use makeEffect that checks that exactly 1 GAT is spent in the transaction inputs) , the proposal passes and authority tokens are issued to the effects. Now the user spends 2 or more GATs from the effects that passed from the legitimate proposal, and mints 1 or more illegitimate GATs because the GAT minting policy validates if |
I see. Let's say we have |
So only allowing one GAT to be burnt at a time won't be sufficient. Also, it would be great if we can keep the ability to burn multiple GATs in one tx. |
Yes, my proposed solution uses the |
What I have in mind: diff --git a/agora/Agora/AuthorityToken.hs b/agora/Agora/AuthorityToken.hs
index 9de629d..d3f245b 100644
--- a/agora/Agora/AuthorityToken.hs
+++ b/agora/Agora/AuthorityToken.hs
@@ -11,6 +11,7 @@ module Agora.AuthorityToken (
singleAuthorityTokenBurned,
) where
+import Agora.Utils (passert, pnegativeSymbolValueOf, ppositiveSymbolValueOf)
import Plutarch.Api.V1 (
PCredential (..),
PCurrencySymbol (..),
@@ -35,6 +36,7 @@ import Plutarch.Extra.Sum (PSum (PSum))
import "liqwid-plutarch-extra" Plutarch.Extra.TermCont (pguardC, pletC, pletFieldsC, pmatchC)
import Plutarch.Extra.Traversable (pfoldMap)
import Plutarch.Extra.Value (psymbolValueOf)
+import Plutarch.Num (pnegate)
--------------------------------------------------------------------------------
@@ -140,30 +142,29 @@ authorityTokenPolicy =
PTxInfo txInfo' <- pmatchC $ pfromData ctx.txInfo
txInfo <- pletFieldsC @'["inputs", "mint", "outputs"] txInfo'
let inputs = txInfo.inputs
- mintedValue = pfromData txInfo.mint
govTokenSpent = pisTokenSpent # (ptoScottEncoding # atAssetClass) # inputs
PMinting ownSymbol' <- pmatchC $ pfromData ctx.purpose
let ownSymbol = pfromData $ pfield @"_0" # ownSymbol'
- mintedATs =
- psymbolValueOf
- # ownSymbol
- # mintedValue
+
+ applySymbolValueOf <- pletC $ plam $ \f -> f # ownSymbol # txInfo.mint
+
+ let mintedATs = applySymbolValueOf # ppositiveSymbolValueOf
+ burntATs = applySymbolValueOf # pnegativeSymbolValueOf
pure $
- pif
- (0 #< mintedATs)
- ( unTermCont $ do
- pguardC "Parent token did not move in minting GATs" govTokenSpent
- pguardC "All outputs only emit valid GATs" $
- pall
- # plam
- (authorityTokensValidIn # ownSymbol #)
- # txInfo.outputs
- pure $ popaque $ pconstant ()
- )
- (pif (singleAuthorityTokenBurned # ownSymbol # inputs # mintedValue)
- (popaque $ pconstant ())
- perror
+ popaque $
+ pif
+ (0 #< mintedATs)
+ ( unTermCont $ do
+ pguardC "No GAT burnt" $ burntATs #== 0
+ pguardC "Parent token did not move in minting GATs" govTokenSpent
+ pguardC "All outputs only emit valid GATs" $
+ pall
+ # plam
+ (authorityTokensValidIn # ownSymbol #)
+ # txInfo.outputs
+ pure $ pconstant ()
)
+ (passert "No GAT minted" (0 #== mintedATs) (pconstant ()))
diff --git a/agora/Agora/Utils.hs b/agora/Agora/Utils.hs
index be892d7..b6daa3e 100644
--- a/agora/Agora/Utils.hs
+++ b/agora/Agora/Utils.hs
@@ -31,16 +31,20 @@ module Agora.Utils (
pinsertUniqueBy,
ptryFromRedeemer,
passert,
+ ppositiveSymbolValueOf,
+ pnegativeSymbolValueOf,
) where
import Plutarch.Api.V1 (KeyGuarantees (Unsorted), PPOSIXTime, PRedeemer, PTokenName, PValidatorHash)
import Plutarch.Api.V1.AssocMap (PMap, plookup)
-import Plutarch.Api.V2 (PScriptHash, PScriptPurpose)
+import Plutarch.Api.V2 (AmountGuarantees, PCurrencySymbol, PMap (PMap), PScriptHash, PScriptPurpose, PValue (PValue))
import Plutarch.Extra.Applicative (PApplicative (ppure))
import Plutarch.Extra.Category (PCategory (pidentity))
import Plutarch.Extra.Functor (PFunctor (PSubcategory, pfmap))
-import Plutarch.Extra.Maybe (pjust, pnothing)
+import "liqwid-plutarch-extra" Plutarch.Extra.List (plookupAssoc)
+import Plutarch.Extra.Maybe (pexpectJustC, pjust, pnothing)
import Plutarch.Extra.Ord (PComparator, POrdering (PLT), pcompareBy, pequateBy)
+import "liqwid-plutarch-extra" Plutarch.Extra.TermCont (pmatchC)
import Plutarch.Extra.Time (PCurrentTime (PCurrentTime))
import Plutarch.Unsafe (punsafeCoerce)
import PlutusLedgerApi.V2 (
@@ -385,3 +389,59 @@ passert ::
Term s a ->
Term s a
passert msg cond x = pif cond x $ ptraceError msg
+
+psymbolValueOfHelper ::
+ forall
+ (keys :: KeyGuarantees)
+ (amounts :: AmountGuarantees)
+ (s :: S).
+ Term
+ s
+ ( (PInteger :--> PBool)
+ :--> PCurrencySymbol
+ :--> ( PValue keys amounts
+ :--> PInteger
+ )
+ )
+psymbolValueOfHelper =
+ phoistAcyclic $
+ plam $ \cond sym value'' -> unTermCont $ do
+ PValue value' <- pmatchC value''
+ PMap value <- pmatchC value'
+ m' <-
+ pexpectJustC
+ 0
+ ( plookupAssoc # pfstBuiltin
+ # psndBuiltin
+ # pdata sym
+ # value
+ )
+ PMap m <- pmatchC (pfromData m')
+ pure $
+ pfoldr
+ # plam
+ ( \x v ->
+ plet (pfromData $ psndBuiltin # x) $ \q ->
+ pif
+ (cond # q)
+ (q + v)
+ v
+ )
+ # 0
+ # m
+
+ppositiveSymbolValueOf ::
+ forall
+ (keys :: KeyGuarantees)
+ (amounts :: AmountGuarantees)
+ (s :: S).
+ Term s (PCurrencySymbol :--> (PValue keys amounts :--> PInteger))
+ppositiveSymbolValueOf = phoistAcyclic $ psymbolValueOfHelper #$ plam (0 #<)
+
+pnegativeSymbolValueOf ::
+ forall
+ (keys :: KeyGuarantees)
+ (amounts :: AmountGuarantees)
+ (s :: S).
+ Term s (PCurrencySymbol :--> (PValue keys amounts :--> PInteger))
+pnegativeSymbolValueOf = phoistAcyclic $ psymbolValueOfHelper #$ plam (#< 0)
|
I like this solution. Can you push them to this branch so we can merge? I will also coordinate with liqwid-plutarch-extra to see if it would be possible to get those functions upstreamed. |
Yep, 2f7f17a. Feel free to cherry pick. The solution is not very efficient(ideally we traverse minted value only once) but will do for now. EDIT: 467fa73 EDIT again: Sorry for not reading your comment carefully. I don't think I can push to your branch. By the way, it would be nice if you can clean up the git history a little bit afterward. |
The same attack may also apply to other scripts, which is a huge issue. I will take a closer look tomorrow. cc @emiflake. |
78d88f7
to
a5687c6
Compare
Okay, it should be good to go. |
It looks like the other place that might have this issue is: It would only be possible here if it were possible to somehow get more than 1 ST on a single UTXO (which at first glance does not appear possible).
Assures that |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would do for now. Thanks for bringing this up @colll78. The fix for stake scripts and regression tests will be included in an upcoming pr.
For stake scripts, the attack vector would be:
|
Co-authored-by: Hongrui Fang <chfanghr@gmail.com>
Co-authored-by: Hongrui Fang <chfanghr@gmail.com>
Co-authored-by: Hongrui Fang <chfanghr@gmail.com>
Your PR #200 forbids minting of SSTs when |
No, it will still work. |
So, essentially, as long as the newly minted SSTs have different names, the attack will still work. The
|
Right, so the refactoring introduces asset class which ironically makes it less secure than the previous usage only of |
Hmm, I wouldn't say that. In fact, using only |
I guess using |
Yeah, you can even skip the stake validator there:
Similar fix to above should work here. |
Fix an exploit that allows burning >2 legitimate GATs from faulty effect validators to mint 1 (or more) illegitimate GAT.