Documentation Index
Fetch the complete documentation index at: https://docs.kontor.network/llms.txt
Use this file to discover all available pages before exploring further.
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:
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:
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):
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):
// 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:
// 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<view-context>) -> integer;
balance-of: func(ctx: borrow<view-context>, account: string) -> integer;
transfer: func(ctx: borrow<proc-context>, to: string, amount: integer) -> result<_, error>;
approve: func(ctx: borrow<proc-context>, spender: string, amount: integer) -> result<_, error>;
allowance: func(ctx: borrow<view-context>, owner: string, spender: string) -> integer;
transfer-from: func(ctx: borrow<proc-context>, from: string, to: string, amount: integer) -> result<_, error>;
}
Using Standards with Dynamic Addresses
Contracts accept any contract address that implements the standard interface:
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<Integer, Error> {
// 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 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:
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<Integer> {
// Call token contract's balance function (view)
token::balance(&token, &holder)
}
}
ContractAddress Type
pub struct ContractAddress {
pub name: String,
pub height: s64,
pub tx_index: s64,
}
Creating addresses:
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:
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:
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 ?:
fn fib_of_sub(
ctx: &ProcContext,
arith_address: ContractAddress,
x: String,
y: String,
) -> Result<u64, Error> {
// 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:
// 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:
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<Integer, Error> {
// 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:
- Call external contracts first (get tokens)
- Validate received amounts
- Update local state
- Return results
The foreign::call Low-Level API
For advanced use cases, you can make dynamic calls:
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:
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
// 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)
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
// 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
// At top of contract
//! This contract requires a token contract implementing:
//! - transfer(to: string, n: integer) -> result<_, error>
//! - balance(acc: string) -> option<integer>