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:
mutualRedeempayout
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:
create(Create an AnyHedge UTXO)mutualRedeem(Mutually redeem an existing AnyHedge UTXO)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":
- 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).
- You have actions (
create,mutualRedeemandpayout). - 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:
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:
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:
{
// ...
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:
- For SQL-like databases, you could create a table per each "state".
- 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:
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.
EventPredictionsOfferBCHEventPredictionsMatchBCHEventPredictionsMatchSidecarBCH
If we were to model this using our Action/States approach, the chart might look as follows:
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:
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:
// 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:
{
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.
In the next tutorial, we'll look at resolving lockscript precommits and a more functional approach to contract system development.