diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3ca43347..99ec6a8b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 \ diff --git a/Cargo.lock b/Cargo.lock index 890d040d..30de661e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/deny.toml b/deny.toml index f846c761..54b2b1f7 100644 --- a/deny.toml +++ b/deny.toml @@ -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" }, diff --git a/processor/src/tests/substrate_signer.rs b/processor/src/tests/substrate_signer.rs index 79ff707c..9dd96791 100644 --- a/processor/src/tests/substrate_signer.rs +++ b/processor/src/tests/substrate_signer.rs @@ -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) }, }, ], diff --git a/substrate/client/src/serai/dex.rs b/substrate/client/src/serai/dex.rs new file mode 100644 index 00000000..698fecfb --- /dev/null +++ b/substrate/client/src/serai/dex.rs @@ -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; + +#[derive(Clone, Copy)] +pub struct SeraiDex<'a>(pub(crate) TemporalSerai<'a>); +impl<'a> SeraiDex<'a> { + pub async fn all_events(&self) -> Result, SeraiError> { + self.0.events::(|_| 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> { + Payload::new( + PALLET, + "add_liquidity", + scale_composite(dex::Call::::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> { + 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::::swap_exact_tokens_for_tokens { + path, + amount_in: amount_in.0, + amount_out_min: amount_out_min.0, + send_to: address.into(), + }), + ) + } +} diff --git a/substrate/client/src/serai/mod.rs b/substrate/client/src/serai/mod.rs index bbe24bcd..d14caba4 100644 --- a/substrate/client/src/serai/mod.rs +++ b/substrate/client/src/serai/mod.rs @@ -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) } diff --git a/substrate/client/tests/common/dex.rs b/substrate/client/tests/common/dex.rs new file mode 100644 index 00000000..b020e850 --- /dev/null +++ b/substrate/client/tests/common/dex.rs @@ -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 +} diff --git a/substrate/client/tests/common/in_instructions.rs b/substrate/client/tests/common/in_instructions.rs index 9848c370..7ec94910 100644 --- a/substrate/client/tests/common/in_instructions.rs +++ b/substrate/client/tests/common/in_instructions.rs @@ -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 +} diff --git a/substrate/client/tests/common/mod.rs b/substrate/client/tests/common/mod.rs index c4b3aaa1..0624ca0c 100644 --- a/substrate/client/tests/common/mod.rs +++ b/substrate/client/tests/common/mod.rs @@ -1,6 +1,7 @@ pub mod tx; pub mod validator_sets; pub mod in_instructions; +pub mod dex; #[macro_export] macro_rules! serai_test { diff --git a/substrate/client/tests/dex.rs b/substrate/client/tests/dex.rs new file mode 100644 index 00000000..6e52763c --- /dev/null +++ b/substrate/client/tests/dex.rs @@ -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 + }] + ); + } + }) +); diff --git a/substrate/coins/pallet/Cargo.toml b/substrate/coins/pallet/Cargo.toml index 1936b400..cafe2f6c 100644 --- a/substrate/coins/pallet/Cargo.toml +++ b/substrate/coins/pallet/Cargo.toml @@ -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", ] diff --git a/substrate/coins/pallet/src/lib.rs b/substrate/coins/pallet/src/lib.rs index b1bf738c..433c2bd7 100644 --- a/substrate/coins/pallet/src/lib.rs +++ b/substrate/coins/pallet/src/lib.rs @@ -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 + TpConfig { + pub trait Config: frame_system::Config { type RuntimeEvent: From> + IsType<::RuntimeEvent>; } @@ -72,12 +74,13 @@ pub mod pallet { impl BuildGenesisConfig for GenesisConfig { 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::::set(c, 0); } // initialize the genesis accounts - for (account, balance) in self.accounts.iter() { + for (account, balance) in &self.accounts { Pallet::::mint(*account, *balance).unwrap(); } } @@ -214,7 +217,75 @@ pub mod pallet { } } - impl OnChargeTransaction for Pallet { + impl Currency for Pallet { + 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 { + 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::mint(*to, Balance { coin: Coin::native(), amount: Amount(amount) })?; + Ok(amount) + } + } + + // TODO: Have DEX implement for Coins, not Coins implement for Coins + impl CoinsTrait for Pallet { + 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 { + 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::mint(*to, Balance { coin, amount: Amount(amount) })?; + Ok(amount) + } + } + + impl OnChargeTransaction for Pallet + where + T: TpConfig, + { type Balance = SubstrateAmount; type LiquidityInfo = Option; diff --git a/substrate/coins/primitives/src/lib.rs b/substrate/coins/primitives/src/lib.rs index 911a4b6f..d7253196 100644 --- a/substrate/coins/primitives/src/lib.rs +++ b/substrate/coins/primitives/src/lib.rs @@ -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() ); } diff --git a/substrate/dex/pallet/Cargo.toml b/substrate/dex/pallet/Cargo.toml new file mode 100644 index 00000000..cff31531 --- /dev/null +++ b/substrate/dex/pallet/Cargo.toml @@ -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 , Akil Demir "] +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", +] diff --git a/substrate/dex/pallet/LICENSE-AGPL3 b/substrate/dex/pallet/LICENSE-AGPL3 new file mode 100644 index 00000000..f684d027 --- /dev/null +++ b/substrate/dex/pallet/LICENSE-AGPL3 @@ -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 . diff --git a/substrate/dex/pallet/LICENSE-APACHE2 b/substrate/dex/pallet/LICENSE-APACHE2 new file mode 100644 index 00000000..fbb0616d --- /dev/null +++ b/substrate/dex/pallet/LICENSE-APACHE2 @@ -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/ \ No newline at end of file diff --git a/substrate/dex/pallet/src/benchmarking.rs b/substrate/dex/pallet/src/benchmarking.rs new file mode 100644 index 00000000..372c9df8 --- /dev/null +++ b/substrate/dex/pallet/src/benchmarking.rs @@ -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 = <::Lookup as StaticLookup>::Source; +type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + +fn get_lp_token_id() -> T::PoolCoinId +where + T::PoolCoinId: Into, +{ + let next_id: u32 = Dex::::get_next_pool_coin_id().into(); + (next_id - 1).into() +} + +fn create_coin(coin: &T::MultiCoinId) -> (T::AccountId, AccountIdLookupOf) +where + T::CoinBalance: From, + T::Currency: Currency, + T::Coins: Coins, +{ + 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::::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( + coin1: &T::MultiCoinId, + coin2: &T::MultiCoinId, +) -> (T::PoolCoinId, T::AccountId, AccountIdLookupOf) +where + T::CoinBalance: From, + T::Currency: Currency, + T::Coins: Coins, + T::PoolCoinId: Into, +{ + assert_eq!(coin1, &T::MultiCoinIdConverter::get_native()); + + let (caller, caller_lookup) = create_coin::(coin2); + + assert_ok!(Dex::::create_pool(coin2.clone())); + let lp_token = get_lp_token_id::(); + + (lp_token, caller, caller_lookup) +} + +benchmarks! { + where_clause { + where + T::CoinBalance: From + Into, + T::Currency: Currency, + T::Balance: From + Into, + T::Coins: Coins, + T::PoolCoinId: Into, + } + + 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::(&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::::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::::get_pool_account(&pool_id)), + add_amount.into() + ); + assert_eq!( + T::Coins::balance( + T::BenchmarkHelper::coin_id(0), + &Dex::::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::(&coin1, &coin2); + let ed: u64 = T::Currency::minimum_balance().into(); + let add_amount = 100 * ed; + let lp_minted = Dex::::calc_lp_amount_for_zero_supply( + &add_amount.into(), + &1000.into() + ).unwrap().into(); + let remove_lp_amount = lp_minted.checked_div(10).unwrap(); + + Dex::::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 = + >::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 = + >::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::(&native, &coin1); + let (_, _) = create_coin::(&coin2); + let ed: u64 = T::Currency::minimum_balance().into(); + let ed_bump = 2u64; + + Dex::::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::::create_pool(coin2.clone())?; + Dex::::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::(&native, &coin1); + let (_, _) = create_coin::(&coin2); + let ed: u64 = T::Currency::minimum_balance().into(); + + Dex::::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::::create_pool(coin2.clone())?; + Dex::::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); +} diff --git a/substrate/dex/pallet/src/lib.rs b/substrate/dex/pallet/src/lib.rs new file mode 100644 index 00000000..5f808da2 --- /dev/null +++ b/substrate/dex/pallet/src/lib.rs @@ -0,0 +1,1217 @@ +// 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. + +//! # Serai Dex pallet +//! +//! Serai Dex pallet based on the [Uniswap V2](https://github.com/Uniswap/v2-core) logic. +//! +//! ## Overview +//! +//! This pallet allows you to: +//! +//! - [create a liquidity pool](`Pallet::create_pool()`) for 2 coins +//! - [provide the liquidity](`Pallet::add_liquidity()`) and receive back an LP token +//! - [exchange the LP token back to coins](`Pallet::remove_liquidity()`) +//! - [swap a specific amount of coins for another](`Pallet::swap_exact_tokens_for_tokens()`) if +//! there is a pool created, or +//! - [swap some coins for a specific amount of +//! another](`Pallet::swap_tokens_for_exact_tokens()`). +//! - [query for an exchange price](`DexApi::quote_price_exact_tokens_for_tokens`) via +//! a runtime call endpoint +//! - [query the size of a liquidity pool](`DexApi::get_reserves`) via a runtime api +//! endpoint. +//! +//! The `quote_price_exact_tokens_for_tokens` and `quote_price_tokens_for_exact_tokens` functions +//! both take a path parameter of the route to take. If you want to swap from native coin to +//! non-native coin 1, you would pass in a path of `[DOT, 1]` or `[1, DOT]`. If you want to swap +//! from non-native coin 1 to non-native coin 2, you would pass in a path of `[1, DOT, 2]`. +//! +//! (For an example of configuring this pallet to use `MultiLocation` as an coin id, see the +//! cumulus repo). +//! +//! Here is an example `state_call` that asks for a quote of a pool of native versus coin 1: +//! +//! ```text +//! curl -sS -H "Content-Type: application/json" -d \ +//! '{ +//! "id": 1, +//! "jsonrpc": "2.0", +//! "method": "state_call", +//! "params": [ +//! "DexApi_quote_price_tokens_for_exact_tokens", +//! "0x0101000000000000000000000011000000000000000000" +//! ] +//! }' \ +//! http://localhost:9933/ +//! ``` +//! (This can be run against the kitchen sync node in the `node` folder of this repo.) +#![deny(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] +use frame_support::traits::{DefensiveOption, Incrementable}; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +pub mod weights; + +#[cfg(test)] +mod tests; + +#[cfg(test)] +mod mock; + +use codec::Codec; +use frame_support::{ + ensure, + traits::tokens::{AssetId as CoinId, Balance}, +}; +use frame_system::{ + ensure_signed, + pallet_prelude::{BlockNumberFor, OriginFor}, +}; +pub use pallet::*; +use sp_arithmetic::traits::Unsigned; +use sp_runtime::{ + traits::{ + CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, Ensure, MaybeDisplay, TrailingZeroInput, + }, + DispatchError, +}; +use sp_std::prelude::*; +use dex_primitives::*; +pub use weights::WeightInfo; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::{pallet_prelude::*, BoundedBTreeSet}; + use sp_runtime::{ + traits::{IntegerSquareRoot, One, Zero}, + Saturating, + }; + + /// Pool ID. + /// + /// The pool's `AccountId` is derived from this type. Any changes to the type may necessitate a + /// migration. + pub type PoolIdOf = (::MultiCoinId, ::MultiCoinId); + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// Overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// Currency type that this works on. + type Currency: Currency; + + /// The `Currency::Balance` type of the native currency. + type Balance: Balance; + + /// The type used to describe the amount of fractions converted into coins. + type CoinBalance: Balance; + + /// A type used for conversions between `Balance` and `CoinBalance`. + type HigherPrecisionBalance: IntegerSquareRoot + + One + + Ensure + + Unsigned + + From + + From + + From + + TryInto + + TryInto; + + /// Identifier for the class of non-native coin. + /// Note: A `From` bound here would prevent `MultiLocation` from being used as an + /// `CoinId`. + type CoinId: frame_support::Serialize + sp_runtime::DeserializeOwned + CoinId; + + /// Type that identifies either the native currency or a token class from `Coins`. + /// `Ord` is added because of `get_pool_id`. + /// + /// The pool's `AccountId` is derived from this type. Any changes to the type may + /// necessitate a migration. + type MultiCoinId: Ord + CoinId + From; + + /// Type to convert an `CoinId` into `MultiCoinId`. + type MultiCoinIdConverter: MultiCoinIdConverter; + + /// `CoinId` to address the lp tokens by. + type PoolCoinId: CoinId + PartialOrd + Incrementable + From; + + /// Registry for the coins. + type Coins: Coins; + + /// Registry for the lp tokens. Ideally only this pallet should have create permissions on + /// the coins. + type PoolCoins: LiquidityTokens< + Self::AccountId, + CoinId = Self::PoolCoinId, + Balance = Self::CoinBalance, + >; + + /// A % the liquidity providers will take of every swap. Represents 10ths of a percent. + #[pallet::constant] + type LPFee: Get; + + /// The minimum LP token amount that could be minted. Ameliorates rounding errors. + #[pallet::constant] + type MintMinLiquidity: Get; + + /// The max number of hops in a swap. + #[pallet::constant] + type MaxSwapPathLength: Get; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + + /// The benchmarks need a way to create coin ids from u32s. + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper: BenchmarkHelper; + } + + /// Map from `PoolCoinId` to `PoolInfo`. This establishes whether a pool has been officially + /// created rather than people sending tokens directly to a pool's public account. + #[pallet::storage] + pub type Pools = + StorageMap<_, Blake2_128Concat, PoolIdOf, PoolInfo, OptionQuery>; + + /// Stores the `PoolCoinId` that is going to be used for the next lp token. + /// This gets incremented whenever a new lp pool is created. + #[pallet::storage] + pub type NextPoolCoinId = StorageValue<_, T::PoolCoinId, OptionQuery>; + + // Pallet's events. + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A successful call of the `CretaPool` extrinsic will create this event. + PoolCreated { + /// The pool id associated with the pool. Note that the order of the coins may not be + /// the same as the order specified in the create pool extrinsic. + pool_id: PoolIdOf, + /// The account ID of the pool. + pool_account: T::AccountId, + /// The id of the liquidity tokens that will be minted when coins are added to this + /// pool. + lp_token: T::PoolCoinId, + }, + + /// A successful call of the `AddLiquidity` extrinsic will create this event. + LiquidityAdded { + /// The account that the liquidity was taken from. + who: T::AccountId, + /// The account that the liquidity tokens were minted to. + mint_to: T::AccountId, + /// The pool id of the pool that the liquidity was added to. + pool_id: PoolIdOf, + /// The amount of the first coin that was added to the pool. + amount1_provided: T::CoinBalance, + /// The amount of the second coin that was added to the pool. + amount2_provided: T::CoinBalance, + /// The id of the lp token that was minted. + lp_token: T::PoolCoinId, + /// The amount of lp tokens that were minted of that id. + lp_token_minted: T::CoinBalance, + }, + + /// A successful call of the `RemoveLiquidity` extrinsic will create this event. + LiquidityRemoved { + /// The account that the liquidity tokens were burned from. + who: T::AccountId, + /// The account that the coins were transferred to. + withdraw_to: T::AccountId, + /// The pool id that the liquidity was removed from. + pool_id: PoolIdOf, + /// The amount of the first coin that was removed from the pool. + amount1: T::CoinBalance, + /// The amount of the second coin that was removed from the pool. + amount2: T::CoinBalance, + /// The id of the lp token that was burned. + lp_token: T::PoolCoinId, + /// The amount of lp tokens that were burned of that id. + lp_token_burned: T::CoinBalance, + }, + /// Coins have been converted from one to another. Both `SwapExactTokenForToken` + /// and `SwapTokenForExactToken` will generate this event. + SwapExecuted { + /// Which account was the instigator of the swap. + who: T::AccountId, + /// The account that the coins were transferred to. + send_to: T::AccountId, + /// The route of coin ids that the swap went through. + /// E.g. A -> Dot -> B + path: BoundedVec, + /// The amount of the first coin that was swapped. + amount_in: T::CoinBalance, + /// The amount of the second coin that was received. + amount_out: T::CoinBalance, + }, + /// An amount has been transferred from one account to another. + Transfer { + /// The account that the coins were transferred from. + from: T::AccountId, + /// The account that the coins were transferred to. + to: T::AccountId, + /// The coin that was transferred. + coin: T::MultiCoinId, + /// The amount of the coin that was transferred. + amount: T::CoinBalance, + }, + } + + #[pallet::genesis_config] + #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)] + pub struct GenesisConfig { + /// Pools to create at launch. + pub pools: Vec, + } + + impl Default for GenesisConfig { + fn default() -> Self { + GenesisConfig { pools: Default::default() } + } + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + for coin in &self.pools { + Pallet::::create_pool(coin.clone().into()).unwrap(); + } + } + } + + #[pallet::error] + pub enum Error { + /// Provided coins are equal. + EqualCoins, + /// Provided coin is not supported for pool. + UnsupportedCoin, + /// Pool already exists. + PoolExists, + /// Desired amount can't be zero. + WrongDesiredAmount, + /// Provided amount should be greater than or equal to the existential deposit/coin's + /// minimal amount. + AmountOneLessThanMinimal, + /// Provided amount should be greater than or equal to the existential deposit/coin's + /// minimal amount. + AmountTwoLessThanMinimal, + /// Reserve needs to always be greater than or equal to the existential deposit/coin's + /// minimal amount. + ReserveLeftLessThanMinimal, + /// Desired amount can't be equal to the pool reserve. + AmountOutTooHigh, + /// The pool doesn't exist. + PoolNotFound, + /// An overflow happened. + Overflow, + /// The minimal amount requirement for the first token in the pair wasn't met. + CoinOneDepositDidNotMeetMinimum, + /// The minimal amount requirement for the second token in the pair wasn't met. + CoinTwoDepositDidNotMeetMinimum, + /// The minimal amount requirement for the first token in the pair wasn't met. + CoinOneWithdrawalDidNotMeetMinimum, + /// The minimal amount requirement for the second token in the pair wasn't met. + CoinTwoWithdrawalDidNotMeetMinimum, + /// Optimal calculated amount is less than desired. + OptimalAmountLessThanDesired, + /// Insufficient liquidity minted. + InsufficientLiquidityMinted, + /// Requested liquidity can't be zero. + ZeroLiquidity, + /// Amount can't be zero. + ZeroAmount, + /// Insufficient liquidity in the pool. + InsufficientLiquidity, + /// Calculated amount out is less than provided minimum amount. + ProvidedMinimumNotSufficientForSwap, + /// Provided maximum amount is not sufficient for swap. + ProvidedMaximumNotSufficientForSwap, + /// Only pools with native on one side are valid. + PoolMustContainNativeCurrency, + /// The provided path must consists of 2 coins at least. + InvalidPath, + /// It was not possible to calculate path data. + PathError, + /// The provided path must consists of unique coins. + NonUniquePath, + /// It was not possible to get or increment the Id of the pool. + IncorrectPoolCoinId, + /// Unable to find an element in an array/vec that should have one-to-one correspondence + /// with another. For example, an array of coins constituting a `path` should have a + /// corresponding array of `amounts` along the path. + CorrespondenceError, + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn integrity_test() { + assert!(T::MaxSwapPathLength::get() > 1, "the `MaxSwapPathLength` should be greater than 1",); + } + } + + impl Pallet { + /// Creates an empty liquidity pool and an associated new `lp_token` coin + /// (the id of which is returned in the `Event::PoolCreated` event). + /// + /// Once a pool is created, someone may [`Pallet::add_liquidity`] to it. + pub(crate) fn create_pool(coin2: T::MultiCoinId) -> DispatchResult { + let coin1 = T::MultiCoinIdConverter::get_native(); + ensure!(coin1 != coin2, Error::::EqualCoins); + + // prepare pool_id + let pool_id = Self::get_pool_id(coin1, coin2); + ensure!(!Pools::::contains_key(&pool_id), Error::::PoolExists); + + let pool_account = Self::get_pool_account(&pool_id); + frame_system::Pallet::::inc_providers(&pool_account); + + let lp_token = NextPoolCoinId::::get() + .or(T::PoolCoinId::initial_value()) + .ok_or(Error::::IncorrectPoolCoinId)?; + let next_lp_token_id = lp_token.increment().ok_or(Error::::IncorrectPoolCoinId)?; + NextPoolCoinId::::set(Some(next_lp_token_id)); + + let pool_info = PoolInfo { lp_token: lp_token.clone() }; + Pools::::insert(pool_id.clone(), pool_info); + + Self::deposit_event(Event::PoolCreated { pool_id, pool_account, lp_token }); + + Ok(()) + } + } + + /// Pallet's callable functions. + // TODO: For all of these calls, limit one of these to always be Coin::Serai + #[pallet::call] + impl Pallet { + /// Provide liquidity into the pool of `coin1` and `coin2`. + /// NOTE: an optimal amount of coin1 and coin2 will be calculated and + /// might be different than the provided `amount1_desired`/`amount2_desired` + /// thus you should provide the min amount you're happy to provide. + /// Params `amount1_min`/`amount2_min` represent that. + /// `mint_to` will be sent the liquidity tokens that represent this share of the pool. + /// + /// Once liquidity is added, someone may successfully call + /// [`Pallet::swap_exact_tokens_for_tokens`] successfully. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::add_liquidity())] + #[allow(clippy::too_many_arguments)] + pub fn add_liquidity( + origin: OriginFor, + coin1: T::MultiCoinId, + coin2: T::MultiCoinId, + amount1_desired: T::CoinBalance, + amount2_desired: T::CoinBalance, + amount1_min: T::CoinBalance, + amount2_min: T::CoinBalance, + mint_to: T::AccountId, + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + + let pool_id = Self::get_pool_id(coin1.clone(), coin2.clone()); + // swap params if needed + let (amount1_desired, amount2_desired, amount1_min, amount2_min) = if pool_id.0 == coin1 { + (amount1_desired, amount2_desired, amount1_min, amount2_min) + } else { + (amount2_desired, amount1_desired, amount2_min, amount1_min) + }; + ensure!( + amount1_desired > Zero::zero() && amount2_desired > Zero::zero(), + Error::::WrongDesiredAmount + ); + + let maybe_pool = Pools::::get(&pool_id); + let pool = maybe_pool.as_ref().ok_or(Error::::PoolNotFound)?; + let pool_account = Self::get_pool_account(&pool_id); + + let (coin1, coin2) = &pool_id; + let reserve1 = Self::get_balance(&pool_account, coin1)?; + let reserve2 = Self::get_balance(&pool_account, coin2)?; + + let amount1: T::CoinBalance; + let amount2: T::CoinBalance; + if reserve1.is_zero() || reserve2.is_zero() { + amount1 = amount1_desired; + amount2 = amount2_desired; + } else { + let amount2_optimal = Self::quote(&amount1_desired, &reserve1, &reserve2)?; + + if amount2_optimal <= amount2_desired { + ensure!(amount2_optimal >= amount2_min, Error::::CoinTwoDepositDidNotMeetMinimum); + amount1 = amount1_desired; + amount2 = amount2_optimal; + } else { + let amount1_optimal = Self::quote(&amount2_desired, &reserve2, &reserve1)?; + ensure!(amount1_optimal <= amount1_desired, Error::::OptimalAmountLessThanDesired); + ensure!(amount1_optimal >= amount1_min, Error::::CoinOneDepositDidNotMeetMinimum); + amount1 = amount1_optimal; + amount2 = amount2_desired; + } + } + + Self::validate_minimal_amount(amount1.saturating_add(reserve1), coin1) + .map_err(|_| Error::::AmountOneLessThanMinimal)?; + Self::validate_minimal_amount(amount2.saturating_add(reserve2), coin2) + .map_err(|_| Error::::AmountTwoLessThanMinimal)?; + + Self::transfer(coin1, &sender, &pool_account, amount1)?; + Self::transfer(coin2, &sender, &pool_account, amount2)?; + + let total_supply = T::PoolCoins::total_issuance(pool.lp_token.clone()); + + let lp_token_amount: T::CoinBalance; + if total_supply.is_zero() { + lp_token_amount = Self::calc_lp_amount_for_zero_supply(&amount1, &amount2)?; + T::PoolCoins::mint_into(pool.lp_token.clone(), &pool_account, T::MintMinLiquidity::get())?; + } else { + let side1 = Self::mul_div(&amount1, &total_supply, &reserve1)?; + let side2 = Self::mul_div(&amount2, &total_supply, &reserve2)?; + lp_token_amount = side1.min(side2); + } + + ensure!( + lp_token_amount > T::MintMinLiquidity::get(), + Error::::InsufficientLiquidityMinted + ); + + T::PoolCoins::mint_into(pool.lp_token.clone(), &mint_to, lp_token_amount)?; + + Self::deposit_event(Event::LiquidityAdded { + who: sender, + mint_to, + pool_id, + amount1_provided: amount1, + amount2_provided: amount2, + lp_token: pool.lp_token.clone(), + lp_token_minted: lp_token_amount, + }); + + Ok(()) + } + + /// Allows you to remove liquidity by providing the `lp_token_burn` tokens that will be + /// burned in the process. With the usage of `amount1_min_receive`/`amount2_min_receive` + /// it's possible to control the min amount of returned tokens you're happy with. + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::remove_liquidity())] + pub fn remove_liquidity( + origin: OriginFor, + coin1: T::MultiCoinId, + coin2: T::MultiCoinId, + lp_token_burn: T::CoinBalance, + amount1_min_receive: T::CoinBalance, + amount2_min_receive: T::CoinBalance, + withdraw_to: T::AccountId, + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + + let pool_id = Self::get_pool_id(coin1.clone(), coin2.clone()); + // swap params if needed + let (amount1_min_receive, amount2_min_receive) = if pool_id.0 == coin1 { + (amount1_min_receive, amount2_min_receive) + } else { + (amount2_min_receive, amount1_min_receive) + }; + let (coin1, coin2) = pool_id.clone(); + + ensure!(lp_token_burn > Zero::zero(), Error::::ZeroLiquidity); + + let maybe_pool = Pools::::get(&pool_id); + let pool = maybe_pool.as_ref().ok_or(Error::::PoolNotFound)?; + + let pool_account = Self::get_pool_account(&pool_id); + let reserve1 = Self::get_balance(&pool_account, &coin1)?; + let reserve2 = Self::get_balance(&pool_account, &coin2)?; + + let total_supply = T::PoolCoins::total_issuance(pool.lp_token.clone()); + let lp_redeem_amount = lp_token_burn; + + let amount1 = Self::mul_div(&lp_redeem_amount, &reserve1, &total_supply)?; + let amount2 = Self::mul_div(&lp_redeem_amount, &reserve2, &total_supply)?; + + ensure!( + !amount1.is_zero() && amount1 >= amount1_min_receive, + Error::::CoinOneWithdrawalDidNotMeetMinimum + ); + ensure!( + !amount2.is_zero() && amount2 >= amount2_min_receive, + Error::::CoinTwoWithdrawalDidNotMeetMinimum + ); + let reserve1_left = reserve1.saturating_sub(amount1); + let reserve2_left = reserve2.saturating_sub(amount2); + Self::validate_minimal_amount(reserve1_left, &coin1) + .map_err(|_| Error::::ReserveLeftLessThanMinimal)?; + Self::validate_minimal_amount(reserve2_left, &coin2) + .map_err(|_| Error::::ReserveLeftLessThanMinimal)?; + + // burn the provided lp token amount that includes the fee + T::PoolCoins::burn_from(pool.lp_token.clone(), &sender, lp_token_burn)?; + + Self::transfer(&coin1, &pool_account, &withdraw_to, amount1)?; + Self::transfer(&coin2, &pool_account, &withdraw_to, amount2)?; + + Self::deposit_event(Event::LiquidityRemoved { + who: sender, + withdraw_to, + pool_id, + amount1, + amount2, + lp_token: pool.lp_token.clone(), + lp_token_burned: lp_token_burn, + }); + + Ok(()) + } + + /// Swap the exact amount of `coin1` into `coin2`. + /// `amount_out_min` param allows you to specify the min amount of the `coin2` + /// you're happy to receive. + /// + /// [`DexApi::quote_price_exact_tokens_for_tokens`] runtime call can be called + /// for a quote. + #[pallet::call_index(2)] + #[pallet::weight(T::WeightInfo::swap_exact_tokens_for_tokens())] + pub fn swap_exact_tokens_for_tokens( + origin: OriginFor, + path: BoundedVec, + amount_in: T::CoinBalance, + amount_out_min: T::CoinBalance, + send_to: T::AccountId, + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + Self::do_swap_exact_tokens_for_tokens( + sender, + path, + amount_in, + Some(amount_out_min), + send_to, + )?; + Ok(()) + } + + /// Swap any amount of `coin1` to get the exact amount of `coin2`. + /// `amount_in_max` param allows to specify the max amount of the `coin1` + /// you're happy to provide. + /// + /// [`DexApi::quote_price_tokens_for_exact_tokens`] runtime call can be called + /// for a quote. + #[pallet::call_index(3)] + #[pallet::weight(T::WeightInfo::swap_tokens_for_exact_tokens())] + pub fn swap_tokens_for_exact_tokens( + origin: OriginFor, + path: BoundedVec, + amount_out: T::CoinBalance, + amount_in_max: T::CoinBalance, + send_to: T::AccountId, + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + Self::do_swap_tokens_for_exact_tokens( + sender, + path, + amount_out, + Some(amount_in_max), + send_to, + )?; + Ok(()) + } + } + + impl Pallet { + /// 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`. + pub fn do_swap_exact_tokens_for_tokens( + sender: T::AccountId, + path: BoundedVec, + amount_in: T::CoinBalance, + amount_out_min: Option, + send_to: T::AccountId, + ) -> Result { + ensure!(amount_in > Zero::zero(), Error::::ZeroAmount); + if let Some(amount_out_min) = amount_out_min { + ensure!(amount_out_min > Zero::zero(), Error::::ZeroAmount); + } + + Self::validate_swap_path(&path)?; + + let amounts = Self::get_amounts_out(&amount_in, &path)?; + let amount_out = + *amounts.last().defensive_ok_or("get_amounts_out() returned an empty result")?; + + if let Some(amount_out_min) = amount_out_min { + ensure!(amount_out >= amount_out_min, Error::::ProvidedMinimumNotSufficientForSwap); + } + + Self::do_swap(sender, &amounts, path, send_to)?; + Ok(amount_out) + } + + /// 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 the `path[1]` coin to `send_to`, + /// + /// If successful returns the amount of the `path[0]` taken to provide `path[1]`. + pub fn do_swap_tokens_for_exact_tokens( + sender: T::AccountId, + path: BoundedVec, + amount_out: T::CoinBalance, + amount_in_max: Option, + send_to: T::AccountId, + ) -> Result { + ensure!(amount_out > Zero::zero(), Error::::ZeroAmount); + if let Some(amount_in_max) = amount_in_max { + ensure!(amount_in_max > Zero::zero(), Error::::ZeroAmount); + } + + Self::validate_swap_path(&path)?; + + let amounts = Self::get_amounts_in(&amount_out, &path)?; + let amount_in = + *amounts.first().defensive_ok_or("get_amounts_in() returned an empty result")?; + + if let Some(amount_in_max) = amount_in_max { + ensure!(amount_in <= amount_in_max, Error::::ProvidedMaximumNotSufficientForSwap); + } + + Self::do_swap(sender, &amounts, path, send_to)?; + Ok(amount_in) + } + + /// Transfer an `amount` of `coin_id`. + fn transfer( + coin_id: &T::MultiCoinId, + from: &T::AccountId, + to: &T::AccountId, + amount: T::CoinBalance, + ) -> Result { + let result = match T::MultiCoinIdConverter::try_convert(coin_id) { + MultiCoinIdConversionResult::Converted(coin_id) => { + T::Coins::transfer(coin_id, from, to, amount) + } + MultiCoinIdConversionResult::Native => { + let amount = Self::convert_coin_balance_to_native_balance(amount)?; + Ok(Self::convert_native_balance_to_coin_balance(T::Currency::transfer( + from, to, amount, + )?)?) + } + MultiCoinIdConversionResult::Unsupported(_) => Err(Error::::UnsupportedCoin.into()), + }; + + if result.is_ok() { + Self::deposit_event(Event::Transfer { + from: from.clone(), + to: to.clone(), + coin: (*coin_id).clone(), + amount, + }); + } + result + } + + /// Convert a `Balance` type to an `CoinBalance`. + pub(crate) fn convert_native_balance_to_coin_balance( + amount: T::Balance, + ) -> Result> { + T::HigherPrecisionBalance::from(amount).try_into().map_err(|_| Error::::Overflow) + } + + /// Convert an `CoinBalance` type to a `Balance`. + pub(crate) fn convert_coin_balance_to_native_balance( + amount: T::CoinBalance, + ) -> Result> { + T::HigherPrecisionBalance::from(amount).try_into().map_err(|_| Error::::Overflow) + } + + /// Convert a `HigherPrecisionBalance` type to an `CoinBalance`. + pub(crate) fn convert_hpb_to_coin_balance( + amount: T::HigherPrecisionBalance, + ) -> Result> { + amount.try_into().map_err(|_| Error::::Overflow) + } + + /// Swap coins along a `path`, depositing in `send_to`. + pub(crate) fn do_swap( + sender: T::AccountId, + amounts: &Vec, + path: BoundedVec, + send_to: T::AccountId, + ) -> Result<(), DispatchError> { + ensure!(amounts.len() > 1, Error::::CorrespondenceError); + if let Some([coin1, coin2]) = &path.get(0 .. 2) { + let pool_id = Self::get_pool_id(coin1.clone(), coin2.clone()); + let pool_account = Self::get_pool_account(&pool_id); + // amounts should always contain a corresponding element to path. + let first_amount = amounts.first().ok_or(Error::::CorrespondenceError)?; + + Self::transfer(coin1, &sender, &pool_account, *first_amount)?; + + let mut i = 0; + let path_len = path.len() as u32; + for coins_pair in path.windows(2) { + if let [coin1, coin2] = coins_pair { + let pool_id = Self::get_pool_id(coin1.clone(), coin2.clone()); + let pool_account = Self::get_pool_account(&pool_id); + + let amount_out = + amounts.get((i + 1) as usize).ok_or(Error::::CorrespondenceError)?; + + let to = if i < path_len - 2 { + let coin3 = path.get((i + 2) as usize).ok_or(Error::::PathError)?; + Self::get_pool_account(&Self::get_pool_id(coin2.clone(), coin3.clone())) + } else { + send_to.clone() + }; + + let reserve = Self::get_balance(&pool_account, coin2)?; + let reserve_left = reserve.saturating_sub(*amount_out); + Self::validate_minimal_amount(reserve_left, coin2) + .map_err(|_| Error::::ReserveLeftLessThanMinimal)?; + + Self::transfer(coin2, &pool_account, &to, *amount_out)?; + } + i.saturating_inc(); + } + Self::deposit_event(Event::SwapExecuted { + who: sender, + send_to, + path, + amount_in: *first_amount, + amount_out: *amounts.last().expect("Always has more than 1 element"), + }); + } else { + return Err(Error::::InvalidPath.into()); + } + Ok(()) + } + + /// The account ID of the pool. + /// + /// This actually does computation. If you need to keep using it, then make sure you cache + /// the value and only call this once. + pub fn get_pool_account(pool_id: &PoolIdOf) -> T::AccountId { + let encoded_pool_id = sp_io::hashing::blake2_256(&Encode::encode(pool_id)[..]); + + Decode::decode(&mut TrailingZeroInput::new(encoded_pool_id.as_ref())) + .expect("infinite length input; no invalid inputs for type; qed") + } + + /// Get the `owner`'s balance of `coin`, which could be the chain's native coin or another + /// fungible. Returns a value in the form of an `CoinBalance`. + fn get_balance( + owner: &T::AccountId, + coin: &T::MultiCoinId, + ) -> Result> { + match T::MultiCoinIdConverter::try_convert(coin) { + MultiCoinIdConversionResult::Converted(coin_id) => { + Ok(<::Coins>::balance(coin_id, owner)) + } + MultiCoinIdConversionResult::Native => { + Self::convert_native_balance_to_coin_balance(<::Currency>::balance(owner)) + } + MultiCoinIdConversionResult::Unsupported(_) => Err(Error::::UnsupportedCoin), + } + } + + /// Returns a pool id constructed from 2 coins. + /// 1. Native coin should be lower than the other coin ids. + /// 2. Two native or two non-native coins are compared by their `Ord` implementation. + /// + /// We expect deterministic order, so (coin1, coin2) or (coin2, coin1) returns the same + /// result. + pub fn get_pool_id(coin1: T::MultiCoinId, coin2: T::MultiCoinId) -> PoolIdOf { + match (T::MultiCoinIdConverter::is_native(&coin1), T::MultiCoinIdConverter::is_native(&coin2)) + { + (true, false) => (coin1, coin2), + (false, true) => (coin2, coin1), + _ => { + // else we want to be deterministic based on `Ord` implementation + if coin1 <= coin2 { + (coin1, coin2) + } else { + (coin2, coin1) + } + } + } + } + + /// Returns the balance of each coin in the pool. + /// The tuple result is in the order requested (not necessarily the same as pool order). + pub fn get_reserves( + coin1: &T::MultiCoinId, + coin2: &T::MultiCoinId, + ) -> Result<(T::CoinBalance, T::CoinBalance), Error> { + let pool_id = Self::get_pool_id(coin1.clone(), coin2.clone()); + let pool_account = Self::get_pool_account(&pool_id); + + let balance1 = Self::get_balance(&pool_account, coin1)?; + let balance2 = Self::get_balance(&pool_account, coin2)?; + + if balance1.is_zero() || balance2.is_zero() { + Err(Error::::PoolNotFound)?; + } + + Ok((balance1, balance2)) + } + + /// Leading to an amount at the end of a `path`, get the required amounts in. + pub(crate) fn get_amounts_in( + amount_out: &T::CoinBalance, + path: &BoundedVec, + ) -> Result, DispatchError> { + let mut amounts: Vec = vec![*amount_out]; + + for coins_pair in path.windows(2).rev() { + if let [coin1, coin2] = coins_pair { + let (reserve_in, reserve_out) = Self::get_reserves(coin1, coin2)?; + let prev_amount = amounts.last().expect("Always has at least one element"); + let amount_in = Self::get_amount_in(prev_amount, &reserve_in, &reserve_out)?; + amounts.push(amount_in); + } + } + + amounts.reverse(); + Ok(amounts) + } + + /// Following an amount into a `path`, get the corresponding amounts out. + pub(crate) fn get_amounts_out( + amount_in: &T::CoinBalance, + path: &BoundedVec, + ) -> Result, DispatchError> { + let mut amounts: Vec = vec![*amount_in]; + + for coins_pair in path.windows(2) { + if let [coin1, coin2] = coins_pair { + let (reserve_in, reserve_out) = Self::get_reserves(coin1, coin2)?; + let prev_amount = amounts.last().expect("Always has at least one element"); + let amount_out = Self::get_amount_out(prev_amount, &reserve_in, &reserve_out)?; + amounts.push(amount_out); + } + } + + Ok(amounts) + } + + /// Used by the RPC service to provide current prices. + pub fn quote_price_exact_tokens_for_tokens( + coin1: T::MultiCoinId, + coin2: T::MultiCoinId, + amount: T::CoinBalance, + include_fee: bool, + ) -> Option { + let pool_id = Self::get_pool_id(coin1.clone(), coin2.clone()); + let pool_account = Self::get_pool_account(&pool_id); + + let balance1 = Self::get_balance(&pool_account, &coin1).ok()?; + let balance2 = Self::get_balance(&pool_account, &coin2).ok()?; + if !balance1.is_zero() { + if include_fee { + Self::get_amount_out(&amount, &balance1, &balance2).ok() + } else { + Self::quote(&amount, &balance1, &balance2).ok() + } + } else { + None + } + } + + /// Used by the RPC service to provide current prices. + pub fn quote_price_tokens_for_exact_tokens( + coin1: T::MultiCoinId, + coin2: T::MultiCoinId, + amount: T::CoinBalance, + include_fee: bool, + ) -> Option { + let pool_id = Self::get_pool_id(coin1.clone(), coin2.clone()); + let pool_account = Self::get_pool_account(&pool_id); + + let balance1 = Self::get_balance(&pool_account, &coin1).ok()?; + let balance2 = Self::get_balance(&pool_account, &coin2).ok()?; + if !balance1.is_zero() { + if include_fee { + Self::get_amount_in(&amount, &balance1, &balance2).ok() + } else { + Self::quote(&amount, &balance2, &balance1).ok() + } + } else { + None + } + } + + /// Calculates the optimal amount from the reserves. + pub fn quote( + amount: &T::CoinBalance, + reserve1: &T::CoinBalance, + reserve2: &T::CoinBalance, + ) -> Result> { + // amount * reserve2 / reserve1 + Self::mul_div(amount, reserve2, reserve1) + } + + pub(super) fn calc_lp_amount_for_zero_supply( + amount1: &T::CoinBalance, + amount2: &T::CoinBalance, + ) -> Result> { + let amount1 = T::HigherPrecisionBalance::from(*amount1); + let amount2 = T::HigherPrecisionBalance::from(*amount2); + + let result = amount1 + .checked_mul(&amount2) + .ok_or(Error::::Overflow)? + .integer_sqrt() + .checked_sub(&T::MintMinLiquidity::get().into()) + .ok_or(Error::::InsufficientLiquidityMinted)?; + + result.try_into().map_err(|_| Error::::Overflow) + } + + fn mul_div( + a: &T::CoinBalance, + b: &T::CoinBalance, + c: &T::CoinBalance, + ) -> Result> { + let a = T::HigherPrecisionBalance::from(*a); + let b = T::HigherPrecisionBalance::from(*b); + let c = T::HigherPrecisionBalance::from(*c); + + let result = a + .checked_mul(&b) + .ok_or(Error::::Overflow)? + .checked_div(&c) + .ok_or(Error::::Overflow)?; + + result.try_into().map_err(|_| Error::::Overflow) + } + + /// Calculates amount out. + /// + /// Given an input amount of an coin and pair reserves, returns the maximum output amount + /// of the other coin. + pub fn get_amount_out( + amount_in: &T::CoinBalance, + reserve_in: &T::CoinBalance, + reserve_out: &T::CoinBalance, + ) -> Result> { + let amount_in = T::HigherPrecisionBalance::from(*amount_in); + let reserve_in = T::HigherPrecisionBalance::from(*reserve_in); + let reserve_out = T::HigherPrecisionBalance::from(*reserve_out); + + if reserve_in.is_zero() || reserve_out.is_zero() { + return Err(Error::::ZeroLiquidity); + } + + let amount_in_with_fee = amount_in + .checked_mul(&(T::HigherPrecisionBalance::from(1000u32) - (T::LPFee::get().into()))) + .ok_or(Error::::Overflow)?; + + let numerator = amount_in_with_fee.checked_mul(&reserve_out).ok_or(Error::::Overflow)?; + + let denominator = reserve_in + .checked_mul(&1000u32.into()) + .ok_or(Error::::Overflow)? + .checked_add(&amount_in_with_fee) + .ok_or(Error::::Overflow)?; + + let result = numerator.checked_div(&denominator).ok_or(Error::::Overflow)?; + + result.try_into().map_err(|_| Error::::Overflow) + } + + /// Calculates amount in. + /// + /// Given an output amount of an coin and pair reserves, returns a required input amount + /// of the other coin. + pub fn get_amount_in( + amount_out: &T::CoinBalance, + reserve_in: &T::CoinBalance, + reserve_out: &T::CoinBalance, + ) -> Result> { + let amount_out = T::HigherPrecisionBalance::from(*amount_out); + let reserve_in = T::HigherPrecisionBalance::from(*reserve_in); + let reserve_out = T::HigherPrecisionBalance::from(*reserve_out); + + if reserve_in.is_zero() || reserve_out.is_zero() { + Err(Error::::ZeroLiquidity)? + } + + if amount_out >= reserve_out { + Err(Error::::AmountOutTooHigh)? + } + + let numerator = reserve_in + .checked_mul(&amount_out) + .ok_or(Error::::Overflow)? + .checked_mul(&1000u32.into()) + .ok_or(Error::::Overflow)?; + + let denominator = reserve_out + .checked_sub(&amount_out) + .ok_or(Error::::Overflow)? + .checked_mul(&(T::HigherPrecisionBalance::from(1000u32) - T::LPFee::get().into())) + .ok_or(Error::::Overflow)?; + + let result = numerator + .checked_div(&denominator) + .ok_or(Error::::Overflow)? + .checked_add(&One::one()) + .ok_or(Error::::Overflow)?; + + result.try_into().map_err(|_| Error::::Overflow) + } + + /// Ensure that a `value` meets the minimum balance requirements of an `coin` class. + fn validate_minimal_amount(value: T::CoinBalance, coin: &T::MultiCoinId) -> Result<(), ()> { + if T::MultiCoinIdConverter::is_native(coin) { + let ed = T::Currency::minimum_balance(); + ensure!(T::HigherPrecisionBalance::from(value) >= T::HigherPrecisionBalance::from(ed), ()); + } else { + let MultiCoinIdConversionResult::Converted(coin_id) = + T::MultiCoinIdConverter::try_convert(coin) + else { + return Err(()); + }; + let minimal = T::Coins::minimum_balance(coin_id); + ensure!(value >= minimal, ()); + } + Ok(()) + } + + /// Ensure that a path is valid. + fn validate_swap_path( + path: &BoundedVec, + ) -> Result<(), DispatchError> { + ensure!(path.len() >= 2, Error::::InvalidPath); + + // validate all the pools in the path are unique + let mut pools = BoundedBTreeSet::, T::MaxSwapPathLength>::new(); + for coins_pair in path.windows(2) { + if let [coin1, coin2] = coins_pair { + let pool_id = Self::get_pool_id(coin1.clone(), coin2.clone()); + let new_element = pools.try_insert(pool_id).map_err(|_| Error::::Overflow)?; + if !new_element { + return Err(Error::::NonUniquePath.into()); + } + } + } + Ok(()) + } + + /// Returns the next pool coin id for benchmark purposes only. + #[cfg(any(test, feature = "runtime-benchmarks"))] + pub fn get_next_pool_coin_id() -> T::PoolCoinId { + NextPoolCoinId::::get() + .or(T::PoolCoinId::initial_value()) + .expect("Next pool coin ID can not be None") + } + } +} + +impl Swap for Pallet { + fn swap_exact_tokens_for_tokens( + sender: T::AccountId, + path: Vec, + amount_in: T::HigherPrecisionBalance, + amount_out_min: Option, + send_to: T::AccountId, + ) -> Result { + let path = path.try_into().map_err(|_| Error::::PathError)?; + let amount_out_min = amount_out_min.map(Self::convert_hpb_to_coin_balance).transpose()?; + let amount_out = Self::do_swap_exact_tokens_for_tokens( + sender, + path, + Self::convert_hpb_to_coin_balance(amount_in)?, + amount_out_min, + send_to, + )?; + Ok(amount_out.into()) + } + + fn swap_tokens_for_exact_tokens( + sender: T::AccountId, + path: Vec, + amount_out: T::HigherPrecisionBalance, + amount_in_max: Option, + send_to: T::AccountId, + ) -> Result { + let path = path.try_into().map_err(|_| Error::::PathError)?; + let amount_in_max = amount_in_max.map(Self::convert_hpb_to_coin_balance).transpose()?; + let amount_in = Self::do_swap_tokens_for_exact_tokens( + sender, + path, + Self::convert_hpb_to_coin_balance(amount_out)?, + amount_in_max, + send_to, + )?; + Ok(amount_in.into()) + } +} + +sp_api::decl_runtime_apis! { + /// This runtime api allows people to query the size of the liquidity pools + /// and quote prices for swaps. + pub trait DexApi where + Balance: Codec + MaybeDisplay, + CoinBalance: frame_support::traits::tokens::Balance, + CoinId: Codec + { + /// Provides a quote for [`Pallet::swap_tokens_for_exact_tokens`]. + /// + /// Note that the price may have changed by the time the transaction is executed. + /// (Use `amount_in_max` to control slippage.) + fn quote_price_tokens_for_exact_tokens( + coin1: CoinId, + coin2: CoinId, + amount: CoinBalance, + include_fee: bool + ) -> Option; + + /// Provides a quote for [`Pallet::swap_exact_tokens_for_tokens`]. + /// + /// Note that the price may have changed by the time the transaction is executed. + /// (Use `amount_out_min` to control slippage.) + fn quote_price_exact_tokens_for_tokens( + coin1: CoinId, + coin2: CoinId, + amount: CoinBalance, + include_fee: bool + ) -> Option; + + /// Returns the size of the liquidity pool for the given coin pair. + fn get_reserves(coin1: CoinId, coin2: CoinId) -> Option<(Balance, Balance)>; + } +} + +sp_core::generate_feature_enabled_macro!( + runtime_benchmarks_enabled, + feature = "runtime-benchmarks", + $ +); diff --git a/substrate/dex/pallet/src/mock.rs b/substrate/dex/pallet/src/mock.rs new file mode 100644 index 00000000..2376b6e1 --- /dev/null +++ b/substrate/dex/pallet/src/mock.rs @@ -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; + +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; + 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 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 { + 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::::default().build_storage().unwrap(); + + let accounts: Vec = vec![ + system_address(b"account1").into(), + system_address(b"account2").into(), + system_address(b"account3").into(), + system_address(b"account4").into(), + ]; + coins::GenesisConfig:: { + 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 +} diff --git a/substrate/dex/pallet/src/tests.rs b/substrate/dex/pallet/src/tests.rs new file mode 100644 index 00000000..4b13dc26 --- /dev/null +++ b/substrate/dex/pallet/src/tests.rs @@ -0,0 +1,1411 @@ +// 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. + +use crate::{mock::*, *}; +use frame_support::{assert_noop, assert_ok}; + +pub use coins_pallet as coins; +pub use dex_primitives as primitives; + +use serai_primitives::{*, Balance}; + +fn events() -> Vec> { + let result = System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| if let mock::RuntimeEvent::Dex(inner) = e { Some(inner) } else { None }) + .collect(); + + System::reset_events(); + + result +} + +fn pools() -> Vec> { + let mut s: Vec<_> = Pools::::iter().map(|x| x.0).collect(); + s.sort(); + s +} + +fn coins() -> Vec { + COINS.to_vec() +} + +fn balance(owner: PublicKey, coin: Coin) -> u64 { + <::Currency>::balance(owner, coin).0 +} + +fn pool_balance(owner: PublicKey, token_id: u32) -> u64 { + <::PoolCoins>::balance(token_id, owner) +} + +fn get_ed() -> u64 { + as primitives::Currency>::minimum_balance() +} + +macro_rules! bvec { + ($( $x:tt )*) => { + vec![$( $x )*].try_into().unwrap() + } +} + +#[test] +fn check_pool_accounts_dont_collide() { + use std::collections::HashSet; + let mut map = HashSet::new(); + + for coin in coins() { + let account = Dex::get_pool_account(&(Coin::native(), coin)); + if map.contains(&account) { + panic!("Collision at {:?}", coin); + } + map.insert(account); + } +} + +#[test] +fn check_max_numbers() { + new_test_ext().execute_with(|| { + assert_eq!(Dex::quote(&3u64, &u64::MAX, &u64::MAX).ok().unwrap(), 3); + assert!(Dex::quote(&u64::MAX, &3u64, &u64::MAX).is_err()); + assert_eq!(Dex::quote(&u64::MAX, &u64::MAX, &1u64).ok().unwrap(), 1); + + assert_eq!(Dex::get_amount_out(&100u64, &u64::MAX, &u64::MAX).ok().unwrap(), 99); + assert_eq!(Dex::get_amount_in(&100u64, &u64::MAX, &u64::MAX).ok().unwrap(), 101); + }); +} + +#[test] +fn can_create_pool() { + new_test_ext().execute_with(|| { + let coin_account_deposit: u64 = 0; + let user: PublicKey = system_address(b"user1").into(); + let token_1 = Coin::native(); + let token_2 = Coin::Monero; + let pool_id = (token_1, token_2); + + let lp_token = Dex::get_next_pool_coin_id(); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_1, amount: Amount(1000) })); + assert_ok!(Dex::create_pool(token_2)); + + assert_eq!(balance(user, Coin::native()), 1000 - coin_account_deposit); + assert_eq!(lp_token + 1, Dex::get_next_pool_coin_id()); + + assert_eq!( + events(), + [Event::::PoolCreated { + pool_id, + pool_account: Dex::get_pool_account(&pool_id), + lp_token + }] + ); + assert_eq!(pools(), vec![pool_id]); + + assert_noop!(Dex::create_pool(token_1), Error::::EqualCoins); + }); +} + +#[test] +fn create_same_pool_twice_should_fail() { + new_test_ext().execute_with(|| { + let token_2 = Coin::Dai; + + let lp_token = Dex::get_next_pool_coin_id(); + assert_ok!(Dex::create_pool(token_2)); + let expected_free = lp_token + 1; + assert_eq!(expected_free, Dex::get_next_pool_coin_id()); + + assert_noop!(Dex::create_pool(token_2), Error::::PoolExists); + assert_eq!(expected_free, Dex::get_next_pool_coin_id()); + }); +} + +#[test] +fn different_pools_should_have_different_lp_tokens() { + new_test_ext().execute_with(|| { + let token_1 = Coin::native(); + let token_2 = Coin::Bitcoin; + let token_3 = Coin::Ether; + let pool_id_1_2 = (token_1, token_2); + let pool_id_1_3 = (token_1, token_3); + + let lp_token2_1 = Dex::get_next_pool_coin_id(); + assert_ok!(Dex::create_pool(token_2)); + let lp_token3_1 = Dex::get_next_pool_coin_id(); + + assert_eq!( + events(), + [Event::::PoolCreated { + pool_id: pool_id_1_2, + pool_account: Dex::get_pool_account(&pool_id_1_2), + lp_token: lp_token2_1 + }] + ); + + assert_ok!(Dex::create_pool(token_3)); + assert_eq!( + events(), + [Event::::PoolCreated { + pool_id: pool_id_1_3, + pool_account: Dex::get_pool_account(&pool_id_1_3), + lp_token: lp_token3_1, + }] + ); + + assert_ne!(lp_token2_1, lp_token3_1); + }); +} + +#[test] +fn can_add_liquidity() { + new_test_ext().execute_with(|| { + let user = system_address(b"user1").into(); + let token_1 = Coin::native(); + let token_2 = Coin::Dai; + let token_3 = Coin::Monero; + + let lp_token1 = Dex::get_next_pool_coin_id(); + assert_ok!(Dex::create_pool(token_2)); + let lp_token2 = Dex::get_next_pool_coin_id(); + assert_ok!(Dex::create_pool(token_3)); + + let ed = get_ed(); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_1, amount: Amount(10000 * 2 + ed) })); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_2, amount: Amount(1000) })); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_3, amount: Amount(1000) })); + + assert_ok!(Dex::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + 10000, + 10, + 10000, + 10, + user, + )); + + let pool_id = (token_1, token_2); + assert!(events().contains(&Event::::LiquidityAdded { + who: user, + mint_to: user, + pool_id, + amount1_provided: 10000, + amount2_provided: 10, + lp_token: lp_token1, + lp_token_minted: 216, + })); + let pallet_account = Dex::get_pool_account(&pool_id); + assert_eq!(balance(pallet_account, token_1), 10000); + assert_eq!(balance(pallet_account, token_2), 10); + assert_eq!(balance(user, token_1), 10000 + ed); + assert_eq!(balance(user, token_2), 1000 - 10); + assert_eq!(pool_balance(user, lp_token1), 216); + + // try to pass the non-native - native coins, the result should be the same + assert_ok!(Dex::add_liquidity( + RuntimeOrigin::signed(user), + token_3, + token_1, + 10, + 10000, + 10, + 10000, + user, + )); + + let pool_id = (token_1, token_3); + assert!(events().contains(&Event::::LiquidityAdded { + who: user, + mint_to: user, + pool_id, + amount1_provided: 10000, + amount2_provided: 10, + lp_token: lp_token2, + lp_token_minted: 216, + })); + let pallet_account = Dex::get_pool_account(&pool_id); + assert_eq!(balance(pallet_account, token_1), 10000); + assert_eq!(balance(pallet_account, token_3), 10); + assert_eq!(balance(user, token_1), ed); + assert_eq!(balance(user, token_3), 1000 - 10); + assert_eq!(pool_balance(user, lp_token2), 216); + }); +} + +#[test] +fn add_tiny_liquidity_leads_to_insufficient_liquidity_minted_error() { + new_test_ext().execute_with(|| { + let user = system_address(b"user1").into(); + let token_1 = Coin::native(); + let token_2 = Coin::Bitcoin; + + assert_ok!(Dex::create_pool(token_2)); + + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_1, amount: Amount(1000) })); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_2, amount: Amount(1000) })); + + assert_noop!( + Dex::add_liquidity(RuntimeOrigin::signed(user), token_1, token_2, get_ed(), 1, 1, 1, user), + Error::::InsufficientLiquidityMinted + ); + }); +} + +#[test] +fn add_tiny_liquidity_directly_to_pool_address() { + new_test_ext().execute_with(|| { + let user = system_address(b"user1").into(); + let token_1 = Coin::native(); + let token_2 = Coin::Ether; + let token_3 = Coin::Dai; + + assert_ok!(Dex::create_pool(token_2)); + assert_ok!(Dex::create_pool(token_3)); + + let ed = get_ed(); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_1, amount: Amount(10000 * 2 + ed) })); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_2, amount: Amount(10000) })); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_3, amount: Amount(10000) })); + + // check we're still able to add the liquidity even when the pool already has some token_1 + let pallet_account = Dex::get_pool_account(&(token_1, token_2)); + assert_ok!(CoinsPallet::mint(pallet_account, Balance { coin: token_1, amount: Amount(1000) })); + + assert_ok!(Dex::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + 10000, + 10, + 10000, + 10, + user, + )); + + // check the same but for token_3 (non-native token) + let pallet_account = Dex::get_pool_account(&(token_1, token_3)); + assert_ok!(CoinsPallet::mint(pallet_account, Balance { coin: token_2, amount: Amount(1) })); + assert_ok!(Dex::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_3, + 10000, + 10, + 10000, + 10, + user, + )); + }); +} + +#[test] +fn can_remove_liquidity() { + new_test_ext().execute_with(|| { + let user = system_address(b"user1").into(); + let token_1 = Coin::native(); + let token_2 = Coin::Monero; + let pool_id = (token_1, token_2); + + let lp_token = Dex::get_next_pool_coin_id(); + assert_ok!(Dex::create_pool(token_2)); + + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_1, amount: Amount(10000000000) })); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_2, amount: Amount(100000) })); + + assert_ok!(Dex::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + 1000000000, + 100000, + 1000000000, + 100000, + user, + )); + + let total_lp_received = pool_balance(user, lp_token); + + assert_ok!(Dex::remove_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + total_lp_received, + 0, + 0, + user, + )); + + assert!(events().contains(&Event::::LiquidityRemoved { + who: user, + withdraw_to: user, + pool_id, + amount1: 999990000, + amount2: 99999, + lp_token, + lp_token_burned: total_lp_received, + })); + + let pool_account = Dex::get_pool_account(&pool_id); + assert_eq!(balance(pool_account, token_1), 10000); + assert_eq!(balance(pool_account, token_2), 1); + assert_eq!(pool_balance(pool_account, lp_token), 100); + + assert_eq!(balance(user, token_1), 10000000000 - 1000000000 + 999990000); + assert_eq!(balance(user, token_2), 99999); + assert_eq!(pool_balance(user, lp_token), 0); + }); +} + +#[test] +fn can_not_redeem_more_lp_tokens_than_were_minted() { + new_test_ext().execute_with(|| { + let user = system_address(b"user1").into(); + let token_1 = Coin::native(); + let token_2 = Coin::Dai; + let lp_token = Dex::get_next_pool_coin_id(); + + assert_ok!(Dex::create_pool(token_2)); + + assert_ok!(CoinsPallet::mint( + user, + Balance { coin: token_1, amount: Amount(10000 + get_ed()) } + )); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_2, amount: Amount(1000) })); + + assert_ok!(Dex::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + 10000, + 10, + 10000, + 10, + user, + )); + + // Only 216 lp_tokens_minted + assert_eq!(pool_balance(user, lp_token), 216); + + assert_noop!( + Dex::remove_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + 216 + 1, // Try and redeem 10 lp tokens while only 9 minted. + 0, + 0, + user, + ), + liquidity_tokens::Error::::NotEnoughCoins + ); + }); +} + +#[test] +fn can_quote_price() { + new_test_ext().execute_with(|| { + let user = system_address(b"user1").into(); + let token_1 = Coin::native(); + let token_2 = Coin::Ether; + + assert_ok!(Dex::create_pool(token_2)); + + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_1, amount: Amount(100000) })); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_2, amount: Amount(1000) })); + + assert_ok!(Dex::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + 10000, + 200, + 1, + 1, + user, + )); + + assert_eq!( + Dex::quote_price_exact_tokens_for_tokens(Coin::native(), token_2, 3000, false,), + Some(60) + ); + // including fee so should get less out... + assert_eq!( + Dex::quote_price_exact_tokens_for_tokens(Coin::native(), token_2, 3000, true,), + Some(46) + ); + // Check it still gives same price: + // (if the above accidentally exchanged then it would not give same quote as before) + assert_eq!( + Dex::quote_price_exact_tokens_for_tokens(Coin::native(), token_2, 3000, false,), + Some(60) + ); + // including fee so should get less out... + assert_eq!( + Dex::quote_price_exact_tokens_for_tokens(Coin::native(), token_2, 3000, true,), + Some(46) + ); + + // Check inverse: + assert_eq!( + Dex::quote_price_exact_tokens_for_tokens(token_2, Coin::native(), 60, false,), + Some(3000) + ); + // including fee so should get less out... + assert_eq!( + Dex::quote_price_exact_tokens_for_tokens(token_2, Coin::native(), 60, true,), + Some(2302) + ); + + // + // same tests as above but for quote_price_tokens_for_exact_tokens: + // + assert_eq!( + Dex::quote_price_tokens_for_exact_tokens(Coin::native(), token_2, 60, false,), + Some(3000) + ); + // including fee so should need to put more in... + assert_eq!( + Dex::quote_price_tokens_for_exact_tokens(Coin::native(), token_2, 60, true,), + Some(4299) + ); + // Check it still gives same price: + // (if the above accidentally exchanged then it would not give same quote as before) + assert_eq!( + Dex::quote_price_tokens_for_exact_tokens(Coin::native(), token_2, 60, false,), + Some(3000) + ); + // including fee so should need to put more in... + assert_eq!( + Dex::quote_price_tokens_for_exact_tokens(Coin::native(), token_2, 60, true,), + Some(4299) + ); + + // Check inverse: + assert_eq!( + Dex::quote_price_tokens_for_exact_tokens(token_2, Coin::native(), 3000, false,), + Some(60) + ); + // including fee so should need to put more in... + assert_eq!( + Dex::quote_price_tokens_for_exact_tokens(token_2, Coin::native(), 3000, true,), + Some(86) + ); + + // + // roundtrip: Without fees one should get the original number + // + let amount_in = 100; + + assert_eq!( + Dex::quote_price_exact_tokens_for_tokens(token_2, Coin::native(), amount_in, false,) + .and_then(|amount| Dex::quote_price_exact_tokens_for_tokens( + Coin::native(), + token_2, + amount, + false, + )), + Some(amount_in) + ); + assert_eq!( + Dex::quote_price_exact_tokens_for_tokens(Coin::native(), token_2, amount_in, false,) + .and_then(|amount| Dex::quote_price_exact_tokens_for_tokens( + token_2, + Coin::native(), + amount, + false, + )), + Some(amount_in) + ); + + assert_eq!( + Dex::quote_price_tokens_for_exact_tokens(token_2, Coin::native(), amount_in, false,) + .and_then(|amount| Dex::quote_price_tokens_for_exact_tokens( + Coin::native(), + token_2, + amount, + false, + )), + Some(amount_in) + ); + assert_eq!( + Dex::quote_price_tokens_for_exact_tokens(Coin::native(), token_2, amount_in, false,) + .and_then(|amount| Dex::quote_price_tokens_for_exact_tokens( + token_2, + Coin::native(), + amount, + false, + )), + Some(amount_in) + ); + }); +} + +#[test] +fn quote_price_exact_tokens_for_tokens_matches_execution() { + new_test_ext().execute_with(|| { + let user = system_address(b"user1").into(); + let user2 = system_address(b"user2").into(); + let token_1 = Coin::native(); + let token_2 = Coin::Bitcoin; + + assert_ok!(Dex::create_pool(token_2)); + + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_1, amount: Amount(100000) })); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_2, amount: Amount(1000) })); + + assert_ok!(Dex::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + 10000, + 200, + 1, + 1, + user, + )); + + let amount = 1; + let quoted_price = 49; + assert_eq!( + Dex::quote_price_exact_tokens_for_tokens(token_2, token_1, amount, true,), + Some(quoted_price) + ); + + assert_ok!(CoinsPallet::mint(user2, Balance { coin: token_2, amount: Amount(amount) })); + let prior_dot_balance = 0; // TODO: This was set to 20000. Why? + assert_eq!(prior_dot_balance, balance(user2, token_1)); + assert_ok!(Dex::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user2), + bvec![token_2, token_1], + amount, + 1, + user2, + )); + + assert_eq!(prior_dot_balance + quoted_price, balance(user2, token_1)); + }); +} + +#[test] +fn quote_price_tokens_for_exact_tokens_matches_execution() { + new_test_ext().execute_with(|| { + let user = system_address(b"user1").into(); + let user2 = system_address(b"user2").into(); + let token_1 = Coin::native(); + let token_2 = Coin::Monero; + + assert_ok!(Dex::create_pool(token_2)); + + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_1, amount: Amount(100000) })); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_2, amount: Amount(1000) })); + + assert_ok!(Dex::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + 10000, + 200, + 1, + 1, + user, + )); + + let amount = 49; + let quoted_price = 1; + assert_eq!( + Dex::quote_price_tokens_for_exact_tokens(token_2, token_1, amount, true,), + Some(quoted_price) + ); + + assert_ok!(CoinsPallet::mint(user2, Balance { coin: token_2, amount: Amount(amount) })); + let prior_dot_balance = 0; // TODO: This was set to 20000. Why? + assert_eq!(prior_dot_balance, balance(user2, token_1)); + let prior_coin_balance = 49; + assert_eq!(prior_coin_balance, balance(user2, token_2)); + assert_ok!(Dex::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user2), + bvec![token_2, token_1], + amount, + 1, + user2, + )); + + assert_eq!(prior_dot_balance + amount, balance(user2, token_1)); + assert_eq!(prior_coin_balance - quoted_price, balance(user2, token_2)); + }); +} + +#[test] +fn can_swap_with_native() { + new_test_ext().execute_with(|| { + let user = system_address(b"user1").into(); + let token_1 = Coin::native(); + let token_2 = Coin::Ether; + let pool_id = (token_1, token_2); + + assert_ok!(Dex::create_pool(token_2)); + + let ed = get_ed(); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_1, amount: Amount(10000 + ed) })); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_2, amount: Amount(1000) })); + + let liquidity1 = 10000; + let liquidity2 = 200; + + assert_ok!(Dex::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + liquidity1, + liquidity2, + 1, + 1, + user, + )); + + let input_amount = 100; + let expect_receive = Dex::get_amount_out(&input_amount, &liquidity2, &liquidity1).ok().unwrap(); + + assert_ok!(Dex::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_2, token_1], + input_amount, + 1, + user, + )); + + let pallet_account = Dex::get_pool_account(&pool_id); + assert_eq!(balance(user, token_1), expect_receive + ed); + assert_eq!(balance(user, token_2), 1000 - liquidity2 - input_amount); + assert_eq!(balance(pallet_account, token_1), liquidity1 - expect_receive); + assert_eq!(balance(pallet_account, token_2), liquidity2 + input_amount); + }); +} + +#[test] +fn can_swap_with_realistic_values() { + new_test_ext().execute_with(|| { + let user = system_address(b"user1").into(); + let sri = Coin::native(); + let dai = Coin::Dai; + assert_ok!(Dex::create_pool(dai)); + + const UNIT: u64 = 1_000_000_000; + + assert_ok!(CoinsPallet::mint(user, Balance { coin: sri, amount: Amount(300_000 * UNIT) })); + assert_ok!(CoinsPallet::mint(user, Balance { coin: dai, amount: Amount(1_100_000 * UNIT) })); + + let liquidity_dot = 200_000 * UNIT; // ratio for a 5$ price + let liquidity_dai = 1_000_000 * UNIT; + assert_ok!(Dex::add_liquidity( + RuntimeOrigin::signed(user), + sri, + dai, + liquidity_dot, + liquidity_dai, + 1, + 1, + user, + )); + + let input_amount = 10 * UNIT; // dai + + assert_ok!(Dex::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![dai, sri], + input_amount, + 1, + user, + )); + + assert!(events().contains(&Event::::SwapExecuted { + who: user, + send_to: user, + path: bvec![dai, sri], + amount_in: 10 * UNIT, // usd + amount_out: 1_993_980_120, // About 2 dot after div by UNIT. + })); + }); +} + +#[test] +fn can_not_swap_in_pool_with_no_liquidity_added_yet() { + new_test_ext().execute_with(|| { + let user = system_address(b"user1").into(); + let token_1 = Coin::native(); + let token_2 = Coin::Monero; + + assert_ok!(Dex::create_pool(token_2)); + + // Check can't swap an empty pool + assert_noop!( + Dex::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_2, token_1], + 10, + 1, + user, + ), + Error::::PoolNotFound + ); + }); +} + +#[test] +fn check_no_panic_when_try_swap_close_to_empty_pool() { + new_test_ext().execute_with(|| { + let user = system_address(b"user1").into(); + let token_1 = Coin::native(); + let token_2 = Coin::Bitcoin; + let pool_id = (token_1, token_2); + let lp_token = Dex::get_next_pool_coin_id(); + + assert_ok!(Dex::create_pool(token_2)); + + let ed = get_ed(); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_1, amount: Amount(10000 + ed) })); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_2, amount: Amount(1000) })); + + let liquidity1 = 10000; + let liquidity2 = 200; + + assert_ok!(Dex::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + liquidity1, + liquidity2, + 1, + 1, + user, + )); + + let lp_token_minted = pool_balance(user, lp_token); + assert!(events().contains(&Event::::LiquidityAdded { + who: user, + mint_to: user, + pool_id, + amount1_provided: liquidity1, + amount2_provided: liquidity2, + lp_token, + lp_token_minted, + })); + + let pallet_account = Dex::get_pool_account(&pool_id); + assert_eq!(balance(pallet_account, token_1), liquidity1); + assert_eq!(balance(pallet_account, token_2), liquidity2); + + assert_ok!(Dex::remove_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + lp_token_minted, + 1, + 1, + user, + )); + + // Now, the pool should exist but be almost empty. + // Let's try and drain it. + assert_eq!(balance(pallet_account, token_1), 708); + assert_eq!(balance(pallet_account, token_2), 15); + + // validate the reserve should always stay above the ED + // Following test fail again due to the force on ED being > 1. + // assert_noop!( + // Dex::swap_tokens_for_exact_tokens( + // RuntimeOrigin::signed(user), + // bvec![token_2, token_1], + // 708 - ed + 1, // amount_out + // 500, // amount_in_max + // user, + // ), + // Error::::ReserveLeftLessThanMinimal + // ); + + assert_ok!(Dex::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_2, token_1], + 608, // amount_out + 500, // amount_in_max + user, + )); + + let token_1_left = balance(pallet_account, token_1); + let token_2_left = balance(pallet_account, token_2); + assert_eq!(token_1_left, 708 - 608); + + // The price for the last tokens should be very high + assert_eq!( + Dex::get_amount_in(&(token_1_left - 1), &token_2_left, &token_1_left).ok().unwrap(), + 10625 + ); + + assert_noop!( + Dex::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_2, token_1], + token_1_left - 1, // amount_out + 1000, // amount_in_max + user, + ), + Error::::ProvidedMaximumNotSufficientForSwap + ); + + // Try to swap what's left in the pool + assert_noop!( + Dex::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_2, token_1], + token_1_left, // amount_out + 1000, // amount_in_max + user, + ), + Error::::AmountOutTooHigh + ); + }); +} + +#[test] +fn swap_should_not_work_if_too_much_slippage() { + new_test_ext().execute_with(|| { + let user = system_address(b"user1").into(); + let token_1 = Coin::native(); + let token_2 = Coin::Ether; + + assert_ok!(Dex::create_pool(token_2)); + + assert_ok!(CoinsPallet::mint( + user, + Balance { coin: token_1, amount: Amount(10000 + get_ed()) } + )); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_2, amount: Amount(1000) })); + + let liquidity1 = 10000; + let liquidity2 = 200; + + assert_ok!(Dex::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + liquidity1, + liquidity2, + 1, + 1, + user, + )); + + let exchange_amount = 100; + + assert_noop!( + Dex::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_2, token_1], + exchange_amount, // amount_in + 4000, // amount_out_min + user, + ), + Error::::ProvidedMinimumNotSufficientForSwap + ); + }); +} + +#[test] +fn can_swap_tokens_for_exact_tokens() { + new_test_ext().execute_with(|| { + let user = system_address(b"user1").into(); + let token_1 = Coin::native(); + let token_2 = Coin::Dai; + let pool_id = (token_1, token_2); + + assert_ok!(Dex::create_pool(token_2)); + + let ed = get_ed(); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_1, amount: Amount(20000 + ed) })); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_2, amount: Amount(1000) })); + + let pallet_account = Dex::get_pool_account(&pool_id); + let before1 = balance(pallet_account, token_1) + balance(user, token_1); + let before2 = balance(pallet_account, token_2) + balance(user, token_2); + + let liquidity1 = 10000; + let liquidity2 = 200; + + assert_ok!(Dex::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + liquidity1, + liquidity2, + 1, + 1, + user, + )); + + let exchange_out = 50; + let expect_in = Dex::get_amount_in(&exchange_out, &liquidity1, &liquidity2).ok().unwrap(); + + assert_ok!(Dex::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_1, token_2], + exchange_out, // amount_out + 3500, // amount_in_max + user, + )); + + assert_eq!(balance(user, token_1), 10000 + ed - expect_in); + assert_eq!(balance(user, token_2), 1000 - liquidity2 + exchange_out); + assert_eq!(balance(pallet_account, token_1), liquidity1 + expect_in); + assert_eq!(balance(pallet_account, token_2), liquidity2 - exchange_out); + + // check invariants: + + // native and coin totals should be preserved. + assert_eq!(before1, balance(pallet_account, token_1) + balance(user, token_1)); + assert_eq!(before2, balance(pallet_account, token_2) + balance(user, token_2)); + }); +} + +#[test] +fn can_swap_tokens_for_exact_tokens_when_not_liquidity_provider() { + new_test_ext().execute_with(|| { + let user = system_address(b"user1").into(); + let user2 = system_address(b"user2").into(); + let token_1 = Coin::native(); + let token_2 = Coin::Monero; + let pool_id = (token_1, token_2); + let lp_token = Dex::get_next_pool_coin_id(); + + assert_ok!(Dex::create_pool(token_2)); + + let ed = get_ed(); + let base1 = 10000; + let base2 = 1000; + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_1, amount: Amount(base1 + ed) })); + assert_ok!(CoinsPallet::mint(user2, Balance { coin: token_1, amount: Amount(base1 + ed) })); + assert_ok!(CoinsPallet::mint(user2, Balance { coin: token_2, amount: Amount(base2) })); + + let pallet_account = Dex::get_pool_account(&pool_id); + let before1 = + balance(pallet_account, token_1) + balance(user, token_1) + balance(user2, token_1); + let before2 = + balance(pallet_account, token_2) + balance(user, token_2) + balance(user2, token_2); + + let liquidity1 = 10000; + let liquidity2 = 200; + + assert_ok!(Dex::add_liquidity( + RuntimeOrigin::signed(user2), + token_1, + token_2, + liquidity1, + liquidity2, + 1, + 1, + user2, + )); + + assert_eq!(balance(user, token_1), base1 + ed); + assert_eq!(balance(user, token_2), 0); + + let exchange_out = 50; + let expect_in = Dex::get_amount_in(&exchange_out, &liquidity1, &liquidity2).ok().unwrap(); + + assert_ok!(Dex::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_1, token_2], + exchange_out, // amount_out + 3500, // amount_in_max + user, + )); + + assert_eq!(balance(user, token_1), base1 + ed - expect_in); + assert_eq!(balance(pallet_account, token_1), liquidity1 + expect_in); + assert_eq!(balance(user, token_2), exchange_out); + assert_eq!(balance(pallet_account, token_2), liquidity2 - exchange_out); + + // check invariants: + + // native and coin totals should be preserved. + assert_eq!( + before1, + balance(pallet_account, token_1) + balance(user, token_1) + balance(user2, token_1) + ); + assert_eq!( + before2, + balance(pallet_account, token_2) + balance(user, token_2) + balance(user2, token_2) + ); + + let lp_token_minted = pool_balance(user2, lp_token); + assert_eq!(lp_token_minted, 1314); + + assert_ok!(Dex::remove_liquidity( + RuntimeOrigin::signed(user2), + token_1, + token_2, + lp_token_minted, + 0, + 0, + user2, + )); + }); +} + +#[test] +fn swap_tokens_for_exact_tokens_should_not_work_if_too_much_slippage() { + new_test_ext().execute_with(|| { + let user = system_address(b"user1").into(); + let token_1 = Coin::native(); + let token_2 = Coin::Ether; + + assert_ok!(Dex::create_pool(token_2)); + + assert_ok!(CoinsPallet::mint( + user, + Balance { coin: token_1, amount: Amount(20000 + get_ed()) } + )); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_2, amount: Amount(1000) })); + + let liquidity1 = 10000; + let liquidity2 = 200; + + assert_ok!(Dex::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + liquidity1, + liquidity2, + 1, + 1, + user, + )); + + let exchange_out = 1; + + assert_noop!( + Dex::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_1, token_2], + exchange_out, // amount_out + 50, // amount_in_max just greater than slippage. + user, + ), + Error::::ProvidedMaximumNotSufficientForSwap + ); + }); +} + +#[test] +fn swap_exact_tokens_for_tokens_in_multi_hops() { + new_test_ext().execute_with(|| { + let user = system_address(b"user1").into(); + let token_1 = Coin::native(); + let token_2 = Coin::Dai; + let token_3 = Coin::Monero; + + assert_ok!(Dex::create_pool(token_2)); + assert_ok!(Dex::create_pool(token_3)); + + let ed = get_ed(); + let base1 = 10000; + let base2 = 10000; + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_1, amount: Amount(base1 * 2 + ed) })); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_2, amount: Amount(base2) })); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_3, amount: Amount(base2) })); + + let liquidity1 = 10000; + let liquidity2 = 200; + let liquidity3 = 2000; + + assert_ok!(Dex::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + liquidity1, + liquidity2, + 1, + 1, + user, + )); + assert_ok!(Dex::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_3, + liquidity1, + liquidity3, + 1, + 1, + user, + )); + + let input_amount = 500; + let expect_out2 = Dex::get_amount_out(&input_amount, &liquidity2, &liquidity1).ok().unwrap(); + let expect_out3 = Dex::get_amount_out(&expect_out2, &liquidity1, &liquidity3).ok().unwrap(); + + assert_noop!( + Dex::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_1], + input_amount, + 80, + user, + ), + Error::::InvalidPath + ); + + assert_noop!( + Dex::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_2, token_1, token_2], + input_amount, + 80, + user, + ), + Error::::NonUniquePath + ); + + assert_ok!(Dex::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_2, token_1, token_3], + input_amount, // amount_in + 80, // amount_out_min + user, + )); + + let pool_id1 = (token_1, token_2); + let pool_id2 = (token_1, token_3); + let pallet_account1 = Dex::get_pool_account(&pool_id1); + let pallet_account2 = Dex::get_pool_account(&pool_id2); + + assert_eq!(balance(user, token_2), base2 - liquidity2 - input_amount); + assert_eq!(balance(pallet_account1, token_2), liquidity2 + input_amount); + assert_eq!(balance(pallet_account1, token_1), liquidity1 - expect_out2); + assert_eq!(balance(pallet_account2, token_1), liquidity1 + expect_out2); + assert_eq!(balance(pallet_account2, token_3), liquidity3 - expect_out3); + assert_eq!(balance(user, token_3), 10000 - liquidity3 + expect_out3); + }); +} + +#[test] +fn swap_tokens_for_exact_tokens_in_multi_hops() { + new_test_ext().execute_with(|| { + let user = system_address(b"user1").into(); + let token_1 = Coin::native(); + let token_2 = Coin::Bitcoin; + let token_3 = Coin::Ether; + + assert_ok!(Dex::create_pool(token_2)); + assert_ok!(Dex::create_pool(token_3)); + + let ed = get_ed(); + let base1 = 10000; + let base2 = 10000; + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_1, amount: Amount(base1 * 2 + ed) })); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_2, amount: Amount(base2) })); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_3, amount: Amount(base2) })); + + let liquidity1 = 10000; + let liquidity2 = 200; + let liquidity3 = 2000; + + assert_ok!(Dex::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + liquidity1, + liquidity2, + 1, + 1, + user, + )); + assert_ok!(Dex::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_3, + liquidity1, + liquidity3, + 1, + 1, + user, + )); + + let exchange_out3 = 100; + let expect_in2 = Dex::get_amount_in(&exchange_out3, &liquidity1, &liquidity3).ok().unwrap(); + let expect_in1 = Dex::get_amount_in(&expect_in2, &liquidity2, &liquidity1).ok().unwrap(); + + assert_ok!(Dex::swap_tokens_for_exact_tokens( + RuntimeOrigin::signed(user), + bvec![token_2, token_1, token_3], + exchange_out3, // amount_out + 1000, // amount_in_max + user, + )); + + let pool_id1 = (token_1, token_2); + let pool_id2 = (token_1, token_3); + let pallet_account1 = Dex::get_pool_account(&pool_id1); + let pallet_account2 = Dex::get_pool_account(&pool_id2); + + assert_eq!(balance(user, token_2), base2 - liquidity2 - expect_in1); + assert_eq!(balance(pallet_account1, token_1), liquidity1 - expect_in2); + assert_eq!(balance(pallet_account1, token_2), liquidity2 + expect_in1); + assert_eq!(balance(pallet_account2, token_1), liquidity1 + expect_in2); + assert_eq!(balance(pallet_account2, token_3), liquidity3 - exchange_out3); + assert_eq!(balance(user, token_3), 10000 - liquidity3 + exchange_out3); + }); +} + +#[test] +fn can_not_swap_same_coin() { + new_test_ext().execute_with(|| { + let user = system_address(b"user1").into(); + let token_1 = Coin::Dai; + + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_1, amount: Amount(1000) })); + + let liquidity1 = 1000; + let liquidity2 = 20; + assert_noop!( + Dex::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_1, + liquidity1, + liquidity2, + 1, + 1, + user, + ), + Error::::PoolNotFound + ); + + let exchange_amount = 10; + assert_noop!( + Dex::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![token_1, token_1], + exchange_amount, + 1, + user, + ), + Error::::PoolNotFound + ); + + assert_noop!( + Dex::swap_exact_tokens_for_tokens( + RuntimeOrigin::signed(user), + bvec![Coin::native(), Coin::native()], + exchange_amount, + 1, + user, + ), + Error::::PoolNotFound + ); + }); +} + +#[test] +fn validate_pool_id_sorting() { + new_test_ext().execute_with(|| { + // Serai < Bitcoin < Ether < Dai < Monero. + // coin1 <= coin2 for this test to pass. + let native = Coin::native(); + let coin1 = Coin::Bitcoin; + let coin2 = Coin::Monero; + assert_eq!(Dex::get_pool_id(native, coin2), (native, coin2)); + assert_eq!(Dex::get_pool_id(coin2, native), (native, coin2)); + assert_eq!(Dex::get_pool_id(native, native), (native, native)); + assert_eq!(Dex::get_pool_id(coin2, coin1), (coin1, coin2)); + assert!(coin2 > coin1); + assert!(coin1 <= coin1); + assert_eq!(coin1, coin1); + assert!(native < coin1); + }); +} + +#[test] +fn cannot_block_pool_creation() { + new_test_ext().execute_with(|| { + // User 1 is the pool creator + let user = system_address(b"user1").into(); + // User 2 is the attacker + let attacker = system_address(b"attacker").into(); + + let ed = get_ed(); + assert_ok!(CoinsPallet::mint( + attacker, + Balance { coin: Coin::native(), amount: Amount(10000 + ed) } + )); + + // The target pool the user wants to create is Native <=> Coin(2) + let token_1 = Coin::native(); + let token_2 = Coin::Ether; + + // Attacker computes the still non-existing pool account for the target pair + let pool_account = Dex::get_pool_account(&Dex::get_pool_id(token_2, token_1)); + // And transfers the ED to that pool account + assert_ok!(CoinsPallet::transfer_internal( + attacker, + pool_account, + Balance { coin: Coin::native(), amount: Amount(ed) } + )); + // Then, the attacker creates 14 tokens and sends one of each to the pool account + // skip the token_1 and token_2 coins. + for coin in coins().into_iter().filter(|c| (*c != token_1 && *c != token_2)) { + assert_ok!(CoinsPallet::mint(attacker, Balance { coin, amount: Amount(1000) })); + assert_ok!(CoinsPallet::transfer_internal( + attacker, + pool_account, + Balance { coin, amount: Amount(1) } + )); + } + + // User can still create the pool + assert_ok!(Dex::create_pool(token_2)); + + // User has to transfer one Coin(2) token to the pool account (otherwise add_liquidity will + // fail with `CoinTwoDepositDidNotMeetMinimum`), also transfer native token for the same error. + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_1, amount: Amount(10000 + ed) })); + assert_ok!(CoinsPallet::mint(user, Balance { coin: token_2, amount: Amount(10000) })); + assert_ok!(CoinsPallet::transfer_internal( + user, + pool_account, + Balance { coin: token_2, amount: Amount(1) } + )); + assert_ok!(CoinsPallet::transfer_internal( + user, + pool_account, + Balance { coin: token_1, amount: Amount(100) } + )); + + // add_liquidity shouldn't fail because of the number of consumers + assert_ok!(Dex::add_liquidity( + RuntimeOrigin::signed(user), + token_1, + token_2, + 9900, + 100, + 9900, + 10, + user, + )); + }); +} diff --git a/substrate/dex/pallet/src/weights.rs b/substrate/dex/pallet/src/weights.rs new file mode 100644 index 00000000..23c2c299 --- /dev/null +++ b/substrate/dex/pallet/src/weights.rs @@ -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(PhantomData); +impl WeightInfo for SubstrateWeight { + /// 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)) + } +} diff --git a/substrate/dex/primitives/Cargo.toml b/substrate/dex/primitives/Cargo.toml new file mode 100644 index 00000000..7b5c0a9a --- /dev/null +++ b/substrate/dex/primitives/Cargo.toml @@ -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 , Akil Demir "] +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", +] diff --git a/substrate/dex/primitives/LICENSE-AGPL3 b/substrate/dex/primitives/LICENSE-AGPL3 new file mode 100644 index 00000000..f684d027 --- /dev/null +++ b/substrate/dex/primitives/LICENSE-AGPL3 @@ -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 . diff --git a/substrate/dex/primitives/LICENSE-APACHE2 b/substrate/dex/primitives/LICENSE-APACHE2 new file mode 100644 index 00000000..fbb0616d --- /dev/null +++ b/substrate/dex/primitives/LICENSE-APACHE2 @@ -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/ \ No newline at end of file diff --git a/substrate/dex/primitives/src/lib.rs b/substrate/dex/primitives/src/lib.rs new file mode 100644 index 00000000..725e37f0 --- /dev/null +++ b/substrate/dex/primitives/src/lib.rs @@ -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 { + /// 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 { + /// 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; +} + +/// Result of `MultiCoinIdConverter::try_convert`. +#[cfg_attr(feature = "std", derive(PartialEq, Debug))] +pub enum MultiCoinIdConversionResult { + /// 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 { + /// 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 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 { + /// 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, + amount_in: Balance, + amount_out_min: Option, + send_to: AccountId, + ) -> Result; + + /// 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, + amount_out: Balance, + amount_in_max: Option, + send_to: AccountId, + ) -> Result; +} + +// TODO: Sized should be there? +/// Native coin trait for Dex pallet. +pub trait Currency: 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; + + /// mints the given `amount` into `to`. + fn mint(to: &AccountId, amount: Self::Balance) -> Result; +} + +/// External coin trait for Dex pallet. +pub trait Coins: 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; + + /// mints the given `amount` of `coin` into `to`. + fn mint( + coin: Self::CoinId, + to: &AccountId, + amount: Self::Balance, + ) -> Result; +} + +/// Liquidity tokens trait for Dex pallet. +pub trait LiquidityTokens: 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; + + /// Burns `amount` from `from`. + fn burn_from( + token: Self::CoinId, + from: &AccountId, + amount: Self::Balance, + ) -> Result; + + /// 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; +} + +pub struct CoinConverter; +impl MultiCoinIdConverter 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 { + if coin.is_native() { + MultiCoinIdConversionResult::Native + } else { + MultiCoinIdConversionResult::Converted(*coin) + } + } +} diff --git a/substrate/in-instructions/pallet/Cargo.toml b/substrate/in-instructions/pallet/Cargo.toml index c662c39e..767f3e3c 100644 --- a/substrate/in-instructions/pallet/Cargo.toml +++ b/substrate/in-instructions/pallet/Cargo.toml @@ -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"] diff --git a/substrate/in-instructions/pallet/src/lib.rs b/substrate/in-instructions/pallet/src/lib.rs index c9a0f32d..30b9dae7 100644 --- a/substrate/in-instructions/pallet/src/lib.rs +++ b/substrate/in-instructions/pallet/src/lib.rs @@ -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 + + ValidatorSetsConfig + { type RuntimeEvent: From> + IsType<::RuntimeEvent>; } @@ -50,6 +62,12 @@ pub mod pallet { InstructionFailure { network: NetworkId, id: u32, index: u32 }, } + #[pallet::error] + pub enum Error { + /// Coin and OutAddress types don't match. + InvalidAddressForCoin, + } + #[pallet::pallet] pub struct Pallet(PhantomData); @@ -79,7 +97,120 @@ pub mod pallet { InInstruction::Transfer(address) => { Coins::::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::::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::::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::::balance(IN_INSTRUCTION_EXECUTOR.into(), Coin::Serai).0; + + // add liquidity + Dex::::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::::balance(IN_INSTRUCTION_EXECUTOR.into(), coin); + let sri_balance = Coins::::balance(IN_INSTRUCTION_EXECUTOR.into(), Coin::Serai); + if coin_balance != Amount(0) { + Coins::::transfer_internal( + IN_INSTRUCTION_EXECUTOR.into(), + address.into(), + Balance { coin, amount: coin_balance }, + )?; + } + if sri_balance != Amount(0) { + Coins::::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::::InvalidAddressForCoin)?; + } + + // mint the given coin on our account + Coins::::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::::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::::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::::burn_non_sri(IN_INSTRUCTION_EXECUTOR.into(), instruction)?; + } + } + } + } } Ok(()) } diff --git a/substrate/in-instructions/primitives/Cargo.toml b/substrate/in-instructions/primitives/Cargo.toml index 789b1bb5..6f445ed2 100644 --- a/substrate/in-instructions/primitives/Cargo.toml +++ b/substrate/in-instructions/primitives/Cargo.toml @@ -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", diff --git a/substrate/in-instructions/primitives/src/lib.rs b/substrate/in-instructions/primitives/src/lib.rs index 74349de9..56a7ec42 100644 --- a/substrate/in-instructions/primitives/src/lib.rs +++ b/substrate/in-instructions/primitives/src/lib.rs @@ -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 { + match self { + Self::Serai(addr) => Some(addr), + _ => None, + } + } + + pub fn as_external(self) -> Option { + 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( diff --git a/substrate/in-instructions/primitives/src/shorthand.rs b/substrate/in-instructions/primitives/src/shorthand.rs index 8f415add..e6a8de51 100644 --- a/substrate/in-instructions/primitives/src/shorthand.rs +++ b/substrate/in-instructions/primitives/src/shorthand.rs @@ -26,7 +26,7 @@ pub enum Shorthand { minimum: Amount, out: OutInstruction, }, - AddLiquidity { + SwapAndAddLiquidity { origin: Option, minimum: Amount, gas: Amount, @@ -47,7 +47,7 @@ impl TryFrom for RefundableInInstruction { Ok(match shorthand { Shorthand::Raw(instruction) => instruction, Shorthand::Swap { .. } => todo!(), - Shorthand::AddLiquidity { .. } => todo!(), + Shorthand::SwapAndAddLiquidity { .. } => todo!(), }) } } diff --git a/substrate/liquidity-tokens/pallet/Cargo.toml b/substrate/liquidity-tokens/pallet/Cargo.toml new file mode 100644 index 00000000..467911df --- /dev/null +++ b/substrate/liquidity-tokens/pallet/Cargo.toml @@ -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 "] +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"] diff --git a/substrate/liquidity-tokens/pallet/LICENSE b/substrate/liquidity-tokens/pallet/LICENSE new file mode 100644 index 00000000..f684d027 --- /dev/null +++ b/substrate/liquidity-tokens/pallet/LICENSE @@ -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 . diff --git a/substrate/liquidity-tokens/pallet/src/lib.rs b/substrate/liquidity-tokens/pallet/src/lib.rs new file mode 100644 index 00000000..3ed46eb2 --- /dev/null +++ b/substrate/liquidity-tokens/pallet/src/lib.rs @@ -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 { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + } + + #[pallet::error] + pub enum Error { + AmountOverflowed, + NotEnoughCoins, + } + + #[pallet::event] + #[pallet::generate_deposit(fn deposit_event)] + pub enum Event { + LtMint { to: Public, token: u32, amount: Amount }, + LtBurn { from: Public, token: u32, amount: Amount }, + } + + #[pallet::pallet] + pub struct Pallet(PhantomData); + + /// 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 = 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 = StorageMap<_, Blake2_128Concat, u32, SubstrateAmount, ValueQuery>; + + // TODO: apis: supply, mint, burn, transfer + impl Pallet { + /// 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> { + let balance = Self::balances(to, token).unwrap_or(0); + + // update the balance + let new_amount = balance.checked_add(amount).ok_or(Error::::AmountOverflowed)?; + + // save + Balances::::set(to, token, Some(new_amount)); + + // update the supply + let new_supply = + Self::supply(token).checked_add(amount).ok_or(Error::::AmountOverflowed)?; + Supply::::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> { + let balance = Self::balances(from, token); + if balance.is_none() { + Err(Error::::NotEnoughCoins)?; + } + + // update the balance + let new_amount = balance.unwrap().checked_sub(amount).ok_or(Error::::NotEnoughCoins)?; + + // save + if new_amount == 0 { + Balances::::remove(from, token); + } else { + Balances::::set(from, token, Some(new_amount)); + } + + // update the supply + let new_supply = Self::supply(token).checked_sub(amount).unwrap(); + if new_supply == 0 { + Supply::::remove(token); + } else { + Supply::::set(token, new_supply); + } + + Self::deposit_event(Event::LtBurn { from, token, amount: Amount(amount) }); + Ok(()) + } + + pub fn total_issuance(token: u32) -> SubstrateAmount { + Supply::::get(token) + } + } + + impl LiquidityTokens for Pallet { + type Balance = SubstrateAmount; + type CoinId = u32; + + fn mint_into( + token: Self::CoinId, + to: &Public, + amount: Self::Balance, + ) -> Result { + Self::mint_into(token, *to, amount)?; + Ok(amount) + } + + fn burn_from( + token: Self::CoinId, + from: &Public, + amount: Self::Balance, + ) -> Result { + Self::burn_from(token, *from, amount)?; + Ok(amount) + } + + fn total_issuance(token: Self::CoinId) -> Self::Balance { + Self::total_issuance(token) + } + + fn coin_ids() -> Vec { + Supply::::iter_keys().collect::>() + } + + fn balance(token: Self::CoinId, of: &Public) -> Self::Balance { + Self::balance(token, *of) + } + } +} + +pub use pallet::*; diff --git a/substrate/node/src/chain_spec.rs b/substrate/node/src/chain_spec.rs index 6941296f..e3dd4669 100644 --- a/substrate/node/src/chain_spec.rs +++ b/substrate/node/src/chain_spec.rs @@ -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; @@ -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() diff --git a/substrate/primitives/src/networks.rs b/substrate/primitives/src/networks.rs index 608217b8..0e8b8c0e 100644 --- a/substrate/primitives/src/networks.rs +++ b/substrate/primitives/src/networks.rs @@ -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 diff --git a/substrate/runtime/Cargo.toml b/substrate/runtime/Cargo.toml index 58563245..6b5d60fb 100644 --- a/substrate/runtime/Cargo.toml +++ b/substrate/runtime/Cargo.toml @@ -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", ] diff --git a/substrate/runtime/src/lib.rs b/substrate/runtime/src/lib.rs index 0ea3c246..b4c86e84 100644 --- a/substrate/runtime/src/lib.rs +++ b/substrate/runtime/src/lib.rs @@ -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 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; + + #[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, diff --git a/substrate/validator-sets/pallet/src/lib.rs b/substrate/validator-sets/pallet/src/lib.rs index f15310f8..83947d32 100644 --- a/substrate/validator-sets/pallet/src/lib.rs +++ b/substrate/validator-sets/pallet/src/lib.rs @@ -409,7 +409,7 @@ pub mod pallet { impl Pallet { 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.