Rust smart contracts aren’t magic: let’s turn on the counter
Three opening questions: problem, why Rust, what’s blocking you?
Problem to solve: You want business rules to run on-chain, self-serve, tamper-proof. A smart contract is the vending machine that enforces those rules without babysitting.
Why Rust? You need speed without memory footguns. On blockchains, every panic or unsafe read can burn real tokens, and Rust’s ownership system keeps those landmines out of your release build.
Where do beginners get stuck? Usually on day zero: Which tools? How do I build Wasm? Why does the framework yell at me? We’ll fix that with a minimal counter contract.
Quick analogy: the blockchain runtime is the property manager
Think of the runtime as the smart gate of your apartment complex. Your contract is the instruction card stored inside that gate box—who can enter, when the door opens, how fees are collected. The Wasm artifact is the standardized chip the property manager requests: compact, auditable, sandboxed. Writing a smart contract is just translating those lobby rules into Rust and flashing them onto that chip.
Five key pins to map the territory
- Rust contracts compile to the
wasm32-unknown-unknowntarget and run inside a sandbox defined by the chain. - Frameworks expose fixed entry points (
#[ink::contract]messages, CosmWasm’sinstantiate/execute/query, Anchor’s handlers). - State storage is mediated—use the provided APIs instead of poking raw memory.
- Calls are deterministic: same inputs, same outputs, otherwise consensus breaks.
- Gas/weight is scarce, so loops, serialization, and logging must be data-driven and predictable.
Framework cheat sheet: ink!, CosmWasm, Anchor
| Chain | Language | Framework traits | Pick it when… |
|---|---|---|---|
| Polkadot / Substrate | Rust | ink! with cargo-contract, tight Substrate integration | You’re shipping on Substrate-based chains |
| Cosmos ecosystem | Rust | CosmWasm modules with cw-* libraries | You’re targeting cross-chain DeFi / staking |
| Solana | Rust | Anchor macros, IDL-driven clients | You need high-throughput account choreography |
Prereqs before touching the keyboard
Bring solid Rust syntax (ownership, borrowing, pattern matching), Cargo workspace basics, and blockchain fundamentals (transactions, state, gas). If those aren’t second nature yet, run through Rustlings before copying code from this page.
Toolchain setup: do it in one go
Validated on macOS 14.6 (ARM), Rust 1.80.1 stable, cargo-contract 4.0.0. Run:
rustup default stable
rustup target add wasm32-unknown-unknown
cargo install cargo-contract --force
Rustup pulls the Wasm toolchain, Cargo installs the latest cargo-contract helper.
Also grab wasm-opt (Binaryen) for size trimming, cosmwasm-check when working in Cosmos land, and solana-cli if you dip into Anchor.
Hands-on: ink! counter in four steps
Step 1: scaffold the project
cargo contract new ink-counter
cd ink-counter
You now have a testable ink! skeleton.
Step 2: lock down Cargo.toml
[package]
name = "ink-counter"
version = "0.1.0"
edition = "2021"
[lib]
name = "ink_counter"
path = "lib.rs"
crate-type = ["cdylib", "rlib"]
[dependencies]
ink = { version = "5", default-features = false, features = ["std"] }
[dev-dependencies]
ink = { version = "5", default-features = false, features = ["std", "ink-as-dependency"] }
[features]
default = ["std"]
std = [
"ink/std",
]
This pins us to ink! 5 and enables std for local testing.
Step 3: write the contract
#![cfg_attr(not(feature = "std"), no_std)]
#[ink::contract]
mod ink_counter {
#[ink(storage)]
pub struct InkCounter {
value: u32,
}
impl InkCounter {
#[ink(constructor)]
pub fn new(init: u32) -> Self {
Self { value: init }
}
#[ink(message)]
pub fn increment(&mut self) {
self.value = self.value.saturating_add(1);
}
#[ink(message)]
pub fn get(&self) -> u32 {
self.value
}
}
#[cfg(test)]
mod tests {
use super::*;
#[ink::test]
fn counter_works() {
let mut contract = InkCounter::new(10);
contract.increment();
assert_eq!(contract.get(), 11);
}
}
}
Run cargo test and you’ll see test counter_works ... ok.
Step 4: build the Wasm bundle
cargo contract build --release
Artifacts land in target/ink/ (.wasm and .contract). The console prints build time and bundle size.
Failure mode and fix
Missing the Wasm target yields:
error: the target `wasm32-unknown-unknown` is not installed
Re-run rustup target add wasm32-unknown-unknown and rebuild.
Performance trade-offs: what you gain and lose with Wasm
Pros: compact modules, fast verification, deterministic execution across validators. Cons: trimmed standard library, no direct OS calls, and every host function is metered. After the first successful build, run wasm-opt -Oz target/ink/ink_counter.wasm to shave 5–15% off gas usage—just watch for longer compile times when squeezing aggressively.
Common pitfalls checklist
- Forgetting
#![cfg_attr(not(feature = "std"), no_std)]breaks the runtime loader. - Using
self.value += 1without guarding overflow—prefersaturating_addor return an error. - Skipping
#[ink::test]means you run plain Rust tests and miss storage simulation. - Deploying straight to mainnet without rehearsing
cargo contract upload --suri //Alice --executewastes fees. - Allowing unchecked dependency bumps: lock ink! in
Cargo.lockand communicate version upgrades.
Wrap-up and next steps
- Rust smart contracts are just rule chips for the runtime; Wasm and framework APIs are the core concepts.
- ink! hides the substrate plumbing; your job is to shape clear entry functions and state transitions.
- Once the counter runs, you can iterate on business logic with confidence.
Action plan:
- Expand tests with
cargo contract testto observe how the borrow checker protects state. - Add
setandresethandlers to explore more storage mutations. - Compare CosmWasm and Anchor starters—pick the chain that matches your roadmap for round two.