Skip to main content

Stateful Contracts

SolidCash supports stateful contracts — contracts that store persistent state across transactions using CashToken NFT commitments. Unlike constructor parameters (which are immutable once set), state variables can be read and modified in every function call.

note

Stateful contracts require p2sh32 address type. The SDK enforces this at construction time.

State Variables

State variables are declared at the top level of a contract, outside any function. They persist across transactions.

pragma solidcash >=0.8.0;

contract Counter(pubkey admin) {
int public counter;
bytes20 internal owner;
bool private initialized;
}

Visibility

ModifierAccess
publicReadable from external contracts; auto-generates a getter
internalAccessible within the contract and inheriting contracts
privateAccessible only within the declaring contract

Constants & Immutables

contract Config() {
int constant MAX_SUPPLY = 1_000_000; // compile-time, must initialize
bytes20 immutable deployer; // set in constructor, read-only after

constructor() {
this.deployer = hash160(msg.sender);
}
}
  • constant — value fixed at compile time; cannot reference runtime values.
  • immutable — assigned once in the constructor; read-only in all other functions.

State Variable Assignment

State variables are accessed and modified using the this. prefix.

function increment() {
this.counter = this.counter + 1;
this.owner = hash160(msg.sender);
}
caution

State changes are enforced by covenant verification. The compiler generates bytecode that checks the output NFT commitment matches the expected new state. If the state you set doesn't match what the bytecode computes, the transaction fails.

Constructor

Stateful contracts can have an explicit constructor to initialize state.

contract Token(pubkey admin) {
int totalSupply;
bytes20 deployer;

constructor() {
this.totalSupply = 1_000_000;
this.deployer = hash160(admin);
}
}
caution

Mappings and token containers cannot be accessed in the constructor because no UTXO commitment exists yet at deployment time.

Mappings

Mappings are key-value stores backed by entry NFTs — one NFT per key.

contract Ledger() {
mapping(bytes20 => int) public balances;

function setBalance(bytes20 account, int amount) {
this.balances[account] = amount;
}

function getBalance(bytes20 account) returns (int) {
return this.balances[account];
}
}

Named Keys & Values

For documentation clarity, keys and values can be named:

mapping(bytes20 holder => int balance) public balances;

Nested Mappings

mapping(bytes20 => mapping(bytes32 => int)) public allowances;

function approve(bytes20 owner, bytes32 spender, int amount) {
this.allowances[owner][spender] = amount;
}

Deleting Entries

delete this.balances[account];

This removes the entry NFT for the given key.

Mapping Iteration

SolidCash can iterate over mappings — something impossible in Solidity. Three iteration modes are supported:

Iterate Values

function totalBalance() returns (int) {
int sum = 0;
for (int balance of this.balances) {
sum = sum + balance;
}
return sum;
}

Iterate Keys

function countHolders() returns (int) {
int count = 0;
for (bytes20 holder in this.balances) {
count = count + 1;
}
return count;
}

Iterate Entries (Key-Value Pairs)

function audit() {
for (bytes20 holder, int balance of this.balances.entries) {
require(balance >= 0);
}
}
tip

Mapping iteration is compiled to entry NFT enumeration. All entry NFTs for the mapping are included as transaction inputs and iterated at script execution time.

Dynamic Arrays

State arrays support push, pop, index access, and deletion.

contract TodoList() {
string[] public items;

function addItem(string item) {
this.items.push(item);
}

function removeItem(int index) {
this.items.delete(index);
}

function removeLast() {
this.items.pop();
}

function updateItem(int index, string newItem) {
this.items[index] = newItem;
}
}

Packed Arrays

For fixed-width element types, the packed modifier stores the entire array as a single blob for better space efficiency:

int[] packed public scores;
note

Packed arrays are only supported for types with a known fixed byte width (e.g., int8, uint32, bytes20). Variable-width types like string or bytes cannot be packed.

Token Containers

Token containers manage CashToken fungible tokens (FTs) and NFTs held by the contract.

contract Vault() {
function checkBalance(bytes32 tokenCategory) returns (int) {
return this.tokens[tokenCategory].tokenAmount;
}

function withdrawFT(bytes32 tokenCategory, bytes20 recipient, int amount) {
require(this.tokens[tokenCategory].tokenAmount >= amount);
this.tokens[tokenCategory].transfer(
new LockingBytecodeP2PKH(recipient), amount
);
}

function withdrawNFT(bytes32 nftCategory, bytes20 recipient, bytes commitment) {
this.tokens[nftCategory].transferNft(
new LockingBytecodeP2PKH(recipient), commitment
);
}
}

Token Container Properties

PropertyTypeDescription
.tokenAmountintFungible token balance
.tokenCategorybytes32Token category ID
.nftCommitmentbytesNFT commitment data

Token Container Methods

MethodDescription
.transfer(lockingBytecode, amount)Send fungible tokens to a recipient
.transferNft(lockingBytecode, commitment)Send an NFT to a recipient
note

Token container keys (bytes32 tokenCategory) must be function parameters, not arbitrary expressions. This ensures the covenant can verify the category at compile time.

Satoshi Container

The satoshi container manages the contract's native BCH balance.

contract Treasury(pubkey admin) {
int counter;

function sendFunds(sig s, bytes20 recipient, int amount) {
require(checkSig(s, admin));
this.transfer(new LockingBytecodeP2PKH(recipient), amount);
this.counter = this.counter + 1;
}
}

Deploy with an initial balance using the SDK:

const { contract } = await StatefulContract.deploy({
// ...
initialBalance: 50_000n, // 50,000 satoshis in satoshi container
});

Partial Struct Field Updates

When a struct is stored in a mapping, individual fields can be updated without rewriting the entire struct:

struct User {
bytes20 addr;
string name;
int balance;
}

contract Registry() {
mapping(bytes20 => User) public users;

function updateName(bytes20 key, string newName) {
this.users[key].name = newName; // only updates name field
}
}

How State Storage Works

Under the hood, stateful contracts use CashToken NFTs to store state:

NFT TypePurposeCapability
Master NFTContract identity and controlminting
Data NFTsState variable values (128 bytes each)mutable
Entry NFTsMapping key-value pairs (one per key)mutable
Event NFTsEmitted event recordsmutable
Token Container NFTsFT/NFT balances per categorymutable
Satoshi ContainerBCH balance (plain UTXO)N/A

Every state-modifying transaction consumes these NFTs as inputs and produces updated versions as outputs. The contract bytecode verifies that output commitments match the expected new state — this is the covenant mechanism that makes stateful contracts trustless.

tip

The SDK handles all NFT management automatically via auto-derivation: it runs the contract bytecode in a local VM, discovers which state changes are needed, and constructs the full transaction including all covenant outputs.