Exploit and Solutions for Calculated Outputs

A quick writeup of an internal ad-hoc conversation we had at GP today. Just wanted to get it on paper in case anyone else is facing it.

The source of the conversation is this line in the AnyHedge contract (require(tx.inputs.length == 1)) which requires an AnyHedge payout transaction to have exactly one input. That’s fine in an isolated situation, but makes it not composable. The reason for that requirement and potential solutions that are friendly to composability are described below. Mostly written by @im_uname based on a discussion with @jimtendo and myself, with some edits and additions by Jim/me.

The problem

If a contract evaluates an output (or multiple outputs for that matter) to fulfill its script, presumably another contract that evaluates to the same output value (or any other non-unique output criteria) can be placed into the same transaction, “re-use” the same output to satisfy its script. its coins can then be burned or stolen against intention of the contract funder.

To solve this, we need to prevent an output from being “reused” in such a way. preventing this re-use requires that the output has some kind of identifier that can be tied to a unique input .

Solution 0: require exactly one input (:x: not composable)

Can’t spend multiple inputs if there is only one! This is what AnyHedge did before introspection and before it was really conceivable to compose contracts :pray:

Solution 1: the “SIGHASH_SINGLE” method (:warning: not generally composable)

Script evaluates its own input_index, compare to the output_index it’s evaluating, make sure they match (or some other 1:1 transformation). While this solves the reuse problem, it unnecessarily constrains the tx shape in a rigid way and is incompatible with general composability.

Solution 2: output value hack (:grimacing:)

Script evaluates the necessary output amount, then before the evaluation concludes, subtracts in sats its own input_index. it is extremely unlikely that some other contract will subtract its own index, pay to the same people, and somehow end up with the same compatible output. problem is this may not be very robust, opening up known unknowns in math holes. Also just an ugly hack to mess around with money amounts.

Solution 3: Nonce in initial contract parameters (:white_check_mark::grey_question:)

While not a theoretical solution, this is a solution that probably works in practice. It’s not a solution in theory because you can still make two contracts with exactly the same parameters and nonce. But in practice… you wouldn’t do that, even in an adversarial situation.

Solution 4: Tokens (:white_check_mark::grey_question:)

The transaction generates outputs as cashtokens NFT outputs instead of normal p2pkh/p2sh outputs. the NFT has a commitment field each, and the commitment field carries the index of the input it’s supposed to evaluate against. since input indexes are unique within a tx, you can’t evaluate against another script employing a similar identifier.

More solutions…

Surely there are other options.

7 Likes

Cauldron, the FBCH Vault and Wrapped BCH all peg the output categoryId and other output constraints to the same input index. So it’s a 1:1 output to input solution and that’s how those contract outpoints can be composed.

These contract have to account for the total input value (sats/tokens) and the entire output value (sats/tokens).

These are implementations of Solution 1?


To add some spice to the above pattern…

The FBCH Coupon limits spending to one per transaction by requiring it’s spent as the last input.

    // The coupon must be spent as the last input, 
    //   therefore only coupon may be spent at a time.
    // OP_INPUTINDEX OP_1 OP_ADD OP_TXINPUTCOUNT OP_NUMEQUAL
    require(this.activeInputIndex+1 == tx.inputs.length);

The active input index could be pegged to any single index to enforce the input is mapping as a “one time use” to some output constraint.


Every unspent outpoint kinda has a nonce already, in the transaction id + index, this is very similar to Solution 4.


There’s also NFT Baton. If the outpoint being spent is an NFT outpoint that the contract secures, this essentially limits the contract to one input as well.*

*EDIT: As long as the NFT is distinct or the total supply is secured. If the contract design has it holding one NFT and someone figures out a way to get two NFTs on the contract, then it’s back to square one.

3 Likes

Solution 6: paired or grouped composition (:warning: not generally composable).

Parallel 1:1 composition using the active input index has proven to be a fairly useful method (Cauldron, WBCH, FBCH) to constrain many outputs with many inputs using introspection.

However, with CashTokens, since each output is limited to a single token category, the 1:1 approach is limited to operating on a single token category.

There are many usecases where a contract may interact with multiple token types in a single transaction:

  • A direct Token to Token dex
  • A dex directed or controlled with NFT commitments
  • An NFT issuing contract paid for with a different token.
    etc.

Abstracting from a single parallel construction where each input must be paired with an output at the same index, it’s also possible to take the modulo of the active index to gang them into pairs or groups

// Get an even subIndex for the current input index
// OP_INPUTINDEX OP_INPUTINDEX OP_2 OP_MOD OP_SUB
int idxEven = this.activeInputIndex - (this.activeInputIndex % 2);   

// Push the odd subIndex of the current input
// OP_DUP OP_1ADD
int idxOdd = groupSubIndex + 1;
...

// ... constraints against odd and even indexed inputs/outputs ...

Using indices transformed into subgroups, it’d be possible to write logic constraining the outputs of the inputs as a group (pairs or larger), without using the index of a particular input directly in a constraint.


For example, it should be possible to create a dex like the Cauldron with WBCH as the base unit, where every even input was WBCH and every odd record in a transaction is a different token.

The outputs being spent (carrying WBCH or other FTs) could pass through a single set of logic using only introspection that could apply constrains across pairs of inputs and outputs.

Just changing the modulo, it should be possible to abstract it into larger groups, or three or four, etc.

1 Like

Solution 7: NFT commitment OP_EVAL

An alternative to limiting input length would be to put the script in the NFT commitment of the input.

Enforcing the NFT category could prevent “unauthorized” unrelated inputs from being evaluated by the contract. But the problem of assuring each input mapped to the correct output intent would remain.

The script on the input could dictate a composition method (atomic, parallel, grouped, first/last), and what to do with itself once used (burn, countdown, increment, etc) with mutation.

1 Like