Saltar al contenido principal

Factory Storage and Getters

If you are starting the tutorial from here, please check out this branch and open it in your IDE.

1. Factory Storage

The Factory contract has storage fields implemented in Solidity that we will need to implement in our contract(s):

    address public feeTo;
address public feeToSetter;

mapping(address => mapping(address => address)) public getPair;
address[] public allPairs;

Ink! uses most Substrate primitive types. Here is a table of conversion between Solidity and ink! types:

Solidityink!
uint256U256
any other uintu128 (or lower)
addressAccountId
mapping(key => value)Mapping(key, value)
mapping(key1 => mapping(key2 => value))Mapping((key1 ,key2), value)

Let's create a storage struct in the ./logics/impls/factory/data.rs file. Name the struct Data and add the required fields:

pub struct Data {
pub fee_to: AccountId,
pub fee_to_setter: AccountId,
pub get_pair: Mapping<(AccountId, AccountId), AccountId>,
pub all_pairs: Vec<AccountId>,
}

The Factory contract will deploy instances of the Pair contract . In Substrate, the contract deployment process is split into two steps:

  1. Deploying your contract code to the blockchain (the Wasm blob will be uploaded and has a unique code_hash).
  2. Creating an instance of your contract (by calling a constructor).

That's why the Factory Storage should save the Pair contract code_hash in order to instantiate it. Add a Pair code_hash field to the Storage:

    pub pair_contract_code_hash: Hash,

OpenBrush uses a specified storage key instead of the default one in the attribute openbrush::upgradeable_storage. It implements all required traits with the specified storage key (storage key is a required input argument of the macro). To generate a unique key, Openbrush provides a openbrush::storage_unique_key! declarative macro that is based on the name of the struct and its file path. Let's add this to our struct and import the required fields:

use ink::{
prelude::vec::Vec,
primitives::Hash,
};
use openbrush::{
storage::Mapping,
traits::{
AccountId,
ZERO_ADDRESS,
},
};

pub const STORAGE_KEY: u32 = openbrush::storage_unique_key!(Data);

#[derive(Debug)]
#[openbrush::upgradeable_storage(STORAGE_KEY)]
pub struct Data {
pub fee_to: AccountId,
pub fee_to_setter: AccountId,
pub get_pair: Mapping<(AccountId, AccountId), AccountId>,
pub all_pairs: Vec<AccountId>,
pub pair_contract_code_hash: Hash,
}

impl Default for Data {
fn default() -> Self {
Self {
fee_to: ZERO_ADDRESS.into(),
fee_to_setter: ZERO_ADDRESS.into(),
get_pair: Default::default(),
all_pairs: Default::default(),
pair_contract_code_hash: Default::default(),
}
}
}

./logics/impls/factory/data.rs

2. Trait for Getters

Unlike Solidity, which will automatically create getters for the storage items, with ink! you will need add them yourself. There is already a Factory trait with fee_to function in the file ./logics/traits/factory.rs.
Add all getters:

use openbrush::traits::AccountId;

#[openbrush::wrapper]
pub type FactoryRef = dyn Factory;

#[openbrush::trait_definition]
pub trait Factory {
#[ink(message)]
fn all_pair_length(&self) -> u64;

#[ink(message)]
fn set_fee_to(&mut self, fee_to: AccountId) -> Result<(), FactoryError>;

#[ink(message)]
fn set_fee_to_setter(&mut self, fee_to_setter: AccountId) -> Result<(), FactoryError>;

#[ink(message)]
fn fee_to(&self) -> AccountId;

#[ink(message)]
fn fee_to_setter(&self) -> AccountId;

#[ink(message)]
fn get_pair(&self, token_a: AccountId, token_b: AccountId) -> Option<AccountId>;

fn _emit_create_pair_event(
&self,
_token_0: AccountId,
_token_1: AccountId,
_pair: AccountId,
_pair_len: u64,
);
}

The last thing to do is to add the Error enum, and each contract should use its own. As they will be used in function arguments, we should implement Scale encoding & decoding. For the moment we don't need a properly defined error, so simply add Error as a field:

...
#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum FactoryError {
Error
}

./logics/traits/factory.rs

3. Implement Getters

in ./logics/impls/factory/factory.rs add and impl a block for the generic type data::Data. We will wrap the Data struct in the Storage trait to add it as trait bound:

pub use crate::{
impls::factory::*,
traits::factory::*,
};
use openbrush::{
traits::{
AccountId,
Storage
},
};

impl<T: Storage<data::Data>> Factory for T {}

all_pair_length

This getter returns the total number of pairs:

    fn all_pair_length(&self) -> u64 {
self.data::<data::Data>().all_pairs.len() as u64
}

set_fee_to

This setter sets the address collecting the fee:

    fn set_fee_to(&mut self, fee_to: AccountId) -> Result<(), FactoryError> {
self.data::<data::Data>().fee_to = fee_to;
Ok(())
}

set_fee_to_setter

This setter sets the address of the fee setter:

    fn set_fee_to_setter(&mut self, fee_to_setter: AccountId) -> Result<(), FactoryError> {
self.data::<data::Data>().fee_to_setter = fee_to_setter;
Ok(())
}

fee_to

This getter returns the address collecting the fee:

    fn fee_to(&self) -> AccountId {
self.data::<data::Data>().fee_to
}

fee_to_setter

This getter returns the address of the fee setter:

    fn fee_to(&self) -> AccountId {
self.data::<data::Data>().fee_to
}

get_pair

This getter takes two addresses as arguments and returns the Pair contract address (or None if not found):

    fn get_pair(&self, token_a: AccountId, token_b: AccountId) -> Option<AccountId> {
self.data::<data::Data>().get_pair.get(&(token_a, token_b))
}

4. Implement gGetters in Contract

In the ./uniswap-v2/contracts folder, create a factory folder containing Cargo.toml and lib.rs files.
The Cargo.toml should look like this:

[package]
name = "factory_contract"
version = "0.1.0"
authors = ["Stake Technologies <devops@stake.co.jp>"]
edition = "2021"

[dependencies]
ink = { version = "4.0.0", default-features = false}

scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2.3", default-features = false, features = ["derive"], optional = true }

openbrush = { git = "https://github.com/727-Ventures/openbrush-contracts", version = "3.0.0", default-features = false }
uniswap_v2 = { path = "../../logics", default-features = false }

[lib]
path = "lib.rs"
crate-type = ["cdylib"]

[features]
default = ["std"]
std = [
"ink/std",
"scale/std",
"scale-info/std",
"openbrush/std",
"uniswap_v2/std"
]
ink-as-dependency = []

[profile.dev]
overflow-checks = false

[profile.release]
overflow-checks = false

In the lib.rs file, create a factory module with Openbrush contract. Import the Storage trait from Openbrush (as well as ZERO_ADDRESS) and SpreadAllocate from ink! As reminder the #![cfg_attr(not(feature = "std"), no_std)] attribute is for conditional compilation and the #![feature(min_specialization)] is the feature needed to enable specialization. Also import everything (with *) from impls::factory and traits::factory:

#![cfg_attr(not(feature = "std"), no_std)]
#![feature(min_specialization)]

#[openbrush::contract]
pub mod factory {
use openbrush::traits::{
Storage,
ZERO_ADDRESS,
};
use uniswap_v2::{
impls::factory::*,
traits::factory::*,
};

Add the storage struct and Factory field (that we defined in traits):

    #[ink(storage)]
#[derive(Default, Storage)]
pub struct FactoryContract {
#[storage_field]
factory: data::Data,
}

Implement the Factory trait in your contract struct:

    impl Factory for FactoryContract {}

Add an impl block for the contract, and add the constructor. The constructor takes 2 arguments fee_to_setter and the pair_code_hash and saved them in storage:

    impl FactoryContract {
#[ink(constructor)]
pub fn new(fee_to_setter: AccountId, pair_code_hash: Hash) -> Self {
let mut instance = Self::default();
instance.factory.pair_contract_code_hash = pair_code_hash;
instance.factory.fee_to_setter = fee_to_setter;
instance.factory.fee_to = ZERO_ADDRESS.into();
instance
}
}

And that's it! Check your Factory contract with (run in contract folder):

cargo contract build

It should now look like this branch.