Skip to main content

Smart Contracts

Overview​

ULedgerNET smart contracts are programs written in WebAssembly (WASM) and executed in the ULVM (ULedgerNET Virtual Machine, ULedgerNET's sandboxed, gas-metered runtime built on wazero, a pure-Go WASM engine with no CGo dependencies).

Contracts define the application-level logic for governance, token operations, agent authorization, data management, and any other custom on-chain computation. The protocol enforces that whatever the contract records happened; the STARK proof layer guarantees the audit trail.


The ULVM Runtime​

Contract binary (WASM)
│
▼
wazero compilation (cached per bytecode SHA256)
│
▼
Module instantiated per invocation
│
▼
Execution with:
- Gas metering (base + per-byte costs)
- Wall-clock timeout (5 seconds)
- Storage transaction (commit on success, rollback on failure)
- Access to host functions via ulvm.env.*
│
▼
Success → storage committed, events emitted
Failure → storage rolled back, gas consumed, error recorded

Concurrency​

The runtime bounds concurrent contract invocations at NumCPU() using a semaphore. This limits the linear memory footprint under high load. Invocations beyond the semaphore bound are queued, not rejected.

Compilation Cache​

WASM modules are compiled once and cached globally in a sync.Map keyed by bytecode SHA256. Subsequent invocations of the same contract bytecode skip recompilation, making repeated invocations cheap.


Host Functions (ULVM Interface)​

Contracts interact with the chain through a set of host functions provided by the ulvm.env module:

FunctionDescription
get_caller()Returns the address of the wallet that invoked the contract.
get_owner()Returns the address of the contract's owner wallet.
get_block_height()Returns the current block height at execution time.
get_storage(key)Reads a value from the contract's persistent storage.
set_storage(key, value)Writes a value to the contract's persistent storage.
delete_storage(key)Deletes a key from the contract's persistent storage.
has_key(key)Checks whether a key exists in storage.
emit_event(data)Emits an event (visible in block receipts and event subscriptions).
hash_data(dataPtr, strategy)Hashes data using SHA256, SHA3-256, or SHA3-512.
log(string)Emits a log message (visible in node logs, not on-chain).
abort()Traps the WASM module (signals a fatal error).

All storage operations (get_storage, set_storage, delete_storage) operate within a storage transaction. If the contract execution fails or reverts, all storage changes are rolled back atomically.


Gas Metering​

Every operation in a contract invocation costs gas:

OperationGas Cost
Base invocation costFixed per invocation
WASM instruction executionPer-instruction metering
get_storage(key)Base + per-byte of key
set_storage(key, value)Base + per-byte of key + per-byte of value
delete_storage(key)Base + per-byte of key
emit_event(data)Base + per-byte of event data
hash_data(data)Base + per-byte of input

Gas limits are set at invocation time via the INVOKE_SMART_CONTRACT transaction payload. If execution exhausts the gas limit before completion, the invocation fails; storage is rolled back but gas is consumed.

The council-voting contract example uses a 100_000 gas limit per invocation.


Contract Lifecycle Transactions​

TransactionDescription
DEPLOY_SMART_CONTRACTDeploy a new contract. Validates WAT/WASM binary, compiles, stores bytecode. Owner is the deploying wallet.
INVOKE_SMART_CONTRACTCall a contract function with arguments and a gas limit.
UPGRADE_SMART_CONTRACTReplace the contract's bytecode. Version is incremented. Only owner (or wallet with upgrade permission) can upgrade.
ROLLBACK_SMART_CONTRACTRevert to the previous version's bytecode.

Contract Addresses​

A contract's address is the transaction ID of its DEPLOY_SMART_CONTRACT transaction, deterministic from the deployment context.

Versioning​

Contracts carry a Version counter and a PreviousVersion reference. Upgrading increments version; rollback restores the prior bytecode. The version history is on-chain.


Writing Contracts (AssemblyScript → WAT → WASM)​

Contracts are authored in AssemblyScript (a TypeScript-like language that compiles to WASM), then compiled to .wat (WebAssembly Text format) and binary .wasm.

Contract Structure​

// Import host functions
@external("env", "get_caller")
declare function get_caller(outPtr: i32): void;

@external("env", "set_storage")
declare function set_storage(keyPtr: i32, keyLen: i32, valPtr: i32, valLen: i32): void;

@external("env", "get_storage")
declare function get_storage(keyPtr: i32, keyLen: i32, outPtr: i32, outLen: i32): i32;

@external("env", "emit_event")
declare function emit_event(dataPtr: i32, dataLen: i32): void;

// Exported contract functions
export function myFunction(arg: i32): i32 {
// Implementation
}

Storage Key Design​

Contract state is stored as key-value pairs in the contract's isolated storage namespace. Key design determines queryability:

council:member:<address> → membership marker ("1")
proposal:<id>:status → "active" | "passed" | "failed"
proposal:<id>:votes:yes → vote count (stringified integer)
voted:<proposalId>:<memberAddr> → voter marker ("1")
token:<memberAddr>:<id>:agent → work token marker

Keys are UTF-16 encoded strings in WASM linear memory (2 bytes per character) when using AssemblyScript.


The Council Voting Contract​

The council-voting.wat contract (deployed in the board-council demo and council E2E suite) is the reference example for governance contracts on ULedger.

Exported Functions​

FunctionCallerDescription
addMember(address)Owner onlyAdd a wallet as a council member.
removeMember(address)Owner onlyRemove a council member.
isMember(address) → boolAnyCheck if a wallet is a member.
authorizeAgent(agentAddr)MemberDelegate voting to an agent wallet.
revokeAgent(agentAddr)MemberRevoke agent delegation.
createProposal(title, desc, durationBlocks, quorum) → idMemberCreate a proposal with block-height deadline.
issueWorkToken(agentAddr, proposalId)MemberIssue a token authorizing the agent to vote on the proposal.
submitWork(proposalId, vote, evidence)AgentCast a vote (1=yes, 2=no, 3=abstain) with optional evidence string.
finalizeProposal(proposalId)AnyTally votes and close the proposal after deadline.
getProposalStatus(proposalId) → stringAnyRead the current proposal status.

Vote Values​

ValueMeaning
1Yes
2No
3Abstain

Access Control Pattern​

The contract enforces its own access control using get_caller() and storage lookups, not ULedger wallet permissions. This is the standard pattern: ULedger capabilities gate whether the wallet can invoke the contract at all; the contract's internal logic gates which functions the caller can execute within the contract.


Contract Events​

Contracts emit events via emit_event(). Events appear in:

  • Block receipts (available via node API).
  • WebSocket event subscriptions (real-time streaming).
  • The SDK's event listener (used by the board-council driver for SSE streaming to the frontend).

Events are part of the block's Merkle record and are therefore provable by the STARK audit trail.


Deployment Example (Go SDK)​

// Deploy the council voting contract
contractAddr, err := deployer.DeployContract(ctx, contractBinary, sdk.DeployOpts{
GasLimit: 1_000_000,
IsUpgradeable: true,
})

// Invoke addMember
receipt, err := sdk.InvokeContract(ctx, contractAddr, "addMember", memberWalletAddr, sdk.InvokeOpts{
GasLimit: 100_000,
CallerWallet: ownerWallet,
})

// Verify state
status, err := sdk.ReadContractState(ctx, contractAddr, "council:member:"+memberWalletAddr)