Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Some improvements on economic security #522

Open
wants to merge 1 commit into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions substrate/client/tests/dex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions substrate/coins/pallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -47,6 +48,7 @@ std = [

"serai-primitives/std",
"coins-primitives/std",
"in-ins-primitives/std",
]

runtime-benchmarks = [
Expand Down
8 changes: 5 additions & 3 deletions substrate/coins/pallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand All @@ -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]
Expand Down Expand Up @@ -159,7 +161,7 @@ pub mod pallet {
///
/// Errors if any amount overflows.
pub fn mint(to: Public, balance: Balance) -> Result<(), Error<T, I>> {
if !T::AllowMint::is_allowed(&balance) {
if !T::AllowMint::is_allowed(&balance, to == SWAP_ACCOUNT.into()) {
Err(Error::<T, I>::MintNotAllowed)?;
}

Expand Down
1 change: 1 addition & 0 deletions substrate/dex/pallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: Config> = StorageMap<_, Blake2_128Concat, PoolId, (), OptionQuery>;

#[pallet::storage]
Expand Down
57 changes: 44 additions & 13 deletions substrate/in-instructions/pallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ pub mod pallet {
pub enum Error<T> {
/// 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]
Expand Down Expand Up @@ -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::<T>::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::<T>::get_pool_id(coin, Coin::Serai).unwrap();
if Dex::<T>::pools(pool_id).is_some() {
let pool_account = Dex::<T>::get_pool_account(pool_id);
let in_pool_sri = Coins::<T>::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::<T>::balance(pool_account, coin).0;

// get out-of-pool
let total = Coins::<T>::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::<T>::TooMuchCoinOutOfPool)?;
}
}
}

Coins::<T>::mint(address.into(), instruction.balance)?;
}
InInstruction::Dex(call) => {
Expand All @@ -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::<T>::mint(IN_INSTRUCTION_EXECUTOR.into(), instruction.balance)?;
Coins::<T>::mint(ADD_LIQUIDITY_ACCOUNT.into(), instruction.balance)?;

// swap half of it for SRI
let half = instruction.balance.amount.0 / 2;
Expand All @@ -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::<T>::balance(IN_INSTRUCTION_EXECUTOR.into(), Coin::Serai).0;
let sri_amount = Coins::<T>::balance(ADD_LIQUIDITY_ACCOUNT.into(), Coin::Serai).0;

// add liquidity
Dex::<T>::add_liquidity(
Expand All @@ -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::<T>::balance(IN_INSTRUCTION_EXECUTOR.into(), coin);
let sri_balance = Coins::<T>::balance(IN_INSTRUCTION_EXECUTOR.into(), Coin::Serai);
let coin_balance = Coins::<T>::balance(ADD_LIQUIDITY_ACCOUNT.into(), coin);
let sri_balance = Coins::<T>::balance(ADD_LIQUIDITY_ACCOUNT.into(), Coin::Serai);
if coin_balance != Amount(0) {
Coins::<T>::transfer_internal(
IN_INSTRUCTION_EXECUTOR.into(),
ADD_LIQUIDITY_ACCOUNT.into(),
address.into(),
Balance { coin, amount: coin_balance },
)?;
}
if sri_balance != Amount(0) {
Coins::<T>::transfer_internal(
IN_INSTRUCTION_EXECUTOR.into(),
ADD_LIQUIDITY_ACCOUNT.into(),
address.into(),
Balance { coin: Coin::Serai, amount: sri_balance },
)?;
Expand All @@ -154,7 +186,7 @@ pub mod pallet {
}

// mint the given coin on our account
Coins::<T>::mint(IN_INSTRUCTION_EXECUTOR.into(), instruction.balance)?;
Coins::<T>::mint(SWAP_ACCOUNT.into(), instruction.balance)?;

// get the path
let mut path = vec![instruction.balance.coin, Coin::Serai];
Expand All @@ -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::<T>::swap_exact_tokens_for_tokens(
origin.clone().into(),
BoundedVec::try_from(path).unwrap(),
Expand All @@ -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::<T>::balance(IN_INSTRUCTION_EXECUTOR.into(), out_balance.coin);
let coin_balance = Coins::<T>::balance(SWAP_ACCOUNT.into(), out_balance.coin);
let instruction = OutInstructionWithBalance {
instruction: OutInstruction {
address: out_address.as_external().unwrap(),
Expand Down
7 changes: 5 additions & 2 deletions substrate/in-instructions/primitives/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand Down
12 changes: 9 additions & 3 deletions substrate/validator-sets/pallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1031,14 +1031,20 @@ pub mod pallet {
}

impl<T: Config> AllowMint for Pallet<T> {
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
}
}

Expand Down
Loading