Get the code
The 12 example contracts live in the framework repository. Clone it:contract-libs/example-contracts/.
Before starting, make sure you can build and test a contract on your machine. See Build and test a contract.
Step 1: Root deployment, the smallest contract that exists
Folder:01-root-deployment/
The absolute minimum: a contract with zero state, zero methods, and exactly one lifecycle hook, #[deploy]. It does nothing interesting on-chain. That’s the point.
What this step teaches:
- Every Grid contract is just a Rust module annotated with
#[contract]. - Inside, you must have a
rootmodule that contains a single struct and animplblock with a#[deploy] async fn deploy() -> Result<Self>. That function is called exactly once when the contract is instantiated. - The macro transforms that declaration into all the glue code needed to make your contract addressable on-chain.
Step 2: Root counter, persistent state and access modifiers
Folder:02-root-counter/
Now the contract actually does something. The root component holds a counter: u64 and exposes three methods that each demonstrate one of the Grid’s access modifiers.
What this step teaches:
- Struct fields are automatically persisted. The runtime serializes the whole struct after every successful activation and re-hydrates it before the next one. You never touch storage APIs for simple state; you just write Rust.
#[view]marks a read-only method. Views take&self, cannot mutate, and can be called by anyone.#[public]marks a state-mutating method anyone can call. Takes&mut self.#[owner]marks a method only the entity’s owner can call. The runtime does the identity check for you, so your code is free of permission logic.
Step 3: Multi-component, two components talking to each other
Folder:03-multi-component/
A Grid contract can define more than one component type. The root is always there, but you can also declare non-root components that get installed onto entities after the fact. This step shows a minimal two-component contract: a counter root plus a resetter component that, when installed, is allowed to call back into the root.
What this step teaches:
- Multiple
pub modblocks inside#[contract], one per component type. #[installation_request]: the root’s gatekeeper method that decides whether a non-root component is allowed to be installed. Use it to enforce who may install which component type and under what conditions.#[install]: the initializer for non-root components. Same role as#[deploy]has for the root.#[private]: a method that can only be called by other components of the same contract, not by outside signers. Together with#[installation_request], this gives a capability-like design: private functions remain callable across components but are not exposed to external signers.- Remote calls with
.remote.method().await: how one component invokes another. Theawaitis genuine; remote calls are asynchronous because the target component may live on a different entity. - Self-references (
self_refs::...): macro-generated, compile-time-typed references to another component of the same contract instance. This is what makes cross-component calls type-safe.
Composing with already-deployed contracts (Steps 4, 6, 10, 11). These steps take aGvmContractdeploy parameter pointing at another contract you’ve already pushed and deployed. From the CLI, that argument has a specific nested-tuple JSON shape that you derive from the bech32m address returned bygen client deploy. See Deploy to DevNet: composing with another contract for the shape and a decoder.
Step 4: Cross-contract install, contracts composing with other contracts
Folder:04-cross-contract-install/
Step 3 showed two components of the same contract cooperating. Step 4 shows two different contracts cooperating: #[deploy] takes a GvmContract for a deployed Step 3 instance and installs its resetter there. The installing component is set as owner of the installed component.
What this step teaches:
- Contract composition follows naturally from the installer pattern learned in Step 3. The only difference is that the installer is triggered from another contract instead of from a client.
- Install ownership. The installing component is set as owner of the installed component, a useful mental model for access control when composing contracts.
Client::installer(): the auto-generated entrypoint for asking a foreign contract to install one of its component types on you.- A
GvmContractis a runtime handle. You pass it around, store it, hand it to other contracts, compare them for equality, and use it to install components. Step 4 passes one in via#[deploy]and calls installation.
Step 5: Custom errors, typed recoverable failure values
Folder:05-custom-errors/
Up to this point every method returned Result<T> but never actually returned an Err. Step 5 introduces typed errors: a counter with a configurable maximum that returns Err(CounterMaxError::MaxReached) once the limit is reached.
What this step teaches:
#[error_type]: the macro that turns anenuminto a Grid-aware error type. It derives Borsh,thiserror, and the glue needed to serialize the error across contract boundaries.- Errors are values, not panics. Returning
Err(...)does not roll back state on its own; the caller receives the error and decides what to do next. - Deploy parameters.
#[deploy]can take arguments the same way any other method can. Each deployment can use different values, so every instance is configured independently.
MaxReached error. You’ll use that very behavior as the setup for Step 6.
Step 6: Async error handling, reacting to another contract’s error
Folder:06-async-error-handling/
A contract that wraps Step 5’s counter and automatically resets it whenever the increment fails with MaxReached. You see two patterns: handle it inline after .await, or schedule a callback that runs in a later activation only if the call fails.
What this step teaches:
gvm_error.try_as::<ConcreteErrorType>(): how to downcast an opaque cross-contract error back into the specific typed error variant you care about, so you can branch on it.- Pattern A,
awaitthenmatch. Wait for the remote result in the same method..awaityields while the call runs; when it completes you branch onOk/Err. - Pattern B,
spawn+on_error. Schedule the remote call and return. If it fails, the runtime invokes your#[private]callback in a separate activation. - Putting it together. Both paths detect
MaxReachedand reset; A keeps everything in one call chain, B splits “fire” and “recover on failure” across activations.
Step 7: Events and context, talking to the outside world
Folder:07-events-and-context/
A counter that emits structured events on every mutation, validates its input, sets a gas budget, and reads the caller’s identity from within the method body.
What this step teaches:
#[event_type] struct SomeEvent { ... }: declarative events. The struct’s fields become the event payload, serialized with Borsh and made visible to indexers and off-chain subscribers..emit(): the only thing you need to call to actually emit an event instance. No manual indexing, no topic juggling.ensure!(cond, err): an ergonomicbail!-style helper for early input validation.#[gas(min, max)]: an attribute that declares the gas bracket a method is allowed to use. The runtime enforces the upper bound and rejects calls that can’t pay the lower one.Self::context(): returns the current activation’s metadata, includingCallerIdentity(an enum with variants likeSignerandComponent). This is how you know who actually triggered the call, useful for logging, access policies that go beyond#[owner], or recording authorship in events.
Step 8: Collections and storage, data structures and the raw storage API
Folder:08-collections-and-storage/
A per-player scoreboard backed by two complementary storage strategies: the main score map lives in a struct field, and detailed per-player audit logs live under manually managed storage keys.
What this step teaches:
IndexMap<K, V>as a struct field. The Grid-provided collection types work exactly like theirindexmapcounterparts but are Borsh-friendly, so you can keep non-trivial amounts of keyed data directly on your component struct.- Why you might not want to keep everything on the struct. Struct fields are loaded and re-saved in full on every call. For data that is large, rarely touched, or addressed by dynamic key, the manual storage API is the right tool.
Storage::save_by_key(key, value)/Storage::load_by_key(key)/Storage::delete_by_key(key): the manual path. You construct the key yourself (typically derived from user input), serialize whatever you want, and decide exactly when to read or write.StorageKey: a typed wrapper for raw bytes, with helpers to derive safe, namespaced keys.
Step 9: Entity creation and callbacks, factories and async results
Folder:09-entity-creation-and-callbacks/
A contract that, on demand, creates a brand-new on-chain entity, installs a worker component on it, and fires an async ping that resolves via a callback. Functionally: a factory.
What this step teaches:
Component::create_new_entity(): yes, contracts can create entities. Combined with installers, this is how you build factory contracts that spin up per-user inboxes, per-pool accounts, per-auction escrows, and so on.- The three callback shapes, seen together. Step 6 already showed
.on_error(...). This step adds:spawn(future).on_success(cb): the callback runs only if the future resolves toOk(...).spawn(future).on_result(cb): the callback runs on both success and error, and receives the fullCallbackResult.
- The mental model: you don’t
.awaitthe spawned call yourself. You register the continuation, return, and the runtime invokes your callback later with the call’s outcome as input, possibly in a different block.
Step 10: Fungible-token integration, calling a real production contract
Folder:10-fungible-token-integration/
A treasury contract that composes with the Grid’s genesis fungible-token contract. On deploy, the treasury installs its own holder component on the FT and stores the reference. From then on, the treasury is a real token account: it can transfer, burn, and check balances.
Getting an FT to compose with. The treasury’sWhat this step teaches:#[deploy]takes aGvmContractpointing at an existing FT instance. The recommended path is to push and deploy your own FT instance first using the genesis fungible-token manifest atcontract-libs/genesis-framework/fungible-token/artifacts/fungible_token_modules.json. See Deploy to DevNet for the push/deploy commands and the JSON shape for theft_contractargument.
- Real cross-contract calls against a non-trivial target. Unlike Step 4 (which installed a component from a contract we also wrote), here the counterparty is a real production contract shipped with the Grid’s genesis.
- Installing a foreign component on yourself via the auto-generated client (
FungibleTokenClient::installer().holder.install(...)). - Remote method chaining on that installed component (
client.holder.remote.transfer(...),.burn(...),.balance()), each returning a future you.await. - Why
self_refsmatter here. The FT’stransfermethod takes a typed holder reference, not a rawGvmComponentId. You pass the typed ref through and the compiler (and the runtime) stops you from handing it a holder that doesn’t belong to the right FT contract.
Step 11: Default routes, making one contract stand in for another
Folder:11-default-routes/
A proxy contract that, on deploy, points at an existing Step 5 counter and sets up a default route so that any method call made against the proxy’s entity silently executes on the target entity instead.
What this step teaches:
Component::set_default_route(target): the primitive. After this call, the proxy’s entity advertises “for this contract code, forward everything totarget”. Callers don’t have to know.- Why this is useful beyond toy examples. Default routes are the primitive behind upgradability (point the route at a new implementation), access control shims (gate the calls before forwarding), telemetry wrappers (count what passes through), and scoped proxies (different callers, different targets).
Component::remove_default_route(contract): the inverse, for when the proxy should stop shadowing.- The granularity: a route covers all methods of the targeted contract on the proxy’s entity. There is no per-method filtering at this layer; if you want that, layer it on top.
Step 12: Delegated access, temporarily borrowing owner authority
Folder:12-delegated-access/
A vault with a #[owner]-gated withdraw method, paired with a manager component that isn’t the vault’s owner but still needs to be able to withdraw. The manager makes the call with a delegated-owner grant attached, and the runtime treats the call as if it came from the vault’s actual owner, but only for this one call.
What this step teaches:
- Access in the Grid is activation-scoped, not session-scoped. A grant gives the recipient authority for a specific call, not for a lifetime. This is why delegated access feels more like a capability token than a role.
- The three explicit parts of a grant. Who receives it (origin), where it applies (scope, usually a specific component), what it grants (attributes, for example
Owner). - The implicit fourth part: the stamp. Before dispatching the outgoing call, the runtime attaches the current activation’s identity to the grant. That stamp is what makes the grant trustworthy. A component can only delegate authority that flows from its own execution; you cannot forge a grant that claims to be somebody else, because the runtime won’t validate it.
- Two equivalent APIs:
.with_delegated_owner(target_component_id): the high-level shorthand. One line: grant owner authority over one target for one call..with_delegated_access(container): the low-level manual builder. Use it when you need non-owner attributes, or when the grant needs a structurewith_delegated_ownerdoesn’t cover.
Delegation grants are an SDK feature, not a CLI one.gen client build-activationandsign-and-submit-activationhave no flag to attach a delegation grant. You can deploy Step 12 from the CLI and calldeposit/get_balance, but to exercise the actual delegated-withdraw flow that’s the lesson, drive it from Rust. Readexample-tests/src/lib.rs::step_12_delegated_accessfor a working reference.
Build and test the contracts
Every contract in the series has a corresponding test inexample-tests/, and the test story itself is worth understanding because it is how real clients talk to these contracts.
Step 1: Build all 12 contracts
The cloned repository ships only the manifests and ABIs under each step’sartifacts/. The compiled .o binaries are produced by the build and are required at test time. Build all 12 examples plus the genesis fungible-token contract that Step 10 depends on:
artifacts/ directory now contains the _modules.json manifest plus a .o binary per component.
Step 2: Run the tests
The tests live inexample-tests/src/lib.rs as plain pub async fn step_0N_xxx(ctx: &TestContext) helpers and are wrapped as #[tokio::test] #[rstest] in example-tests/tests/example_contracts_tests.rs. The wrappers carry #[ignore] so they don’t run under a plain cargo test (they need a live validator and would fail loudly without one); pass --ignored once you’re ready.
--ignored is a test-runner flag, not a Cargo flag, so it has to come after --. Each test also spawns its own validator subprocess on port 30001, which means they need to run serially or they’ll collide; use --test-threads=1:
#[ignore]; it’s purely a guard on these examples so they don’t fail in CI without a validator.
What each helper does
Eachstep_0N_xxx helper walks through a full client-side interaction with one step’s contract:
- Generate a client. The
#[generate_contract_client("…modules.json")]macro at the top of the file reads each contract’s ABI and generates a typed Rust module (push_contract,deploy_contract, and aRootstruct whose methods mirror the contract’s own). No hand-written bindings, no ABI-encoding boilerplate in the test body. - Push the binary.
push_contract(account, tokio::fs::read).execute(&gen_client, &signer)uploads the compiled RISC-V artifact and returns a content-addressed code id. - Deploy.
deploy_contract(account, code_id, ...deploy_args).execute(&gen_client, &signer)instantiates the code and runs its#[deploy]. The return value is a typed client you use for subsequent calls. - Call methods. Views look like
root.get_counter(account).execute(&gen_client).await; transactions look likeroot.increment().execute(&gen_client, &signer).await. Views take the caller’sGvmAccountexplicitly; transactions derive caller identity from the signer. - Assert. The same
Result<T>that your contract returns lands in the test. Success values are what methods returned; strongly-typed errors (like Step 5’sCounterMaxError) surface as errors the test can match against.
step_0N_xxx helper. It is, line for line, what an application would do.
What’s next
- Build and test a contract: the local build/test loop you’ll use to work through every step in this series.
- Deploy to DevNet: take any of these contracts from local-test-passing to running on DevNet, including the
GvmContractJSON shape for composing steps. - Genie SDK reference: the full surface area behind the macros you’ve been using.
- Grid framework reference: the fungible-token primitive that Step 10 composes with.
- Contract test tools reference: the test fixture (
TestContext, validator modes) the helpers above are built on.

