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.

2 Likes