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:
Solidity | ink! |
---|---|
uint256 | U256 |
any other uint | u128 (or lower) |
address | AccountId |
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:
- Deploying your contract code to the blockchain (the Wasm blob will be uploaded and has a unique
code_hash
). - 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.