Contract macro and tooling for Dusk Network WASM smart contracts.
The #[contract] macro eliminates boilerplate by automatically generating WASM exports, schemas, and data-driver implementations from a single annotated contract module.
Copy the contract-template directory and rename it:
cp -r contract-template my-contract
cd my-contractUpdate Cargo.toml:
- Replace
YOUR_CONTRACT_NAMEwith your contract name (e.g.,my-contract) - Add any additional dependencies your contract needs
Edit src/lib.rs:
#![no_std]
#![cfg(target_family = "wasm")]
extern crate alloc;
#[dusk_forge::contract]
mod my_contract {
use dusk_core::abi;
/// Contract state.
pub struct MyContract {
value: u64,
}
impl MyContract {
/// Creates a new contract instance.
pub const fn new() -> Self {
Self { value: 0 }
}
/// Returns the current value.
pub fn get_value(&self) -> u64 {
self.value
}
/// Sets the value.
pub fn set_value(&mut self, value: u64) {
self.value = value;
}
}
}The template includes a Makefile that handles building, optimization, and testing:
make wasm # Build optimized contract WASM
make test # Build and run tests
make expand # Show macro-expanded code (useful for debugging)
make help # Show all available targetsThe contract WASM will be at target/contract/wasm32-unknown-unknown/release/my_contract.wasm
Note: The template enables
overflow-checks = truein release builds. This is critical for contract security - never disable it.
The #[contract] macro expects:
| Element | Requirement |
|---|---|
| Module | Annotated with #[dusk_forge::contract] |
| Struct | Single public struct (the contract state) |
| Constructor | pub const fn new() -> Self |
| Methods | pub fn methods become contract functions |
impl MyContract {
// Public methods are exported as contract function
pub fn deposit(&mut self, amount: u64) { ... }
// Private methods are internal helper, they will NOT be exported
fn validate(&self) -> bool { ... }
}| Signature | Input Type | Output Type |
|---|---|---|
fn get(&self) -> u64 |
() |
u64 |
fn set(&mut self, v: u64) |
u64 |
() |
fn transfer(&mut self, to: Address, amount: u64) |
(Address, u64) |
() |
Multiple parameters are automatically tupled.
Emit events using abi::emit:
use dusk_core::abi;
pub fn transfer(&mut self, to: Address, amount: u64) {
// ... transfer logic ...
abi::emit("transfer", TransferEvent { from: self.owner, to, amount });
}Events are automatically detected and included in the contract schema.
Expose trait methods using the expose attribute:
#[contract(expose = [owner, transfer_ownership, renounce_ownership])]
impl Ownable for MyContract {
fn owner(&self) -> Address {
self.owner
}
fn owner_mut(&mut self) -> &mut Address {
&mut self.owner // NOT exposed (not in list)
}
// Empty body = use trait's default implementation
fn transfer_ownership(&mut self, new: Address) {}
// Empty body = use trait's default implementation
fn renounce_ownership(&mut self) {}
}- Only methods listed in
exposebecome contract functions - Empty method bodies signal the macro to use the trait's default implementation
- Methods with actual implementations use your code
For functions that stream data via abi::feed():
/// Streams all pending items to the host.
#[contract(feeds = "(ItemId, Item)")]
pub fn get_all_items(&self) {
for (id, item) in &self.items {
abi::feed((*id, item.clone()));
}
}The feeds attribute tells the data-driver what type to decode.
The data-driver is a separate WASM build that provides JSON encoding/decoding for external tools (wallets, explorers, etc.).
make wasm-dd # Build data-driver WASM
make expand-dd # Show macro-expanded data-driver code (useful for debugging)The data-driver WASM will be at target/data-driver/wasm32-unknown-unknown/release/my_contract.wasm
The data-driver WASM exports these functions:
| Export | Description |
|---|---|
init |
Initialize the driver (call once at startup) |
get_schema |
Returns the contract schema as JSON |
encode_input_fn |
Encodes JSON input for a contract function call |
decode_output_fn |
Decodes rkyv output to JSON |
decode_event |
Decodes rkyv event data to JSON |
For JavaScript integration, use w3sper which provides a high-level API for working with data-drivers.
For types requiring custom encoding/decoding:
#[contract]
mod my_contract {
// ... contract impl ...
/// Custom encoder for the "special_data" function.
#[contract(encode_input = "special_data")]
fn encode_special(json: &str) -> Result<alloc::vec::Vec<u8>, dusk_data_driver::Error> {
// Custom encoding logic
}
/// Custom decoder for the "special_data" function.
#[contract(decode_output = "special_data")]
fn decode_special(rkyv: &[u8]) -> Result<dusk_data_driver::JsonValue, dusk_data_driver::Error> {
// Custom decoding logic
}
}The macro generates a CONTRACT_SCHEMA constant with metadata:
// Access the schema
let schema_json = CONTRACT_SCHEMA.to_json();The schema includes:
- Contract name
- All public functions with their input/output types
- Doc comments
- Events with topics and data types
- Import paths for type resolution
Contracts have two build targets from the same source:
- Contract WASM - Runs on-chain in the Dusk VM
- Data-driver WASM - Runs off-chain for JSON encoding/decoding
All runtime dependencies go in the WASM-only section because contracts are gated by #![cfg(target_family = "wasm")]:
[target.'cfg(target_family = "wasm")'.dependencies]
dusk-core = "1.4"
dusk-data-driver = { version = "0.3", optional = true } # Only for data-driver
dusk-forge = "0.1"
[dev-dependencies]
dusk-core = "1.4" # Same types, but for host-side tests
dusk-vm = "0.1" # To run contract in tests[features]
# Contract WASM - uses custom allocator for on-chain execution
contract = ["dusk-core/abi-dlmalloc"]
# Data-driver WASM - enable serde for JSON serialization
data-driver = [
"dusk-core/serde",
"dep:dusk-data-driver",
"dusk-data-driver/wasm-export",
]
# Data-driver with memory exports for JavaScript
data-driver-js = ["data-driver", "dusk-data-driver/alloc"]The contract and data-driver features are mutually exclusive - never enable both at the same time. The Makefile handles this by explicitly selecting one feature per build target.
| Dependency Type | Where to Add | Feature Flags |
|---|---|---|
| Both builds | WASM-only section | None needed |
| Contract-only | WASM-only section with optional = true |
Add dep:name to contract feature |
| Data-driver-only | WASM-only section with optional = true |
Add dep:name to data-driver feature |
If a dependency has types used in function signatures, also add name/serde to the data-driver feature to enable JSON serialization.
Always enable overflow checks for contract safety:
[profile.release]
overflow-checks = trueThis prevents integer overflow vulnerabilities. The contract template includes this by default - never remove it.
The contract template includes a Makefile with the following targets:
| Target | Description |
|---|---|
make wasm |
Build optimized contract WASM |
make wasm-dd |
Build optimized data-driver WASM |
make all-wasm |
Build both contract and data-driver |
make test |
Build WASMs and run tests |
make clippy |
Run clippy with strict warnings |
make expand |
Show macro-expanded contract code |
make expand-dd |
Show macro-expanded data-driver code |
make clean |
Clean all build artifacts |
make help |
Show all targets and configuration |
Override Makefile variables as needed:
make wasm CONTRACT_FEATURE=contract # Custom contract feature name
make wasm WASM_OPT_LEVEL=-Os # Use -Os instead of -Oz
make wasm STACK_SIZE=131072 # 128KB stack instead of 64KB
make wasm-dd DD_FEATURE=data-driver # Use data-driver instead of data-driver-js- Rust nightly toolchain with
wasm32-unknown-unknowntarget jq(for parsing cargo metadata)wasm-opt(optional, for smaller binaries - install via binaryen)cargo-expand(optional, formake expand- install viacargo install cargo-expand)
dusk-forge/
├── src/lib.rs # Re-exports the contract macro
├── contract-macro/ # Proc-macro implementation
├── contract-template/ # Template for new contracts
├── tests/test-bridge/ # Integration tests
└── docs/
└── design.md # Detailed macro internals
# Run all tests
make test
# Run clippy
make clippy
# Show available commands
make helpThis Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. Please see the LICENSE for further details.
The included illustration is created by Regisha Dauven and is used with exclusive permission. Redistribution, modification, or reuse of the illustration by third parties is prohibited without explicit permission from the creator.
