Skip to content

[Feature] Dynamic trait based contract calling #631

Open
@Robbepop

Description

@Robbepop

Since ink! 3.0-rc1 it is possible to define special trait definitions for ink! smart contract using the #[ink::trait_definition] proc. macro.

Motivation

Defining and implementing such a trait works as expected with regard to some technical limitations.
Smart contracts today can some limited use of traits by calling or instantiating a contract using just the trait it implements.

However, this process is static and not dynamic, meaning that it is currently not possible to store a contract, e.g. in a ink_storage::Lazy<dyn MyContractTrait> or having an input parameter to a contract constructor or message with contract: &dyn MyContractTrait and be able to call constructors and messages defined in the trait dynamically on the provided contract instance.

This feature is critical to making trait definitions and implementation actually an integrated user experience for ink! smart contracts.

Syntax

There are several different ways in which a smart contract might want to interact with another smart contract dynamically.

As Message Parameter

It should be possible to declare a smart contract message or constructor with an input argument with contract: &[mut] dyn MyContractTrait where MyContractTrait is a trait definition that was defined using ink!'s #[ink::trait_definition] and contract is an instance of a smart contract (basically its AccountId) that implements this exact trait.

Then the ink! constructor or message receiving this argument can call any messages defined on the trait definition using the given contract instance. Note that it would not be possible to call trait definition constructors on contract since it represents an already instantiated contract.
Also we have to differentiate between contract: &dyn MyContractTrait and &mut dyn MyContractTrait where only the latter allows to call &mut self defined ink! messages.

It would be possible to syntactically detect usages of &dyn Trait in the input parameters of an ink! smart contract message or constructor and convert the type into something that is actually usable as input parameter, e.g. implements scale::Codec and has an AccountId field for the indirection etc. However, with general ink! design we try to be very explicit about user intentions which is why it might be preferable to start with an ink! annotation again and see how far we can get with it.

The proposed ink! annotation could be something like #[ink(dyn)] or #[ink(trait)].

Example

In the following examples we took the #[ink(trait)] as proposed ink! annotation. Note that this design is not final.

#[ink(message)]
pub fn call_back(&self, #[ink(trait)] callback: &dyn ContractCallback) {
    if self.condition() {
        callback.do_something();
    }
}

Or ...

#[ink(message)]
pub fn call_back_mut(&self, #[ink(trait)] callback: &mut dyn ContractCallback) {
    if self.condition() {
        callback.mutate_something();
    }
}

Where do_something is an ink! message defined in the ContractCallback trait definition as &self message and mutate_something is an ink! message defined there as well as &mut self message:

#[ink::trait_definition]
pub trait ContractCallback {
    #[ink(message)]
    fn do_something(&self);
    #[ink(message)]
    fn mutate_something(&mut self);
}

Further complications arise in the case of multiple trait bounds such as &dyn MyContractTraitA + MyContractTraitB + .... If we find out that these cases introduce severe complications we might drop them from the initial support and will try to work on these enhancements on a later point in time.

As Storage Entity

It might also be handy to store references to other smart contracts and being able to call them by traits that they implement.

Given the ContractCallback trait from last section the natural way a Rust programmer would make use an instance implementing this trait is by using the following storage fields and types:

#[ink(storage)]
pub struct MyStorage {
    value: Box<dyn ContractCallback>,
    vec: Vec<Box<dyn ContractCallback>>, // ... or similar
    array: [Box<dyn ContractCallback>] // ... etc
}

So it is all about utilizing Rust's owning smart references which is the Box<T> abstraction.
However, to a normal Rust developer usage of a Box would imply usage of dynamic heap memory allocations which we do not really need for our purposes since the underlying AccountId with which we identify or point to or reference a smart contract through its implemented interface is already serving as a pointer and the smart contract instance itself with its defined trait implementation that is about to be dispatched is already sufficient to perfectly emulate the dynamic dispatch.

So per design we ideally want to convert usages of Box<dyn Trait> into some other static type that acts like a type that implements the trait and therefore can be used interchangeably. This might count for the input parameters described above as well.
On the other handside making it possible to use Box<T> in places that are later completely replaced by some other non-allocating type that is to be used to indirect trait based contract calls (scale::Codec impl and internal AccountId) might confuse users which is why we should actually be more clever about how exactly we determine usage of dynamic trait based indirect calls for contracts.

The current proposal is to again introduce a new ink! annotation #[ink(dyn)] or #[ink(trait)] etc. in order for a user to flag a field of the #[ink(storage)] struct so that the ink! code analysis can recursively step through the syntactic type and replace uses of dyn Trait with whatever utility type is to be used in order to actually call traits implemented by contracts via cross contract calling.

As there might be aliases or custom types that might hide such parameters from the #[ink(storage)] struct definition we need to allow users to apply this new ink! annotation on structs fields, enums variant fields and type alises.

Example

In the following examples we took the #[ink(trait)] as proposed ink! annotation. Note that this design is not final.

#[ink(storage)]
pub struct MyStorage {
    #[ink(trait)]
    single: dyn ContractCallback, // replaces dyn Trait directly
    #[ink(trait)]
    vec: StorageVec<dyn ContractCallback>, // replaces dyn Trait in the generic argument of the StorageVec
    #[ink(trait)]
    array: [dyn ContractCallback; 10], // replaces dyn Trait of the array argument,
    #[ink(trait)]
    aliased: Aliased,
}

#[ink(trait)]
type Aliased = dyn ContractCallback; // replaces dyn Trait directly

pub struct MyStruct {
    #[ink(trait)]
    single: dyn ContractCallback, // replaces dyn Trait directly
}

pub enum MyEnum {
    A(#[ink(trait)] dyn ContractCallback), // replaces dyn Trait directly
    B,
}

As Dynamic Let-Binding

TODO

Metadata

Metadata

Assignees

Labels

A-ink_lang[ink_lang] Work itemB-designDesigning a new component, interface or functionality.B-enhancementNew feature or requestB-researchResearch task that has open questions that need to be resolved.E-mentor-availableA mentor for this issue is available

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions