Skip to main content

contract

Attribute Macro contract 

Source
#[contract]
Expand description

Expose methods for cross-contract calls.

This macro may be applied to:

  • a trait
  • a trait implementation
  • an inherent implementation
  • a free function

Usage examples are in the top-level documentation.

§Interface vs implementation

The easiest mental model is:

  • Put #[contract] on a trait when you want to define a reusable interface.
  • Put #[contract] on a trait impl when you want to export an implementation of that interface.
  • Put #[contract] on an inherent impl when you want a self-contained contract with methods defined directly on the type.
  • Put #[contract] on a free function when you only want an exported entrypoint and do not need a generated client.
§Client objects

Generated clients default to the interface name plus Client:

  • #[contract(interface = "IERC20")] trait Token generates IERC20Client
  • #[contract] trait Foo generates FooClient
  • #[contract] impl Foo { ... } generates FooClient

Use client = CustomClient only when you want to override that default.

For trait impls, the trait is authoritative for the client name. Adding an explicit client = ... attribute to the impl is rejected to avoid double-generation.

The macro reserves new, with_gas_limit, with_commit_locking, with_read_only, and the try_ prefix for method/function identifiers on exported contract surfaces. #[contract] rejects trait methods, trait-impl methods, public inherent-impl methods, and free #[contract] fn names that use them to avoid collisions with generated client helpers and keep naming consistent across contract surfaces.

§Contract exports

The generated exports module is an internal implementation detail. The exports = "custom_name" attribute only overrides that internal module name when you need a disambiguating name during macro expansion; it is not a public API surface. Free-function contracts still support exports = ..., but they do not generate a client or <Interface>Entrypoints wrapper.

Generated entrypoint metadata lives on <Interface>Entrypoints:

  • trait definitions generate the wrapper alongside the client
  • bare impls generate the wrapper together with the exports module

For bare impls, the default interface name and domain separator are derived from the self type name.

§Events

Declare emitted event types with emits(MyEvent, OtherEvent) on a trait definition or inherent impl. This retains their definitions in the contract spec. For a trait impl, declare events on the trait instead. Runtime event logging alone does not retain an event’s spec.

§Errors

If the macro includes throws = ErrorType, the implementation may pass values of that type to throw. throws accepts a single type path:

ⓘ
#[contract(throws = errors::MyError)]

or a list of type paths:

ⓘ
#[contract(throws(errors::AlphaError, errors::BetaError))]

throws belongs to the interface:

  • use it on a trait definition
  • or on an inherent impl
  • not on a trait impl

throws declares the interface’s error contract: it is published in the contract spec and documents what callers should expect. It does not restrict what a caller can catch — exceptions are identified on the wire by a selector derived from the type, so a caller can match any exception type it knows about, declared or not. Two types sharing a last path segment (foo::Error and bar::Error) are fine.

Generated clients expose:

  • client.method(...) for the success-oriented path
  • client.try_method(...) -> Result<_, CallError> for inspecting exceptions, matched with match_exn!
§Storage

If a method takes self or &mut self, the SDK reconstructs the contract state from storage before calling the method.

If the receiver is &mut self, the updated state is written back only when the method completes successfully. Exceptions and faults revert the frame.

The state type should derive Storage (or use storage).

§Endpoint identifier customization

Each exported contract entrypoint is named with an identifier derived from:

  • the interface name
  • a version
  • the method name

interface = "InterfaceName" overrides the interface-name portion.

For bare impls, omitting interface = "..." uses the self type name for this metadata by default.

For trait impls, a plain #[contract] inherits the interface name, full version, and entrypoint IDs from the trait, including across crates and through trait aliases or re-exports. The implementing crate’s version does not affect that interface’s identity. There is no need to repeat interface or version on the impl.

version = "0.3.0" overrides the version on a trait definition, bare impl, or free function; otherwise those declarations use their crate’s own version. Entrypoint IDs use the version through its first nonzero component: 1.2.3 uses 1, 0.2.3 uses 0.2, and 0.0.3 uses 0.0.3. For 0.0.0, the entire version string is used, including any prerelease or build suffix.

version is not allowed on trait impls, even if it matches the trait. Remove the argument to inherit the trait’s version.

For compatibility, an explicit interface argument on a trait impl is accepted as an assertion that the name matches the trait. A mismatch is rejected at compile time; the trait remains authoritative. Turn a trait, impl block, or free function into contract entrypoints, generating exports, a typed client, and spec registration.

§State

Methods taking &self/&mut self load the contract’s state from storage: the self type must be a storage struct (annotate it with #[storage]; a unit struct like struct Token; works for contracts that keep no state in the struct). Contracts with only associated functions need no storage annotation.

§Method visibility (bare impl blocks)

In a bare (non-trait) impl block, pub methods become entrypoints and private methods are skipped (use them for helpers). Restricted visibility (pub(crate), pub(super), pub(in ...)) is a compile error because the intent is ambiguous. Trait impls export every trait method regardless of visibility.

§Arguments

version = "x.y.z", interface = "Name", exports = "mod_name", client = ClientIdent, throws = Error / throws(E1, E2), emits(Event1, Event2).

Trait implementations inherit the trait’s interface name, version, and entrypoint IDs, including when the trait comes from another crate or is imported under an alias. A plain #[contract] impl Trait for Contract is sufficient. version is not allowed on trait impls. An explicit interface argument is an optional assertion that the name matches the trait. For trait definitions, bare impls, and free functions, the version still defaults to the declaring crate’s CARGO_PKG_VERSION.

No throws(...) type may contain a thruput_sdk::signer::Signed: abort frames carry no auth-token handles, so such a payload could only decode to an uninitialized sentinel. Spec canonicalization rejects it when the contract is built. See #[contract_exception].

§Declared method effects

On trait methods and public methods of bare impls, use #[contract(effect = "pure")], #[contract(effect = "readOnly")], or #[contract(effect = "stateChanging")]. Omission leaves the effect absent, never read-only. Trait implementations inherit declarations from the trait; annotating an implementation method is rejected.

Effects cover the whole invocation, including nested calls. pure uses only arguments and constants, without state or execution-context reads. readOnly permits those reads but no application effects. stateChanging permits storage deltas, events, transfers and deployment; it need not change state on every call. Event emission is an application effect.

These are author declarations. The macro checks syntax and duplicates, not method behavior; neither the compiler nor runtime enforces the declared effects. Metadata does not change entrypoint IDs, execution paths, authorization, gas accounting, or whether a call can fail.

§Limitations

Generic parameters — including explicit lifetime parameters — are not supported on #[contract] items or their methods: generated clients, entrypoint wrappers, and spec consts are non-generic, so a spliced T or 'a cannot resolve there. Elided borrow lifetimes (&self, &[u8]) are fine, and #[derive(Event)] types may carry lifetime parameters (folded to 'static in the spec). Associated types are not supported in #[contract] traits. A struct must carry only one #[contract] impl block: depending on configuration a second block either collides on generated items (E0428 on names you never wrote) or compiles while silently merging the entrypoint surface — neither is supported.

§Interface id

A #[contract] trait gains an associated INTERFACE_ID: thruput_sdk::InterfaceId, derived from the effective interface name and major version (the pair that also namespaces its entrypoint IDs), and the generated <Iface>Entrypoints wrapper carries the same constant. The name is reserved on contract traits. Every #[contract] impl block also records its interface’s id in the contract’s interface table (thruput_sdk::interface_table), which is what thruput-erc165-if answers supports_interface from.

§Storage-domain collision detection

When a contract uses multiple storage structs, #[contract] generates a lib test that verifies their domain strings are all distinct. This check runs as a #[cfg(test)] lib test; a crate that sets [lib] test = false in its Cargo.toml, or that only runs an external test harness without compiling lib tests, will silently skip this collision check at build time.