diff --git a/Cargo.lock b/Cargo.lock index c9406a006..0e3cf8194 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7397,6 +7397,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "serai-coins-primitives", + "serai-in-instructions-primitives", "serai-primitives", "sp-core", "sp-runtime", diff --git a/substrate/client/tests/dex.rs b/substrate/client/tests/dex.rs index 8796fe0b1..6aa56ddc0 100644 --- a/substrate/client/tests/dex.rs +++ b/substrate/client/tests/dex.rs @@ -11,7 +11,7 @@ use serai_client::{ SeraiAddress, PublicKey, }, in_instructions::primitives::{ - InInstruction, InInstructionWithBalance, Batch, IN_INSTRUCTION_EXECUTOR, OutAddress, + InInstruction, InInstructionWithBalance, Batch, ADD_LIQUIDITY_ACCOUNT, SWAP_ACCOUNT, OutAddress, }, dex::DexEvent, Serai, @@ -270,7 +270,7 @@ serai_test!( assert_eq!( events, vec![DexEvent::LiquidityAdded { - who: IN_INSTRUCTION_EXECUTOR, + who: ADD_LIQUIDITY_ACCOUNT, mint_to: pair.public().into(), pool_id: Coin::Bitcoin, coin_amount: 10_000_000_000_000, // half of sent amount @@ -357,8 +357,8 @@ serai_test!( assert_eq!( events, vec![DexEvent::SwapExecuted { - who: IN_INSTRUCTION_EXECUTOR, - send_to: IN_INSTRUCTION_EXECUTOR, + who: SWAP_ACCOUNT, + send_to: SWAP_ACCOUNT, path, amount_in: 20_000_000_000_000, amount_out: 11066655622377 @@ -396,7 +396,7 @@ serai_test!( assert_eq!( events, vec![DexEvent::SwapExecuted { - who: IN_INSTRUCTION_EXECUTOR, + who: SWAP_ACCOUNT, send_to: out_address.as_native().unwrap(), path, amount_in: 20_000_000_000_000, @@ -434,7 +434,7 @@ serai_test!( assert_eq!( events, vec![DexEvent::SwapExecuted { - who: IN_INSTRUCTION_EXECUTOR, + who: SWAP_ACCOUNT, send_to: out_address.as_native().unwrap(), path, amount_in: 10_000_000_000_000, diff --git a/substrate/coins/pallet/Cargo.toml b/substrate/coins/pallet/Cargo.toml index 75011add0..40ac4c6c3 100644 --- a/substrate/coins/pallet/Cargo.toml +++ b/substrate/coins/pallet/Cargo.toml @@ -33,6 +33,7 @@ pallet-transaction-payment = { git = "https://github.com/serai-dex/substrate", d serai-primitives = { path = "../../primitives", default-features = false, features = ["serde"] } coins-primitives = { package = "serai-coins-primitives", path = "../primitives", default-features = false } +in-ins-primitives = { package = "serai-in-instructions-primitives", path = "../../in-instructions/primitives", default-features = false } [features] std = [ @@ -47,6 +48,7 @@ std = [ "serai-primitives/std", "coins-primitives/std", + "in-ins-primitives/std", ] runtime-benchmarks = [ diff --git a/substrate/coins/pallet/src/lib.rs b/substrate/coins/pallet/src/lib.rs index 510e0edfb..de1920046 100644 --- a/substrate/coins/pallet/src/lib.rs +++ b/substrate/coins/pallet/src/lib.rs @@ -3,11 +3,11 @@ use serai_primitives::{Coin, SubstrateAmount, Balance}; pub trait AllowMint { - fn is_allowed(balance: &Balance) -> bool; + fn is_allowed(balance: &Balance, for_swap: bool) -> bool; } impl AllowMint for () { - fn is_allowed(_: &Balance) -> bool { + fn is_allowed(_: &Balance, _: bool) -> bool { true } } @@ -33,6 +33,8 @@ pub mod pallet { pub use coins_primitives as primitives; use primitives::*; + use in_ins_primitives::SWAP_ACCOUNT; + type LiquidityTokensInstance = crate::Instance1; #[pallet::config] @@ -159,7 +161,7 @@ pub mod pallet { /// /// Errors if any amount overflows. pub fn mint(to: Public, balance: Balance) -> Result<(), Error> { - if !T::AllowMint::is_allowed(&balance) { + if !T::AllowMint::is_allowed(&balance, to == SWAP_ACCOUNT.into()) { Err(Error::::MintNotAllowed)?; } diff --git a/substrate/dex/pallet/src/lib.rs b/substrate/dex/pallet/src/lib.rs index 352205029..9420dcc60 100644 --- a/substrate/dex/pallet/src/lib.rs +++ b/substrate/dex/pallet/src/lib.rs @@ -151,6 +151,7 @@ pub mod pallet { /// Map from `PoolId` to `()`. This establishes whether a pool has been officially /// created rather than people sending tokens directly to a pool's public account. #[pallet::storage] + #[pallet::getter(fn pools)] pub type Pools = StorageMap<_, Blake2_128Concat, PoolId, (), OptionQuery>; #[pallet::storage] diff --git a/substrate/in-instructions/pallet/src/lib.rs b/substrate/in-instructions/pallet/src/lib.rs index 3ec63ae58..82de0820d 100644 --- a/substrate/in-instructions/pallet/src/lib.rs +++ b/substrate/in-instructions/pallet/src/lib.rs @@ -52,6 +52,10 @@ pub mod pallet { pub enum Error { /// Coin and OutAddress types don't match. InvalidAddressForCoin, + /// SRI minting is not allowed. + CantMintSRI, + /// There is too much coin that is not in the pool. + TooMuchCoinOutOfPool, } #[pallet::pallet] @@ -85,6 +89,34 @@ pub mod pallet { fn execute(instruction: InInstructionWithBalance) -> Result<(), DispatchError> { match instruction.instruction { InInstruction::Transfer(address) => { + let coin = instruction.balance.coin; + if coin == Coin::Serai { + Err(Error::::CantMintSRI)?; + } + + // check how much in-pool vs out-of-pool if we have a pool and has certain amount of + // liquidty available. + let pool_id = Dex::::get_pool_id(coin, Coin::Serai).unwrap(); + if Dex::::pools(pool_id).is_some() { + let pool_account = Dex::::get_pool_account(pool_id); + let in_pool_sri = Coins::::balance(pool_account, Coin::Serai).0; + + // TODO: should be a const where? + if in_pool_sri >= 10_000 * 10_u64.pow(8) { + let in_pool = Coins::::balance(pool_account, coin).0; + + // get out-of-pool + let total = Coins::::supply(coin); + let out_of_pool = total - in_pool; + let new_out_of_pool = out_of_pool.saturating_add(instruction.balance.amount.0); + + // only allow if out-of-pool is less than 20% of pool value + if new_out_of_pool > in_pool / 5 { + Err(Error::::TooMuchCoinOutOfPool)?; + } + } + } + Coins::::mint(address.into(), instruction.balance)?; } InInstruction::Dex(call) => { @@ -93,11 +125,11 @@ pub mod pallet { // called directly from Serai with a native transaction. match call { DexCall::SwapAndAddLiquidity(address) => { - let origin = RawOrigin::Signed(IN_INSTRUCTION_EXECUTOR.into()); + let origin = RawOrigin::Signed(ADD_LIQUIDITY_ACCOUNT.into()); let coin = instruction.balance.coin; // mint the given coin on the account - Coins::::mint(IN_INSTRUCTION_EXECUTOR.into(), instruction.balance)?; + Coins::::mint(ADD_LIQUIDITY_ACCOUNT.into(), instruction.balance)?; // swap half of it for SRI let half = instruction.balance.amount.0 / 2; @@ -107,11 +139,11 @@ pub mod pallet { path, half, 1, // minimum out, so we accept whatever we get. - IN_INSTRUCTION_EXECUTOR.into(), + ADD_LIQUIDITY_ACCOUNT.into(), )?; // get how much we got for our swap - let sri_amount = Coins::::balance(IN_INSTRUCTION_EXECUTOR.into(), Coin::Serai).0; + let sri_amount = Coins::::balance(ADD_LIQUIDITY_ACCOUNT.into(), Coin::Serai).0; // add liquidity Dex::::add_liquidity( @@ -127,18 +159,18 @@ pub mod pallet { // 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); + let coin_balance = Coins::::balance(ADD_LIQUIDITY_ACCOUNT.into(), coin); + let sri_balance = Coins::::balance(ADD_LIQUIDITY_ACCOUNT.into(), Coin::Serai); if coin_balance != Amount(0) { Coins::::transfer_internal( - IN_INSTRUCTION_EXECUTOR.into(), + ADD_LIQUIDITY_ACCOUNT.into(), address.into(), Balance { coin, amount: coin_balance }, )?; } if sri_balance != Amount(0) { Coins::::transfer_internal( - IN_INSTRUCTION_EXECUTOR.into(), + ADD_LIQUIDITY_ACCOUNT.into(), address.into(), Balance { coin: Coin::Serai, amount: sri_balance }, )?; @@ -154,7 +186,7 @@ pub mod pallet { } // mint the given coin on our account - Coins::::mint(IN_INSTRUCTION_EXECUTOR.into(), instruction.balance)?; + Coins::::mint(SWAP_ACCOUNT.into(), instruction.balance)?; // get the path let mut path = vec![instruction.balance.coin, Coin::Serai]; @@ -166,13 +198,13 @@ pub mod pallet { // 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 + SWAP_ACCOUNT } else { out_address.clone().as_native().unwrap() }; // do the swap - let origin = RawOrigin::Signed(IN_INSTRUCTION_EXECUTOR.into()); + let origin = RawOrigin::Signed(SWAP_ACCOUNT.into()); Dex::::swap_exact_tokens_for_tokens( origin.clone().into(), BoundedVec::try_from(path).unwrap(), @@ -185,8 +217,7 @@ pub mod pallet { // 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 coin_balance = Coins::::balance(SWAP_ACCOUNT.into(), out_balance.coin); let instruction = OutInstructionWithBalance { instruction: OutInstruction { address: out_address.as_external().unwrap(), diff --git a/substrate/in-instructions/primitives/src/lib.rs b/substrate/in-instructions/primitives/src/lib.rs index afa11cac1..01788100f 100644 --- a/substrate/in-instructions/primitives/src/lib.rs +++ b/substrate/in-instructions/primitives/src/lib.rs @@ -27,8 +27,11 @@ 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"); +// This is the account which will be the origin for add liquidity instructions. +pub const ADD_LIQUIDITY_ACCOUNT: SeraiAddress = system_address(b"add-liquidty-account"); + +// This is the account which will be the origin for swap intructions. +pub const SWAP_ACCOUNT: SeraiAddress = system_address(b"swap-account"); #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] #[cfg_attr(feature = "std", derive(Zeroize))] diff --git a/substrate/validator-sets/pallet/src/lib.rs b/substrate/validator-sets/pallet/src/lib.rs index c1c7b1798..e0e11fc66 100644 --- a/substrate/validator-sets/pallet/src/lib.rs +++ b/substrate/validator-sets/pallet/src/lib.rs @@ -1031,14 +1031,20 @@ pub mod pallet { } impl AllowMint for Pallet { - fn is_allowed(balance: &Balance) -> bool { + fn is_allowed(balance: &Balance, for_swap: bool) -> bool { // get the required stake let current_required = Self::required_stake_for_network(balance.coin.network()); let new_required = current_required + Self::required_stake(balance); // get the total stake for the network & compare. - let staked = Self::total_allocated_stake(balance.coin.network()).unwrap_or(Amount(0)); - staked.0 >= new_required + let mut staked = Self::total_allocated_stake(balance.coin.network()).unwrap_or(Amount(0)).0; + + // we leave 5% buffer on our stake for swap mints. + if !for_swap { + staked -= staked / 20; // TODO: this should be a const but where? + } + + staked >= new_required } }