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:
| Function | Description |
|---|---|
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:
| Operation | Gas Cost |
|---|---|
| Base invocation cost | Fixed per invocation |
| WASM instruction execution | Per-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
| Transaction | Description |
|---|---|
DEPLOY_SMART_CONTRACT | Deploy a new contract. Validates WAT/WASM binary, compiles, stores bytecode. Owner is the deploying wallet. |
INVOKE_SMART_CONTRACT | Call a contract function with arguments and a gas limit. |
UPGRADE_SMART_CONTRACT | Replace the contract's bytecode. Version is incremented. Only owner (or wallet with upgrade permission) can upgrade. |
ROLLBACK_SMART_CONTRACT | Revert 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
| Function | Caller | Description |
|---|---|---|
addMember(address) | Owner only | Add a wallet as a council member. |
removeMember(address) | Owner only | Remove a council member. |
isMember(address) → bool | Any | Check if a wallet is a member. |
authorizeAgent(agentAddr) | Member | Delegate voting to an agent wallet. |
revokeAgent(agentAddr) | Member | Revoke agent delegation. |
createProposal(title, desc, durationBlocks, quorum) → id | Member | Create a proposal with block-height deadline. |
issueWorkToken(agentAddr, proposalId) | Member | Issue a token authorizing the agent to vote on the proposal. |
submitWork(proposalId, vote, evidence) | Agent | Cast a vote (1=yes, 2=no, 3=abstain) with optional evidence string. |
finalizeProposal(proposalId) | Any | Tally votes and close the proposal after deadline. |
getProposalStatus(proposalId) → string | Any | Read the current proposal status. |
Vote Values
| Value | Meaning |
|---|---|
1 | Yes |
2 | No |
3 | Abstain |
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)