Skip to content

Modeling Contract Systems

Introduction

Before BCH had Introspection and CashTokens, contracts were usually quite simple. Now that we have Introspection and CashTokens, we can build very advanced systems of contracts that interact with each other, granting us EVM-like capabilities, albeit modelled very differently due to BCH using a UTXO model.

The below tries to describe a (relatively) generalized approach for modelling Bitcoin Cash Smart Contract Systems.

INFO

This approach might not be perfect and suggestions/alternatives are certainly most welcome. This is the best conceptualization I've been able to come up with but I've deliberately tried to keep the door-open for other approaches where the following conceptualizations might not work.

Actions and States

When working with the Bitcoin Cash Contract-Systems, UTXOs will often enforce that other UTXOs are also present in the same transaction (e.g. Sidecar NFTs that carry data).

For this reason, it's often not sufficient to simply think about Bitcoin Cash Smart Contracts as individual entities: Instead, we often need to think about these individual Smart Contracts in terms of how they are composed together. This is (in part) why CashConnect uses the concept of actions - which represent individual Smart Contracts acting in tandem with each other (e.g. multiple contracts used in a single transaction).

One way to think about this is by asking "what do your contracts do?" For example, if you were creating a Dapp, what would the UI look like for operating on your contract? What would your functions for interacting with your contracts be called?

First Example: AnyHedge

To illustrate, we're going to start with the AnyHedge Contract because it's a very simple contract and therefore easy to model and think about.

INFO

AnyHedge is technically a single CashScript file - but the same concepts are applicable when working with systems of many contracts ("Contract Systems").

If we look at the AnyHedge CashScript file, we can see it only has the following unlock methods:

  1. mutualRedeem
  2. payout

This is pretty easy to translate to the concept of actions within our template. For example, if we were building a simple template for AnyHedge, we could do this with only three actions:

  1. create (Create an AnyHedge UTXO)
  2. mutualRedeem (Mutually redeem an existing AnyHedge UTXO)
  3. payout (Payout an existing AnyGedge UTXO)

The more difficult question here might be "how would we persist the data for this UTXO in a backend?" In general, a good way to mentally model this kind of thing might be to think of it as something like a "Workflow Diagram":

  1. You have states (as a general rule of thumb, you can think of a "state" as a UTXO and the data associated with that UTXO).
  2. You have actions (create, mutualRedeem and payout).
  3. And your actions consume states and emit new states.

For example, if we were to draw a chart showing the States and Actions of an AnyHedge Contract, it might look like so:

flowchart LR
    Create --> AnyHedgeUTXO
    AnyHedgeUTXO --> MutualRedeem
    AnyHedgeUTXO --> Payout
    MutualRedeem --> AnyHedgeMutuallyRedeemed
    Payout --> AnyHedgePaidOut

    style AnyHedgeUTXO fill:#2E86AB
    style AnyHedgeMutuallyRedeemed fill:#2E86AB
    style AnyHedgePaidOut fill:#2E86AB

In the above, we are representing our states in blue. This consume/emit pattern is neat because it mimics the way that Bitcoin Cash UTXOs actually behave.

INFO

It is not compulsory that states are consumed. There might be some kinds of states that do not behave this way (e.g. some Wallet Templates). But, in the above example, as we are treating our "states" as "UTXOs", so it makes sense to model it like this.

In our Template code, we might then define our states like so, using the Lockscript params of the AnyHedge Contract:

ts
import { Vars } from '@cashconnect-js/templates-dev';

const AnyHedgeLockscriptParameters = {
  shortMutualRedeemPublicKey: Vars.publicKey('Short Mutual Redeem Public Key'),
  longMutualRedeemPublicKey: Vars.publicKey('Short Mutual Redeem Public Key'),
  enableMutualRedemption: Vars.number('Enable Mutual Redemption'),
}

const AnyHedgeUTXOState = {
  outpointTransactionHash: Vars.transactionHash('Outpoint Transaction Hash'),
  outpointIndex: Vars.transactionHash('Outpoint Index'),
  // Spread our Lockscript Parameters in.
  ...AnyHedgeLockscriptParameters,
}

// These are virtual states. They are not compulsory, but you'll likely want to track Mutually Redeemd and Paid Out UTXOs in your database.
const AnyHedgeMutuallyRedeemedState = {
  ...AnyHedgeUTXOState
}

const AnyHedgePaidOutState = {
  ...AnyHedgeUTXOState
}

... and our Actions might look as follows:

ts
{
  // ...
  actions: {
    // Create an AnyHedge UTXO
    create: {
      params: {
        ...AnyHedgeLockscriptParameters,
      }
      instructions: [
        // ...
      ],
      // Return the state we defined earlier.
      returns: AnyHedgeUTXOState,
    },
    // Mutually redeem an AnyHedge UTXO
    mutualRedeem: {
      params: {
        // Spread our state into the params
        ...AnyHedgeUTXOState,
        // Unlock params.
        shortMutualRedeemSignature: Vars.signature('Short Mutual Redeem Signature'),
        longMutualRedeemSignature: Vars.signature('Long Mutual Redeem Signature')
      },
      instructions: [
        // ...
      ],
      // Return a virtual state (so that we can save it in our DB.
      returns: AnyHedgeMutuallyRedeemedState
    }
    // Payout an AnyHedge UTXO
    payout: {
      params: {
        // Spread our state into the params
        ...AnyHedgeUTXOState,
        // Unlock params.
        settlementMessage: Vars.signature('Oracle Settlement Message'),
        settlementSignature: Vars.signature('Oracle Settlement Signature')
        previousMessage: Vars.signature('Previous Oracle Settlement Message'),
        previousSignature: Vars.signature('Previous Oracle Settlement Signature')
      },
      instructions: [
        // ...
      ],
      // Return a virtual state (so that we can save it in our DB.
      returns: AnyHedgePaidOutState
    }
  }
  // ...
}

INFO

Note that CashConnect is (currently) unopinionated about how you define these states. Although states will be formally supported in the schema later, to ensure versatility while experimenting with different contract designs, it simply expects params and returns and leaves it up to the Developer as to how they think states, params, etc, can be best represented in code.

Once states are formalized into the schema, this params and returns approach will still be available so that we can accommodate for those unusual cases that don't quite fit with the above model.

When it comes to database persistence, we have a few options:

  1. For SQL-like databases, you could create a table per each "state".
  2. For NoSQL-like databases, you could create a single collection and use a discriminated union (e.g. type) to indicate what kind of state it is.

INFO

I will have generalized utils/tooling coming to help with Database Persistence, but it probably won't be until Q1 2026.

In practice, you'll likely want to keep track of these states (e.g. have they been spent?), so you will likely want a consumed property or similar. As a very rough example:

ts
interface AnyHedgeUTXOModel {
  ...AnyHedgeUTXOState,
  // Some way to annotate the status of the state.
  consumed?: {
    action: string,
    states: [ReferencesToOtherStates]
  }
}

INFO

Note that this is just one approach to database persistence! You could also store a table of executed actions and define relations there between states.

But, personally, I found that a bit complex to work with (due to relations) so am just tracking a consumed property for each state in my current implementations. That said, if anyone has found other neat ways to model storage, please let us know!

One current gap in the above is that it assumes that you can easily determine the state - which is not necessarily the case if someone broadcasts directly to the blockchain, outside of your platform. An approach for handling this in CashConnect will be coming (I'm calling it State Inference for now), but will not be available until I add the outpoint type (see Roadmap). For now, you'll have to manually monitor the UTXOs in your database and use JS to infer state.

Second Example: BCH Guru Event Predictions

WARNING

Guru Event Prediction Templates are not quite finalized yet. Once completed, I'll get clearance from the Guru Team to share the full code/template and link it here (Guru and its templates will be made fully public in their own repository as the intent is to have them audited eventually). For now, I'm just copying+pasting the relevant snippets/concepts.

BCH Guru's Event Predictions platform is composed of a few different contracts that need to operate in tandem. The below are the three CashScript contracts for BCH Gameplay.

  1. EventPredictionsOfferBCH
  2. EventPredictionsMatchBCH
  3. EventPredictionsMatchSidecarBCH

If we were to model this using our Action/States approach, the chart might look as follows:

flowchart LR
    Offer([Offer])
    OfferExpired([OfferExpired])
    OfferCancelled([OfferCancelled])
    Match([Match])
    MatchPaidOut([MatchPaidOut])
    MatchVoided([MatchVoided])
    CreateOffer --> Offer
    Offer --> ExpireOffer --> OfferExpired
    Offer --> CancelOffer --> OfferCancelled
    Offer -->|Two Offers - Yes+No| CreateMatch
    CreateMatch -.-> Offer
    %% AcceptOffer --> |If Liquidity remains, refund into new offer| Offer
    CreateMatch -.- Comment
    CreateMatch --> Match
    Match --> PayoutMatch --> MatchPaidOut
    Match --> VoidMatch --> MatchVoided

    style Offer fill:#2E86AB
    style OfferExpired fill:#2E86AB
    style OfferCancelled fill:#2E86AB
    style Match fill:#2E86AB
    style MatchPaidOut fill:#2E86AB
    style MatchVoided fill:#2E86AB

    Comment@{ shape: comment, label: "If Liquidity remains on either Offer, it will be refunded into new Offer States." }

One interesting thing to note about this example is that the CreateMatch action depends upon two states (the Yes side of the Offer and the No side of the Offer).

To handle cases like this, CashConnect supports the use of Objects as a param type. The code for the CreateOffer action then looks something like this:

ts
const EventPredictionsBCHCreateMatch = {
  params: {
    offerNo: {
      type: 'object',
      properties: OfferState,
    },
    offerYes: {
      type: 'object',
      properties: OfferState,
    },
  },
  instructions: [
    // ...
  ]
  returns: {
    match: {
      type: 'object',
      properties: MatchState,
    },
    // NOTE: These offer states are only available if there is remaining liquidity
    newOfferNo: {
      type: 'object',
      properties: OfferState,
      required: false,
    },
    newOfferYes: {
      type: 'object',
      properties: OfferState,
      required: false,
    },
  },
}

Note in the above that required is set to false for newOfferNo and newOfferYes: For BCH Guru's Event Predictions Product, new offers are only created assuming there is enough remaining liquidity (otherwise, OfferDepleted virtual states are created in the backend so that users can track what has occurred).

Managing multiple states does require a bit of juggling and CashConnect provides utilities in the @cashconnect-js/templates-dev package to help with this. Some of the available utilities are:

ts
// Creates a new state by mapping fields to a prefixed namespace.
Instructions.createState(...);

// Forwards all fields from a prefixed state, allowing selective overrides.
Instructions.forwardState(...);

// Copies all fields from one prefixed state to another prefix, allowing selective overrides.
Instructions.copyState(...);

// Flattens a prefixed state into unprefixed keys.
Instructions.flattenState(...);

To give an example of how these are used in the CreateMatch action:

ts
{
  instructions: [
    // ... Preceding Transaction Instructions
    Instructions.createState('match', MatchState, {
      matchTxHash: expr`createMatchTxHash`,
      matchTxIndex: expr`0`,
      matchAmount: expr`totalContribution`,
      eventId: expr`offerNo.eventId`,
      oraclePublicKey: expr`offerNo.oraclePublicKey`,
      guruPublicKey: expr`offerNo.guruPublicKey`,
      maturityTimestamp: expr`offerNo.maturityTimestamp`,
      guruFeeLockingBytecode: expr`offerNo.guruFeeLockingBytecode`,
      dividendFeeLockingBytecode: expr`offerNo.dividendFeeLockingBytecode`,
      oracleFeeLockingBytecode: expr`offerNo.oracleFeeLockingBytecode`,
      noPayoutLockingBytecode: expr`offerNo.payoutLockingBytecode`,
      yesPayoutLockingBytecode: expr`offerYes.payoutLockingBytecode`,
      noContribution: expr`noContribution`,
      yesContribution: expr`yesContribution`,
    }),
    Instructions.if(
      expr`noRemainingLiquidity >= offerNo.minAllowedOfferAmount`,
      // Forward the previous offerNo state and over-ride updated parameters.
      Instructions.copyState('offerNo', 'newOfferNo', OfferState, {
        offerTxHash: expr`createMatchTxHash`,
        offerOutpointIndex: expr`2`,
        liquidityAmount: expr`noRemainingLiquidity`,
      })
    ),
    Instructions.if(
      expr`yesRemainingLiquidity >= offerYes.minAllowedOfferAmount`,
      // Forward the previous offerYes state and over-ride updated parameters.
      Instructions.copyState('offerYes', 'newOfferYes', OfferState, {
        offerTxHash: expr`createMatchTxHash`,
        offerOutpointIndex: expr`3`,
        liquidityAmount: expr`yesRemainingLiquidity`,
      })
    ),
  ]
  // ...
}

Quirk

CashASM variables are flattened and uses the same character set that JS variables use. This means that . characters are not supported. CashConnect instead uses __ as a separator and the expr utility automatically replaces all instances of . with __ characters. If you are using raw CashASM (instead of the expr utility), you must make sure to use __ when accessing nested object properties.

The above approach leads to a lot of repetition in the resulting JSON schema so it would be ideal to formalize these operations eventually. However, before attempting this formalization, we probably want to have a larger repertoire of Contract-Systems and Templates to work with.

Other Examples

BCHouse/Flipstarter

I've lost the Mermaid code, but see picture diagram on CashConnect Telegram Group here.

Wallet/Vault Rough Example

I'll give more detail on this in a future tutorial. States are not treated as UTXOs in this case.

flowchart LR
    CreateWallet --> WalletState
    WalletState --> GetReceivingAddress
    WalletState --> SignTransaction

    style WalletState fill:#2E86AB

In the next tutorial, we'll look at resolving lockscript precommits and a more functional approach to contract system development.