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-unknown target and run inside a sandbox defined by the chain.
  • Frameworks expose fixed entry points (#[ink::contract] messages, CosmWasm’s instantiate/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

ChainLanguageFramework traitsPick it when…
Polkadot / SubstrateRustink! with cargo-contract, tight Substrate integrationYou’re shipping on Substrate-based chains
Cosmos ecosystemRustCosmWasm modules with cw-* librariesYou’re targeting cross-chain DeFi / staking
SolanaRustAnchor macros, IDL-driven clientsYou 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 += 1 without guarding overflow—prefer saturating_add or 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 --execute wastes fees.
  • Allowing unchecked dependency bumps: lock ink! in Cargo.lock and 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 test to observe how the borrow checker protects state.
  • Add set and reset handlers to explore more storage mutations.
  • Compare CosmWasm and Anchor starters—pick the chain that matches your roadmap for round two.