Pair Storage and Getters
If you are starting the tutorial from here, Please checkout this branch and open it in your IDE.
1. Logics Crate
As described in the File & Folder structure section, the Pair business logic will be in the uniswap-v2 logics crate. Let's create (empty) files and folders so your project looks like this:
├── uniswap-v2
│ ├── contracts
│ ├── logics
│ │ ├── impls
│ │ │ ├── pair
│ │ │ │ ├── mod.rs
│ │ │ │ ├── data.rs
│ │ │ │ └── pair.rs
│ │ │ └── mod.rs
│ │ └── traits
│ │ ├── mod.rs
│ │ ├── pair.rs
│ ├── Cargo.toml
│ └── lib.rs
├── Cargo.lock
├── Cargo.toml
├── .rustfmt
└── .gitignore
The ./uniswap-v2/logics/Cargo.toml will be a rlib
crate named "uniswap_v2""
and import crates from ink!, scale, and Openbrush (with feature "psp22"
)
[package]
name = "uniswap_v2"
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, features = ["psp22"] }
[lib]
name = "uniswap_v2"
path = "lib.rs"
crate-type = [
"rlib",
]
[features]
default = ["std"]
std = [
"ink/std",
"scale/std",
"scale-info/std",
"openbrush/std",
]
./uniswap-v2/logics/Cargo.toml
The lib.rs
file should contain a conditional compilation attribute. It should also export impls
and traits
#![cfg_attr(not(feature = "std"), no_std)]
pub mod impls;
pub mod traits;
./uniswap-v2/logics/lib.rs
2. Pair Storage
The Uniswap V2 Pair contract has storage fields in Solidity that we should implement as shown below:
address public factory;
address public token0;
address public token1;
uint112 private reserve0; // uses single storage slot, accessible via getReserves
uint112 private reserve1; // uses single storage slot, accessible via getReserves
uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves
uint public price0CumulativeLast;
uint public price1CumulativeLast;
uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event
ink! uses most Substrate primitive types. Here is a conversion table 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 ./logics/impls/pair/data.rs. Name the struct Data
and add all the required fields.
pub struct Data {
pub factory: AccountId,
pub token_0: AccountId,
pub token_1: AccountId,
pub reserve_0: Balance,
pub reserve_1: Balance,
pub block_timestamp_last: Timestamp,
pub price_0_cumulative_last: Balance,
pub price_1_cumulative_last: Balance,
pub k_last: u128,
}
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 the 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 openbrush::traits::{
AccountId,
Balance,
Timestamp,
};
pub const STORAGE_KEY: u32 = openbrush::storage_unique_key!(Data);
#[derive(Debug)]
#[openbrush::upgradeable_storage(STORAGE_KEY)]
pub struct Data {
pub factory: AccountId,
pub token_0: AccountId,
pub token_1: AccountId,
pub reserve_0: Balance,
pub reserve_1: Balance,
pub block_timestamp_last: Timestamp,
pub price_0_cumulative_last: Balance,
pub price_1_cumulative_last: Balance,
pub k_last: u128,
}
./logics/impls/pair/data.rs
And impl Default
for the Data
struct:
...
impl Default for Data {
fn default() -> Self {
Self {
factory: ZERO_ADDRESS.into(),
token_0: ZERO_ADDRESS.into(),
token_1: ZERO_ADDRESS.into(),
reserve_0: 0,
reserve_1: 0,
block_timestamp_last: 0,
price_0_cumulative_last: Default::default(),
price_1_cumulative_last: Default::default(),
k_last: Default::default(),
}
}
}
3. Trait for Getters
Unlike Solidity that will automatically create getters for the storage items, you need to add them yourself in ink!. For this we will create a trait and add generic implementation.
in the ./logics/traits/pair.rs file, let's create a trait with the getters functions and make them callable with #[ink(message)]
:
pub trait Pair {
#[ink(message)]
fn get_reserves(&self) -> (Balance, Balance, Timestamp);
#[ink(message)]
fn initialize(&mut self, token_0: AccountId, token_1: AccountId) -> Result<(), PairError>;
#[ink(message)]
fn get_token_0(&self) -> AccountId;
#[ink(message)]
fn get_token_1(&self) -> AccountId;
}
Openbrush provides #[openbrush::trait_definition]
that will make sure your trait (and its default implementation) will be generated in the contract. Also, you can create a wrapper around this trait so it can be used for cross-contract calls (so no need to import the contract as ink-as-dependancy). Import what is needed from Openbrush:
use openbrush::traits::{
AccountId,
Balance,
Timestamp,
};
#[openbrush::wrapper]
pub type PairRef = dyn Pair;
#[openbrush::trait_definition]
pub trait Pair {
#[ink(message)]
fn get_reserves(&self) -> (Balance, Balance, Timestamp);
#[ink(message)]
fn initialize(&mut self, token_0: AccountId, token_1: AccountId) -> Result<(), PairError>;
#[ink(message)]
fn get_token_0(&self) -> AccountId;
#[ink(message)]
fn get_token_1(&self) -> AccountId;
}
./logics/traits/pair.rs
The last thing to add will be the Error enum, and each contract should use its own. As it will be used in function arguments it should implement Scale encode & decode.
For the moment we don't need a proper error so just add Error
as field:
...
#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum PairError {
Error,
}
./logics/traits/pair.rs
4. Implement Getters
in ./logics/impls/pair/pair.rs add an impl block for generic type data::Data
. We wrap the Data struct in Storage trait to add it as trait bound.
impl<T: Storage<data::Data>> Pair for T {}
get_reserves
This function should return a tuple of reserves & timestamp of type (Balance, Balance, Timestamp)
. It takes &self
as it should access to Data storage struct but will not modify it hence no need for a mutable ref.
fn get_reserves(&self) -> (Balance, Balance, Timestamp) {
(
self.data::<data::Data>().reserve_0,
self.data::<data::Data>().reserve_1,
self.data::<data::Data>().block_timestamp_last,
)
}
initialize
This method is more of a setter as it will set token address in storage. That's why it takes a &mut self
as the first argument.
As a general rule if a function only takes &self
then it will not modify the state so it will only be called as a query.
If the functions takes an &mut self
it will make state change so can be called as a transaction, and should return a Result<T, E>.
Only factory can call this function, but we will add only_owner
modifier later in this tutorial.
fn initialize(
&mut self,
token_0: AccountId,
token_1: AccountId,
) -> Result<(), PairError> {
self.data::<data::Data>().token_0 = token_0;
self.data::<data::Data>().token_1 = token_1;
Ok(())
}
get_token
These two functions return the accountId of the tokens
fn get_token_0(&self) -> AccountId {
self.data::<data::Data>().token_0
}
fn get_token_1(&self) -> AccountId {
self.data::<data::Data>().token_1
}
Add imports, and your file should look like this:
pub use crate::{
impls::pair::*,
traits::pair::*,
};
use openbrush::traits::{
AccountId,
Balance,
Storage,
Timestamp,
};
impl<T: Storage<data::Data>> Pair for T {
fn get_reserves(&self) -> (Balance, Balance, Timestamp) {
(
self.data::<data::Data>().reserve_0,
self.data::<data::Data>().reserve_1,
self.data::<data::Data>().block_timestamp_last,
)
}
fn initialize(
&mut self,
token_0: AccountId,
token_1: AccountId,
) -> Result<(), PairError> {
self.data::<data::Data>().token_0 = token_0;
self.data::<data::Data>().token_1 = token_1;
Ok(())
}
fn get_token_0(&self) -> AccountId {
self.data::<data::Data>().token_0
}
fn get_token_1(&self) -> AccountId {
self.data::<data::Data>().token_1
}
}
5. Implement Getters to Pair contract
In ./contracts/pair/Cargo.toml import the uniswap-v2 logics crate and add it to the std features
...
uniswap_v2 = { path = "../../logics", default-features = false }
...
std = [
"ink/std",
"scale/std",
"scale-info/std",
"openbrush/std",
"uniswap_v2/std"
]
In the contract lib.rs import everything from pair traits (and impls):
use uniswap_v2::{
impls::pair::*,
traits::pair::*,
};
Add the Data
storage struct to the contract storage struct:
#[ink(storage)]
#[derive(Default, Storage)]
pub struct PairContract {
#[storage_field]
psp22: psp22::Data,
#[storage_field]
pair: data::Data,
}
And just below the storage struct impl Pair trait for the PairContract:
impl Pair for PairContract {}
And that's it!
In this section we've gone over how to create a trait and its generic implementation, and added them to the Pair contract. Check your Pair contract with (run from the contract folder):
cargo contract build
It should now look like this branch.