| Safe Haskell | Safe-Inferred |
|---|---|
| Language | Haskell2010 |
Convex.ThreatModel.MutualExclusion
Description
Threat model for detecting Mutual Exclusion vulnerabilities.
A Mutual Exclusion Attack exploits validators that use list.find or similar
functions to locate "their" continuation output without enforcing uniqueness.
When multiple inputs try to match outputs by property (e.g., same owner, same
script address), having duplicate outputs allows cross-matching between
different inputs.
Example Vulnerability ==
Consider an account validator that locates its continuation by finding an output with the same owner:
-- Aiken pseudocode
let my_output = outputs |> list.find(fn(o) { o.datum.owner == my_owner })
If an attacker creates two identical outputs, two different account inputs can both "find" the same output (or each find a different one that doesn't actually correspond to their input).
Attack Scenario ==
- Transaction spends Account A (owner: Alice) and Account B (owner: Bob)
- Original outputs: Output A' (owner: Alice), Output B' (owner: Bob)
- Attack: Duplicate Output A' so we have two outputs with owner: Alice
- Now Account A and Account B can both claim Output A' as "theirs" since list.find returns the first match
- This breaks the 1:1 correspondence between inputs and outputs
Note: the duplicate carries the SAME value as the original output, not just
the same address and datum. This also catches validators that locate "their"
continuation by list.find-ing on the output's value (e.g. matching an
expected payout amount) rather than on the datum. The duplicate's ADA is
handled by the test harness's rebalancing (it tops up or draws down the
wallet's change output to absorb whatever a TxModifier adds or removes),
so this attack doesn't need to hand-balance the duplicate itself.
The rebalancing cannot conjure *native tokens*, though: a duplicate of an output that carries tokens (e.g. a state thread token) needs those tokens to come from somewhere, and the signing wallet has none, so such transactions are skipped rather than tested. For a unique thread token that skip mirrors reality - a real attacker cannot obtain a second copy of the token either, so the duplicated-output transaction isn't constructible on-chain in the first place, and a validator that locates its continuation by that token is not vulnerable to this attack.
Consequences ==
- Cross-matching: Input A claims Output B's value, Input B claims nothing
- State corruption: Multiple inputs modify the same output
- Fund theft: Attacker can redirect funds by manipulating which input matches which output
Root Cause ==
Validators that:
- Use list.find to locate continuations without enforcing uniqueness
- Match outputs by datum properties without checking index correspondence
- Don't verify that each input has exactly one matching output
Mitigation ==
A secure validator should:
- Use index-based matching (output[i] corresponds to input[i])
- Verify no duplicate outputs exist with the same matching criteria
- Use list.filter and check exactly one result
This threat model tests if duplicating a script continuation output still allows the transaction to validate. If it does, the validator has a Mutual Exclusion vulnerability.
Synopsis
Documentation
mutualExclusionAttack :: ThreatModel () Source #
Check for Mutual Exclusion vulnerabilities by duplicating script outputs.
For a transaction with script continuation outputs:
- Find a script output (continuation) that goes back to a script address
- Duplicate it — add another output with the SAME address, value, and datum
(the test harness's rebalancing absorbs the duplicate's ADA by adjusting
the wallet's change output, so this doesn't trip
ValueNotConservedUtxo; a duplicate carrying native tokens the wallet cannot supply is skipped, see the module header) - If the transaction still validates, the script doesn't properly enforce mutual exclusion between inputs and outputs
This catches vulnerability patterns in bank_02 and bank_03 where multiple account inputs can cross-match outputs because the validator uses list.find without enforcing uniqueness.