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.
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:initHook: 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.fallbackHook: 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
viewContext:- Read-only access to storage
- Callable via the API exposed by Kontor nodes
fall(Fallback) Context:- Used exclusively for the
fallbackhook - Can be converted into other context types
- Used exclusively for the
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:- Signer Access: Available only in
proccontexts, allows access to the signer of the current transaction - Cross-Contract Calls: A function for calling other contracts, contributing to the gas expenditure of the transaction
- Storage Functions:
getandsetoperations for primitive types (e.g., strings, u64), parameterized by a key or path- Checking if a key exists in storage
- Regex-based path matching
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
getandsetoperations 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
Maptypes. 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
fallbackhook 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
Errortype, which can be returned from functions. - Error Propagation: Errors can be propagated up the call stack using the
?operator. - Error Handling:
Results can be handled usingmatchexpressions overOkandErrvariants or with the many other error handling strategies available in Rust.
Err or panics, 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 likedelegatecall 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)