Create Pair
If you are starting the tutorial from here, please check out this branch and open it in your IDE.
1. Add Create Pair to Factory Trait
We will implement the createPair function of the Factory contract.
In the ./logics/traits/factory.rs file, add the create_pair function to the Factory trait, as well as the internal child function _instantiate_pair that will need to be implemented in the contract crate.
The reason why we need an internal _instantiate_pair function here is because the instantiate builder is not part of the #[openbrush::wrapper]
, so we will need to use the one from ink! by importing the Pair contract as an ink-as-dependancy
.
The create_pair message function returns the address of the instantiated Pair contract.
The function that emits a create_pair event will also have to be implemented in the contract:
pub trait Factory {
...
#[ink(message)]
fn create_pair(
&mut self,
token_a: AccountId,
token_b: AccountId,
) -> Result<AccountId, FactoryError>;
fn _instantiate_pair(&mut self, salt_bytes: &[u8]) -> Result<AccountId, FactoryError>;
...
fn _emit_create_pair_event(
&self,
_token_0: AccountId,
_token_1: AccountId,
_pair: AccountId,
_pair_len: u64,
);
}
2. Implement Create Pair
In the ./logics/impls/factory/factory.rs file, let's implement the create_pair function body:
1. Checks that addresses are not identical
AccountId derives Eq
trait, so comparison operators can be used:
impl<T: Storage<data::Data>> Factory for T {
...
fn create_pair(
&mut self,
token_a: AccountId,
token_b: AccountId,
) -> Result<AccountId, FactoryError> {
if token_a == token_b {
return Err(FactoryError::IdenticalAddresses)
}
}
}
2. Order The Tuple
let token_pair = if token_a < token_b {
(token_a, token_b)
} else {
(token_b, token_a)
};
3. Check if the First Tuple Address is not the ZERO_ADDRESS
if token_pair.0 == ZERO_ADDRESS.into() {
return Err(FactoryError::ZeroAddress)
}
4. Instantiate the Pair Contract
The generate_address function in pallet_contracts
is akin to the formula of ETH's CREATE2 opcode. There is no CREATE equivalent because CREATE2 is strictly more powerful. Formula: hash(deploying_address ++ code_hash ++ salt)
Instantiation of a contract will define its own contract address by using the concatenated hash of:
- salt (in bytes)
- address of deployer
- code_hash
As the code_hash
and deployer
(Factory contract address) values will be unchanged during each call, the salt_bytes
value must be unique for each call. As the Factory contract will instantiate a unique Pair contract for each pair, we will hash over the token address to produce a unique salt:
let salt = Self::env().hash_encoded::<Blake2x256, _>(&token_pair);
let pair_contract = self._instantiate_pair(salt.as_ref())?;
5. Initialize Pair
PairRef::initialize(&pair_contract, token_pair.0, token_pair.1)?;
6. Create Storage Mappings in Both Directions and Push the Pair Address to all_pairs
self.data::<data::Data>()
.get_pair
.insert(&(token_pair.0, token_pair.1), &pair_contract);
self.data::<data::Data>()
.get_pair
.insert(&(token_pair.1, token_pair.0), &pair_contract);
self.data::<data::Data>().all_pairs.push(pair_contract);
6. Emit a create_pair
Event
self._emit_create_pair_event(
token_pair.0,
token_pair.1,
pair_contract,
self.all_pair_length(),
);
7. Return the Address of the Instantiated Contract
Ok(pair_contract)
The entire function should look like this:
fn create_pair(
&mut self,
token_a: AccountId,
token_b: AccountId,
) -> Result<AccountId, FactoryError> {
if token_a == token_b {
return Err(FactoryError::IdenticalAddresses)
}
let token_pair = if token_a < token_b {
(token_a, token_b)
} else {
(token_b, token_a)
};
if token_pair.0 == ZERO_ADDRESS.into() {
return Err(FactoryError::ZeroAddress)
}
let salt = Self::env().hash_encoded::<Blake2x256, _>(&token_pair);
let pair_contract = self._instantiate_pair(salt.as_ref())?;
PairRef::initialize(&pair_contract, token_pair.0, token_pair.1)?;
self.data::<data::Data>()
.get_pair
.insert(&(token_pair.0, token_pair.1), &pair_contract);
self.data::<data::Data>()
.get_pair
.insert(&(token_pair.1, token_pair.0), &pair_contract);
self.data::<data::Data>().all_pairs.push(pair_contract);
self._emit_create_pair_event(
token_pair.0,
token_pair.1,
pair_contract,
self.all_pair_length(),
);
Ok(pair_contract)
}
Implement an _instantiate_pair function with an unimplemented!()
macro in the body, to ensure it will be overridden (default
keyword should be added):
default fn _instantiate_pair(&mut self, _salt_bytes: &[u8]) -> Result<AccountId, FactoryError> {
// needs to be overridden in contract
unimplemented!()
}
Add import statements:
use crate::traits::pair::PairRef;
pub use crate::{
impls::factory::*,
traits::factory::*,
};
use ink::env::hash::Blake2x256;
use openbrush::traits::{
AccountId,
Storage,
ZERO_ADDRESS,
};
...
3. Implement Event
In the ./logics/impls/factory/factory.rs file, add empty implementation of _emit_create_pair_event:
default fn _emit_create_pair_event(
&self,
_token_0: AccountId,
_token_1: AccountId,
_pair: AccountId,
_pair_len: u64,
) {
}
Within the contracts folder, in the ./contracts/factory/lib.rs file, add a PairCreated
event struct and override the implementation of emit event:
...
use ink::{
codegen::{
EmitEvent,
Env,
},
ToAccountId,
};
...
#[ink(event)]
pub struct PairCreated {
#[ink(topic)]
pub token_0: AccountId,
#[ink(topic)]
pub token_1: AccountId,
pub pair: AccountId,
pub pair_len: u64,
}
...
impl Factory for FactoryContract {
fn _emit_create_pair_event(
&self,
token_0: AccountId,
token_1: AccountId,
pair: AccountId,
pair_len: u64,
) {
EmitEvent::<FactoryContract>::emit_event(
self.env(),
PairCreated {
token_0,
token_1,
pair,
pair_len,
},
)
}
}
4. Override _instantiate_pair
As it's not possible to call a constructor of a contract using #[openbrush::wrapper]
, we will need to use a contract Ref from ink!.
If you would like to import a contract as an ink-as-dependency
, it should be built as a library crate rlib
. Add this to the Cargo.toml
of the Pair contract in the ./contracts/pair/Cargo.toml file:
...
[lib]
name = "pair_contract"
path = "lib.rs"
crate-type = [
"cdylib",
"rlib"
]
...
Then import the Pair contract as an ink-as-dependency
in the Factory contract. Add the dependency to the Cargo.toml
of the Factory contract in the ./contracts/factory/Cargo.toml file:
...
pair_contract = { path = "../pair", default-features = false, features = ["ink-as-dependency"] }
...
[features]
default = ["std"]
std = [
"ink/std",
"scale/std",
"scale-info/std",
"openbrush/std",
"uniswap_v2/std",
"pair_contract/std",
]
In the contract crate ./contracts/factory/lib.rs add import statements:
...
use openbrush::traits::{
Storage,
ZERO_ADDRESS,
};
use pair_contract::pair::PairContractRef;
In _instantiate_pair function body: