diff --git a/Cargo.lock b/Cargo.lock index 1a811a63..ba66df86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8433,6 +8433,7 @@ name = "serai-in-instructions-pallet" version = "0.1.0" dependencies = [ "bitvec", + "borsh", "frame-support", "frame-system", "parity-scale-codec", diff --git a/substrate/abi/src/in_instructions.rs b/substrate/abi/src/in_instructions.rs index a3ced76a..b71eead0 100644 --- a/substrate/abi/src/in_instructions.rs +++ b/substrate/abi/src/in_instructions.rs @@ -1,9 +1,15 @@ use borsh::{BorshSerialize, BorshDeserialize}; use serai_primitives::{ - BlockHash, network_id::ExternalNetworkId, validator_sets::Session, instructions::SignedBatch, + BlockHash, network_id::ExternalNetworkId, validator_sets::Session, address::SeraiAddress, + instructions::SignedBatch, }; +/// The address used for executing `InInstruction`s. +pub fn address() -> SeraiAddress { + SeraiAddress::system(borsh::to_vec(b"InInstructions").unwrap()) +} + /// A call to `InInstruction`s. #[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] pub enum Call { diff --git a/substrate/in-instructions/Cargo.toml b/substrate/in-instructions/Cargo.toml index 2b9deb0e..13bb1c85 100644 --- a/substrate/in-instructions/Cargo.toml +++ b/substrate/in-instructions/Cargo.toml @@ -21,6 +21,7 @@ workspace = true [dependencies] scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +borsh = { version = "1", default-features = false } sp-core = { 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 } @@ -40,6 +41,7 @@ serai-genesis-liquidity-pallet = { path = "../genesis-liquidity", default-featur [features] std = [ "scale/std", + "borsh/std", "sp-core/std", "sp-application-crypto/std", diff --git a/substrate/in-instructions/src/lib.rs b/substrate/in-instructions/src/lib.rs index d070e2fd..067830d9 100644 --- a/substrate/in-instructions/src/lib.rs +++ b/substrate/in-instructions/src/lib.rs @@ -7,10 +7,11 @@ extern crate alloc; #[expect(clippy::cast_possible_truncation)] #[frame_support::pallet] mod pallet { + use sp_core::sr25519::Public; use sp_application_crypto::RuntimePublic; + use frame_support::{pallet_prelude::*, dispatch::RawOrigin}; use frame_system::pallet_prelude::*; - use frame_support::pallet_prelude::*; use serai_abi::{primitives::prelude::*, in_instructions::Event}; @@ -59,6 +60,127 @@ mod pallet { fn emit_event(event: Event) { Core::::emit_event(event) } + + /// Execute an `InInstructionWithBalance`. + /// + /// We execute this within a database layer to ensure it's atomic, executing entirely or not at + /// all. + #[frame_support::transactional] + fn execute(instruction: InInstructionWithBalance) -> DispatchResult { + let InInstructionWithBalance { instruction, balance: external_balance } = instruction; + let balance = >::from(external_balance); + + // Mint the balance to ourself + let address = serai_abi::in_instructions::address(); + Coins::::mint(address.into(), balance)?; + + match instruction { + InInstruction::GenesisLiquidity(address) => { + serai_genesis_liquidity_pallet::Pallet::::add_liquidity(address, external_balance)?; + } + InInstruction::SwapToStakedSri { validator, minimum } => todo!("TODO"), + InInstruction::TransferWithSwap { to, maximum_to_swap, sri } => { + serai_dex_pallet::Pallet::::swap_for( + RawOrigin::Signed(address.into()).into(), + Balance { coin: Coin::Serai, amount: sri }, + Balance { coin: balance.coin, amount: maximum_to_swap }, + )?; + + Coins::::transfer_fn( + address.into(), + to.into(), + Balance { + coin: balance.coin, + amount: Coins::::balance(Public::from(address), balance.coin), + }, + )?; + Coins::::transfer_fn( + address.into(), + to.into(), + Balance { + coin: Coin::Serai, + amount: Coins::::balance(Public::from(address), Coin::Serai), + }, + )?; + } + InInstruction::Transfer { to } => { + Coins::::transfer_fn(address.into(), to.into(), balance)?; + } + InInstruction::SwapAndAddLiquidity { + address: destination, + coin, + sri_minimum, + sri_for_fees, + } => { + let external_coin = external_balance.coin; + serai_dex_pallet::Pallet::::swap( + RawOrigin::Signed(address.into()).into(), + Balance { + coin: Coin::External(external_coin), + amount: (balance.amount - coin).ok_or(serai_dex_pallet::Error::::Underflow)?, + }, + Balance { + coin: Coin::Serai, + amount: (sri_minimum + sri_for_fees).ok_or(serai_dex_pallet::Error::::Overflow)?, + }, + )?; + + let sri_intended = (Coins::::balance(Public::from(address), Coin::Serai) - + sri_for_fees) + .expect("swapped to amount sufficient for minimum, fees, but received less than fees?"); + let coin_intended = coin; + let coin_minimum = coin; + serai_dex_pallet::Pallet::::add_liquidity( + RawOrigin::Signed(address.into()).into(), + external_coin, + sri_intended, + coin_intended, + sri_minimum, + coin_minimum, + )?; + + // Transfer the rest, which will be more than the amount requested for fees, to the + // destination + Coins::::transfer_fn( + address.into(), + destination.into(), + Balance { + coin: Coin::Serai, + amount: Coins::::balance(Public::from(address), Coin::Serai), + }, + )?; + } + InInstruction::Swap { address: destination, minimum_to_receive } => { + serai_dex_pallet::Pallet::::swap( + RawOrigin::Signed(address.into()).into(), + balance, + minimum_to_receive, + )?; + + let coin = minimum_to_receive.coin; + let received_amount = Coins::::balance(Public::from(address), coin); + let received = Balance { coin, amount: received_amount }; + Coins::::transfer_fn(address.into(), destination.into(), received)?; + } + InInstruction::SwapOut { instruction, minimum_to_receive } => { + serai_dex_pallet::Pallet::::swap( + RawOrigin::Signed(address.into()).into(), + balance, + minimum_to_receive.into(), + )?; + + let coin = minimum_to_receive.coin; + let received_amount = Coins::::balance(Public::from(address), Coin::from(coin)); + let received = ExternalBalance { coin, amount: received_amount }; + Coins::::burn_with_instruction( + RawOrigin::Signed(address.into()).into(), + OutInstructionWithBalance { instruction, balance: received }, + )?; + } + }; + + Ok(()) + } } #[pallet::call] @@ -67,7 +189,29 @@ mod pallet { #[pallet::call_index(0)] #[pallet::weight((0, DispatchClass::Normal))] // TODO pub fn execute_batch(origin: OriginFor, batch: SignedBatch) -> DispatchResult { - todo!("TODO") + let batch = batch.batch; + let network = batch.network(); + + let mut in_instruction_results = bitvec::vec::BitVec::new(); + for instruction in batch.instructions() { + in_instruction_results.push(Self::execute(instruction.clone()).is_ok()); + } + + // The publishing session is always the current session + let publishing_session = + serai_validator_sets_pallet::Pallet::::current_session(NetworkId::from(network)) + .expect("`SignedBatch` for a network without a session was validated"); + + Self::emit_event(Event::Batch { + network, + publishing_session, + id: batch.id(), + external_network_block_hash: batch.external_network_block_hash(), + in_instructions_hash: sp_core::blake2_256(&borsh::to_vec(batch.instructions()).unwrap()), + in_instruction_results, + }); + + Ok(()) } } @@ -121,6 +265,13 @@ mod pallet { } } + // Verify every coin with the `Batch` corresponds to this network + for instruction in batch.batch.instructions() { + if instruction.balance.coin.network() != network { + Err(InvalidTransaction::Custom(2))?; + } + } + // Verify this is the first `Batch` for this block let current_block_number = frame_system::Pallet::::block_number(); if BlockOfLastBatch::::get(network) == Some(current_block_number) { diff --git a/substrate/primitives/src/instructions/in/mod.rs b/substrate/primitives/src/instructions/in/mod.rs index b3385df6..c6aa4c11 100644 --- a/substrate/primitives/src/instructions/in/mod.rs +++ b/substrate/primitives/src/instructions/in/mod.rs @@ -10,15 +10,6 @@ use crate::{ mod batch; pub use batch::*; -/// The destination for coins. -#[derive(Clone, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] -pub enum Destination { - /// The Serai address to transfer the coins to. - Serai(SeraiAddress), - /// Burn the coins with the included `OutInstruction`. - Burn(OutInstruction), -} - /// An instruction on how to handle coins in. #[derive(Clone, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] pub enum InInstruction { @@ -33,10 +24,10 @@ pub enum InInstruction { }, /// Transfer the coins to a Serai address, swapping some for SRI. TransferWithSwap { - /// The Serai address to transfer the coins to, after swapping some. + /// The Serai address to transfer the coins to. to: SeraiAddress, /// The maximum amount of coins to swap for the intended amount of SRI. - maximum_swap: Amount, + maximum_to_swap: Amount, /// The SRI amount to swap some of the coins for. sri: Amount, }, @@ -47,23 +38,28 @@ pub enum InInstruction { }, /// Swap part of the coins to SRI and add the coins as liquidity. SwapAndAddLiquidity { - /// The owner to-be of the added liquidity. - owner: SeraiAddress, - /// The amount of SRI to add within the liquidity position. - sri: Amount, - /// The minimum amount of the coin to add as liquidity. - minimum_coin: Amount, + /// The recipient to-be of the added liquidity. + address: SeraiAddress, + /// The amount of the coin to add within the liquidity position. + coin: Amount, + /// The minimum amount of SRI to add as liquidity. + sri_minimum: Amount, /// The amount of SRI to swap to and send to the owner to-be to pay for transactions on Serai. sri_for_fees: Amount, }, /// Swap the coins. Swap { - /// The minimum balance to receive. - minimum_out: Balance, - /// The destination to transfer the balance to. - /// - /// If `Destination::Burn`, the balance out will be burnt with the included `OutInstruction`. - destination: Destination, + /// The destination to received the coins swapped to with. + address: SeraiAddress, + /// The minimum balance to receive from the swap. + minimum_to_receive: Balance, + }, + /// Swap the coins, burning them with the included `OutInstruction`. + SwapOut { + /// The instruction to burn the coins swapped to with. + instruction: OutInstruction, + /// The minimum balance to receive from the swap. + minimum_to_receive: ExternalBalance, }, }