ink icon indicating copy to clipboard operation
ink copied to clipboard

Add syntax sugar for SpreadLayout::spread_allocate using ink! constructors

Open Robbepop opened this issue 4 years ago • 0 comments

Spread allocate constructor syntax sugar

Motivation

With #961 it will become common to define ink! constructors in a way to allow for storage facilities that require initialization using SpreadLayout::spread_allocate. An example ERC-20 using this initialization method may look like the following:

mod erc20 {
    #[ink(storage)]
    pub struct Erc20 {
        total_supply: Balance,
        balances: Mapping<AccountId, Balance>,
    }

    impl Erc20 {
        /// Initializes the ERC-20 smart contract with the initial supply.
        #[ink(constructor)]
        fn new(initial_supply: Balance) -> Self {
            // Root key usually is initialized as `[0x00; 32]` in ink!:
            let root_key = Key::from([0x00; 32]);
            let mut ptr = KeyPtr::from(root_key);
            let mut contract = <Self as SpreadLayout>::spread_allocate(&mut ptr);
            contract.initialize(initial_supply);
            contract
        }

        /// Private and hidden initializer for the `new` constructor.
        fn initialize(&mut self, initial_supply: Balance) {
            let owner = self.env().caller();
            self.mapping.insert(owner, initial_supply);
            self.total_supply = initial_supply;
        }
    }
}

The above code is quite verbose for a constructor pattern that may become a common pattern for ink! constructors with more non-cached storage facilities introduced in the future. To counteract the verbosity we might be required to add some syntax sugar to ink! in order to smart contract authors to be able to write new ink! constructors with less code duplication.

Things like the root key that is equal to [0x00; 32] by default is something that users might want to configure and that might not be obvious and where smart contract authors might experience inconsistencies frequently if care is not always taken.

Proposals

There are current 2 proposals for new syntax sugar to aide in this situation.

Proposal 1: Extend ink! Constructors Semantics

This is the more lightweight proposal and proposes to add a new ink! attribute to the already existing #[ink(constructor)] attribute.

mod erc20 {
    #[ink(storage)]
    pub struct Erc20 {
        total_supply: Balance,
        balances: Mapping<AccountId, Balance>,
    }

    impl Erc20 {
        /// Initializes the ERC-20 smart contract.
        #[ink(constructor, initializer(new_init))]
        fn new(initial_supply: Balance) -> Self;

        /// Private and hidden initializer for the `new` constructor.
        fn new_init(&mut self, initial_supply: Balance) {
            let owner = self.env().caller();
            self.mapping.insert(owner, initial_supply);
            self.total_supply = initial_supply;
        }
    }
}

With this syntax sugar introduction that smart contract author may add #[ink(initializer(new_init))] to their #[ink(constructor)] annotated ink! constructor in order to indicate that a private function called new_init will be called after a automated SpreadLayout::spread_allocate initialization of the ink! storage struct. This also enforces that the annotated constructor no longer has a body since ink! will automatically generate the body for the ink! constructor very similar as to what the original example above does. This also enforces that the calling initializer fulfills some properties. For example, it must have a &mut self receiver and won't return anything. Later proposals might allow for return types. Any inputs of the #[ink(constructor)] must also be given to the ink! initializer which can be seen with the initial_supply: Balance input.

Advantages

  • This design integrates very well into the rest of the ink! design with the initializer being able to emit events naturally, use the self.env() syntax or call other messages and methods defined on the ink! storage struct.
  • The proposal makes up for a very simple and straight forward and transparent code generation or expansion that may be easily understandable by ink! smart contract authors.

Disadvantages

  • This proposal introduces non-Rust syntax since methods without function body are usually not permitted in Rust implementation blocks. They are only allowed in Rust trait definitions. It would be a great bummer for this proposal if we could no longer use syn's default parsing because of this or if tools such as rust-analyzer would have problems with non-Rust-like syntax such as this one. Link to syn ImplItemMethod docs: https://docs.rs/syn/1.0.80/syn/struct.ImplItemMethod.html
  • Other &mut self ink! messages and methods are able to call into the initializer method since it is just yet another method defined on the ink! storage struct.

Extensions

We might require ink! smart contract authors to flag their chosen initializer methods with #[ink(initializer)] so that ink! can perform some other checks on them. For example it might be useful to enforce that those initializers are private and locally enforce some other properties.

Proposal 2: Introduce ink! Initializers

Given the following ink! smart contract code:

mod erc20 {
    #[ink(storage)]
    pub struct Erc20 {
        total_supply: Balance,
        balances: Mapping<AccountId, Balance>,
    }

    impl Erc20 {
        /// Initializes the ERC-20 smart contract with the initial supply.
        #[ink(initializer)]
        fn new(&mut self, initial_supply: Balance) {
            let owner = self.env().caller();
            self.mapping.insert(owner, initial_supply);
            self.total_supply = initial_supply;
        }
    }
}

Using the 2nd proposal ink! will expand the above code to roughly the following:

mod erc20 {
    #[ink(storage)]
    pub struct Erc20 {
        total_supply: Balance,
        balances: Mapping<AccountId, Balance>,
    }

    impl Erc20 {
        /// Initializes the ERC-20 smart contract.
        #[ink(constructor]
        fn new(initial_supply: Balance) -> Self {
            struct Initializer(pub Erc20);

            impl Initializer {
                fn into_initialize(mut self, initial_supply: Balance) -> Self {
                    self.initialize(initial_supply);
                    self
                }
                fn initialize(&mut self, initial_supply: Balance) {
                    let owner = self.env_caller();
                    self.balances.insert(owner, initial_supply);
                    self.total_supply = initial_supply;
                }
                fn finish(self) -> Erc20 { self.0 }
            }

            impl core::ops::Deref for Initializer {
                type Target = Erc20;
                fn deref(&self) -> &Self::Target {
                    &self.0
                }
            }

            impl core::ops::DerefMut for Initializer {
                fn deref_mut(&mut self) -> &mut Self::Target {
                    &mut self.0
                }
            }

            let root_key = Key::from([0x00; 32]);
            let mut ptr = KeyPtr::from(root_key);
            let contract = <Self as SpreadLayout>::spread_allocate(&mut ptr);
            let mut initializer = Initializer(contract)
                .into_initialize(initial_supply)
                .finish()
        }
    }
}

This proposal expands to much more and less transparent code than the first proposal. The automatically generated Initializer struct serves as purpose for a thin-wrapper around the given ink! smart contract (in this case Erc20) so that the &mut self method body is directly applicable in the codegen.

Advantages

  • There is less typing overhead compared to the first proposal. An ink! smart contract author only has to specify the #[ink(initializer)] annotated &mut self method and they are good to go.
  • The actual &mut self ink! initializer won't be callable from other methods and messages defined on the ink! storage struct.
  • The proposal fits into Rust's current syntax model and unlike the first proposal won't require syntactical structure that are unusual for Rust code, e.g. the methods without function body.

Disadvantages

  • It might be very confusing for ink! users to define a &mut self receiver method that ends up being generated as a usual ink! constructor method that no longer has a self receiver just like normal Rust constructors.
  • Not being able to call the defined initializer method from other methods and messages defined on the ink! storage struct might also lead to confusions for ink! smart contract authors.
  • The automatically generated code is bigger, more complex and less transparent to ink! smart contract authors. Also there might be some undiscovered problems with emitting events, using self.env() syntax or calling other ink! methods, trait methods, trait messages or messages.
  • All ink! properties defined on the #[ink(initializer)] must be forwarded to the automatically generated ink! constructor. For example selector = 0xC0DECAFE must be implicitly applied to the automatically generated ink! constructor instead.

Robbepop avatar Oct 16 '21 13:10 Robbepop