Contract Deployment
When Do You Need a Deployment?
In the UTXO model, multiple UTXOs can live on the same address and are spendable under the same conditions. Many contracts, like multisig wallets, vaults, or escrows, work exactly this way. You compile the contract, share the address, and anyone can send funds to it. There is no "deployment" step: the contract is ready to use the moment you know its address.
Deployment becomes necessary when you're building stateful contract systems with authentication. In these systems, a unique CashToken category (token ID) identifies the contract and its state. The token ID is created through a special genesis transaction, and the contract UTXOs are initialized with the right tokens, capabilities, and state. This is what we mean by "deploying" a contract.
If your contract simply enforces spending conditions (like requiring multiple signatures or a timelock), you don't need a deployment. Just use the contract address. Deployment is for systems where CashTokens authenticate and track contract state.
Before the Genesis Transaction
Contract Addresses
CashScript contract addresses are deterministic: they are derived from the contract's compiled bytecode (the artifact) combined with the constructor arguments. Given the same artifact and the same arguments, you will always get the same address.
import { Contract } from 'cashscript';
import artifact from './my_contract.artifact.js';
const constructorArgs = [oraclePublicKey, startBlockHeight] as const;
const contract = new Contract(artifact, [...constructorArgs], { provider });
console.log(contract.address); // same inputs → same address every time
console.log(contract.tokenAddress); // CashToken-aware address
This means you can reconstruct a contract's address at any time, on any machine, without querying the blockchain, as long as you have the artifact and the constructor arguments. This property is essential for verifying deployments.
Preparing Constructor Arguments
Some constructor arguments are straightforward constants (block heights, timelocks). Others require preparation before deployment:
- Token IDs from other categories — in complex systems with multiple token categories, contracts often reference each other's token IDs as constructor arguments. Note that token IDs must be byte-reversed when passed as constructor arguments, because the Bitcoin Cash VM uses little-endian byte order internally while token IDs are displayed in big-endian (the same applies to txids).
- Locking bytecodes from other contracts — in multi-contract systems, one contract may reference another by locking bytecode. This creates a dependency chain: you must instantiate the referenced contract first, extract its locking bytecode, and pass it as a constructor argument to the dependent contract.
- Public keys — for contracts that validate signed messages (e.g. from an oracle provider), you need the signer's public key. For owner authentication, you need to derive the public key hash from a keypair ahead of time.
import { binToHex, cashAddressToLockingBytecode } from '@bitauth/libauth';
// Reverse byte order of a hex string (big-endian ↔ little-endian)
function reverseHex(hex: string): string {
return hex.match(/../g)!.reverse().join('');
}
// Token IDs must be byte-reversed for use as constructor args
const argsA = [reverseHex(tokenIdB), reverseHex(tokenIdC)] as const;
const contractA = new Contract(artifactA, [...argsA], { provider });
// Extract locking bytecode to pass to a dependent contract
const lockingBytecodeResult = cashAddressToLockingBytecode(contractA.address);
if (typeof lockingBytecodeResult === 'string') throw new Error('Invalid address');
const lockingBytecodeA = binToHex(lockingBytecodeResult.bytecode);
const argsB = [lockingBytecodeA, oraclePublicKey, startBlockHeight] as const;
const contractB = new Contract(artifactB, [...argsB], { provider });
Constructor arguments are baked into the contract address. If any argument changes, even a single byte of a public key, the contract address changes entirely. Double-check all arguments before deploying. Some parameters, like fee destination addresses, are permanent once deployed and cannot be changed. The wallets behind these addresses require proper creation, backup, and security before deployment.
Token IDs
In Bitcoin Cash, a new token category is created when a UTXO with vout: 0 is spent as input index 0 of a transaction. Both conditions must be met. The resulting token ID equals the txid of that spent UTXO.
// The token ID equals the txid of the vout0 UTXO spent as input index 0
const tokenId = genesisUtxo.txid;
Once you have the vout0 UTXO, its txid (the future token ID) is already known before constructing the genesis transaction.
Setup Wallet
To create vout0 UTXOs and broadcast the genesis transaction, you need a funded wallet. This is typically a standard P2PKH wallet derived from a WIF private key or an HD seedphrase, with enough BCH to fund all contract outputs (each needs at least dust amount, typically 1000 sats).
The setup wallet may already have UTXOs at vout 0, but usually you need to prepare them. You can do this by sending BCH from the setup wallet back to itself as the sole output of a transaction. Since it's the only output, it will be at index 0.
import { TransactionBuilder, SignatureTemplate, ElectrumNetworkProvider } from 'cashscript';
import type { Utxo } from 'cashscript';
// Helper to create a vout0 UTXO by sending BCH to yourself
async function createVout0(
provider: ElectrumNetworkProvider,
address: string,
utxos: Utxo[],
template: SignatureTemplate,
amount: bigint,
): Promise<Utxo> {
// Pick a non-vout0 UTXO to avoid consuming an existing vout0
const selectedUtxo = utxos.find(utxo => utxo.satoshis > amount && utxo.vout !== 0 && !utxo.token);
if (!selectedUtxo) throw new Error('No eligible UTXO available');
const txBuilder = new TransactionBuilder({ provider });
txBuilder.addInput(selectedUtxo, template.unlockP2PKH());
txBuilder.addOutput({ to: address, amount: selectedUtxo.satoshis - 500n });
const txDetails = await txBuilder.send();
return { satoshis: selectedUtxo.satoshis - 500n, txid: txDetails.txid, vout: 0 };
}
For deployments with multiple token categories, you need multiple vout0 UTXOs, one per token category you want to create. Since contracts may reference each other's token IDs as constructor arguments, it is recommended to prepare all vout0 UTXOs first so that every token ID is known before instantiating any contracts. The genesis transactions themselves can then be broadcast in parallel since they have no inter-transaction dependencies.
If you plan to include a BCMR OP_RETURN in your genesis transaction, keep the setup wallet "quiet" with minimal transaction history. Some wallets resolve token metadata by querying all transactions on the vout0 address, and a busy address can cause this resolution to be slow or fail.
Always test your full deployment flow on chipnet before mainnet. Validating your transaction structure, constructor arguments, and initial state encoding on chipnet first avoids costly mistakes.
Genesis Transaction
The genesis transaction creates the CashToken category and distributes the initial tokens to contract addresses. This typically includes:
- Fungible tokens — the initial token supply distributed to contract addresses
- NFTs with minting capability — for contracts that need to create new NFTs during operation
- NFTs with mutable capability — for contracts that store updateable state in the NFT commitment
- NFTs with no capability (immutable) — for contracts that carry fixed identifying data
When fungible token supply will be locked inside covenants, it is common to mint the maximum possible amount (9223372036854775807). This is only safe when the covenants strictly enforce the actual circulating supply. Minting the max avoids needing to predict future supply needs.
import { TransactionBuilder, SignatureTemplate } from 'cashscript';
const template = new SignatureTemplate(privateKey);
const tokenId = genesisUtxo.txid;
const txBuilder = new TransactionBuilder({ provider });
txBuilder.addInput(genesisUtxo, template.unlockP2PKH());
txBuilder.addOutput({
to: contractA.tokenAddress, amount: 1000n,
token: { category: tokenId, amount: 9223372036854775807n },
});
txBuilder.addOutput({
to: contractB.tokenAddress, amount: 1000n,
token: {
category: tokenId, amount: 0n,
nft: { capability: 'minting', commitment: initialStateHex },
},
});
// ... add more outputs as needed
txBuilder.addBchChangeOutputIfNeeded({ to: changeAddress, feeRate: 1.0 });
const txDetails = await txBuilder.send();
Initial State via NFT Commitments
NFT commitments are used to encode the initial state of a contract at deployment. For example, a contract might store a starting counter, a block height, or a configuration value in its NFT commitment. The commitment is a hex-encoded byte string that the contract's covenant logic knows how to read and update.
Use @bitauth/libauth to encode values into commitment bytes. This ensures values follow the same VM number encoding that the Bitcoin Cash VM uses:
import { binToHex, bigIntToVmNumber, padMinimallyEncodedVmNumber } from '@bitauth/libauth';
// Encode a bigint as a fixed-length VM number (matching OP_NUM2BIN behavior)
function bigIntToFixedBytes(value: bigint, byteLength: number): string {
const minimal = bigIntToVmNumber(value);
if (minimal.length > byteLength) throw new Error('value exceeds the requested byteLength');
// Return early if already the right length — padMinimallyEncodedVmNumber
// would overshoot by one byte in this case
if (minimal.length === byteLength) return binToHex(minimal);
return binToHex(padMinimallyEncodedVmNumber(minimal, byteLength));
}
// Encode initial state as a commitment (e.g. 4-byte counter + 4-byte blockHeight)
function encodeInitialState(counter: bigint, blockHeight: bigint): string {
return bigIntToFixedBytes(counter, 4) + bigIntToFixedBytes(blockHeight, 4);
}
UTXO Duplication
For systems expecting concurrent usage, you can create multiple identical contract UTXOs in the same genesis transaction. Each duplicate UTXO sits on the same contract address with the same token type, allowing independent transactions to spend different UTXOs without conflicting. The number of duplicates you create determines the system's concurrency capacity. See accidental race-conditions in the lifecycle guide for more on why this matters.
When duplicating contract UTXOs that hold fungible tokens, the total supply should be evenly distributed across them. The last UTXO can absorb the remainder to ensure the total is exact:
const supplyPerUtxo = MAX_TOKEN_SUPPLY / BigInt(numberOfDuplicates);
const remainder = MAX_TOKEN_SUPPLY % BigInt(numberOfDuplicates);
const supplyLastUtxo = supplyPerUtxo + remainder;
BCMR Authchain
The Bitcoin Cash Metadata Registry (BCMR) is the standard for associating metadata (name, ticker, decimals, icon) with CashToken categories. The metadata is resolved through an authchain, a chain of transactions starting from a specific output in the genesis transaction.
To set up the authchain during deployment, include a dust output (e.g. 1000 sats) to a designated authchain address as part of the genesis transaction. This output becomes the starting point for metadata resolution. Wallets and indexers follow the authchain from this output to find the latest BCMR metadata.
// Include a dust output for the BCMR authchain
txBuilder.addOutput({ to: bcmrAuthchainAddress, amount: 1000n });
Metadata can be published in the genesis transaction itself by including an OP_RETURN output with the BCMR protocol identifier, the SHA-256 hash of the registry JSON, and the registry URL. Alternatively, the metadata can be published in a later authchain transaction. This approach is simpler since you don't need to know the registry hash at genesis time. See the CashTokens guide for more on BCMR metadata and tooling.
In a two-step deployment, you prepare the vout0 UTXOs first (learning the token IDs), publish the BCMR registry, then broadcast the genesis transaction with the registry hash in the OP_RETURN. This avoids needing a follow-up authchain update.
Post-Deployment
Once the genesis transaction is broadcast:
- Save the deployment configuration — persist the final deployment config (token IDs, constructor args, network) so it can be referenced by application code, verification scripts, and future deployments.
- Verify BCMR indexing — if you published BCMR metadata, check that a BCMR indexer has resolved your token metadata correctly. Wallets rely on this for displaying token names and icons.
- Verify the deployment — if you built a standalone verification script, run it against the live deployment to confirm everything matches.
- Set up infrastructure — see the infrastructure guide for guidance on storing contract details and setting up transaction servers.
Deployment Configuration
After the genesis transaction, it's worth capturing all deployment parameters into a typed configuration object. This makes deployments reproducible and easy to reference in application code, verification scripts, and tests.
A deployment configuration typically includes:
- Name and network — a human-readable identifier and whether it targets
mainnetorchipnet - Contract version — which version of the contract artifacts are being deployed
- Token IDs — the CashToken category IDs created during genesis
- Contract parameters — the constructor arguments used to instantiate the contracts
interface MyDeployment {
name: string;
network: 'mainnet' | 'chipnet';
contractVersion: string;
tokenIds: {
myTokenId: string;
};
contractParams: {
oraclePublicKey: string;
startBlockHeight: bigint;
// ... other params
};
}
Maintaining named deployment objects lets you keep multiple deployments side by side: production, staging, and testing variants with different parameters (e.g. testing oracles), so you can iterate quickly during development before committing to a mainnet deployment.
Use your deployment configuration as the single source of truth for verification scripts, application code, and documentation. If the deployment config is correct, everything downstream can be derived from it.
Verifying a Deployment
A contract system is only trustless if its deployment can be independently verified. Deployment scripts may not be open source, and even if they are, there is no guarantee the published script is what was actually used. Contract developers should provide standalone verification scripts so that security researchers and technical users can independently confirm a deployment's correctness.
Since contract addresses are deterministic, verification works by reconstructing every expected address from the contract artifacts and deployment parameters, then comparing the result against what actually exists on-chain.
import { Contract } from 'cashscript';
import artifact from './my_contract.artifact.js';
// Reconstruct all contract addresses from the deployment config
const constructorArgs = [
deployment.contractParams.oraclePublicKey,
deployment.contractParams.startBlockHeight,
] as const;
const contract = new Contract(artifact, [...constructorArgs], { provider });
// This should match the address where tokens were sent
const expectedAddress = contract.tokenAddress;
Provide verification scripts as part of your project, not bundled into the deployment script itself. They should be runnable independently by anyone with the contract artifacts and deployment configuration.
What to Verify
Verification should inspect every output of the genesis transaction, not just the ones you expect. If tokens are sent to an unexpected address, like a regular P2PKH, this could give full authority over the token category to the deployer. For example, a minting NFT on a personal address means the holder can mint unlimited tokens outside the contract's rules.
Build a list of all expected contract addresses, then iterate over all outputs and check:
- No unexpected token outputs — every output carrying token data must go to a known contract address
- Token category — do the outputs carry the correct token ID?
- NFT capability — is it
minting,mutable, ornoneas expected for each contract address? - NFT commitment — does it contain the correct initial state?
- Fungible token amount — is the total supply distributed correctly? Fungible tokens should only appear on the expected contract addresses.
- Genesis transaction inputs — does the first input's previous txid match the expected token ID? This confirms the token was genuinely created in this transaction.
// Build the list of all expected contract addresses
const expectedAddresses = new Set([
contractA.tokenAddress,
contractB.tokenAddress,
// ... all expected contract addresses
]);
// Iterate over ALL outputs of the genesis transaction
for (const output of genesisTxOutputs) {
if (output.tokenData && !expectedAddresses.has(output.address)) {
throw new Error(`Unexpected token output to address: ${output.address}`);
}
// Validate each known address has the right capability, commitment, and amount
}