Matching gifts with cryptocurrency: the fine-print in contracts (part II)


[continued from part I]

Avoiding contractual scams

Time to revisit a question glossed over earlier. While the smart-contract sketched above sounds good on paper, skeptical donors will be rightfully asking a pragmatic question: how can they be confident that a matching-gifts campaign launched by a sponsor at some blockchain address is in fact operating according to these rules? If the contract functions as described above, all is well. But what if the contract has a backdoor designed to divert funds to a private wallet, instead of delivering them to the nonprofit? Since the trigger for such malicious logic could be arbitrary, past performance is no guarantee of future results. For example, the contract may act honestly for the first few donations— perhaps those arranged by accomplices of the supporter to help build confidence— only to start embezzling funds after a certain trigger is hit.

Transparency of blockchains goes a long way to alleviate these risks. In particular, the sponsor can publish the source code of the contract, along with the version of the Solidity compiler used to convert that code into low-level EVM byte-code. Automated tools already exist for verifying this correspondence; see Etherscan for examples of verified contracts. This reduces the problem of verifying contract behavior to source code auditing, which is somewhat more tractable than reverse engineering EVM byte-code. There are still shenanigans possible at source code level, as starkly demonstrated by the Solidity Underhanded Contest, a competition to come up with the most creative backdoor possible that can stay undetected by human reviewers. In practice there would be one “canonical” matching campaign contract, already audited and in widespread use, similar to the canonical multi-sig wallet contract. Establishing the authenticity of an alleged matching campaign boils down to verifying that a copy of that exact contract has been deployed. (There is an interesting edge-case involving the CREATE2 extension: until recently, Ethereum contracts were considered immutable. A contract at a given address could self-destruct but it could not be replaced by another contract. This is no longer the case for contract launched via CREATE2, so it is important to also verify that the contract was deployed using the standard, original CREATE instruction or alternatively that its initialization code has no external dependencies that may differ between multiple invocations.)

In addition to verifying contract source code, it is necessary to inspect parameters such as the destination address for the nonprofit receiving donations, committed match ratio (in case this is not hard-coded as one-for-one in code) and funding level of the contract.

Difficult case: Bitcoin

In contrast to Ethereum’s full-fledged programming language for smart contracts. Bitcoin has a far more limited scripting language to express spending conditions. This makes it difficult to achieve parity with the Ethereum implementation of a matching campaign. A more limited notion of “matching” can be achieved by leveraging different signatures types in Bitcoin, but at the expense of reverting to all-or-none semantics. Similar to the prior art in Ethereum, the sponsor is only on the hook for matching donations if one or more other participants materialize with donations exceeding a threshold. Below that threshold, nothing happens.

There is also precedent for constructing this type of crowd-funding transaction. To make this more concrete, suppose the sponsor is willing to match donations up to 1 bitcoin to a specific charity. As proof of her commitment, the sponsor creates and signs a partial transaction:

As it stands, this TX is bogus: consensus rules require that the inputs provide an amount of funds greater than or equal to the outputs, with the difference going to miners as incentive to include the transaction. Since 1 < 2, this transaction can never get mined— as it stands. But this is where use of SIGHASH_ANYONECANPAY comes in; additional inputs can be added to the “source” side of the transaction, as long as outputs on the “destination” remain the same. This allows building the transaction up, layer by layer, with more participants chipping in with a signed input of their own, until the total inputs add up to 2 BTC— or ideally slightly more than 2 BTC to make room for transaction fees. Once that threshold is reached, the transaction can be broadcast.

Compared to the Ethereum case, this construction comes with some caveats and limitations. First the activity of building up to the full amount must be coordinated off-chain, for example using an old-fashioned website. It is not possible to broadcast a partial TX, have it sit in mempool while collecting additional inputs. An invalid TX with insufficient funds will not be relayed around the network. This stands in contrast to Ethereum where all future donations can be processed on chain once the contract is launched. Second, the sponsor can bail out at any time, by broadcasting a different transaction that spends the source input in a different way. It’s not even considered a double-spend since there were no other valid transactions involving that input as far as mempool is concerned. (While the input address can be constrained using time-locks in its redeem script, the same restriction will also apply to the donation. A fully funded TX will also get stuck and not deliver any funds to the nonprofit until the time-lock expires.)

Change is tricky

As sketched above, the arrangement also requires exactly sized inputs, because there is no meaningful way to redirect change. Consider the situation after a first volunteer pledges 0.9 BTC, leaving the campaign just 0.1 BTC away from the goalpost. If a second volunteer as a UTXO worth 0.2 BTC, they would have to first chip away a separate 0.1BTC output first. Directly feeding in the 0.2 BTC UTXO would result in half the funds getting wasted as mining fees. The outputs are already fixed and agreed upon by previous signatures; there is no way for the last volunteer to redirect any excess contribution to a change address. This can be addressed using a different signature scheme combining SIGHASH_ANYONECANPAY and SIGHASH_SINGLE. This latter flag indicates that a given input is signing only its corresponding output, rather than all outputs. That allows each donor (other than the sponsor) to also designate a change address corresponding to their contribution, in case they only want to donate a fraction of one of their UTXO. Unfortunately this arrangement also allows the sponsor to abscond with funds. Since SIGHASH_SINGLE means individual donors are not in fact validating the first output— ostensibly going to the nonprofit— a dishonest sponsor can collect additional inputs, switch the first output to send 2BTC to a private wallet and broadcast that altered transaction.

A variant of that problem can happen even with an honest sponsor an unwitting contributors racing each other. Suppose Alice and Bob both come across a partially signed transaction that has garnered multiple donations, but has fallen 0.1 BTC short of the goal to trigger the matching promise. Both spontaneously decide to chip in 0.1 BTC to push that campaign across the finish line. If they both sign using SIGHASH_ANYONECANPAY and attempt to broadcast the now valid transaction, there is an opportunity for an unscrupulous miner to steal funds. Instead of considering these conflicting TX as double-spends and only accepting one, an opportunistic miner could merge contributions from Alice and Bob into a single TX. Since both signatures only commit to outputs but expressly allow additional inputs, this merge will not invalidate signatures. The result is a new TX where the input side has 0.1BTC excess, which will line the miners’ pockets as excess transaction fee instead of reaching the charitable organization. One mitigation is to ensure that anyone who is adding the “final” input that will trigger the donation use SIGHASH_ALL to cover all inputs, preventing any other inputs from being merged. The problem with that logic is it assumes global coordination among participants. In a public campaign, typically no one can know in advance when the funding objectives are reached. (Suppose the campaign was 0.2 BTC short of the goal and three people each decide to chip in 0.1 BTC, each individually assuming that the threshold is still not met after their contribution.)

For this reason, this construction is only suitable for “small group matching”— a single, large donation in response to a pledge for a comparable amount from the sponsor. Alice creates the 1 → 2 original transaction pledging one-for-one matching, Bob adds his own exact 1 BTC input, signs all inputs/outputs prior to broadcasting the transaction. If Carol happened to be doing the same, these two transactions could not be merged and either Bob or Carol’s attempt would fail without any loss of funds. For now the construction of a more efficient structure for incrementally raising funds with matching campaign on Bitcoin remains an open problem.

CP

Leave a comment