diff --git a/Cargo.lock b/Cargo.lock index f4fb2868..dced179e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8522,22 +8522,14 @@ dependencies = [ "bitvec", "frame-support", "frame-system", - "pallet-babe", - "pallet-grandpa", - "pallet-timestamp", "parity-scale-codec", + "serai-abi", "serai-coins-pallet", + "serai-core-pallet", "serai-dex-pallet", - "serai-economic-security-pallet", - "serai-emissions-pallet", "serai-genesis-liquidity-pallet", - "serai-primitives", "serai-validator-sets-pallet", - "sp-application-crypto", "sp-core", - "sp-io", - "sp-runtime", - "sp-std", ] [[package]] @@ -9025,6 +9017,7 @@ dependencies = [ "serai-core-pallet", "serai-dex-pallet", "serai-genesis-liquidity-pallet", + "serai-in-instructions-pallet", "serai-signals-pallet", "serai-validator-sets-pallet", "sp-api", diff --git a/substrate/in-instructions/Cargo.toml b/substrate/in-instructions/Cargo.toml index 0f32921e..a9cb2ca2 100644 --- a/substrate/in-instructions/Cargo.toml +++ b/substrate/in-instructions/Cargo.toml @@ -3,7 +3,7 @@ name = "serai-in-instructions-pallet" version = "0.1.0" description = "Execute calls via In Instructions from unsigned transactions" license = "AGPL-3.0-only" -repository = "https://github.com/serai-dex/serai/tree/develop/substrate/genesis-liquidity" +repository = "https://github.com/serai-dex/serai/tree/develop/substrate/in-instructions" authors = ["Luke Parker "] edition = "2021" publish = false @@ -20,67 +20,60 @@ ignored = ["scale"] workspace = true [dependencies] -bitvec = { version = "1", default-features = false, features = ["alloc"] } +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } -scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive", "max-encoded-len"] } - -sp-std = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false } -sp-application-crypto = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false } -sp-io = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false } -sp-runtime = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false } sp-core = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false } frame-system = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false } frame-support = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false } -serai-primitives = { path = "../primitives", default-features = false } - -coins-pallet = { package = "serai-coins-pallet", path = "../coins", default-features = false } -dex-pallet = { package = "serai-dex-pallet", path = "../dex", default-features = false } -validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../validator-sets", default-features = false } -genesis-liquidity-pallet = { package = "serai-genesis-liquidity-pallet", path = "../genesis-liquidity", default-features = false } -emissions-pallet = { package = "serai-emissions-pallet", path = "../emissions", default-features = false } - -[dev-dependencies] -pallet-babe = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false } -pallet-grandpa = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false } -pallet-timestamp = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false } - -economic-security-pallet = { package = "serai-economic-security-pallet", path = "../economic-security", default-features = false } +bitvec = { version = "1", default-features = false, features = ["alloc"] } +serai-abi = { path = "../abi", default-features = false, features = ["substrate"] } +serai-core-pallet = { path = "../core", default-features = false } +serai-coins-pallet = { path = "../coins", default-features = false } +serai-validator-sets-pallet = { path = "../validator-sets", default-features = false } +serai-dex-pallet = { path = "../dex", default-features = false } +serai-genesis-liquidity-pallet = { path = "../genesis-liquidity", default-features = false } [features] std = [ "scale/std", - "sp-std/std", - "sp-application-crypto/std", - "sp-io/std", - "sp-runtime/std", "sp-core/std", "frame-system/std", "frame-support/std", - "serai-primitives/std", - - "coins-pallet/std", - "dex-pallet/std", - "validator-sets-pallet/std", - "genesis-liquidity-pallet/std", - "emissions-pallet/std", - - "economic-security-pallet/std", - - "pallet-babe/std", - "pallet-grandpa/std", - "pallet-timestamp/std", + "bitvec/std", + "serai-abi/std", + "serai-core-pallet/std", + "serai-coins-pallet/std", + "serai-validator-sets-pallet/std", + "serai-dex-pallet/std", + "serai-genesis-liquidity-pallet/std", ] try-runtime = [ "frame-system/try-runtime", "frame-support/try-runtime", - "sp-runtime/try-runtime", + "serai-abi/try-runtime", + "serai-core-pallet/try-runtime", + "serai-coins-pallet/try-runtime", + "serai-validator-sets-pallet/try-runtime", + "serai-dex-pallet/try-runtime", + "serai-genesis-liquidity-pallet/try-runtime", +] + +runtime-benchmarks = [ + "frame-system/runtime-benchmarks", + "frame-support/runtime-benchmarks", + + "serai-core-pallet/runtime-benchmarks", + "serai-coins-pallet/runtime-benchmarks", + "serai-validator-sets-pallet/runtime-benchmarks", + "serai-dex-pallet/runtime-benchmarks", + "serai-genesis-liquidity-pallet/runtime-benchmarks", ] default = ["std"] diff --git a/substrate/in-instructions/README.md b/substrate/in-instructions/README.md new file mode 100644 index 00000000..dce85e6f --- /dev/null +++ b/substrate/in-instructions/README.md @@ -0,0 +1 @@ +# Serai In-Instructions Pallet diff --git a/substrate/in-instructions/src/lib.rs b/substrate/in-instructions/src/lib.rs index 152fa2d3..739ecb3a 100644 --- a/substrate/in-instructions/src/lib.rs +++ b/substrate/in-instructions/src/lib.rs @@ -1,387 +1,58 @@ -#![cfg_attr(docsrs, feature(doc_cfg))] -#![cfg_attr(docsrs, feature(doc_cfg))] -#![cfg_attr(not(feature = "std"), no_std)] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] +#![cfg_attr(not(any(feature = "std", test)), no_std)] -use sp_io::hashing::blake2_256; +extern crate alloc; -use serai_primitives::*; - -pub use in_instructions_primitives as primitives; -use primitives::*; - -#[cfg(test)] -mod mock; - -#[cfg(test)] -mod tests; - -// TODO: Investigate why Substrate generates these -#[allow( - unreachable_patterns, - clippy::cast_possible_truncation, - clippy::no_effect_underscore_binding, - clippy::empty_docs -)] +#[expect(clippy::cast_possible_truncation)] #[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; - +mod pallet { + use frame_system::pallet_prelude::*; 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, ExternalValidatorSet}, - Config as ValidatorSetsConfig, Pallet as ValidatorSets, - }; + use serai_abi::{primitives::prelude::*, in_instructions::Event}; - use genesis_liquidity_pallet::{ - Pallet as GenesisLiq, Config as GenesisLiqConfig, primitives::GENESIS_LIQUIDITY_ACCOUNT, - }; - use emissions_pallet::{Pallet as Emissions, Config as EmissionsConfig, primitives::POL_ACCOUNT}; + use serai_core_pallet::Pallet as Core; + type Coins = serai_coins_pallet::Pallet; + type LiquidityTokens = + serai_coins_pallet::Pallet; use super::*; + /// The configuration of this pallet. #[pallet::config] pub trait Config: frame_system::Config - + CoinsConfig - + DexConfig - + ValidatorSetsConfig - + GenesisLiqConfig - + EmissionsConfig + + serai_core_pallet::Config + + serai_coins_pallet::Config + + serai_validator_sets_pallet::Config + + serai_coins_pallet::Config + + serai_dex_pallet::Config + + serai_genesis_liquidity_pallet::Config { - type RuntimeEvent: From> + IsType<::RuntimeEvent>; - } - - #[pallet::event] - #[pallet::generate_deposit(fn deposit_event)] - pub enum Event { - Batch { - network: ExternalNetworkId, - publishing_session: Session, - id: u32, - external_network_block_hash: BlockHash, - in_instructions_hash: [u8; 32], - in_instruction_results: bitvec::vec::BitVec, - }, - Halt { - network: ExternalNetworkId, - }, } + /// An error incurred. #[pallet::error] - pub enum Error { - /// Coin and OutAddress types don't match. - InvalidAddressForCoin, - } + pub enum Error {} + /// The Pallet struct. #[pallet::pallet] - pub struct Pallet(PhantomData); - - // The ID of the last executed Batch for a network. - #[pallet::storage] - #[pallet::getter(fn batches)] - pub(crate) type LastBatch = - StorageMap<_, Identity, ExternalNetworkId, u32, OptionQuery>; - - // The last Serai block in which this validator set included a batch - #[pallet::storage] - #[pallet::getter(fn last_batch_block)] - pub(crate) type LastBatchBlock = - StorageMap<_, Identity, ExternalNetworkId, BlockNumberFor, OptionQuery>; - - // Halted networks. - #[pallet::storage] - pub(crate) type Halted = StorageMap<_, Identity, ExternalNetworkId, (), OptionQuery>; + pub struct Pallet(_); impl Pallet { - // Use a dedicated transaction layer when executing this InInstruction - // This lets it individually error without causing any storage modifications - #[frame_support::transactional] - fn execute(instruction: &InInstructionWithBalance) -> Result<(), DispatchError> { - match &instruction.instruction { - InInstruction::Transfer(address) => { - Coins::::mint((*address).into(), instruction.balance.into())?; - } - 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 address = *address; - let coin = instruction.balance.coin; - - // mint the given coin on the account - Coins::::mint(IN_INSTRUCTION_EXECUTOR.into(), instruction.balance.into())?; - - // swap half of it for SRI - let half = instruction.balance.amount.0 / 2; - let path = BoundedVec::try_from(vec![coin.into(), 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, - 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.into()); - 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: coin.into(), 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.into())?; - - // get the path - let mut path = vec![instruction.balance.coin.into(), 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.clone().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.clone().as_external().unwrap(), - }, - balance: ExternalBalance { - coin: out_balance.coin.try_into().unwrap(), - amount: coin_balance, - }, - }; - Coins::::burn_with_instruction(origin.into(), instruction)?; - } - } - } - } - InInstruction::GenesisLiquidity(address) => { - Coins::::mint(GENESIS_LIQUIDITY_ACCOUNT.into(), instruction.balance.into())?; - GenesisLiq::::add_coin_liquidity((*address).into(), instruction.balance)?; - } - InInstruction::SwapToStakedSRI(address, network) => { - Coins::::mint(POL_ACCOUNT.into(), instruction.balance.into())?; - Emissions::::swap_to_staked_sri((*address).into(), *network, instruction.balance)?; - } - } - Ok(()) + fn emit_event(event: Event) { + Core::::emit_event(event) } - - pub fn halt(network: ExternalNetworkId) -> Result<(), DispatchError> { - Halted::::set(network, Some(())); - Self::deposit_event(Event::Halt { network }); - Ok(()) - } - } - - fn keys_for_network( - network: ExternalNetworkId, - ) -> Result<(Session, Option, Option), InvalidTransaction> { - // If there's no session set, and therefore no keys set, then this must be an invalid signature - let Some(session) = ValidatorSets::::session(NetworkId::from(network)) else { - Err(InvalidTransaction::BadProof)? - }; - let mut set = ExternalValidatorSet { network, session }; - let latest = ValidatorSets::::keys(set).map(|keys| keys.0); - let prior = if set.session.0 != 0 { - set.session.0 -= 1; - ValidatorSets::::keys(set).map(|keys| keys.0) - } else { - None - }; - if prior.is_none() && latest.is_none() { - Err(InvalidTransaction::BadProof)?; - } - Ok((session, prior, latest)) } #[pallet::call] impl Pallet { + /// Execute a batch of `InInstruction`s. #[pallet::call_index(0)] - #[pallet::weight((0, DispatchClass::Operational))] // TODO - pub fn execute_batch(origin: OriginFor, _batch: SignedBatch) -> DispatchResult { - ensure_none(origin)?; - - // The entire Batch execution is handled in pre_dispatch - - Ok(()) - } - } - - #[pallet::validate_unsigned] - impl ValidateUnsigned for Pallet { - type Call = Call; - - fn validate_unsigned(_: TransactionSource, call: &Self::Call) -> TransactionValidity { - // Match to be exhaustive - let batch = match call { - Call::execute_batch { ref batch } => batch, - Call::__Ignore(_, _) => unreachable!(), - }; - - // verify the batch size - // TODO: Merge this encode with the one done by batch_message - if batch.batch.encode().len() > MAX_BATCH_SIZE { - Err(InvalidTransaction::ExhaustsResources)?; - } - let network = batch.batch.network; - - // verify the signature - let (current_session, prior, current) = keys_for_network::(network)?; - let prior_session = Session(current_session.0 - 1); - let batch_message = batch_message(&batch.batch); - // Check the prior key first since only a single `Batch` (the last one) will be when prior is - // Some yet prior wasn't the signing key - let valid_by_prior = - if let Some(key) = prior { key.verify(&batch_message, &batch.signature) } else { false }; - let valid = valid_by_prior || - (if let Some(key) = current { - key.verify(&batch_message, &batch.signature) - } else { - false - }); - if !valid { - Err(InvalidTransaction::BadProof)?; - } - - let batch = &batch.batch; - - if Halted::::contains_key(network) { - Err(InvalidTransaction::Custom(1))?; - } - - // If it wasn't valid by the prior key, meaning it was valid by the current key, the current - // key is publishing `Batch`s. This should only happen once the current key has verified all - // `Batch`s published by the prior key, meaning they are accepting the hand-over. - if prior.is_some() && (!valid_by_prior) { - ValidatorSets::::retire_set(ValidatorSet { - network: network.into(), - session: prior_session, - }); - } - - // check that this validator set isn't publishing a batch more than once per block - let current_block = >::block_number(); - let last_block = LastBatchBlock::::get(network).unwrap_or(Zero::zero()); - if last_block >= current_block { - Err(InvalidTransaction::Future)?; - } - LastBatchBlock::::insert(batch.network, frame_system::Pallet::::block_number()); - - // Verify the batch is sequential - // LastBatch has the last ID set. The next ID should be it + 1 - // If there's no ID, the next ID should be 0 - let expected = LastBatch::::get(network).map_or(0, |prev| prev + 1); - if batch.id < expected { - Err(InvalidTransaction::Stale)?; - } - if batch.id > expected { - Err(InvalidTransaction::Future)?; - } - LastBatch::::insert(batch.network, batch.id); - - let in_instructions_hash = blake2_256(&batch.instructions.encode()); - let mut in_instruction_results = bitvec::vec::BitVec::new(); - for instruction in &batch.instructions { - // Verify this coin is for this network - if instruction.balance.coin.network() != batch.network { - Err(InvalidTransaction::Custom(2))?; - } - - in_instruction_results.push(Self::execute(instruction).is_ok()); - } - - Self::deposit_event(Event::Batch { - network: batch.network, - publishing_session: if valid_by_prior { prior_session } else { current_session }, - id: batch.id, - external_network_block_hash: batch.external_network_block_hash, - in_instructions_hash, - in_instruction_results, - }); - - ValidTransaction::with_tag_prefix("in-instructions") - .and_provides((batch.network, batch.id)) - // Set a 10 block longevity, though this should be included in the next block - .longevity(10) - .propagate(true) - .build() - } - - // Explicitly provide a pre-dispatch which calls validate_unsigned - fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> { - Self::validate_unsigned(TransactionSource::InBlock, call).map(|_| ()) + #[pallet::weight((0, DispatchClass::Normal))] // TODO + pub fn execute_batch(origin: OriginFor, batch: SignedBatch) -> DispatchResult { + todo!("TODO") } } } diff --git a/substrate/in-instructions/src/mock.rs b/substrate/in-instructions/src/mock.rs deleted file mode 100644 index c1e4ef50..00000000 --- a/substrate/in-instructions/src/mock.rs +++ /dev/null @@ -1,209 +0,0 @@ -//! Test environment for InInstructions pallet. - -use super::*; - -use std::collections::HashMap; - -use frame_support::{ - construct_runtime, - traits::{ConstU16, ConstU32, ConstU64}, -}; - -use sp_core::{H256, Pair, sr25519::Public}; -use sp_runtime::{ - traits::{BlakeTwo256, IdentityLookup}, - BuildStorage, -}; - -use validator_sets::{primitives::MAX_KEY_SHARES_PER_SET_U32, MembershipProof}; - -pub use crate as in_instructions; -pub use coins_pallet as coins; -pub use validator_sets_pallet as validator_sets; -pub use genesis_liquidity_pallet as genesis_liquidity; -pub use emissions_pallet as emissions; -pub use dex_pallet as dex; -pub use pallet_babe as babe; -pub use pallet_grandpa as grandpa; -pub use pallet_timestamp as timestamp; -pub use economic_security_pallet as economic_security; - -type Block = frame_system::mocking::MockBlock; -// Maximum number of authorities per session. -pub type MaxAuthorities = ConstU32<{ MAX_KEY_SHARES_PER_SET_U32 }>; - -pub const MEDIAN_PRICE_WINDOW_LENGTH: u16 = 10; - -construct_runtime!( - pub enum Test - { - System: frame_system, - Timestamp: timestamp, - Coins: coins, - LiquidityTokens: coins::::{Pallet, Call, Storage, Event}, - Emissions: emissions, - ValidatorSets: validator_sets, - GenesisLiquidity: genesis_liquidity, - EconomicSecurity: economic_security, - Dex: dex, - Babe: babe, - Grandpa: grandpa, - InInstructions: in_instructions, - } -); - -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 timestamp::Config for Test { - type Moment = u64; - type OnTimestampSet = Babe; - type MinimumPeriod = ConstU64<{ (TARGET_BLOCK_TIME * 1000) / 2 }>; - type WeightInfo = (); -} - -impl babe::Config for Test { - type EpochDuration = ConstU64<{ FAST_EPOCH_DURATION }>; - - type ExpectedBlockTime = ConstU64<{ TARGET_BLOCK_TIME * 1000 }>; - type EpochChangeTrigger = babe::ExternalTrigger; - type DisabledValidators = ValidatorSets; - - type WeightInfo = (); - type MaxAuthorities = MaxAuthorities; - - type KeyOwnerProof = MembershipProof; - type EquivocationReportSystem = (); -} - -impl grandpa::Config for Test { - type RuntimeEvent = RuntimeEvent; - - type WeightInfo = (); - type MaxAuthorities = MaxAuthorities; - - type MaxSetIdSessionEntries = ConstU64<0>; - type KeyOwnerProof = MembershipProof; - type EquivocationReportSystem = (); -} - -impl coins::Config for Test { - type RuntimeEvent = RuntimeEvent; - type AllowMint = ValidatorSets; -} - -impl coins::Config for Test { - type RuntimeEvent = RuntimeEvent; - type AllowMint = (); -} - -impl dex::Config for Test { - type RuntimeEvent = RuntimeEvent; - - type LPFee = ConstU32<3>; // 0.3% - type MintMinLiquidity = ConstU64<10000>; - - type MaxSwapPathLength = ConstU32<3>; // coin1 -> SRI -> coin2 - - type MedianPriceWindowLength = ConstU16<{ MEDIAN_PRICE_WINDOW_LENGTH }>; - - type WeightInfo = dex::weights::SubstrateWeight; -} - -impl validator_sets::Config for Test { - type RuntimeEvent = RuntimeEvent; - type ShouldEndSession = Babe; -} - -impl genesis_liquidity::Config for Test { - type RuntimeEvent = RuntimeEvent; -} - -impl emissions::Config for Test { - type RuntimeEvent = RuntimeEvent; -} - -impl economic_security::Config for Test { - type RuntimeEvent = RuntimeEvent; -} - -impl Config for Test { - type RuntimeEvent = RuntimeEvent; -} - -// Amounts for single key share per network -pub fn key_shares() -> HashMap { - HashMap::from([ - (NetworkId::Serai, Amount(50_000 * 10_u64.pow(8))), - (NetworkId::External(ExternalNetworkId::Bitcoin), Amount(1_000_000 * 10_u64.pow(8))), - (NetworkId::External(ExternalNetworkId::Ethereum), Amount(1_000_000 * 10_u64.pow(8))), - (NetworkId::External(ExternalNetworkId::Monero), Amount(100_000 * 10_u64.pow(8))), - ]) -} - -pub(crate) fn new_test_ext() -> sp_io::TestExternalities { - let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); - let networks: Vec<(NetworkId, Amount)> = key_shares().into_iter().collect::>(); - - let accounts: Vec = vec![ - insecure_pair_from_name("Alice").public(), - insecure_pair_from_name("Bob").public(), - insecure_pair_from_name("Charlie").public(), - insecure_pair_from_name("Dave").public(), - insecure_pair_from_name("Eve").public(), - insecure_pair_from_name("Ferdie").public(), - ]; - let validators = accounts.clone(); - - coins::GenesisConfig:: { - accounts: accounts - .into_iter() - .map(|a| (a, Balance { coin: Coin::Serai, amount: Amount(1 << 60) })) - .collect(), - _ignore: Default::default(), - } - .assimilate_storage(&mut t) - .unwrap(); - - #[expect(unused_variables, unreachable_code, clippy::diverging_sub_expression)] - validator_sets::GenesisConfig:: { - networks: networks.clone(), - participants: validators - .clone() - .into_iter() - .map(|p| { - let keys: validator_sets_pallet::AllEmbeddedEllipticCurveKeysAtGenesis = todo!("TODO"); - (p, keys) - }) - .collect(), - } - .assimilate_storage(&mut t) - .unwrap(); - - let mut ext = sp_io::TestExternalities::new(t); - ext.execute_with(|| System::set_block_number(0)); - ext -} diff --git a/substrate/in-instructions/src/tests.rs b/substrate/in-instructions/src/tests.rs deleted file mode 100644 index 97a5bbc9..00000000 --- a/substrate/in-instructions/src/tests.rs +++ /dev/null @@ -1,507 +0,0 @@ -use super::*; -use crate::mock::*; - -use emissions_pallet::primitives::POL_ACCOUNT; -use genesis_liquidity_pallet::primitives::INITIAL_GENESIS_LP_SHARES; -use scale::Encode; - -use frame_support::{pallet_prelude::InvalidTransaction, traits::OnFinalize}; -use frame_system::RawOrigin; - -use sp_core::{sr25519::Public, Pair}; -use sp_runtime::{traits::ValidateUnsigned, transaction_validity::TransactionSource}; - -use validator_sets::{Pallet as ValidatorSets, primitives::KeyPair}; -use coins::primitives::{OutInstruction, OutInstructionWithBalance}; -use genesis_liquidity::primitives::GENESIS_LIQUIDITY_ACCOUNT; - -fn set_keys_for_session(key: Public) { - for n in EXTERNAL_NETWORKS { - ValidatorSets::::set_keys( - RawOrigin::None.into(), - n, - KeyPair(key, vec![].try_into().unwrap()), - vec![].try_into().unwrap(), - Signature([0u8; 64]), - ) - .unwrap(); - } -} - -#[expect(dead_code)] -fn get_events() -> Vec> { - let events = System::events() - .iter() - .filter_map(|event| { - if let RuntimeEvent::InInstructions(e) = &event.event { - Some(e.clone()) - } else { - None - } - }) - .collect::>(); - - System::reset_events(); - events -} - -fn make_liquid_pool(coin: ExternalCoin, amount: u64) { - // mint coins so that we can add liquidity - let account = insecure_pair_from_name("make-pool-account").public(); - Coins::mint(account, ExternalBalance { coin, amount: Amount(amount) }.into()).unwrap(); - Coins::mint(account, Balance { coin: Coin::Serai, amount: Amount(amount) }).unwrap(); - - // make some liquid pool - Dex::add_liquidity(RawOrigin::Signed(account).into(), coin, amount, amount, 1, 1, account) - .unwrap(); -} - -#[test] -fn validate_batch() { - new_test_ext().execute_with(|| { - let pair = insecure_pair_from_name("Alice"); - set_keys_for_session(pair.public()); - - let mut batch_size = 0; - let mut batch = Batch { - network: ExternalNetworkId::Monero, - id: 1, - external_network_block_hash: BlockHash([0u8; 32]), - instructions: vec![], - }; - - // batch size bigger than MAX_BATCH_SIZE should fail - while batch_size <= MAX_BATCH_SIZE + 1000 { - batch.instructions.push(InInstructionWithBalance { - instruction: InInstruction::Transfer(SeraiAddress::new([0u8; 32])), - balance: ExternalBalance { coin: ExternalCoin::Monero, amount: Amount(1) }, - }); - batch_size = batch.encode().len(); - } - - let call = pallet::Call::::execute_batch { - batch: SignedBatch { batch: batch.clone(), signature: Signature([0u8; 64]) }, - }; - assert_eq!( - InInstructions::validate_unsigned(TransactionSource::External, &call), - InvalidTransaction::ExhaustsResources.into() - ); - - // reduce the batch size into allowed size - while batch_size > MAX_BATCH_SIZE { - batch.instructions.pop(); - batch_size = batch.encode().len(); - } - - // 0 signature should be invalid - let call = pallet::Call::::execute_batch { - batch: SignedBatch { batch: batch.clone(), signature: Signature([0u8; 64]) }, - }; - assert_eq!( - InInstructions::validate_unsigned(TransactionSource::External, &call), - InvalidTransaction::BadProof.into() - ); - - // submit a valid signature - let signature = pair.sign(&batch_message(&batch)); - - // network shouldn't be halted - InInstructions::halt(ExternalNetworkId::Monero).unwrap(); - let call = pallet::Call::::execute_batch { - batch: SignedBatch { batch: batch.clone(), signature }, - }; - assert_eq!( - InInstructions::validate_unsigned(TransactionSource::External, &call), - InvalidTransaction::Custom(1).into() // network halted error - ); - - // submit from an un-halted network - batch.network = ExternalNetworkId::Bitcoin; - let signature = pair.sign(&batch_message(&batch)); - - // can't submit in the first block(Block 0) - let call = pallet::Call::::execute_batch { - batch: SignedBatch { batch: batch.clone(), signature: signature.clone() }, - }; - assert_eq!( - InInstructions::validate_unsigned(TransactionSource::External, &call), - InvalidTransaction::Future.into() - ); - - // update block number - System::set_block_number(1); - - // first batch id should be 0 - let call = pallet::Call::::execute_batch { - batch: SignedBatch { batch: batch.clone(), signature: signature.clone() }, - }; - assert_eq!( - InInstructions::validate_unsigned(TransactionSource::External, &call), - InvalidTransaction::Future.into() - ); - - // update batch id - batch.id = 0; - let signature = pair.sign(&batch_message(&batch)); - - // can't have more than 1 batch per block - let call = pallet::Call::::execute_batch { - batch: SignedBatch { batch: batch.clone(), signature: signature.clone() }, - }; - assert_eq!( - InInstructions::validate_unsigned(TransactionSource::External, &call), - InvalidTransaction::Future.into() - ); - - // update block number - System::set_block_number(2); - - // network and the instruction coins should match - let call = pallet::Call::::execute_batch { - batch: SignedBatch { batch: batch.clone(), signature }, - }; - assert_eq!( - InInstructions::validate_unsigned(TransactionSource::External, &call), - InvalidTransaction::Custom(2).into() // network and instruction coins doesn't match error - ); - - // update block number & batch - System::set_block_number(3); - for ins in &mut batch.instructions { - ins.balance.coin = ExternalCoin::Bitcoin; - } - let signature = pair.sign(&batch_message(&batch)); - - // batch id can't be equal or less than previous id - let call = pallet::Call::::execute_batch { - batch: SignedBatch { batch: batch.clone(), signature }, - }; - assert_eq!( - InInstructions::validate_unsigned(TransactionSource::External, &call), - InvalidTransaction::Stale.into() - ); - - // update block number & batch - System::set_block_number(4); - batch.id += 2; - let signature = pair.sign(&batch_message(&batch)); - - // batch id can't be incremented more than once per batch - let call = pallet::Call::::execute_batch { - batch: SignedBatch { batch: batch.clone(), signature }, - }; - assert_eq!( - InInstructions::validate_unsigned(TransactionSource::External, &call), - InvalidTransaction::Future.into() - ); - - // update block number & batch - System::set_block_number(5); - batch.id = (batch.id - 2) + 1; - let signature = pair.sign(&batch_message(&batch)); - - // it should now pass - let call = pallet::Call::::execute_batch { - batch: SignedBatch { batch: batch.clone(), signature }, - }; - InInstructions::validate_unsigned(TransactionSource::External, &call).unwrap(); - }); -} - -#[test] -fn transfer_instruction() { - new_test_ext().execute_with(|| { - let coin = ExternalCoin::Bitcoin; - let amount = Amount(2 * 10u64.pow(coin.decimals())); - let account = insecure_pair_from_name("random1").public(); - let batch = SignedBatch { - batch: Batch { - network: coin.network(), - id: 0, - external_network_block_hash: BlockHash([0u8; 32]), - instructions: vec![InInstructionWithBalance { - instruction: InInstruction::Transfer(account.into()), - balance: ExternalBalance { coin, amount }, - }], - }, - signature: Signature([0u8; 64]), - }; - InInstructions::execute_batch(RawOrigin::None.into(), batch).unwrap(); - - // check that account has the coins - assert_eq!(Coins::balance(account, coin.into()), amount); - }) -} - -#[test] -fn dex_instruction_add_liquidity() { - new_test_ext().execute_with(|| { - let coin = ExternalCoin::Ether; - let amount = Amount(2 * 10u64.pow(coin.decimals())); - let account = insecure_pair_from_name("random1").public(); - - let batch = SignedBatch { - batch: Batch { - network: coin.network(), - id: 0, - external_network_block_hash: BlockHash([0u8; 32]), - instructions: vec![InInstructionWithBalance { - instruction: InInstruction::Dex(DexCall::SwapAndAddLiquidity(account.into())), - balance: ExternalBalance { coin, amount }, - }], - }, - signature: Signature([0u8; 64]), - }; - - // we should have a liquid pool before we can swap - InInstructions::execute_batch(RawOrigin::None.into(), batch.clone()).unwrap(); - - // check that the instruction is failed - /* TODO - assert_eq!( - get_events() - .into_iter() - .filter(|event| matches!(event, in_instructions::Event::::InstructionFailure { .. })) - .collect::>(), - vec![in_instructions::Event::::InstructionFailure { - network: batch.batch.network, - id: batch.batch.id, - index: 0 - }] - ); - */ - - let original_coin_amount = 5 * 10u64.pow(coin.decimals()); - make_liquid_pool(coin, original_coin_amount); - - // this should now be successful - InInstructions::execute_batch(RawOrigin::None.into(), batch).unwrap(); - - // check that the instruction was successful - /* TODO - assert_eq!( - get_events() - .into_iter() - .filter(|event| matches!(event, in_instructions::Event::::InstructionFailure { .. })) - .collect::>(), - vec![] - ); - */ - - // check that we now have a Ether pool with correct liquidity - // we can't know the actual SRI amount since we don't know the result of the swap. - // Moreover, knowing exactly how much isn't the responsibility of InInstruction pallet, - // it is responsibility of the Dex pallet. - let (coin_amount, _serai_amount) = Dex::get_reserves(&coin.into(), &Coin::Serai).unwrap(); - assert_eq!(coin_amount, original_coin_amount + amount.0); - - // assert that the account got the liquidity tokens, again we don't how much and - // it isn't this pallets responsibility. - assert!(LiquidityTokens::balance(account, coin.into()).0 > 0); - - // check that in ins account doesn't have the coins - assert_eq!(Coins::balance(IN_INSTRUCTION_EXECUTOR.into(), coin.into()), Amount(0)); - assert_eq!(Coins::balance(IN_INSTRUCTION_EXECUTOR.into(), Coin::Serai), Amount(0)); - }) -} - -#[test] -fn dex_instruction_swap() { - new_test_ext().execute_with(|| { - let coin = ExternalCoin::Bitcoin; - let amount = Amount(2 * 10u64.pow(coin.decimals())); - let account = insecure_pair_from_name("random1").public(); - - // make a pool so that can actually swap - make_liquid_pool(coin, 5 * 10u64.pow(coin.decimals())); - - let mut batch = SignedBatch { - batch: Batch { - network: coin.network(), - id: 0, - external_network_block_hash: BlockHash([0u8; 32]), - instructions: vec![InInstructionWithBalance { - instruction: InInstruction::Dex(DexCall::Swap( - Balance { coin: Coin::Serai, amount: Amount(1) }, - OutAddress::External(ExternalAddress::new([0u8; 64].to_vec()).unwrap()), - )), - balance: ExternalBalance { coin, amount }, - }], - }, - signature: Signature([0u8; 64]), - }; - - // we can't send SRI to external address - InInstructions::execute_batch(RawOrigin::None.into(), batch.clone()).unwrap(); - - // check that the instruction was failed - /* TODO - assert_eq!( - get_events() - .into_iter() - .filter(|event| matches!(event, in_instructions::Event::::InstructionFailure { .. })) - .collect::>(), - vec![in_instructions::Event::::InstructionFailure { - network: batch.batch.network, - id: batch.batch.id, - index: 0 - }] - ); - */ - - // make it internal address - batch.batch.instructions[0].instruction = InInstruction::Dex(DexCall::Swap( - Balance { coin: Coin::Serai, amount: Amount(1) }, - OutAddress::Serai(account.into()), - )); - - // check that swap is successful this time - assert_eq!(Coins::balance(account, Coin::Serai), Amount(0)); - InInstructions::execute_batch(RawOrigin::None.into(), batch.clone()).unwrap(); - assert!(Coins::balance(account, Coin::Serai).0 > 0); - - // make another pool for external coin - let coin2 = ExternalCoin::Monero; - make_liquid_pool(coin2, 5 * 10u64.pow(coin.decimals())); - - // update the batch - let out_addr = ExternalAddress::new([0u8; 64].to_vec()).unwrap(); - batch.batch.instructions[0].instruction = InInstruction::Dex(DexCall::Swap( - Balance { coin: ExternalCoin::Monero.into(), amount: Amount(1) }, - OutAddress::External(out_addr.clone()), - )); - InInstructions::execute_batch(RawOrigin::None.into(), batch.clone()).unwrap(); - - // check that we got out instruction - let events = System::events() - .iter() - .filter_map(|event| { - if let RuntimeEvent::Coins(e) = &event.event { - if matches!(e, coins::Event::::BurnWithInstruction { .. }) { - Some(e.clone()) - } else { - None - } - } else { - None - } - }) - .collect::>(); - - assert_eq!( - events, - vec![coins::Event::::BurnWithInstruction { - from: IN_INSTRUCTION_EXECUTOR.into(), - instruction: OutInstructionWithBalance { - instruction: OutInstruction { address: out_addr }, - balance: ExternalBalance { coin: coin2, amount: Amount(68228493) } - } - }] - ) - }) -} - -#[test] -fn genesis_liquidity_instruction() { - new_test_ext().execute_with(|| { - let coin = ExternalCoin::Bitcoin; - let amount = Amount(2 * 10u64.pow(coin.decimals())); - let account = insecure_pair_from_name("random1").public(); - - let batch = SignedBatch { - batch: Batch { - network: coin.network(), - id: 0, - external_network_block_hash: BlockHash([0u8; 32]), - instructions: vec![InInstructionWithBalance { - instruction: InInstruction::GenesisLiquidity(account.into()), - balance: ExternalBalance { coin, amount }, - }], - }, - signature: Signature([0u8; 64]), - }; - - InInstructions::execute_batch(RawOrigin::None.into(), batch.clone()).unwrap(); - - // check that genesis liq account got the coins - assert_eq!(Coins::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), coin.into()), amount); - - // check that it registered the liquidity for the account - // detailed tests about the amounts has to be done in GenesisLiquidity pallet tests. - let liquidity_amount = GenesisLiquidity::liquidity(coin, account).unwrap(); - assert_eq!(liquidity_amount.coins, amount.0); - assert_eq!(liquidity_amount.shares, INITIAL_GENESIS_LP_SHARES); - - let supply = GenesisLiquidity::supply(coin).unwrap(); - assert_eq!(supply.coins, amount.0); - assert_eq!(supply.shares, INITIAL_GENESIS_LP_SHARES); - }) -} - -#[test] -fn swap_to_staked_sri_instruction() { - new_test_ext().execute_with(|| { - let coin = ExternalCoin::Monero; - let key_share = - ValidatorSets::::allocation_per_key_share(NetworkId::from(coin.network())).unwrap(); - let amount = Amount(2 * key_share.0); - let account = insecure_pair_from_name("random1").public(); - - // make a pool so that can actually swap - make_liquid_pool(coin, 5 * 10u64.pow(coin.decimals())); - - // set the keys to set the TAS for the network - ValidatorSets::::set_keys( - RawOrigin::None.into(), - coin.network(), - KeyPair(insecure_pair_from_name("random-key").public(), Vec::new().try_into().unwrap()), - Vec::new().try_into().unwrap(), - Signature([0u8; 64]), - ) - .unwrap(); - - // make sure account doesn't already have lTs or allocation - let current_liq_tokens = LiquidityTokens::balance(POL_ACCOUNT.into(), coin.into()).0; - assert_eq!(current_liq_tokens, 0); - assert_eq!(ValidatorSets::::allocation((NetworkId::from(coin.network()), account)), None); - - // we need this so that value for the coin exist - Dex::on_finalize(0); - System::set_block_number(1); // we need this for the spot price - - let batch = SignedBatch { - batch: Batch { - network: coin.network(), - id: 0, - external_network_block_hash: BlockHash([0u8; 32]), - instructions: vec![InInstructionWithBalance { - instruction: InInstruction::SwapToStakedSRI(account.into(), coin.network().into()), - balance: ExternalBalance { coin, amount }, - }], - }, - signature: Signature([0u8; 64]), - }; - - InInstructions::execute_batch(RawOrigin::None.into(), batch.clone()).unwrap(); - - // assert that we added liq from POL account - assert!(LiquidityTokens::balance(POL_ACCOUNT.into(), coin.into()).0 > current_liq_tokens); - - // assert that user allocated SRI for the network - let value = Dex::spot_price_for_block(0, coin).unwrap(); - let sri_amount = Amount( - u64::try_from( - u128::from(amount.0) - .checked_mul(u128::from(value.0)) - .unwrap() - .checked_div(u128::from(10u64.pow(coin.decimals()))) - .unwrap(), - ) - .unwrap(), - ); - assert_eq!( - ValidatorSets::::allocation((NetworkId::from(coin.network()), account)).unwrap(), - sri_amount - ); - }) -} diff --git a/substrate/primitives/src/instructions/in/batch.rs b/substrate/primitives/src/instructions/in/batch.rs index 8d39ba25..4f8508af 100644 --- a/substrate/primitives/src/instructions/in/batch.rs +++ b/substrate/primitives/src/instructions/in/batch.rs @@ -171,3 +171,26 @@ impl Zeroize for SignedBatch { self.signature.0.as_mut().zeroize(); } } + +#[cfg(feature = "non_canonical_scale_derivations")] +impl scale::Encode for SignedBatch { + fn using_encoded R>(&self, f: F) -> R { + f(&borsh::to_vec(self).unwrap()) + } +} +#[cfg(feature = "non_canonical_scale_derivations")] +impl scale::MaxEncodedLen for SignedBatch { + fn max_encoded_len() -> usize { + Batch::MAX_SIZE + 64 + } +} +#[cfg(feature = "non_canonical_scale_derivations")] +impl scale::EncodeLike for SignedBatch {} +#[cfg(feature = "non_canonical_scale_derivations")] +impl scale::Decode for SignedBatch { + fn decode(input: &mut I) -> Result { + crate::read_scale_as_borsh(input) + } +} +#[cfg(feature = "non_canonical_scale_derivations")] +impl scale::DecodeWithMemTracking for SignedBatch {} diff --git a/substrate/runtime/Cargo.toml b/substrate/runtime/Cargo.toml index 7a7b2a85..454e85cb 100644 --- a/substrate/runtime/Cargo.toml +++ b/substrate/runtime/Cargo.toml @@ -55,6 +55,7 @@ serai-validator-sets-pallet = { path = "../validator-sets", default-features = f serai-signals-pallet = { path = "../signals", default-features = false } serai-dex-pallet = { path = "../dex", default-features = false } serai-genesis-liquidity-pallet = { path = "../genesis-liquidity", default-features = false } +serai-in-instructions-pallet = { path = "../in-instructions", default-features = false } [build-dependencies] substrate-wasm-builder = { git = "https://github.com/serai-dex/patch-polkadot-sdk" } @@ -92,6 +93,7 @@ std = [ "serai-signals-pallet/std", "serai-dex-pallet/std", "serai-genesis-liquidity-pallet/std", + "serai-in-instructions-pallet/std", ] try-runtime = [ @@ -113,6 +115,7 @@ try-runtime = [ "serai-signals-pallet/try-runtime", "serai-dex-pallet/try-runtime", "serai-genesis-liquidity-pallet/try-runtime", + "serai-in-instructions-pallet/try-runtime", ] runtime-benchmarks = [ @@ -131,6 +134,7 @@ runtime-benchmarks = [ "serai-signals-pallet/runtime-benchmarks", "serai-dex-pallet/runtime-benchmarks", "serai-genesis-liquidity-pallet/runtime-benchmarks", + "serai-in-instructions-pallet/runtime-benchmarks", ] default = ["std"] diff --git a/substrate/runtime/src/wasm/mod.rs b/substrate/runtime/src/wasm/mod.rs index d862bcc7..00eff9da 100644 --- a/substrate/runtime/src/wasm/mod.rs +++ b/substrate/runtime/src/wasm/mod.rs @@ -99,6 +99,9 @@ mod runtime { #[runtime::pallet_index(7)] pub type GenesisLiquidity = serai_genesis_liquidity_pallet::Pallet; + #[runtime::pallet_index(8)] + pub type InInstructions = serai_in_instructions_pallet::Pallet; + #[runtime::pallet_index(0xfd)] #[runtime::disable_inherent] pub type Timestamp = pallet_timestamp::Pallet; @@ -178,6 +181,7 @@ impl serai_coins_pallet::Config for Runtime { } impl serai_dex_pallet::Config for Runtime {} impl serai_genesis_liquidity_pallet::Config for Runtime {} +impl serai_in_instructions_pallet::Config for Runtime {} impl pallet_timestamp::Config for Runtime { type Moment = u64; @@ -355,7 +359,9 @@ impl From for RuntimeCall { serai_abi::Call::InInstructions(call) => { use serai_abi::in_instructions::Call; match call { - Call::execute_batch { .. } => todo!("TODO"), + Call::execute_batch { batch } => { + RuntimeCall::InInstructions(serai_in_instructions_pallet::Call::execute_batch { batch }) + } } } }