diff --git a/Cargo.lock b/Cargo.lock index 193d94de2..9bb3dc6f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5830,6 +5830,21 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-balanced-currency-swap-bridges-initializer" +version = "0.1.0" +dependencies = [ + "frame-support", + "frame-system", + "pallet-balances", + "pallet-pot", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-std", + "sp-tracing", +] + [[package]] name = "pallet-balances" version = "4.0.0-dev" diff --git a/crates/pallet-balanced-currency-swap-bridges-initializer/Cargo.toml b/crates/pallet-balanced-currency-swap-bridges-initializer/Cargo.toml new file mode 100644 index 000000000..f383f4049 --- /dev/null +++ b/crates/pallet-balanced-currency-swap-bridges-initializer/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "pallet-balanced-currency-swap-bridges-initializer" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = ["derive"] } +frame-support = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "locked/polkadot-v0.9.38" } +frame-system = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "locked/polkadot-v0.9.38" } +scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } +sp-std = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "locked/polkadot-v0.9.38" } +sp-tracing = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "locked/polkadot-v0.9.38" } + +[dev-dependencies] +pallet-pot = { version = "0.1", path = "../pallet-pot", default-features = false } + +pallet-balances = { git = "https://github.com/humanode-network/substrate", branch = "locked/polkadot-v0.9.38" } +sp-core = { git = "https://github.com/humanode-network/substrate", branch = "locked/polkadot-v0.9.38" } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-support/std", + "frame-system/std", + "pallet-balances/std", + "pallet-pot/std", + "scale-info/std", + "sp-std/std", + "sp-tracing/std", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-balances/try-runtime", + "pallet-pot/try-runtime", +] diff --git a/crates/pallet-balanced-currency-swap-bridges-initializer/src/lib.rs b/crates/pallet-balanced-currency-swap-bridges-initializer/src/lib.rs new file mode 100644 index 000000000..40e6fa420 --- /dev/null +++ b/crates/pallet-balanced-currency-swap-bridges-initializer/src/lib.rs @@ -0,0 +1,438 @@ +//! A substrate pallet for bridges pot currency swap initialization logic. + +// Either generate code at stadard mode, or `no_std`, based on the `std` feature presence. +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::{ + sp_runtime::{ + traits::{CheckedAdd, CheckedSub, Convert, Get, Zero}, + ArithmeticError, DispatchError, + }, + storage::with_storage_layer, + traits::{fungible, Currency, StorageVersion}, + weights::Weight, +}; +pub use pallet::*; +use sp_std::cmp::Ordering; +pub use weights::*; + +pub mod weights; + +mod upgrade_init; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +/// The native balance from a given config. +type NativeBalanceOf = + <::NativeCurrency as Currency<::AccountId>>::Balance; + +/// The evm balance from a given config. +type EvmBalanceOf = + <::EvmCurrency as Currency<::EvmAccountId>>::Balance; + +/// The current storage version. +const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + +/// The current bridges initializer version. +pub const CURRENT_BRIDGES_INITIALIZER_VERSION: u16 = 1; + +// We have to temporarily allow some clippy lints. Later on we'll send patches to substrate to +// fix them at their end. +#[allow(clippy::missing_docs_in_private_items)] +#[frame_support::pallet] +pub mod pallet { + use frame_support::{pallet_prelude::*, sp_runtime::traits::MaybeDisplay}; + use frame_system::pallet_prelude::*; + use sp_std::fmt::Debug; + + use super::*; + + /// The Bridge Pot Currency Swap Initializer Pallet. + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + /// Configuration trait of this pallet. + #[pallet::config] + pub trait Config: frame_system::Config { + /// The evm user account identifier type. + type EvmAccountId: Parameter + + Member + + MaybeSerializeDeserialize + + Debug + + MaybeDisplay + + Ord + + MaxEncodedLen; + + /// The interface into native currency implementation. + type NativeCurrency: Currency + + fungible::Inspect>; + + /// The interface into evm currency implementation. + type EvmCurrency: Currency + + fungible::Inspect>; + + /// The converter to determine how the balance amount should be converted from + /// native currency to evm currency. + type BalanceConverterNativeToEvm: Convert, EvmBalanceOf>; + + /// The converter to determine how the balance amount should be converted from + /// evm currency to native currency. + type BalanceConverterEvmToNative: Convert, NativeBalanceOf>; + + /// The native-evm bridge pot account. + type NativeEvmBridgePot: Get; + + /// The native treasury pot account. + type NativeTreasuryPot: Get; + + /// The evm-native bridge pot account. + type EvmNativeBridgePot: Get; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + /// The initializer version. + #[pallet::storage] + #[pallet::getter(fn initializer_version)] + pub type InitializerVersion = StorageValue<_, u16, ValueQuery>; + + #[pallet::genesis_config] + pub struct GenesisConfig(PhantomData); + + // The default value for the genesis config type. + #[cfg(feature = "std")] + impl Default for GenesisConfig { + fn default() -> Self { + Self(PhantomData) + } + } + + // The build of genesis for the pallet. + #[pallet::genesis_build] + impl GenesisBuild for GenesisConfig { + fn build(&self) { + let is_balanced = Pallet::::is_balanced().unwrap_or_default(); + + if !is_balanced { + match Pallet::::initialize() { + Ok(_) => {} + Err(err) => panic!("error during bridges initialization: {err:?}",), + } + } + + >::put(CURRENT_BRIDGES_INITIALIZER_VERSION); + } + } + + #[pallet::error] + pub enum Error { + /// The currencies are not balanced. + NotBalanced, + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_runtime_upgrade() -> Weight { + upgrade_init::on_runtime_upgrade::() + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, &'static str> { + upgrade_init::pre_upgrade::() + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(state: Vec) -> Result<(), &'static str> { + upgrade_init::post_upgrade::(state) + } + } + + #[pallet::call] + impl Pallet { + /// Verify if currencies are balanced. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::verify_balanced())] + pub fn verify_balanced(_origin: OriginFor) -> DispatchResult { + if !Pallet::::is_balanced()? { + return Err(Error::::NotBalanced.into()); + } + + Ok(()) + } + } +} + +impl Pallet { + /// Initialize bridges pot accounts. + pub fn initialize() -> Result { + let mut weight = T::DbWeight::get().reads(0); + + with_storage_layer(move || { + let (native_evm_bridge_minimum_balance, evm_native_bridge_minimum_balance) = + Self::bridges_miminum_balances(); + + let evm_total_issuance = T::EvmCurrency::total_issuance(); + let evm_bridge_balance = T::EvmCurrency::total_balance(&T::EvmNativeBridgePot::get()); + weight += T::DbWeight::get().reads(2); + + let evm_swappable = evm_total_issuance + .checked_sub(&evm_bridge_balance) + .expect("evm_total_issuance is greater than evm_bridge_balance; qed."); + + let native_swap_reserved = T::BalanceConverterEvmToNative::convert(evm_swappable); + let native_bridge_balance = native_swap_reserved + .checked_add(&native_evm_bridge_minimum_balance) + .ok_or(ArithmeticError::Overflow)?; + weight += T::DbWeight::get().reads(1); + + weight += Self::make_native_bridge_balance_be(native_bridge_balance)?; + + let native_total_issuance = T::NativeCurrency::total_issuance(); + weight += T::DbWeight::get().reads(1); + + let native_swappable = native_total_issuance + .checked_sub(&native_bridge_balance) + .expect("native_total_issuance is greater than native_bridge_balance; qed."); + + let evm_swap_reserved = T::BalanceConverterNativeToEvm::convert(native_swappable); + let evm_bridge_balance = evm_swap_reserved + .checked_add(&evm_native_bridge_minimum_balance) + .ok_or(ArithmeticError::Overflow)?; + weight += T::DbWeight::get().reads(1); + + weight += Self::make_evm_bridge_balance_be(evm_bridge_balance)?; + + if !Self::is_balanced()? { + return Err::<(), DispatchError>(Error::::NotBalanced.into()); + } + weight += T::DbWeight::get().reads(8); + + debug_assert!( + T::NativeCurrency::total_issuance() + == T::BalanceConverterEvmToNative::convert(T::EvmCurrency::total_issuance()), + "we must ensure that the native and evm total issuances are proportionally equal" + ); + + Ok(()) + })?; + + Ok(weight) + } + + /// A helper function to calculate bridges minimum balances be proportionally equal. + fn bridges_miminum_balances() -> (NativeBalanceOf, EvmBalanceOf) { + let native_ed = T::NativeCurrency::minimum_balance(); + let evm_ed = T::EvmCurrency::minimum_balance(); + + match native_ed.cmp(&T::BalanceConverterEvmToNative::convert(evm_ed)) { + Ordering::Greater => ( + native_ed, + T::BalanceConverterNativeToEvm::convert(native_ed), + ), + Ordering::Less => (T::BalanceConverterEvmToNative::convert(evm_ed), evm_ed), + Ordering::Equal => (native_ed, evm_ed), + } + } + + /// Make native bridge balance be provided amount value. + /// + /// This function TRANSFERS the tokens to/from the treasury to balance the bridge pots. + /// It will not change the total issuance, but it can change the native swappable balance value. + fn make_native_bridge_balance_be(amount: NativeBalanceOf) -> Result { + let native_total_issuance_before = T::NativeCurrency::total_issuance(); + let current_native_bridge_balance = + T::NativeCurrency::total_balance(&T::NativeEvmBridgePot::get()); + let mut weight = T::DbWeight::get().reads(1); + + if current_native_bridge_balance == Zero::zero() { + let imbalance = T::NativeCurrency::withdraw( + &T::NativeTreasuryPot::get(), + amount, + frame_support::traits::WithdrawReasons::TRANSFER, + frame_support::traits::ExistenceRequirement::KeepAlive, + )?; + weight += T::DbWeight::get().writes(1); + + T::NativeCurrency::resolve_creating(&T::NativeEvmBridgePot::get(), imbalance); + weight += T::DbWeight::get().writes(1); + + return Ok(weight); + } + + match current_native_bridge_balance.cmp(&amount) { + Ordering::Less => { + let imbalance = T::NativeCurrency::withdraw( + &T::NativeTreasuryPot::get(), + amount + .checked_sub(¤t_native_bridge_balance) + .expect("current_native_bridge_balance is less than amount; qed."), + frame_support::traits::WithdrawReasons::TRANSFER, + frame_support::traits::ExistenceRequirement::KeepAlive, + )?; + weight += T::DbWeight::get().writes(1); + + // We can safely ignore the result as overflow can't be reached. + // current_native_bridge_balance < amount. The resulted balance is equal to amount. + let _ = T::NativeCurrency::resolve_into_existing( + &T::NativeEvmBridgePot::get(), + imbalance, + ); + weight += T::DbWeight::get().writes(1); + } + Ordering::Greater => { + let imbalance = T::NativeCurrency::withdraw( + &T::NativeEvmBridgePot::get(), + current_native_bridge_balance + .checked_sub(&amount) + .expect("current_native_bridge_balance is greater than amount; qed."), + frame_support::traits::WithdrawReasons::TRANSFER, + frame_support::traits::ExistenceRequirement::KeepAlive, + )?; + weight += T::DbWeight::get().writes(1); + + // We can safely ignore the result as overflow can't be reached. + // current_native_bridge_balance + current_native_treasury < total_issuance. + // So, imbalance + current_native_treasury < total_issuance. + let _ = T::NativeCurrency::resolve_into_existing( + &T::NativeTreasuryPot::get(), + imbalance, + ); + weight += T::DbWeight::get().writes(1); + } + Ordering::Equal => {} + } + + debug_assert!( + native_total_issuance_before == T::NativeCurrency::total_issuance(), + "we must ensure that the native total issuance isn't altered" + ); + + Ok(weight) + } + + /// Make evm bridge balance be provided amount value. + /// + /// This function MINTS/BURNS the tokens as it needs to balance out the currencies and bridge pots. + /// The logic shouldn't change evm swappable balance value, but it can change the total evm issuance. + fn make_evm_bridge_balance_be(amount: EvmBalanceOf) -> Result { + let evm_swappable_balance_before = + swappable_balance::()?; + + let current_evm_bridge_balance = + T::EvmCurrency::total_balance(&T::EvmNativeBridgePot::get()); + let mut weight = T::DbWeight::get().reads(1); + + if current_evm_bridge_balance == Zero::zero() { + let imbalance = T::EvmCurrency::issue(amount); + weight += T::DbWeight::get().writes(1); + + T::EvmCurrency::resolve_creating(&T::EvmNativeBridgePot::get(), imbalance); + weight += T::DbWeight::get().writes(1); + + return Ok(weight); + } + + match current_evm_bridge_balance.cmp(&amount) { + Ordering::Less => { + let imbalance = T::EvmCurrency::issue( + amount + .checked_sub(¤t_evm_bridge_balance) + .expect("current_evm_bridge_balance is less than amount; qed."), + ); + weight += T::DbWeight::get().writes(1); + + // We can safely ignore the result as overflow can't be reached. + // current_evm_bridge_balance < amount. The resulted balance is equal to amount. + let _ = + T::EvmCurrency::resolve_into_existing(&T::EvmNativeBridgePot::get(), imbalance); + weight += T::DbWeight::get().writes(1); + } + Ordering::Greater => { + let imbalance = T::EvmCurrency::burn( + current_evm_bridge_balance + .checked_sub(&amount) + .expect("current_evm_bridge_balance is greater than amount; qed."), + ); + weight += T::DbWeight::get().writes(1); + + // We can safely ignore the result as underflow can't be reached. + // current_evm_bridge_balance > amount => imbalance < current_evm_bridge_balance. + let _ = T::EvmCurrency::settle( + &T::EvmNativeBridgePot::get(), + imbalance, + frame_support::traits::WithdrawReasons::RESERVE, + frame_support::traits::ExistenceRequirement::KeepAlive, + ); + weight += T::DbWeight::get().writes(1); + } + Ordering::Equal => {} + } + + debug_assert!( + evm_swappable_balance_before + == swappable_balance::()?, + "we must ensure that the evm swappable balance isn't altered" + ); + + Ok(weight) + } + + /// Verify currencies balanced requirements. + pub fn is_balanced() -> Result { + let (native_evm_bridge_minimum_balance, evm_native_bridge_minimum_balance) = + Self::bridges_miminum_balances(); + + let is_balanced_native_evm = + swap_reserved_balance::( + native_evm_bridge_minimum_balance, + )? == T::BalanceConverterEvmToNative::convert(swappable_balance::< + T::EvmAccountId, + T::EvmCurrency, + T::EvmNativeBridgePot, + >()?); + + let is_balanced_evm_native = T::BalanceConverterNativeToEvm::convert(swappable_balance::< + T::AccountId, + T::NativeCurrency, + T::NativeEvmBridgePot, + >()?) + == swap_reserved_balance::( + evm_native_bridge_minimum_balance, + )?; + + Ok(is_balanced_native_evm && is_balanced_evm_native) + } +} + +/// A helper function to calculate swappable balance. +fn swappable_balance, B: Get>( +) -> Result { + let total = C::total_issuance(); + let bridge = C::total_balance(&B::get()); + + let swappable = total + .checked_sub(&bridge) + .ok_or(ArithmeticError::Underflow)?; + + Ok(swappable) +} + +/// A helper function to calculate swap reserved balance. +fn swap_reserved_balance, B: Get>( + bridge_minimum_balance: C::Balance, +) -> Result { + let bridge = C::total_balance(&B::get()); + + let reserved = bridge + .checked_sub(&bridge_minimum_balance) + .ok_or(ArithmeticError::Underflow)?; + + Ok(reserved) +} diff --git a/crates/pallet-balanced-currency-swap-bridges-initializer/src/mock/mod.rs b/crates/pallet-balanced-currency-swap-bridges-initializer/src/mock/mod.rs new file mode 100644 index 000000000..aa3853449 --- /dev/null +++ b/crates/pallet-balanced-currency-swap-bridges-initializer/src/mock/mod.rs @@ -0,0 +1,90 @@ +//! The mocks for the pallet testing. + +use frame_support::{parameter_types, sp_io, sp_runtime::BuildStorage}; + +use crate::{self as pallet_balanced_currency_swap_bridges_initializer}; + +pub mod v0; +pub mod v1; + +pub(crate) const EXISTENTIAL_DEPOSIT_NATIVE: u64 = 10; +pub(crate) const EXISTENTIAL_DEPOSIT_EVM: u64 = 20; + +pub(crate) type AccountId = u64; +pub(crate) type EvmAccountId = u64; + +type Balance = u64; + +type BalancesInstanceNative = pallet_balances::Instance1; +type BalancesInstanceEvm = pallet_balances::Instance2; + +parameter_types! { + pub NativeTreasury: AccountId = 4200; +} + +// This function basically just builds a genesis storage key/value store according to +// our desired mockup. +pub fn new_test_ext_with(genesis_config: impl BuildStorage) -> sp_io::TestExternalities { + let storage = genesis_config.build_storage().unwrap(); + storage.into() +} + +pub fn runtime_lock() -> std::sync::MutexGuard<'static, ()> { + static MOCK_RUNTIME_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(()); + + // Ignore the poisoning for the tests that panic. + // We only care about concurrency here, not about the poisoning. + match MOCK_RUNTIME_MUTEX.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + } +} + +pub fn with_runtime_lock(f: impl FnOnce() -> R) -> R { + let lock = runtime_lock(); + let res = f(); + drop(lock); + res +} + +#[derive(Clone, Copy)] +pub(crate) struct AccountInfo { + pub account: u64, + pub balance: u64, +} + +impl From for (u64, u64) { + fn from(account_info: AccountInfo) -> Self { + (account_info.account, account_info.balance) + } +} + +pub(crate) const ALICE: AccountInfo = AccountInfo { + account: 4201, + balance: 20, +}; + +pub(crate) const BOB: AccountInfo = AccountInfo { + account: 4202, + balance: 30, +}; + +pub(crate) const LION: AccountInfo = AccountInfo { + account: 4211, + balance: 200, +}; + +pub(crate) const DOG: AccountInfo = AccountInfo { + account: 4212, + balance: 300, +}; + +pub(crate) const CAT: AccountInfo = AccountInfo { + account: 4213, + balance: 400, +}; + +pub(crate) const FISH: AccountInfo = AccountInfo { + account: 4214, + balance: 500, +}; diff --git a/crates/pallet-balanced-currency-swap-bridges-initializer/src/mock/v0.rs b/crates/pallet-balanced-currency-swap-bridges-initializer/src/mock/v0.rs new file mode 100644 index 000000000..fdbe283cf --- /dev/null +++ b/crates/pallet-balanced-currency-swap-bridges-initializer/src/mock/v0.rs @@ -0,0 +1,87 @@ +//! The v0 mock that represents just a case with two separate accounts system +//! without bridges initialization logic at runtime. + +// Allow simple integer arithmetic in tests. +#![allow(clippy::integer_arithmetic)] + +use frame_support::{ + sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + }, + traits::{ConstU32, ConstU64, StorageMapShim}, +}; +use sp_core::H256; + +use super::*; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Balances: pallet_balances::, + EvmBalances: pallet_balances::, + } +); + +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +impl pallet_balances::Config for Test { + type Balance = u64; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU64; + type AccountStore = System; + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); +} + +impl pallet_balances::Config for Test { + type Balance = u64; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU64; + type AccountStore = StorageMapShim< + pallet_balances::Account, + frame_system::Provider, + EvmAccountId, + pallet_balances::AccountData, + >; + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); +} diff --git a/crates/pallet-balanced-currency-swap-bridges-initializer/src/mock/v1.rs b/crates/pallet-balanced-currency-swap-bridges-initializer/src/mock/v1.rs new file mode 100644 index 000000000..d9801d64d --- /dev/null +++ b/crates/pallet-balanced-currency-swap-bridges-initializer/src/mock/v1.rs @@ -0,0 +1,135 @@ +//! The v1 mock that includes bridges initialization logic at runtime. + +// Allow simple integer arithmetic in tests. +#![allow(clippy::integer_arithmetic)] + +use frame_support::{ + parameter_types, + sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, Identity, IdentityLookup}, + }, + traits::{ConstU32, ConstU64, StorageMapShim}, + PalletId, +}; +use sp_core::H256; + +use super::*; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Balances: pallet_balances::, + EvmBalances: pallet_balances::, + SwapBridgeNativeToEvmPot: pallet_pot::, + SwapBridgeEvmToNativePot: pallet_pot::, + EvmNativeBridgesInitializer: pallet_balanced_currency_swap_bridges_initializer, + } +); + +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +impl pallet_balances::Config for Test { + type Balance = u64; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU64; + type AccountStore = System; + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); +} + +impl pallet_balances::Config for Test { + type Balance = u64; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU64; + type AccountStore = StorageMapShim< + pallet_balances::Account, + frame_system::Provider, + EvmAccountId, + pallet_balances::AccountData, + >; + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); +} + +parameter_types! { + pub const SwapBridgeNativeToEvmPotPalletId: PalletId = PalletId(*b"humanoNE"); + pub const SwapBridgeEvmToNativePotPalletId: PalletId = PalletId(*b"humanoEN"); +} + +type PotInstanceSwapBridgeNativeToEvm = pallet_pot::Instance1; +type PotInstanceSwapBridgeEvmToNative = pallet_pot::Instance2; + +impl pallet_pot::Config for Test { + type RuntimeEvent = RuntimeEvent; + type AccountId = AccountId; + type PalletId = SwapBridgeNativeToEvmPotPalletId; + type Currency = Balances; +} + +impl pallet_pot::Config for Test { + type RuntimeEvent = RuntimeEvent; + type AccountId = EvmAccountId; + type PalletId = SwapBridgeEvmToNativePotPalletId; + type Currency = EvmBalances; +} + +parameter_types! { + pub const SwapBridgeNativeToEvmPalletId: PalletId = PalletId(*b"hmsb/ne1"); + pub const SwapBridgeEvmToNativePalletId: PalletId = PalletId(*b"hmsb/en1"); +} + +parameter_types! { + pub SwapBridgeNativeToEvmPotAccountId: AccountId = SwapBridgeNativeToEvmPot::account_id(); + pub SwapBridgeEvmToNativePotAccountId: AccountId = SwapBridgeEvmToNativePot::account_id(); +} + +impl pallet_balanced_currency_swap_bridges_initializer::Config for Test { + type EvmAccountId = EvmAccountId; + type NativeCurrency = Balances; + type EvmCurrency = EvmBalances; + type BalanceConverterEvmToNative = Identity; + type BalanceConverterNativeToEvm = Identity; + type NativeEvmBridgePot = SwapBridgeNativeToEvmPotAccountId; + type NativeTreasuryPot = NativeTreasury; + type EvmNativeBridgePot = SwapBridgeEvmToNativePotAccountId; + type WeightInfo = (); +} diff --git a/crates/pallet-balanced-currency-swap-bridges-initializer/src/tests.rs b/crates/pallet-balanced-currency-swap-bridges-initializer/src/tests.rs new file mode 100644 index 000000000..36f972d44 --- /dev/null +++ b/crates/pallet-balanced-currency-swap-bridges-initializer/src/tests.rs @@ -0,0 +1,437 @@ +use frame_support::{ + assert_storage_noop, + traits::{Currency, OnRuntimeUpgrade}, +}; + +use crate::{ + mock::{new_test_ext_with, v0, v1, with_runtime_lock, *}, + swappable_balance, InitializerVersion, CURRENT_BRIDGES_INITIALIZER_VERSION, +}; + +/// This test verifies that balanced bridges initialization works in case bridge pot accounts +/// have been created with existential deposit balance values at genesis. +#[test] +fn initialization_bridges_ed_works() { + with_runtime_lock(|| { + let treasury = AccountInfo { + account: NativeTreasury::get(), + balance: 1450, + }; + let swap_bridge_native_evm = AccountInfo { + account: v1::SwapBridgeNativeToEvmPot::account_id(), + balance: EXISTENTIAL_DEPOSIT_NATIVE, + }; + + let swap_bridge_evm_native = AccountInfo { + account: v1::SwapBridgeEvmToNativePot::account_id(), + balance: EXISTENTIAL_DEPOSIT_EVM, + }; + + let config = v1::GenesisConfig { + balances: pallet_balances::GenesisConfig { + balances: vec![ + treasury.into(), + ALICE.into(), + BOB.into(), + swap_bridge_native_evm.into(), + ], + }, + evm_balances: pallet_balances::GenesisConfig { + balances: vec![ + LION.into(), + DOG.into(), + CAT.into(), + FISH.into(), + swap_bridge_evm_native.into(), + ], + }, + swap_bridge_native_to_evm_pot: pallet_pot::GenesisConfig { + initial_state: pallet_pot::InitialState::Initialized, + }, + swap_bridge_evm_to_native_pot: pallet_pot::GenesisConfig { + initial_state: pallet_pot::InitialState::Initialized, + }, + ..Default::default() + }; + new_test_ext_with(config).execute_with(move || { + assert_eq!( + >::get(), + CURRENT_BRIDGES_INITIALIZER_VERSION + ); + assert_eq!( + v1::Balances::total_balance(&v1::SwapBridgeNativeToEvmPot::account_id()), + LION.balance + + DOG.balance + + CAT.balance + + FISH.balance + + EXISTENTIAL_DEPOSIT_NATIVE + + (EXISTENTIAL_DEPOSIT_EVM - EXISTENTIAL_DEPOSIT_NATIVE) + ); + assert_eq!( + v1::Balances::total_balance(&NativeTreasury::get()), + treasury.balance + - (LION.balance + DOG.balance + CAT.balance + FISH.balance) + - (EXISTENTIAL_DEPOSIT_EVM - EXISTENTIAL_DEPOSIT_NATIVE) + ); + assert_eq!( + v1::EvmBalances::total_balance(&v1::SwapBridgeEvmToNativePot::account_id(),), + v1::Balances::total_balance(&NativeTreasury::get()) + + ALICE.balance + + BOB.balance + + EXISTENTIAL_DEPOSIT_EVM + ); + }); + }) +} + +/// This test verifies that balanced bridges initialization works in case bridge pot accounts +/// have been created with existential deposit balance values plus some deltas at genesis. +#[test] +fn initialization_bridges_ed_delta_works() { + with_runtime_lock(|| { + let treasury = AccountInfo { + account: NativeTreasury::get(), + balance: 1450, + }; + let native_bridge_delta = 100; + let swap_bridge_native_evm = AccountInfo { + account: v1::SwapBridgeNativeToEvmPot::account_id(), + balance: EXISTENTIAL_DEPOSIT_NATIVE + native_bridge_delta, + }; + + let evm_bridge_delta = 50; + let swap_bridge_evm_native = AccountInfo { + account: v1::SwapBridgeEvmToNativePot::account_id(), + balance: EXISTENTIAL_DEPOSIT_EVM + evm_bridge_delta, + }; + + let config = v1::GenesisConfig { + balances: pallet_balances::GenesisConfig { + balances: vec![ + treasury.into(), + ALICE.into(), + BOB.into(), + swap_bridge_native_evm.into(), + ], + }, + evm_balances: pallet_balances::GenesisConfig { + balances: vec![ + LION.into(), + DOG.into(), + CAT.into(), + FISH.into(), + swap_bridge_evm_native.into(), + ], + }, + swap_bridge_native_to_evm_pot: pallet_pot::GenesisConfig { + initial_state: pallet_pot::InitialState::Initialized, + }, + swap_bridge_evm_to_native_pot: pallet_pot::GenesisConfig { + initial_state: pallet_pot::InitialState::Initialized, + }, + ..Default::default() + }; + new_test_ext_with(config).execute_with(move || { + assert_eq!( + >::get(), + CURRENT_BRIDGES_INITIALIZER_VERSION + ); + assert_eq!( + v1::Balances::total_balance(&v1::SwapBridgeNativeToEvmPot::account_id()), + LION.balance + + DOG.balance + + CAT.balance + + FISH.balance + + EXISTENTIAL_DEPOSIT_NATIVE + + (EXISTENTIAL_DEPOSIT_EVM - EXISTENTIAL_DEPOSIT_NATIVE) + ); + assert_eq!( + v1::Balances::total_balance(&NativeTreasury::get()), + treasury.balance - (LION.balance + DOG.balance + CAT.balance + FISH.balance) + + native_bridge_delta + - (EXISTENTIAL_DEPOSIT_EVM - EXISTENTIAL_DEPOSIT_NATIVE) + ); + assert_eq!( + v1::EvmBalances::total_balance(&v1::SwapBridgeEvmToNativePot::account_id(),), + v1::Balances::total_balance(&NativeTreasury::get()) + + ALICE.balance + + BOB.balance + + EXISTENTIAL_DEPOSIT_EVM + ); + }); + }) +} + +/// This test verifies idempotency of balanced bridges initialization algorithm by changing +/// balances state and applying initialization operation several times. +#[test] +fn initialization_idempotency() { + with_runtime_lock(|| { + let treasury = AccountInfo { + account: NativeTreasury::get(), + balance: 1450, + }; + let swap_bridge_native_evm = AccountInfo { + account: v1::SwapBridgeNativeToEvmPot::account_id(), + balance: EXISTENTIAL_DEPOSIT_NATIVE, + }; + + let swap_bridge_evm_native = AccountInfo { + account: v1::SwapBridgeEvmToNativePot::account_id(), + balance: EXISTENTIAL_DEPOSIT_EVM, + }; + + let config = v1::GenesisConfig { + balances: pallet_balances::GenesisConfig { + balances: vec![ + treasury.into(), + ALICE.into(), + BOB.into(), + swap_bridge_native_evm.into(), + ], + }, + evm_balances: pallet_balances::GenesisConfig { + balances: vec![ + LION.into(), + DOG.into(), + CAT.into(), + FISH.into(), + swap_bridge_evm_native.into(), + ], + }, + swap_bridge_native_to_evm_pot: pallet_pot::GenesisConfig { + initial_state: pallet_pot::InitialState::Initialized, + }, + swap_bridge_evm_to_native_pot: pallet_pot::GenesisConfig { + initial_state: pallet_pot::InitialState::Initialized, + }, + ..Default::default() + }; + new_test_ext_with(config).execute_with(move || { + // Verify that bridges initialization has been applied at genesis. + assert_eq!( + >::get(), + CURRENT_BRIDGES_INITIALIZER_VERSION + ); + assert!(v1::EvmNativeBridgesInitializer::is_balanced().unwrap()); + assert_eq!( + v1::Balances::total_balance(&v1::SwapBridgeNativeToEvmPot::account_id()), + LION.balance + + DOG.balance + + CAT.balance + + FISH.balance + + EXISTENTIAL_DEPOSIT_NATIVE + + (EXISTENTIAL_DEPOSIT_EVM - EXISTENTIAL_DEPOSIT_NATIVE) + ); + assert_eq!( + v1::Balances::total_balance(&NativeTreasury::get()), + treasury.balance + - (LION.balance + DOG.balance + CAT.balance + FISH.balance) + - (EXISTENTIAL_DEPOSIT_EVM - EXISTENTIAL_DEPOSIT_NATIVE) + ); + assert_eq!( + v1::EvmBalances::total_balance(&v1::SwapBridgeEvmToNativePot::account_id(),), + v1::Balances::total_balance(&NativeTreasury::get()) + + ALICE.balance + + BOB.balance + + EXISTENTIAL_DEPOSIT_EVM + ); + + // Do it twice to ensure immediate reinvocation idempotency. + assert_storage_noop!(v1::EvmNativeBridgesInitializer::initialize().unwrap()); + assert_storage_noop!(v1::EvmNativeBridgesInitializer::initialize().unwrap()); + + for attempt in 0..5 { + // Send to an existing account. + v1::Balances::transfer(Some(ALICE.account).into(), BOB.account, 1).unwrap(); + // Create a new one account. + v1::EvmBalances::transfer( + Some(FISH.account).into(), + 5234 + attempt, + EXISTENTIAL_DEPOSIT_EVM, + ) + .unwrap(); + + // Initialize bridges one more time after a change. + assert_storage_noop!(v1::EvmNativeBridgesInitializer::initialize().unwrap()); + } + }); + }) +} + +/// This test verifies that balanced bridges initialization works in case genesis configuration +/// leads to 0 evm swappable balance. +#[test] +fn initialization_evm_swappable_zero() { + with_runtime_lock(|| { + let treasury = AccountInfo { + account: NativeTreasury::get(), + balance: EXISTENTIAL_DEPOSIT_NATIVE, + }; + let swap_bridge_native_evm = AccountInfo { + account: v1::SwapBridgeNativeToEvmPot::account_id(), + balance: u64::MAX - EXISTENTIAL_DEPOSIT_NATIVE, + }; + + let swap_bridge_evm_native = AccountInfo { + account: v1::SwapBridgeEvmToNativePot::account_id(), + balance: EXISTENTIAL_DEPOSIT_EVM, + }; + + let config = v1::GenesisConfig { + balances: pallet_balances::GenesisConfig { + balances: vec![treasury.into(), swap_bridge_native_evm.into()], + }, + evm_balances: pallet_balances::GenesisConfig { + balances: vec![swap_bridge_evm_native.into()], + }, + swap_bridge_native_to_evm_pot: pallet_pot::GenesisConfig { + initial_state: pallet_pot::InitialState::Initialized, + }, + swap_bridge_evm_to_native_pot: pallet_pot::GenesisConfig { + initial_state: pallet_pot::InitialState::Initialized, + }, + ..Default::default() + }; + new_test_ext_with(config).execute_with(move || { + assert_eq!( + swappable_balance::< + EvmAccountId, + v1::EvmBalances, + v1::SwapBridgeEvmToNativePotAccountId, + >() + .unwrap(), + 0 + ); + assert_eq!( + swappable_balance::< + AccountId, + v1::Balances, + v1::SwapBridgeNativeToEvmPotAccountId, + >() + .unwrap(), + EXISTENTIAL_DEPOSIT_NATIVE + ((u64::MAX - EXISTENTIAL_DEPOSIT_NATIVE) - EXISTENTIAL_DEPOSIT_EVM) + ); + }); + }) +} + +/// This test verifies that balanced bridges initialization fails in case genesis configuration +/// contains native treasury account with insufficient balance to properly perform initialization. +#[test] +#[should_panic = "error during bridges initialization: Module(ModuleError { index: 1, error: [2, 0, 0, 0], message: Some(\"InsufficientBalance\") })"] +fn initialization_fails_treasury_insufficient_balance() { + with_runtime_lock(|| { + let treasury = AccountInfo { + account: NativeTreasury::get(), + balance: EXISTENTIAL_DEPOSIT_NATIVE + 10, + }; + let swap_bridge_native_evm = AccountInfo { + account: v1::SwapBridgeNativeToEvmPot::account_id(), + balance: EXISTENTIAL_DEPOSIT_NATIVE, + }; + + let evm_bridge_delta = 50; + let swap_bridge_evm_native = AccountInfo { + account: v1::SwapBridgeEvmToNativePot::account_id(), + balance: EXISTENTIAL_DEPOSIT_EVM + evm_bridge_delta, + }; + + let config = v1::GenesisConfig { + balances: pallet_balances::GenesisConfig { + balances: vec![treasury.into(), swap_bridge_native_evm.into()], + }, + evm_balances: pallet_balances::GenesisConfig { + balances: vec![ + LION.into(), + DOG.into(), + CAT.into(), + FISH.into(), + swap_bridge_evm_native.into(), + ], + }, + swap_bridge_native_to_evm_pot: pallet_pot::GenesisConfig { + initial_state: pallet_pot::InitialState::Initialized, + }, + swap_bridge_evm_to_native_pot: pallet_pot::GenesisConfig { + initial_state: pallet_pot::InitialState::Initialized, + }, + ..Default::default() + }; + new_test_ext_with(config).execute_with(move || {}); + }) +} + +/// This test simulates runtime upgrade operation by using different mocked runtime versions and +/// verifies that balanced bridges initialization works as expected for `on_runtime_upgrade` call. +/// +/// - v0: just contains native and evm balances. +/// - v1: v0 with balanced bridges currency swap initializer pallet. +#[test] +fn runtime_upgrade() { + with_runtime_lock(|| { + let treasury = AccountInfo { + account: NativeTreasury::get(), + balance: 1450, + }; + + let v1_config = v0::GenesisConfig { + balances: pallet_balances::GenesisConfig { + balances: vec![treasury.into(), ALICE.into(), BOB.into()], + }, + evm_balances: pallet_balances::GenesisConfig { + balances: vec![LION.into(), DOG.into(), CAT.into(), FISH.into()], + }, + ..Default::default() + }; + + new_test_ext_with(v1_config).execute_with(move || { + // Check test preconditions. + assert_eq!( + v1::Balances::total_balance(&v1::SwapBridgeNativeToEvmPot::account_id()), + 0 + ); + assert_eq!( + v1::Balances::total_balance(&v1::SwapBridgeEvmToNativePot::account_id()), + 0 + ); + assert_eq!(>::get(), 0); + + // Do runtime upgrade hook. + v1::AllPalletsWithoutSystem::on_runtime_upgrade(); + + // Verify bridges initialization result. + assert_eq!( + >::get(), + CURRENT_BRIDGES_INITIALIZER_VERSION + ); + assert!(v1::EvmNativeBridgesInitializer::is_balanced().unwrap()); + assert_eq!( + v1::Balances::total_balance(&v1::SwapBridgeNativeToEvmPot::account_id()), + LION.balance + + DOG.balance + + CAT.balance + + FISH.balance + + EXISTENTIAL_DEPOSIT_NATIVE + + (EXISTENTIAL_DEPOSIT_EVM - EXISTENTIAL_DEPOSIT_NATIVE) + ); + assert_eq!( + v1::Balances::total_balance(&NativeTreasury::get()), + treasury.balance + - (LION.balance + + DOG.balance + + CAT.balance + + FISH.balance + + EXISTENTIAL_DEPOSIT_NATIVE) + - (EXISTENTIAL_DEPOSIT_EVM - EXISTENTIAL_DEPOSIT_NATIVE) + ); + assert_eq!( + v1::EvmBalances::total_balance(&v1::SwapBridgeEvmToNativePot::account_id(),), + v1::Balances::total_balance(&NativeTreasury::get()) + + ALICE.balance + + BOB.balance + + EXISTENTIAL_DEPOSIT_EVM + ); + }); + }) +} diff --git a/crates/pallet-balanced-currency-swap-bridges-initializer/src/upgrade_init.rs b/crates/pallet-balanced-currency-swap-bridges-initializer/src/upgrade_init.rs new file mode 100644 index 000000000..5c904673b --- /dev/null +++ b/crates/pallet-balanced-currency-swap-bridges-initializer/src/upgrade_init.rs @@ -0,0 +1,48 @@ +//! Initialization of the bridge pot accounts on runtime upgrade. + +use frame_support::pallet_prelude::*; + +use crate::{Config, InitializerVersion, Pallet, CURRENT_BRIDGES_INITIALIZER_VERSION}; + +/// Initialize the bridges pot accounts. +pub fn on_runtime_upgrade() -> Weight { + let initializer_version = >::get(); + let mut weight = T::DbWeight::get().reads(1); + + if initializer_version != CURRENT_BRIDGES_INITIALIZER_VERSION { + let is_balanced = Pallet::::is_balanced().unwrap_or_default(); + weight += T::DbWeight::get().reads(8); + + if !is_balanced { + match Pallet::::initialize() { + Ok(w) => weight += w, + Err(err) => sp_tracing::error!("error during bridges initialization: {err:?}"), + } + } + + >::put(CURRENT_BRIDGES_INITIALIZER_VERSION); + } + + weight +} + +/// Check the state before the bridges initialization. +/// +/// Panics if anything goes wrong. +#[cfg(feature = "try-runtime")] +pub fn pre_upgrade() -> Result, &'static str> { + // Do nothing. + Ok(Vec::new()) +} + +/// Check the state after the bridges initialization. +/// +/// Panics if anything goes wrong. +#[cfg(feature = "try-runtime")] +pub fn post_upgrade(_state: Vec) -> Result<(), &'static str> { + if !Pallet::::is_balanced()? { + return Err("currencies are not balanced"); + } + + Ok(()) +} diff --git a/crates/pallet-balanced-currency-swap-bridges-initializer/src/weights.rs b/crates/pallet-balanced-currency-swap-bridges-initializer/src/weights.rs new file mode 100644 index 000000000..2c55d478b --- /dev/null +++ b/crates/pallet-balanced-currency-swap-bridges-initializer/src/weights.rs @@ -0,0 +1,15 @@ +//! Weights definition for pallet-bridges-initializer-currency-swap. + +use frame_support::weights::Weight; + +/// Weight functions needed for pallet-bridges-initializer-currency-swap. +pub trait WeightInfo { + /// A function to calculate required weights for `verify_balanced` call. + fn verify_balanced() -> Weight; +} + +impl WeightInfo for () { + fn verify_balanced() -> Weight { + Weight::zero() + } +} diff --git a/utils/checks/snapshots/features.yaml b/utils/checks/snapshots/features.yaml index f494ea86d..0df8472d1 100644 --- a/utils/checks/snapshots/features.yaml +++ b/utils/checks/snapshots/features.yaml @@ -1967,6 +1967,10 @@ - name: pallet-babe 4.0.0-dev features: - std +- name: pallet-balanced-currency-swap-bridges-initializer 0.1.0 + features: + - default + - std - name: pallet-balances 4.0.0-dev features: - default