# Introduction
Source: https://docs.kontor.network/docs/consensus/introduction
Kontor is the first metaprotocol to transcend Bitcoin's 10-minute block time and transaction throughput limits, offering confirmations in only 1–2 seconds, dramatically lower fees, and over 1,000 transactions per second—all while relying entirely on Bitcoin consensus for transaction finality.
Like all metaprotocols, every Kontor transaction is simply extra data stored inside regular Bitcoin transaction. **Embedded consensus** is the foundation: Kontor indexers scan Bitcoin blocks, find Kontor transactions, and execute them according to the Kontor protocol rules. Because Bitcoin provides an immutable log and Kontor's rules are deterministic, every indexer arrives at the same state.
Kontor, however, includes three sets of innovations which greatly improve the performance, scalability and functionality of the Bitcoin network without compromising its security guarantees:
[Transaction Encoding](/docs/consensus/transaction-encoding) — Kontor offers the best of both worlds when it comes to token transfer semantics: Kontor tokens can be "attached" to Bitcoin UTXOs for trading with standard Bitcoin tools (PSBTs, atomic swaps), or "detached" for direct use in smart contracts.
[Optimistic Consensus](/docs/consensus/optimistic-consensus) — Kontor includes a novel consensus layer that offers real-time transaction finality with a Byzantine fault–tolerant prediction market on Bitcoin's mempool.
[Scalability](/docs/consensus/scalability) — Kontor's transaction encoding scheme is heavily optimized for space efficiency, with trustless BLS signature aggregation, compact registry IDs, efficient binary encoding, and data compression, allowing Kontor to achieve 1,000+ TPS at less than 10 WU per transaction.
# Optimistic Consensus
Source: https://docs.kontor.network/docs/consensus/optimistic-consensus
Bitcoin blocks take ten minutes on average, and prudent recipients wait for multiple confirmations before considering a transaction final. For many applications—exchanges, point-of-sale, real-time trading—this latency is prohibitive. Kontor's optimistic consensus provides intra-block confirmations while preserving Bitcoin as the ultimate source of truth.
This is possible because stateful metaprotocols have an extra degree of freedom that Nakamoto consensus leaves unspecified: the ordering of transactions within a single Bitcoin block. Two transactions that modify the same contract state produce different results depending on which executes first. Since all transactions in a block appear to the network simultaneously, this ordering is semantically arbitrary—it must be resolved by some rule, but the choice of rule is up to the metaprotocol.
Rather than waiting for Bitcoin confirmation to determine ordering, a network of KOR stakers runs BFT consensus to produce signed transaction batches that assign deterministic positions *before* Bitcoin confirmation. Users can transact with confidence after batch inclusion—typically within 1–2 seconds—because the ordering is already fixed. When the underlying Bitcoin transaction eventually confirms, execution proceeds at the pre-assigned position.
## A Virtual Prediction Market
When stakers sign a batch, they are participating in a virtual prediction market. Each signature is a prediction backed by slashable collateral: "these transactions will confirm on Bitcoin without conflict before this batch expires." The staker is betting their capital on this outcome.
If the prediction holds—the batched transactions confirm normally on Bitcoin—stakers earn fees and protocol emissions. If the prediction fails—a conflicting transaction confirms on Bitcoin instead—the batched transactions are rolled back and stakers are slashed. This creates strong economic incentives for stakers to only sign batches they believe will succeed.
The market structure provides several properties:
**Economic finality.** When a quorum (>2/3 of total stake) signs a batch, the transaction has economic finality proportional to the total stake at risk. Recipients can calibrate their acceptance threshold based on transaction value—small payments may accept minimal stake backing, while large transfers may require the full quorum.
**Measurable risk.** Unlike vague "pre-confirmation" signals, optimistic consensus makes the remaining risk explicit and quantifiable. The stake backing each batch is public. The slashing conditions are deterministic. Recipients know exactly what is at risk and can make informed decisions.
**Graceful degradation.** If stakers fail to reach consensus, or if the staker network goes offline entirely, transactions still confirm on Bitcoin normally. Unbatched transactions execute at block-end using standard lexicographic ordering. The staker layer accelerates finality but never blocks it.
## Protocol Flow
```mermaid theme={null}
sequenceDiagram
participant User
participant Stakers
participant Bitcoin
participant Recipient
User->>Stakers: Submit transaction
Stakers->>Stakers: Validate & batch via BFT
Stakers->>User: Signed batch (position assigned)
Stakers->>Recipient: Signed batch
Note over Recipient: Optimistic confirmation (~1-2 seconds)
User->>Bitcoin: Broadcast transaction
Bitcoin->>Bitcoin: Mine in block
Note over Recipient: Batch-Confirmed (~10+ minutes)
```
1. **User submits transaction.** The user broadcasts their Kontor transaction to the staker network (and to Bitcoin).
2. **Stakers validate and batch.** Stakers validate the transaction and include it in the next batch via BFT consensus.
3. **Quorum signs batch.** A quorum of stakers (>2/3 of total stake) signs the batch, each committing slashable stake on the batch's success.
4. **User receives confirmation.** The signed batch assigns the transaction a deterministic position. The recipient can now treat the transaction as confirmed, backed by the bonded stake.
5. **Bitcoin confirmation.** When the transaction confirms on Bitcoin, it becomes *Batch-Confirmed*—both batched by stakers and confirmed by Bitcoin. The transaction executes at its batch-assigned position.
## Ordering
Each transaction in a batch receives a position: `(batch_id, index)`. Positions are totally ordered—earlier batches come first, and within a batch, earlier indices come first. Execution proceeds strictly by position, ensuring deterministic execution order even as transactions confirm on Bitcoin out of order.
**Unbatched transactions** (those that confirm on Bitcoin without being batched) execute at "block-end"—after all batched transactions, ordered by their position within the Bitcoin block.
```mermaid theme={null}
flowchart TD
subgraph P["Temporal Order"]
direction LR
Bn["Batchh,n t₁, t₅"]
Bn1["Batchh,n+1 t₃"]
dots["..."]
Bh["Blockh+i t₁, t₂, t₃, t₄, t₅"]
Bn --> Bn1 --> dots --> Bh
end
subgraph E["Execution order"]
direction LR
t1["t₁"] --> t5["t₅"] --> t3["t₃"] --> t2["t₂"] --> t4["t₄"]
end
Bn -.-> t1
Bn1 -.-> t3
Bh -.-> t2
Bh -.-> t4
style Bn fill:#4a90d9,color:#fff
style Bn1 fill:#50b87d,color:#fff
style t1 fill:#4a90d9,color:#fff
style t5 fill:#4a90d9,color:#fff
style t3 fill:#50b87d,color:#fff
style t2 fill:#777,color:#fff
style t4 fill:#777,color:#fff
```
## Conflict Resolution
Bitcoin is the ultimate arbiter. When a batched transaction confirms on Bitcoin, it becomes Batch-Confirmed and executes at its assigned position. If a *conflicting* transaction confirms instead (one that spends the same inputs), the batched transaction is rolled back and stakers who signed the batch are slashed.
## Security Guarantees
The protocol's core properties have been formally verified:
**Safety.** No two valid batches contain conflicting transactions, under honest supermajority (>2/3 stake). This follows from quorum intersection: any two quorums overlap in at least 1/3 stake, and honest stakers refuse to sign conflicting batches.
**Liveness.** Valid transactions are eventually included in batches. If stakers fail to reach consensus, transactions still confirm on Bitcoin and execute at block-end.
**Accountability.** Any safety violation is attributable to at least 1/3 of total stake with cryptographic evidence—the violating stakers' signatures on conflicting batches serve as proof.
***
For the complete specification, see [Optimistic Consensus Specification](/docs/resources/optimistic-consensus).
# Scalability
Source: https://docs.kontor.network/docs/consensus/scalability
As a metaprotocol, Kontor inherits Bitcoin's constraints: 4,000,000 weight units per block and a ten-minute block time. A naive implementation would be limited to roughly the same throughput as Bitcoin itself—about 10 transactions per second. Kontor reaches 1,000+ operations per second by bundling many operations into single Bitcoin transactions and minimizing per-operation payload overhead. Depending on batch size, this reduces weight per operation by 35–120×.
### Transaction Bundling
The most significant optimization is trustless BLS signature aggregation. Vanilla Bitcoin signatures require one 64-byte Schnorr signature per input, and these cannot be aggregated across different keys. BLS signatures can be aggregated: N signatures from N different signers compress into a single 48-byte aggregate signature. Bundling packs multiple signed operations into a single Bitcoin transaction, amortizing the fixed transaction overhead (inputs, outputs, witness structure) across the batch. Kontor users can choose to use their regular Bitcoin addresses, or they can use Kontor BLS addresses for lower fees and greater scalability.
1. **Users sign operations.** Each user signs their Kontor operation using a BLS key derived from their seed phrase. The signature commits to the operation details: target contract, function, arguments.
2. **Bundlers collect operations.** Bundlers are infrastructure providers who collect signed operations from users. Any party can run a bundler—there is no permissioning or staking requirement.
3. **Bundlers aggregate and publish.** The bundler combines all signatures into a single BLS aggregate signature, constructs a Bitcoin transaction containing all operations, and broadcasts it to the Bitcoin network.
4. **Indexers verify and execute.** When the transaction confirms, indexers verify the aggregate signature against all (public key, message) pairs and execute each operation with its designated signer as caller.
Users do not need to trust bundlers. The BLS signature scheme has a key property: given signed operations, *anyone* can produce a valid aggregate.
## Payload-size optimizations
Kontor reduces per-operation payload size via:
### Compact Registry IDs
Kontor maintains a deterministic registry that maps long identifiers (public keys, contract addresses, etc.) to compact 4-byte numeric IDs.
When a new identifier first appears (a new public key signs an operation, or a new contract is deployed), the indexer assigns it the next sequential ID. This is deterministic—all indexers processing the same blocks assign the same IDs.
**Space savings:** 4 bytes instead of 32+ bytes per identifier—an 8–16× reduction in identifier overhead.
### Binary Encoding
Operations are encoded in a compact binary format to remove syntactic overhead and improve compressibility:
```
BinaryCall:
signer_id: 4 bytes (registry ID)
contract_id: 4 bytes (registry ID)
function_index: 2 bytes (index into contract exports)
args: variable (compact serialization)
```
Combined with registry IDs, this produces compact payloads that compress well.
### Zstd Compression
The final layer is Zstd compression. After operations are serialized in binary format with registry IDs, compression exploits redundancy across the batch: repeated contract IDs, common function indices, and similar argument patterns.
## Benchmarks
The following table shows how each optimization contributes to throughput at batch size N=100:
| Configuration | WU/op | TPS | Improvement |
| ---------------------- | ------- | ------- | ----------- |
| Vanilla Bitcoin P2TR | 615 | 11 | baseline |
| Naive Kontor (N=1) | 770–824 | 8 | 0.7× |
| + BLS batching (N=100) | 33–38 | 172–199 | 16–19× |
| + Registry IDs | 21–25 | 264–305 | 25–29× |
| + Binary + Zstd | 15–18 | 366–443 | 35–42× |
Throughput scales with batch size:
| Batch Size | WU/op | TPS | Improvement |
| ---------- | ------- | --------- | ----------- |
| 1 | 696–698 | 9 | 1× |
| 10 | 80–82 | 79–81 | 8× |
| 100 | 15–18 | 366–443 | 35–42× |
| 1,000 | 7–10 | 683–986 | 65–93× |
| 10,000 | 5–8 | 771–1,295 | 73–123× |
## Fee model
### Fee components
Users pay two types of fees:
**Bundling fee.** Compensates the bundler for Bitcoin block space. This is the user's share of the total Bitcoin transaction fee, amortized across all operations in the bundle.
**Ordering fee (optional).** Paid to consensus stakers for optimistic ordering. This is independent of bundling.
### Cost model
The bundler's cost per operation is:
```
cost = (BTC_fee / N) + operational_overhead
```
Where:
* `BTC_fee` is the total Bitcoin transaction fee for the bundle
* `N` is the number of operations in the bundle
* `operational_overhead` covers bandwidth, computation, and infrastructure
As `N` increases, the per-operation share of the Bitcoin transaction fee decreases proportionally to (1/N). For example, a bundler processing 1,000 operations pays roughly the same total Bitcoin fee as a bundler processing 10 operations, but spreads it across 100× more users.
### Fee Burn
A fraction of bundling fees can be burned. The remainder goes to the bundler:
```
bundler_revenue = (1 - burn_rate) × bundling_fee
```
The burn creates deflationary pressure on KOR while ensuring bundlers remain profitable.
### Market Structure
The bundling market is designed to be competitive:
**Permissionless entry.** Anyone can run a bundler. There is no staking requirement, no approval process, no geographic restriction.
**User choice.** Users can submit to any bundler, self-bundle, or submit to multiple bundlers simultaneously. There is no lock-in.
**Price transparency.** Bundling fees are visible on-chain. Users can compare bundler prices and switch freely.
These dynamics drive fees toward marginal cost. Bundlers compete on price, latency, and reliability. Over time, the market tends toward efficient pricing where bundlers earn normal returns on capital.
***
For the complete specification and detailed benchmark results, see [Scalability Specification](/docs/resources/scalability).
# Transaction Encoding
Source: https://docs.kontor.network/docs/consensus/transaction-encoding
Kontor uses a hybrid-UTXO model, in which assets can exist one of two states, and move fluidly between them, even within a single block.
**Attached state:** Assets are "attached" to Bitcoin UTXOs, making them tradeable using standard Bitcoin tools:
* PSBTs (Partially Signed Bitcoin Transactions)
* Bitcoin Script interactions
* Native Bitcoin wallet support
* Atomic swaps with other Bitcoin metaprotocol tokens
**Detached state:** Assets are "detached" for use in smart contracts:
* Held in contract state
* Subject to programmable logic
* Can be transferred between contracts
* Can cross-contract call with type safety
This hybrid model gives Kontor unique interoperability—assets are first-class Bitcoin citizens while also supporting rich programmable logic. Users can perform single-confirmation atomic swaps between BTC, KOR, and other Bitcoin metaprotocol assets (Ordinals, Runes, Counterparty), or chain a swap directly into a contract call within a single Bitcoin transaction.
**Example flow:**
1. User holds BTC, wants to call a Kontor contract (which requires KOR for gas)
2. User constructs a Bitcoin transaction that atomically: swaps BTC → KOR, then calls the contract with KOR
3. All happens in one transaction, one confirmation
4. No pre-funding, no separate accounts, no bridges
# Dual-Token Model
Source: https://docs.kontor.network/docs/economics/dual-token-model
Bitcoin was designed as sound, deflationary digital money with predictable scarcity. This makes BTC excellent as a store of value and medium of exchange—digital gold. However, using BTC for application-layer services creates fundamental problems. Some projects attempt to use BTC as "gas" for computational services or try to fund long-term data storage with one-time BTC payments. These approaches misunderstand what BTC is and create economic problems.
Kontor addresses this by introducing KOR, a token with economic properties purpose-built for application-layer services—gas metering and storage incentives. This allows BTC to remain what it is (digital gold) while enabling the functionality that applications need.
## BTC is Digital Gold, KOR is Digital Gasoline
Gold's value comes from scarcity and durability—you hold it, you don't burn it. Gasoline's value comes from consumption—it powers activity, and its price reflects supply-and-demand balance. If gold were used as fuel, either it would be too expensive to use (making services uneconomical), or its consumption would be too high (destroying its monetary properties). The solution is to separate the store-of-value function from the utility function.
**How it works in practice:**
* BTC remains the unit of account—assets are priced in BTC, exchanges quote pairs against BTC, settlement happens in BTC
* KOR is a metered input—users acquire what they need for immediate use through atomic swaps
* Users pay both BTC fees (to miners for inclusion) and KOR fees (to the protocol for execution and storage)
* The dual-token model ensures neither fee cannibalizes the other
## Economic Alignment with Bitcoin
Every Kontor operation requires a Bitcoin transaction that pays Bitcoin miners. This is direct and unavoidable. The more successful Kontor becomes, the more valuable Bitcoin blockspace becomes. This stands in contrast to sidechains and rollups, which explicitly aim to move transaction volume off Bitcoin. Kontor's architecture is not parasitic—it strengthens Bitcoin's fee market and contributes to long-term security.
***
For the perpetual emissions model that funds storage, see [Perpetual Economics](/docs/economics/perpetual).
# Economic Security
Source: https://docs.kontor.network/docs/economics/economic-security
Cryptographic security ensures that only authorized parties can perform certain operations—you cannot spend someone else's Bitcoin without their private key. Economic security goes further: it ensures that even authorized parties who *could* misbehave choose not to, because doing so is unprofitable. In Kontor's storage protocol, nodes have possession of file data and could theoretically delete it, store only partial copies, or collude with others to manipulate rewards. The protocol cannot cryptographically prevent these actions. Instead, it makes them economically irrational.
This approach relies on a fundamental assumption: storage nodes are profit-maximizing agents who respond to incentives. They will take any action that increases expected profit and avoid any action that decreases it. The protocol's security derives from structuring costs and rewards such that honest behavior—actually storing files, responding to challenges, and maintaining availability—yields higher expected returns than any alternative strategy. When this condition holds, rational operators naturally behave honestly even without surveillance or enforcement beyond what the protocol itself provides.
The security analysis examines potential attacks not by asking whether they are technically possible, but whether they are economically viable. For each attack vector, the protocol must ensure that expected costs exceed expected benefits. This often involves making capital requirements dominate operational costs, ensuring penalties exceed gains, or structuring rewards so that cooperative behavior pays better than defection. The result is a system where security emerges from aligned incentives rather than imposed restrictions.
## Capital Cost Dominance
The protocol's security fundamentally depends on capital costs exceeding storage costs. Each node must stake KOR proportional to its commitments: $k_{req}(n) = (\sum_f k_f) \cdot \lambda_{stake}(n)$, where the sum is over all stored files and $\lambda_{stake}$ is the dynamic stake factor. This staked capital cannot be used for other purposes, creating an opportunity cost $k_{req} \cdot \rho$ where $\rho$ is the operator's discount rate. With realistic parameters, this opportunity cost dwarfs the physical cost of storing data.
This asymmetry is what enables economic security. Many attacks attempt to reduce costs while maintaining revenues: storing partial files and gambling on challenges, running many identity nodes on shared storage, or claiming to store files without actually possessing the data. All these attacks save on storage costs, which are linear in data size and relatively cheap (fractions of a cent per gigabyte-month). However, they cannot avoid capital costs. To earn rewards, a node must maintain adequate stake regardless of whether it actually stores the data. Since capital costs dominate by orders of magnitude, the savings from not storing data are negligible compared to the capital that must still be committed.
The dominance relationship must hold across the full range of file sizes the protocol supports. For small files, physical storage costs approach zero, so capital costs obviously dominate. For large files, physical costs rise but remain sublinear relative to rewards due to logarithmic emission scaling. The protocol enforces a maximum file size $s_{max}$ partly to ensure this relationship remains valid—extremely large files could potentially reverse the dominance if storage costs grew large enough relative to emission-driven rewards. Within the bounded size range, honest storage remains the most capital-efficient strategy.
## Sybil Resistance via Dynamic Staking
A Sybil attack involves creating multiple node identities to game the protocol. The naive version attempts to collect multiple reward streams while storing only one physical copy of the data—creating, say, ten node identities that all claim to store a file, earning ten times the rewards while paying only once for storage. This fails immediately due to capital cost dominance: the attacker must post full stake for each identity, paying ten times the capital cost to save on storage costs that are already negligible. The attack is obviously unprofitable.
A more sophisticated version uses Sybil identities not to save storage costs but to compartmentalize risk. Large operators face correlated failure risks—a data center fire, network outage, or software bug could cause them to fail challenges on many files simultaneously. If all files are stored under one identity, such an event could lead to complete stake forfeiture via the cascading insufficient-stake mechanism. By fragmenting their portfolio across many small identities, operators can limit the damage from any single failure event, preserving most of their stake even if some identities are slashed.
The protocol counters this through the dynamic stake factor $\lambda_{stake}(n) = 1 + \lambda_{slash} / \ln(2 + |F_n|)$, which imposes a capital penalty on nodes storing few files. A node storing a single file must stake $k_f \cdot \lambda_{stake}$ where $\lambda_{stake}$ can be 20-30× higher than for a large diversified node. To fragment a 100,000-file portfolio into individual single-file nodes would require roughly 20-30× more total capital than operating as a single large node. This makes risk compartmentalization prohibitively expensive—operators save more capital through consolidation than they gain through risk isolation.
The logarithmic scaling of $\lambda_{stake}$ means the penalty decreases smoothly as portfolio size grows. A node with ten files faces a moderate penalty; a node with a hundred files faces a small penalty; a node with thousands faces essentially no penalty. This creates the right incentive gradient: small-scale operators can still participate profitably (the penalty isn't infinite), but large-scale operators are strongly incentivized to consolidate rather than fragment. The system achieves Sybil resistance without completely excluding small participants.
## Wash-Trading Prevention
Wash-trading involves storing your own data to farm rewards: create a file agreement, run storage nodes to store it, collect emissions, and hope the rewards exceed the fees paid. This attack would allow users to mint KOR through self-dealing rather than providing genuine storage services to others. The protocol prevents this through the relationship between user fees and emission values.
The user fee is $\upsilon_f = \chi_{fee} \cdot k_f$, where $k_f$ is the per-node base stake requirement and $\chi_{fee} \approx 0.003$ is a protocol parameter. This fee is paid once and burned immediately. The attacker's revenue comes from emissions $\varepsilon_f(t)$ distributed over time and divided among all nodes storing the file. For the attack to be profitable, the net present value of future emissions must exceed the upfront fee.
The key parameter that prevents profitability is $\chi_{fee}$. Consider the simplified case where an attacker controls all nodes storing their own file. The file's emissions are $\varepsilon_f(t) = \varepsilon(t) \cdot (\omega_f / \Omega(t))$, and the attacker captures all of this. Over a planning horizon $h$ with discount rate $\rho$, the NPV is approximately $\varepsilon_f \cdot (1 - (1+\rho)^{-h}) / \rho$. For the attack to fail, we need $\upsilon_f > \text{NPV}$, which after substitution and simplification yields a condition on $\chi_{fee}$ relative to the emission-to-stake ratio and discount rate.
With $\chi_{fee} = 0.003$ and realistic parameters (20% annual discount rate, typical emission levels), the upfront fee exceeds the recoverable emissions even over multi-year horizons. The attacker loses money on each wash-traded file. Furthermore, the attacker still faces ongoing costs—proving costs when challenged, opportunity costs on staked capital—that further reduce profitability. The mechanism doesn't rely on detecting wash-trading; it simply makes the economics unfavorable regardless of the attacker's intent.
## Collusion and Redistribution
Storage nodes might collude to manipulate reward distributions. One scenario involves intentional slashing: multiple nodes storing the same file agree that one will deliberately fail a challenge, getting slashed, with the redistributed stake benefiting the co-conspirators. The question is whether the group profits collectively from this coordinated action.
When a node is slashed an amount $S = k_f \cdot \lambda_{slash}$, a fraction $\beta_{slash}$ is burned and the remainder $(1 - \beta_{slash}) \cdot S$ is distributed equally among the other $(|N_f| - 1)$ nodes storing that file. For a colluding group of size $k$ out of $n$ total nodes, the group's net change is $-S + (k-1)/(n-1) \cdot (1-\beta_{slash}) \cdot S$. This is positive—the group profits—only if $(k-1)/(n-1) \cdot (1-\beta_{slash}) > 1$.
Since both $(k-1)/(n-1) \leq 1$ and $(1-\beta_{slash}) < 1$, their product is always less than 1. The group always loses money on net. The burn fraction ensures that redistributed amounts are always smaller than slashed amounts, and the equal division among many nodes means even a large colluding group captures only a fraction of the redistribution. The collusion is strictly unprofitable—the group would be better off not executing the attack and avoiding the slashing entirely.
Other collusion scenarios face similar structural barriers. Attempts to coordinate exits to trigger leave fees fail because fees are burned, not redistributed. Attempts to form cartels to monopolize files and extract rents fail because the sponsorship market allows any member to profitably defect. The protocol systematically avoids creating situations where coordinated misbehavior yields higher returns than independent honest behavior.
## Detection Economics
The challenge mechanism provides probabilistic guarantees for detecting data loss. Each file expects $C_{target} = 12$ challenges per year regardless of network size. When challenged, a node must prove knowledge of $s_{chal} = 100$ randomly selected sectors from the file. The probability of detecting missing data depends only on the fraction lost, not the absolute file size.
For a node that has lost fraction $\mu$ of its data, the probability of detection per challenge is approximately $1 - (1-\mu)^{s_{chal}}$. With $s_{chal} = 100$, even 10% data loss yields 99.997% detection probability per challenge. Given 12 challenges per year, the expected time to detection is measured in months, not years. For complete data loss ($\mu = 1$), detection is nearly certain on the first challenge—the annual detection probability is $1 - e^{-C_{target}} \approx 99.9994\%$.
The economic implication is that selective storage—keeping only part of a file to reduce costs—carries enormous risk. The node saves marginal storage costs (already small relative to capital costs) but faces near-certain detection within a short timeframe. When detected, the node loses its stake $k_f \cdot \lambda_{slash}$ plus all future earning potential from that file. The expected value calculation is decisively negative: tiny savings versus large probable losses.
This detection probability is what makes the capital requirements effective. Without reliable detection, nodes could safely store nothing and gamble on not being checked. With high detection probability, storing less than the full file is a losing bet. The challenge rate $C_{target}$ is calibrated to make the risk unacceptable—12 challenges per year provides multiple opportunities for detection within reasonable timeframes, ensuring that data loss is caught before files degrade beyond recovery thresholds (the protocol assumes erasure coding provides 10% fault tolerance).
# Introduction
Source: https://docs.kontor.network/docs/economics/introduction
Bitcoin was designed as sound, deflationary digital money with predictable scarcity. This makes BTC excellent as a store of value and medium of exchange—digital gold. However, as developers have attempted to build applications on Bitcoin, they have often tried to use BTC itself for purposes it was never intended to serve. Some projects attempt to use BTC as "gas" for computational services, paying miners in BTC proportional to execution costs. Others try to fund long-term data storage with one-time BTC payments. These approaches fundamentally misunderstand what BTC is and create economic problems that undermine both the application layers and Bitcoin itself, not to mention fueling deep discord within the greater Bitcoin community.
Kontor addresses this by introducing a token for its metaprotocol, KOR, with economic properties purpose-built for application-layer services—gas metering and storage incentives. This allows BTC to remain what it is (digital gold) while enabling the functionality that applications need. The challenge is structuring this dual-token model so that KOR complements rather than competes with Bitcoin, creating a synergistic relationship where both tokens' value propositions are strengthened.
## Economic Alignment with Bitcoin
When users interact with Kontor—deploying contracts, calling functions, storing files—they must submit Bitcoin transactions that pay Bitcoin miners. This is not optional or mediated through bridges; it is direct and unavoidable. Every Kontor operation increases demand for Bitcoin blockspace, which increases fee revenue for miners, which strengthens Bitcoin's economic model. The more successful Kontor becomes, the more valuable Bitcoin blockspace becomes.
This stands in contrast to sidechains and rollups, which explicitly aim to move transaction volume off Bitcoin. Their value proposition is "do more while using Bitcoin less"—execute thousands of transactions off-chain, then settle the results with a single Bitcoin transaction. This reduces the per-transaction cost to users, but it also reduces fee revenue to miners. When a rollup succeeds, Bitcoin miners earn less, not more. This creates misaligned incentives: rollup operators benefit from transaction volume that Bitcoin miners never see. The relationship is fundamentally extractive.
Kontor's architecture is not parasitic. The protocol cannot move transactions "off-chain" because it has no separate chain. Scaling happens through efficiency in how data is encoded and how state is computed, but every state transition still requires a Bitcoin transaction, and there is a constant-factor relationship between Kontor data payload size and Bitcoin transaction fees. Users pay both BTC fees (to miners for inclusion) and KOR fees (to the protocol for execution and storage). The dual-token model ensures that neither fee cannibalizes the other—BTC compensates miners for the global service of transaction ordering and finality, while KOR compensates storage nodes for the application-specific service of maintaining protocol state and data.
This alignment extends to Bitcoin's long-term sustainability. Bitcoin's security budget depends on transaction fees replacing diminishing block subsidies as issuance approaches zero. Kontor contributes to this fee market by adding a category of transactions that have inelastic demand—if you want to interact with Kontor applications, you must pay Bitcoin fees, regardless of the fee rate. This provides steady demand that supports miner revenue even as block subsidies decline. Kontor's success directly translates to Bitcoin's long-term security.
## Gas and Smart Contract Execution
Attempting to use BTC as gas for smart contract execution creates fundamental problems. Gas systems require tokens whose supply can adjust to meet operational needs—users pay fees that compensate network operators for ongoing costs, and these operators need predictable revenue in terms that match their expenses. Bitcoin's deflationary model, by design, does not support this. BTC's fixed supply and diminishing issuance make it excellent as a store of value but problematic as a metered resource for computational services.
The analogy is straightforward: BTC is digital gold, KOR is digital gasoline. Gold's value comes from scarcity and durability—you hold it, you don't burn it. Gasoline's value comes from consumption—it powers activity, and its price reflects supply-and-demand balance in the service market. If gold were used as fuel, either it would be too expensive to use (making the service uneconomical), or its consumption would be too high (destroying its monetary properties). The solution is to separate the store-of-value function from the utility function, using different tokens with different economic properties for different purposes.
KOR has controlled inflation through emissions that reward storage nodes and other protocol participants. Users pay KOR for smart contract execution, and these fees are burned, creating deflationary pressure. The balance between emissions and burns determines net inflation, which adjusts based on protocol usage and network conditions. This is fundamentally different from BTC's fixed-supply model—KOR's supply expands and contracts in response to protocol needs, providing flexibility that enables sustainable service delivery.
This dual-token model respects BTC's monetary properties while adding the flexibility needed for application-layer services. BTC remains the unit of account—assets are priced in BTC, exchanges quote pairs against BTC, and settlement happens in BTC. KOR is a metered input—users acquire what they need for immediate use, similar to how one buys gasoline. The relationship is complementary: demand for Kontor services increases demand for both KOR (to pay gas) and BTC (to pay miners), and neither token undermines the other's value proposition.
## Perpetual Storage Incentives
Attempting to fund perpetual storage with one-time BTC payments faces an impossibility: a finite payment cannot fund infinite service. Even if you collect a large upfront fee, it will eventually be depleted—you cannot pay for decades of ongoing storage costs with a fixed amount paid today. Storage nodes incur continuous expenses for equipment, bandwidth, power, and facilities. These costs must be met year after year, indefinitely, yet the initial payment is spent once and is gone.
Kontor addresses this through perpetual emissions: storage nodes earn KOR each block proportional to the files they store. This creates an ongoing revenue stream that lasts as long as the protocol exists. The system is designed so that for any rational operator, the net present value of future emissions exceeds storage costs, making it profitable to maintain files indefinitely. The user's one-time fee is burned rather than escrowed—it serves to prevent spam and create deflationary pressure, but it does not directly fund the storage. The actual funding comes from inflation, which can adjust dynamically based on network conditions.
This emission-based model enables responsive incentives that fixed payments cannot provide. If replication falls below safe levels, emissions can increase, making storage more profitable and attracting additional nodes. If costs change—due to technology improvements, energy price fluctuations, or capital market conditions—nodes respond by entering or exiting based on profitability, and the protocol adjusts emissions to maintain adequate participation. The system self-regulates without requiring governance decisions or manual parameter updates.
The long-term sustainability comes from balancing emissions against burns. In the early network, emissions dominate—the protocol is inflationary by design to bootstrap participation. As the network matures, burn mechanisms (file creation fees, smart contract gas, slashing, exit fees) remove KOR from circulation. Eventually, as smart contract usage grows and file creation slows, burns can offset emissions, stabilizing or even reversing inflation. The dual-token model enables this: BTC remains deflationary and appreciates, while KOR inflates or deflates based on protocol dynamics, providing the economic flexibility needed to guarantee perpetual storage.
# Long-Term Economic Dynamics
Source: https://docs.kontor.network/docs/economics/long-term-dynamics
## Dual-Token Equilibrium
The protocol uses two tokens with fundamentally different purposes. BTC serves as the unit of account, the settlement asset, and the currency for pricing and trading. Users denominate value in BTC: a file costs $X$ satoshis to store, an asset trades at $Y$ satoshis, a service charges $Z$ satoshis. KOR serves as a metered resource for computational work (gas) and as the incentive mechanism for storage provision (emissions and stake requirements). This separation allows each token to have economics appropriate to its role.
BTC is deflationary and scarce—properties that make it effective money but problematic as fuel. If smart contract gas were paid in BTC, the incentive structure for long-term storage would be difficult to sustain. Emissions in a deflationary currency create different dynamics than in an inflationary one: operators would need to forecast BTC appreciation when evaluating storage profitability, and the protocol would struggle to maintain adequate security budget as BTC becomes more valuable. Using BTC as fuel would also require Bitcoin users to spend their store-of-value asset for routine operations, creating psychological friction.
KOR, on the other hand, is inflationary with dynamic adjustments—properties that make it not particularly well suited to serve as a store of value but which make it very effective as fuel and incentive mechanism. Users acquire KOR as needed for specific operations rather than holding it long-term. The atomic swap mechanism allows BTC holders to exchange for KOR and immediately use it for contract calls or storage fees within a single Bitcoin transaction confirmation. This removes friction from the user experience while preserving economic separation. The KOR/BTC exchange rate is market-determined, reflecting supply and demand dynamics.
The dual-token structure provides an adjustment mechanism for protocol economics. If KOR inflation rises, market forces respond through the exchange rate: KOR becomes less valuable relative to BTC, making storage and smart contracts cheaper in BTC terms while maintaining the same KOR-denominated prices. This transfers inflation costs to KOR holders rather than to users who transact in BTC. The protocol's real security budget, measured in BTC terms, adjusts to market conditions rather than being rigidly fixed.
## Network Maturity and Inflation
The inflation rate at any time is determined by the balance between emissions and burns: $\mu(t) = (\varepsilon(t) - \Phi_{burned}(t)) / KOR_{total}(t)$ where $\varepsilon(t)$ represents total emissions and $\Phi_{burned}(t)$ represents total burns during that period. In the early network, emissions dominate and $\mu(t)$ remains positive at roughly 5-10% annually (before dynamic health adjustments). As the network evolves, several factors reduce inflation.
First, emission tapering means new files contribute less to total emissions than early files. The growth rate of $\varepsilon(t)$ slows even as file count increases. Second, the user fee mechanism burns a fraction of each file's effective emission value upfront—each new file pays $\upsilon_f = \chi_{fee} \cdot k_f$, calibrated to represent multiple years of that file's emission allocation. This creates immediate deflation with each file creation that partially offsets ongoing inflation from existing files. Third, smart contract gas burns create increasingly significant deflationary pressure as application usage grows.
The protocol is designed to remain moderately inflationary even in steady state, providing ongoing security budget for storage. The economic model projects that under reasonable growth assumptions—file creation growing then stabilizing, smart contract usage increasing, network health maintained—net inflation gradually declines from initial rates toward lower single digits over a period of years. The exact trajectory depends on exogenous factors including market conditions, application adoption rates, and competing platforms.
The dynamic emission multiplier $\alpha(t)$ can disrupt this trajectory. If network health deteriorates and $\alpha(t)$ remains elevated or reaches $\alpha_{max}$ for extended periods, emissions increase and inflation rises. The protocol prioritizes maintaining file availability over controlling inflation, up to a point. Stress conditions may moderately increase inflation in exchange for preserving data availability guarantees.
## Emission Tapering Mechanics
A protocol promising perpetual storage faces an inflation problem: if emissions remain constant while file count grows without bound, either total emissions must increase proportionally (causing runaway inflation) or per-file emissions must decrease (eventually making storage unprofitable). Kontor addresses this through rank-based tapering, where each file's perpetual emission weight is fixed at creation time based on when it enters the network.
The emission weight formula $\omega_f = \ln(s_{bytes}) / \ln(1 + rank_f)$ includes the file's creation rank in the denominator. Files created when the network is small receive permanently higher emission weights than identical files created later. A file created at rank 1,000 receives higher emissions than an identical file created at rank 1,000,000. This tapering occurs automatically without protocol changes or governance interventions.
As the network grows and $\Omega(t) = \sum_f \omega_f$ increases, new files contribute proportionally less to the total. The growth rate of $\Omega(t)$ is sublinear—approximately $O(N \cdot \ln(\ln(N)))$ where $N$ is the total file count. Even as the network scales to billions of files, total emission weight grows slowly enough that per-file emissions for new files remain viable while total network emissions stay bounded relative to supply.
Early storage nodes benefit by being "grandfathered" into higher reward tiers. A node joining when the network is small locks in higher emission weights on those early files permanently. This creates bootstrap incentives: early participants take on greater risk with unproven protocols and limited network effects, compensated through permanently higher returns. The mechanism aligns individual incentives with network growth—early nodes benefit as the network expands because their fixed high-weight files become relatively more valuable as new files arrive with lower weights.
## Smart Contract Fees
The protocol's economic model anticipates a structural shift as the network matures. Initially, file storage dominates activity: users create file agreements, storage nodes earn emissions, and burn mechanisms (user fees, leave fees, slashing) remain modest compared to emissions. The network is intentionally inflationary during this growth phase to attract storage providers and bootstrap data availability. Emissions rise with file creation, but rank-based tapering keeps growth rate manageable.
Over time, smart contract activity becomes increasingly significant. The file storage layer reaches a mature state where new file creation rate stabilizes—not because the protocol limits it, but because addressable demand plateaus. Meanwhile, smart contract usage continues growing as applications are built and adopted. DeFi protocols, asset management systems, and on-chain markets generate transaction volume that scales with economic activity rather than data storage needs. Each smart contract call consumes gas, and all gas fees are burned.
This transition is critical for long-term sustainability. Storage emissions must continue indefinitely to maintain file availability, but unbounded emission rate growth would lead to unbounded inflation. The solution is not capping emissions, which would compromise storage guarantees, but developing sufficient burn rate from smart contracts to offset storage emissions. The economic model projects that mature network state involves storage emissions providing ongoing incentives for file availability while smart contract burns create comparable or greater deflationary pressure. Ultimately the users who burn fees for smart contract execution will be paying for perpetual storage indirectly.
# Network Health and Stability
Source: https://docs.kontor.network/docs/economics/network-health
## Measuring Network Health
The file storage protocol makes a perpetual commitment: once a user pays to store a file, the network must maintain that file forever. This creates a coordination problem across thousands of independent storage nodes making individual economic decisions. The protocol cannot directly control whether nodes join or leave—it can only shape incentives. The challenge is to design an incentive structure that keeps files safely replicated even as node operators respond to changing profitability, market conditions, and operational costs.
To manage this, the protocol needs a quantitative measure of whether the network is fulfilling its commitments. The most obvious metric would be counting how many files are at or near minimum replication, but this is reactive—it measures failure rather than health. A better approach is to track the aggregate replication level across the network and use this as a forward-looking indicator. When aggregate replication is high, the system has buffer against shocks; when it falls, the system needs to respond before individual files become critically under-replicated.
Kontor uses emission-weighted replication $R(t) = \sum(\omega_f \cdot |N_f|) / \Omega(t)$, where each file's replication count is weighted by its emission weight $\omega_f$. This means economically important files—those created early or those that are larger—contribute proportionally more to the metric. A file representing 10% of network emissions contributes 10% to the health measurement. This weighting ensures the protocol prioritizes maintaining replication on high-value files, where "value" is defined by the file's ongoing cost to the network in terms of inflation.
The metric is smoothed over a trailing window using a weighted moving average, giving more weight to recent observations. This filtering prevents the system from overreacting to temporary fluctuations while still responding to genuine trends. The critical reference point is $n_{min}$, the minimum required replication. The health buffer $m(t) = R(t) - n_{min}$ represents how much room exists above this threshold. A large positive buffer indicates a healthy network; a shrinking buffer signals growing risk.
## Dynamic Emission Response
The protocol's ability to guarantee perpetual storage depends on maintaining adequate economic incentives for storage nodes. If rewards fall too low relative to costs, rational operators will exit, reducing replication and potentially causing files to drop below minimum thresholds. The protocol cannot force nodes to participate, but it can adjust the reward structure to make participation more attractive when needed.
This is achieved through a dynamic emission multiplier $\alpha(t)$ that modulates the baseline inflation rate based on network health. Total emissions each block are $\varepsilon(t) = \text{KOR}_{total}(t-1) \cdot \alpha(t-1) \cdot \mu_0 / B$, where $\mu_0$ is the baseline annual inflation rate and $\alpha(t)$ adjusts in response to the health buffer. When the buffer is large, $\alpha(t)$ stays near 1, keeping inflation at the baseline. As the buffer shrinks, $\alpha(t)$ increases, raising emissions and making storage more profitable. If the buffer reaches zero—meaning weighted average replication has fallen to the minimum threshold—$\alpha(t)$ immediately jumps to its maximum value $\alpha_{max}$.
This counter-cyclical policy is analogous to monetary policy in traditional economies: expand the money supply during stress, contract during stability. The mathematical form is $\alpha(t) = \min(\alpha_{max}, 1 + \kappa_\alpha / m(t))$ when $m(t) > 0$, creating an inverse relationship where the emission multiplier accelerates as problems worsen. A small deterioration produces a modest increase; a severe deterioration produces a strong response. The parameter $\kappa_\alpha$ controls sensitivity—how aggressively the system responds to changes in health.
The adjustment mechanism is deterministic and transparent. Every participant can observe the health metric and calculate the resulting emission rate. This predictability allows nodes to forecast expected rewards and make informed decisions about participation. The protocol updates the multiplier at the end of each block based on current conditions, and this updated value determines emissions for the subsequent block.
## Feedback Loops and Equilibrium
Understanding system stability requires examining the feedback loops that connect network health, emissions, and node behavior. The intended stabilizing mechanism works as follows: declining replication triggers higher emissions, which increases per-node profitability, which attracts new participants or retains existing ones, which raises replication, which eventually reduces emissions back toward baseline. This negative feedback loop creates a self-regulating system that tends toward equilibrium without requiring governance intervention.
However, the system also contains a potentially destabilizing positive feedback loop operating through external market prices. If the KOR/USD exchange rate falls, storage nodes' fiat-denominated profitability declines even if KOR-denominated rewards remain constant. This may cause exits, reducing replication and triggering emission increases. Higher emissions expand KOR supply, which can put additional downward pressure on the exchange rate, amplifying the initial shock. This pro-cyclical dynamic represents a systemic vulnerability, particularly during market stress or loss of confidence.
The overall stability depends on which feedback loop dominates. When the health buffer is positive and the emission multiplier is below its maximum, the stabilizing monetary policy can effectively counteract deteriorating conditions by making storage more profitable. The system self-corrects through purely economic mechanisms. However, if conditions worsen to the point where $\alpha(t) = \alpha_{max}$ for extended periods, the stabilizing mechanism saturates—it has no more room to increase incentives. At this point, recovery depends on exogenous factors: improved market sentiment, reduced operational costs, or increased demand for storage services.
This design accepts that no purely endogenous mechanism can guarantee stability under all conditions. Instead, it provides strong self-correction within a bounded range of stress, while acknowledging that extreme shocks may require time and favorable external conditions to resolve. The emission cap $\alpha_{max}$ represents an explicit trade-off: it prevents unlimited inflation but also limits the protocol's corrective power during severe crises.
## The Vulnerability Penalty
Beyond adjusting global emissions, the protocol uses targeted mechanisms to prevent cascading failures. The leave fee $\varphi_{leave}(f,t) = k_f \cdot (n_{min} / |N_f|)^2$ creates a direct cost for exits that scales with file vulnerability. When a file has replication far above the minimum, the fee is negligible—nodes can leave freely when storage becomes unprofitable. As replication approaches $n_{min}$, the fee grows quadratically, becoming prohibitively expensive precisely when additional exits would be most harmful.
This structure creates hysteresis in node behavior. Nodes join when expected profit exceeds zero, but they require much larger losses before exiting becomes rational due to the leave fee. The gap between entry and exit thresholds stabilizes replication by preventing churn—nodes do not repeatedly join and leave in response to small profit fluctuations. For files near minimum replication, the high exit barrier means nodes must experience substantial sustained losses before departure makes economic sense, providing a cushion against cascading exits during temporary market stress.
The fee is burned rather than redistributed to remaining nodes. This design prevents strategic behavior where nodes might engineer exits to capture redistributed fees, or where colluding groups might profit from coordinated departures. Burning ensures the decision calculus is simple: exiting costs you money with no compensating benefit to anyone. This makes leaves a pure deadweight loss that rational operators prefer to avoid.
## Self-Correction Mechanisms
The protocol's self-correcting behavior emerges from the interaction of multiple simultaneous mechanisms. When a file becomes under-replicated, the emission-weighted health metric $R(t)$ falls, triggering increased total emissions through $\alpha(t)$. These higher emissions distribute across all files proportionally to their weights, but under-replicated files see larger per-node increases because the same total is divided among fewer participants. The leave fee increases quadratically, raising the cost of further exits. If nodes fail challenges and get slashed, a portion of their slashed stake redistributes to remaining honest nodes, directly improving their profitability.
These forces compound to create progressively stronger correction as problems worsen. Small deviations from equilibrium produce gentle nudges; large deviations produce powerful responses. No central coordination is required. Each node makes independent decisions based on local profitability calculations, but the aggregate behavior stabilizes because individual incentives are designed to align with collective network needs. A rational operator seeking to maximize profit will naturally choose actions that support network health.
The system's dynamic characteristics—response speed, stability margins, tendency to overshoot or oscillate—depend on protocol parameters like the health averaging window $W_R$ and emission sensitivity $\kappa_\alpha$. These represent fundamental trade-offs. Longer averaging windows make the system more stable against noise but slower to detect genuine problems. Higher sensitivity produces faster corrections but risks overreacting to transient conditions. The chosen parameters reflect a balance informed by economic modeling and simulation, designed to provide adequate responsiveness without inducing volatility or instability in the emission rate.
# Storage Node Economics
Source: https://docs.kontor.network/docs/economics/node-economics
Storage nodes are modeled as rational economic actors seeking to maximize profit. The protocol does not rely on altruism or assume operators will act against their financial interests. Instead, it structures incentives so that profit-maximizing behavior aligns with protocol goals: storing files reliably and responding to challenges honestly. A node's decision to join, remain in, or exit a file agreement is determined by comparing the net present value of expected revenues against costs over some time horizon. If this calculation is positive, the node participates; if negative, it exits (when permitted) or declines to join.
The protocol must ensure that for any file with sufficient replication, honest storage is more profitable than any alternative strategy—including storing nothing, storing partial data, or colluding with other nodes. This is achieved through a combination of ongoing rewards, capital lock-up requirements, and penalties for detectable misbehavior. The economic parameters are calibrated so that deviating from honest behavior either reduces expected revenue or increases risk beyond what rational operators will accept.
## Revenue Model: Emissions and Redistribution
A storage node's primary revenue is KOR emissions. Each block, a node storing file $f$ receives $r_{storage}(n,f,t) = \varepsilon_f(t) / |N_f|$, where $\varepsilon_f(t)$ is the file's emission allocation and $|N_f|$ is the number of nodes storing it. This base reward is modified by sponsorship agreements—if the node is sponsored, it pays a commission to its sponsor; if it sponsors others, it earns commissions from them. The total revenue scales inversely with replication: fewer nodes storing a file means higher per-node rewards, creating a natural incentive for nodes to join under-replicated files.
Secondary revenue comes from slashing redistribution. When a node fails a challenge, it is slashed an amount $k_f \cdot \lambda_{slash}$, of which a fraction $(1 - \beta_{slash})$ is distributed equally to the other nodes storing that file. The expected value of this revenue is $E[r_{slash}] = (p_f / |N_f|) \cdot p_{fail} \cdot (1 - \beta_{slash}) \cdot k_f \cdot \lambda_{slash}$, where $p_f$ is the per-file challenge probability and $p_{fail}$ is the probability any given node fails when challenged. This creates an additional incentive for honest behavior—by maintaining their data and responding to challenges, nodes not only avoid being slashed themselves but also benefit when others fail.
Sponsorship provides a third revenue stream. When a new node (the "entrant") needs file data and cannot obtain it off-chain, it can enter a time-limited agreement with an existing storer (the "sponsor"). The sponsor provides the data in exchange for a commission rate $\gamma_{rate}$ on the entrant's rewards for duration $\gamma_{duration}$. These terms are negotiated off-chain and recorded on-chain. For the sponsor, the NPV of the commission stream must exceed the bandwidth cost of transferring the file. For the entrant, paying the commission must be cheaper than alternative methods of obtaining the data or more profitable than not storing the file at all.
## Cost Structure and Capital Requirements
Storage nodes face three categories of costs. Physical storage costs—the cost of disk space, bandwidth for data repair and replication—are typically small and scale linearly with file size. Proving costs arise from the need to periodically submit storage proofs on-chain. Nodes can aggregate multiple challenges into a single Bitcoin transaction during the $W_{proof}$ window (\~2 weeks), amortizing the transaction fee across many proofs. With typical parameters, proving costs are negligible for operators with diversified portfolios.
Capital costs dominate. Each node must maintain staked KOR proportional to its commitments: $k_{req}(n) = (\sum_f k_f) \cdot \lambda_{stake}(n)$, where the sum is over all files the node stores and $\lambda_{stake} = 1 + \lambda_{slash} / \ln(2 + |F_n|)$ is a dynamic stake factor that penalizes fragmentation. This staked KOR cannot be used for other purposes, creating an opportunity cost $k_{req} \cdot \rho$ where $\rho$ is the operator's discount rate. For a file with base stake requirement $k_f$, emission rate $\varepsilon_f$, and replication $|N_f|$, the break-even condition is approximately $(\varepsilon_f / |N_f|) \approx k_f \cdot \lambda_{stake} \cdot \rho$, with physical storage and proving costs as second-order terms.
The design deliberately makes capital costs dominate storage costs. This asymmetry is what enables security: attacks that attempt to collect rewards without actually storing data (such as storing only partial files and gambling on challenges, or running many Sybil identities on shared storage) must still bear full capital costs. The protocol's security assumption is that capital is more expensive than storage, which holds true across realistic parameter ranges and makes honest storage the most profitable strategy.
## Node Decision Framework
Nodes evaluate decisions by computing the net present value of expected profit over a planning horizon $h$. To decide whether to join a file agreement, a node calculates $\text{NPV}(\{E[\pi(n, t+i | F_n \cup \{f\})] - E[\pi(n, t+i | F_n)]\}, \rho, h)$ and joins if positive. This marginal analysis accounts for the additional revenue from the new file, the increased stake requirement (which may change $\lambda_{stake}$ if the portfolio is small), changes to proving costs, and physical storage costs. Nodes forecast future parameters—replication levels, exchange rates, Bitcoin fee rates—based on current conditions.
To decide whether to leave, a node compares the NPV of future profit with and without the file, minus the leave fee $\varphi_{leave}(f,t) = k_f \cdot (n_{min} / |N_f|)^2$. The leave fee increases quadratically as replication approaches the minimum threshold $n_{min}$, making exits increasingly expensive precisely when files are most vulnerable. Leaving is only permitted when $|N_f| > n_{min}$, and even then, the node must have sufficient liquid balance to pay the fee. This creates hysteresis: nodes require a substantial profit deterioration before exiting becomes rational.
When challenged, a node must decide whether to generate and submit a proof. The decision is straightforward: if the cost of proving (in BTC transaction fees) is less than the total loss from being slashed—which includes not just the immediate stake loss $k_f \cdot \lambda_{slash}$ but also all future earnings from that file—the node proves. The protocol ensures this inequality holds for honest nodes: proving costs are designed to be far smaller than slashing penalties. For dishonest nodes that deleted the data, generating a valid proof is impossible regardless of cost.
## The Sponsorship Market
The sponsorship mechanism solves a bootstrapping problem: how do new nodes obtain file data when they don't already have it? Nodes will not join a file agreement without being able to respond to challenges, which requires possessing the actual data. If all existing storers refuse to share, they create a permanent monopoly with no possibility of new entrants. The sponsorship market breaks this by making it profitable for existing storers to provide data to newcomers.
A sponsorship agreement is an on-chain contract specifying the entrant, sponsor, file, commission rate $\gamma_{rate}$, and duration $\gamma_{duration}$. During the agreement, the entrant pays $\gamma_{rate}$ fraction of its file rewards to the sponsor. This creates a competitive market: multiple existing storers can offer to sponsor, competing on commission rates and terms. The equilibrium commission rate $\gamma_{eq}$ balances the sponsor's bandwidth cost against the NPV of the commission stream. For typical files and parameters, $\gamma_{eq}$ is low enough that entrants find sponsorship attractive while sponsors earn meaningful returns.
This market structure prevents gatekeeping. Even if a cartel of existing nodes attempts to monopolize a file by refusing to share data, any single cartel member has an incentive to defect and capture the entire sponsorship revenue for themselves. The protocol does not mandate sponsorship—nodes can join files directly if they obtain data through other means—but it provides a permissionless fallback that ensures no file can be permanently locked to a closed group. This keeps the network open and maintains the property that storage rewards naturally attract new participants when existing nodes are earning excess profits.
# Perpetual Storage Economics
Source: https://docs.kontor.network/docs/economics/perpetual
Traditional cloud storage operates on a subscription model: users pay monthly or annually, and if payments stop, data disappears. This model is incompatible with blockchain use cases, where assets and applications need to reference data that must remain available indefinitely. Storing data directly on Bitcoin is one solution—it's permanently available in the blockchain—but it's prohibitively expensive and imposes costs on the entire network. A file stored on-chain must be downloaded and validated by every full node forever. Kontor addresses this by separating data availability from blockchain storage: file metadata and cryptographic proofs anchor on Bitcoin, while the data itself lives off-chain with storage nodes who are economically incentivized to maintain it.
The fundamental challenge is sustainability: how can a one-time upfront payment from a user fund perpetual storage? The naive approach would be to collect a large enough fee to cover expected storage costs forever, but this requires predicting future costs decades in advance and creates perverse incentives—nodes might prefer the protocol to fail early so they can claim the remaining endowment. Kontor instead treats storage as an ongoing service funded by continuous token emissions, with economic mechanisms designed to ensure rational operators find it profitable to store data long-term.
## Emission-Based Incentives
Storage nodes earn KOR through block emissions tied to the files they store. Each block, the protocol distributes newly minted KOR to storage nodes in proportion to the emission weights of the files they're storing. This creates a perpetual revenue stream: as long as the protocol exists and emits KOR, storage nodes have an economic reason to maintain their data. The system is designed so that for any rational operator, the net present value of future emissions exceeds the costs of storage, making it profitable to continue storing files indefinitely.
Users pay a one-time fee when creating a file agreement, but this fee is entirely burned—it doesn't fund the storage directly. Instead, the fee serves two purposes: it prevents spam by imposing a cost barrier, and it creates deflationary pressure to offset the inflationary emissions. The actual incentive for storage comes from emissions, which scale with the total KOR supply and adjust dynamically based on network health. This separation of concerns—users pay to create scarcity, emissions fund the service—is what allows the system to function without predicting future economics.
## Logarithmic Scaling and Emission Weights
Each file receives an emission weight $\omega_f = \ln(s_{bytes}) / \ln(1 + rank)$, where $s_{bytes}$ is the file size in bytes and $rank$ is its creation order. This logarithmic relationship is a core design principle: the economic value of data is largely independent of its physical size. A 1 KB file containing a private key may be far more valuable than a 1 GB video file. Linear scaling would create a structural bias where large files dominate emissions, making small but critical files economically unattractive to store. Logarithmic scaling compresses the size difference: a 1 GB file has only about twice the emission weight of a 1 MB file, ensuring that storage nodes find both worth maintaining.
The emission weight determines a file's share of total network emissions. Each block, total emissions $\varepsilon(t)$ are calculated based on current supply and network health, then distributed to files in proportion to their weights: $\varepsilon_f(t) = \varepsilon(t) \cdot (\omega_f / \Omega(t))$, where $\Omega(t)$ is the sum of all file weights. These file-level emissions are then split equally among the nodes storing that file. This proportional allocation means that a file's ongoing "cost" to the network (in terms of inflation) corresponds to its relative importance, not its absolute size.
## Emission Tapering Through Rank
The rank term in the emission weight formula—$\ln(1 + rank)$—creates a "grandfathering" effect where earlier files receive permanently higher rewards than later ones. A file created when the network has 1,000 files gets a higher weight than an identical file created when the network has 1,000,000 files. This is deliberate: as the network grows and total emissions increase, new files receive proportionally smaller shares, preventing runaway inflation. The system naturally tapers without requiring protocol upgrades or governance decisions.
This rank-based tapering ensures that total emissions grow sublinearly with network size. Even as the network scales to billions of files, the aggregate emission rate remains bounded because each new file contributes less to $\Omega(t)$ than the previous one. Early storage nodes are rewarded for bootstrap risk, and the emission weight of their files becomes increasingly valuable relative to newer files. Meanwhile, new files remain profitable to store because their stake requirements (which are also based on emission weights) scale down proportionally with their emission potential.
## Deflationary Mechanisms
Emissions create inflation; burns create deflation. Kontor implements several burn mechanisms that remove KOR from circulation. When a user creates a file agreement, the entire storage fee $\upsilon_f$ is burned. When nodes voluntarily exit agreements, they pay a leave fee that is burned. When nodes are slashed for failing challenges, a portion ($\beta_{slash}$) of the slashed amount is burned, with the remainder redistributed to honest nodes. Most significantly, all gas fees from smart contract execution are burned.
The interplay between emissions and burns determines long-term inflation. In the early network, storage-related burns (file creation, exits, slashing) dominate but remain relatively modest—the network is inflationary by design to incentivize growth. As the network matures and file creation slows, storage emissions stabilize while smart contract activity (and therefore gas burns) continues growing. The system is architected so that eventually, gas burns from a thriving application ecosystem offset storage emissions, bringing net inflation down even as the protocol continues to guarantee perpetual storage through ongoing rewards.
# Welcome to Kontor!
Source: https://docs.kontor.network/docs/introduction/home
Kontor is a next-generation Bitcoin metaprotocol that brings programmable assets, scalable file storage, high-throughput and low-latency execution directly to the Bitcoin blockchain.
**Current Status:** Kontor is under active development with Bitcoin `signet` support. For the latest updates, please see the [Kontor website](https://kontor.network).
## Fast and Scalable
Kontor's optimistic consensus layer provides transaction confirmations in 1–2 seconds while Bitcoin remains the ultimate source of finality. Bundling amortizes Bitcoin transaction fees across thousands of operations, bringing per-operation costs down significantly. Combined with BLS signature aggregation and compact data encoding, Kontor processes over 1,000 transactions per second—all entirely on the Bitcoin blockchain.
[Learn about how Kontor extends Bitcoin's consensus system →](/docs/consensus/introduction)
## Smart Contracts with Sigil
Write, deploy and run rich smart contract applications directly on the Bitcoin blockchain. The Sigil framework uses the WebAssembly Component Model to make writing contracts easier than ever before, with strong type safety, native cross-contract calls and best-in-class developer ergonomics.
[Start building with the Sigil framework →](/docs/sigil/introduction)
## Perpetual File Storage
Pay once, store forever. Kontor's storage protocol provides scalable, permanent file storage where the data lives off-chain but the storage *system* itself is entirely on-chain. Kontor supports files from kilobytes to gigabytes with logarithmic cost scaling and strong security guarantees.
[Learn about Kontor's scalable storage system →](/docs/storage/introduction)
## Kontor Economics
The Kontor economic model was designed from first principles to support data storage and smart contract execution. BTC remains the unit of account while KOR meters execution and incentivizes storage through perpetual emissions. Dynamic mechanisms ensure network health and long-term sustainability.
[Explore the economics of the Kontor ecosystem →](/docs/economics/introduction)
## Resources
Access technical specifications, comprehensive FAQs, and developer resources.
[View all resources →](/docs/resources/)
# How Does Kontor Work?
Source: https://docs.kontor.network/docs/introduction/how-it-works
**Kontor is a next-generation metaprotocol on Bitcoin.** A metaprotocol is a set of rules for interpreting Bitcoin transactions in a way that Bitcoin itself doesn't recognize—adding functionality on top of Bitcoin without changing Bitcoin's consensus rules. Other metaprotocols, like Colored Coins, Counterparty, Ordinals, Runes, BRC-20 and Alkanes all work the same way: they use Bitcoin for transaction ordering and finality, but add meaning to transactions that Bitcoin miners don't need to understand. Kontor distinguishes itself by providing rich smart contracts with expressive languages, native composability between contracts, and deep integration with Bitcoin's transaction model. The aim is to support the kind of complex financial applications—intricate asset logic, composable DeFi protocols, and programmable primitives—that have historically required purpose-built blockchains, while preserving the security and value alignment that make Bitcoin metaprotocols distinctive.
**Kontor transactions embed data in Bitcoin witnesses.** Kontor uses Taproot to encode its transactions. Each Kontor operation is a commit-reveal pair: you create a Bitcoin transaction (the "commit") with a special Taproot output, then spend it immediately (the "reveal") with a script that contains your actual Kontor data. The data sits in Bitcoin's witness section—the part of a transaction that stores signatures and other information that doesn't affect the core transaction graph. Bitcoin nodes relay these transactions, miners include them in blocks, and the data is preserved in the blockchain forever, but Bitcoin itself doesn't execute any Kontor logic. To Bitcoin, these are just normal transactions paying normal fees.
**Indexers interpret the protocol.** The key innovation of metaprotocols is that protocol state is derived, not enforced by miners. Anyone can run a Kontor indexer—software that scans Bitcoin blocks, finds Kontor transactions, and processes them according to Kontor's rules. If you deploy a smart contract, the indexer stores the contract's WebAssembly bytecode. If you call a contract function, the indexer executes that function in a sandboxed environment and updates the contract's state. Crucially, every indexer that processes the same Bitcoin history will arrive at the same state, because the rules are deterministic. You don't need to trust a specific indexer; you can verify the state yourself by running your own.
**Metaprotocols vs sidechains and rollups.** The metaprotocol architecture is fundamentally different from Layer-2 approaches like sidechains or rollups. Sidechains have their own consensus mechanisms and validators—you're trusting a separate network to correctly process transactions and honestly report state back to Bitcoin. Rollups use cryptographic proofs to reduce trust, but still operate as separate systems that must bridge assets on and off Bitcoin, creating economic friction and competing with Bitcoin for transaction fees. Metaprotocols, by contrast, have the same security model as Bitcoin itself: every transaction is a Bitcoin transaction, secured by Bitcoin's full hashpower. The only difference is that users must run or trust an additional codebase (the indexer) to interpret protocol state. This makes metaprotocols fundamentally synergistic with Bitcoin—they increase network activity, pay miners more fees, and extend Bitcoin's functionality without requiring changes to Bitcoin or creating alternative consensus mechanisms.
***
For details on staker security and slashing conditions, see [Optimistic Consensus](/docs/consensus/optimistic-consensus). For smart contract security, see [Contract Security](/docs/sigil/security). For storage network security, see [Storage Security](/docs/storage/storage-security).
# What is Kontor?
Source: https://docs.kontor.network/docs/introduction/what-is-kontor
Kontor is a Bitcoin metaprotocol that brings programmable assets and verifiable long-term file storage directly to Bitcoin. Unlike sidechains or rollups that create separate networks, Kontor operates entirely on Bitcoin itself—every Kontor transaction is a Bitcoin transaction, secured by Bitcoin's full hashpower and finality guarantees. This means you can build and deploy sophisticated applications on Bitcoin without leaving the Bitcoin network or trusting additional consensus mechanisms.
At its core, Kontor addresses what blockchains are actually used for: creating, managing, and transferring digital assets. It provides two key capabilities that Bitcoin alone cannot offer. First, **Sigil**, a next-generation WebAssembly-based smart contract framework that lets developers write expressive, type-safe contracts in familiar languages like Rust. Second, a **file persistence protocol** that stores data off-chain with cryptographic proofs and economic incentives anchored on Bitcoin, solving the problem of expensive on-chain storage without sacrificing persistence or availability.
Kontor introduces **KOR**, a native token designed specifically for metering smart contract execution and incentivizing perpetual file storage. KOR complements rather than competes with BTC: Bitcoin remains the settlement layer and unit of account, while KOR acts as "gas" for computation and storage rewards. This dual-token model respects BTC's role as digital gold while providing the economic infrastructure needed for a thriving application ecosystem.
While Kontor settles on Bitcoin, it doesn't inherit Bitcoin's confirmation latency. Through **optimistic consensus**, a network of KOR stakers coordinates transaction ordering before Bitcoin confirmation, providing 1–2 second finality backed by slashable collateral. Combined with BLS signature aggregation and compression, Kontor achieves over 1,000 transactions per second—all recorded on Bitcoin.
Importantly, Kontor is fully interoperable with Bitcoin and other Bitcoin metaprotocols like Ordinals, Runes, and Counterparty. Through a novel hybrid UTXO model, assets can be "attached" to Bitcoin UTXOs for atomic swaps and native Bitcoin Script interactions, then "detached" for use in smart contracts—all in a single confirmation. This means any Bitcoin user can interact with Kontor immediately, using standard Bitcoin addresses and wallets, without bridging assets or managing separate accounts.
Kontor extends Bitcoin's functionality without changing its security model or value proposition. It's a practical path toward decentralized finance on Bitcoin that miners are incentivized to support, developers can build on today, and the broader Bitcoin community can embrace as genuinely Bitcoin-native infrastructure.
# Why Build on Kontor?
Source: https://docs.kontor.network/docs/introduction/why-kontor
**Build where the value is.** Bitcoin is the deepest, most liquid, and most trusted blockchain network in the world. By building on Kontor, you're tapping directly into Bitcoin's ecosystem—its users, its capital, its security guarantees—without the friction of bridges, wrapped tokens, or convincing people to move to yet another chain. Your applications settle on Bitcoin itself, with the same finality guarantees as BTC transactions. There's no separate network to bootstrap, no additional consensus to trust, and no parasitic relationship with the underlying chain.
**Fast and scalable.** Kontor's optimistic consensus layer provides 1–2 second transaction confirmations backed by staker collateral, while Bitcoin remains the ultimate source of finality. Through BLS signature aggregation and compression, Kontor achieves over 1,000 TPS, with dramatically reduced transaction fees—all settling directly on Bitcoin. You get modern application speed without leaving Bitcoin.
**Developer experience.** Kontor's Sigil framework is built on WebAssembly and designed to make writing smart contracts feel like writing normal software. If you know Rust, you can start building immediately—no learning obscure DSLs or fighting with baroque tooling. Sigil offers strong compile-time type safety, native cross-contract calls, and an expressive storage interface that elegantly encodes rich application logic.
**True interoperability.** Kontor offers single-confirmation atomic swaps with Bitcoin and all major Bitcoin metaprotocols—Ordinals, Runes, Counterparty. You can chain a BTC-to-KOR swap directly with a contract call, meaning any Bitcoin user can interact with your application immediately without leaving their wallet or pre-funding a separate account. The hybrid UTXO model treats assets as first-class citizens: they attach to UTXOs for PSBT-based trading and detach for programmable logic. This level of interoperability isn't possible on sidechains or rollups; it requires being genuinely on-chain.
**Aligned with Bitcoin.** Unlike Layer-2s that siphon fees and activity away from Bitcoin, Kontor is synergistic: every Kontor transaction pays Bitcoin miners, every storage agreement increases on-chain activity, and the protocol operates to expand BTC's utility without competing for its monetary premium. KOR is a metered resource for execution and storage, complementary by design. Miners are incentivized to support Kontor because it makes their blocks more valuable without challenging Bitcoin's core value proposition.
# Kontor vs. Alkanes
Source: https://docs.kontor.network/docs/resources/alkanes
Conceptually, Alkanes and Kontor are very much aligned. Both are metaprotocols that bring programmability to Bitcoin with a WASM-based smart contract system. In practice, however, they are quite different to work with. Most significantly, Alkanes is:
1. Fully reliant on Bitcoin’s UTXO model
2. Does not have a gas currency
3. Builds on top of Runes infrastructure
Kontor, by contrast, has a **hybrid accounts/UTXO model** (first implemented in Counterparty), that supports interoperability with Bitcoin via PSBTs and atomic swaps. Kontor has a sophisticated tokenomics for its native currency, which is the basis for its gas system and provides incentives for perpetual Bitcoin-anchored file storage. Kontor’s Sigil smart contract framework emphasizes **clarity, ergonomics, and guardrails** with typed entrypoints, structured state, a testing-friendly design.
# Smart Contracts
## Programming Model and ABI
With Alkanes, each contract implements a single WASM entrypoint and **manually dispatches on numeric opcodes**, parsing raw input bytes, and populating a response buffer. For each contract, you have to implement `AlkaneResponder::execute`, a required `__execute` export, and a `match` over integer opcodes, plus a **DIY key-value storage** layout built from “pointers” (string keypaths). This is a direct consequence of over-reliance on the “Protorunes” specification: method invocations are encoded as Protorunes messages in `OP_RETURN`, with inputs packed into “cellpacks.” That means all decoding, validation, and error reporting live in the custom opcode handlers.
Sigil’s core idea—*first-class, typed contract methods*—makes contracts read like ordinary Rust services: multiple named entrypoints, explicit parameter and return types, and interface boundaries that the compiler can check. That yields self-documenting interfaces/ABIs, clear call sites, and predictable error surfaces. It also unlocks straightforward scaffolding (client SDKs, mocks, auto-generated bindings) without bespoke conventions, and Sigil makes malformed calls impossible to even *construct* in most client code.
## Storage
Alkanes' **string-keyed key-value storage** pushes a lot of design burden onto developers: you invent your own keyspace, document it, and keep it consistent across versions and tooling. A ledger becomes `"/balance/{addr}"`; a cap becomes `"/cap"`, etc. This is fine for small demos, but entropy accumulates in real projects, especially those with multiple authors.
Sigil's storage model uses **typed, structured storage with derive macros** (`StorageRoot`, `Storage`) that generate ORM-like interfaces at compile time. Instead of manually constructing string keys and calling generic `get_value::()` / `set_value::()` functions, you declare Rust structs and the framework generates type-safe accessor methods. For example, a `TokenStorage` struct with a `ledger: Map` field automatically gets `storage(ctx).ledger().get(ctx, key)` and `storage(ctx).ledger().set(ctx, key, value)` methods—no string keys to type, no risk of typos or collisions, and full IDE autocomplete support.
This approach provides several advantages: **compile-time type checking** catches storage access errors before deployment; **nested field access** (e.g., `account.balance(ctx)`) reads specific fields without deserializing entire structures; and **refactoring safety**—renaming a field updates all access points automatically. The framework also generates storage "wrappers" for nested structs, allowing you to read or update sub-fields in complex data structures without manual keyspace management. In Alkanes, equivalent functionality requires careful string interpolation, manual (de)serialization, and vigilant documentation to avoid conflicts between similarly-named keys.
## Safety
Alkanes contracts have **huge surface area**, so there’s a significant risk of reentrancy attacks, callee-controlled control flow, and the need for manual checks-effects-interactions discipline. Alkanes does provide a “call without side effects” mode, but that’s an opt-in pattern; the default is general side-effecting calls.
Sigil’s typed, multi-entry interface encourages **capability scoping**—each method can enforce narrow invariants and accept strongly typed inputs. It also makes it natural to *ban or constrain risky cross-contract patterns* (e.g., reentrancy-enabling call cycles, which are simply disabled, or storage-context borrowing) at the framework layer.
## Tooling, Testing, and DX
A Sigil-style contract is just **Rust with a clean contract boundary.** The natural dev loop is `cargo test` against in-memory harnesses and method-level unit tests; your IDE and linter do the heavy lifting. Mocking callers and state is trivial; running hundreds of fast, pure tests per second is the norm. This is exactly what you want when tightening invariants, refactoring storage layouts, or reproducing edge cases.
With Alkanes, you can test pure helper code natively, but **end-to-end testing typically requires a regtest stack** (Bitcoin Core + Metashrew + a key-value store). This makes it much harder to develop and test Alkanes contracts. Many failure modes only manifest with fully constructed Protorunes messages, UTXO selection, and witness envelopes.
On the *interface* side, Sigil-style typed methods are **immediately discoverable** in code and in client SDKs; Alkanes’ numeric opcodes require you to shepherd a separate, human-maintained mapping (e.g., “77 = mint”), keep encoders/decoders in sync across languages, and document versioning conventions for opcodes and their byte layouts.
## Performance and Resource Contention
While Alkanes’ adoption of the Protorunes specification and its UTXO model nominally allows for parallel execution of contract calls, **every mutating call must be a real Bitcoin transaction,** each of which consumes at least one UTXO. In UTXO land, *each output can be spent exactly once* and conflicting spends must serialize; that’s a hard concurrency ceiling and a source of contention in busy workflows. This makes Alkanes unsuitable for building standard DeFi applications such as AMMs. The fact is that Bitcoin has \~10-minute blocks and **single-digit TPS** on-chain; the bottleneck for state-changing calls is the Bitcoin protocol, not CPU time inside an indexer.
## Side-by-Side Comparison
Below are two minimal “counter” contracts that expose the same three capabilities:
* initialize the counter to 0
* increment by `n`
* read the current value
***
### Sigil — Counter Contract
```wit theme={null}
// counter.wit
package kontor:contract;
world contract {
include kontor:built-in/built-in;
use kontor:built-in/context.{view-context, proc-context};
use kontor:built-in/numbers.{integer};
export init: func(ctx: borrow);
export inc: func(ctx: borrow, n: integer);
export get: func(ctx: borrow) -> integer;
}
```
```rust theme={null}
// counter.rs
use stdlib::*;
contract!(name = "counter");
#[derive(Clone, Default, StorageRoot)]
struct CounterStorage {
pub value: Integer,
}
impl Guest for Counter {
fn init(ctx: &ProcContext) {
// Initialize storage root and set value = 0
CounterStorage::default().init(ctx);
storage(ctx).value().set(ctx, Integer::from(0));
}
fn inc(ctx: &ProcContext, n: Integer) {
let s = storage(ctx);
let cur = s.value().get(ctx).unwrap_or_default();
s.value().set(ctx, cur + n);
}
fn get(ctx: &ViewContext) -> Integer {
storage(ctx).value().get(ctx).unwrap_or_default()
}
}
```
***
### Alkanes — Counter Contract
```rust theme={null}
use anyhow::{anyhow, Result};
use alkanes_runtime::runtime::AlkaneResponder;
use alkanes_support::response::CallResponse;
use alkanes_support::storage::StoragePointer;
use metashrew_support::compat::{to_arraybuffer_layout, to_ptr};
#[derive(Default)]
pub struct Counter;
impl Counter {
fn ptr(&self) -> StoragePointer {
StoragePointer::from_keyword("/counter")
}
}
impl AlkaneResponder for Counter {
fn execute(&self) -> Result {
let context = self.context().ok_or_else(|| anyhow!("no context"))?;
let mut inputs = context.inputs.clone();
let mut resp = CallResponse::forward(&context.incoming_alkanes);
// 0 = init, 1 = inc(n), 2 = get()
match shift_or_err(&mut inputs)? {
0u128 => {
// init: set to 0
self.ptr().set_value::(0);
Ok(resp)
}
1u128 => {
// inc(n): parse n and add (u128 is the canonical doc type)
let n: u128 = shift_or_err(&mut inputs)?;
let cur: u128 = self.ptr().get_value::();
self.ptr().set_value::(cur.checked_add(n).ok_or_else(|| anyhow!("overflow"))?);
Ok(resp)
}
2u128 => {
// get(): return current value in little-endian bytes
let cur: u128 = self.ptr().get_value::();
resp.data = cur.to_le_bytes().to_vec();
Ok(resp)
}
_ => Err(anyhow!("unrecognized opcode")),
}
}
}
#[no_mangle]
pub extern "C" fn __execute() -> i32 {
let mut bytes = to_arraybuffer_layout(&Counter::default().run());
to_ptr(&mut bytes) + 4
}
```
***
Even with an extremely simple contract, the greater ergonomics of the Sigil version are readily apparent. The Sigil contract reads like normal Rust: you update fields on a struct and return typed values. There’s no manual packing/unpacking, no global keyspace to maintain, and far fewer opportunities to drift between the contract and its clients. Alkanes requires developers to invent and document an opcode table, hand-roll a storage key convention (e.g., `"/counter"`), and write custom (de)serialization for arguments and return values.
# Kontor vs. BRC2.0
Source: https://docs.kontor.network/docs/resources/brc2-0
Both BRC2.0 and Kontor both aim to bring programmability to Bitcoin with metaprotocol-based smart contract execution. Unlike Kontor, however, BRC-2.0:
1. Uses a port of the EVM for its contract runtime
2. Does not have a native gas currency
BRC2.0's choice of using the EVM execution environment aims to leverage Ethereum's large developer ecosystem and smart contract libraries. However, the constraints of running EVM on Bitcoin create significant compatibility limitations. The core issue is address translation: BRC2.0 maps Bitcoin addresses to EVM addresses via `keccak256(bitcoin_pkscript).slice(-40)`, which creates addresses that have no corresponding private keys. These addresses look like standard Ethereum addresses but cannot produce signatures verifiable by `ecrecover`. This affects:
* Multisig wallets and contract-based accounts
* Meta-transactions and gasless transaction patterns
* EIP-712 typed signatures (used throughout DeFi)
* Account abstraction schemes (ERC-4337)
* Standard `permit()` functions on tokens
BRC2.0 provides a BIP-322 signature verification precompile at address `0xfe`, but this requires rewriting any contract that performs signature verification. Existing Ethereum contracts, including widely-used protocols like Uniswap, Aave, and Safe, therefore need substantial modification to work in BRC2.0. The pattern of using `ecrecover` to verify signatures must be replaced with calls to the custom precompile. This means the benefit of leveraging existing Ethereum codebases is difficult to realize. Wallet integration also requires different infrastructure. Users cannot use MetaMask or standard Web3 wallets; they need Bitcoin wallets supporting BIP-322 signing, but this standard has very limited support.
Beyond the compatibility issues, the EVM's 256-bit stack-based architecture and Solidity's design choices create different trade-offs than WebAssembly-based systems. The EVM was designed for Ethereum's specific constraints in 2015, while WebAssembly emerged only later as a portable compilation target for broader industry adoption.
# Kontor File Storage
Source: https://docs.kontor.network/docs/resources/file-storage
The file storage specification provides comprehensive technical details on file persistence, challenge-response mechanisms, cryptographic proofs, and economic incentives.
Read the complete technical specification for the file persistence protocol (PDF)
## Key Topics Covered
* File preparation and Merkle commitment structure
* Challenge-response mechanism and detection guarantees
* Nova IVC proof system and multi-file aggregation
* Sponsorship markets and trustless data distribution
* Security analysis and attack resistance
* Protocol parameters and cryptographic primitives
# Kontor vs. Move
Source: https://docs.kontor.network/docs/resources/move
The biggest difference between the Sigil and Move smart contract systems is that Sigil is an embedded DSL in Rust designed to make writing contracts feel like writing regular Rust code. Sigil's emphasis is on simplicity, readability, and overall developer experience. Move, by contrast, is a novel DSL with Rust-like syntax that implements an idiosyncratic resource-oriented programming model designed to support greater theoretical performance and safety guarantees but requires learning a significant number of new concepts and implementing complex workflows to fit the Move model. Because of precisely these limitations, Move has split into at least two different main versions: Aptos Move and Sui Move, each of which has its own (significant) variations in programming model, and incompatible tooling. As such, the Move ecosystem is fragmented, and the tooling is even less mature than it would be otherwise.
A significant fraction of Move's design quirks were introduced to support formal verification. Formal verification, while a very interesting technology, is not the silver bullet for code correctness that it is often made out to be. Verification of any meaningful amount of code is labor-intensive and highly technical, which explains its limited adoption up to the present day. Only a few Aptos Move dApps have used formal verification so far, and [Sui Move had no support until 2025](https://blog.sui.io/asymptotic-move-prover-formal-verification/). Even when formal verification is employed, it is usually practical to verify only a small core set of functions and simple properties they should exhibit.
Along similar lines, while Move’s resource model makes it impossible to copy or implicitly drop assets—which does remove a class of bugs—it does not verify business intent in general. Mis‑scoped authorities, mispriced fees, incorrect caps, or time/sequence errors still compile and must be guarded explicitly. You also pay ceremony—abilities, `acquires`, and resource plumbing that ripple through refactors. In Move, explicitly declaring `acquires` shapes is a benefit for auditability and scheduling, but it adds significant friction to development.
Overall, Sigil is much easier to write: it lets one author contracts as idiomatic Rust modules with a typed, multi‑method ABI defined in WIT. You work with familiar constructs (structs, methods, maps) and call signatures like `mint(ctx, n: integer)` or `balance(ctx, acc: string) -> option`. Testing feels like ordinary Rust: spin up an in‑memory runtime, call methods directly, assert results. Most mistakes surface as compiler errors or method‑signature mismatches rather than as byte‑level bugs at runtime.
# Kontor Optimistic Consensus
Source: https://docs.kontor.network/docs/resources/optimistic-consensus
The Optimistic Consensus specification provides a formal description of the protocol for sub-block transaction confirmation.
Read the complete consensus protocol specification (PDF)
## Key Topics Covered
* Protocol model with state-machine replication and threat model
* Transaction submission, validation, and batch formation
* State execution ordering and finalization criteria
* Stake requirements, rewards, and slashing conditions
* Security analysis with safety, liveness, and accountability proofs
* Formal verification in Lean 4
# Kontor Proof-of-Retrievability
Source: https://docs.kontor.network/docs/resources/proof-of-retrievability
The Proof-of-Retrievability specification details the cryptographic system underlying the storage protocol's challenge-response mechanism.
Read the complete cryptographic specification for storage proofs (PDF)
## Key Topics Covered
* Multi-file proof aggregation with File Ledger construction
* Challenge derivation from Bitcoin block entropy
* Nova IVC circuit design with Poseidon hashing
* Spartan SNARK compression for compact proofs
* Security analysis and computational assumptions
# Kontor Scalability
Source: https://docs.kontor.network/docs/resources/scalability
The Scalability specification provides a technical description of how Kontor achieves high throughput within Bitcoin's constraints.
Read the complete scalability analysis and benchmarks (PDF)
## Key Topics Covered
* Transaction format and Bitcoin embedding
* Optimization stack: BLS aggregation, registry IDs, binary encoding, Zstd compression
* Performance benchmarks across workload scenarios
* Bundler economics and fee market dynamics
# Kontor Whitepaper
Source: https://docs.kontor.network/docs/resources/whitepaper
The comprehensive Kontor whitepaper provides in-depth technical details about the protocol design, architecture, and implementation.
Read the full technical specification and protocol design (PDF)
## Key Topics Covered
* Metaprotocol architecture and embedded consensus
* Sigil smart contract framework design
* File persistence protocol specifications
* KOR tokenomics and economic model
* Security considerations and attack resistance
* Implementation details and technical specifications
# Architecture
Source: https://docs.kontor.network/docs/sigil/architecture
## WebAssembly Runtime
Like many other smart contract frameworks, Sigil is built on WebAssembly (WASM), which provides several key benefits for blockchain smart contracts:
* **Deterministic Execution**: Contracts can be executed and interrupted based on gas limits and memory constraints, ensuring predictable behavior.
* **Efficient Bytecode**: WASM offers a portable and efficient bytecode format for the Sigil runtime.
Unlike other WASM-based blockchain languages, Sigil is deeply integrated with the modern **WebAssembly Component Model**, providing a structured and interoperable approach to contract development. Sigil's runtime primitives and userspace abstractions enable secure, gas-efficient contracts with a familiar development experience. Storage is isolated, cross-contract calls are convenient to perform (while re-entrances are strictly prohibited), and a `fallback` hook enables support for contract versioning via proxying.
## WebAssembly Component Model
Traditional WASM modules compiled from other languages produce self-contained but unstructured files, making their contents opaque to other code modules. The WASM Component Model introduces a language-agnostic structure, enhancing reusability and transparency. In Sigil:
* **WIT Signatures**: Every Sigil contract includes a WebAssembly Interface Type (WIT) "header" file that explicitly exports all public functions in a standardized, high-level type language. This makes contracts more accessible to developers and tooling.
* **Runtime Validation**: The WIT file is compiled into the WASM code, allowing the Sigil runtime to extract and validate the contract's interface at runtime and for tooling integration.
* **Future Component Imports**: The Component Model lays the foundation for importing other components for code reuse, though cross-contract calls currently use a foreign call primitive in an isolated execution context.
* **WAVE Format**: Sigil uses the WebAssembly Value Expression (WAVE) format for passing arguments and returning results, standardizing value types across languages compiled to WASM.
* **Exported Function Conventions**: Exported functions in WIT files follow standard WIT types, with two additional conventions:
* **Hook-Based Execution**: Functions with specific names serve as hooks and are called under certain conditions and not explicitly by a user.
* **Execution Context**: The first argument of every function is an execution context, defining its behavior and access rights.
# Key Features
### Hooks
Sigil defines a minimal but full-featured set of hooks to handle specific contract behaviors:
* **`init` Hook**: Runs when a contract is published and is primarily for setting initial storage values or perform data migrations for contract upgrades. It currently supports arbitrary contract code execution.
* **`fallback` Hook**: Accepts WAVE arguments and returns WAVE results. This hook is called when no other function matches the call, and it is primarily used to implement proxy contracts for versioning in userspace, similar to how proxying is implemented in Solidity.
### Execution Contexts
Public functions in Sigil are "decorated" with one of three execution contexts, controlling their behavior and access:
* **`proc` (Procedure) Context**:
* Can read and write to storage
* Callable when submitting blockchain transactions
* **`view` Context**:
* Read-only access to storage
* Callable via the API exposed by Kontor nodes
* **`fall` (Fallback) Context**:
* Used exclusively for the `fallback` hook
* Can be converted into other context types
Both `proc` and `view` functions can be called within a single contract or across contracts via foreign calls, with static type safety in the SDK and runtime checks ensuring compliance with read-only or read-write rules. Unlike some blockchain languages, Sigil does not implicitly expose storage contents. Developers must explicitly create `view` functions to expose data, enabling fine-grained control for future migrations or upgrades. These functions can include arbitrary code, such as userspace authentication or authorization.
### Primitives
Sigil's runtime primitives keep the virtual machine simple and secure, encouraging userspace abstractions:
1. **Signer Access**: Available only in `proc` contexts, allows access to the signer of the current transaction
2. **Cross-Contract Calls**: A function for calling other contracts, contributing to the gas expenditure of the transaction
3. **Storage Functions**:
* `get` and `set` operations for primitive types (e.g., strings, u64), parameterized by a key or path
* Checking if a key exists in storage
* Regex-based path matching
The Sigil Rust SDK abstracts these primitives into higher-level, ORM-like interfaces, reducing the need for direct interaction.
### Library Code and SDK
Sigil emphasizes userspace development through Rust traits and macros:
* **Traits for Runtime Primitives**: The runtime implements traits for all primitives, allowing pure Rust libraries (including Sigil's standard library) to operate independently of the runtime. Developers can implement in-memory versions of these traits for testing without relying on the Sigil runtime.
* **Ordinary Rust Programming**: Rust macros make Sigil programming feel simple and familiar.
* **Private Helper Functions**: Non-exported functions (without WIT signatures) compile to regular WASM instructions, behaving like standard Rust functions.
### Storage and Versioning
Sigil's storage system is designed for simplicity, isolation, and predictable gas usage:
* **Primitive Operations**: Storage uses path-keyed `get` and `set` operations for basic types (e.g., integers, booleans, strings).
* **ORM-like Macros**: The SDK provides macros for declaring persistent data types, including records, enums, non-recursive sum types, and built-in `Map` types. Nested fields can be accessed (e.g., `foo.bar.baz`) without retrieving entire structures.
* **Isolation**: Each contract's storage is isolated, with no equivalent to Ethereum's `delegatecall`. This simplifies the mental model, treating foreign contract calls like web API calls.
* **Gas Tracking**: The design avoids complex operations (e.g., SQL-like joins) to make gas consumption intuitive.
* **Versioning**: Sigil supports userspace versioning through the `fallback` hook and proxy contracts. Developers can implement advanced upgrade mechanisms, such as DAO-like voting for safe upgrades, avoiding reliance on key-based upgrades that risk rug pulls. External calls to proxy contracts appear as standard contract calls, enhancing transparency.
### Error Handling
Sigil provides a robust error handling mechanism:
* **Error Types**: Errors are represented as a custom `Error` type, which can be returned from functions.
* **Error Propagation**: Errors can be propagated up the call stack using the `?` operator.
* **Error Handling**: `Result`s can be handled using `match` expressions over `Ok` and `Err` variants or with the many other error handling strategies available in Rust.
When a function returns an `Err` or `panic`s, all storage changes are rolled back. For nested calls, the entire call chain is rolled back if any function errors. This means if Contract A calls Contract B and B returns an error, ALL storage changes in both A and B are rolled back—the entire transaction has no effect. A transaction either completes fully with all storage changes persisted, or fails entirely with no storage changes.
### Safety
Sigil simplifies reasoning about safety compared to languages like Solidity by eliminating features like `delegatecall` and completely disallowing cross-contract calling cycles. However, developers should still follow the **Checks-Effects-Interactions (CEI)** pattern:
* **Check**: Verify conditions (e.g., sufficient funds)
* **Effect**: Update storage (e.g., deduct funds)
* **Interaction**: Perform external operations (e.g., cross-contract calls)
For example:
```rust theme={null}
check_sufficient_funds(); // Check
transfer_funds_in_storage(); // Effect
do_external_operations(); // Interaction
```
Leveraging this pattern makes code easier to reason about and adds an additional defensive layer.
# 6. Automated Market-Maker (AMM)
Source: https://docs.kontor.network/docs/sigil/examples/amm
**Full working code**: The complete source code for this example is available in [`test-contracts/amm/`](https://github.com/KontorProtocol/Kontor/tree/main/test-contracts/amm)
This example builds on top of the Token contract and demonstrates swaps and liquidity provision on an AMM. It demonstrates transfers of balances using the `token-dyn` interface for cross-contract calls to the Token contract, and complex integer arithmetic using the 256-bit `Integer` built-in type.
**Standards Pattern**: This AMM accepts any token contract address that implements the transfer interface. The compiler ensures the AMM makes valid calls to that interface, but cannot verify at compile time that user-provided addresses actually implement it.
## WIT Interface
Imports types:
* `signer`: Transaction sender (identity)
* `error`: Failure handling
* `integer`: Amounts
and exports functions:
* `init`: Initializes the contract
* `create`: Create a liquidity pool for a new token pair
* `fee`: Get the fee (in basis points of input amount) for trading in a pair
* `balance`: Queries a liquidity provider (LP) share balance
* `token-balance`: Queries the balance of tokens in a liquidity pool
* `deposit`: Deposit both tokens of a pair and receive LP shares
* `withdraw`: Burn LP shares and withdraw the tokens they represent
* `swap`: Swap an amount of one token of a pair for the other token
* `quote-deposit`: Receive a quote of cost of shares received when depositing
* `quote-withdraw`: Receive a quote of tokens received for shares when withdrawing
* `quote-swap`: Receive a quote of out-tokens received when swapping
```wit theme={null}
package root:component;
world root {
include kontor:built-in/built-in;
use kontor:built-in/context.{view-context, proc-context, signer};
use kontor:built-in/error.{error};
use kontor:built-in/foreign.{contract-address};
use kontor:built-in/numbers.{integer, decimal};
record token-pair {
a: contract-address,
b: contract-address,
}
record deposit-result {
lp-shares: integer,
deposit-a: integer,
deposit-b: integer,
}
record withdraw-result {
amount-a: integer,
amount-b: integer,
}
export init: func(ctx: borrow);
export create: func(ctx: borrow, pair: token-pair, amount-a: integer, amount-b: integer, fee-bps: integer) -> result;
export fee: func(ctx: borrow, pair: token-pair) -> result;
export balance: func(ctx: borrow, pair: token-pair, acc: string) -> option;
export token-balance: func(ctx: borrow, pair: token-pair, token: contract-address) -> result;
export quote-deposit: func(ctx: borrow, pair: token-pair, amount-a: integer, amount-b: integer) -> result;
export deposit: func(ctx: borrow, pair: token-pair, amount-a: integer, amount-b: integer) -> result;
export quote-withdraw: func(ctx: borrow, pair: token-pair, shares: integer) -> result;
export withdraw: func(ctx: borrow, pair: token-pair, shares: integer) -> result;
export swap: func(ctx: borrow, pair: token-pair, token-in: contract-address, amount-in: integer, min-out: integer) -> result;
export quote-swap: func(ctx: borrow, pair: token-pair, token-in: contract-address, amount-in: integer) -> result;
}
```
## Rust Implementation
* `AMMStorage` represents the contract’s persistent storage. The `StorageRoot` derive macro enables storage capabilities for the type and marks it as the "root" storage type. The root storage is accessible via a generated function: `storage`, which provides an ORM-like interface to contract functions. Every contract with storage must have exactly one type that derives `StorageRoot`.
* `Map` is a storage-enabled mapping type that can hold values of any type that also derive `Storage`. Holds mappings from token pairs to `Pool`.
* `Pool` contains the information about the state of the pool for a token pair.
* The `ctx` parameter must be passed to every operation that performs a database call. These functions take an `impl ReadContext` or an `impl WriteContext`, depending on their behavior. `ProcContext` implements both, while `ViewContext` implements only `ReadContext`. This ensures state mutations occur only as the result of a transaction while allowing the same functions and methods to be used across procedures and views.
```rust theme={null}
use stdlib::*;
contract!(name = "amm");
interface!(name = "token_dyn", path = "../../token/contract/wit");
#[derive(Clone, Storage)]
struct Pool {
pub token_a: ContractAddress,
pub balance_a: Integer,
pub token_b: ContractAddress,
pub balance_b: Integer,
pub fee_bps: Integer,
pub lp_total_supply: Integer,
pub lp_ledger: Map,
}
#[derive(Clone, StorageRoot)]
struct AMMStorage {
pub pools: Map,
pub custodian: String,
}
fn pair_id(pair: &TokenPair) -> String {
format!("{}::{}", pair.a, pair.b)
}
fn pair_other_token(
pair: &TokenPair,
token_in: &ContractAddress,
) -> Result {
if token_in == &pair.a {
Ok(pair.b.clone())
} else if token_in == &pair.b {
Ok(pair.a.clone())
} else {
Err(Error::Message(format!("token {} not in pair", token_in)))
}
}
fn validate_pair(pair: &TokenPair) -> Result<(), Error> {
if pair.a.name.is_empty() || pair.b.name.is_empty() {
return Err(Error::Message(
"Token addresses must not be empty".to_string(),
));
}
if pair.a.to_string() >= pair.b.to_string() {
return Err(Error::Message(
"Token pair must be ordered A < B".to_string(),
));
}
Ok(())
}
fn validate_amount(amount: Integer) -> Result<(), Error> {
// 0 < amount < sqrt(MAX_INT)
if amount <= Integer::default() || amount > "340_282_366_920_938_463_463_374_607_431".into() {
return Err(Error::Message("bad amount".to_string()));
}
Ok(())
}
fn calc_swap_result(
amount_in: Integer,
bal_in: Integer,
bal_out: Integer,
fee_bps: Integer,
) -> Result {
validate_amount(amount_in)?;
validate_amount(bal_in)?;
validate_amount(bal_out)?;
// input amount less fee, round down
let bps_in_100pct = 10000.into();
let in_less_fee = amount_in * (bps_in_100pct - fee_bps) / bps_in_100pct;
let new_bal_in = bal_in + in_less_fee;
validate_amount(new_bal_in)?;
// calculate output amount from delta in output-token balance, round down
let k = bal_in * bal_out;
Ok((bal_out * new_bal_in - k) / new_bal_in)
}
fn get_pool(ctx: &impl ReadContext, pair: &TokenPair) -> Result {
let id = pair_id(pair);
let pools = ctx.model().pools();
pools
.get(&id)
.ok_or_else(|| Error::Message("Pool not found".to_string()))
}
fn quote_swap(
ctx: &impl ReadContext,
pair: &TokenPair,
token_in: &ContractAddress,
amount_in: Integer,
) -> Result {
let pool = get_pool(ctx, pair)?;
let (bal_in, bal_out) = if token_in == &pair.a {
(pool.balance_a(), pool.balance_b())
} else {
(pool.balance_b(), pool.balance_a())
};
calc_swap_result(amount_in, bal_in, bal_out, pool.fee_bps())
}
fn quote_deposit(
ctx: &impl ReadContext,
pool: &PoolWrapper,
amount_a: Integer,
amount_b: Integer,
) -> Result {
validate_amount(amount_a)?;
validate_amount(amount_b)?;
let lp_supply = pool.lp_total_supply();
let balance_a = pool.balance_a();
let balance_b = pool.balance_b();
let lp_shares = if amount_a * balance_b < amount_b * balance_a {
amount_a * lp_supply / balance_a
} else {
amount_b * lp_supply / balance_b
};
let supply_minus_one = lp_supply - 1.into();
Ok(DepositResult {
deposit_a: (lp_shares * balance_a + supply_minus_one) / lp_supply, // round up
deposit_b: (lp_shares * balance_b + supply_minus_one) / lp_supply, // round up
lp_shares,
})
}
fn quote_withdraw(
ctx: &impl ReadContext,
pool: &PoolWrapper,
shares: Integer,
) -> Result {
validate_amount(shares)?;
let lp_total_supply = pool.lp_total_supply();
Ok(WithdrawResult {
amount_a: shares * pool.balance_a() / lp_total_supply,
amount_b: shares * pool.balance_b() / lp_total_supply,
})
}
impl Guest for Amm {
fn init(ctx: &ProcContext) {
let custodian = ctx.contract_signer().to_string();
AMMStorage {
pools: Map::default(),
custodian,
}
.init(ctx)
}
fn create(
ctx: &ProcContext,
pair: TokenPair,
amount_a: Integer,
amount_b: Integer,
fee_bps: Integer,
) -> Result {
validate_pair(&pair)?;
validate_amount(amount_a)?;
validate_amount(amount_b)?;
match get_pool(ctx, &pair) {
Ok(_) => Err(Error::Message(
"pool for this pair already exists".to_string(),
)),
Err(_) => Ok(()),
}?;
let lp_shares = (amount_a * amount_b).sqrt()?;
let admin = ctx.signer().to_string();
ctx.model().pools().set(
pair_id(&pair),
Pool {
token_a: pair.a.clone(),
balance_a: amount_a,
token_b: pair.b.clone(),
balance_b: amount_b,
fee_bps,
lp_total_supply: lp_shares,
lp_ledger: Map::new(&[(admin, lp_shares)]),
},
);
let custodian = ctx.contract_signer().to_string();
token_dyn::transfer(&pair.a, ctx.signer(), &custodian, amount_a)?;
token_dyn::transfer(&pair.b, ctx.signer(), &custodian, amount_b)?;
Ok(lp_shares)
}
fn fee(ctx: &ViewContext, pair: TokenPair) -> Result {
Ok(get_pool(ctx, &pair)?.fee_bps())
}
fn balance(ctx: &ViewContext, pair: TokenPair, acc: String) -> Option {
get_pool(ctx, &pair)
.ok()
.and_then(|p| p.lp_ledger().get(acc))
}
fn token_balance(
ctx: &ViewContext,
pair: TokenPair,
token: ContractAddress,
) -> Result {
pair_other_token(&pair, &token)?;
let pool = get_pool(ctx, &pair)?;
if token == pair.a {
Ok(pool.balance_a())
} else {
Ok(pool.balance_b())
}
}
fn quote_deposit(
ctx: &ViewContext,
pair: TokenPair,
amount_a: Integer,
amount_b: Integer,
) -> Result {
quote_deposit(ctx, &get_pool(ctx, &pair)?, amount_a, amount_b)
}
fn deposit(
ctx: &ProcContext,
pair: TokenPair,
amount_a: Integer,
amount_b: Integer,
) -> Result {
let pool = get_pool(ctx, &pair)?;
let res = quote_deposit(ctx, &pool, amount_a, amount_b)?;
let ledger = pool.lp_ledger();
let addr = ctx.model().custodian();
pool.update_balance_a(|b| b + res.deposit_a);
pool.update_balance_b(|b| b + res.deposit_b);
let user = ctx.signer().to_string();
let bal = ledger.get(&user).unwrap_or_default();
ledger.set(user, bal + res.lp_shares);
pool.set_lp_total_supply(pool.lp_total_supply() + res.lp_shares);
token_dyn::transfer(&pair.a, ctx.signer(), &addr, res.deposit_a)?;
token_dyn::transfer(&pair.b, ctx.signer(), &addr, res.deposit_b)?;
Ok(res)
}
fn quote_withdraw(
ctx: &ViewContext,
pair: TokenPair,
shares: Integer,
) -> Result {
quote_withdraw(ctx, &get_pool(ctx, &pair)?, shares)
}
fn withdraw(
ctx: &ProcContext,
pair: TokenPair,
shares: Integer,
) -> Result {
let pool = get_pool(ctx, &pair)?;
let res = quote_withdraw(ctx, &pool, shares)?;
let ledger = pool.lp_ledger();
let user = ctx.signer().to_string();
let total = pool.lp_total_supply();
let bal = ledger.get(&user).unwrap_or_default();
if total < shares {
return Err(Error::Message("insufficient total supply".to_string()));
}
if bal < shares {
return Err(Error::Message("insufficient share balance".to_string()));
}
ledger.set(user.clone(), bal - shares);
pool.set_lp_total_supply(total - shares);
pool.update_balance_a(|b| b - res.amount_a);
pool.update_balance_b(|b| b - res.amount_b);
token_dyn::transfer(&pair.a, ctx.contract_signer(), &user, res.amount_a)?;
token_dyn::transfer(&pair.b, ctx.contract_signer(), &user, res.amount_b)?;
Ok(res)
}
fn quote_swap(
ctx: &ViewContext,
pair: TokenPair,
token_in: ContractAddress,
amount_in: Integer,
) -> Result {
quote_swap(ctx, &pair, &token_in, amount_in)
}
fn swap(
ctx: &ProcContext,
pair: TokenPair,
token_in: ContractAddress,
amount_in: Integer,
min_out: Integer,
) -> Result {
let token_out = pair_other_token(&pair, &token_in)?;
let amount_out = quote_swap(ctx, &pair, &token_in, amount_in)?;
if amount_out < min_out {
return Err(Error::Message(format!(
"amount out ({}) below minimum",
amount_out
)));
}
let pool = get_pool(ctx, &pair)?;
if token_in == pair.a {
pool.update_balance_a(|b| b + amount_in);
pool.update_balance_b(|b| b - amount_out);
} else {
pool.update_balance_a(|b| b - amount_out);
pool.update_balance_b(|b| b + amount_in);
}
token_dyn::transfer(
&token_in,
ctx.signer(),
&ctx.model().custodian(),
amount_in,
)?;
token_dyn::transfer(
&token_out,
ctx.contract_signer(),
&ctx.signer().to_string(),
amount_out,
)?;
Ok(amount_out)
}
}
```
## Testing
Similar to the tests for other contracts, this one uses the `interface!` macro to generate an interface for calling the contract. It also includes assertions for error handling. For all calls, the first `?` operator checks whether the runtime threw an error during execution. For contract functions that explicitly return a `Result`, the result can be "unwrapped" with an additional `?` operator or left omitted to make an assertion on an error.
The test publishes three contracts: the `amm` contract and two separate instances of the `token` contract to serve as the trading pair.
When working with numbers in Sigil, either the `Integer` or `Decimal` types should be used. `From` instances have been implemented for many of the primitive types which is why there are many `.into()`s in the test. One can also write: `Integer::from(100)`, or `Decimal::from("1.5")`, or even `let x: Decimal = 1.5.into()`, etc.
```rust theme={null}
#[cfg(test)]
mod tests {
use testlib::*;
interface!(name = "amm");
interface!(name = "token_a", path = "../../token/contract/wit");
interface!(name = "token_b", path = "../../token/contract/wit");
#[testlib::test(contracts_dir = "../../")]
async fn test_contract() -> Result<()> {
let admin = runtime.identity().await?;
let minter = runtime.identity().await?;
let amm = runtime.publish(&admin, "amm").await?;
let token_a = runtime.publish(&admin, "token").await?;
let token_b = runtime.publish(&admin, "token").await?;
token_a::mint(runtime, &token_a, &minter, 1000.into()).await??;
token_b::mint(runtime, &token_b, &minter, 1000.into()).await??;
token_a::transfer(runtime, &token_a, &minter, &admin, 100.into()).await??;
token_b::transfer(runtime, &token_b, &minter, &admin, 500.into()).await??;
let pair = amm::TokenPair {
a: token_a.clone(),
b: token_b.clone(),
};
let res = amm::create(
runtime,
&amm,
&admin,
pair.clone(),
100.into(),
500.into(),
0.into(),
)
.await?;
assert_eq!(res, Ok(223.into()));
let bal_a = amm::token_balance(runtime, &amm, pair.clone(), token_a.clone()).await?;
assert_eq!(bal_a, Ok(100.into()));
let bal_b = amm::token_balance(runtime, &amm, pair.clone(), token_b.clone()).await?;
assert_eq!(bal_b, Ok(500.into()));
let k1 = bal_a.unwrap() * bal_b.unwrap();
let res = amm::quote_swap(runtime, &amm, pair.clone(), token_a.clone(), 10.into()).await?;
assert_eq!(res, Ok(45.into()));
let res = amm::quote_swap(runtime, &amm, pair.clone(), token_a.clone(), 100.into()).await?;
assert_eq!(res, Ok(250.into()));
let res = amm::quote_swap(runtime, &amm, pair.clone(), token_a.clone(), 1000.into()).await?;
assert_eq!(res, Ok(454.into()));
let res = amm::swap(
runtime,
&amm,
&minter,
pair.clone(),
token_a.clone(),
10.into(),
46.into(),
)
.await?;
assert!(res.is_err()); // below minimum
let res = amm::swap(
runtime,
&amm,
&minter,
pair.clone(),
token_a.clone(),
10.into(),
45.into(),
)
.await?;
assert_eq!(res, Ok(45.into()));
let bal_a = amm::token_balance(runtime, &amm, pair.clone(), token_a.clone()).await?;
let bal_b = amm::token_balance(runtime, &amm, pair.clone(), token_b.clone()).await?;
let k2 = bal_a.unwrap() * bal_b.unwrap();
assert!(k2 >= k1);
let res = amm::quote_swap(runtime, &amm, pair.clone(), token_b.clone(), 45.into()).await?;
assert_eq!(res, Ok(9.into()));
let res = amm::swap(
runtime,
&amm,
&minter,
pair.clone(),
token_b.clone(),
45.into(),
0.into(),
)
.await?;
assert_eq!(res, Ok(9.into()));
let bal_a = amm::token_balance(runtime, &amm, pair.clone(), token_a.clone()).await?;
let bal_b = amm::token_balance(runtime, &amm, pair.clone(), token_b.clone()).await?;
let k3 = bal_a.unwrap() * bal_b.unwrap();
assert!(k3 >= k2);
Ok(())
}
}
```
# 1. Hello World
Source: https://docs.kontor.network/docs/sigil/examples/hello-world
**Full working code**: The complete source code for this example is shown on this page. More complete example contracts are available in [`test-contracts/`](https://github.com/KontorProtocol/Kontor/tree/main/test-contracts)
This example walks you through creating your first Sigil contract—a simple "Hello World" that demonstrates the fundamental structure and workflow.
## Project Structure
```
hello-world/
├── Cargo.toml # Workspace configuration
├── contract/
│ ├── Cargo.toml # Contract dependencies
│ ├── wit/
│ │ └── contract.wit
│ └── src/
│ └── lib.rs
└── test/
├── Cargo.toml # Test dependencies
└── src/
└── lib.rs
```
## Step 1: Workspace Configuration
**Root `Cargo.toml`:**
```toml theme={null}
[workspace]
members = ["contract", "test"]
resolver = "2"
[workspace.dependencies]
stdlib = { path = "../../Kontor/core/stdlib" }
testlib = { path = "../../Kontor/core/testlib" }
```
The paths shown assume the `Documentation` and `Kontor` repos are siblings. If you cloned them to different locations, adjust these paths to point to your Kontor installation. See [Getting Started](/docs/sigil/quickstart#working-with-example-contracts) for setup details.
## Step 2: Contract Configuration
**`contract/Cargo.toml`:**
```toml theme={null}
[package]
name = "hello-world"
version = "0.1.0"
edition = "2024"
[dependencies]
stdlib = { workspace = true }
[lib]
crate-type = ["cdylib"] # Required for WASM compilation
```
**Key points:**
* `crate-type = ["cdylib"]` - Builds a dynamic library for WASM
* `stdlib` provides the `contract!` macro and runtime primitives
## Step 3: Define the Contract Interface (WIT)
Create `contract/wit/contract.wit`:
```wit theme={null}
package root:component;
world root {
include kontor:built-in/built-in;
use kontor:built-in/context.{view-context, proc-context};
export init: func(ctx: borrow);
export hello-world: func(ctx: borrow) -> string;
}
```
**What this defines:**
* `init` - Called once when contract is deployed
* `hello-world` - Returns a greeting string (read-only)
The `kontor:built-in` types are available via a symlink at `wit/deps/built-in.wit`.
## Step 4: Implement the Contract
Create `contract/src/lib.rs`:
```rust contract/src/lib.rs theme={null}
use stdlib::*;
contract!(name = "hello-world");
impl Guest for HelloWorld {
fn init(_ctx: &ProcContext) {}
fn hello_world(_ctx: &ViewContext) -> String {
"Hello, World!".to_string()
}
}
```
**How it works:**
* `contract!(name = "hello-world")` - Generates the `Guest` trait and WASM bindings
* `impl Guest for HelloWorld` - The contract name is converted to PascalCase
* `init()` - Empty because we don't need storage
* `hello_world()` - Returns a static string
## Step 5: Build and Test
The contract is automatically built when you run tests. The test directory's `build.rs` handles compilation, optimization, and compression:
```bash theme={null}
cd test
cargo test
```
The `build.rs` automatically:
1. Compiles the contract to WASM
2. Optimizes with `wasm-opt -Oz --enable-bulk-memory --enable-sign-ext`
3. Compresses with `brotli` to create `.wasm.br`
**To build manually without running tests:**
```bash theme={null}
cd contract
cargo build --release --target wasm32-unknown-unknown
wasm-opt -Oz --enable-bulk-memory --enable-sign-ext \
target/wasm32-unknown-unknown/release/hello_world.wasm \
-o hello_world_opt.wasm
brotli -Zf hello_world_opt.wasm -o hello_world.wasm.br
```
## Step 6: Write Tests
Create `test/src/lib.rs`:
```rust test/src/lib.rs theme={null}
#[cfg(test)]
mod tests {
use testlib::*;
interface!(name = "hello-world");
#[testlib::test]
async fn test_contract() -> Result<()> {
let alice = runtime.identity().await?;
let hello_world = runtime.publish(&alice, "hello-world").await?;
let result = hello_world::hello_world(runtime, &hello_world).await?;
assert_eq!(result, "Hello, World!");
Ok(())
}
}
```
**What this does:**
* `interface!` - Generates type-safe bindings from the WIT file
* `#[testlib::test]` - Auto-injects the `runtime` variable
* `runtime.identity()` - Creates a test user with gas funding
* `runtime.publish()` - Deploys the contract and returns its address
* `hello_world::hello_world()` - Calls the contract function
**Run tests:**
```bash theme={null}
cd test
cargo test
```
## Common First-Time Issues
**`contract!` macro not found**
```
error: cannot find macro `contract` in this scope
```
**Solution:** Add `use stdlib::*;` at the top of your file
**Missing Guest trait implementation**
```
error: not all trait items implemented, missing: `hello_world`
```
**Solution:** Ensure you've implemented all functions listed in your WIT file. Function names with hyphens in WIT become underscores in Rust.
**WIT file parse error**
```
error: expected `include`, `use`, `import`, `export`, or `type`
```
**Solution:** Ensure you have `include kontor:built-in/built-in;` in your WIT world
**Build target not installed**
```
error: can't find crate for `core`
```
**Solution:** Install the WASM target: `rustup target add wasm32-unknown-unknown`
# Examples Overview
Source: https://docs.kontor.network/docs/sigil/examples/index
This section covers the example contracts in `example-contracts/`. Each is a self-contained example that builds on concepts introduced in previous ones, progressing from a minimal contract to production-grade DeFi applications. The examples demonstrate how to develop blockchain smart contracts with seamless Rust-like testing and rapid feedback in a local development environment.
## Suggested Learning Order
1. **hello-world** → Minimal contract structure, basic functions
2. **token** → Map-based ledger, Integer type, error handling
3. **shared-account** → Nested Maps, authorization patterns
4. **proxy** → Fallback function, delegation patterns
5. **pool** → DeFi math, liquidity pools
6. **amm** → Multi-pool AMM, production patterns
## Example Contracts
### hello-world
Minimal contract demonstrating basic contract structure, view functions, and simple state management.
[View Example →](/docs/sigil/examples/hello-world)
***
### token (59 lines)
Fungible token with Map-based ledger, arbitrary precision Integer arithmetic, balance transfers, and error handling for insufficient funds.
[View Example →](/docs/sigil/examples/token)
***
### shared-account
Multi-tenant account management with two-level nested Maps, authorization patterns (owner and tenants), helper functions for access control, and contract receiving tokens via `contract_signer()`.
[View Example →](/docs/sigil/examples/shared-account)
***
### proxy
Generic contract delegation using the fallback function, FallContext, and `foreign::call` for dynamic forwarding. Demonstrates proxy/upgrade patterns.
[View Example →](/docs/sigil/examples/proxy)
***
### pool (200+ lines)
Single liquidity pool implementation with constant product AMM math, LP token management, swap calculations, and slippage protection using Integer arithmetic.
[View Example →](/docs/sigil/examples/pool)
***
### amm (350+ lines)
Multi-pool automated market maker managing multiple token pairs with Map of Pool structs, dynamic token handling, pool creation, and production-grade validation logic.
[View Example →](/docs/sigil/examples/amm)
***
## Project Structure
Each contract directory contains:
* **`contract/`** - Rust code (`src/lib.rs`), WIT interface (`wit/contract.wit`), and `wit/deps/` (symlink to built-in types)
* **`test/`** - Tests (`src/lib.rs`), `build.rs` (compiles, optimizes, compresses contract), and dependencies
* **Root `Cargo.toml`** - Workspace configuration
## Running Examples
To test any example contract:
```bash theme={null}
cd example-contracts/token # or any other example
cargo test
```
Tests use the Kontor runtime for simulation, loading contract bytes from `target/` after compilation. This provides fast iteration like standard Rust development. Each example assumes familiarity with prior ones, focusing on new concepts.
## Finding the Code
All example contracts are in the repository under `example-contracts/`:
```
example-contracts/
├── crypto/
├── token/
├── arith/
├── fib/
├── proxy/
├── shared-account/
├── pool/
└── amm/
```
# 7. Liquidity Pool
Source: https://docs.kontor.network/docs/sigil/examples/pool
**Full working code**: The complete source code for this example is available in [`test-contracts/pool/`](https://github.com/KontorProtocol/Kontor/tree/main/test-contracts/pool)
This example closely mirrors the AMM contract's logic but supports only one token pair. This design allows the pool to implement a token interface, where the pool's token represents shares in the liquidity pool, enabling interaction through standard token methods (demonstrated at the test's end). In a production environment, a middleware application would manage pool creation for users and maintain a mapping of token pairs to pool contract IDs.
## WIT Interface
```wit theme={null}
package root:component;
world root {
include kontor:built-in/built-in;
use kontor:built-in/context.{view-context, proc-context, signer};
use kontor:built-in/error.{error};
use kontor:built-in/foreign.{contract-address};
use kontor:built-in/numbers.{integer, decimal};
record deposit-result {
lp-shares: integer,
deposit-a: integer,
deposit-b: integer,
}
record withdraw-result {
amount-a: integer,
amount-b: integer,
}
export init: func(ctx: borrow);
export re-init: func(ctx: borrow, token-a: contract-address, amount-a: integer, token-b: contract-address, amount-b: integer, fee: integer) -> result;
export fee: func(ctx: borrow) -> integer;
export balance: func(ctx: borrow, acc: string) -> option;
export transfer: func(ctx: borrow, to: string, n: integer) -> result<_, error>;
export token-balance: func(ctx: borrow, token: contract-address) -> result;
export quote-deposit: func(ctx: borrow, amount-a: integer, amount-b: integer) -> result;
export deposit: func(ctx: borrow, amount-a: integer, amount-b: integer) -> result;
export quote-withdraw: func(ctx: borrow, shares: integer) -> result;
export withdraw: func(ctx: borrow, shares: integer) -> result;
export swap: func(ctx: borrow, token-in: contract-address, amount-in: integer, min-out: integer) -> result;
export quote-swap: func(ctx: borrow, token-in: contract-address, amount-in: integer) -> result;
}
```
## Rust Implementation
```rust theme={null}
use stdlib::*;
contract!(name = "pool");
interface!(name = "token_dyn", path = "../../token/contract/wit");
#[derive(Clone, StorageRoot)]
struct PoolStorage {
pub token_a: ContractAddress,
pub token_b: ContractAddress,
pub fee_bps: Integer,
pub lp_total_supply: Integer,
pub lp_ledger: Map,
pub custodian: String,
}
impl PoolStorage {
pub fn new(
ctx: &ProcContext,
token_a: ContractAddress,
amount_a: Integer,
token_b: ContractAddress,
amount_b: Integer,
fee_bps: Integer,
) -> Result {
validate_amount(amount_a)?;
validate_amount(amount_b)?;
let lp_shares = (amount_a * amount_b).sqrt()?;
let custodian = ctx.contract_signer().to_string();
let pool = PoolStorage {
token_a: token_a.clone(),
token_b: token_b.clone(),
fee_bps,
lp_total_supply: lp_shares,
lp_ledger: Map::new(&[(ctx.signer().to_string(), lp_shares)]),
custodian: custodian.clone(),
};
token_dyn::transfer(&token_a, ctx.signer(), &custodian, amount_a)?;
token_dyn::transfer(&token_b, ctx.signer(), &custodian, amount_b)?;
Ok(pool)
}
}
fn token_out(ctx: &impl ReadContext, token_in: &ContractAddress) -> Result {
let token_a = ctx.model().token_a();
let token_b = ctx.model().token_b();
if token_in == &token_a {
Ok(token_b)
} else if token_in == &token_b {
Ok(token_a)
} else {
Err(Error::Message(format!("token {} not in pair", token_in)))
}
}
fn validate_amount(amount: Integer) -> Result<(), Error> {
// 0 < amount < sqrt(MAX_INT)
if amount <= Integer::default() || amount > "340_282_366_920_938_463_463_374_607_431".into() {
return Err(Error::Message("bad amount".to_string()));
}
Ok(())
}
fn calc_swap_result(
amount_in: Integer,
bal_in: Integer,
bal_out: Integer,
fee_bps: Integer,
) -> Result {
validate_amount(amount_in)?;
validate_amount(bal_in)?;
validate_amount(bal_out)?;
// input amount less fee, round down
let bps_in_100pct = 10000.into();
let in_less_fee = amount_in * (bps_in_100pct - fee_bps) / bps_in_100pct;
let new_bal_in = bal_in + in_less_fee;
validate_amount(new_bal_in)?;
// calculate output amount from delta in output-token balance, round down
let k = bal_in * bal_out;
Ok((bal_out * new_bal_in - k) / new_bal_in)
}
fn quote_swap(
ctx: &impl ReadContext,
token_in: &ContractAddress,
amount_in: Integer,
) -> Result {
let custodian = ctx.model().custodian();
let bal_in = token_dyn::balance(token_in, &custodian).unwrap_or_default();
let bal_out = token_dyn::balance(&token_out(ctx, token_in)?, &custodian).unwrap_or_default();
calc_swap_result(amount_in, bal_in, bal_out, ctx.model().fee_bps())
}
fn quote_deposit(
ctx: &impl ReadContext,
amount_a: Integer,
amount_b: Integer,
) -> Result {
validate_amount(amount_a)?;
validate_amount(amount_b)?;
let token_a = ctx.model().token_a();
let token_b = ctx.model().token_b();
let lp_supply = ctx.model().lp_total_supply();
let custodian = ctx.model().custodian();
let bal_a = token_dyn::balance(&token_a, &custodian).unwrap_or_default();
let bal_b = token_dyn::balance(&token_b, &custodian).unwrap_or_default();
let lp_shares = if amount_a * bal_b < amount_b * bal_a {
amount_a * lp_supply / bal_a
} else {
amount_b * lp_supply / bal_b
};
let supply_minus_one = lp_supply - 1.into();
Ok(DepositResult {
deposit_a: (lp_shares * bal_a + supply_minus_one) / lp_supply, // round up
deposit_b: (lp_shares * bal_b + supply_minus_one) / lp_supply, // round up
lp_shares,
})
}
fn quote_withdraw(ctx: &impl ReadContext, shares: Integer) -> Result {
validate_amount(shares)?;
let token_a = ctx.model().token_a();
let token_b = ctx.model().token_b();
let lp_supply = ctx.model().lp_total_supply();
let custodian = ctx.model().custodian();
let bal_a = token_dyn::balance(&token_a, &custodian).unwrap_or_default();
let bal_b = token_dyn::balance(&token_b, &custodian).unwrap_or_default();
Ok(WithdrawResult {
amount_a: shares * bal_a / lp_supply,
amount_b: shares * bal_b / lp_supply,
})
}
impl Guest for Pool {
// Dummy implementation for testing purposes.
fn init(ctx: &ProcContext) {
PoolStorage {
token_a: ContractAddress {
name: "".to_string(),
height: 0,
tx_index: 0,
},
token_b: ContractAddress {
name: "".to_string(),
height: 0,
tx_index: 0,
},
lp_ledger: Map::default(),
lp_total_supply: 0.into(),
fee_bps: 0.into(),
custodian: "".to_string(),
}
.init(ctx);
}
// This represents the production init function.
// Only for local testing purposes.
fn re_init(
ctx: &ProcContext,
token_a: ContractAddress,
amount_a: Integer,
token_b: ContractAddress,
amount_b: Integer,
fee: Integer,
) -> Result {
PoolStorage::new(ctx, token_a, amount_a, token_b, amount_b, fee)?.init(ctx);
Ok(ctx.model().lp_total_supply())
}
fn fee(ctx: &ViewContext) -> Integer {
ctx.model().fee_bps()
}
fn balance(ctx: &ViewContext, acc: String) -> Option {
ctx.model().lp_ledger().get(acc)
}
fn transfer(ctx: &ProcContext, to: String, n: Integer) -> Result<(), Error> {
let from = ctx.signer().to_string();
let ledger = ctx.model().lp_ledger();
let from_balance = ledger.get(&from).unwrap_or_default();
let to_balance = ledger.get(&to).unwrap_or_default();
if from_balance < n {
return Err(Error::Message("insufficient funds".to_string()));
}
ledger.set(from, from_balance - n);
ledger.set(to, to_balance + n);
Ok(())
}
fn token_balance(ctx: &ViewContext, token: ContractAddress) -> Result {
token_out(ctx, &token)?;
Ok(token_dyn::balance(&token, &ctx.model().custodian()).unwrap_or_default())
}
fn quote_deposit(
ctx: &ViewContext,
amount_a: Integer,
amount_b: Integer,
) -> Result {
quote_deposit(ctx, amount_a, amount_b)
}
fn deposit(
ctx: &ProcContext,
amount_a: Integer,
amount_b: Integer,
) -> Result {
let res = quote_deposit(ctx, amount_a, amount_b)?;
let ledger = ctx.model().lp_ledger();
let custodian = ctx.model().custodian();
let user = ctx.signer().to_string();
let bal = ledger.get(&user).unwrap_or_default();
ledger.set(user, bal + res.lp_shares);
ctx.model().set_lp_total_supply(ctx.model().lp_total_supply() + res.lp_shares);
token_dyn::transfer(
&ctx.model().token_a(),
ctx.signer(),
&custodian,
res.deposit_a,
)?;
token_dyn::transfer(
&ctx.model().token_b(),
ctx.signer(),
&custodian,
res.deposit_b,
)?;
Ok(res)
}
fn quote_withdraw(ctx: &ViewContext, shares: Integer) -> Result {
quote_withdraw(ctx, shares)
}
fn withdraw(ctx: &ProcContext, shares: Integer) -> Result {
let res = quote_withdraw(ctx, shares)?;
let ledger = ctx.model().lp_ledger();
let user = ctx.signer().to_string();
let total = ctx.model().lp_total_supply();
let bal = ledger.get(&user).unwrap_or_default();
if total < shares {
return Err(Error::Message("insufficient total supply".to_string()));
}
if bal < shares {
return Err(Error::Message("insufficient share balance".to_string()));
}
ledger.set(user.clone(), bal - shares);
ctx.model().set_lp_total_supply(total - shares);
token_dyn::transfer(
&ctx.model().token_a(),
ctx.contract_signer(),
&user,
res.amount_a,
)?;
token_dyn::transfer(
&ctx.model().token_b(),
ctx.contract_signer(),
&user,
res.amount_b,
)?;
Ok(res)
}
fn quote_swap(
ctx: &ViewContext,
token_in: ContractAddress,
amount_in: Integer,
) -> Result {
quote_swap(ctx, &token_in, amount_in)
}
fn swap(
ctx: &ProcContext,
token_in: ContractAddress,
amount_in: Integer,
min_out: Integer,
) -> Result {
let token_out = token_out(ctx, &token_in)?;
let amount_out = quote_swap(ctx, &token_in, amount_in)?;
if amount_out < min_out {
return Err(Error::Message(format!(
"amount out ({}) below minimum",
amount_out
)));
}
token_dyn::transfer(
&token_in,
ctx.signer(),
&ctx.model().custodian(),
amount_in,
)?;
token_dyn::transfer(
&token_out,
ctx.contract_signer(),
&ctx.signer().to_string(),
amount_out,
)?;
Ok(amount_out)
}
}
```
## Testing
```rust theme={null}
#[cfg(test)]
mod tests {
use testlib::*;
interface!(name = "pool");
interface!(name = "token_a", path = "../../token/contract/wit");
interface!(name = "token_b", path = "../../token/contract/wit");
#[testlib::test(contracts_dir = "../../")]
async fn test_contract() -> Result<()> {
let admin = runtime.identity().await?;
let minter = runtime.identity().await?;
let pool = runtime.publish(&admin, "pool").await?;
let token_a = runtime.publish(&admin, "token").await?;
let token_b = runtime.publish(&admin, "token").await?;
token_a::mint(runtime, &token_a, &minter, 1000.into()).await??;
token_b::mint(runtime, &token_b, &minter, 1000.into()).await??;
token_a::transfer(runtime, &token_a, &minter, &admin, 100.into()).await??;
token_b::transfer(runtime, &token_b, &minter, &admin, 500.into()).await??;
let res = pool::re_init(
runtime,
&pool,
&admin,
token_a.clone(),
100.into(),
token_b.clone(),
500.into(),
0.into(),
)
.await?;
assert_eq!(res, Ok(223.into()));
let bal_a = pool::token_balance(runtime, &pool, token_a.clone()).await?;
assert_eq!(bal_a, Ok(100.into()));
let bal_b = pool::token_balance(runtime, &pool, token_b.clone()).await?;
assert_eq!(bal_b, Ok(500.into()));
let k1 = bal_a.unwrap() * bal_b.unwrap();
let res = pool::quote_swap(runtime, &pool, token_a.clone(), 10.into()).await?;
assert_eq!(res, Ok(45.into()));
let res = pool::quote_swap(runtime, &pool, token_a.clone(), 100.into()).await?;
assert_eq!(res, Ok(250.into()));
let res = pool::quote_swap(runtime, &pool, token_a.clone(), 1000.into()).await?;
assert_eq!(res, Ok(454.into()));
let res = pool::swap(runtime, &pool, &minter, token_a.clone(), 10.into(), 46.into()).await?;
assert!(res.is_err()); // below minimum
let res = pool::swap(runtime, &pool, &minter, token_a.clone(), 10.into(), 45.into()).await?;
assert_eq!(res, Ok(45.into()));
let bal_a = pool::token_balance(runtime, &pool, token_a.clone()).await?;
let bal_b = pool::token_balance(runtime, &pool, token_b.clone()).await?;
let k2 = bal_a.unwrap() * bal_b.unwrap();
assert!(k2 >= k1);
let res = pool::quote_swap(runtime, &pool, token_b.clone(), 45.into()).await?;
assert_eq!(res, Ok(9.into()));
let res = pool::swap(runtime, &pool, &minter, token_b.clone(), 45.into(), 0.into()).await?;
assert_eq!(res, Ok(9.into()));
let bal_a = pool::token_balance(runtime, &pool, token_a.clone()).await?;
let bal_b = pool::token_balance(runtime, &pool, token_b.clone()).await?;
let k3 = bal_a.unwrap() * bal_b.unwrap();
assert!(k3 >= k2);
// use token interface to transfer shares
let res = token::balance(runtime, &pool, &admin).await?;
assert_eq!(res, Some(223.into()));
let res = token::balance(runtime, &pool, &minter).await?;
assert_eq!(res, None);
token::transfer(runtime, &pool, &admin, &minter, 23.into()).await??;
let res = token::balance(runtime, &pool, &admin).await?;
assert_eq!(res, Some(200.into()));
let res = token::balance(runtime, &pool, &minter).await?;
assert_eq!(res, Some(23.into()));
Ok(())
}
}
```
# 5. Proxy
Source: https://docs.kontor.network/docs/sigil/examples/proxy
**Full working code**: The complete source code for this example is available in [`test-contracts/proxy/`](https://github.com/KontorProtocol/Kontor/tree/main/test-contracts/proxy)
This example demonstrates proxying (a common pattern used in contract upgrades), utilizing the `fallback` hook and introducing the `foreign::call` built-in and aliased imports.
## WIT Interface
The proxy contract exports functions for delegation and configuration:
```wit theme={null}
package root:component;
world root {
include kontor:built-in/built-in;
use kontor:built-in/context.{fall-context, proc-context, view-context, signer};
use kontor:built-in/foreign.{contract-address};
export fallback: func(ctx: borrow, expr: string) -> string;
export init: func(ctx: borrow);
export get-contract-address: func(ctx: borrow) -> option;
export set-contract-address: func(ctx: borrow, contract-address: contract-address);
}
```
## Rust Implementation
The proxy contract stores a target contract address and forwards all calls to it using the `fallback` hook:
```rust theme={null}
use stdlib::*;
contract!(name = "proxy");
#[derive(Clone, StorageRoot, Default)]
struct ProxyStorage {
contract_address: Option,
}
impl Guest for Proxy {
fn fallback(ctx: &FallContext, expr: String) -> String {
let ctx_ = &ctx.view_context();
if let Some(contract_address) = ctx_.model().contract_address() {
foreign::call(ctx.signer(), &contract_address, &expr)
} else {
"".to_string()
}
}
fn init(ctx: &ProcContext) {
ProxyStorage::default().init(ctx)
}
fn get_contract_address(ctx: &ViewContext) -> Option {
ctx.model().contract_address()
}
fn set_contract_address(ctx: &ProcContext, contract_address: ContractAddress) {
ctx.model().set_contract_address(Some(contract_address));
}
}
```
**Key points:**
* Storage accessed via `ctx.model()`
* `fallback` handles missing function calls by forwarding to the target contract
* Uses `if let Some(...)` to safely handle the `Option`
* `FallContext` can be converted to ViewContext for read-only access
* `foreign::call` is the low-level API used by `interface!` and `import!` macros
## Testing
The test demonstrates configuring the proxy and forwarding calls to a shared-account contract:
```rust theme={null}
#[cfg(test)]
mod tests {
use testlib::*;
interface!(name = "proxy");
interface!(name = "token", path = "../../token/contract/wit");
interface!(name = "shared-account", path = "../../shared-account/contract/wit");
#[testlib::test(contracts_dir = "../../")]
async fn test_shared_account_contract() -> Result<()> {
let alice = runtime.identity().await?;
let bob = runtime.identity().await?;
let claire = runtime.identity().await?;
let dara = runtime.identity().await?;
let proxy = runtime.publish(&alice, "proxy").await?;
let token = runtime.publish(&alice, "token").await?;
let shared_account = runtime.publish(&alice, "shared-account").await?;
// Configure proxy to forward to shared-account
proxy::set_contract_address(runtime, &proxy, &alice, shared_account.clone()).await?;
let result = proxy::get_contract_address(runtime, &proxy).await?;
assert_eq!(result, Some(shared_account.clone()));
// Use token contract
token::mint(runtime, &token, &alice, 100.into()).await??;
// Call shared-account THROUGH the proxy using the fallback function
let account_id = shared_account::open(
runtime,
&proxy,
&alice,
token.clone(),
50.into(),
vec![&bob, &dara],
)
.await??;
let result = shared_account::balance(runtime, &proxy, &account_id).await?;
assert_eq!(result, Some(50.into()));
// Further deposits and withdrawals through proxy
shared_account::deposit(runtime, &proxy, &alice, token.clone(), &account_id, 25.into()).await??;
let result = shared_account::balance(runtime, &proxy, &account_id).await?;
assert_eq!(result, Some(75.into()));
// Test withdrawals
shared_account::withdraw(runtime, &proxy, &bob, token.clone(), &account_id, 25.into()).await??;
let result = shared_account::balance(runtime, &proxy, &account_id).await?;
assert_eq!(result, Some(50.into()));
// Test error handling
let result = shared_account::withdraw(runtime, &proxy, &claire, token.clone(), &account_id, 1.into()).await?;
assert_eq!(result, Err(Error::Message("unauthorized".to_string())));
Ok(())
}
}
```
**Key points:**
* Uses `#[testlib::test]` with auto-injected `runtime`
* Publishes all three contracts (proxy, token, shared-account)
* Configures proxy to point to shared-account
* Calls shared-account functions through the proxy address
* The proxy's `fallback` function automatically forwards calls
# 3. Shared Account
Source: https://docs.kontor.network/docs/sigil/examples/shared-account
**Full working code**: The complete source code for this example is available in [`test-contracts/shared-account/`](https://github.com/KontorProtocol/Kontor/tree/main/test-contracts/shared-account)
This example demonstrates a simple multi-tenant shared account introducing static contract imports, complex storage structs, authorization logic, ID generation, and cross-contract calls.
## WIT Interface
Exports functions for multi-tenant account management with token deposits and withdrawals:
```wit theme={null}
package root:component;
world root {
include kontor:built-in/built-in;
use kontor:built-in/context.{view-context, proc-context, signer};
use kontor:built-in/error.{error};
use kontor:built-in/foreign.{contract-address};
use kontor:built-in/numbers.{integer, decimal};
export init: func(ctx: borrow);
export open: func(ctx: borrow, token: contract-address, n: integer, other-tenants: list) -> result;
export deposit: func(ctx: borrow, token: contract-address, account-id: string, n: integer) -> result<_, error>;
export withdraw: func(ctx: borrow, token: contract-address, account-id: string, n: integer) -> result<_, error>;
export balance: func(ctx: borrow, account-id: string) -> option;
export token-balance: func(ctx: borrow, token: contract-address, holder: string) -> option;
export tenants: func(ctx: borrow, account-id: string) -> option>;
}
```
## Rust Implementation
* The `interface!` macro generates an interface for cross-contract calls to the `token` contract with a dynamic address (passed as a parameter). Use `import!` when the dependency contract has been previously published and the contract address is known at compile time.
* The `StorageRoot` macro, used for the root storage type, and the `Storage` macro, used for nested storage types in the `Account` struct, enable persistent storage.
* `other_tenants` uses `Map` because the storage layer does not currently support list types, and `Map` provides a limited interface with the `keys` method for iteration. Even with a `List` type using a `Map` here could make sense. Instead of `bool` an `enum` or `struct` that defines the their "role" in the account could be written.
* `authorized` Verifies procedure permissions
* `open`: Verifies token balance, generates an ID using `crypto::generate_id()`, sets the account, and transfers tokens to `ctx.contract_signer()`
* `deposit`/`withdraw`: Authorize the caller, verify balances, update storage, and call the token contract following CEI pattern
* `balance`/`tenants`: Query the storage for account details
```rust theme={null}
use stdlib::*;
contract!(name = "shared-account");
interface!(name = "token", path = "../../token/contract/wit");
#[derive(Clone, Default, Storage)]
struct Account {
pub other_tenants: Map,
pub balance: Integer,
pub owner: String,
}
#[derive(Clone, Default, StorageRoot)]
struct SharedAccountStorage {
pub accounts: Map,
}
fn authorized(signer: &Signer, account: &AccountModel) -> bool {
account.owner() == signer.to_string()
|| account
.other_tenants()
.get(signer.to_string())
.is_some_and(|b| b)
}
fn insufficient_balance_error() -> Error {
Error::Message("insufficient balance".to_string())
}
fn unauthorized_error() -> Error {
Error::Message("unauthorized".to_string())
}
fn unknown_error() -> Error {
Error::Message("unknown account".to_string())
}
impl Guest for SharedAccount {
fn init(ctx: &ProcContext) {
SharedAccountStorage::default().init(ctx);
}
fn open(
ctx: &ProcContext,
token: ContractAddress,
n: Integer,
other_tenants: Vec,
) -> Result {
let signer = ctx.signer();
let balance =
token::balance(&token, &signer.to_string()).ok_or(insufficient_balance_error())?;
if balance < n {
return Err(insufficient_balance_error());
}
let account_id = ctx.generate_id();
ctx.model().accounts().set(
account_id.clone(),
Account {
balance: n,
owner: ctx.signer().to_string(),
other_tenants: Map::new(
&other_tenants
.into_iter()
.map(|t| (t, true))
.collect::>(),
),
},
);
token::transfer(&token, signer, &ctx.contract_signer().to_string(), n)?;
Ok(account_id)
}
fn deposit(
ctx: &ProcContext,
token: ContractAddress,
account_id: String,
n: Integer,
) -> Result<(), Error> {
let signer = ctx.signer();
let balance =
token::balance(&token, &signer.to_string()).ok_or(insufficient_balance_error())?;
if balance < n {
return Err(insufficient_balance_error());
}
let account = ctx
.model()
.accounts()
.get(account_id)
.ok_or(unknown_error())?;
if !authorized(&signer, &account) {
return Err(unauthorized_error());
}
account.update_balance(|b| b + n);
token::transfer(&token, signer, &ctx.contract_signer().to_string(), n)
}
fn withdraw(
ctx: &ProcContext,
token: ContractAddress,
account_id: String,
n: Integer,
) -> Result<(), Error> {
let signer = ctx.signer();
let account = ctx
.model()
.accounts()
.get(account_id)
.ok_or(unknown_error())?;
if !authorized(&signer, &account) {
return Err(unauthorized_error());
}
let balance = account.balance();
if balance < n {
return Err(insufficient_balance_error());
}
account.set_balance(balance - n);
token::transfer(&token, ctx.contract_signer(), &signer.to_string(), n)
}
fn balance(ctx: &ViewContext, account_id: String) -> Option {
ctx.model().accounts().get(account_id).map(|a| a.balance())
}
fn token_balance(
_ctx: &ViewContext,
token: ContractAddress,
holder: String,
) -> Option {
token::balance(&token, &holder)
}
fn tenants(ctx: &ViewContext, account_id: String) -> Option> {
ctx.model().accounts().get(account_id).map(|a| {
[a.owner()]
.into_iter()
.chain(a.other_tenants().keys())
.collect()
})
}
}
```
**Key points:**
* Uses `interface!` not `import!` for dynamic token contract address
* `ctx.model()` for storage access
* Map operations: `accounts().get(id)` returns `Option`
* Nested struct updates: `account.update_balance(|b| b + n)`
* Error helpers return `Error::Message(...to_string())`
* `authorized()` function takes `&AccountModel` not `&AccountWrapper`
* `ctx.generate_id()` creates unique account IDs
* Calls `token::balance()` and `token::transfer()` with contract addresses
## Testing
The test demonstrates creating accounts, depositing, withdrawing, and testing authorization:
```rust theme={null}
#[cfg(test)]
mod tests {
use testlib::*;
interface!(name = "token", path = "../../token/contract/wit");
interface!(name = "shared-account");
#[testlib::test(contracts_dir = "../../")]
async fn test_shared_account_contract() -> Result<()> {
let alice = runtime.identity().await?;
let bob = runtime.identity().await?;
let claire = runtime.identity().await?;
let dara = runtime.identity().await?;
let token = runtime.publish(&alice, "token").await?;
let shared_account = runtime.publish(&alice, "shared-account").await?;
token::mint(runtime, &token, &alice, 100.into()).await??;
let account_id = shared_account::open(
runtime,
&shared_account,
&alice,
token.clone(),
50.into(),
vec![&bob, &dara],
)
.await??;
let result = shared_account::balance(runtime, &shared_account, &account_id).await?;
assert_eq!(result, Some(50.into()));
shared_account::deposit(runtime, &shared_account, &alice, token.clone(), &account_id, 25.into())
.await??;
let result = shared_account::balance(runtime, &shared_account, &account_id).await?;
assert_eq!(result, Some(75.into()));
shared_account::withdraw(runtime, &shared_account, &bob, token.clone(), &account_id, 25.into())
.await??;
let result = shared_account::balance(runtime, &shared_account, &account_id).await?;
assert_eq!(result, Some(50.into()));
shared_account::withdraw(runtime, &shared_account, &alice, token.clone(), &account_id, 50.into())
.await??;
let result = shared_account::balance(runtime, &shared_account, &account_id).await?;
assert_eq!(result, Some(0.into()));
let result = shared_account::withdraw(runtime, &shared_account, &bob, token.clone(), &account_id, 1.into())
.await?;
assert_eq!(
result,
Err(Error::Message("insufficient balance".to_string()))
);
let result = shared_account::withdraw(runtime, &shared_account, &claire, token.clone(), &account_id, 1.into())
.await?;
assert_eq!(result, Err(Error::Message("unauthorized".to_string())));
let result =
shared_account::token_balance(runtime, &shared_account, token.clone(), &alice).await?;
assert_eq!(result, Some(75.into()));
let result = token::balance(runtime, &token, &bob).await?;
assert_eq!(result, Some(25.into()));
let result = shared_account::tenants(runtime, &shared_account, &account_id)
.await?
.unwrap();
assert_eq!(result.iter().len(), 3);
assert!(result.contains(&alice.to_string()));
assert!(result.contains(&dara.to_string()));
assert!(result.contains(&bob.to_string()));
Ok(())
}
}
```
# 2. Token
Source: https://docs.kontor.network/docs/sigil/examples/token
**Full working code**: The complete source code for this example is available in [`test-contracts/test-token/`](https://github.com/KontorProtocol/Kontor/tree/main/test-contracts/test-token)
This example demonstrates persistent storage, user balances, transactions, and error handling for a fungible token.
## WIT Interface
Exports functions for a fungible token with minting, burning, transfers, and balance queries:
```wit theme={null}
package root:component;
world root {
include kontor:built-in/built-in;
use kontor:built-in/context.{core-context, view-context, proc-context, signer};
use kontor:built-in/error.{error};
use kontor:built-in/numbers.{integer};
record balance {
key: string,
value: integer,
}
export init: func(ctx: borrow);
export mint: func(ctx: borrow, n: integer) -> result<_, error>;
export burn: func(ctx: borrow, n: integer) -> result<_, error>;
export transfer: func(ctx: borrow, to: string, n: integer) -> result<_, error>;
export balance: func(ctx: borrow, acc: string) -> option;
export balances: func(ctx: borrow) -> list;
export total-supply: func(ctx: borrow) -> integer;
}
```
## Rust Implementation
The token contract uses a Map to store account balances, with helper functions for validation and a total supply tracker:
```rust theme={null}
use stdlib::*;
contract!(name = "token");
const BURNER: &str = "burn";
#[derive(Clone, Default, StorageRoot)]
struct TokenStorage {
pub ledger: Map,
pub total_supply: Integer,
}
fn assert_gt_zero(n: Integer) -> Result<(), Error> {
if n <= 0.into() {
return Err(Error::Message("Amount must be positive".to_string()));
}
Ok(())
}
impl Guest for Token {
fn init(ctx: &ProcContext) {
TokenStorage::default().init(ctx);
}
fn mint(ctx: &ProcContext, n: Integer) -> Result<(), Error> {
assert_gt_zero(n)?;
let to = ctx.signer().to_string();
let ledger = ctx.model().ledger();
let balance = ledger.get(&to).unwrap_or_default();
ledger.set(to, balance.add(n)?);
ctx.model().try_update_total_supply(|t| t.add(n))?;
Ok(())
}
fn burn(ctx: &ProcContext, n: Integer) -> Result<(), Error> {
Self::transfer(ctx, BURNER.to_string(), n)?;
ctx.model().try_update_total_supply(|t| t.sub(n))?;
Ok(())
}
fn transfer(ctx: &ProcContext, to: String, n: Integer) -> Result<(), Error> {
assert_gt_zero(n)?;
let from = ctx.signer().to_string();
let ledger = ctx.model().ledger();
let from_balance = ledger.get(&from).unwrap_or_default();
let to_balance = ledger.get(&to).unwrap_or_default();
if from_balance < n {
return Err(Error::Message("insufficient funds".to_string()));
}
ledger.set(from, from_balance.sub(n)?);
ledger.set(to, to_balance.add(n)?);
Ok(())
}
fn balance(ctx: &ViewContext, acc: String) -> Option {
ctx.model().ledger().get(acc)
}
fn balances(ctx: &ViewContext) -> Vec {
ctx.model()
.ledger()
.keys()
.filter_map(|k| {
if [BURNER.to_string()].contains(&k) {
None
} else {
Some(Balance {
value: ctx.model().ledger().get(&k).unwrap_or_default(),
key: k,
})
}
})
.collect()
}
fn total_supply(ctx: &ViewContext) -> Integer {
ctx.model().total_supply()
}
}
```
**Key points:**
* Storage accessed via `ctx.model()`, not a separate `storage()` function
* Map operations: `ledger.get(&key)` and `ledger.set(key, value)` - no extra ctx parameter
* Errors use `Error::Message(...to_string())` variant
* Uses checked arithmetic (`.add(n)?`, `.sub(n)?`) for overflow safety
* Helper validation function (`assert_gt_zero`)
* Total supply tracking with `try_update_*` closures
## Testing
The test demonstrates minting, transferring, and error handling with proper use of the double `??` operator for functions returning `Result`:
```rust theme={null}
#[cfg(test)]
mod tests {
use testlib::*;
interface!(name = "token");
#[testlib::test]
async fn test_contract() -> Result<()> {
let minter = runtime.identity().await?;
let holder = runtime.identity().await?;
let token = runtime.publish(&minter, "token").await?;
token::mint(runtime, &token, &minter, 900.into()).await??;
token::mint(runtime, &token, &minter, 100.into()).await??;
let result = token::balance(runtime, &token, &minter).await?;
assert_eq!(result, Some(1000.into()));
// Test insufficient funds error
let result = token::transfer(runtime, &token, &holder, &minter, 123.into()).await?;
assert_eq!(
result,
Err(Error::Message("insufficient funds".to_string()))
);
token::transfer(runtime, &token, &minter, &holder, 40.into()).await??;
token::transfer(runtime, &token, &minter, &holder, 2.into()).await??;
let result = token::balance(runtime, &token, &holder).await?;
assert_eq!(result, Some(42.into()));
let result = token::balance(runtime, &token, &minter).await?;
assert_eq!(result, Some(958.into()));
// Test non-existent account
let result = token::balance(runtime, &token, "foo").await?;
assert_eq!(result, None);
// Test balances list and total supply
let balances = token::balances(runtime, &token).await?;
assert_eq!(balances.len(), 2);
let total = balances
.iter()
.fold(Integer::from(0), |acc, x| acc + x.value);
assert_eq!(total, token::total_supply(runtime, &token).await?);
Ok(())
}
}
```
**Key points:**
* Uses `interface!` to generate bindings, not `import!`
* `#[testlib::test]` auto-injects `runtime` variable
* `runtime.identity()` creates test users
* `runtime.publish()` deploys the contract
* Functions returning `Result` use `??` to unwrap both the runtime Result and contract Result
* Integers created with `.into()` or `Integer::from()`
# Introduction
Source: https://docs.kontor.network/docs/sigil/introduction
**Sigil** is a **next-generation WebAssembly-based smart contract framework** that enables developers to develop, deploy, and run rich smart contract applications on Bitcoin with the Kontor metaprotocol. Sigil is designed to offer strong compile-time type safety, powerful language built-ins, an expressive storage interface, rich native types, native cross-contract calls and excellent overall ergonomics.
**Attention:** Sigil is currently in *early preview*. Planned features include:
* Typescript SDK for interacting with contracts client-side
* Support for list types in storage
* Native `Address` type
* Enhanced querying capabilities, including key prefix matching for `Map` types
## Why Sigil?
The primary goal of the Sigil framework is to make writing safe, secure smart contracts easy and natural. Unlike other smart contract frameworks, writing contracts in Sigil is designed to feel like writing normal software, requiring developers to learn a minimum number of additional concepts and to jump through as few hoops as possible to write, deploy and run their contracts on Bitcoin with Kontor.
Sigil notably leverages the WebAssembly Component Model to allow contracts to link natively and with full type-safety. This provides deep integration with conventional IDEs and popular Rust tooling. It shifts errors to build-time, keeps determinism and gas behavior predictable, and reduces the trusted computing base. Sigil thus dramatically improves developer ergonomics while fully supporting a flexible and extensible ecosystem built on open standards.
## WebAssembly Component Model
WebAssembly is a natural choice for blockchain smart contracts because it offers deterministic execution, efficient bytecode, and broad language support. However, most WASM-based blockchain platforms treat contracts as isolated modules with custom ABIs for host interaction—requiring developers to manually serialize data, manage dependencies, and coordinate interfaces through ad-hoc conventions.
Sigil takes a new approach by adopting the WebAssembly Component Model, a standardized system for composing WASM modules with typed interfaces. This means contracts can call each other with full type safety, interfaces are specified in a language-agnostic IDL (WIT), and integration errors are caught at compile time rather than discovered when transactions fail on-chain.
## Type Safety
In Sigil, contracts export functions with explicit signatures defined in WIT. If Contract A calls Contract B, Sigil generates a typed stub for B's interface that A imports at compile time. If B's interface changes—say, adding a field to a struct or modifying a function parameter—A will fail to compile until the change is addressed. This eliminates an entire class of bugs common in other smart contract systems, where interface mismatches cause runtime failures after deployment.
The same type checking applies to storage: Sigil's storage system uses a path-based hierarchy with Rust traits that ensure you can't accidentally store the wrong type at a given path or retrieve a value with an incorrect type annotation.
This type safety extends to contract standards: an interface like ERC-20 can be defined once in WIT. When you use the `import!` macro to reference a specific contract at a known address, the compiler verifies that contract implements the interface exactly—providing full compile-time safety. When using `interface!` to accept dynamic contract addresses, the compiler ensures your contract makes valid calls to the interface, but runtime errors can still occur if users provide addresses that don't implement the expected interface.
## Contract Structure and Execution
A Sigil contract defines an `init` function (called once when the contract is deployed), procedural functions that can read and write state, and view functions that are read-only. Functions receive a context object that provides access to the transaction signer, enables cross-contract calls, and exposes storage operations.
Storage is lazy-loaded—reading a value queries the database only for that specific path rather than deserializing an entire contract state object. This makes gas costs predictable and proportional to actual usage. Cross-contract calls are synchronous and type-checked; the callee executes immediately within the caller's transaction, and re-entrancy is prohibited to prevent common exploit patterns.
## Developer Experience
Sigil provides simple macros that generate the necessary boilerplate, allowing developers to focus on application logic rather than framework integration. The `contract!` macro sets up the module structure and WIT bindings. The `#[derive(Storage)]` macro generates typed accessors for contract state. Cross-contract calls use the `import!` macro to pull in another contract's interface and generate a local stub.
The result is code that looks like ordinary Rust: strongly typed, well-integrated with language tooling, and testable using standard Rust test frameworks. Because the runtime primitives are abstracted behind traits, the same contract code runs in unit tests (with an in-memory backend) and on-chain (with the indexer's SQLite backend) without modification.
# Getting Started
Source: https://docs.kontor.network/docs/sigil/quickstart
This guide walks you through setting up your development environment for Sigil smart contract development.
## Full Development Environment
If you're setting up a full Kontor development environment (including running the indexer and tests), see the [Kontor README](https://github.com/KontorProtocol/Kontor/blob/main/README.md) which covers:
* Installing all build dependencies (Bitcoin, ZMQ, etc.)
* Compiling Bitcoin Core for local testing
* Running the full test suite
* Docker deployment
## Contract Development Only
To develop and test Sigil contracts without running a local indexer:
### Install Dependencies
**MacOS:**
```bash theme={null}
brew install binaryen brotli
```
**Ubuntu:**
```bash theme={null}
sudo apt install binaryen brotli
```
### Set Up Rust
1. Install Rust from [rust-lang.org/tools/install](https://www.rust-lang.org/tools/install)
2. Add the WebAssembly target:
```bash theme={null}
rustup target add wasm32-unknown-unknown
```
3. Install additional tools:
```bash theme={null}
cargo install wasm-opt
```
## Working with Example Contracts
The [example contracts](https://github.com/KontorProtocol/Kontor/tree/main/test-contracts) in the Kontor repository require the Kontor indexer `core` libraries.
**The examples live in the same repository as the `core` libraries**, so the paths work out of the box:
```bash theme={null}
git clone https://github.com/KontorProtocol/Kontor.git
cd Kontor/test-contracts
```
**If you copy an example into your own workspace**, update the workspace dependencies in your root `Cargo.toml`:
```toml theme={null}
[workspace.dependencies]
testlib = { path = "/path/to/Kontor/core/testlib" }
stdlib = { path = "/path/to/Kontor/core/stdlib" }
```
Alternatively, create your own contract workspace from scratch following the [Hello World example](/docs/sigil/examples/hello-world), which shows the complete setup.
## Next Steps
After completing Hello World, explore more examples:
* [**Token**](/docs/sigil/examples/token) - Learn about storage, maps, and state management
* [**Shared Account**](/docs/sigil/examples/shared-account) - See cross-contract calls in action
* [**AMM**](/docs/sigil/examples/amm) - Build a full automated market maker with complex logic
* [**Pool**](/docs/sigil/examples/pool) - Understand liquidity pool patterns
For deeper understanding, see the [Reference documentation](/docs/sigil/reference/contract-structure).
# Contract Structure
Source: https://docs.kontor.network/docs/sigil/reference/contract-structure
A Sigil contract consists of five key components: imports, the contract declaration, interface imports (optional), storage definitions, and the Guest trait implementation containing your contract logic.
## Anatomy of a Contract
```rust theme={null}
// 1. Import the standard library
use stdlib::*;
// 2. Declare the contract
contract!(name = "token");
// 3. Import other contract interfaces (optional)
interface!(name = "other_contract", path = "other/wit");
// 4. Define storage structure(s)
#[derive(Clone, Default, StorageRoot)]
struct TokenStorage {
pub ledger: Map,
}
// 5. Implement the Guest trait (contract logic)
impl Guest for Token {
fn init(ctx: &ProcContext) {
TokenStorage::default().init(ctx);
}
fn mint(ctx: &ProcContext, n: Integer) {
let ledger = ctx.model().ledger();
let account = ctx.signer().to_string();
let balance = ledger.get(&account).unwrap_or_default();
ledger.set(account, balance + n);
}
fn balance(ctx: &ViewContext, acc: String) -> Option {
ctx.model().ledger().get(&acc)
}
}
```
## The contract! Macro
The `contract!` macro generates the necessary boilerplate for your contract:
```rust theme={null}
contract!(name = "my_contract");
```
**What it does:**
* Generates wit-bindgen glue code
* Creates the `Guest` trait you implement
* Sets up WASM component exports
* The name must match your WIT package
**Requirements:**
* Must appear after `use stdlib::*;`
* Name must match the contract name in your WIT file
## Context Types
Every function in a Sigil contract receives a context as its first parameter. There are three context types:
### ViewContext - Read-Only Queries
```rust theme={null}
fn balance(ctx: &ViewContext, acc: String) -> Option {
ctx.model().ledger().get(acc)
}
```
**Available methods:**
* `ctx.storage()` - Access low-level storage
* `ctx.model()` - Get generated storage model (read-only)
**Use for:** Functions that don't modify state, callable via API
### ProcContext - State-Modifying Transactions
```rust theme={null}
fn transfer(ctx: &ProcContext, to: String, n: Integer) -> Result<(), Error> {
let from = ctx.signer().to_string();
// ... modify storage
}
```
**Available methods:**
* `ctx.signer()` - Transaction signer
* `ctx.contract_signer()` - Contract's own address (for receiving tokens)
* `ctx.storage()` - Access storage
* `ctx.model()` - Get generated storage model (read-write)
* `ctx.generate_id()` - Generate unique IDs
* `ctx.view_context()` - Get read-only view
**Use for:** Functions that modify state, called via blockchain transactions
### FallContext - Fallback Handler
```rust theme={null}
fn fallback(ctx: &FallContext, expr: String) -> String {
// Proxy to another contract
if let Some(addr) = ctx.view_context().model().contract_address() {
foreign::call(ctx.signer(), &addr, &expr)
} else {
"".to_string()
}
}
```
**Available methods:**
* `ctx.signer()` - Returns `Option` (may be None for view calls)
* `ctx.proc_context()` - Returns `Option` (Some if called with signer)
* `ctx.view_context()` - Always available
**Use for:** Generic delegation and proxy patterns
## WIT Files
WIT (WebAssembly Interface Type) files define your contract's public interface.
### Relationship to Rust Code
* WIT files are **hand-written** by you
* They define the **public API** of your contract
* Every exported function in WIT must be implemented in Rust
* The `contract!` macro generates Rust types from your WIT
### Example WIT File
```wit theme={null}
package root:component;
world root {
// Include built-in types and interfaces
include kontor:built-in/built-in;
// Import specific types you'll use
use kontor:built-in/context.{view-context, proc-context};
use kontor:built-in/numbers.{integer};
use kontor:built-in/error.{error};
// Export functions (your contract's API)
export init: func(ctx: borrow);
export mint: func(ctx: borrow, n: integer);
export transfer: func(ctx: borrow, to: string, n: integer) -> result<_, error>;
export balance: func(ctx: borrow, acc: string) -> option;
}
```
### Built-in WIT Interfaces
The `kontor:built-in` package provides:
* **`context`** - Storage and execution context (ViewContext, ProcContext, FallContext)
* **`numbers`** - Arbitrary precision Integer and Decimal types
* **`error`** - Error type with variants (Message, Overflow, DivByZero, SyntaxError)
* **`crypto`** - Hash functions
* **`foreign`** - Cross-contract calls
See `core/indexer/src/runtime/wit/deps/built-in.wit` for complete reference.
## Module Organization
### Workspace Structure
Contracts are organized in a Cargo workspace:
```toml theme={null}
[workspace]
members = ["token", "amm", "pool"]
resolver = "2"
[workspace.dependencies]
wit-bindgen = "=0.47.0"
stdlib = { path = "../core/stdlib" }
```
### One Contract Per Crate
Each contract is a separate Rust crate with:
* `Cargo.toml` - Dependencies and build config
* `src/lib.rs` - Contract implementation
* `wit/contract.wit` - Interface definition
* `wit/deps/` - Symlink to built-in types
### Build Configuration
**Cargo.toml:**
```toml theme={null}
[lib]
crate-type = ["cdylib"] # Required for WASM
[dependencies]
wit-bindgen = "=0.47.0" # Must be exact version
stdlib = { path = "../core/stdlib" }
```
**.cargo/config.toml:**
```toml theme={null}
[build]
target = "wasm32-unknown-unknown"
[target.wasm32-unknown-unknown]
rustflags = ["-C", "target-feature=-simd128"]
```
## Hooks
Sigil defines specific function names that serve as hooks:
### init Hook
```rust theme={null}
fn init(ctx: &ProcContext) {
MyStorage::default().init(ctx);
}
```
**Called when:**
* Contract is first deployed (publish transaction)
* Automatically by the runtime
**Use for:**
* Setting initial storage values
* Contract initialization logic
* Data migrations for upgrades
### fallback Hook
```rust theme={null}
fn fallback(ctx: &FallContext, expr: String) -> String {
// Handle calls to non-existent functions
}
```
**Called when:**
* A function is called that doesn't exist
* Primarily for proxy contracts
**Use for:**
* Implementing proxy patterns
* Version upgrades
* Generic delegation
## Quick Reference
### Context Types
**ProcContext**
* Enables state-modifying operations, such as balance transfers
* Provides write access to storage and signer access via `ctx.signer()`
* Used in functions like `mint` or `transfer` to update blockchain state
**ViewContext**
* Supports read-only queries for inspecting contract state
* Restricts access to read-only storage operations, no signer or mutations allowed
* Used in functions like `balance` for retrieving data without modifying the blockchain
**FallContext**
* Manages unmatched calls via the `fallback` hook, enabling proxy patterns
* Converts to `ViewContext` or `Option` for storage reads
* Exclusive to the `fallback` function for dynamic routing
### Context Traits (for helper functions)
**WriteContext**
* For mutation logic (e.g., internal updates)
* Implemented only by `ProcContext`
**ReadContext**
* For read-only logic (e.g., internal queries)
* Implemented by both `ViewContext` and `ProcContext`, enabling shared read operations
### Storage
**StorageRoot**
* Marks the root struct or enum for contract storage
**Map\**
* Key-value store for collections (e.g., account balances)
* Supports `get`, `set`, and `keys` methods
**Storage Access**
* `ctx.model()` - Returns the typed storage model
* Field accessors (e.g., `ctx.model().ledger()`) - Provide structured access
* Get/set methods - No ctx parameter needed (e.g., `ledger.get(&key)`, `ledger.set(key, value)`)
### Signer Access
* `ctx.signer()` - Retrieves the transaction signer (ProcContext only)
* `ctx.contract_signer()` - Returns the contract's own address (for receiving tokens)
### Cross-Contract Calls
**Static imports:**
```rust theme={null}
import!(name = "token", height = 12345, tx_index = 0, path = "path/to/wit");
// Direct calls: token::transfer(...)
```
**Dynamic interfaces:**
```rust theme={null}
interface!(name = "token_dyn", path = "path/to/wit");
// Dynamic calls with address: token_dyn::transfer(&address, ...)
```
### Utilities
**crypto::generate\_id() -> String**
* Generates unique IDs for entities (e.g., account IDs)
**crypto::hash(String) -> (String, Vec\)**
* Applies sha256 hash and returns hex encoded string and raw bytes
**crypto::hash\_with\_salt(data: String, salt: String) -> (String, Vec\)**
* Applies sha256 hash to string concatenated with salt
### Error Type
```rust theme={null}
enum Error {
Message(String), // Custom error messages
Overflow(String), // Arithmetic overflow
DivByZero(String), // Division by zero
SyntaxError(String), // Syntax errors
}
```
### Macros
**contract!(name = "name")**
* Defines contract name and generates boilerplate code from WIT file
**import!(name, height, tx\_index, path)**
* Statically imports another contract's WIT for cross-contract calls
**interface!(name, path)**
* Defines dynamic interface for runtime contract calls with dynamic addresses
# Cross-Contract Calls
Source: https://docs.kontor.network/docs/sigil/reference/cross-contract-calls
Sigil contracts can call other contracts with full type safety. The framework provides two macros for importing contract interfaces: `interface!` for dynamic addresses and `import!` for fixed addresses.
## The interface! Macro
Import another contract's interface without specifying its address:
```rust theme={null}
interface!(name = "token", path = "token/wit");
```
**What it does:**
* Reads the WIT file from the specified path
* Generates type-safe Rust bindings
* Creates functions you can call from your contract
## The import! Macro
Import a specific contract instance at a known address:
```rust theme={null}
import!(
name = "token",
mod_name = "my_token",
height = 100,
tx_index = 0,
path = "token/wit"
);
```
**Parameters:**
* `name` - Contract name
* `mod_name` - Rust module name (avoid conflicts)
* `height` - Block height where contract was deployed
* `tx_index` - Transaction index in that block
* `path` - Path to WIT file
The macro generates functions similar to `interface!` but with the contract address fixed at compile time.
### When to Use Each
**Use `interface!` for dynamic addresses** (passed as parameters):
```rust theme={null}
fn deposit(ctx: &ProcContext, token: ContractAddress, amount: Integer) -> Result<(), Error> {
token::transfer(&token, ctx.signer(), &ctx.contract_signer().to_string(), amount)?;
// ...
}
```
**Use `import!` for fixed addresses** (known at compile time):
```rust theme={null}
// In tests or contracts with known dependencies
import!(
name = "arith",
mod_name = "arith_v1",
height = 1,
tx_index = 0,
path = "arith/wit"
);
fn calculate(ctx: &ProcContext, x: u64) -> u64 {
arith_v1::eval(ctx.signer(), x, arith_v1::Op::Id).value
}
```
## Standard Interfaces
The `interface!` macro enables **standards** similar to Ethereum's ERC-20: define an interface once, and contracts can work with any implementation of that interface.
### Defining a Token Standard
Extract common token functions into a standalone WIT file that any token can implement:
```wit theme={null}
// erc20-standard.wit
package erc20:standard;
interface erc20 {
use kontor:built-in/context.{view-context, proc-context};
use kontor:built-in/error.{error};
use kontor:built-in/numbers.{integer};
total-supply: func(ctx: borrow) -> integer;
balance-of: func(ctx: borrow, account: string) -> integer;
transfer: func(ctx: borrow, to: string, amount: integer) -> result<_, error>;
approve: func(ctx: borrow, spender: string, amount: integer) -> result<_, error>;
allowance: func(ctx: borrow, owner: string, spender: string) -> integer;
transfer-from: func(ctx: borrow, from: string, to: string, amount: integer) -> result<_, error>;
}
```
### Using Standards with Dynamic Addresses
Contracts accept any contract address that implements the standard interface:
```rust theme={null}
contract!(name = "dex");
// Import the standard interface
interface!(name = "erc20", path = "../erc20-standard.wit");
impl Guest for Dex {
fn swap(
ctx: &ProcContext,
token: ContractAddress, // User can pass any token
amount: Integer,
) -> Result {
// Compiler ensures this call is valid for the interface
// Runtime error if token doesn't actually implement erc20
erc20::transfer(&token, ctx.signer(), &ctx.contract_signer().to_string(), amount)?;
Ok(amount)
}
}
```
The [AMM example](/docs/sigil/examples/amm) demonstrates this pattern, accepting any token address and calling its transfer functions.
### How This Differs from Ethereum
**Ethereum's ERC-20:** A social convention where contracts implement functions named `transfer`, `balanceOf`, `approve`, etc. No compile-time or runtime verification that a contract actually implements the standard—you just call functions and hope they exist.
**Sigil's `interface!`:** The WIT interface provides strong typing in your contract code:
* The compiler ensures your contract makes valid calls to the interface
* The compiler ensures implementations export all required functions with correct signatures
* The runtime validates that the target contract matches the interface before executing calls
* Interface mismatches surface as clear runtime errors, not undefined behavior
## Making Cross-Contract Calls
### Basic Example
From the shared-account contract:
```rust theme={null}
use stdlib::*;
contract!(name = "shared-account");
// Import token interface dynamically
interface!(name = "token", path = "token/wit");
impl Guest for SharedAccount {
fn deposit(
ctx: &ProcContext,
token: ContractAddress, // Dynamic token address
account_id: String,
n: Integer,
) -> Result<(), Error> {
// Call token contract's transfer function
token::transfer(&token, ctx.signer(), &ctx.contract_signer().to_string(), n)?;
// Update local state
let account = ctx.model().accounts().get(account_id).ok_or(unknown_error())?;
account.update_balance(|b| b + n);
Ok(())
}
fn token_balance(
_ctx: &ViewContext,
token: ContractAddress,
holder: String,
) -> Option {
// Call token contract's balance function (view)
token::balance(&token, &holder)
}
}
```
## ContractAddress Type
```rust theme={null}
pub struct ContractAddress {
pub name: String,
pub height: s64,
pub tx_index: s64,
}
```
**Creating addresses:**
```rust theme={null}
let addr = ContractAddress {
name: "token".to_string(),
height: 100,
tx_index: 0,
};
```
## Call Context and Signers
### The Signer Parameter
When making cross-contract calls, you choose whose authority to use:
**Execute as the current user:**
```rust theme={null}
fn transfer_from_caller(ctx: &ProcContext, token: ContractAddress, to: String, n: Integer) -> Result<(), Error> {
// Transfer from caller's account
token::transfer(&token, ctx.signer(), &to, n)
}
```
**Execute as the contract itself:**
```rust theme={null}
fn transfer_from_contract(ctx: &ProcContext, token: ContractAddress, to: String, n: Integer) -> Result<(), Error> {
// Transfer from contract's own account
token::transfer(&token, ctx.contract_signer(), &to, n)
}
```
**When to use contract\_signer():**
* Contract holds tokens on behalf of users
* Contract acts as custodian
* Escrow patterns
## Error Propagation
Errors from cross-contract calls propagate automatically with `?`:
```rust theme={null}
fn fib_of_sub(
ctx: &ProcContext,
arith_address: ContractAddress,
x: String,
y: String,
) -> Result {
// If arith::checked_sub returns an error, it propagates
let n = arith::checked_sub(&arith_address, &x, &y)?;
Ok(Self::fib(ctx, arith_address, n))
}
```
**Cross-contract errors roll back all storage changes** in the entire call chain.
## Re-entrancy Protection
The runtime prevents re-entrancy automatically:
```
Contract A calls Contract B
Contract B tries to call Contract A
→ Error: "reentrancy prevented"
```
**Example:**
```rust theme={null}
// arith contract calls fib contract
// fib contract calls back to arith -> OK (different direction)
// But if arith calls fib, and fib calls back to arith:
// -> Runtime error: "reentrancy prevented"
```
**Why this matters:**
* Prevents common exploit patterns
* Simplifies reasoning about contract behavior
* No need for re-entrancy guards
## Multi-Contract Interaction Example
AMM (Automated Market Maker) calling Token contract:
```rust theme={null}
contract!(name = "amm");
interface!(name = "token_dyn", path = "token/wit");
fn create(
ctx: &ProcContext,
pair: TokenPair,
amount_a: Integer,
amount_b: Integer,
fee_bps: Integer,
) -> Result {
// Transfer tokens from user to AMM contract
let custodian = ctx.contract_signer().to_string();
token_dyn::transfer(&pair.a, ctx.signer(), &custodian, amount_a)?;
token_dyn::transfer(&pair.b, ctx.signer(), &custodian, amount_b)?;
// Create pool
let lp_shares = (amount_a * amount_b).sqrt()?;
// Store pool data in AMM's storage
// ...
Ok(lp_shares)
}
```
**Pattern:**
1. Call external contracts first (get tokens)
2. Validate received amounts
3. Update local state
4. Return results
## The foreign::call Low-Level API
For advanced use cases, you can make dynamic calls:
```rust theme={null}
use stdlib::*;
fn fallback(ctx: &FallContext, expr: String) -> String {
if let Some(addr) = ctx.view_context().model().contract_address() {
// Dynamic call to any contract
foreign::call(ctx.signer(), &addr, &expr)
} else {
"".to_string()
}
}
```
**Signature:**
```rust theme={null}
pub fn call(
signer: Option<&Signer>,
contract_address: &ContractAddress,
expr: &str
) -> String
```
**When to use:**
* Proxy contracts
* Generic delegation patterns
* When you need to construct calls dynamically
* Fallback handlers
**Most contracts should use `interface!` or `import!` instead** for type safety.
## Best Practices
**1. Use interface! for flexibility**
```rust theme={null}
// Good: Accept any token
fn deposit(ctx: &ProcContext, token: ContractAddress, n: Integer) -> Result<(), Error> {
token::transfer(&token, ctx.signer(), &ctx.contract_signer().to_string(), n)
}
```
**2. Validate before external calls (CEI pattern)**
```rust theme={null}
fn withdraw(ctx: &ProcContext, token: ContractAddress, n: Integer) -> Result<(), Error> {
// Check: Verify authorization and balance
let account = ctx.model().account();
if account.balance() < n {
return Err(insufficient_balance_error());
}
// Effect: Update local state first
account.set_balance(account.balance() - n);
// Interaction: Make external call last
token::transfer(&token, ctx.contract_signer(), &ctx.signer().to_string(), n)?;
Ok(())
}
```
**3. Handle cross-contract errors**
```rust theme={null}
// Propagate errors
token::transfer(&addr, signer, to, amount)?;
// Or handle explicitly
match token::transfer(&addr, signer, to, amount) {
Ok(()) => { /* success */ },
Err(e) => {
// Log or handle error
return Err(Error::Message(format!("Transfer failed: {}", e)));
}
}
```
**4. Document contract dependencies**
```rust theme={null}
// At top of contract
//! This contract requires a token contract implementing:
//! - transfer(to: string, n: integer) -> result<_, error>
//! - balance(acc: string) -> option
```
# Error Handling
Source: https://docs.kontor.network/docs/sigil/reference/error-handling
Sigil provides robust error handling through Rust's `Result` type combined with automatic storage rollback. Understanding when to use errors versus panics, and how rollback works across contract calls, is essential for writing correct contracts.
## The Error Type
```rust theme={null}
pub enum Error {
Message(String),
Overflow(String),
DivByZero(String),
SyntaxError(String),
}
```
Defined in `core/indexer/src/runtime/wit/deps/built-in.wit`.
## When to Use Result vs Panic
### Use Result for Expected Errors
**Expected failure cases:**
```rust theme={null}
fn transfer(ctx: &ProcContext, to: String, n: Integer) -> Result<(), Error> {
let from = ctx.signer().to_string();
let ledger = ctx.model().ledger();
let from_balance = ledger.get(&from).unwrap_or_default();
// Business logic error - use Result
if from_balance < n {
return Err(Error::Message("insufficient funds".to_string()));
}
ledger.set(from, from_balance - n);
let to_balance = ledger.get(&to).unwrap_or_default();
ledger.set(to, to_balance + n);
Ok(())
}
```
**Use cases:**
* Business logic errors (insufficient funds, unauthorized access)
* Invalid input validation
* Cross-contract call errors
* Any error the user might trigger through normal operation
### Use panic! for:
**Invariant violations:**
```rust theme={null}
fn internal_operation(ctx: &ProcContext) {
let value = ctx.model().critical_value();
// This should never be None if contract is correct
if value.is_none() {
panic!("Critical invariant violated: value must exist");
}
}
```
**Use cases:**
* Invariant violations (should never happen)
* Internal bugs
* Unrecoverable errors
**Important:** Panics roll back ALL storage changes across the entire call chain.
## Creating Custom Error Messages
### Basic Error Messages
```rust theme={null}
fn validate_pair(pair: &TokenPair) -> Result<(), Error> {
if pair.a.name.is_empty() || pair.b.name.is_empty() {
return Err(Error::Message(
"Token addresses must not be empty".to_string()
));
}
if pair.a.to_string() >= pair.b.to_string() {
return Err(Error::Message(
"Token pair must be ordered A < B".to_string()
));
}
Ok(())
}
```
### Reusable Error Constructors
```rust theme={null}
fn insufficient_balance_error() -> Error {
Error::Message("insufficient balance".to_string())
}
fn unauthorized_error() -> Error {
Error::Message("unauthorized".to_string())
}
fn unknown_error() -> Error {
Error::Message("unknown account".to_string())
}
// Usage
fn deposit(ctx: &ProcContext, account_id: String, n: Integer) -> Result<(), Error> {
let account = ctx
.model()
.accounts()
.get(account_id)
.ok_or(unknown_error())?;
if !authorized(ctx.signer(), &account) {
return Err(unauthorized_error());
}
// ...
}
```
## Overflow and Division Errors
### Checked Operations
Integer and Decimal types provide checked operations:
```rust theme={null}
impl Integer {
fn add(self, other: Integer) -> Result; // Overflow checked
fn sub(self, other: Integer) -> Result;
fn mul(self, other: Integer) -> Result;
fn div(self, other: Integer) -> Result; // Div by zero checked
fn sqrt(self) -> Result;
}
```
**Usage:**
```rust theme={null}
fn mint_checked(ctx: &ProcContext, n: Integer) -> Result<(), Error> {
let to = ctx.signer().to_string();
let ledger = ctx.model().ledger();
let balance = ledger.get(&to).unwrap_or_default();
// .add() returns Result
ledger.set(to, balance.add(n)?);
Ok(())
}
```
### Unchecked Operations
Standard operators panic on overflow:
```rust theme={null}
let result = a + b; // Panics on overflow
let result = a - b; // Panics on underflow
let result = a * b; // Panics on overflow
let result = a / b; // Panics on division by zero
```
**When to use unchecked:**
* When you've already validated the operation won't overflow
* When panic is acceptable (simple contracts, testing)
* When performance is critical (avoid double checks)
**When to use checked:**
* User-provided inputs
* Complex calculations
* When you want to return a specific error message
## Rollback Semantics
### What Rolls Back
When a function returns an `Err` or panics, **ALL storage modifications are rolled back**.
**Example:**
```rust theme={null}
fn outer(ctx: &ProcContext) -> Result<(), Error> {
ctx.model().set_count(1);
inner(ctx)?; // If this errors, set_count(1) is rolled back
ctx.model().set_count(3);
Ok(())
}
fn inner(ctx: &ProcContext) -> Result<(), Error> {
ctx.model().set_count(2);
Err(Error::Message("oops".to_string()))
// Both set_count(1) and set_count(2) are rolled back
}
```
### Cross-Contract Rollback
**Both errors and panics roll back the entire call chain:**
```rust theme={null}
fn outer(ctx: &ProcContext) -> Result<(), Error> {
ctx.model().set_count(1);
inner(ctx)?; // If this errors, set_count(1) is rolled back
ctx.model().set_count(3);
Ok(())
}
fn inner(ctx: &ProcContext) -> Result<(), Error> {
ctx.model().set_count(2);
Err(Error::Message("oops".to_string()))
// Both set_count(1) and set_count(2) are rolled back
}
```
**This applies across cross-contract calls:**
* If Contract A calls Contract B, and B returns an error
* ALL storage changes in both A and B are rolled back
* The entire transaction has no effect
### No Partial Commits
A transaction either:
* Completes fully with all storage changes persisted
* Fails entirely with no storage changes
This applies across all cross-contract calls in the transaction.
## Error Handling Best Practices
### 1. Validate Early
```rust theme={null}
fn create_pool(ctx: &ProcContext, pair: TokenPair, amount_a: Integer, amount_b: Integer) -> Result<(), Error> {
// Validate all inputs first
validate_pair(&pair)?;
validate_amount(amount_a)?;
validate_amount(amount_b)?;
// Then proceed with logic
// ...
}
```
### 2. Use Descriptive Error Messages
```rust theme={null}
// Bad
Err(Error::Message("error".to_string()))
// Good
Err(Error::Message("Pool for this pair already exists".to_string()))
```
### 3. Propagate Cross-Contract Errors
```rust theme={null}
fn deposit(ctx: &ProcContext, token: ContractAddress, n: Integer) -> Result<(), Error> {
// Propagate token transfer errors to caller
token::transfer(&token, ctx.signer(), &ctx.contract_signer().to_string(), n)?;
// Update local state
let account = ctx.model().account();
account.update_balance(|b| b + n);
Ok(())
}
```
### 4. Handle Option Types Carefully
```rust theme={null}
// Option 1: Use ok_or
let account = ctx.model().accounts().get(id).ok_or(unknown_error())?;
// Option 2: Pattern match
match ctx.model().accounts().get(id) {
Some(account) => { /* ... */ },
None => return Err(unknown_error()),
}
// Option 3: Use unwrap_or_default for safe defaults
let balance = ledger.get(&account).unwrap_or_default();
```
### 5. Follow Checks-Effects-Interactions (CEI) Pattern
```rust theme={null}
fn withdraw(ctx: &ProcContext, token: ContractAddress, amount: Integer) -> Result<(), Error> {
// CHECK: Verify conditions
let account = ctx.model().account();
if account.balance() < amount {
return Err(insufficient_balance_error());
}
// EFFECT: Update storage
account.set_balance(account.balance() - amount);
// INTERACTION: Make external calls
token::transfer(&token, ctx.contract_signer(), &ctx.signer().to_string(), amount)?;
Ok(())
}
```
**Why this matters:**
* External calls can fail
* State changes before the call are preserved (unless you propagate the error)
* CEI pattern makes reasoning easier
## Testing Errors
### Testing Expected Errors
```rust theme={null}
#[testlib::test]
async fn test_insufficient_funds() -> Result<()> {
let alice = runtime.identity().await?;
let bob = runtime.identity().await?;
let token = runtime.publish(&alice, "token").await?;
// Alice has no tokens, transfer should fail
let result = token::transfer(runtime, &token, &alice, &bob, 100.into()).await?;
assert_eq!(
result,
Err(Error::Message("insufficient funds".to_string()))
);
Ok(())
}
```
### Testing Error Messages
```rust theme={null}
let result = risky_operation().await;
assert!(result.is_err_and(|e|
e.root_cause().to_string().contains("expected substring")
));
```
# Numbers and Arithmetic
Source: https://docs.kontor.network/docs/sigil/reference/numbers
Sigil provides arbitrary precision number types—`Integer` and `Decimal`—for financial calculations that require exact arithmetic without overflow or rounding errors.
## Integer Type
The `Integer` type provides 256-bit signed arbitrary precision integers.
### Structure
```rust theme={null}
pub struct Integer {
r0: u64,
r1: u64,
r2: u64,
r3: u64,
sign: Sign, // Plus or Minus
}
```
**Maximum value:** 2^256 - 1 (roughly 115 quattuorvigintillion)
### Creating Integers
```rust theme={null}
// From literals
let a: Integer = 42.into();
let b: Integer = "1_000_000_000_000_000_000".into();
// From u64
let c = Integer::from(12345u64);
// Default is zero
let zero = Integer::default();
```
### Arithmetic Operations
**Unchecked operations** (panic on overflow):
```rust theme={null}
let sum = a + b; // Panics on overflow
let diff = a - b; // Panics on underflow
let product = a * b; // Panics on overflow
let quotient = a / b; // Panics on division by zero
let remainder = a % b;
```
**Checked operations** (return Result):
```rust theme={null}
let sum = a.add(b)?; // Returns Result
let diff = a.sub(b)?;
let product = a.mul(b)?;
let quotient = a.div(b)?; // Returns Error::DivByZero on zero
let sqrt = a.sqrt()?;
```
### Comparisons
```rust theme={null}
if a > b { }
if a == b { }
if a <= b { }
// Find maximum
let max = if a > b { a } else { b };
```
### Conversions
```rust theme={null}
// To string
let s = a.to_string();
// From string
let n: Integer = "12345".into();
// To Decimal
let d = Decimal::from(a);
```
### When to Use Integer
**Use Integer for:**
* Token balances
* Large financial calculations
* Vote counts
* Pool liquidity amounts
* Any value that might exceed u64 (18.4 quintillion)
* Exact arithmetic requirements
**Use u64 for:**
* Small counters
* Block heights
* Array indices
* When you know the value fits in 64 bits
## Decimal Type
The `Decimal` type provides arbitrary precision decimals for calculations requiring fractional values.
### Structure
```rust theme={null}
pub struct Decimal {
r0: u64,
r1: u64,
r2: u64,
r3: u64,
sign: Sign,
}
```
### Creating Decimals
```rust theme={null}
// From Integer
let i = Integer::from(100);
let d = Decimal::from(i);
// From u64
let d = Decimal::from(42);
```
### Operations
```rust theme={null}
// Logarithm (base 10)
let d = Decimal::from(100);
let log = d.log10()?; // Result: "2.0"
// Conversion to Integer (truncates)
let i = d.to_integer();
```
### When to Use Decimal
**Use Decimal for:**
* Price calculations
* Percentage calculations
* Logarithmic operations
* Scientific calculations
**Most contracts use Integer** for exact arithmetic and avoid decimals entirely.
## Example: Token Contract
```rust theme={null}
use stdlib::*;
contract!(name = "token");
#[derive(Clone, Default, StorageRoot)]
struct TokenStorage {
pub ledger: Map,
}
impl Guest for Token {
fn mint(ctx: &ProcContext, n: Integer) {
let to = ctx.signer().to_string();
let ledger = ctx.model().ledger();
let balance = ledger.get(&to).unwrap_or_default();
// Unchecked: simple addition
ledger.set(to, balance + n);
}
fn mint_checked(ctx: &ProcContext, n: Integer) -> Result<(), Error> {
let to = ctx.signer().to_string();
let ledger = ctx.model().ledger();
let balance = ledger.get(&to).unwrap_or_default();
// Checked: returns overflow error
ledger.set(to, balance.add(n)?);
Ok(())
}
fn transfer(ctx: &ProcContext, to: String, n: Integer) -> Result<(), Error> {
let from = ctx.signer().to_string();
let ledger = ctx.model().ledger();
let from_balance = ledger.get(&from).unwrap_or_default();
let to_balance = ledger.get(&to).unwrap_or_default();
if from_balance < n {
return Err(Error::Message("insufficient funds".to_string()));
}
// Unchecked: subtraction safe after validation
ledger.set(from, from_balance - n);
ledger.set(to, to_balance + n);
Ok(())
}
fn balance_log10(ctx: &ViewContext, acc: String) -> Result