#![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(not(feature = "std"), no_std)] use sp_io::hashing::blake2_256; use serai_primitives::*; pub use in_instructions_primitives as primitives; use primitives::*; // TODO: Investigate why Substrate generates these #[allow( unreachable_patterns, clippy::cast_possible_truncation, clippy::no_effect_underscore_binding, clippy::empty_docs )] #[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::*, 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 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 super::*; #[pallet::config] pub trait Config: frame_system::Config + CoinsConfig + DexConfig + ValidatorSetsConfig + GenesisLiqConfig + EmissionsConfig { 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: NetworkId, }, } #[pallet::error] pub enum Error { /// Coin and OutAddress types don't match. InvalidAddressForCoin, } #[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>; 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(()) } 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 { #[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(|_| ()).map_err(Into::into) } } } pub use pallet::*;