Skip to main content

contract

Attribute Macro contract 

Source
#[contract]
Expand description

#[contract] macro to derive contract implementation.

This macro is supposed to be applied to an implementation of the struct that in turn implements IoType trait. IoType is most commonly obtained by deriving TrivialType (IoType is implemented for all types that implement TrivialType).

#[contract] macro will process public methods annotated with the following attributes:

  • #[init] - a method that can be called to produce an initial state of the contract, called once during contacts lifetime
  • #[update] - a method that can read and/or modify state and/or slots of the contact; might be called by user transaction directly or by another contract
  • #[view] - a method that can only read blockchain data, can read state or slots of the contract, but can’t modify their contents

Each argument (except self) of these methods has to be annotated with one of the following attributes (must be in this order):

  • #[env] - an environment variable, used to access ephemeral execution environment, call methods on other contracts, etc.
  • #[tmp] - a temporary ephemeral value to store auxiliary data while processing a transaction
  • #[slot] - a slot corresponding to this contract
  • #[input] - a method input coming from user transaction or invocation from another contract
  • #[output] - a method output serving as an alternative to returning values from a function directly, useful to reduce stack usage, its current contents provided by the caller can be read as well, which allows updating values in-place

Metadata of the state type and of #[tmp], #[slot], #[input], #[output] and return types must not be nested deeper than MAX_METADATA_DEPTH, which is checked during compilation.

Contents of #[input] arguments and initial contents of #[output] arguments come from the caller, so they are not trusted: before the method is called, generated code parses them (see IoType::try_from_ptr() and IoType::try_from_mut_ptr()) and returns ContractError::BadInput if they are not valid values of their types. The method only gets valid values, and for types that accept every bit pattern (see AnyBitPattern) there is nothing to check. Sizes, capacities and alignment of these arguments must match the method’s metadata, which the executor guarantees: the RISC-V VM checks them, while the native executor trusts callers and only catches mismatches with debug assertions. The state and #[tmp] and #[slot] arguments are storage of the contract, which is only written by the contract itself or on its behalf, so they are not parsed.

A method gets Env with a lifetime that covers its call. To lend an #[output] to calls with Env::lend_output(), the method must declare the output with that lifetime:

ⓘ
#[update]
pub fn forward<'a>(#[env] env: &mut Env<'a>, #[output] out: &'a mut Output) {
    let mut out = env.lend_output(out);
    // ...
}

A #[view] method does the same with &Env<'a>. With elided lifetimes compilation will fail with “lifetime may not live long enough”, and rustc will suggest the signature above. Lifetimes of the state, #[tmp] and #[slot] arguments, and of the last #[output] of #[init] without a return value, which is the initial state, can’t be named: they are trusted without being parsed, so they must not be lent.

§For struct implementation

§#[init]

Initializer’s purpose is to produce the initial state of the contract.

The initial state is the return value (Self or Result<Self, ContractError>) or, without a return value, the last #[output], which must be MaybeData<Self>.

This method type supports the following arguments (must be in this order):

  • #[env] read-only and read-write
  • #[tmp] read-only and read-write
  • #[slot] read-only and read-write
  • #[input]
  • #[output]

self argument is not supported in any way in this context since the state of the contract is just being created.

§#[update]

Generic method contract that can both update the contract’s own state and contents of slots.

This method type supports the following arguments (must be in this order):

  • &self or &mut self depending on whether state reads and/or modification are required
  • #[env] read-only and read-write
  • #[tmp] read-only and read-write
  • #[slot] read-only and read-write
  • #[input]
  • #[output]

§#[view]

Similar to #[update], but can only access read-only view of the state and slots, can be called outside of the block context and can only call other #[view] methods.

This method type supports the following arguments (must be in this order):

  • &self
  • #[env] read-only
  • #[slot] read-only
  • #[input]
  • #[output]

§For trait definition and trait implementation

§#[update]

Generic method contract that can (in case of trait indirectly) both update the contract’s own state and contents of slots.

This method type supports the following arguments in the trait context (must be in this order):

  • #[env] read-only and read-write
  • #[input]
  • #[output]

§#[view]

Similar to #[update], but can only access (in case of trait indirectly) read-only view of the state and slots, can be called outside of the block context and can only call other #[view] methods.

This method type supports the following arguments in the trait context (must be in this order):

  • #[env] read-only
  • #[input]
  • #[output]

§Generated code

This macro will produce several key outputs:

  • Contract trait implementation (for struct implementation)
  • FFI function for every method, which can be used by the host to call into the guest environment (for struct and trait implementation)
  • InternalArgs struct corresponding to each method, used as its sole input for FFI function
  • Struct implementing ExternalArgs trait for each method, usable by other contracts to call into this contract through the host, host will interpret it based on metadata and generate InternalArgs (for struct and trait implementation, trait definitions)
  • Extension trait (for struct and trait implementation) that simplifies interaction with host and removes the need to construct ExternalArgs manually, providing nice strongly typed methods instead, implemented for Env struct (for struct and trait implementation, trait definitions)
  • Metadata as defined in ContractMetadataKind stored in the ab-contract-metadata link section when compiled with guest feature enabled (for method, struct, and trait implementation)

§Contract trait implementation

Contract trait is required by a few other components in the system, so it is automatically implemented by the macro, see trait details.

§FFI function

Macro generates an FFI function with C ABI that looks like this:

ⓘ
#[cfg_attr(feature = "guest", unsafe(no_mangle))]
pub unsafe extern "C" fn {prefix}_{method}(
    args: &mut InternalArgs,
) -> ExitCode {
    // ...
}

Where {prefix} is derived from struct or trait name and {method} is the original method name from struct or trait implementation.

Example with struct implementation:

ⓘ
// This
#[contract]
impl Example {
    #[view]
    pub fn hello() {}
}

// Will generate this
#[cfg_attr(feature = "guest", unsafe(no_mangle))]
pub unsafe extern "C" fn example_hello(
    args: &mut InternalArgs,
) -> ExitCode {
    // ...
}

Example with trait implementation:

ⓘ
// This
#[contract]
impl Fungible for Token {
    #[view]
    pub fn balance(#[slot] address: &Address) -> Balance {}
}

// Will generate this
#[cfg_attr(feature = "guest", unsafe(no_mangle))]
pub unsafe extern "C" fn fungible_balance(
    args: &mut InternalArgs,
) -> ExitCode {
    // ...
}

These generated functions are public and available in generated submodules, but there should generally be no need to call them directly.

§InternalArgs struct

InternalArgs is generated for each method and is used as input to the above FFI functions. Its fields are generated based on function arguments, processing them in the same order as in function signature. It is possible for the host to build this data structure dynamically using available contact metadata.

Fields in the data structure are pointers, along with sizes and capacities where needed, some are read-only, some can be written to if changes need to be communicated back to the host. read-only here means that the host will not read the value, even if the contract modifies it.

§&self

&self is a read-only state of the contract and generates three fields, all of which are read-only:

ⓘ
#[repr(C)]
pub struct InternalArgs<'internal_args> {
    pub self_ptr: NonNull<<StructName as IoType>::PointerType>,
    pub self_size: u32,
    pub self_capacity: u32,
    // ...
}

This allows a contract to read the current state of the contract.

§&mut self

&mut self is a read-write state of the contract and generates three fields, self_ptr and self_size can be written to, while self_capacity is read-only:

ⓘ
#[repr(C)]
pub struct InternalArgs<'internal_args> {
    pub self_ptr: NonNull<<StructName as IoType>::PointerType>,
    pub self_size: u32,
    pub self_capacity: u32,
    // ...
}

This allows a contract to not only read, but also change the current state of the contract. self_capacity is defined by both the type used and the size of the value used (whichever is bigger in the case of variable-sized types) and corresponds to the amount of memory that host allocated for the guest behind self_ptr. In the case of variable-sized types, guest can replace self_ptr with a pointer to a guest-allocated region of memory that the host must read updated value from. This is helpful in case an increase of the value size beyond allocated capacity is needed.

§#[env] env: &Env

#[env] env: &Env is for accessing an ephemeral environment with method calls restricted to #[view]. Since this is a system-provided data structure with a known layout, only read-only field is generated:

ⓘ
#[repr(C)]
pub struct InternalArgs<'internal_args> {
    // ...
    pub env: &'internal_args Env<'internal_args>,
    // ...
}

§#[env] env: &mut Env

#[env] env: &mut Env is for accessing ephemeral environment without method calls restrictions. Since this is a system-provided data structure with a known layout, only read-write field is generated:

ⓘ
#[repr(C)]
pub struct InternalArgs<'internal_args> {
    // ...
    pub env: &'internal_args mut Env<'internal_args>,
    // ...
}

§#[tmp] tmp: &MaybeData<Tmp>

#[tmp] tmp: &MaybeData<Tmp> is for accessing ephemeral value with auxiliary data and generates three fields, all of which are read-only:

ⓘ
#[repr(C)]
pub struct InternalArgs<'internal_args> {
    // ...
    pub tmp_ptr: NonNull<
        <
            <StructName as Contract>::Tmp as IoType
        >::PointerType,
    >,
    pub tmp_size: u32,
    pub tmp_capacity: u32,
    // ...
}

This allows a contract to read the current ephemeral value of the contract.

§#[tmp] tmp: &mut MaybeData<Tmp>

#[tmp] tmp: &mut MaybeData<Tmp> is for accessing ephemeral value with auxiliary data and generates three fields, tmp_ptr and tmp_size can be written to, while tmp_capacity is read-only:

ⓘ
#[repr(C)]
pub struct InternalArgs<'internal_args> {
    // ...
    pub tmp_ptr: NonNull<
        <
            <StructName as Contract>::Tmp as IoType
        >::PointerType,
    >,
    pub tmp_size: u32,
    pub tmp_capacity: u32,
    // ...
}

This allows a contract to not only read, but also change the ephemeral value of the contract. tmp_capacity is defined by both the type used and the size of the value used (whichever is bigger in the case of variable-sized types) and corresponds to the amount of memory that host allocated for the guest behind tmp_ptr. In the case of variable-sized types, guest can replace tmp_ptr with a pointer to a guest-allocated region of memory that the host must read updated value from. This is helpful in case an increase of the value size beyond allocated capacity is needed.

§#[slot] slot: &MaybeData<Slot> and #[slot] (address, slot): (&Address, &MaybeData<Slot>)

#[slot] slot: &MaybeData<Slot> and its variant with explicit address argument is for accessing slot data (that corresponds to an optional address argument) and generates four fields, all of which are read-only:

ⓘ
#[repr(C)]
pub struct InternalArgs<'internal_args> {
    // ...
    pub slot_address: &'internal_args Address,
    pub slot_ptr: NonNull<
        <
            <StructName as Contract>::Slot as IoType
        >::PointerType,
    >,
    pub slot_size: u32,
    pub slot_capacity: u32,
    // ...
}

This allows a contract to read slot data.

§#[slot] slot: &mut MaybeData<Slot> and #[slot] (address, slot): (&Address, &mut MaybeData<Slot>)

#[slot] slot: &mut MaybeData<Slot> and its variant with explicit address argument are for accessing slot data (that corresponds to optional address argument) and generates four fields, slot_ptr and slot_size can be written to, while slot_address and slot_capacity are read-only:

ⓘ
#[repr(C)]
pub struct InternalArgs<'internal_args> {
    // ...
    pub slot_address: &'internal_args Address,
    pub slot_ptr: NonNull<
        <
            <StructName as Contract>::Slot as IoType
        >::PointerType,
    >,
    pub slot_size: u32,
    pub slot_capacity: u32,
    // ...
}

This allows a contract to not only read, but also change slot data. slot_capacity is defined by both the type used and the size of the value used (whichever is bigger in the case of variable-sized types) and corresponds to the amount of memory that host allocated for the guest behind slot_ptr. In the case of variable-sized types, guest can replace slot_ptr with a pointer to a guest-allocated region of memory that the host must read updated value from. This is helpful in case an increase of the value size beyond allocated capacity is needed.

Slot changes done by the method call will not be persisted if it returns an error or the host rejects its results (like an invalid slot size), including changes done by methods it called, even if those calls succeeded.

§#[input] input: &InputValue

#[input] input: &InputValue is a read-only input to the contract call and generates three fields, all of which are read-only:

ⓘ
#[repr(C)]
pub struct InternalArgs<'internal_args> {
    // ...
    pub input_ptr: NonNull<<InputValue as IoType>::PointerType>,
    pub input_size: u32,
    pub input_capacity: u32,
    // ...
}

The contents are parsed before the method is called, ContractError::BadInput is returned if they are not a valid InputValue.

§#[output] output: &mut OutputValue

#[output] output: &mut OutputValue is a read-write output of the contract call, where OutputValue is any type implementing IoType, like MaybeData<T>, VariableBytes<N> or a TrivialType. It generates three fields, output_size can be written to, while output_ptr and output_capacity are read-only (see the exception for #[init] below):

ⓘ
#[repr(C)]
pub struct InternalArgs<'internal_args> {
    // ...
    pub output_ptr: NonNull<<OutputValue as IoType>::PointerType>,
    pub output_size: u32,
    pub output_capacity: u32,
    // ...
}

The caller provides output_size bytes of current contents of the output (bytes beyond the size are not initialized), the contract can both read and modify them, and the modified value will be propagated back to the caller to observe. The current contents are parsed like those of #[input] before the method is called. The size of a TrivialType output is always the size of the type, while MaybeData<T> and VariableElements<T> (like VariableBytes<N>) can be empty when they are only used to return a value. Unlike an input, an empty MaybeData<T> output must still have capacity for T, since the contract can write a value into it. output_ptr pointer must not be changed as the host will not follow it to the new address, the output size is fully constrained by capacity specified in output_capacity. The only exception is the last #[output] of #[init] method without a return value, which is the contract’s initial state. In this case, its pointer can be changed to point to a different memory location, which is not limited by output_capacity allocation from the host, while the capacity the host provides must still be at least the size of the state, like that of other MaybeData<T> outputs. With a return value, the return value is the initial state, and all #[output] arguments are regular outputs.

#[output] may be used as an alternative to -> ReturnValue and -> Result<ReturnValue, ContractError> in case the data structure is large and allocation on the stack is undesirable, which is especially helpful in case of a variable-sized contract state.

§-> ReturnValue and -> Result<ReturnValue, ContractError>

Return values (wrapped in Result or not) are only possible with TrivialType types, hence they don’t need size and capacity as separate fields:

ⓘ
#[repr(C)]
pub struct InternalArgs<'internal_args> {
    // ...
    pub ok_result: &'internal_args mut MaybeUninit<ReturnValue>,
}

NOTE: In case ReturnValue in -> ReturnValue or -> Result<ReturnValue, ContractError> is (), it will be omitted in InternalArgs.

§ExternalArgs implementation

Macro generates a struct that implements ExternalArgs for each method that other contracts give to the host when they want to call into another (or even the same) contract.

Here is an example with struct implementation, but it works the same way with trait definition and implementation too:

ⓘ
// This
#[contract]
impl Example {
    #[view]
    pub fn hello() {}
}

#[repr(C)]
pub struct ExampleHelloArgs<'external_args> {
    // ...
}

#[automatically_derived]
unsafe impl ExternalArgs for ExampleHelloArgs {
    // ...
}

impl ExternalArgs<'external_args> {
    pub fn new(
        // ...
    ) -> Self {
        // ...
    }
}

Struct name if generated by concatenating struct or trait name on which name was generated, method name, and Args suffix, which is done to make it more convenient to use externally.

&self, &mut self, #[env] and #[tmp] arguments of the method are controlled fully by the host and not present in ExternalArgs.

ExternalArgs::new() method is generated for convenient construction of the instance, though in most cases Extension trait is used with a more convenient API. Fields are private and are only set by ExternalArgs::new(), since the host dereferences pointers in them according to the method signature. Each #[output] is given as &mut impl OutputArg<OutputValue> (see Extension trait for why), and its updated size can be read with an output_size() getter after the method call.

§#[slot]

Each #[slot] argument in ExternalArgs is represented by a single read-only address pointer:

ⓘ
#[repr(C)]
pub struct ExternalArgs<'external_args> {
    // ...
    slot_ptr: NonNull<Address>,
    // ...
}

§#[input]

Each #[input] argument in ExternalArgs is represented by three read-only fields, a pointer to data, and size + capacity:

ⓘ
#[repr(C)]
pub struct ExternalArgs<'external_args> {
    // ...
    input_ptr: NonNull<<InputValue as IoType>::PointerType>,
    input_size: u32,
    input_capacity: u32,
    // ...
}

§#[output], -> ReturnValue and -> Result<ReturnValue, ContractError>

Each #[output] argument in ExternalArgs is represented by three fields, output_ptr and output_capacity are read-only, while output_size is updated by the host after the method call:

ⓘ
#[repr(C)]
pub struct ExternalArgs<'external_args> {
    // ...
    output_ptr: NonNull<<OutputValue as IoType>::PointerType>,
    output_size: u32,
    output_capacity: u32,
    // ...
}

Each return value is represented by just a pointer since return type is TrivialType and don’t need separate size and capacity fields:

ⓘ
#[repr(C)]
pub struct ExternalArgs<'external_args> {
    // ...
    ok_result: &'external_args mut MaybeUninit<ReturnValue>,
}

Some arguments are omitted in ExternalArgs:

  • the contract’s initial state produced by #[init] method, which is processed by the execution environment itself: ReturnValue or, when it is (), the last #[output]
  • ReturnValue when it is (), since there is no point in having a pointer for it

The host passes the current output_size to the callee as is, so that the callee can both read and modify the output, and updates output_size after the call.

§Extension trait

Extension trait is just a convenient wrapper, whose safe methods take strongly typed arguments, construct ExternalArgs while respecting Rust safety invariants, and call Env::call() with it. Extension trait usage is not mandatory, but it does make method calls much more convenient in most simple cases.

Generated methods are named like FFI functions and take &self for #[view] methods and &mut self otherwise. Their arguments reflect ExternalArgs fields, preceded by the method context (except when calling a #[view] method, where context is not applicable) and the address of the contract being called:

ⓘ
// This
#[contract]
impl Token {
    #[view]
    pub fn balance(#[slot] target: &MaybeData<Slot>) -> Balance {
        // ...
    }

    #[view]
    pub fn balances(
        #[slot] target: &MaybeData<Slot>,
        #[output] balances: &mut VariableElements<Balance>,
    ) {
        // ...
    }

    #[update]
    pub fn transfer(
        #[env] env: &mut Env<'_>,
        #[slot] (from_address, from): (&Address, &mut MaybeData<Slot>),
        #[slot] to: &mut MaybeData<Slot>,
        #[input] &amount: &Balance,
    ) -> Result<(), ContractError> {
        // ...
    }
}

// Will generate this
pub trait TokenExt {
    fn token_balance(
        &self,
        contract: Address,
        target: &Address,
    ) -> Result<Balance, ContractError>;

    fn token_balances(
        &self,
        contract: Address,
        target: &Address,
        balances: &mut impl OutputArg<VariableElements<Balance>>,
    ) -> Result<(), ContractError>;

    fn token_transfer(
        &mut self,
        method_context: MethodContext,
        contract: Address,
        from: &Address,
        to: &Address,
        amount: &Balance,
    ) -> Result<(), ContractError>;
}

impl TokenExt for Env<'_> {
    // ...
}

Each #[output] is given as OutputArg. A call can leave contents behind in it that are not valid values, for example when it fails halfway. Memory whose contents accept every bit pattern (see AnyBitPattern) is given directly, like &mut value for a u64 or &mut VariableElements::from_uninit(..).expect(..) for VariableElements<Balance>. Memory of other types is given in a Lent, which parses the contents before they are accessed. After a successful call, the size of each output is set to the one the callee reported. The return value is parsed, and ContractError::BadOutput is returned if it is not a valid value of its type, with sizes of outputs already set.

The name of the extension trait is created as struct or trait name followed by Ext suffix.

§Metadata

There are several places where metadata is being generated, see ContractMetadataKind for details.

First, the #[contract] macro generates a public METADATA constant for each method individually.

Second, for each trait that contract can implement #[contract] macro generates an associated constant METADATA that essentially aggregates metadata of all annotated methods.

Third, Contract trait implementation generated by #[contract] macro contains MAIN_CONTRACT_METADATA associated constant, which is similar in nature to METADATA constant for traits described above.

Lastly, for the whole contract as a project, both trait and contract metadata is concatenated and stored in the ab-contract-metadata link section that can later be inspected externally to understand everything about the contract’s interfaces, auto-generate UI, etc.