{- | Threat model for detecting signatory threshold vulnerabilities (1-of-N bugs). A signatory threshold vulnerability occurs when a smart contract allows transactions to succeed with fewer required signatories than expected. This can happen when: 1. The validator uses @list.any@ instead of @list.all@ for checking signatures 2. The validator only checks that ONE of the required signers signed, not ALL 3. The multi-signature threshold logic is incorrectly implemented == Consequences == 1. __Unauthorized fund extraction__: An attacker who is one of N required signers can unilaterally withdraw funds that should require N-of-N approval. 2. __Governance bypass__: Multi-signature governance schemes become ineffective when a single party can act alone. 3. __Trust assumption violation__: Users who deposited funds expecting N-of-N security only get 1-of-N security. == Vulnerable Pattern == @ -- Aiken example: Checks ANY signer instead of ALL validator spend(..., datum, _redeemer, _ctx) { list.any(datum.required_signers, fn(signer) { list.has(ctx.transaction.extra_signatories, signer) }) } @ Should be: @ -- Correct: Checks ALL signers validator spend(..., datum, _redeemer, _ctx) { list.all(datum.required_signers, fn(signer) { list.has(ctx.transaction.extra_signatories, signer) }) } @ == Mitigation == A secure multi-signature validator should: - Check that ALL required signers have signed (use @list.all@, not @list.any@) - Verify the exact threshold (e.g., 2-of-3, 3-of-5) is met - Use a proper multi-sig scheme with explicit threshold parameter This threat model tests if a transaction with multiple required signers still validates when required signatories are removed one at a time. -} module Convex.ThreatModel.SignatoryRemoval ( signatoryRemoval, ) where import Convex.ThreatModel ( ThreatModel (Named), failPrecondition, getTxRequiredSigners, pickAny, shouldNotValidate, threatPrecondition, ) import Convex.ThreatModel.TxModifier (removeRequiredSigner) {- | Generic threat model: remove required signatories one at a time. For a transaction with multiple required signers, this threat model: 1. Gets all required signers from the transaction body 2. Picks one signer at random 3. Removes that signer from the required signers list 4. Checks that the transaction should NOT validate If the transaction still validates with fewer signers, the contract has a threshold vulnerability (e.g., 1-of-N instead of N-of-N). This is a property-based approach - QuickCheck will run this multiple times with different random signers being removed, exploring the space of possible single-signer removals. Note: This threat model requires at least one required signer in the transaction. Transactions with no required signers will be skipped. -} signatoryRemoval :: ThreatModel () signatoryRemoval :: ThreatModel () signatoryRemoval = String -> ThreatModel () -> ThreatModel () forall a. String -> ThreatModel a -> ThreatModel a Named String "Signatory Removal" (ThreatModel () -> ThreatModel ()) -> ThreatModel () -> ThreatModel () forall a b. (a -> b) -> a -> b $ do [Hash PaymentKey] signers <- ThreatModel [Hash PaymentKey] getTxRequiredSigners -- Precondition: there must be at least one required signer to remove ThreatModel () -> ThreatModel () forall a. ThreatModel a -> ThreatModel a threatPrecondition (ThreatModel () -> ThreatModel ()) -> ThreatModel () -> ThreatModel () forall a b. (a -> b) -> a -> b $ do case [Hash PaymentKey] signers of [] -> String -> ThreatModel () forall a. String -> ThreatModel a failPrecondition String "No required signers in transaction" [Hash PaymentKey] _ -> () -> ThreatModel () forall a. a -> ThreatModel a forall (f :: * -> *) a. Applicative f => a -> f a pure () -- Pick a random signer to remove Hash PaymentKey signer <- [Hash PaymentKey] -> ThreatModel (Hash PaymentKey) forall a. Show a => [a] -> ThreatModel a pickAny [Hash PaymentKey] signers -- The transaction should NOT validate with this signer removed TxModifier -> ThreatModel () shouldNotValidate (Hash PaymentKey -> TxModifier removeRequiredSigner Hash PaymentKey signer)