Add Dex pallet (#407)

* Move pallet-asset-conversion

* update licensing

* initial integration

* Integrate Currency & Assets types

* integrate liquidity tokens

* fmt

* integrate dex pallet tests

* fmt

* compilation error fixes

* integrate dex benchmarks

* fmt

* cargo clippy

* replace all occurrences of "asset" with "coin"

* add the actual add liq/swap logic to in-instructions

* add client side & tests

* fix deny

* Lint and changes

- Renames InInstruction::AddLiquidity to InInstruction::SwapAndAddLiquidity
- Makes create_pool an internal function
- Makes dex-pallet exclusively create pools against a native coin
- Removes various fees
- Adds new crates to GH workflow

* Fix rebase artifacts

* Correct other rebase artifact

* Correct CI specification for liquidity-tokens

* Correct primitives' test to the standardized pallet account scheme

---------

Co-authored-by: Luke Parker <lukeparker5132@gmail.com>
This commit is contained in:
akildemir 2023-11-05 20:02:34 +03:00 committed by GitHub
parent facb5817c4
commit 899a9604e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 5327 additions and 27 deletions

View file

@ -62,6 +62,9 @@ jobs:
-p serai-primitives \
-p serai-coins-primitives \
-p serai-coins-pallet \
-p serai-liquidity-tokens-pallet \
-p serai-dex-primitives \
-p serai-dex-pallet \
-p serai-validator-sets-primitives \
-p serai-validator-sets-pallet \
-p serai-in-instructions-primitives \

54
Cargo.lock generated
View file

@ -8179,6 +8179,7 @@ dependencies = [
"parity-scale-codec",
"scale-info",
"serai-coins-primitives",
"serai-dex-primitives",
"serai-primitives",
"sp-core",
"sp-runtime",
@ -8259,6 +8260,40 @@ dependencies = [
"rocksdb",
]
[[package]]
name = "serai-dex-pallet"
version = "0.1.0"
dependencies = [
"frame-benchmarking",
"frame-support",
"frame-system",
"parity-scale-codec",
"scale-info",
"serai-coins-pallet",
"serai-dex-primitives",
"serai-liquidity-tokens-pallet",
"serai-primitives",
"sp-api",
"sp-arithmetic",
"sp-core",
"sp-io",
"sp-runtime",
"sp-std",
]
[[package]]
name = "serai-dex-primitives"
version = "0.1.0"
dependencies = [
"frame-benchmarking",
"frame-support",
"parity-scale-codec",
"scale-info",
"serai-primitives",
"sp-runtime",
"sp-std",
]
[[package]]
name = "serai-docker-tests"
version = "0.1.0"
@ -8301,6 +8336,7 @@ dependencies = [
"parity-scale-codec",
"scale-info",
"serai-coins-pallet",
"serai-dex-pallet",
"serai-in-instructions-primitives",
"serai-primitives",
"serai-validator-sets-pallet",
@ -8308,6 +8344,7 @@ dependencies = [
"sp-core",
"sp-io",
"sp-runtime",
"sp-std",
"thiserror",
]
@ -8326,6 +8363,20 @@ dependencies = [
"zeroize",
]
[[package]]
name = "serai-liquidity-tokens-pallet"
version = "0.1.0"
dependencies = [
"frame-support",
"frame-system",
"parity-scale-codec",
"scale-info",
"serai-dex-primitives",
"serai-primitives",
"sp-core",
"sp-std",
]
[[package]]
name = "serai-message-queue"
version = "0.1.0"
@ -8543,7 +8594,10 @@ dependencies = [
"parity-scale-codec",
"scale-info",
"serai-coins-pallet",
"serai-dex-pallet",
"serai-dex-primitives",
"serai-in-instructions-pallet",
"serai-liquidity-tokens-pallet",
"serai-primitives",
"serai-signals-pallet",
"serai-validator-sets-pallet",

View file

@ -55,6 +55,9 @@ exceptions = [
{ allow = ["AGPL-3.0"], name = "serai-coordinator" },
{ allow = ["AGPL-3.0"], name = "serai-coins-pallet" },
{ allow = ["AGPL-3.0"], name = "serai-liquidity-tokens-pallet" },
{ allow = ["AGPL-3.0"], name = "serai-dex-primitives" },
{ allow = ["AGPL-3.0"], name = "serai-dex-pallet" },
{ allow = ["AGPL-3.0"], name = "serai-in-instructions-pallet" },

View file

@ -42,7 +42,7 @@ async fn test_substrate_signer() {
balance: Balance { coin: Coin::Bitcoin, amount: Amount(1000) },
},
InInstructionWithBalance {
instruction: InInstruction::Dex(Data::new(vec![0xcc; 128]).unwrap()),
instruction: InInstruction::Dex(DexCall::SwapAndAddLiquidity(SeraiAddress([0xbb; 32]))),
balance: Balance { coin: Coin::Monero, amount: Amount(9999999999999999) },
},
],

View file

@ -0,0 +1,71 @@
use sp_core::bounded_vec::BoundedVec;
use serai_runtime::{
primitives::{SeraiAddress, Amount, Coin},
dex, Dex, Runtime,
};
use subxt::tx::Payload;
use crate::{SeraiError, Composite, TemporalSerai, scale_composite};
const PALLET: &str = "Dex";
pub type DexEvent = dex::Event<Runtime>;
#[derive(Clone, Copy)]
pub struct SeraiDex<'a>(pub(crate) TemporalSerai<'a>);
impl<'a> SeraiDex<'a> {
pub async fn all_events(&self) -> Result<Vec<DexEvent>, SeraiError> {
self.0.events::<Dex, _>(|_| true).await
}
pub fn add_liquidity(
coin: Coin,
coin_amount: Amount,
sri_amount: Amount,
min_coin_amount: Amount,
min_sri_amount: Amount,
address: SeraiAddress,
) -> Payload<Composite<()>> {
Payload::new(
PALLET,
"add_liquidity",
scale_composite(dex::Call::<Runtime>::add_liquidity {
coin1: coin,
coin2: Coin::Serai,
amount1_desired: coin_amount.0,
amount2_desired: sri_amount.0,
amount1_min: min_coin_amount.0,
amount2_min: min_sri_amount.0,
mint_to: address.into(),
}),
)
}
pub fn swap(
from_coin: Coin,
to_coin: Coin,
amount_in: Amount,
amount_out_min: Amount,
address: SeraiAddress,
) -> Payload<Composite<()>> {
let path = if to_coin.is_native() {
BoundedVec::try_from(vec![from_coin, Coin::Serai]).unwrap()
} else if from_coin.is_native() {
BoundedVec::try_from(vec![Coin::Serai, to_coin]).unwrap()
} else {
BoundedVec::try_from(vec![from_coin, Coin::Serai, to_coin]).unwrap()
};
Payload::new(
PALLET,
"swap_exact_tokens_for_tokens",
scale_composite(dex::Call::<Runtime>::swap_exact_tokens_for_tokens {
path,
amount_in: amount_in.0,
amount_out_min: amount_out_min.0,
send_to: address.into(),
}),
)
}
}

View file

@ -35,6 +35,8 @@ use serai_runtime::{
pub mod coins;
pub use coins::SeraiCoins;
pub mod dex;
pub use dex::SeraiDex;
pub mod in_instructions;
pub use in_instructions::SeraiInInstructions;
pub mod validator_sets;
@ -347,6 +349,10 @@ impl<'a> TemporalSerai<'a> {
SeraiCoins(self)
}
pub fn dex(self) -> SeraiDex<'a> {
SeraiDex(self)
}
pub fn in_instructions(self) -> SeraiInInstructions<'a> {
SeraiInInstructions(self)
}

View file

@ -0,0 +1,55 @@
use serai_runtime::primitives::{Coin, Amount};
use serai_client::{Serai, SeraiDex, PairSigner};
use sp_core::{sr25519::Pair, Pair as PairTrait};
use subxt::config::extrinsic_params::BaseExtrinsicParamsBuilder;
use crate::common::tx::publish_tx;
#[allow(dead_code)]
pub async fn add_liquidity(
serai: &Serai,
coin: Coin,
coin_amount: Amount,
sri_amount: Amount,
nonce: u32,
pair: Pair,
) -> [u8; 32] {
let address = pair.public();
let tx = serai
.sign(
&PairSigner::new(pair),
&SeraiDex::add_liquidity(coin, coin_amount, sri_amount, Amount(1), Amount(1), address.into()),
nonce,
BaseExtrinsicParamsBuilder::new(),
)
.unwrap();
publish_tx(serai, &tx).await
}
#[allow(dead_code)]
pub async fn swap(
serai: &Serai,
from_coin: Coin,
to_coin: Coin,
amount_in: Amount,
amount_out_min: Amount,
nonce: u32,
pair: Pair,
) -> [u8; 32] {
let address = pair.public();
let tx = serai
.sign(
&PairSigner::new(pair),
&SeraiDex::swap(from_coin, to_coin, amount_in, amount_out_min, address.into()),
nonce,
BaseExtrinsicParamsBuilder::new(),
)
.unwrap();
publish_tx(serai, &tx).await
}

View file

@ -1,3 +1,4 @@
use rand_core::{RngCore, OsRng};
use blake2::{
digest::{consts::U32, Digest},
Blake2b,
@ -8,10 +9,10 @@ use scale::Encode;
use sp_core::Pair;
use serai_client::{
primitives::insecure_pair_from_name,
primitives::{insecure_pair_from_name, BlockHash, NetworkId, Balance, SeraiAddress},
validator_sets::primitives::{Session, ValidatorSet},
in_instructions::{
primitives::{Batch, SignedBatch, batch_message},
primitives::{Batch, SignedBatch, batch_message, InInstruction, InInstructionWithBalance},
InInstructionsEvent,
},
SeraiInInstructions, Serai,
@ -60,3 +61,27 @@ pub async fn provide_batch(serai: &Serai, batch: Batch) -> [u8; 32] {
block
}
#[allow(dead_code)]
pub async fn mint_coin(
serai: &Serai,
balance: Balance,
network: NetworkId,
batch_id: u32,
address: SeraiAddress,
) -> [u8; 32] {
let mut block_hash = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block_hash.0);
let batch = Batch {
network,
id: batch_id,
block: block_hash,
instructions: vec![InInstructionWithBalance {
instruction: InInstruction::Transfer(address),
balance,
}],
};
provide_batch(serai, batch).await
}

View file

@ -1,6 +1,7 @@
pub mod tx;
pub mod validator_sets;
pub mod in_instructions;
pub mod dex;
#[macro_export]
macro_rules! serai_test {

View file

@ -0,0 +1,452 @@
use rand_core::{RngCore, OsRng};
use scale::Encode;
use sp_core::{Pair as PairTrait, bounded_vec::BoundedVec, hashing::blake2_256};
use serai_runtime::in_instructions::primitives::DexCall;
use serai_client::{
primitives::{
Amount, NetworkId, Coin, Balance, BlockHash, insecure_pair_from_name, ExternalAddress,
SeraiAddress, PublicKey,
},
in_instructions::primitives::{
InInstruction, InInstructionWithBalance, Batch, IN_INSTRUCTION_EXECUTOR, OutAddress,
},
dex::DexEvent,
Serai,
};
mod common;
use common::{
in_instructions::{provide_batch, mint_coin},
dex::{add_liquidity as common_add_liquidity, swap as common_swap},
};
// TODO: Calculate all constants in the following tests
// TODO: Check LP token, coin balances
// TODO: Modularize common code
// TODO: Check Transfer events
serai_test!(
create_pool: (|serai: Serai| async move {
let block = serai.block_by_number(0).await.unwrap().unwrap().hash();
let events = serai.as_of(block).dex().all_events().await.unwrap();
assert_eq!(
events,
vec![
DexEvent::PoolCreated {
pool_id: (Coin::Serai, Coin::Bitcoin),
pool_account: PublicKey::from_raw(blake2_256(&(Coin::Serai, Coin::Bitcoin).encode())),
lp_token: 0,
},
DexEvent::PoolCreated {
pool_id: (Coin::Serai, Coin::Ether),
pool_account: PublicKey::from_raw(blake2_256(&(Coin::Serai, Coin::Ether).encode())),
lp_token: 1,
},
DexEvent::PoolCreated {
pool_id: (Coin::Serai, Coin::Dai),
pool_account: PublicKey::from_raw(blake2_256(&(Coin::Serai, Coin::Dai).encode())),
lp_token: 2,
},
DexEvent::PoolCreated {
pool_id: (Coin::Serai, Coin::Monero),
pool_account: PublicKey::from_raw(blake2_256(&(Coin::Serai, Coin::Monero).encode())),
lp_token: 3,
},
]
);
})
add_liquidity: (|serai: Serai| async move {
let coin = Coin::Monero;
let pair = insecure_pair_from_name("Ferdie");
// mint sriXMR in the account so that we can add liq.
// Ferdie account is already pre-funded with SRI.
mint_coin(
&serai,
Balance { coin, amount: Amount(100_000_000_000_000) },
NetworkId::Monero,
0,
pair.clone().public().into(),
)
.await;
// add liquidity
let coin_amount = Amount(50_000_000_000_000);
let sri_amount = Amount(50_000_000_000_000);
let block = common_add_liquidity(&serai,
coin,
coin_amount,
sri_amount,
0,
pair.clone()
).await;
// get only the add liq events
let mut events = serai.as_of(block).dex().all_events().await.unwrap();
events.retain(|e| matches!(e, DexEvent::LiquidityAdded { .. }));
assert_eq!(
events,
vec![DexEvent::LiquidityAdded {
who: pair.public(),
mint_to: pair.public(),
pool_id: (Coin::Serai, Coin::Monero),
amount1_provided: coin_amount.0,
amount2_provided: sri_amount.0,
lp_token: 3,
lp_token_minted: 49_999999990000
}]
);
})
// Tests coin -> SRI and SRI -> coin swaps.
swap_coin_to_sri: (|serai: Serai| async move {
let coin = Coin::Ether;
let pair = insecure_pair_from_name("Ferdie");
// mint sriXMR in the account so that we can add liq.
// Ferdie account is already pre-funded with SRI.
mint_coin(
&serai,
Balance { coin, amount: Amount(100_000_000_000_000) },
NetworkId::Ethereum,
0,
pair.clone().public().into(),
)
.await;
// add liquidity
common_add_liquidity(&serai,
coin,
Amount(50_000_000_000_000),
Amount(50_000_000_000_000),
0,
pair.clone()
).await;
// now that we have our liquid pool, swap some coin to SRI.
let mut amount_in = Amount(25_000_000_000_000);
let mut block = common_swap(&serai, coin, Coin::Serai, amount_in, Amount(1), 1, pair.clone())
.await;
// get only the swap events
let mut events = serai.as_of(block).dex().all_events().await.unwrap();
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
let mut path = BoundedVec::try_from(vec![coin, Coin::Serai]).unwrap();
assert_eq!(
events,
vec![DexEvent::SwapExecuted {
who: pair.clone().public(),
send_to: pair.public(),
path,
amount_in: amount_in.0,
amount_out: 16633299966633
}]
);
// now swap some SRI to coin
amount_in = Amount(10_000_000_000_000);
block = common_swap(&serai, Coin::Serai, coin, amount_in, Amount(1), 2, pair.clone()).await;
// get only the swap events
let mut events = serai.as_of(block).dex().all_events().await.unwrap();
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
path = BoundedVec::try_from(vec![Coin::Serai, coin]).unwrap();
assert_eq!(
events,
vec![DexEvent::SwapExecuted {
who: pair.clone().public(),
send_to: pair.public(),
path,
amount_in: amount_in.0,
amount_out: 17254428681101
}]
);
})
swap_coin_to_coin: (|serai: Serai| async move {
let coin1 = Coin::Monero;
let coin2 = Coin::Dai;
let pair = insecure_pair_from_name("Ferdie");
// mint coins
mint_coin(
&serai,
Balance { coin: coin1, amount: Amount(100_000_000_000_000) },
NetworkId::Monero,
0,
pair.clone().public().into(),
)
.await;
mint_coin(
&serai,
Balance { coin: coin2, amount: Amount(100_000_000_000_000) },
NetworkId::Ethereum,
0,
pair.clone().public().into(),
)
.await;
// add liquidity to pools
common_add_liquidity(&serai,
coin1,
Amount(50_000_000_000_000),
Amount(50_000_000_000_000),
0,
pair.clone()
).await;
common_add_liquidity(&serai,
coin2,
Amount(50_000_000_000_000),
Amount(50_000_000_000_000),
1,
pair.clone()
).await;
// swap coin1 -> coin2
let amount_in = Amount(25_000_000_000_000);
let block = common_swap(&serai, coin1, coin2, amount_in, Amount(1), 2, pair.clone()).await;
// get only the swap events
let mut events = serai.as_of(block).dex().all_events().await.unwrap();
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
let path = BoundedVec::try_from(vec![coin1, Coin::Serai, coin2]).unwrap();
assert_eq!(
events,
vec![DexEvent::SwapExecuted {
who: pair.clone().public(),
send_to: pair.public(),
path,
amount_in: amount_in.0,
amount_out: 12453103964435,
}]
);
})
add_liquidity_in_instructions: (|serai: Serai| async move {
let coin = Coin::Bitcoin;
let pair = insecure_pair_from_name("Ferdie");
let mut batch_id = 0;
// mint sriBTC in the account so that we can add liq.
// Ferdie account is already pre-funded with SRI.
mint_coin(
&serai,
Balance { coin, amount: Amount(100_000_000_000_000) },
NetworkId::Bitcoin,
batch_id,
pair.clone().public().into(),
)
.await;
batch_id += 1;
// add liquidity
common_add_liquidity(&serai,
coin,
Amount(50_000_000_000_000),
Amount(50_000_000_000_000),
0,
pair.clone()
).await;
// now that we have our liquid SRI/BTC pool, we can add more liquidity to it via an
// InInstruction
let mut block_hash = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block_hash.0);
let batch = Batch {
network: NetworkId::Bitcoin,
id: batch_id,
block: block_hash,
instructions: vec![InInstructionWithBalance {
instruction: InInstruction::Dex(DexCall::SwapAndAddLiquidity(pair.public().into())),
balance: Balance { coin: Coin::Bitcoin, amount: Amount(20_000_000_000_000) },
}],
};
let block = provide_batch(&serai, batch).await;
let mut events = serai.as_of(block).dex().all_events().await.unwrap();
events.retain(|e| matches!(e, DexEvent::LiquidityAdded { .. }));
assert_eq!(
events,
vec![DexEvent::LiquidityAdded {
who: IN_INSTRUCTION_EXECUTOR.into(),
mint_to: pair.public(),
pool_id: (Coin::Serai, Coin::Bitcoin),
amount1_provided: 6_947_918_403_646,
amount2_provided: 10_000_000_000_000, // half of sent amount
lp_token: 0,
lp_token_minted: 8333333333332
}]
);
})
swap_in_instructions: (|serai: Serai| async move {
let coin1 = Coin::Monero;
let coin2 = Coin::Ether;
let pair = insecure_pair_from_name("Ferdie");
let mut coin1_batch_id = 0;
let mut coin2_batch_id = 0;
// mint coins
mint_coin(
&serai,
Balance { coin: coin1, amount: Amount(100_000_000_000_000) },
NetworkId::Monero,
coin1_batch_id,
pair.clone().public().into(),
)
.await;
coin1_batch_id += 1;
mint_coin(
&serai,
Balance { coin: coin2, amount: Amount(100_000_000_000_000) },
NetworkId::Ethereum,
coin2_batch_id,
pair.clone().public().into(),
)
.await;
coin2_batch_id += 1;
// add liquidity to pools
common_add_liquidity(&serai,
coin1,
Amount(50_000_000_000_000),
Amount(50_000_000_000_000),
0,
pair.clone()
).await;
common_add_liquidity(&serai,
coin2,
Amount(50_000_000_000_000),
Amount(50_000_000_000_000),
1,
pair.clone()
).await;
// rand address bytes
let mut rand_bytes = vec![0; 32];
OsRng.fill_bytes(&mut rand_bytes);
// XMR -> ETH
{
// make an out address
let out_address = OutAddress::External(ExternalAddress::new(rand_bytes.clone()).unwrap());
// amount is the min out amount
let out_balance = Balance { coin: coin2, amount: Amount(1) };
// now that we have our pools, we can try to swap
let mut block_hash = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block_hash.0);
let batch = Batch {
network: NetworkId::Monero,
id: coin1_batch_id,
block: block_hash,
instructions: vec![InInstructionWithBalance {
instruction: InInstruction::Dex(DexCall::Swap(out_balance, out_address)),
balance: Balance { coin: coin1, amount: Amount(20_000_000_000_000) },
}],
};
let block = provide_batch(&serai, batch).await;
coin1_batch_id += 1;
let mut events = serai.as_of(block).dex().all_events().await.unwrap();
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
let path = BoundedVec::try_from(vec![coin1, Coin::Serai, coin2]).unwrap();
assert_eq!(
events,
vec![DexEvent::SwapExecuted {
who: IN_INSTRUCTION_EXECUTOR.into(),
send_to: IN_INSTRUCTION_EXECUTOR.into(),
path,
amount_in: 20_000_000_000_000,
amount_out: 11066655622377
}]
);
}
// ETH -> sriXMR
{
// make an out address
let out_address =
OutAddress::Serai(SeraiAddress::new(rand_bytes.clone().try_into().unwrap()));
// amount is the min out amount
let out_balance = Balance { coin: coin1, amount: Amount(1) };
// now that we have our pools, we can try to swap
let mut block_hash = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block_hash.0);
let batch = Batch {
network: NetworkId::Ethereum,
id: coin2_batch_id,
block: block_hash,
instructions: vec![InInstructionWithBalance {
instruction: InInstruction::Dex(DexCall::Swap(out_balance, out_address.clone())),
balance: Balance { coin: coin2, amount: Amount(20_000_000_000_000) },
}],
};
let block = provide_batch(&serai, batch).await;
let mut events = serai.as_of(block).dex().all_events().await.unwrap();
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
let path = BoundedVec::try_from(vec![coin2, Coin::Serai, coin1]).unwrap();
assert_eq!(
events,
vec![DexEvent::SwapExecuted {
who: IN_INSTRUCTION_EXECUTOR.into(),
send_to: out_address.as_native().unwrap().into(),
path,
amount_in: 20_000_000_000_000,
amount_out: 26440798801319
}]
);
}
// XMR -> SRI
{
// make an out address
let out_address = OutAddress::Serai(SeraiAddress::new(rand_bytes.try_into().unwrap()));
// amount is the min out amount
let out_balance = Balance { coin: Coin::Serai, amount: Amount(1) };
// now that we have our pools, we can try to swap
let mut block_hash = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block_hash.0);
let batch = Batch {
network: NetworkId::Monero,
id: coin1_batch_id,
block: block_hash,
instructions: vec![InInstructionWithBalance {
instruction: InInstruction::Dex(DexCall::Swap(out_balance, out_address.clone())),
balance: Balance { coin: coin1, amount: Amount(10_000_000_000_000) },
}],
};
let block = provide_batch(&serai, batch).await;
let mut events = serai.as_of(block).dex().all_events().await.unwrap();
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
let path = BoundedVec::try_from(vec![coin1, Coin::Serai]).unwrap();
assert_eq!(
events,
vec![DexEvent::SwapExecuted {
who: IN_INSTRUCTION_EXECUTOR.into(),
send_to: out_address.as_native().unwrap().into(),
path,
amount_in: 10_000_000_000_000,
amount_out: 10711005507065
}]
);
}
})
);

View file

@ -24,6 +24,8 @@ sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features
pallet-transaction-payment = { git = "https://github.com/serai-dex/substrate", default-features = false }
dex-primitives = { package = "serai-dex-primitives", path = "../../dex/primitives", default-features = false }
serai-primitives = { path = "../../primitives", default-features = false }
coins-primitives = { package = "serai-coins-primitives", path = "../primitives", default-features = false }
@ -38,6 +40,8 @@ std = [
"pallet-transaction-payment/std",
"dex-primitives/std",
"serai-primitives/std",
"coins-primitives/std",
]

View file

@ -14,12 +14,14 @@ pub mod pallet {
use pallet_transaction_payment::{Config as TpConfig, OnChargeTransaction};
use dex_primitives::{Currency, Coins as CoinsTrait};
use serai_primitives::*;
pub use coins_primitives as primitives;
use primitives::*;
#[pallet::config]
pub trait Config: frame_system::Config<AccountId = Public> + TpConfig {
pub trait Config: frame_system::Config<AccountId = Public> {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
}
@ -72,12 +74,13 @@ pub mod pallet {
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
// initialize the supply of the coins
for c in COINS.iter() {
// TODO: Don't use COINS yet GenesisConfig so we can safely expand COINS
for c in &COINS {
Supply::<T>::set(c, 0);
}
// initialize the genesis accounts
for (account, balance) in self.accounts.iter() {
for (account, balance) in &self.accounts {
Pallet::<T>::mint(*account, *balance).unwrap();
}
}
@ -214,7 +217,75 @@ pub mod pallet {
}
}
impl<T: Config> OnChargeTransaction<T> for Pallet<T> {
impl<T: Config> Currency<T::AccountId> for Pallet<T> {
type Balance = SubstrateAmount;
fn balance(of: &Public) -> Self::Balance {
Self::balance(*of, Coin::Serai).0
}
/// TODO: make sure of coin precision here.
fn minimum_balance() -> Self::Balance {
1
}
fn transfer(
from: &Public,
to: &Public,
amount: Self::Balance,
) -> Result<Self::Balance, DispatchError> {
let balance = Balance { coin: Coin::Serai, amount: Amount(amount) };
Self::transfer_internal(*from, *to, balance)?;
Ok(amount)
}
fn mint(to: &Public, amount: Self::Balance) -> Result<Self::Balance, DispatchError> {
Self::mint(*to, Balance { coin: Coin::native(), amount: Amount(amount) })?;
Ok(amount)
}
}
// TODO: Have DEX implement for Coins, not Coins implement for Coins
impl<T: Config> CoinsTrait<T::AccountId> for Pallet<T> {
type Balance = SubstrateAmount;
type CoinId = Coin;
// TODO: Swap the order of these arguments
fn balance(coin: Self::CoinId, of: &Public) -> Self::Balance {
Self::balance(*of, coin).0
}
fn minimum_balance(_: Self::CoinId) -> Self::Balance {
1
}
// TODO: Move coin next to amount
fn transfer(
coin: Self::CoinId,
from: &Public,
to: &Public,
amount: Self::Balance,
) -> Result<Self::Balance, DispatchError> {
let balance = Balance { coin, amount: Amount(amount) };
Self::transfer_internal(*from, *to, balance)?;
Ok(amount)
}
// TODO: Move coin next to amount
fn mint(
coin: Self::CoinId,
to: &Public,
amount: Self::Balance,
) -> Result<Self::Balance, DispatchError> {
Self::mint(*to, Balance { coin, amount: Amount(amount) })?;
Ok(amount)
}
}
impl<T: Config> OnChargeTransaction<T> for Pallet<T>
where
T: TpConfig,
{
type Balance = SubstrateAmount;
type LiquidityInfo = Option<SubstrateAmount>;

View file

@ -12,7 +12,7 @@ use scale_info::TypeInfo;
use serai_primitives::{Balance, SeraiAddress, ExternalAddress, Data, system_address};
pub const FEE_ACCOUNT: SeraiAddress = system_address(b"FeeAccount");
pub const FEE_ACCOUNT: SeraiAddress = system_address(b"Coins-fees");
#[derive(
Clone, PartialEq, Eq, Debug, Serialize, Deserialize, Encode, Decode, MaxEncodedLen, TypeInfo,
@ -46,6 +46,6 @@ fn address() {
use sp_runtime::traits::TrailingZeroInput;
assert_eq!(
FEE_ACCOUNT,
SeraiAddress::decode(&mut TrailingZeroInput::new(b"FeeAccount")).unwrap()
SeraiAddress::decode(&mut TrailingZeroInput::new(b"Coins-fees")).unwrap()
);
}

View file

@ -0,0 +1,75 @@
[package]
name = "serai-dex-pallet"
version = "0.1.0"
description = "DEX pallet for Serai"
license = "AGPL-3.0-only"
repository = "https://github.com/serai-dex/serai/tree/develop/substrate/dex/pallet"
authors = ["Parity Technologies <admin@parity.io>, Akil Demir <aeg_asd@hotmail.com>"]
edition = "2021"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false }
scale-info = { version = "2.5.0", default-features = false, features = ["derive"] }
sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-arithmetic = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-io = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-api = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false }
frame-system = { git = "https://github.com/serai-dex/substrate", default-features = false }
frame-support = { git = "https://github.com/serai-dex/substrate", default-features = false }
frame-benchmarking = { git = "https://github.com/serai-dex/substrate", default-features = false, optional = true }
dex-primitives = { package = "serai-dex-primitives", path = "../primitives", default-features = false }
[dev-dependencies]
serai-primitives = { path = "../../primitives", default-features = false }
coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", default-features = false }
liquidity-tokens-pallet = { package = "serai-liquidity-tokens-pallet", path = "../../liquidity-tokens/pallet", default-features = false }
[features]
default = ["std"]
std = [
"codec/std",
"scale-info/std",
"sp-std/std",
"sp-arithmetic/std",
"sp-io/std",
"sp-api/std",
"sp-runtime/std",
"sp-core/std",
"serai-primitives/std",
"dex-primitives/std",
"frame-system/std",
"frame-support/std",
"frame-benchmarking?/std",
"coins-pallet/std",
"liquidity-tokens-pallet/std",
]
runtime-benchmarks = [
"sp-runtime/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"frame-benchmarking/runtime-benchmarks",
"dex-primitives/runtime-benchmarks",
]
try-runtime = [
"sp-runtime/try-runtime",
"frame-system/try-runtime",
"frame-support/try-runtime",
]

View file

@ -0,0 +1,15 @@
AGPL-3.0-only license
Copyright (c) 2023 Luke Parker
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License Version 3 as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

View file

@ -0,0 +1,211 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
NOTE
Individual files contain the following tag instead of the full license
text.
SPDX-License-Identifier: Apache-2.0
This enables machine processing of license information based on the SPDX
License Identifiers that are here available: http://spdx.org/licenses/

View file

@ -0,0 +1,271 @@
// This file was originally:
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// It has been forked into a crate distributed under the AGPL 3.0.
// Please check the current distribution for up-to-date copyright and licensing information.
//! Dex pallet benchmarking.
use super::*;
use frame_benchmarking::{benchmarks, whitelisted_caller};
use frame_support::{assert_ok, storage::bounded_vec::BoundedVec};
use frame_system::RawOrigin as SystemOrigin;
use sp_runtime::traits::{Bounded, StaticLookup};
use sp_std::{ops::Div, prelude::*};
use crate::Pallet as Dex;
const INITIAL_COIN_BALANCE: u64 = 1_000_000_000;
type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
fn get_lp_token_id<T: Config>() -> T::PoolCoinId
where
T::PoolCoinId: Into<u32>,
{
let next_id: u32 = Dex::<T>::get_next_pool_coin_id().into();
(next_id - 1).into()
}
fn create_coin<T: Config>(coin: &T::MultiCoinId) -> (T::AccountId, AccountIdLookupOf<T>)
where
T::CoinBalance: From<u64>,
T::Currency: Currency<T::AccountId>,
T::Coins: Coins<T::AccountId>,
{
let caller: T::AccountId = whitelisted_caller();
let caller_lookup = T::Lookup::unlookup(caller.clone());
if let MultiCoinIdConversionResult::Converted(coin_id) =
T::MultiCoinIdConverter::try_convert(coin)
{
assert_ok!(T::Currency::mint(&caller, BalanceOf::<T>::max_value().div(1000u32.into())));
assert_ok!(T::Coins::mint(coin_id, &caller, INITIAL_COIN_BALANCE.into()));
}
(caller, caller_lookup)
}
fn create_coin_and_pool<T: Config>(
coin1: &T::MultiCoinId,
coin2: &T::MultiCoinId,
) -> (T::PoolCoinId, T::AccountId, AccountIdLookupOf<T>)
where
T::CoinBalance: From<u64>,
T::Currency: Currency<T::AccountId>,
T::Coins: Coins<T::AccountId>,
T::PoolCoinId: Into<u32>,
{
assert_eq!(coin1, &T::MultiCoinIdConverter::get_native());
let (caller, caller_lookup) = create_coin::<T>(coin2);
assert_ok!(Dex::<T>::create_pool(coin2.clone()));
let lp_token = get_lp_token_id::<T>();
(lp_token, caller, caller_lookup)
}
benchmarks! {
where_clause {
where
T::CoinBalance: From<u64> + Into<u64>,
T::Currency: Currency<T::AccountId>,
T::Balance: From<u64> + Into<u64>,
T::Coins: Coins<T::AccountId>,
T::PoolCoinId: Into<u32>,
}
add_liquidity {
let coin1 = T::MultiCoinIdConverter::get_native();
let coin2: T::MultiCoinId = T::BenchmarkHelper::coin_id(0).into();
let (lp_token, caller, _) = create_coin_and_pool::<T>(&coin1, &coin2);
let ed: u64 = T::Currency::minimum_balance().into();
let add_amount = 1000 + ed;
}: _(
SystemOrigin::Signed(caller.clone()),
coin1.clone(),
coin2.clone(),
add_amount.into(),
1000.into(),
0.into(),
0.into(),
caller.clone()
)
verify {
let pool_id = (coin1.clone(), coin2.clone());
let lp_minted = Dex::<T>::calc_lp_amount_for_zero_supply(
&add_amount.into(),
&1000.into()
).unwrap().into();
assert_eq!(
T::PoolCoins::balance(lp_token, &caller),
lp_minted.into()
);
assert_eq!(
T::Currency::balance(&Dex::<T>::get_pool_account(&pool_id)),
add_amount.into()
);
assert_eq!(
T::Coins::balance(
T::BenchmarkHelper::coin_id(0),
&Dex::<T>::get_pool_account(&pool_id)
),
1000.into()
);
}
remove_liquidity {
let coin1 = T::MultiCoinIdConverter::get_native();
let coin2: T::MultiCoinId = T::BenchmarkHelper::coin_id(0).into();
let (lp_token, caller, _) = create_coin_and_pool::<T>(&coin1, &coin2);
let ed: u64 = T::Currency::minimum_balance().into();
let add_amount = 100 * ed;
let lp_minted = Dex::<T>::calc_lp_amount_for_zero_supply(
&add_amount.into(),
&1000.into()
).unwrap().into();
let remove_lp_amount = lp_minted.checked_div(10).unwrap();
Dex::<T>::add_liquidity(
SystemOrigin::Signed(caller.clone()).into(),
coin1.clone(),
coin2.clone(),
add_amount.into(),
1000.into(),
0.into(),
0.into(),
caller.clone(),
)?;
let total_supply =
<T::PoolCoins as LiquidityTokens<T::AccountId>>::total_issuance(lp_token.clone());
}: _(
SystemOrigin::Signed(caller.clone()),
coin1,
coin2,
remove_lp_amount.into(),
0.into(),
0.into(),
caller.clone()
)
verify {
let new_total_supply =
<T::PoolCoins as LiquidityTokens<T::AccountId>>::total_issuance(lp_token.clone());
assert_eq!(
new_total_supply,
total_supply - remove_lp_amount.into()
);
}
swap_exact_tokens_for_tokens {
let native = T::MultiCoinIdConverter::get_native();
let coin1: T::MultiCoinId = T::BenchmarkHelper::coin_id(1).into();
let coin2: T::MultiCoinId = T::BenchmarkHelper::coin_id(2).into();
let (_, caller, _) = create_coin_and_pool::<T>(&native, &coin1);
let (_, _) = create_coin::<T>(&coin2);
let ed: u64 = T::Currency::minimum_balance().into();
let ed_bump = 2u64;
Dex::<T>::add_liquidity(
SystemOrigin::Signed(caller.clone()).into(),
native.clone(),
coin1.clone(),
// TODO: this call otherwise fails with `InsufficientLiquidityMinted`.
// might be again related to their expectance on ed being > 1.
(100 * (ed + ed_bump)).into(),
200.into(),
0.into(),
0.into(),
caller.clone(),
)?;
let swap_amount = 100.into();
// since we only allow the native-coin pools, then the worst case scenario would be to swap
// coin1-native-coin2
Dex::<T>::create_pool(coin2.clone())?;
Dex::<T>::add_liquidity(
SystemOrigin::Signed(caller.clone()).into(),
native.clone(),
coin2.clone(),
(500 * ed).into(),
1000.into(),
0.into(),
0.into(),
caller.clone(),
)?;
let path = vec![coin1.clone(), native.clone(), coin2.clone()];
let path = BoundedVec::<_, T::MaxSwapPathLength>::try_from(path).unwrap();
let native_balance = T::Currency::balance(&caller);
let coin1_balance = T::Coins::balance(T::BenchmarkHelper::coin_id(1), &caller);
}: _(SystemOrigin::Signed(caller.clone()), path, swap_amount, 1.into(), caller.clone())
verify {
let ed_bump = 2u64;
let new_coin1_balance = T::Coins::balance(T::BenchmarkHelper::coin_id(1), &caller);
assert_eq!(new_coin1_balance, coin1_balance - 100.into());
}
swap_tokens_for_exact_tokens {
let native = T::MultiCoinIdConverter::get_native();
let coin1: T::MultiCoinId = T::BenchmarkHelper::coin_id(1).into();
let coin2: T::MultiCoinId = T::BenchmarkHelper::coin_id(2).into();
let (_, caller, _) = create_coin_and_pool::<T>(&native, &coin1);
let (_, _) = create_coin::<T>(&coin2);
let ed: u64 = T::Currency::minimum_balance().into();
Dex::<T>::add_liquidity(
SystemOrigin::Signed(caller.clone()).into(),
native.clone(),
coin1.clone(),
(1000 * ed).into(),
500.into(),
0.into(),
0.into(),
caller.clone(),
)?;
// since we only allow the native-coin pools, then the worst case scenario would be to swap
// coin1-native-coin2
Dex::<T>::create_pool(coin2.clone())?;
Dex::<T>::add_liquidity(
SystemOrigin::Signed(caller.clone()).into(),
native.clone(),
coin2.clone(),
(500 * ed).into(),
1000.into(),
0.into(),
0.into(),
caller.clone(),
)?;
let path = vec![coin1.clone(), native.clone(), coin2.clone()];
let path: BoundedVec<_, T::MaxSwapPathLength> = BoundedVec::try_from(path).unwrap();
let coin2_balance = T::Coins::balance(T::BenchmarkHelper::coin_id(2), &caller);
}: _(
SystemOrigin::Signed(caller.clone()),
path.clone(),
100.into(),
(1000 * ed).into(),
caller.clone()
)
verify {
let new_coin2_balance = T::Coins::balance(T::BenchmarkHelper::coin_id(2), &caller);
assert_eq!(new_coin2_balance, coin2_balance + 100.into());
}
impl_benchmark_test_suite!(Dex, crate::mock::new_test_ext(), crate::mock::Test);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,155 @@
// This file was originally:
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// It has been forked into a crate distributed under the AGPL 3.0.
// Please check the current distribution for up-to-date copyright and licensing information.
//! Test environment for Dex pallet.
use super::*;
use crate as dex;
use frame_support::{
construct_runtime,
traits::{ConstU32, ConstU64},
};
use sp_core::{H256, sr25519::Public};
use sp_runtime::{
traits::{BlakeTwo256, IdentityLookup},
BuildStorage,
};
use serai_primitives::{Coin, Balance, Amount, system_address};
pub use coins_pallet as coins;
pub use liquidity_tokens_pallet as liquidity_tokens;
type Block = frame_system::mocking::MockBlock<Test>;
construct_runtime!(
pub enum Test
{
System: frame_system,
CoinsPallet: coins,
LiquidityTokens: liquidity_tokens,
Dex: dex,
}
);
impl frame_system::Config for Test {
type BaseCallFilter = frame_support::traits::Everything;
type BlockWeights = ();
type BlockLength = ();
type RuntimeOrigin = RuntimeOrigin;
type RuntimeCall = RuntimeCall;
type Nonce = u64;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = Public;
type Lookup = IdentityLookup<Self::AccountId>;
type Block = Block;
type RuntimeEvent = RuntimeEvent;
type BlockHashCount = ConstU64<250>;
type DbWeight = ();
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = ();
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = ();
type OnSetCode = ();
type MaxConsumers = ConstU32<16>;
}
impl coins::Config for Test {
type RuntimeEvent = RuntimeEvent;
}
impl liquidity_tokens::Config for Test {
type RuntimeEvent = RuntimeEvent;
}
pub struct CoinConverter;
impl MultiCoinIdConverter<Coin, Coin> for CoinConverter {
/// Returns the MultiCoinId representing the native currency of the chain.
fn get_native() -> Coin {
Coin::Serai
}
/// Returns true if the given MultiCoinId is the native currency.
fn is_native(coin: &Coin) -> bool {
coin.is_native()
}
/// If it's not native, returns the CoinId for the given MultiCoinId.
fn try_convert(coin: &Coin) -> MultiCoinIdConversionResult<Coin, Coin> {
if coin.is_native() {
MultiCoinIdConversionResult::Native
} else {
MultiCoinIdConversionResult::Converted(*coin)
}
}
}
impl Config for Test {
type RuntimeEvent = RuntimeEvent;
type Currency = CoinsPallet;
type CoinBalance = u64;
type CoinId = Coin;
type PoolCoinId = u32;
type Coins = CoinsPallet;
type PoolCoins = LiquidityTokens;
type WeightInfo = ();
type LPFee = ConstU32<3>; // means 0.3%
type MaxSwapPathLength = ConstU32<4>;
// 100 is good enough when the main currency has 12 decimals.
type MintMinLiquidity = ConstU64<100>;
type Balance = u64;
type HigherPrecisionBalance = u128;
type MultiCoinId = Coin;
type MultiCoinIdConverter = CoinConverter;
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper = ();
}
pub(crate) fn new_test_ext() -> sp_io::TestExternalities {
let mut t = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
let accounts: Vec<Public> = vec![
system_address(b"account1").into(),
system_address(b"account2").into(),
system_address(b"account3").into(),
system_address(b"account4").into(),
];
coins::GenesisConfig::<Test> {
accounts: accounts
.into_iter()
.map(|a| (a, Balance { coin: Coin::Serai, amount: Amount(1 << 60) }))
.collect(),
}
.assimilate_storage(&mut t)
.unwrap();
let mut ext = sp_io::TestExternalities::new(t);
ext.execute_with(|| System::set_block_number(1));
ext
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,259 @@
// This file was originally:
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// It has been forked into a crate distributed under the AGPL 3.0.
// Please check the current distribution for up-to-date copyright and licensing information.
//! Autogenerated weights for pallet_coin_conversion
//!
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
//! DATE: 2023-07-18, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! WORST CASE MAP SIZE: `1000000`
//! HOSTNAME: `runner-gghbxkbs-project-145-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz`
//! EXECUTION: ``, WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024`
// Executed Command:
// target/production/substrate
// benchmark
// pallet
// --steps=50
// --repeat=20
// --extrinsic=*
// --wasm-execution=compiled
// --heap-pages=4096
// --json-file=/builds/parity/mirrors/substrate/.git/.artifacts/bench.json
// --pallet=pallet_coin_conversion
// --chain=dev
// --header=./HEADER-APACHE2
// --output=./frame/coin-conversion/src/weights.rs
// --template=./.maintain/frame-weight-template.hbs
#![cfg_attr(rustfmt, rustfmt_skip)]
#![allow(unused_parens)]
#![allow(unused_imports)]
#![allow(missing_docs)]
use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
use core::marker::PhantomData;
/// Weight functions needed for pallet_coin_conversion.
pub trait WeightInfo {
fn create_pool() -> Weight;
fn add_liquidity() -> Weight;
fn remove_liquidity() -> Weight;
fn swap_exact_tokens_for_tokens() -> Weight;
fn swap_tokens_for_exact_tokens() -> Weight;
}
/// Weights for pallet_coin_conversion using the Substrate node and recommended hardware.
pub struct SubstrateWeight<T>(PhantomData<T>);
impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
/// Storage: `CoinConversion::Pools` (r:1 w:1)
/// Proof: `CoinConversion::Pools` (`max_values`: None, `max_size`: Some(30), added: 2505, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:2 w:2)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
/// Storage: `Coins::Account` (r:1 w:1)
/// Proof: `Coins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
/// Storage: `Coins::Coin` (r:1 w:1)
/// Proof: `Coins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
/// Storage: `CoinConversion::NextPoolCoinId` (r:1 w:1)
/// Proof: `CoinConversion::NextPoolCoinId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
/// Storage: `PoolCoins::Coin` (r:1 w:1)
/// Proof: `PoolCoins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
/// Storage: `PoolCoins::Account` (r:1 w:1)
/// Proof: `PoolCoins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
fn create_pool() -> Weight {
// Proof Size summary in bytes:
// Measured: `729`
// Estimated: `6196`
// Minimum execution time: 131_688_000 picoseconds.
Weight::from_parts(134_092_000, 6196)
.saturating_add(T::DbWeight::get().reads(8_u64))
.saturating_add(T::DbWeight::get().writes(8_u64))
}
/// Storage: `CoinConversion::Pools` (r:1 w:0)
/// Proof: `CoinConversion::Pools` (`max_values`: None, `max_size`: Some(30), added: 2505, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:1 w:1)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
/// Storage: `Coins::Coin` (r:1 w:1)
/// Proof: `Coins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
/// Storage: `Coins::Account` (r:2 w:2)
/// Proof: `Coins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
/// Storage: `PoolCoins::Coin` (r:1 w:1)
/// Proof: `PoolCoins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
/// Storage: `PoolCoins::Account` (r:2 w:2)
/// Proof: `PoolCoins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
fn add_liquidity() -> Weight {
// Proof Size summary in bytes:
// Measured: `1382`
// Estimated: `6208`
// Minimum execution time: 157_310_000 picoseconds.
Weight::from_parts(161_547_000, 6208)
.saturating_add(T::DbWeight::get().reads(8_u64))
.saturating_add(T::DbWeight::get().writes(7_u64))
}
/// Storage: `CoinConversion::Pools` (r:1 w:0)
/// Proof: `CoinConversion::Pools` (`max_values`: None, `max_size`: Some(30), added: 2505, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:1 w:1)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
/// Storage: `Coins::Coin` (r:1 w:1)
/// Proof: `Coins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
/// Storage: `Coins::Account` (r:2 w:2)
/// Proof: `Coins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
/// Storage: `PoolCoins::Coin` (r:1 w:1)
/// Proof: `PoolCoins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
/// Storage: `PoolCoins::Account` (r:1 w:1)
/// Proof: `PoolCoins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
fn remove_liquidity() -> Weight {
// Proof Size summary in bytes:
// Measured: `1371`
// Estimated: `6208`
// Minimum execution time: 142_769_000 picoseconds.
Weight::from_parts(145_139_000, 6208)
.saturating_add(T::DbWeight::get().reads(7_u64))
.saturating_add(T::DbWeight::get().writes(6_u64))
}
/// Storage: `System::Account` (r:1 w:1)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
/// Storage: `Coins::Coin` (r:3 w:3)
/// Proof: `Coins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
/// Storage: `Coins::Account` (r:6 w:6)
/// Proof: `Coins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
fn swap_exact_tokens_for_tokens() -> Weight {
// Proof Size summary in bytes:
// Measured: `1738`
// Estimated: `16644`
// Minimum execution time: 213_186_000 picoseconds.
Weight::from_parts(217_471_000, 16644)
.saturating_add(T::DbWeight::get().reads(10_u64))
.saturating_add(T::DbWeight::get().writes(10_u64))
}
/// Storage: `Coins::Coin` (r:3 w:3)
/// Proof: `Coins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
/// Storage: `Coins::Account` (r:6 w:6)
/// Proof: `Coins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:1 w:1)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
fn swap_tokens_for_exact_tokens() -> Weight {
// Proof Size summary in bytes:
// Measured: `1738`
// Estimated: `16644`
// Minimum execution time: 213_793_000 picoseconds.
Weight::from_parts(218_584_000, 16644)
.saturating_add(T::DbWeight::get().reads(10_u64))
.saturating_add(T::DbWeight::get().writes(10_u64))
}
}
// For backwards compatibility and tests.
impl WeightInfo for () {
/// Storage: `CoinConversion::Pools` (r:1 w:1)
/// Proof: `CoinConversion::Pools` (`max_values`: None, `max_size`: Some(30), added: 2505, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:2 w:2)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
/// Storage: `Coins::Account` (r:1 w:1)
/// Proof: `Coins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
/// Storage: `Coins::Coin` (r:1 w:1)
/// Proof: `Coins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
/// Storage: `CoinConversion::NextPoolCoinId` (r:1 w:1)
/// Proof: `CoinConversion::NextPoolCoinId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
/// Storage: `PoolCoins::Coin` (r:1 w:1)
/// Proof: `PoolCoins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
/// Storage: `PoolCoins::Account` (r:1 w:1)
/// Proof: `PoolCoins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
fn create_pool() -> Weight {
// Proof Size summary in bytes:
// Measured: `729`
// Estimated: `6196`
// Minimum execution time: 131_688_000 picoseconds.
Weight::from_parts(134_092_000, 6196)
.saturating_add(RocksDbWeight::get().reads(8_u64))
.saturating_add(RocksDbWeight::get().writes(8_u64))
}
/// Storage: `CoinConversion::Pools` (r:1 w:0)
/// Proof: `CoinConversion::Pools` (`max_values`: None, `max_size`: Some(30), added: 2505, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:1 w:1)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
/// Storage: `Coins::Coin` (r:1 w:1)
/// Proof: `Coins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
/// Storage: `Coins::Account` (r:2 w:2)
/// Proof: `Coins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
/// Storage: `PoolCoins::Coin` (r:1 w:1)
/// Proof: `PoolCoins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
/// Storage: `PoolCoins::Account` (r:2 w:2)
/// Proof: `PoolCoins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
fn add_liquidity() -> Weight {
// Proof Size summary in bytes:
// Measured: `1382`
// Estimated: `6208`
// Minimum execution time: 157_310_000 picoseconds.
Weight::from_parts(161_547_000, 6208)
.saturating_add(RocksDbWeight::get().reads(8_u64))
.saturating_add(RocksDbWeight::get().writes(7_u64))
}
/// Storage: `CoinConversion::Pools` (r:1 w:0)
/// Proof: `CoinConversion::Pools` (`max_values`: None, `max_size`: Some(30), added: 2505, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:1 w:1)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
/// Storage: `Coins::Coin` (r:1 w:1)
/// Proof: `Coins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
/// Storage: `Coins::Account` (r:2 w:2)
/// Proof: `Coins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
/// Storage: `PoolCoins::Coin` (r:1 w:1)
/// Proof: `PoolCoins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
/// Storage: `PoolCoins::Account` (r:1 w:1)
/// Proof: `PoolCoins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
fn remove_liquidity() -> Weight {
// Proof Size summary in bytes:
// Measured: `1371`
// Estimated: `6208`
// Minimum execution time: 142_769_000 picoseconds.
Weight::from_parts(145_139_000, 6208)
.saturating_add(RocksDbWeight::get().reads(7_u64))
.saturating_add(RocksDbWeight::get().writes(6_u64))
}
/// Storage: `System::Account` (r:1 w:1)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
/// Storage: `Coins::Coin` (r:3 w:3)
/// Proof: `Coins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
/// Storage: `Coins::Account` (r:6 w:6)
/// Proof: `Coins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
fn swap_exact_tokens_for_tokens() -> Weight {
// Proof Size summary in bytes:
// Measured: `1738`
// Estimated: `16644`
// Minimum execution time: 213_186_000 picoseconds.
Weight::from_parts(217_471_000, 16644)
.saturating_add(RocksDbWeight::get().reads(10_u64))
.saturating_add(RocksDbWeight::get().writes(10_u64))
}
/// Storage: `Coins::Coin` (r:3 w:3)
/// Proof: `Coins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
/// Storage: `Coins::Account` (r:6 w:6)
/// Proof: `Coins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:1 w:1)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
fn swap_tokens_for_exact_tokens() -> Weight {
// Proof Size summary in bytes:
// Measured: `1738`
// Estimated: `16644`
// Minimum execution time: 213_793_000 picoseconds.
Weight::from_parts(218_584_000, 16644)
.saturating_add(RocksDbWeight::get().reads(10_u64))
.saturating_add(RocksDbWeight::get().writes(10_u64))
}
}

View file

@ -0,0 +1,44 @@
[package]
name = "serai-dex-primitives"
version = "0.1.0"
description = "Dex pallet primitives"
license = "AGPL-3.0-only"
repository = "https://github.com/serai-dex/serai/tree/develop/substrate/dex/primitives"
authors = ["Parity Technologies <admin@parity.io>, Akil Demir <aeg_asd@hotmail.com>"]
edition = "2021"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false }
scale-info = { version = "2.5.0", default-features = false, features = ["derive"] }
frame-support = { git = "https://github.com/serai-dex/substrate", default-features = false }
frame-benchmarking = { git = "https://github.com/serai-dex/substrate", default-features = false, optional = true }
sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false }
serai-primitives = { path = "../../primitives", default-features = false }
[features]
default = [ "std" ]
std = [
"codec/std",
"scale-info/std",
"frame-support/std",
"frame-benchmarking?/std",
"serai-primitives/std",
"sp-runtime/std",
"sp-std/std",
]
runtime-benchmarks = [
"frame-benchmarking/runtime-benchmarks",
"frame-support/runtime-benchmarks",
]

View file

@ -0,0 +1,15 @@
AGPL-3.0-only license
Copyright (c) 2023 Luke Parker
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License Version 3 as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

View file

@ -0,0 +1,211 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
NOTE
Individual files contain the following tag instead of the full license
text.
SPDX-License-Identifier: Apache-2.0
This enables machine processing of license information based on the SPDX
License Identifiers that are here available: http://spdx.org/licenses/

View file

@ -0,0 +1,221 @@
// This file was originally:
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// It has been forked into a crate distributed under the AGPL 3.0.
// Please check the current distribution for up-to-date copyright and licensing information.
#![cfg_attr(not(feature = "std"), no_std)]
use codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use sp_runtime::DispatchError;
use sp_std::vec::Vec;
use frame_support::traits::tokens::{Balance, AssetId as CoinId};
use serai_primitives::Coin;
/// Stores the lp_token coin id a particular pool has been assigned.
#[derive(Decode, Encode, Default, PartialEq, Eq, MaxEncodedLen, TypeInfo)]
pub struct PoolInfo<PoolCoinId> {
/// Liquidity pool coin
pub lp_token: PoolCoinId,
}
/// A trait that converts between a MultiCoinId and either the native currency or an CoinId.
pub trait MultiCoinIdConverter<MultiCoinId, CoinId> {
/// Returns the MultiCoinId representing the native currency of the chain.
fn get_native() -> MultiCoinId;
/// Returns true if the given MultiCoinId is the native currency.
fn is_native(coin: &MultiCoinId) -> bool;
/// If it's not native, returns the CoinId for the given MultiCoinId.
fn try_convert(coin: &MultiCoinId) -> MultiCoinIdConversionResult<MultiCoinId, CoinId>;
}
/// Result of `MultiCoinIdConverter::try_convert`.
#[cfg_attr(feature = "std", derive(PartialEq, Debug))]
pub enum MultiCoinIdConversionResult<MultiCoinId, CoinId> {
/// Input coin is successfully converted. Means that converted coin is supported.
Converted(CoinId),
/// Means that input coin is the chain's native coin, if it has one, so no conversion (see
/// `MultiCoinIdConverter::get_native`).
Native,
/// Means input coin is not supported for pool.
Unsupported(MultiCoinId),
}
/// Benchmark Helper
#[cfg(feature = "runtime-benchmarks")]
pub trait BenchmarkHelper<CoinId, MultiCoinId> {
/// Returns an `CoinId` from a given integer.
fn coin_id(coin_id: u32) -> CoinId;
}
#[cfg(feature = "runtime-benchmarks")]
mod runtime_benchmarks {
use super::*;
use serai_primitives::COINS;
impl BenchmarkHelper<Coin, Coin> for () {
fn coin_id(coin_id: u32) -> Coin {
// we shift id 1 unit to the left, since id 0 is the native coin.
COINS[(usize::try_from(coin_id).unwrap() % COINS.len()) + 1]
}
}
}
/// Trait for providing methods to swap between the various coin classes.
pub trait Swap<AccountId, Balance, MultiCoinId> {
/// Swap exactly `amount_in` of coin `path[0]` for coin `path[1]`.
/// If an `amount_out_min` is specified, it will return an error if it is unable to acquire
/// the amount desired.
///
/// Withdraws the `path[0]` coin from `sender`, deposits the `path[1]` coin to `send_to`,
///
/// If successful, returns the amount of `path[1]` acquired for the `amount_in`.
fn swap_exact_tokens_for_tokens(
sender: AccountId,
path: Vec<MultiCoinId>,
amount_in: Balance,
amount_out_min: Option<Balance>,
send_to: AccountId,
) -> Result<Balance, DispatchError>;
/// Take the `path[0]` coin and swap some amount for `amount_out` of the `path[1]`. If an
/// `amount_in_max` is specified, it will return an error if acquiring `amount_out` would be
/// too costly.
///
/// Withdraws `path[0]` coin from `sender`, deposits `path[1]` coin to `send_to`,
///
/// If successful returns the amount of the `path[0]` taken to provide `path[1]`.
fn swap_tokens_for_exact_tokens(
sender: AccountId,
path: Vec<MultiCoinId>,
amount_out: Balance,
amount_in_max: Option<Balance>,
send_to: AccountId,
) -> Result<Balance, DispatchError>;
}
// TODO: Sized should be there?
/// Native coin trait for Dex pallet.
pub trait Currency<AccountId>: Sized {
/// Balance of an Account.
type Balance: Balance;
/// Returns the balance of an account.
fn balance(of: &AccountId) -> Self::Balance;
/// Returns the minimum allowed balance of an account
fn minimum_balance() -> Self::Balance;
/// Transfers the given `amount` from `from` to `to`.
fn transfer(
from: &AccountId,
to: &AccountId,
amount: Self::Balance,
) -> Result<Self::Balance, DispatchError>;
/// mints the given `amount` into `to`.
fn mint(to: &AccountId, amount: Self::Balance) -> Result<Self::Balance, DispatchError>;
}
/// External coin trait for Dex pallet.
pub trait Coins<AccountId>: Sized {
/// Balance of an Account.
type Balance: Balance;
/// Coin identifier.
type CoinId: CoinId;
/// Returns the balance of an account.
fn balance(coin: Self::CoinId, of: &AccountId) -> Self::Balance;
/// Returns the minimum allowed balance of an account
fn minimum_balance(coin: Self::CoinId) -> Self::Balance;
/// Transfers the given `amount` from `from` to `to`.
fn transfer(
coin: Self::CoinId,
from: &AccountId,
to: &AccountId,
amount: Self::Balance,
) -> Result<Self::Balance, DispatchError>;
/// mints the given `amount` of `coin` into `to`.
fn mint(
coin: Self::CoinId,
to: &AccountId,
amount: Self::Balance,
) -> Result<Self::Balance, DispatchError>;
}
/// Liquidity tokens trait for Dex pallet.
pub trait LiquidityTokens<AccountId>: Sized {
/// Amount type.
type Balance: Balance;
/// Coin identifier.
type CoinId: CoinId;
/// Returns the `token` balance of and account.
fn balance(token: Self::CoinId, of: &AccountId) -> Self::Balance;
/// Mints `amount` to `to`.
fn mint_into(
token: Self::CoinId,
to: &AccountId,
amount: Self::Balance,
) -> Result<Self::Balance, DispatchError>;
/// Burns `amount` from `from`.
fn burn_from(
token: Self::CoinId,
from: &AccountId,
amount: Self::Balance,
) -> Result<Self::Balance, DispatchError>;
/// Returns total supply for `token`.
fn total_issuance(token: Self::CoinId) -> Self::Balance;
/// Returns an iterator of the collections in existence.
fn coin_ids() -> Vec<Self::CoinId>;
}
pub struct CoinConverter;
impl MultiCoinIdConverter<Coin, Coin> for CoinConverter {
/// Returns the MultiCoinId representing the native currency of the chain.
fn get_native() -> Coin {
Coin::native()
}
/// Returns true if the given MultiCoinId is the native currency.
fn is_native(coin: &Coin) -> bool {
coin.is_native()
}
/// If it's not native, returns the CoinId for the given MultiCoinId.
fn try_convert(coin: &Coin) -> MultiCoinIdConversionResult<Coin, Coin> {
if coin.is_native() {
MultiCoinIdConversionResult::Native
} else {
MultiCoinIdConversionResult::Converted(*coin)
}
}
}

View file

@ -17,10 +17,11 @@ thiserror = { version = "1", optional = true }
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive", "max-encoded-len"] }
scale-info = { version = "2", default-features = false, features = ["derive"] }
sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-io = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-application-crypto = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-io = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false }
frame-system = { git = "https://github.com/serai-dex/substrate", default-features = false }
frame-support = { git = "https://github.com/serai-dex/substrate", default-features = false }
@ -29,6 +30,7 @@ serai-primitives = { path = "../../primitives", default-features = false }
in-instructions-primitives = { package = "serai-in-instructions-primitives", path = "../primitives", default-features = false }
coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", default-features = false }
dex-pallet = { package = "serai-dex-pallet", path = "../../dex/pallet", default-features = false }
validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../../validator-sets/pallet", default-features = false }
[features]
@ -38,10 +40,11 @@ std = [
"scale/std",
"scale-info/std",
"sp-core/std",
"sp-io/std",
"sp-std/std",
"sp-application-crypto/std",
"sp-io/std",
"sp-runtime/std",
"sp-core/std",
"frame-system/std",
"frame-support/std",
@ -50,6 +53,7 @@ std = [
"in-instructions-primitives/std",
"coins-pallet/std",
"dex-pallet/std",
"validator-sets-pallet/std",
]
default = ["std"]

View file

@ -23,14 +23,21 @@ pub enum PalletError {
#[frame_support::pallet]
pub mod pallet {
use sp_std::vec;
use sp_application_crypto::RuntimePublic;
use sp_runtime::traits::Zero;
use sp_core::sr25519::Public;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
use serai_primitives::{Coin, SubstrateAmount, Amount, Balance};
use coins_pallet::{Config as CoinsConfig, Pallet as Coins};
use frame_support::pallet_prelude::*;
use frame_system::{pallet_prelude::*, RawOrigin};
use coins_pallet::{
Config as CoinsConfig, Pallet as Coins,
primitives::{OutInstruction, OutInstructionWithBalance},
};
use dex_pallet::{Config as DexConfig, Pallet as Dex};
use validator_sets_pallet::{
primitives::{Session, ValidatorSet},
Config as ValidatorSetsConfig, Pallet as ValidatorSets,
@ -39,7 +46,12 @@ pub mod pallet {
use super::*;
#[pallet::config]
pub trait Config: frame_system::Config + ValidatorSetsConfig + CoinsConfig {
pub trait Config:
frame_system::Config
+ CoinsConfig
+ DexConfig<MultiCoinId = Coin, CoinBalance = SubstrateAmount>
+ ValidatorSetsConfig
{
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
}
@ -50,6 +62,12 @@ pub mod pallet {
InstructionFailure { network: NetworkId, id: u32, index: u32 },
}
#[pallet::error]
pub enum Error<T> {
/// Coin and OutAddress types don't match.
InvalidAddressForCoin,
}
#[pallet::pallet]
pub struct Pallet<T>(PhantomData<T>);
@ -79,7 +97,120 @@ pub mod pallet {
InInstruction::Transfer(address) => {
Coins::<T>::mint(address.into(), instruction.balance)?;
}
_ => panic!("unsupported instruction"),
InInstruction::Dex(call) => {
// This will only be initiated by external chain transactions. That is why we only need
// add liquidity and swaps. Other functionalities (such as remove_liq, etc) will be
// called directly from Serai with a native transaction.
match call {
DexCall::SwapAndAddLiquidity(address) => {
let origin = RawOrigin::Signed(IN_INSTRUCTION_EXECUTOR.into());
let coin = instruction.balance.coin;
// mint the given coin on the account
Coins::<T>::mint(IN_INSTRUCTION_EXECUTOR.into(), instruction.balance)?;
// swap half of it for SRI
let half = instruction.balance.amount.0 / 2;
let path = BoundedVec::try_from(vec![coin, Coin::Serai]).unwrap();
Dex::<T>::swap_exact_tokens_for_tokens(
origin.clone().into(),
path,
half,
1, // minimum out, so we accept whatever we get.
IN_INSTRUCTION_EXECUTOR.into(),
)?;
// get how much we got for our swap
let sri_amount = Coins::<T>::balance(IN_INSTRUCTION_EXECUTOR.into(), Coin::Serai).0;
// add liquidity
Dex::<T>::add_liquidity(
origin.clone().into(),
coin,
Coin::Serai,
half,
sri_amount,
1,
1,
address.into(),
)?;
// TODO: minimums are set to 1 above to guarantee successful adding liq call.
// Ideally we either get this info from user or send the leftovers back to user.
// Let's send the leftovers back to user for now.
let coin_balance = Coins::<T>::balance(IN_INSTRUCTION_EXECUTOR.into(), coin);
let sri_balance = Coins::<T>::balance(IN_INSTRUCTION_EXECUTOR.into(), Coin::Serai);
if coin_balance != Amount(0) {
Coins::<T>::transfer_internal(
IN_INSTRUCTION_EXECUTOR.into(),
address.into(),
Balance { coin, amount: coin_balance },
)?;
}
if sri_balance != Amount(0) {
Coins::<T>::transfer_internal(
IN_INSTRUCTION_EXECUTOR.into(),
address.into(),
Balance { coin: Coin::Serai, amount: sri_balance },
)?;
}
}
DexCall::Swap(out_balance, out_address) => {
let send_to_external = !out_address.is_native();
let native_coin = out_balance.coin.is_native();
// we can't send native coin to external chain
if native_coin && send_to_external {
Err(Error::<T>::InvalidAddressForCoin)?;
}
// mint the given coin on our account
Coins::<T>::mint(IN_INSTRUCTION_EXECUTOR.into(), instruction.balance)?;
// get the path
let mut path = vec![instruction.balance.coin, Coin::Serai];
if !native_coin {
path.push(out_balance.coin);
}
// get the swap address
// if the address is internal, we can directly swap to it. if not, we swap to
// ourselves and burn the coins to send them back on the external chain.
let send_to = if send_to_external {
IN_INSTRUCTION_EXECUTOR
} else {
out_address.clone().as_native().unwrap()
};
// do the swap
let origin = RawOrigin::Signed(IN_INSTRUCTION_EXECUTOR.into());
Dex::<T>::swap_exact_tokens_for_tokens(
origin.into(),
BoundedVec::try_from(path).unwrap(),
instruction.balance.amount.0,
out_balance.amount.0,
send_to.into(),
)?;
// burn the received coins so that they sent back to the user
// if it is requested to an external address.
if send_to_external {
// see how much we got
let coin_balance =
Coins::<T>::balance(IN_INSTRUCTION_EXECUTOR.into(), out_balance.coin);
let instruction = OutInstructionWithBalance {
instruction: OutInstruction {
address: out_address.as_external().unwrap(),
// TODO: Properly pass data. Replace address with an OutInstruction entirely?
data: None,
},
balance: Balance { coin: out_balance.coin, amount: coin_balance },
};
Coins::<T>::burn_non_sri(IN_INSTRUCTION_EXECUTOR.into(), instruction)?;
}
}
}
}
}
Ok(())
}

View file

@ -18,8 +18,8 @@ serde = { version = "1", default-features = false, features = ["derive", "alloc"
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2", default-features = false, features = ["derive"] }
sp-application-crypto = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-application-crypto = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false }
serai-primitives = { path = "../../primitives", default-features = false }
@ -35,6 +35,7 @@ std = [
"scale-info/std",
"sp-std/std",
"sp-application-crypto/std",
"sp-runtime/std",
"serai-primitives/std",

View file

@ -16,20 +16,65 @@ use sp_application_crypto::sr25519::Signature;
use sp_std::vec::Vec;
use sp_runtime::RuntimeDebug;
use serai_primitives::{BlockHash, Balance, NetworkId, SeraiAddress, ExternalAddress, Data};
#[rustfmt::skip]
use serai_primitives::{BlockHash, Balance, NetworkId, SeraiAddress, ExternalAddress, system_address};
mod shorthand;
pub use shorthand::*;
pub const MAX_BATCH_SIZE: usize = 25_000; // ~25kb
// This is the account which will be the origin for any dispatched `InInstruction`s.
pub const IN_INSTRUCTION_EXECUTOR: SeraiAddress = system_address(b"InInstructions-executor");
#[derive(
Clone, PartialEq, Eq, Debug, Serialize, Deserialize, Encode, Decode, MaxEncodedLen, TypeInfo,
)]
#[cfg_attr(feature = "std", derive(Zeroize))]
pub enum OutAddress {
Serai(SeraiAddress),
External(ExternalAddress),
}
impl OutAddress {
pub fn is_native(&self) -> bool {
matches!(self, Self::Serai(_))
}
pub fn as_native(self) -> Option<SeraiAddress> {
match self {
Self::Serai(addr) => Some(addr),
_ => None,
}
}
pub fn as_external(self) -> Option<ExternalAddress> {
match self {
Self::External(addr) => Some(addr),
Self::Serai(_) => None,
}
}
}
#[derive(
Clone, PartialEq, Eq, Debug, Serialize, Deserialize, Encode, Decode, MaxEncodedLen, TypeInfo,
)]
#[cfg_attr(feature = "std", derive(Zeroize))]
pub enum DexCall {
// address to send the lp tokens to
// TODO: Update this per documentation/Shorthand
SwapAndAddLiquidity(SeraiAddress),
// minimum out balance and out address
Swap(Balance, OutAddress),
}
#[derive(
Clone, PartialEq, Eq, Debug, Serialize, Deserialize, Encode, Decode, MaxEncodedLen, TypeInfo,
)]
#[cfg_attr(feature = "std", derive(Zeroize))]
pub enum InInstruction {
Transfer(SeraiAddress),
Dex(Data),
Dex(DexCall),
}
#[derive(

View file

@ -26,7 +26,7 @@ pub enum Shorthand {
minimum: Amount,
out: OutInstruction,
},
AddLiquidity {
SwapAndAddLiquidity {
origin: Option<ExternalAddress>,
minimum: Amount,
gas: Amount,
@ -47,7 +47,7 @@ impl TryFrom<Shorthand> for RefundableInInstruction {
Ok(match shorthand {
Shorthand::Raw(instruction) => instruction,
Shorthand::Swap { .. } => todo!(),
Shorthand::AddLiquidity { .. } => todo!(),
Shorthand::SwapAndAddLiquidity { .. } => todo!(),
})
}
}

View file

@ -0,0 +1,46 @@
[package]
name = "serai-liquidity-tokens-pallet"
version = "0.1.0"
description = "liquidity tokens pallet for Serai"
license = "AGPL-3.0-only"
repository = "https://github.com/serai-dex/serai/tree/develop/substrate/liquidity-tokens/pallet"
authors = ["Akil Demir <aeg_asd@hotmail.com>"]
edition = "2021"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
parity-scale-codec = { version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2", default-features = false, features = ["derive"] }
frame-system = { git = "https://github.com/serai-dex/substrate", default-features = false }
frame-support = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false }
dex-primitives = { package = "serai-dex-primitives", path = "../../dex/primitives", default-features = false }
serai-primitives = { path = "../../primitives", default-features = false }
[features]
std = [
"frame-system/std",
"frame-support/std",
"sp-core/std",
"sp-std/std",
"dex-primitives/std",
"serai-primitives/std",
]
runtime-benchmarks = [
"frame-system/runtime-benchmarks",
"frame-support/runtime-benchmarks",
]
default = ["std"]

View file

@ -0,0 +1,15 @@
AGPL-3.0-only license
Copyright (c) 2023 Luke Parker
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License Version 3 as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

View file

@ -0,0 +1,152 @@
#![cfg_attr(not(feature = "std"), no_std)]
#[frame_support::pallet]
pub mod pallet {
use sp_core::sr25519::Public;
use sp_std::vec::Vec;
use frame_support::pallet_prelude::*;
use dex_primitives::LiquidityTokens;
use serai_primitives::*;
#[pallet::config]
pub trait Config: frame_system::Config<AccountId = Public> {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
}
#[pallet::error]
pub enum Error<T> {
AmountOverflowed,
NotEnoughCoins,
}
#[pallet::event]
#[pallet::generate_deposit(fn deposit_event)]
pub enum Event<T: Config> {
LtMint { to: Public, token: u32, amount: Amount },
LtBurn { from: Public, token: u32, amount: Amount },
}
#[pallet::pallet]
pub struct Pallet<T>(PhantomData<T>);
/// The amount of coins each account has.
// Identity is used as the second key's hasher due to it being a non-manipulatable fixed-space
// ID.
#[pallet::storage]
#[pallet::getter(fn balances)]
pub type Balances<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
Public,
Blake2_128Concat,
u32,
SubstrateAmount,
OptionQuery,
>;
/// The total supply of each coin.
// We use Identity type here again due to reasons stated in the Balances Storage.
#[pallet::storage]
#[pallet::getter(fn supply)]
pub type Supply<T: Config> = StorageMap<_, Blake2_128Concat, u32, SubstrateAmount, ValueQuery>;
// TODO: apis: supply, mint, burn, transfer
impl<T: Config> Pallet<T> {
/// Returns the balance of a given account for `token`.
pub fn balance(token: u32, of: T::AccountId) -> SubstrateAmount {
Self::balances(of, token).unwrap_or(0)
}
/// Mint `balance` to the given account.
///
/// Errors if any amount overflows.
pub fn mint_into(token: u32, to: Public, amount: SubstrateAmount) -> Result<(), Error<T>> {
let balance = Self::balances(to, token).unwrap_or(0);
// update the balance
let new_amount = balance.checked_add(amount).ok_or(Error::<T>::AmountOverflowed)?;
// save
Balances::<T>::set(to, token, Some(new_amount));
// update the supply
let new_supply =
Self::supply(token).checked_add(amount).ok_or(Error::<T>::AmountOverflowed)?;
Supply::<T>::set(token, new_supply);
Self::deposit_event(Event::LtMint { to, token, amount: Amount(amount) });
Ok(())
}
// Burn `balance` from the specified account.
pub fn burn_from(token: u32, from: Public, amount: SubstrateAmount) -> Result<(), Error<T>> {
let balance = Self::balances(from, token);
if balance.is_none() {
Err(Error::<T>::NotEnoughCoins)?;
}
// update the balance
let new_amount = balance.unwrap().checked_sub(amount).ok_or(Error::<T>::NotEnoughCoins)?;
// save
if new_amount == 0 {
Balances::<T>::remove(from, token);
} else {
Balances::<T>::set(from, token, Some(new_amount));
}
// update the supply
let new_supply = Self::supply(token).checked_sub(amount).unwrap();
if new_supply == 0 {
Supply::<T>::remove(token);
} else {
Supply::<T>::set(token, new_supply);
}
Self::deposit_event(Event::LtBurn { from, token, amount: Amount(amount) });
Ok(())
}
pub fn total_issuance(token: u32) -> SubstrateAmount {
Supply::<T>::get(token)
}
}
impl<T: Config> LiquidityTokens<T::AccountId> for Pallet<T> {
type Balance = SubstrateAmount;
type CoinId = u32;
fn mint_into(
token: Self::CoinId,
to: &Public,
amount: Self::Balance,
) -> Result<Self::Balance, DispatchError> {
Self::mint_into(token, *to, amount)?;
Ok(amount)
}
fn burn_from(
token: Self::CoinId,
from: &Public,
amount: Self::Balance,
) -> Result<Self::Balance, DispatchError> {
Self::burn_from(token, *from, amount)?;
Ok(amount)
}
fn total_issuance(token: Self::CoinId) -> Self::Balance {
Self::total_issuance(token)
}
fn coin_ids() -> Vec<Self::CoinId> {
Supply::<T>::iter_keys().collect::<Vec<Self::CoinId>>()
}
fn balance(token: Self::CoinId, of: &Public) -> Self::Balance {
Self::balance(token, *of)
}
}
}
pub use pallet::*;

View file

@ -6,8 +6,8 @@ use sc_service::ChainType;
use serai_runtime::{
primitives::*, WASM_BINARY, opaque::SessionKeys, BABE_GENESIS_EPOCH_CONFIG, RuntimeGenesisConfig,
SystemConfig, ValidatorSetsConfig, SessionConfig, BabeConfig, GrandpaConfig,
AuthorityDiscoveryConfig, CoinsConfig,
SystemConfig, CoinsConfig, DexConfig, ValidatorSetsConfig, SessionConfig, BabeConfig,
GrandpaConfig, AuthorityDiscoveryConfig,
};
pub type ChainSpec = sc_service::GenericChainSpec<RuntimeGenesisConfig>;
@ -43,6 +43,8 @@ fn testnet_genesis(
.collect(),
},
dex: DexConfig { pools: vec![Coin::Bitcoin, Coin::Ether, Coin::Dai, Coin::Monero] },
validator_sets: ValidatorSetsConfig {
networks: serai_runtime::primitives::NETWORKS
.iter()

View file

@ -42,6 +42,8 @@ pub const COINS: [Coin; 5] = [Coin::Serai, Coin::Bitcoin, Coin::Ether, Coin::Dai
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Debug,
Serialize,
@ -61,6 +63,10 @@ pub enum Coin {
}
impl Coin {
pub fn native() -> Coin {
Coin::Serai
}
pub fn network(&self) -> NetworkId {
match self {
Coin::Serai => NetworkId::Serai,
@ -101,6 +107,10 @@ impl Coin {
Coin::Monero => 12,
}
}
pub fn is_native(&self) -> bool {
matches!(self, Coin::Serai)
}
}
// Max of 8 coins per network

View file

@ -40,12 +40,15 @@ frame-executive = { git = "https://github.com/serai-dex/substrate", default-feat
frame-benchmarking = { git = "https://github.com/serai-dex/substrate", default-features = false, optional = true }
serai-primitives = { path = "../primitives", default-features = false }
serai-dex-primitives = { path = "../dex/primitives", default-features = false }
pallet-timestamp = { git = "https://github.com/serai-dex/substrate", default-features = false }
pallet-transaction-payment = { git = "https://github.com/serai-dex/substrate", default-features = false }
coins-pallet = { package = "serai-coins-pallet", path = "../coins/pallet", default-features = false }
liquidity-tokens-pallet = { package = "serai-liquidity-tokens-pallet", path = "../liquidity-tokens/pallet", default-features = false }
dex-pallet = { package = "serai-dex-pallet", path = "../dex/pallet", default-features = false }
validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../validator-sets/pallet", default-features = false }
pallet-session = { git = "https://github.com/serai-dex/substrate", default-features = false }
@ -94,12 +97,15 @@ std = [
"frame-executive/std",
"serai-primitives/std",
"serai-dex-primitives/std",
"pallet-timestamp/std",
"pallet-transaction-payment/std",
"coins-pallet/std",
"liquidity-tokens-pallet/std",
"dex-pallet/std",
"validator-sets-pallet/std",
"pallet-session/std",
@ -126,6 +132,8 @@ runtime-benchmarks = [
"pallet-timestamp/runtime-benchmarks",
"dex-pallet/runtime-benchmarks",
"pallet-babe/runtime-benchmarks",
"pallet-grandpa/runtime-benchmarks",
]

View file

@ -17,6 +17,8 @@ pub use pallet_timestamp as timestamp;
pub use pallet_transaction_payment as transaction_payment;
pub use coins_pallet as coins;
pub use liquidity_tokens_pallet as liquidity_tokens;
pub use dex_pallet as dex;
pub use validator_sets_pallet as validator_sets;
pub use pallet_session as session;
@ -45,7 +47,7 @@ use sp_runtime::{
ApplyExtrinsicResult, Perbill,
};
use primitives::{PublicKey, SeraiAddress, AccountLookup, Signature, SubstrateAmount};
use primitives::{PublicKey, SeraiAddress, Coin, AccountLookup, Signature, SubstrateAmount};
use support::{
traits::{ConstU8, ConstU32, ConstU64, Contains},
@ -167,6 +169,7 @@ impl Contains<RuntimeCall> for CallFilter {
// All of these pallets are our own, and all of their written calls are intended to be called
RuntimeCall::Coins(call) => !matches!(call, coins::Call::__Ignore(_, _)),
RuntimeCall::Dex(call) => !matches!(call, dex::Call::__Ignore(_, _)),
RuntimeCall::ValidatorSets(call) => !matches!(call, validator_sets::Call::__Ignore(_, _)),
RuntimeCall::InInstructions(call) => !matches!(call, in_instructions::Call::__Ignore(_, _)),
RuntimeCall::Signals(call) => !matches!(call, signals::Call::__Ignore(_, _)),
@ -243,6 +246,37 @@ impl coins::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
}
impl liquidity_tokens::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
}
impl dex::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type Currency = Coins;
type Balance = SubstrateAmount;
type CoinBalance = SubstrateAmount;
// TODO: Review if this should be u64/u128 or u64/u256 (and rounding in general).
type HigherPrecisionBalance = u128;
type CoinId = Coin;
type MultiCoinId = Coin;
type MultiCoinIdConverter = serai_dex_primitives::CoinConverter;
type PoolCoinId = u32;
type Coins = Coins;
type PoolCoins = LiquidityTokens;
type LPFee = ConstU32<3>; // 0.3%
type MintMinLiquidity = ConstU64<10000>;
type MaxSwapPathLength = ConstU32<3>; // coin1 -> SRI -> coin2
type WeightInfo = dex::weights::SubstrateWeight<Runtime>;
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper = ();
}
impl validator_sets::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
}
@ -342,6 +376,8 @@ construct_runtime!(
TransactionPayment: transaction_payment,
Coins: coins,
LiquidityTokens: liquidity_tokens,
Dex: dex,
ValidatorSets: validator_sets,
Session: session,

View file

@ -409,7 +409,7 @@ pub mod pallet {
impl<T: Config> Pallet<T> {
fn account() -> T::AccountId {
system_address(b"validator-sets").into()
system_address(b"ValidatorSets").into()
}
// is_bft returns if the network is able to survive any single node becoming byzantine.