diff --git a/Cargo.lock b/Cargo.lock index e793163bb..27541d2ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5495,13 +5495,16 @@ dependencies = [ "acala-primitives", "frame-support", "frame-system", + "log", "module-dex", "module-support", "nutsfinance-stable-asset", "orml-tokens", "orml-traits", + "orml-utilities", "pallet-balances", "parity-scale-codec", + "parking_lot 0.12.1", "scale-info", "serde", "sp-core", diff --git a/modules/aggregated-dex/Cargo.toml b/modules/aggregated-dex/Cargo.toml index 3ab934180..9d65bf489 100644 --- a/modules/aggregated-dex/Cargo.toml +++ b/modules/aggregated-dex/Cargo.toml @@ -8,36 +8,45 @@ edition = "2021" serde = { version = "1.0.136", optional = true } codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["max-encoded-len"] } scale-info = { version = "2.1", default-features = false, features = ["derive"] } +log = { version = "0.4.17", default-features = false } + +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.26", default-features = false } sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.26", default-features = false } sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.26", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.26", default-features = false } frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.26", default-features = false } frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.26", default-features = false } -sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.26", default-features = false } -orml-traits = { path = "../../orml/traits", default-features = false } -support = { package = "module-support", path = "../support", default-features = false } -primitives = { package = "acala-primitives", path = "../../primitives", default-features = false } -nutsfinance-stable-asset = { path = "../../ecosystem-modules/stable-asset/lib/stable-asset", version = "0.1.0", default-features = false } + module-dex = { package = "module-dex", path = "../dex", default-features = false } +primitives = { package = "acala-primitives", path = "../../primitives", default-features = false } +support = { package = "module-support", path = "../support", default-features = false } + orml-tokens = { path = "../../orml/tokens", default-features = false } +orml-traits = { path = "../../orml/traits", default-features = false } +orml-utilities = { path = "../../orml/utilities", default-features = false } + +nutsfinance-stable-asset = { path = "../../ecosystem-modules/stable-asset/lib/stable-asset", version = "0.1.0", default-features = false } [dev-dependencies] -orml-tokens = { path = "../../orml/tokens" } -sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.26" } -sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.26" } pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.26" } +parking_lot = { version = "0.12.0" } [features] default = ["std"] std = [ "serde", + "log/std", "codec/std", "scale-info/std", + "sp-io/std", + "sp-core/std", "sp-runtime/std", + "sp-std/std", "frame-support/std", "frame-system/std", - "sp-std/std", "orml-traits/std", "orml-tokens/std", + "orml-utilities/std", "support/std", "primitives/std", "nutsfinance-stable-asset/std", diff --git a/modules/aggregated-dex/src/lib.rs b/modules/aggregated-dex/src/lib.rs index 5cfed06e8..12e36752c 100644 --- a/modules/aggregated-dex/src/lib.rs +++ b/modules/aggregated-dex/src/lib.rs @@ -22,12 +22,25 @@ #![allow(clippy::unused_unit)] #![allow(clippy::type_complexity)] -use frame_support::{pallet_prelude::*, transactional}; -use frame_system::pallet_prelude::*; +use codec::Decode; +use frame_support::{pallet_prelude::*, transactional, PalletId}; +use frame_system::{ + offchain::{SendTransactionTypes, SubmitTransaction}, + pallet_prelude::*, +}; use nutsfinance_stable_asset::traits::StableAsset as StableAssetT; -use primitives::{Balance, CurrencyId}; -use sp_runtime::traits::{Convert, Zero}; -use sp_std::{marker::PhantomData, vec::Vec}; +use orml_utilities::OffchainErr; +use primitives::{Balance, CurrencyId, TradingPair}; +use sp_runtime::offchain::storage_lock::StorageLockGuard; +use sp_runtime::{ + offchain::{ + storage::StorageValueRef, + storage_lock::{StorageLock, Time}, + Duration, + }, + traits::{AccountIdConversion, Convert, Zero}, +}; +use sp_std::{collections::btree_map::BTreeMap, marker::PhantomData, prelude::*, vec::Vec}; use support::{AggregatedSwapPath, DEXManager, RebasedStableAssetError, Swap, SwapLimit}; mod mock; @@ -35,8 +48,15 @@ mod tests; pub mod weights; pub use module::*; +use module_dex::TradingPairStatuses; pub use weights::WeightInfo; +pub const OFFCHAIN_WORKER_DATA: &[u8] = b"acala/dex-bot/data/"; +pub const OFFCHAIN_WORKER_LOCK: &[u8] = b"acala/dex-bot/lock/"; +pub const OFFCHAIN_WORKER_MAX_ITERATIONS: &[u8] = b"acala/dex-bot/max-iterations/"; +pub const LOCK_DURATION: u64 = 100; +pub const DEFAULT_MAX_ITERATIONS: u32 = 100; + pub type SwapPath = AggregatedSwapPath; #[frame_support::pallet] @@ -44,7 +64,9 @@ pub mod module { use super::*; #[pallet::config] - pub trait Config: frame_system::Config { + pub trait Config: frame_system::Config + SendTransactionTypes> + module_dex::Config { + type Event: From> + IsType<::Event>; + /// DEX type DEX: DEXManager; @@ -68,6 +90,13 @@ pub mod module { #[pallet::constant] type SwapPathLimit: Get; + /// Treasury account participate in Rebalance swap. + #[pallet::constant] + type TreasuryPallet: Get; + + #[pallet::constant] + type UnsignedPriority: Get; + type WeightInfo: WeightInfo; } @@ -81,9 +110,29 @@ pub mod module { InvalidTokenIndex, /// The SwapPath is invalid. InvalidSwapPath, + /// Rebalance swap info is invalid. + RebalanceSwapInfoInvalid, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + /// Rebalance trading path and balance. + RebalanceTrading { + currency_id: CurrencyId, + supply_amount: Balance, + target_amount: Balance, + swap_path: Vec, + }, + /// Add rebalance info. + SetupRebalanceSwapInfo { + currency_id: CurrencyId, + supply_amount: Balance, + threshold: Balance, + }, } - /// The specific swap paths for AggregatedSwap do aggreated_swap to swap TokenA to TokenB + /// The specific swap paths for AggregatedSwap do aggregated_swap to swap TokenA to TokenB /// /// AggregatedSwapPaths: Map: (token_a: CurrencyId, token_b: CurrencyId) => paths: Vec #[pallet::storage] @@ -91,13 +140,42 @@ pub mod module { pub type AggregatedSwapPaths = StorageMap<_, Twox64Concat, (CurrencyId, CurrencyId), BoundedVec, OptionQuery>; + /// The specific rebalance swap paths doing aggregated_swap from TokenA to TokenA + /// + /// AggregatedSwapPaths: Map: CurrencyId => paths: Vec + #[pallet::storage] + #[pallet::getter(fn rebalance_swap_paths)] + pub type RebalanceSwapPaths = + StorageMap<_, Twox64Concat, CurrencyId, BoundedVec, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn rebalance_supply_threshold)] + pub type RebalanceSupplyThreshold = + StorageMap<_, Twox64Concat, CurrencyId, (Balance, Balance), OptionQuery>; + #[pallet::pallet] #[pallet::generate_store(pub(super) trait Store)] #[pallet::without_storage_info] pub struct Pallet(_); #[pallet::hooks] - impl Hooks for Pallet {} + impl Hooks for Pallet { + fn offchain_worker(now: T::BlockNumber) { + if let Err(e) = Self::_offchain_worker(now) { + log::info!( + target: "dex-bot", + "offchain worker: cannot run at {:?}: {:?}", + now, e, + ); + } else { + log::debug!( + target: "dex-bot", + "offchain worker: start at block: {:?} already done!", + now, + ); + } + } + } #[pallet::call] impl Pallet { @@ -177,10 +255,101 @@ pub mod module { Ok(()) } + + /// Update the rebalance swap paths for AggregatedSwap to swap TokenA to TokenA. + /// + /// Requires `GovernanceOrigin` + /// + /// Parameters: + /// - `updates`: Vec<(CurrencyId, Option>)> + #[pallet::weight(::WeightInfo::update_aggregated_swap_paths(updates.len() as u32))] + #[transactional] + pub fn update_rebalance_swap_paths( + origin: OriginFor, + updates: Vec<(CurrencyId, Option>)>, + ) -> DispatchResult { + T::GovernanceOrigin::ensure_origin(origin)?; + + for (key, maybe_paths) in updates { + if let Some(paths) = maybe_paths { + let paths: BoundedVec = + paths.try_into().map_err(|_| Error::::InvalidSwapPath)?; + let (supply_currency_id, target_currency_id) = Self::check_swap_paths(&paths)?; + ensure!( + key == supply_currency_id && supply_currency_id == target_currency_id, + Error::::InvalidSwapPath + ); + RebalanceSwapPaths::::insert(key, paths); + } else { + RebalanceSwapPaths::::remove(key); + } + } + + Ok(()) + } + + /// Update the rebalance swap information for specify token. + /// + /// Parameters: + /// - `currency_id`: the token used for rebalance swap + /// - `supply_amount`: the supply amount of `currency_id` used for rebalance swap + /// - `threshold`: the target amount of `currency_id` used for rebalance swap + #[pallet::weight(::WeightInfo::set_rebalance_swap_info())] + #[transactional] + pub fn set_rebalance_swap_info( + origin: OriginFor, + currency_id: CurrencyId, + #[pallet::compact] supply_amount: Balance, + #[pallet::compact] threshold: Balance, + ) -> DispatchResult { + T::GovernanceOrigin::ensure_origin(origin)?; + Self::do_set_rebalance_swap_info(currency_id, supply_amount, threshold) + } + + /// Force execution rebalance swap by offchain worker. + /// + /// Parameters: + /// - `currency_id`: the token used for rebalance swap + /// - `swap_path`: the aggregated swap path used for rebalance swap + #[pallet::weight(::WeightInfo::force_rebalance_swap())] + #[transactional] + pub fn force_rebalance_swap( + origin: OriginFor, + currency_id: CurrencyId, + swap_path: Vec, + ) -> DispatchResult { + ensure_none(origin)?; + Self::do_rebalance_swap(currency_id, swap_path) + } + } + + #[pallet::validate_unsigned] + impl ValidateUnsigned for Pallet { + type Call = Call; + fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { + if let Call::force_rebalance_swap { + currency_id, + swap_path: _, + } = call + { + ValidTransaction::with_tag_prefix("DexBotOffchainWorker") + .priority(T::UnsignedPriority::get()) + .and_provides((>::block_number(), currency_id)) + .longevity(64_u64) + .propagate(true) + .build() + } else { + InvalidTransaction::Call.into() + } + } } } impl Pallet { + fn treasury_account() -> T::AccountId { + T::TreasuryPallet::get().into_account_truncating() + } + fn check_swap_paths(paths: &[SwapPath]) -> sp_std::result::Result<(CurrencyId, CurrencyId), DispatchError> { ensure!(!paths.is_empty(), Error::::InvalidSwapPath); let mut supply_currency_id: Option = None; @@ -379,6 +548,192 @@ impl Pallet { } } } + + fn do_set_rebalance_swap_info( + currency_id: CurrencyId, + supply_amount: Balance, + threshold: Balance, + ) -> DispatchResult { + ensure!(threshold > supply_amount, Error::::RebalanceSwapInfoInvalid); + RebalanceSupplyThreshold::::try_mutate(currency_id, |maybe_supply_threshold| -> DispatchResult { + *maybe_supply_threshold = Some((supply_amount, threshold)); + Ok(()) + })?; + Self::deposit_event(Event::SetupRebalanceSwapInfo { + currency_id, + supply_amount, + threshold, + }); + Ok(()) + } + + fn submit_rebalance_swap_tx(currency_id: CurrencyId, swap_path: Vec) { + let call = Call::::force_rebalance_swap { currency_id, swap_path }; + if let Err(err) = SubmitTransaction::>::submit_unsigned_transaction(call.into()) { + log::info!( + target: "dex-bot", + "offchain worker: submit unsigned swap from currency:{:?}, failed: {:?}", + currency_id, err, + ); + } + } + + pub fn calculate_rebalance_paths( + // mut _finished: bool, + // mut iteration_count: u32, + max_iterations: u32, + // mut _last_currency_id: Option, + start_key: Option>, + mut guard: Option<&mut StorageLockGuard