diff --git a/Cargo.lock b/Cargo.lock index 5e8c95cc1..4a08fe50d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4991,6 +4991,7 @@ dependencies = [ "pallet-genesis-history", "pallet-identity", "pallet-lbp", + "pallet-liquidation", "pallet-liquidity-mining", "pallet-message-queue", "pallet-multisig", @@ -8511,6 +8512,43 @@ dependencies = [ "test-utils", ] +[[package]] +name = "pallet-liquidation" +version = "1.0.0" +dependencies = [ + "ethabi", + "evm", + "frame-benchmarking", + "frame-support", + "frame-system", + "hex-literal", + "hydra-dx-math", + "hydradx-traits", + "log", + "module-evm-utility-macro", + "num_enum", + "orml-tokens", + "orml-traits", + "pallet-asset-registry", + "pallet-balances", + "pallet-currencies", + "pallet-evm-accounts", + "pallet-omnipool", + "pallet-route-executor", + "parity-scale-codec", + "parking_lot 0.12.3", + "pretty_assertions", + "proptest", + "scale-info", + "sp-api", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "test-utils", +] + [[package]] name = "pallet-liquidity-mining" version = "4.4.2" @@ -12255,6 +12293,7 @@ dependencies = [ "pallet-evm-precompile-call-permit", "pallet-im-online", "pallet-lbp", + "pallet-liquidation", "pallet-liquidity-mining", "pallet-omnipool", "pallet-omnipool-liquidity-mining", diff --git a/Cargo.toml b/Cargo.toml index f96bbb48e..cc229911d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ members = [ 'pallets/evm-accounts', 'pallets/dynamic-evm-fee', 'pallets/xyk-liquidity-mining', + 'pallets/liquidation', 'precompiles/call-permit', 'runtime-mock' ] @@ -143,6 +144,7 @@ pallet-xyk-liquidity-mining = { path = "pallets/xyk-liquidity-mining", default-f pallet-referrals = { path = "pallets/referrals", default-features = false } pallet-evm-accounts = { path = "pallets/evm-accounts", default-features = false } pallet-evm-accounts-rpc-runtime-api = { path = "pallets/evm-accounts/rpc/runtime-api", default-features = false } +pallet-liquidation = { path = "pallets/liquidation", default-features = false } hydra-dx-build-script-utils = { path = "utils/build-script-utils", default-features = false } scraper = { path = "scraper", default-features = false } diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 6ece013ee..e9f537f2e 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -41,14 +41,15 @@ pallet-staking = { workspace = true } pallet-lbp = { workspace = true } pallet-xyk = { workspace = true } pallet-evm-accounts = { workspace = true } +pallet-xyk-liquidity-mining = { workspace = true } +pallet-transaction-pause = { workspace = true } +pallet-liquidation = { workspace = true } pallet-treasury = { workspace = true } pallet-democracy = { workspace = true } pallet-scheduler = { workspace = true } pallet-elections-phragmen = { workspace = true } pallet-tips = { workspace = true } -pallet-xyk-liquidity-mining = { workspace = true } -pallet-transaction-pause = { workspace = true } # collator support pallet-collator-selection = { workspace = true } @@ -217,6 +218,7 @@ std = [ "pallet-dynamic-evm-fee/std", "precompile-utils/std", "pallet-transaction-pause/std", + "pallet-liquidation/std", ] # we don't include integration tests when benchmarking feature is enabled diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs index b6a64ba7c..644844477 100644 --- a/integration-tests/src/lib.rs +++ b/integration-tests/src/lib.rs @@ -33,6 +33,7 @@ pub mod utils; mod vesting; mod xyk; mod xyk_liquidity_mining; +mod liquidation; #[macro_export] macro_rules! assert_balance { diff --git a/integration-tests/src/liquidation.rs b/integration-tests/src/liquidation.rs new file mode 100644 index 000000000..9fd88d5de --- /dev/null +++ b/integration-tests/src/liquidation.rs @@ -0,0 +1,52 @@ +#![cfg(test)] + +use ethabi::ethereum_types::H160; +use fp_evm::ExitSucceed; +use crate::polkadot_test_net::*; + +use frame_support::{ + assert_noop, assert_ok, + sp_runtime::RuntimeDebug, +}; +use hex_literal::hex; +use orml_traits::currency::MultiCurrency; +use orml_traits::MultiCurrencyExtended; +use sp_runtime::FixedPointNumber; +use sp_runtime::{FixedU128, Permill}; +use xcm_emulator::TestExt; +use hydradx_traits::evm::EvmAddress; +use num_enum::{IntoPrimitive, TryFromPrimitive}; +use fp_evm::ExitReason::Succeed; +use fp_evm::ExitSucceed::Returned; +use hydradx_traits::evm::{CallContext, EVM}; + +const PATH_TO_SNAPSHOT: &str = "evm-snapshot/SNAPSHOT"; + +#[module_evm_utility_macro::generate_function_selector] +#[derive(RuntimeDebug, Eq, PartialEq, TryFromPrimitive, IntoPrimitive)] +#[repr(u32)] +pub enum Function { + GetPool = "getPool", +} + +#[test] +fn liquidation() { + TestNet::reset(); + hydra_live_ext(PATH_TO_SNAPSHOT).execute_with(|| { + // let mut storage = pallet_evm::AccountCodes::::iter(); + // for i in storage { + // println!("------ {:?}", i.0); + // + // } + + let contract = EvmAddress::from_slice(hex!("82db570265c37bE24caf5bc943428a6848c3e9a6").as_slice()); + + let data = Into::::into(Function::GetPool).to_be_bytes().to_vec(); + let context = CallContext::new_view(contract); + + let (res, value) = hydradx_runtime::evm::Executor::::view(context, data, 500_000); + + // assert_eq!(res, Succeed(Returned)); + println!("---- {:?}", value); + }); +} diff --git a/pallets/liquidation/Cargo.toml b/pallets/liquidation/Cargo.toml new file mode 100644 index 000000000..37a1132ee --- /dev/null +++ b/pallets/liquidation/Cargo.toml @@ -0,0 +1,86 @@ +[package] +name = 'pallet-liquidation' +version = '1.0.0' +description = 'A pallet for money market liquidations' +authors = ['GalacticCouncil'] +edition = '2021' +license = 'Apache 2.0' +repository = "https://github.com/galacticcouncil/Hydradx-node" + +[dependencies] +# parity +codec = { workspace = true, features = ["derive", "max-encoded-len"] } +scale-info = { workspace = true } +log = { workspace = true } + +# primitives +sp-runtime = { workspace = true } +sp-std = { workspace = true } +sp-core = { workspace = true } +sp-io = { workspace = true } +sp-arithmetic = { workspace = true } + +# FRAME +frame-support = { workspace = true } +frame-system = { workspace = true } + +evm = { workspace = true, features = ["with-codec"] } +module-evm-utility-macro = { workspace = true } +num_enum = { workspace = true, default-features = false } +ethabi = { workspace = true } + +# HydraDX dependencies +hydradx-traits = { workspace = true } + +# Optional imports for benchmarking +frame-benchmarking = { workspace = true, optional = true } + +[dev-dependencies] +hydra-dx-math = { workspace = true } +pallet-omnipool = { workspace = true } +pallet-asset-registry = { workspace = true } +pallet-route-executor = { workspace = true } +pallet-balances = { workspace = true } +pallet-currencies = { workspace = true } +pallet-evm-accounts = { workspace = true } +sp-api = { workspace = true } +orml-traits = { workspace = true } +orml-tokens = { workspace = true, features = ["std"] } +hex-literal = { workspace = true } +proptest = { workspace = true } +pretty_assertions = { workspace = true } +test-utils = { workspace = true } +parking_lot = { workspace = true } + +[features] +default = ['std'] +std = [ + 'codec/std', + 'frame-support/std', + 'frame-system/std', + 'sp-runtime/std', + 'sp-core/std', + 'sp-io/std', + 'sp-std/std', + 'sp-arithmetic/std', + 'sp-api/std', + 'scale-info/std', + 'orml-tokens/std', + 'orml-traits/std', + 'hydradx-traits/std', + 'hydra-dx-math/std', + 'frame-benchmarking/std', + 'pallet-balances/std', + 'pallet-currencies/std', + 'pallet-route-executor/std', + 'pallet-omnipool/std', + 'pallet-asset-registry/std', + 'pallet-evm-accounts/std', +] + +runtime-benchmarks = [ + "frame-benchmarking", + "frame-system/runtime-benchmarks", + "frame-support/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/liquidation/README.md b/pallets/liquidation/README.md new file mode 100644 index 000000000..972f6e386 --- /dev/null +++ b/pallets/liquidation/README.md @@ -0,0 +1,6 @@ +# Pallet (Money market) Liquidation +## Description + +## Notes + +## Dispatachable functions \ No newline at end of file diff --git a/pallets/liquidation/src/benchmarks.rs b/pallets/liquidation/src/benchmarks.rs new file mode 100644 index 000000000..ea6cda44f --- /dev/null +++ b/pallets/liquidation/src/benchmarks.rs @@ -0,0 +1,56 @@ +// Copyright (C) 2020-2023 Intergalactic, Limited (GIB). SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#![cfg(feature = "runtime-benchmarks")] +use super::*; +use frame_benchmarking::{account, benchmarks}; +use frame_support::assert_ok; +use frame_support::traits::fungibles::Mutate; +use frame_system::RawOrigin; + +pub const ONE: Balance = 1_000_000_000_000; +pub const HDX: u32 = 0; +pub const DAI: u32 = 2; + +benchmarks! { + where_clause { where + AssetIdOf: From, + ::Currency: Mutate, Balance = Balance>, + T: crate::Config + pallet_otc::Config, + } + settle_otc_order { + let account: T::AccountId = account("acc", 1, 1); + + ::Currency::mint_into(HDX.into(), &account, 1_000_000_000 * ONE)?; + ::Currency::mint_into(DAI.into(), &account, 1_000_000_000 * ONE)?; + + assert_ok!( + pallet_otc::Pallet::::place_order(RawOrigin::Signed(account).into(), HDX.into(), DAI.into(), 100_000_000 * ONE, 202_020_001 * ONE, true) + ); + + let route = ::Router::get_route(AssetPair { + asset_in: DAI.into(), + asset_out: HDX.into(), + }); + + }: _(RawOrigin::None, 0u32, 2 * ONE, route) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mock::*; + use frame_benchmarking::impl_benchmark_test_suite; + + impl_benchmark_test_suite!(Pallet, super::ExtBuilder::default().build().0, super::Test); +} diff --git a/pallets/liquidation/src/lib.rs b/pallets/liquidation/src/lib.rs new file mode 100644 index 000000000..0eea9ba0e --- /dev/null +++ b/pallets/liquidation/src/lib.rs @@ -0,0 +1,314 @@ +// This file is part of HydraDX. +// Copyright (C) 2020-2023 Intergalactic, Limited (GIB). SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Pallet (Money market) Liquidation +//! +//! ## Description +//! +//! ## Notes +//! +//! ## Dispatachable functions + +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::{ + PalletId, pallet_prelude::*, + traits::tokens::{Fortitude, Precision, Preservation}, + traits::fungibles::{Inspect, Mutate}, +}; +use frame_system::{ + ensure_signed, RawOrigin, + pallet_prelude::OriginFor, +}; +use hydradx_traits::{ + router::{ + AmmTradeWeights, AmountInAndOut, RouteProvider, RouteSpotPriceProvider, RouterT, Trade, + }, + evm::{CallContext, EVM, EvmAddress, Erc20Mapping, InspectEvmAccounts}, +}; +use sp_arithmetic::{ + ArithmeticError, +}; +use sp_runtime::{ + SaturatedConversion, + traits::{AccountIdConversion, CheckedConversion}, +}; +use ethabi::ethereum_types::BigEndianHash; +use sp_core::crypto::AccountId32; +use sp_std::vec; +use sp_std::vec::Vec; + +#[cfg(test)] +mod tests; + +// #[cfg(feature = "runtime-benchmarks")] +// mod benchmarks; + +pub mod weights; + +pub use weights::WeightInfo; + +// Re-export pallet items so that they can be accessed from the crate namespace. +pub use pallet::*; +use evm::ExitReason; +use num_enum::{IntoPrimitive, TryFromPrimitive}; +use sp_core::{H256, U256}; + +pub type Balance = u128; +pub type AssetId = u32; +pub type NamedReserveIdentifier = [u8; 8]; +pub type CallResult = (ExitReason, Vec); + +pub const PALLET_ID: PalletId = PalletId(*b"lqdation"); + +#[module_evm_utility_macro::generate_function_selector] +#[derive(RuntimeDebug, Eq, PartialEq, TryFromPrimitive, IntoPrimitive)] +#[repr(u32)] +pub enum Function { + LiquidationCall = "liquidationCall(address,address,address,uint256,bool)", +} + +#[frame_support::pallet] +pub mod pallet { + use evm::ExitSucceed; + use super::*; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// Named reservable multi currency. + type Currency: Mutate; + + /// EVM handler + type Evm: EVM; + + /// Router implementation. + type Router: RouteProvider + + RouterT, AmountInAndOut> + + RouteSpotPriceProvider; + + /// Money market contract address + type MoneyMarketContract: Get; + + /// EVM address converter + type EvmAccounts: InspectEvmAccounts; + + /// Mapping between AssetId and ERC20 address. + type Erc20Mapping: Erc20Mapping; + + /// Account who receives the profit. + #[pallet::constant] + type ProfitReceiver: Get; + + /// Router weight information. + type RouterWeightInfo: AmmTradeWeights>; + + /// Weight information for the extrinsics. + type WeightInfo: WeightInfo; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + /// A trade has been executed + Liquidated { + liquidator: T::AccountId, + evm_address: EvmAddress, + debt_asset: AssetId, + collateral_asset: AssetId, + debt_to_cover: Balance, + }, + } + + #[pallet::error] + pub enum Error { + /// AssetId to EVM address conversion failed + AssetConversionFailed, + /// EVM call failed + EvmExecutionFailed, + /// Provided route doesn't match the existing route + InvalidRoute, + /// Initial and final balance are different + BalanceInconsistency, + /// Trade amount higher than necessary + TradeAmountTooHigh, + /// Trade amount lower than necessary + TradeAmountTooLow, + /// Price for a route is not available + PriceNotAvailable, + } + + #[pallet::call] + impl Pallet + where + T::AccountId: AsRef<[u8; 32]> + frame_support::traits::IsType, + { + /// Close an existing OTC arbitrage opportunity. + /// + /// Executes a trade between an OTC order and some route. + /// If the OTC order is partially fillable, the extrinsic fails if the existing arbitrage + /// opportunity is not closed or reduced after the trade. + /// If the OTC order is not partially fillable, fails if there is no profit after the trade. + /// + /// `Origin` calling this extrinsic is not paying or receiving anything. + /// + /// The profit made by closing the arbitrage is transferred to `FeeReceiver`. + /// + /// Parameters: + /// - `origin`: Signed or unsigned origin. Unsigned origin doesn't pay the TX fee, + /// but can be submitted only by a collator. + /// - `otc_id`: ID of the OTC order with existing arbitrage opportunity. + /// - `amount`: Amount necessary to close the arb. + /// - `route`: The route we trade against. Required for the fee calculation. + /// + /// Emits `Executed` event when successful. + /// + #[pallet::call_index(0)] + #[pallet::weight(::WeightInfo::liquidate())] + pub fn liquidate( + origin: OriginFor, + collateral_asset: AssetId, + debt_asset: AssetId, + user: EvmAddress, + debt_to_cover: Balance, + route: Vec>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let pallet_acc = Self::account_id(); + + let debt_asset_initial_balance = ::Currency::balance(debt_asset, &pallet_acc); + + // mint borrow asset + ::Currency::mint_into(debt_asset, &pallet_acc, debt_to_cover)?; + + // liquidation + let caller_evm_account = T::EvmAccounts::evm_address(&pallet_acc); + let mm_contract_address = T::MoneyMarketContract::get(); + let context = CallContext::new_call(mm_contract_address, caller_evm_account); + let collateral_asset_evm_address = T::Erc20Mapping::encode_evm_address(collateral_asset).ok_or(Error::::AssetConversionFailed)?; + let debt_asset_evm_address = T::Erc20Mapping::encode_evm_address(debt_asset).ok_or(Error::::AssetConversionFailed)?; + let data = Self::encode_liquidation_call_data( + collateral_asset_evm_address, + debt_asset_evm_address, + user, + debt_to_cover, + false, // TODO + ); + // + let value = U256::zero(); // TODO + let gas = 200_000; // TODO + let (exit_reason, _value) = T::Evm::call(context, data, value, gas); + if exit_reason != ExitReason::Succeed(ExitSucceed::Returned) { + return Err(Error::::EvmExecutionFailed.into()); + } + + // swap collateral asset for borrow asset + let collateral_earned = ::Currency::balance(collateral_asset, &pallet_acc); + T::Router::sell( + RawOrigin::Signed(pallet_acc.clone()).into(), + collateral_asset, + debt_asset, + collateral_earned, + 1, + route.clone(), + )?; + + //burn + let debt_asset_final_balance = ::Currency::balance(debt_asset, &pallet_acc); + let debt_asset_earned = debt_asset_final_balance.checked_sub(debt_asset_initial_balance).ok_or(ArithmeticError::Overflow)?; + // ensure that we get back at least the amount we minted + ensure!(debt_asset_earned >= debt_to_cover,ArithmeticError::Overflow); + + ::Currency::burn_from(debt_asset, &pallet_acc, debt_to_cover, Precision::Exact, Fortitude::Force)?; + + // transfer remaining balance + let transferable_amount = debt_asset_final_balance.checked_sub(debt_to_cover).ok_or(ArithmeticError::Overflow)?; + ::Currency::transfer( + debt_asset, + &pallet_acc, + &T::ProfitReceiver::get(), + transferable_amount, + Preservation::Expendable, + )?; + + Self::deposit_event(Event::Liquidated { + liquidator: who, + evm_address: user, + debt_asset, + collateral_asset, + debt_to_cover, + }); + + Ok(()) + } + } +} + +impl Pallet { + fn account_id() -> T::AccountId { + PALLET_ID.into_account_truncating() + } + + fn encode_liquidation_call_data(collateral_asset: EvmAddress, debt_asset: EvmAddress, user: EvmAddress, debt_to_cover: Balance, receive_atoken: bool) -> Vec { + let mut data = Into::::into(Function::LiquidationCall).to_be_bytes().to_vec(); + data.extend_from_slice(H256::from(collateral_asset).as_bytes()); + data.extend_from_slice(H256::from(debt_asset).as_bytes()); + data.extend_from_slice(H256::from(user).as_bytes()); + data.extend_from_slice(H256::from_uint(&U256::from(debt_to_cover.saturated_into::())).as_bytes()); + let mut buffer = [0u8; 32]; + if receive_atoken { + buffer[31] = 1; + } + data.extend_from_slice(&buffer); + + data + } + + fn decode_liquidation_call_data(data: Vec) -> Option<(EvmAddress, EvmAddress, EvmAddress, Balance, bool)> { + if data.len() != 164 { + return None; + } + let data = data.clone(); + + let function_u32: u32 = u32::from_be_bytes(data[0..4].try_into().unwrap()); + let function: Function = Function::try_from(function_u32).unwrap(); + if function != Function::LiquidationCall { + return None; + } + + let collateral_asset_h256 = H256::from_slice(&data[4..36]); + let collateral_asset = EvmAddress::from(collateral_asset_h256); + + let debt_asset_h256 = H256::from_slice(&data[36..68]); + let debt_asset = EvmAddress::from(debt_asset_h256); + + let user_h256 = H256::from_slice(&data[68..100]); + let user = EvmAddress::from(user_h256); + + let debt_to_cover_u256 = U256::checked_from(&data[100..132])?; + let debt_to_cover = Balance::try_from(debt_to_cover_u256).ok()?; + + let receive_atoken_h256 = H256::from_slice(&data[132..164]); + let receive_atoken = !receive_atoken_h256.is_zero(); + + Some((collateral_asset, debt_asset, user, debt_to_cover, receive_atoken)) + } +} + diff --git a/pallets/liquidation/src/tests/liquidation.rs b/pallets/liquidation/src/tests/liquidation.rs new file mode 100644 index 000000000..84cc141f8 --- /dev/null +++ b/pallets/liquidation/src/tests/liquidation.rs @@ -0,0 +1,87 @@ +// we don't need to run tests with benchmarking feature +#![cfg(not(feature = "runtime-benchmarks"))] +#![allow(clippy::bool_assert_comparison)] + +pub use crate::tests::mock::*; +use frame_support::assert_ok; +use orml_traits::MultiCurrency; +use hydradx_traits::{ + evm::InspectEvmAccounts, + router::{AssetPair, RouteProvider}, +}; +use crate::Event; + +pub fn expect_last_events(e: Vec) { + test_utils::expect_events::(e); +} + +#[test] +fn liquidation_should_transfer_profit_to_treasury() { + ExtBuilder::default().build().execute_with(|| { + let bob_evm_address = EvmAccounts::evm_address(&BOB); + let debt_to_cover = 1_000 * ONE; + + let route = Router::get_route(AssetPair { + asset_in: HDX, + asset_out: DOT, + }); + + let hdx_total_issuance = Currencies::total_issuance(HDX); + let dot_total_issuance = Currencies::total_issuance(DOT); + + let hdx_alice_balance_before = Currencies::free_balance(HDX, &ALICE); + let dot_alice_balance_before = Currencies::free_balance(DOT, &ALICE); + + assert!(Currencies::free_balance(HDX, &Liquidation::account_id()) == 0); + assert!(Currencies::free_balance(DOT, &Liquidation::account_id()) == 0); + + let hdx_contract_balance_before = Currencies::free_balance(HDX, &MONEY_MARKET); + let dot_contract_balance_before = Currencies::free_balance(DOT, &MONEY_MARKET); + + assert_ok!( + EvmAccounts::bind_evm_address( + RuntimeOrigin::signed(Liquidation::account_id()), + ) + ); + assert_ok!( + EvmAccounts::bind_evm_address( + RuntimeOrigin::signed(MONEY_MARKET), + ) + ); + + assert_ok!(Liquidation::liquidate( + RuntimeOrigin::signed(ALICE), + HDX, // collateral + DOT, // debt + bob_evm_address, + debt_to_cover, + route, + )); + + // total issuance should not change + assert_eq!(hdx_total_issuance, Currencies::total_issuance(HDX)); + assert_eq!(dot_total_issuance, Currencies::total_issuance(DOT)); + + assert_eq!(hdx_alice_balance_before, Currencies::free_balance(HDX, &ALICE)); + assert_eq!(dot_alice_balance_before, Currencies::free_balance(DOT, &ALICE)); + + assert!(Currencies::free_balance(HDX, &Liquidation::account_id()) == 0); + assert!(Currencies::free_balance(DOT, &Liquidation::account_id()) == 0); + + assert_eq!(Currencies::free_balance(HDX, &TreasuryAccount::get()), 0); + let profit = 2976143141153081; + assert_eq!(Currencies::free_balance(DOT, &TreasuryAccount::get()), profit); + + assert_eq!(Currencies::free_balance(HDX, &MONEY_MARKET), hdx_contract_balance_before - 2 * debt_to_cover); + assert_eq!(Currencies::free_balance(DOT, &MONEY_MARKET), dot_contract_balance_before + debt_to_cover); + + expect_last_events(vec![Event::Liquidated { + liquidator: ALICE, + evm_address: bob_evm_address, + debt_asset: DOT, + collateral_asset: HDX, + debt_to_cover, + } + .into()]); + }); +} diff --git a/pallets/liquidation/src/tests/mock.rs b/pallets/liquidation/src/tests/mock.rs new file mode 100644 index 000000000..2f557022b --- /dev/null +++ b/pallets/liquidation/src/tests/mock.rs @@ -0,0 +1,643 @@ +use ethabi::ethereum_types::H160; +use evm::{ExitError, ExitSucceed}; +use crate as pallet_liquidation; +use crate::*; +use frame_support::{ + assert_ok, parameter_types, + sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup, IdentifyAccount, Verify}, + BuildStorage, Permill, FixedU128, MultiSignature, + }, + traits::{ + tokens::nonfungibles::{Create, Inspect, Mutate}, + Everything, Nothing, + }, +}; +use frame_system::{EnsureRoot, EnsureSigned}; +use hydra_dx_math::{ema::EmaPrice, ratio::Ratio}; +use hydradx_traits::{ + router::{PoolType, RefundEdCalculator}, + OraclePeriod, PriceOracle, +}; +use orml_traits::{parameter_type_with_key, GetByKey}; +use pallet_currencies::{fungibles::FungibleCurrencies, BasicCurrencyAdapter, MockBoundErc20, MockErc20Currency}; +use pallet_omnipool::traits::ExternalPriceProvider; +use sp_core::H256; +use hex_literal::hex; + +type Block = frame_system::mocking::MockBlock; + +pub type Signature = MultiSignature; +pub type AccountId = <::Signer as IdentifyAccount>::AccountId; +pub type Amount = i128; +pub type AssetId = u32; +pub type Balance = u128; +pub type NamedReserveIdentifier = [u8; 8]; + +pub const HDX: AssetId = 0; +pub const LRNA: AssetId = 1; +pub const DAI: AssetId = 2; +pub const DOT: AssetId = 3; +pub const KSM: AssetId = 4; +pub const BTC: AssetId = 5; + +pub const ONE: Balance = 1_000_000_000_000; +pub const ALICE_HDX_INITIAL_BALANCE: Balance = 1_000_000_000_000 * ONE; +pub const ALICE_DOT_INITIAL_BALANCE: Balance = 1_000_000_000_000 * ONE; + +pub const ALICE: AccountId = AccountId::new([1; 32]); +pub const BOB: AccountId = AccountId::new([2; 32]); +pub const MONEY_MARKET: AccountId = AccountId::new([9; 32]); + +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system, + Balances: pallet_balances, + Tokens: orml_tokens, + Currencies: pallet_currencies, + AssetRegistry: pallet_asset_registry, + Omnipool: pallet_omnipool, + Router: pallet_route_executor, + EvmAccounts: pallet_evm_accounts, + Liquidation: pallet_liquidation, + } +); + +parameter_types! { + pub MoneyMarketContract: EvmAddress = EvmAddress::from_slice(&[9; 20]); +} + +parameter_type_with_key! { + pub ExistentialDeposits: |_currency_id: AssetId| -> Balance { + 1 + }; +} + +pub struct EvmMock; +impl EVM for EvmMock { + fn call(context: CallContext, data: Vec, _value: U256, _gas: u64) -> CallResult { + if context.contract == MoneyMarketContract::get() { + let maybe_data = Liquidation::decode_liquidation_call_data(data); + match maybe_data { + Some(data) => { + let collateral_asset = HydraErc20Mapping::decode_evm_address(data.0); + let debt_asset = HydraErc20Mapping::decode_evm_address(data.1); + + if collateral_asset.is_none() || debt_asset.is_none() { + return (ExitReason::Error(ExitError::DesignatedInvalid), vec![]) + }; + + let collateral_asset = collateral_asset.unwrap(); + let debt_asset = debt_asset.unwrap(); + + let caller = EvmAccounts::account_id(context.sender); + let contract_addr = EvmAccounts::account_id(context.contract); + let amount = data.3; + + let first_transfer_result = Currencies::transfer(RuntimeOrigin::signed(caller.clone()), contract_addr.clone(), debt_asset, amount); + let second_transfer_result = Currencies::transfer(RuntimeOrigin::signed(contract_addr), caller, collateral_asset, 2 * amount); + + if first_transfer_result.is_err() || second_transfer_result.is_err() { + return (ExitReason::Error(ExitError::DesignatedInvalid), vec![]) + } + }, + None => return (ExitReason::Error(ExitError::DesignatedInvalid), vec![]) + } + + (ExitReason::Succeed(ExitSucceed::Returned), vec![]) + } else { + (ExitReason::Error(ExitError::DesignatedInvalid), vec![]) + } + } + + fn view(_context: CallContext, _data: Vec, _gas: u64) -> CallResult { + unimplemented!() + } +} + +pub struct HydraErc20Mapping; +impl Erc20Mapping for HydraErc20Mapping { + fn encode_evm_address(asset_id: AssetId) -> Option { + let asset_id_bytes: [u8; 4] = asset_id.to_le_bytes(); + + let mut evm_address_bytes = [0u8; 20]; + + evm_address_bytes[15] = 1; + + for i in 0..4 { + evm_address_bytes[16 + i] = asset_id_bytes[3 - i]; + } + + Some(EvmAddress::from(evm_address_bytes)) + } + + fn decode_evm_address(evm_address: EvmAddress) -> Option { + if !is_asset_address(evm_address) { + return None; + } + + let mut asset_id: u32 = 0; + for byte in evm_address.as_bytes() { + asset_id = (asset_id << 8) | (*byte as u32); + } + + Some(asset_id) + } +} + +pub fn is_asset_address(address: H160) -> bool { + let asset_address_prefix = &(H160::from(hex!("0000000000000000000000000000000100000000"))[0..16]); + + &address.to_fixed_bytes()[0..16] == asset_address_prefix +} + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type Currency = FungibleCurrencies; + type Evm = EvmMock; + type Router = Router; + type MoneyMarketContract = MoneyMarketContract; + type EvmAccounts = EvmAccounts; + type Erc20Mapping = HydraErc20Mapping; + type ProfitReceiver = TreasuryAccount; + type RouterWeightInfo = (); + type WeightInfo = (); +} + + +parameter_types! { + pub DefaultRoutePoolType: PoolType = PoolType::Omnipool; + pub const RouteValidationOraclePeriod: OraclePeriod = OraclePeriod::TenMinutes; +} + +pub struct MockedEdCalculator; + +impl RefundEdCalculator for MockedEdCalculator { + fn calculate() -> Balance { + 1_000_000_000_000 + } +} + +pub struct PriceProviderMock {} + +impl PriceOracle for PriceProviderMock { + type Price = Ratio; + + fn price(route: &[Trade], _: OraclePeriod) -> Option { + let has_insufficient_asset = route.iter().any(|t| t.asset_in > 2000 || t.asset_out > 2000); + if has_insufficient_asset { + return None; + } + Some(Ratio::new(88, 100)) + } +} + +impl pallet_route_executor::Config for Test { + type RuntimeEvent = RuntimeEvent; + type AssetId = AssetId; + type Balance = Balance; + type NativeAssetId = HDXAssetId; + type Currency = FungibleCurrencies; + type InspectRegistry = AssetRegistry; + type AMM = Omnipool; + type EdToRefundCalculator = MockedEdCalculator; + type OraclePriceProvider = PriceProviderMock; + type OraclePeriod = RouteValidationOraclePeriod; + type DefaultRoutePoolType = DefaultRoutePoolType; + type TechnicalOrigin = EnsureRoot; + type WeightInfo = (); +} + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 63; + pub const MaxReserves: u32 = 50; +} + +impl frame_system::Config for Test { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type RuntimeTask = RuntimeTask; + type Nonce = u64; + type Block = Block; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; + type SingleBlockMigrations = (); + type MultiBlockMigrator = (); + type PreInherents = (); + type PostInherents = (); + type PostTransactions = (); +} + +impl orml_tokens::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type Amount = Amount; + type CurrencyId = AssetId; + type WeightInfo = (); + type ExistentialDeposits = ExistentialDeposits; + type MaxLocks = (); + type DustRemovalWhitelist = Nothing; + type ReserveIdentifier = NamedReserveIdentifier; + type MaxReserves = MaxReserves; + type CurrencyHooks = (); +} + +parameter_types! { + pub const HDXAssetId: AssetId = HDX; + pub const LRNAAssetId: AssetId = LRNA; + pub const PositionCollectionId: u32 = 1000; + + pub const ExistentialDeposit: u128 = 500; + pub ProtocolFee: Permill = Permill::from_percent(0); + pub AssetFee: Permill = Permill::from_percent(0); + pub AssetWeightCap: Permill = Permill::from_percent(100); + pub MinAddedLiquidity: Balance = 1000u128; + pub MinTradeAmount: Balance = 1000u128; + pub MaxInRatio: Balance = 1u128; + pub MaxOutRatio: Balance = 1u128; + pub const TVLCap: Balance = Balance::MAX; + + pub const TransactionByteFee: Balance = 10 * ONE / 100_000; + + pub const TreasuryPalletId: PalletId = PalletId(*b"aca/trsy"); + pub TreasuryAccount: AccountId = TreasuryPalletId::get().into_account_truncating(); +} + +impl pallet_balances::Config for Test { + type MaxLocks = (); + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = frame_system::Pallet; + type WeightInfo = (); + type MaxReserves = MaxReserves; + type ReserveIdentifier = NamedReserveIdentifier; + type FreezeIdentifier = (); + type MaxFreezes = (); + type RuntimeHoldReason = (); + type RuntimeFreezeReason = (); +} + +impl pallet_currencies::Config for Test { + type RuntimeEvent = RuntimeEvent; + type MultiCurrency = Tokens; + type NativeCurrency = BasicCurrencyAdapter; + type Erc20Currency = MockErc20Currency; + type BoundErc20 = MockBoundErc20; + type GetNativeCurrencyId = HDXAssetId; + type WeightInfo = (); +} + +parameter_types! { + pub const MinTradingLimit: Balance = 1_000; + pub const MinPoolLiquidity: Balance = 1_000; + pub const DiscountedFee: (u32, u32) = (7, 10_000); +} + +pub struct AllowPools; + +impl hydradx_traits::CanCreatePool for AllowPools { + fn can_create(_asset_a: AssetId, _asset_b: AssetId) -> bool { + true + } +} + +pub struct AssetPairAccountIdTest; +impl hydradx_traits::AssetPairAccountIdFor for AssetPairAccountIdTest { + fn from_assets(asset_a: AssetId, asset_b: AssetId, _: &str) -> u64 { + let mut a = asset_a as u128; + let mut b = asset_b as u128; + if a > b { + std::mem::swap(&mut a, &mut b) + } + (a * 1000 + b) as u64 + } +} + +parameter_types! { + #[derive(PartialEq, Debug)] + pub RegistryStringLimit: u32 = 100; + #[derive(PartialEq, Debug)] + pub MinRegistryStringLimit: u32 = 2; + pub const SequentialIdOffset: u32 = 1_000_000; +} + +type AssetLocation = u8; + +impl pallet_asset_registry::Config for Test { + type RuntimeEvent = RuntimeEvent; + type RegistryOrigin = EnsureRoot; + type Currency = Tokens; + type UpdateOrigin = EnsureSigned; + type AssetId = AssetId; + type AssetNativeLocation = AssetLocation; + type StringLimit = RegistryStringLimit; + type MinStringLimit = MinRegistryStringLimit; + type SequentialIdStartAt = SequentialIdOffset; + type RegExternalWeightMultiplier = frame_support::traits::ConstU64<1>; + type RegisterAssetHook = (); + type WeightInfo = (); +} + +pub struct DummyDuster; + +impl hydradx_traits::pools::DustRemovalAccountWhitelist for DummyDuster { + type Error = DispatchError; + + fn add_account(_account: &AccountId) -> Result<(), Self::Error> { + unimplemented!() + } + + fn remove_account(_account: &AccountId) -> Result<(), Self::Error> { + unimplemented!() + } +} + +impl pallet_omnipool::Config for Test { + type RuntimeEvent = RuntimeEvent; + type AssetId = AssetId; + type PositionItemId = u32; + type Currency = Currencies; + type HubAssetId = LRNAAssetId; + type WeightInfo = (); + type HdxAssetId = HDXAssetId; + type NFTCollectionId = PositionCollectionId; + type NFTHandler = DummyNFT; + type AssetRegistry = AssetRegistry; + type MinimumTradingLimit = MinTradeAmount; + type MinimumPoolLiquidity = MinAddedLiquidity; + type TechnicalOrigin = EnsureRoot; + type MaxInRatio = MaxInRatio; + type MaxOutRatio = MaxOutRatio; + type CollectionId = u32; + type AuthorityOrigin = EnsureRoot; + type OmnipoolHooks = (); + type PriceBarrier = (); + type MinWithdrawalFee = (); + type ExternalPriceOracle = WithdrawFeePriceOracle; + type Fee = FeeProvider; +} + +pub struct DummyNFT; + +impl Inspect for DummyNFT { + type ItemId = u32; + type CollectionId = u32; + + fn owner(_class: &Self::CollectionId, _instance: &Self::ItemId) -> Option { + todo!() + } +} + +impl Create for DummyNFT { + fn create_collection(_class: &Self::CollectionId, _who: &AccountId, _admin: &AccountId) -> DispatchResult { + Ok(()) + } +} + +impl Mutate for DummyNFT { + fn mint_into(_class: &Self::CollectionId, _instance: &Self::ItemId, _who: &AccountId) -> DispatchResult { + Ok(()) + } + + fn burn( + _class: &Self::CollectionId, + _instance: &Self::ItemId, + _maybe_check_owner: Option<&AccountId>, + ) -> DispatchResult { + Ok(()) + } +} + +pub struct WithdrawFeePriceOracle; + +impl ExternalPriceProvider for WithdrawFeePriceOracle { + type Error = DispatchError; + + fn get_price(_asset_a: AssetId, _asset_b: AssetId) -> Result { + todo!() + } + + fn get_price_weight() -> Weight { + todo!() + } +} + +pub struct FeeProvider; + +impl GetByKey for FeeProvider { + fn get(_: &AssetId) -> (Permill, Permill) { + (Permill::from_percent(0), Permill::from_percent(0)) + } +} + +pub struct EvmNonceProviderMock; +impl pallet_evm_accounts::EvmNonceProvider for EvmNonceProviderMock { + fn get_nonce(_evm_address: H160) -> U256 { + U256::zero() + } +} + +impl pallet_evm_accounts::Config for Test { + type RuntimeEvent = RuntimeEvent; + type FeeMultiplier = sp_core::ConstU32<10>; + type EvmNonceProvider = EvmNonceProviderMock; + type ControllerOrigin = EnsureRoot; + type WeightInfo = (); +} + +pub(crate) type Extrinsic = sp_runtime::testing::TestXt; +impl frame_system::offchain::SendTransactionTypes for Test +where + RuntimeCall: From, +{ + type OverarchingCall = RuntimeCall; + type Extrinsic = Extrinsic; +} + +pub struct ExtBuilder { + endowed_accounts: Vec<(AccountId, AssetId, Balance)>, + init_pool: Option<(FixedU128, FixedU128)>, + omnipool_liquidity: Vec<(AccountId, AssetId, Balance)>, //who, asset, amount/ +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { + endowed_accounts: vec![ + (ALICE, HDX, ALICE_HDX_INITIAL_BALANCE), + (MONEY_MARKET, HDX, 1_000_000_000_000 * ONE), + (ALICE, LRNA, 1_000_000_000_000 * ONE), + (ALICE, DAI, 1_000_000_000_000_000_000 * ONE), + (ALICE, DOT, ALICE_DOT_INITIAL_BALANCE), + (ALICE, KSM, 1_000_000_000_000 * ONE), + (ALICE, BTC, 1_000_000_000_000 * ONE), + (BOB, HDX, 1_000_000_000 * ONE), + (BOB, DAI, 1_000_000_000 * ONE), + (Omnipool::protocol_account(), HDX, 1_000_000 * ONE), + (Omnipool::protocol_account(), LRNA, 1_000_000 * ONE), + (Omnipool::protocol_account(), DAI, 1_000_000 * ONE), + (Omnipool::protocol_account(), DOT, 1_000_000 * ONE), + (Omnipool::protocol_account(), KSM, 1_000_000 * ONE), + (Omnipool::protocol_account(), BTC, 1_000_000 * ONE), + ], + init_pool: Some((FixedU128::from_float(0.5), FixedU128::from(1))), + omnipool_liquidity: vec![(ALICE, KSM, 5_000 * ONE)], + } + } +} + +impl ExtBuilder { + pub fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + let registered_assets = vec![ + ( + Some(LRNA), + Some::>(b"LRNA".to_vec().try_into().unwrap()), + 10_000, + Some::>(b"LRNA".to_vec().try_into().unwrap()), + Some(12), + None::, + true, + ), + ( + Some(DAI), + Some::>(b"DAI".to_vec().try_into().unwrap()), + 10_000, + Some::>(b"DAI".to_vec().try_into().unwrap()), + Some(12), + None::, + true, + ), + ( + Some(DOT), + Some::>(b"DOT".to_vec().try_into().unwrap()), + 10_000, + Some::>(b"DOT".to_vec().try_into().unwrap()), + Some(12), + None::, + true, + ), + ( + Some(KSM), + Some::>(b"KSM".to_vec().try_into().unwrap()), + 10_000, + Some::>(b"KSM".to_vec().try_into().unwrap()), + Some(12), + None::, + true, + ), + ( + Some(BTC), + Some::>(b"BTC".to_vec().try_into().unwrap()), + 10_000, + Some::>(b"BTC".to_vec().try_into().unwrap()), + Some(12), + None::, + false, + ), + ]; + + let mut initial_native_accounts: Vec<(AccountId, Balance)> = vec![]; + let additional_accounts: Vec<(AccountId, Balance)> = self + .endowed_accounts + .iter() + .filter(|a| a.1 == HDX) + .flat_map(|(x, _, amount)| vec![((*x).clone(), *amount)]) + .collect::<_>(); + + initial_native_accounts.extend(additional_accounts); + + pallet_asset_registry::GenesisConfig:: { + registered_assets, + ..Default::default() + } + .assimilate_storage(&mut t) + .unwrap(); + + pallet_balances::GenesisConfig:: { + balances: initial_native_accounts, + } + .assimilate_storage(&mut t) + .unwrap(); + + orml_tokens::GenesisConfig:: { + balances: self.endowed_accounts, + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext: sp_io::TestExternalities = t.into(); + + ext.execute_with(|| { + System::set_block_number(1); + }); + + if let Some((stable_price, native_price)) = self.init_pool { + ext.execute_with(|| { + assert_ok!(Omnipool::add_token( + RuntimeOrigin::root(), + HDXAssetId::get(), + native_price, + Permill::from_percent(100), + Omnipool::protocol_account(), + )); + assert_ok!(Omnipool::add_token( + RuntimeOrigin::root(), + DAI, + stable_price, + Permill::from_percent(100), + Omnipool::protocol_account(), + )); + assert_ok!(Omnipool::add_token( + RuntimeOrigin::root(), + DOT, + stable_price, + Permill::from_percent(100), + Omnipool::protocol_account(), + )); + assert_ok!(Omnipool::add_token( + RuntimeOrigin::root(), + KSM, + stable_price, + Permill::from_percent(100), + Omnipool::protocol_account(), + )); + assert_ok!(Omnipool::add_token( + RuntimeOrigin::root(), + BTC, + stable_price, + Permill::from_percent(100), + Omnipool::protocol_account(), + )); + + for p in self.omnipool_liquidity { + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(p.0), p.1, p.2)); + } + }); + } + + ext + } +} diff --git a/pallets/liquidation/src/tests/mod.rs b/pallets/liquidation/src/tests/mod.rs new file mode 100644 index 000000000..ae773b372 --- /dev/null +++ b/pallets/liquidation/src/tests/mod.rs @@ -0,0 +1,2 @@ +mod mock; +mod liquidation; \ No newline at end of file diff --git a/pallets/liquidation/src/weights.rs b/pallets/liquidation/src/weights.rs new file mode 100644 index 000000000..d5154d187 --- /dev/null +++ b/pallets/liquidation/src/weights.rs @@ -0,0 +1,71 @@ +// This file is part of HydraDX. + +// Copyright (C) 2020-2023 Intergalactic, Limited (GIB). +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +//! Autogenerated weights for `pallet_otc_settlements` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 +//! DATE: 2024-09-11, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `bench-bot`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` + +// Executed Command: +// target/release/hydradx +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --wasm-execution=compiled +// --pallet=pallet-otc-settlements +// --extrinsic=* +// --template=scripts/pallet-weight-template.hbs +// --output=./weights/pallet_otc_settlements.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{ + traits::Get, + weights::{constants::RocksDbWeight, Weight}, +}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_otc. +pub trait WeightInfo { + fn liquidate() -> Weight; +} + +/// Weights for pallet_otc using the hydraDX node and recommended hardware. +impl WeightInfo for () { + /// Storage: `OTC::Orders` (r:1 w:0) + /// Proof: `OTC::Orders` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Tokens::Accounts` (r:1 w:0) + /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + fn liquidate() -> Weight { + // Proof Size summary in bytes: + // Measured: `747` + // Estimated: `6196` + // Minimum execution time: 101_333_000 picoseconds. + Weight::from_parts(102_441_000, 6196) + } +} diff --git a/runtime/hydradx/Cargo.toml b/runtime/hydradx/Cargo.toml index 2763a8943..aad7513de 100644 --- a/runtime/hydradx/Cargo.toml +++ b/runtime/hydradx/Cargo.toml @@ -24,6 +24,8 @@ evm = { workspace = true, features = ["with-codec"] } # local dependencies primitives = { workspace = true } +hydradx-adapters = { workspace = true } +hydradx-traits = { workspace = true } pallet-claims = { workspace = true } pallet-genesis-history = { workspace = true } pallet-omnipool = { workspace = true } @@ -41,6 +43,21 @@ pallet-referrals = { workspace = true } pallet-evm-accounts = { workspace = true } pallet-evm-accounts-rpc-runtime-api = { workspace = true } pallet-xyk-liquidity-mining = { workspace = true } +pallet-relaychain-info = { workspace = true } +pallet-transaction-multi-payment = { workspace = true, features = ["evm"] } +pallet-asset-registry = { workspace = true } +pallet-collator-rewards = { workspace = true } +pallet-currencies = { workspace = true } +pallet-currencies-rpc-runtime-api = { workspace = true } +pallet-ema-oracle = { workspace = true } +pallet-transaction-pause = { workspace = true } +pallet-duster = { workspace = true } +warehouse-liquidity-mining = { workspace = true } +pallet-otc = { workspace = true } +pallet-otc-settlements = { workspace = true } +pallet-route-executor = { workspace = true } +pallet-staking = { workspace = true } +pallet-liquidation = { workspace = true } # pallets pallet-bags-list = { workspace = true } @@ -65,24 +82,6 @@ pallet-uniques = { workspace = true } pallet-message-queue = { workspace = true } pallet-state-trie-migration = { workspace = true } -# Warehouse dependencies -hydradx-adapters = { workspace = true } -hydradx-traits = { workspace = true } -pallet-relaychain-info = { workspace = true } -pallet-transaction-multi-payment = { workspace = true, features = ["evm"] } -pallet-asset-registry = { workspace = true } -pallet-collator-rewards = { workspace = true } -pallet-currencies = { workspace = true } -pallet-currencies-rpc-runtime-api = { workspace = true } -pallet-ema-oracle = { workspace = true } -pallet-transaction-pause = { workspace = true } -pallet-duster = { workspace = true } -warehouse-liquidity-mining = { workspace = true } -pallet-otc = { workspace = true } -pallet-otc-settlements = { workspace = true } -pallet-route-executor = { workspace = true } -pallet-staking = { workspace = true } - # ORML dependencies orml-tokens = { workspace = true } orml-traits = { workspace = true } @@ -218,6 +217,7 @@ runtime-benchmarks = [ "pallet-evm-accounts/runtime-benchmarks", "pallet-message-queue/runtime-benchmarks", "pallet-state-trie-migration/runtime-benchmarks", + "pallet-liquidation/runtime-benchmarks", ] std = [ "codec/std", @@ -329,6 +329,7 @@ std = [ "parachains-common/std", "polkadot-runtime-common/std", "pallet-state-trie-migration/std", + "pallet-liquidation/std", ] try-runtime = [ "frame-try-runtime", @@ -400,6 +401,7 @@ try-runtime = [ "pallet-xyk-liquidity-mining/try-runtime", "pallet-message-queue/try-runtime", "pallet-state-trie-migration/try-runtime", + "pallet-liquidation/try-runtime", ] metadata-hash = [ diff --git a/runtime/hydradx/src/assets.rs b/runtime/hydradx/src/assets.rs index 28ab1275b..bae765577 100644 --- a/runtime/hydradx/src/assets.rs +++ b/runtime/hydradx/src/assets.rs @@ -1581,6 +1581,27 @@ impl pallet_referrals::Config for Runtime { type BenchmarkHelper = ReferralsBenchmarkHelper; } + +parameter_types! { + pub const MoneyMarketContract: evm::EvmAddress = evm::EvmAddress::zero(); // TODO: +} + +impl pallet_liquidation::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = FungibleCurrencies; + type Evm = evm::Executor; + #[cfg(not(feature = "runtime-benchmarks"))] + type Router = Router; + #[cfg(feature = "runtime-benchmarks")] + type Router = pallet_route_executor::DummyRouter; + type MoneyMarketContract = MoneyMarketContract; + type EvmAccounts = EVMAccounts; + type Erc20Mapping = evm::precompiles::erc20_mapping::HydraErc20Mapping; + type ProfitReceiver = TreasuryAccount; + type RouterWeightInfo = RouterWeightInfo; + type WeightInfo = weights::pallet_liquidation::HydraWeight; +} + pub struct ConvertViaOmnipool(PhantomData); impl Convert for ConvertViaOmnipool where diff --git a/runtime/hydradx/src/evm/liquidation.rs b/runtime/hydradx/src/evm/liquidation.rs new file mode 100644 index 000000000..ddf1b416f --- /dev/null +++ b/runtime/hydradx/src/evm/liquidation.rs @@ -0,0 +1,22 @@ +use hydradx_runtime::evm::{ + precompiles::handle::EvmDataWriter +}; +use num_enum::{IntoPrimitive, TryFromPrimitive}; +use sp_core::{H256, U256}; + +#[module_evm_utility_macro::generate_function_selector] +#[derive(RuntimeDebug, Eq, PartialEq, TryFromPrimitive, IntoPrimitive)] +#[repr(u32)] +pub enum Function { + LiquidationCall = "liquidationCall(address,address,address,uint256,bool)", +} + +fn liquidation_call_context(collateral_asset: H256, debt_asset: H256, user: H256, debt_to_cover: U256, receive_atoken: bool) { + let call_context = EvmDataWriter::new_with_selector(Function::LiquidationCall) + .write(collateral_asset) + .write(debt_asset) + .write(user) + .write(debt_to_cover) + .write(receive_atoken) + .build(); +} \ No newline at end of file diff --git a/runtime/hydradx/src/evm/precompiles/erc20_mapping.rs b/runtime/hydradx/src/evm/precompiles/erc20_mapping.rs index a8be96d75..20f2ef3c6 100644 --- a/runtime/hydradx/src/evm/precompiles/erc20_mapping.rs +++ b/runtime/hydradx/src/evm/precompiles/erc20_mapping.rs @@ -22,22 +22,15 @@ use crate::evm::EvmAddress; use crate::Runtime; use hex_literal::hex; -use hydradx_traits::RegisterAssetHook; +use hydradx_traits::{evm::Erc20Mapping, RegisterAssetHook}; use primitive_types::H160; use primitives::AssetId; -/// A mapping between AssetId and Erc20 EVM address. -pub trait Erc20Mapping { - fn encode_evm_address(asset_id: AssetId) -> Option; - - fn decode_evm_address(evm_address: EvmAddress) -> Option; -} - pub struct HydraErc20Mapping; /// Erc20Mapping logic for HydraDX /// The asset id (with type u32) is encoded in the last 4 bytes of EVM address -impl Erc20Mapping for HydraErc20Mapping { +impl Erc20Mapping for HydraErc20Mapping { fn encode_evm_address(asset_id: AssetId) -> Option { let asset_id_bytes: [u8; 4] = asset_id.to_le_bytes(); diff --git a/runtime/hydradx/src/evm/precompiles/multicurrency.rs b/runtime/hydradx/src/evm/precompiles/multicurrency.rs index 8a5e21b9c..3fb9c3e3f 100644 --- a/runtime/hydradx/src/evm/precompiles/multicurrency.rs +++ b/runtime/hydradx/src/evm/precompiles/multicurrency.rs @@ -24,7 +24,7 @@ use crate::evm::precompiles::revert; use crate::{ evm::{ precompiles::{ - erc20_mapping::{Erc20Mapping, HydraErc20Mapping}, + erc20_mapping::HydraErc20Mapping, handle::{EvmDataWriter, FunctionModifier, PrecompileHandleExt}, substrate::RuntimeHelper, succeed, Address, Output, @@ -35,7 +35,7 @@ use crate::{ }; use codec::EncodeLike; use frame_support::traits::{IsType, OriginTrait}; -use hydradx_traits::evm::InspectEvmAccounts; +use hydradx_traits::evm::{Erc20Mapping, InspectEvmAccounts}; use hydradx_traits::registry::Inspect as InspectRegistry; use orml_traits::{MultiCurrency as MultiCurrencyT, MultiCurrency}; use pallet_evm::{AddressMapping, ExitRevert, Precompile, PrecompileFailure, PrecompileHandle, PrecompileResult}; diff --git a/runtime/hydradx/src/evm/precompiles/tests/erc20_mapping.rs b/runtime/hydradx/src/evm/precompiles/tests/erc20_mapping.rs index 56167a8d6..ebd1d0351 100644 --- a/runtime/hydradx/src/evm/precompiles/tests/erc20_mapping.rs +++ b/runtime/hydradx/src/evm/precompiles/tests/erc20_mapping.rs @@ -1,6 +1,7 @@ -use crate::evm::precompiles::erc20_mapping::{Erc20Mapping, HydraErc20Mapping}; +use crate::evm::precompiles::erc20_mapping::HydraErc20Mapping; use hex_literal::hex; use primitive_types::H160; +use hydradx_traits::evm::Erc20Mapping; macro_rules! encode { ($asset_id:expr) => {{ diff --git a/runtime/hydradx/src/lib.rs b/runtime/hydradx/src/lib.rs index c43d03c37..590fa2973 100644 --- a/runtime/hydradx/src/lib.rs +++ b/runtime/hydradx/src/lib.rs @@ -184,6 +184,7 @@ construct_runtime!( LBP: pallet_lbp = 73, XYK: pallet_xyk = 74, Referrals: pallet_referrals = 75, + Liquidation: pallet_liquidation = 76, // ORML related modules Tokens: orml_tokens = 77, @@ -304,6 +305,7 @@ mod benches { [pallet_evm_accounts, EVMAccounts] [pallet_otc, OTC] [pallet_otc_settlements, OtcSettlements] + [pallet_liquidation, Liquidation] [pallet_state_trie_migration, StateTrieMigration] [frame_system, SystemBench::] [pallet_balances, Balances] diff --git a/runtime/hydradx/src/weights/mod.rs b/runtime/hydradx/src/weights/mod.rs index 46314002c..0be8156a7 100644 --- a/runtime/hydradx/src/weights/mod.rs +++ b/runtime/hydradx/src/weights/mod.rs @@ -44,3 +44,4 @@ pub mod pallet_utility; pub mod pallet_xcm; pub mod pallet_xyk; pub mod pallet_xyk_liquidity_mining; +pub mod pallet_liquidation; \ No newline at end of file diff --git a/runtime/hydradx/src/weights/pallet_liquidation.rs b/runtime/hydradx/src/weights/pallet_liquidation.rs new file mode 100644 index 000000000..41d2d44c4 --- /dev/null +++ b/runtime/hydradx/src/weights/pallet_liquidation.rs @@ -0,0 +1,57 @@ +// This file is part of HydraDX. + +// Copyright (C) 2020-2023 Intergalactic, Limited (GIB). +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +//! Autogenerated weights for `pallet_liquidation` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 +//! DATE: 2024-09-10, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `bench-bot`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` + +// Executed Command: +// target/release/hydradx +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --wasm-execution=compiled +// --pallet=pallet-liquidation +// --extrinsic=* +// --template=scripts/pallet-weight-template.hbs +// --output=./weights/pallet_liquidation.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weights for `pallet_liquidation`. +pub struct WeightInfo(PhantomData); + +/// Weights for `pallet_liquidation` using the HydraDX node and recommended hardware. +pub struct HydraWeight(PhantomData); +impl pallet_liquidation::WeightInfo for HydraWeight { + fn liquidate() -> Weight { + Weight::zero() + } +} \ No newline at end of file diff --git a/traits/src/evm.rs b/traits/src/evm.rs index 9f68b1bdd..5b269fcbc 100644 --- a/traits/src/evm.rs +++ b/traits/src/evm.rs @@ -82,3 +82,17 @@ pub trait ERC20 { fn transfer(context: CallContext, to: EvmAddress, value: Self::Balance) -> DispatchResult; fn transfer_from(context: CallContext, from: EvmAddress, to: EvmAddress, value: Self::Balance) -> DispatchResult; } + +/// A mapping between AssetId and Erc20 EVM address. +pub trait Erc20Mapping { + fn encode_evm_address(asset_id: AssetId) -> Option; + + fn decode_evm_address(evm_address: EvmAddress) -> Option; +} + +/// Money market liquidation interface adapter +pub trait Liquidation { + type Balance; + + fn liquidate(context: CallContext, to: EvmAddress, value: Self::Balance) -> DispatchResult; +} \ No newline at end of file