Skip to main content

Shared Boundary Rewrites and Rules

The compiler rewrites shared signatures to transport token values through module-owned wire wrappers. This page is about the boundary where a local module-owned value becomes a network value and then becomes module-owned again on the receiving side.

That boundary has two jobs. First, it prevents callers from manually constructing or inspecting token payloads that belong to another module. Second, in proof-enabled IC/Ref builds, sr9 calls carry proof material so the receiver can reject invalid or non-canister callers before token conversion.

<TokenName>Shared Module and Wire Format

To move token values across canisters, the defining module must provide a public <TokenName>Shared submodule with a private, shareable wire representation:

public module TokenShared {
private type Shared = { /* wire fields */ };
private func fromShared(s : Shared) : Token;
private func toShared(t : Token) : Shared;
};

The compiler rewrites shared signatures so every token position (including nested inside records, tuples, variants, options, and arrays) is replaced with the wire wrapper. It inserts fromShared on entry and toShared on return.

toShared should consume the value (burn, zero, or invalidate) so you do not return an asset while keeping a live alias inside the canister. The compiler does not automatically invent the burn; the conversion body and its contracts must establish the intended asset discipline. This is the same ownership concern described in mutable-record aliasing, applied to token values at a shared boundary.

This page is only about shared/network boundaries. Inside verified module code, opaque/token handles may be stored and routed as sealed identities through capability-summarized synchronous module APIs, including supported handle-bearing containers. Local-only opaque handles still do not appear in public actor/shared signatures; use shareable token wrappers for canister calls.

Stable upgrade storage is not a public shared/Candid boundary. Source code can declare stable token/opaque handles, including inside supported containers. Stable upgrade must not call toShared/fromShared; those hooks are only for shared-boundary transport and explicit import/export. For token, stable compatibility is tied to the defining token module CHASH; if the token module identity changes, that is an explicit migration to a new token identity, not an automatic stable upgrade. In a multi-actor protocol, upgrading one actor should therefore make changed token identities incompatible with actors that have not upgraded yet, unless those actors explicitly accept the new identity. For local opaque, stable compatibility is ordinary Motoko raw stable type compatibility; shape changes require an explicit owner-module migration.

Token Module Rules

These rules are required for shareable token types:

  • A module may define at most one token type.
  • If a module defines public type Name = token { ... }, it must also define a public module NameShared with:
    • private, non-polymorphic type Shared whose representation is shareable
    • private, non-async toShared(t : Name) : Shared
    • private, non-async fromShared(s : Shared) : Name
    • no other items (except a compiler-inserted proof helper)
  • toShared and fromShared are untrusted and must verify.
  • Any shared method that accepts or returns shareable token values must be marked sr9.

Proofs, Tags, and Call Safety

The compiler inserts a private proof helper inside <TokenName>Shared that enforces a round-trip property:

toShared(fromShared(s)) == s

If the implementations are wrong, verification fails.

Additional rules enforced by the compiler:

  • The compiler computes a module hash (CHASH) from the typed AST and tags the wire type as { __mod$<hash> : NameShared.Shared }.
  • In proof-enabled IC/Ref builds, verified shared calls wrap arguments and returns as { __proof$ : Blob; __data$ : <wire> } and validate proofs before use.
  • User code cannot define __mod$, __proof$, or __data$-prefixed fields or construct the wire wrapper manually.
  • from_candid, to_candid, and call_raw are disabled to prevent bypassing tag checks.
  • In proof-enabled builds, sr9 rejects non-canister principals (anon, self-auth, derived, reserved) before proof parsing.
  • The compiler injects controller-only __s9_set_proof and __s9_set_gate_key methods to install proof material at runtime.

These rules are enforced before verification. For example, a missing <TokenName>Shared module, a public Shared type, an async toShared, an extra member in <TokenName>Shared, a polymorphic token type, a second token type in the same module, or a token-bearing shared method without sr9 is rejected by the frontend.

sr9 is required for shared signatures that expose shareable token values, and it is also allowed without token values when you want the shared method to be a proof-enabled actor boundary. In that form it is useful for protocols where one Sector9 actor imports another actor and wants downstream calls to carry proof meaning. The caller still reasons from the callee's contracts and shared interface; the proof wrapper ties that reasoning to a checked canister participant at the message boundary.

Design Checklist

Before exposing a token value over a shared method, check each item:

  • The token owner module defines the token type and its minimal <TokenName>Shared module.
  • Shared is private, non-polymorphic, and shareable.
  • toShared and fromShared are non-async and verify their intended round trip.
  • The public shared method is marked sr9.
  • The method has explicit entry guards and postconditions for the protocol facts callers rely on.
  • Asset identity includes the relevant module hash when spoofing between modules would be dangerous.

Summary

  • The compiler rewrites shared signatures to wire types and inserts toShared and fromShared at the boundary.
  • <TokenName>Shared is required for every shareable token type and must be minimal and non-async.
  • sr9 methods enforce proof-based authentication in proof-enabled builds, and token wrappers use module-hash tagging for shared-boundary wire types.
  • Use Digital Asset System Overview for identity and proof flow, then Token Tutorials for concrete patterns.