Dispatch InInstruction as expected

This commit is contained in:
Luke Parker
2025-12-11 03:45:17 -05:00
parent 2fbe925c4d
commit 5a3cf1f2be
5 changed files with 182 additions and 26 deletions

1
Cargo.lock generated
View File

@@ -8433,6 +8433,7 @@ name = "serai-in-instructions-pallet"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bitvec", "bitvec",
"borsh",
"frame-support", "frame-support",
"frame-system", "frame-system",
"parity-scale-codec", "parity-scale-codec",

View File

@@ -1,9 +1,15 @@
use borsh::{BorshSerialize, BorshDeserialize}; use borsh::{BorshSerialize, BorshDeserialize};
use serai_primitives::{ 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. /// A call to `InInstruction`s.
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] #[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
pub enum Call { pub enum Call {

View File

@@ -21,6 +21,7 @@ workspace = true
[dependencies] [dependencies]
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } 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-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 } 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] [features]
std = [ std = [
"scale/std", "scale/std",
"borsh/std",
"sp-core/std", "sp-core/std",
"sp-application-crypto/std", "sp-application-crypto/std",

View File

@@ -7,10 +7,11 @@ extern crate alloc;
#[expect(clippy::cast_possible_truncation)] #[expect(clippy::cast_possible_truncation)]
#[frame_support::pallet] #[frame_support::pallet]
mod pallet { mod pallet {
use sp_core::sr25519::Public;
use sp_application_crypto::RuntimePublic; use sp_application_crypto::RuntimePublic;
use frame_support::{pallet_prelude::*, dispatch::RawOrigin};
use frame_system::pallet_prelude::*; use frame_system::pallet_prelude::*;
use frame_support::pallet_prelude::*;
use serai_abi::{primitives::prelude::*, in_instructions::Event}; use serai_abi::{primitives::prelude::*, in_instructions::Event};
@@ -59,6 +60,127 @@ mod pallet {
fn emit_event(event: Event) { fn emit_event(event: Event) {
Core::<T>::emit_event(event) Core::<T>::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 = <Balance as From<ExternalBalance>>::from(external_balance);
// Mint the balance to ourself
let address = serai_abi::in_instructions::address();
Coins::<T>::mint(address.into(), balance)?;
match instruction {
InInstruction::GenesisLiquidity(address) => {
serai_genesis_liquidity_pallet::Pallet::<T>::add_liquidity(address, external_balance)?;
}
InInstruction::SwapToStakedSri { validator, minimum } => todo!("TODO"),
InInstruction::TransferWithSwap { to, maximum_to_swap, sri } => {
serai_dex_pallet::Pallet::<T>::swap_for(
RawOrigin::Signed(address.into()).into(),
Balance { coin: Coin::Serai, amount: sri },
Balance { coin: balance.coin, amount: maximum_to_swap },
)?;
Coins::<T>::transfer_fn(
address.into(),
to.into(),
Balance {
coin: balance.coin,
amount: Coins::<T>::balance(Public::from(address), balance.coin),
},
)?;
Coins::<T>::transfer_fn(
address.into(),
to.into(),
Balance {
coin: Coin::Serai,
amount: Coins::<T>::balance(Public::from(address), Coin::Serai),
},
)?;
}
InInstruction::Transfer { to } => {
Coins::<T>::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::<T>::swap(
RawOrigin::Signed(address.into()).into(),
Balance {
coin: Coin::External(external_coin),
amount: (balance.amount - coin).ok_or(serai_dex_pallet::Error::<T>::Underflow)?,
},
Balance {
coin: Coin::Serai,
amount: (sri_minimum + sri_for_fees).ok_or(serai_dex_pallet::Error::<T>::Overflow)?,
},
)?;
let sri_intended = (Coins::<T>::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::<T>::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::<T>::transfer_fn(
address.into(),
destination.into(),
Balance {
coin: Coin::Serai,
amount: Coins::<T>::balance(Public::from(address), Coin::Serai),
},
)?;
}
InInstruction::Swap { address: destination, minimum_to_receive } => {
serai_dex_pallet::Pallet::<T>::swap(
RawOrigin::Signed(address.into()).into(),
balance,
minimum_to_receive,
)?;
let coin = minimum_to_receive.coin;
let received_amount = Coins::<T>::balance(Public::from(address), coin);
let received = Balance { coin, amount: received_amount };
Coins::<T>::transfer_fn(address.into(), destination.into(), received)?;
}
InInstruction::SwapOut { instruction, minimum_to_receive } => {
serai_dex_pallet::Pallet::<T>::swap(
RawOrigin::Signed(address.into()).into(),
balance,
minimum_to_receive.into(),
)?;
let coin = minimum_to_receive.coin;
let received_amount = Coins::<T>::balance(Public::from(address), Coin::from(coin));
let received = ExternalBalance { coin, amount: received_amount };
Coins::<T>::burn_with_instruction(
RawOrigin::Signed(address.into()).into(),
OutInstructionWithBalance { instruction, balance: received },
)?;
}
};
Ok(())
}
} }
#[pallet::call] #[pallet::call]
@@ -67,7 +189,29 @@ mod pallet {
#[pallet::call_index(0)] #[pallet::call_index(0)]
#[pallet::weight((0, DispatchClass::Normal))] // TODO #[pallet::weight((0, DispatchClass::Normal))] // TODO
pub fn execute_batch(origin: OriginFor<T>, batch: SignedBatch) -> DispatchResult { pub fn execute_batch(origin: OriginFor<T>, 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::<T>::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 // Verify this is the first `Batch` for this block
let current_block_number = frame_system::Pallet::<T>::block_number(); let current_block_number = frame_system::Pallet::<T>::block_number();
if BlockOfLastBatch::<T>::get(network) == Some(current_block_number) { if BlockOfLastBatch::<T>::get(network) == Some(current_block_number) {

View File

@@ -10,15 +10,6 @@ use crate::{
mod batch; mod batch;
pub use 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. /// An instruction on how to handle coins in.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] #[derive(Clone, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)]
pub enum InInstruction { pub enum InInstruction {
@@ -33,10 +24,10 @@ pub enum InInstruction {
}, },
/// Transfer the coins to a Serai address, swapping some for SRI. /// Transfer the coins to a Serai address, swapping some for SRI.
TransferWithSwap { TransferWithSwap {
/// The Serai address to transfer the coins to, after swapping some. /// The Serai address to transfer the coins to.
to: SeraiAddress, to: SeraiAddress,
/// The maximum amount of coins to swap for the intended amount of SRI. /// 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. /// The SRI amount to swap some of the coins for.
sri: Amount, sri: Amount,
}, },
@@ -47,23 +38,28 @@ pub enum InInstruction {
}, },
/// Swap part of the coins to SRI and add the coins as liquidity. /// Swap part of the coins to SRI and add the coins as liquidity.
SwapAndAddLiquidity { SwapAndAddLiquidity {
/// The owner to-be of the added liquidity. /// The recipient to-be of the added liquidity.
owner: SeraiAddress, address: SeraiAddress,
/// The amount of SRI to add within the liquidity position. /// The amount of the coin to add within the liquidity position.
sri: Amount, coin: Amount,
/// The minimum amount of the coin to add as liquidity. /// The minimum amount of SRI to add as liquidity.
minimum_coin: Amount, sri_minimum: Amount,
/// The amount of SRI to swap to and send to the owner to-be to pay for transactions on Serai. /// The amount of SRI to swap to and send to the owner to-be to pay for transactions on Serai.
sri_for_fees: Amount, sri_for_fees: Amount,
}, },
/// Swap the coins. /// Swap the coins.
Swap { Swap {
/// The minimum balance to receive. /// The destination to received the coins swapped to with.
minimum_out: Balance, address: SeraiAddress,
/// The destination to transfer the balance to. /// The minimum balance to receive from the swap.
/// minimum_to_receive: Balance,
/// If `Destination::Burn`, the balance out will be burnt with the included `OutInstruction`. },
destination: Destination, /// 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,
}, },
} }