メインコンテンツへスキップする

Swap

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

1. Add Swap Functions to Pair Trait

At this stage, we will implement a swap function in the Pair contract.
Swap is a way for traders to exchange one PSP22 token for another one in a simple way.
In the ./logics/traits/pair.rs file add the swap function to the Pair trait. As this function modifies the state, &mut self should be used as the first argument. Also, we will add a function to emit a swap event in the contract:

pub trait Pair {
...
#[ink(message)]
fn swap(
&mut self,
amount_0_out: Balance,
amount_1_out: Balance,
to: AccountId,
) -> Result<(), PairError>;
...
fn _emit_swap_event(
&self,
_sender: AccountId,
_amount_0_in: Balance,
_amount_1_in: Balance,
_amount_0_out: Balance,
_amount_1_out: Balance,
_to: AccountId,
);
}

2. Swap

First, check user inputs, then get_reserves and check liquidity:

impl<T: Storage<data::Data> + Storage<psp22::Data>> Pair for T {
...
fn swap(
&mut self,
amount_0_out: Balance,
amount_1_out: Balance,
to: AccountId,
) -> Result<(), PairError> {
if amount_0_out == 0 && amount_1_out == 0 {
return Err(PairError::InsufficientOutputAmount)
}
let reserves = self.get_reserves();
if amount_0_out >= reserves.0 || amount_1_out >= reserves.1 {
return Err(PairError::InsufficientLiquidity)
}
}
...
}

Then, obtain the token_0 and token_1 addresses and process the swap. Ensure the amount transferred out is not 0:

...
let token_0 = self.data::<data::Data>().token_0;
let token_1 = self.data::<data::Data>().token_1;

if to == token_0 || to == token_1 {
return Err(PairError::InvalidTo)
}
if amount_0_out > 0 {
self._safe_transfer(token_0, to, amount_0_out)?;
}
if amount_1_out > 0 {
self._safe_transfer(token_1, to, amount_1_out)?;
}
...

Then, obtain the balance of both token contracts, which will be used to update the price:

...
let contract = Self::env().account_id();
let balance_0 = PSP22Ref::balance_of(&token_0, contract);
let balance_1 = PSP22Ref::balance_of(&token_1, contract);
...

Ensure that no swap attempted will leave the trading pair with less than the minimum amount of reserves. balance_0 and balance_1 are the balances/reserves after the swap is finished, and reserve_0 and reserve_1 are the values previous to that (swap is done first and then possibly reverted, if the requirements are not met). We will need to check that the swap did not reduce the product of the reserves (otherwise liquidity from the pool can be stolen). Hence the reason why we check balance_0 * balance_1 >= reserve_0 * reserve_1.
balance_0_adjusted and balance_1_adjusted are adjusted with 0.3% swap liquidity provider fees.

...
let amount_0_in = if balance_0
> reserves
.0
.checked_sub(amount_0_out)
.ok_or(PairError::SubUnderFlow4)?
{
balance_0
.checked_sub(
reserves
.0
.checked_sub(amount_0_out)
.ok_or(PairError::SubUnderFlow5)?,
)
.ok_or(PairError::SubUnderFlow6)?
} else {
0
};
let amount_1_in = if balance_1
> reserves
.1
.checked_sub(amount_1_out)
.ok_or(PairError::SubUnderFlow7)?
{
balance_1
.checked_sub(
reserves
.1
.checked_sub(amount_1_out)
.ok_or(PairError::SubUnderFlow8)?,
)
.ok_or(PairError::SubUnderFlow9)?
} else {
0
};
if amount_0_in == 0 && amount_1_in == 0 {
return Err(PairError::InsufficientInputAmount)
}

let balance_0_adjusted = balance_0
.checked_mul(1000)
.ok_or(PairError::MulOverFlow8)?
.checked_sub(amount_0_in.checked_mul(3).ok_or(PairError::MulOverFlow9)?)
.ok_or(PairError::SubUnderFlow10)?;
let balance_1_adjusted = balance_1
.checked_mul(1000)
.ok_or(PairError::MulOverFlow10)?
.checked_sub(amount_1_in.checked_mul(3).ok_or(PairError::MulOverFlow11)?)
.ok_or(PairError::SubUnderFlow11)?;

if balance_0_adjusted
.checked_mul(balance_1_adjusted)
.ok_or(PairError::MulOverFlow16)?
< reserves
.0
.checked_mul(reserves.1)
.ok_or(PairError::MulOverFlow17)?
.checked_mul(1000u128.pow(2))
.ok_or(PairError::MulOverFlow18)?
{
return Err(PairError::K)
}

Then update the pool reserves, and emit a swap event:

        self._update(balance_0, balance_1, reserves.0, reserves.1)?;

self._emit_swap_event(
Self::env().caller(),
amount_0_in,
amount_1_in,
amount_0_out,
amount_1_out,
to,
);
Ok(())

Add the empty implementation of _emit_swap_event. It should have the default keyword, as we will override this function in the Pair contract:

impl<T: Storage<data::Data> + Storage<psp22::Data>> Pair for T {
...
default fn _emit_swap_event(
&self,
_sender: AccountId,
_amount_0_in: Balance,
_amount_1_in: Balance,
_amount_0_out: Balance,
_amount_1_out: Balance,
_to: AccountId,
) {
}
...
}

Add the Error fields to PairError in the ./logics/traits/pair.rs file:

#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum PairError {
PSP22Error(PSP22Error),
TransferError,
K,
InsufficientLiquidityMinted,
InsufficientLiquidityBurned,
InsufficientOutputAmount,
InsufficientLiquidity,
Overflow,
InvalidTo,
InsufficientInputAmount,
SubUnderFlow1,
SubUnderFlow2,
SubUnderFlow3,
SubUnderFlow4,
SubUnderFlow5,
SubUnderFlow6,
SubUnderFlow7,
SubUnderFlow8,
SubUnderFlow9,
SubUnderFlow10,
SubUnderFlow11,
SubUnderFlow14,
MulOverFlow1,
MulOverFlow2,
MulOverFlow3,
MulOverFlow4,
MulOverFlow5,
MulOverFlow6,
MulOverFlow7,
MulOverFlow8,
MulOverFlow9,
MulOverFlow10,
MulOverFlow11,
MulOverFlow14,
MulOverFlow15,
MulOverFlow16,
MulOverFlow17,
MulOverFlow18,
DivByZero1,
DivByZero2,
DivByZero3,
DivByZero4,
DivByZero5,
AddOverflow1,
}

3. Implement Event

In the contracts ./cotnracts/pair/lib.rs file, add the Event struct and override the implementation of emit event:

...
#[ink(event)]
pub struct Swap {
#[ink(topic)]
pub sender: AccountId,
pub amount_0_in: Balance,
pub amount_1_in: Balance,
pub amount_0_out: Balance,
pub amount_1_out: Balance,
#[ink(topic)]
pub to: AccountId,
}
...
impl Pair for PairContract {
...
fn _emit_swap_event(
&self,
sender: AccountId,
amount_0_in: Balance,
amount_1_in: Balance,
amount_0_out: Balance,
amount_1_out: Balance,
to: AccountId,
) {
self.env().emit_event(Swap {
sender,
amount_0_in,
amount_1_in,
amount_0_out,
amount_1_out,
to,
})
}
}
...

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

cargo contract build

It should now look like this branch.