diff --git a/Cargo.lock b/Cargo.lock index 64799e7..7e2d846 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8734,7 +8734,7 @@ dependencies = [ "frame-system", "funty 3.0.0-rc2", "hex", - "hex-literal 0.3.4", + "hex-literal 0.4.1", "log", "pallet-assets", "pallet-balances", diff --git a/bridge/Cargo.toml b/bridge/Cargo.toml index 5d76f41..cf0417d 100644 --- a/bridge/Cargo.toml +++ b/bridge/Cargo.toml @@ -15,7 +15,7 @@ funty = { version = "3.0.0-rc1", default-features = false } hex = {version = "0.4.3", default-features = false, features = ["alloc"] } fixed = {version = "1.23.0", default-features = false } bounded-collections = { version = "0.1.4", default-features = false } -hex-literal = { version = "0.3", default-features = false } +hex-literal = { version = "0.4.1", default-features = false } # Substrate sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } @@ -41,7 +41,7 @@ sygma-fee-handler-router = { path = "../fee-handler-router", default-features = [dev-dependencies] assert_matches = "1.4.0" -hex-literal = "0.3" +hex-literal = "0.4.1" # Substrate sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" } diff --git a/bridge/src/benchmarking.rs b/bridge/src/benchmarking.rs index 6768a7f..52bafd7 100644 --- a/bridge/src/benchmarking.rs +++ b/bridge/src/benchmarking.rs @@ -166,6 +166,32 @@ mod benchmarks { assert_eq!(Balances::::free_balance(treasury_account), fee.into()); } + #[benchmark] + fn withdraw_liquidity() { + let native_location: MultiLocation = MultiLocation::here(); + let bridge_account: AccountId32 = AccountId32::new([101u8; 32]); + let amount = 200_000_000_000_000u128; // 200 with 12 decimals + let to_account: AccountId32 = AccountId32::new([100u8; 32]); + let test_mpc_addr: MpcAddress = MpcAddress([1u8; 20]); + + SygmaBridge::::set_mpc_address(SystemOrigin::Root.into(), test_mpc_addr).unwrap(); + + let _ = as Currency<_>>::make_free_balance_be( + &bridge_account.clone().into(), + (amount).into(), + ); + assert_eq!(Balances::::free_balance(bridge_account.clone()), (amount).into()); + + #[extrinsic_call] + withdraw_liquidity( + SystemOrigin::Root, + Box::new((Concrete(native_location), Fungible(amount)).into()), + to_account.clone(), + ); + + assert_eq!(Balances::::free_balance(to_account), amount.into()); + } + #[benchmark] fn retry() { let dest_domain_id: DomainID = 1; diff --git a/bridge/src/lib.rs b/bridge/src/lib.rs index 73b9017..33dcfdb 100644 --- a/bridge/src/lib.rs +++ b/bridge/src/lib.rs @@ -40,7 +40,7 @@ pub mod pallet { use sp_io::{crypto::secp256k1_ecdsa_recover, hashing::keccak_256}; use sp_runtime::{ traits::{AccountIdConversion, Clear}, - RuntimeDebug, + AccountId32, RuntimeDebug, }; use sp_std::{boxed::Box, convert::From, vec, vec::Vec}; use xcm::latest::{prelude::*, MultiLocation}; @@ -73,6 +73,7 @@ pub mod pallet { fn deposit() -> Weight; fn retry() -> Weight; fn execute_proposal(n: u32) -> Weight; + fn withdraw_liquidity() -> Weight; } #[pallet::pallet] @@ -170,6 +171,8 @@ pub mod pallet { RegisterDestDomain { sender: T::AccountId, domain_id: DomainID, chain_id: ChainID }, /// When unregistering a dest domainID with its corresponding chainID UnregisterDestDomain { sender: T::AccountId, domain_id: DomainID, chain_id: ChainID }, + /// When withdraw liquidity happen + WithdrawLiquidity { sender: T::AccountId, asset: MultiAsset, to: AccountId32 }, } #[pallet::error] @@ -600,6 +603,57 @@ pub mod pallet { Ok(()) } + + /// Withdraw liquidity from TransferReserveAccount to the provided account + #[pallet::call_index(8)] + #[pallet::weight(::WeightInfo::withdraw_liquidity())] + pub fn withdraw_liquidity( + origin: OriginFor, + asset: Box, + to: AccountId32, + ) -> DispatchResult { + ensure!( + >::has_access( + ::PalletIndex::get(), + b"withdraw_liquidity".to_vec(), + origin.clone() + ), + Error::::AccessDenied + ); + // Check MPC address + ensure!(!MpcAddr::::get().is_clear(), Error::::MissingMpcAddress); + + // Withdraw asset from TransferReserveAccount + T::AssetTransactor::withdraw_asset( + &asset, + &Junction::AccountId32 { + network: None, + id: T::TransferReserveAccount::get().into(), + } + .into(), + None, + ) + .map_err(|_| Error::::TransactFailed)?; + + // Deposit asset to provided account + T::AssetTransactor::deposit_asset( + &asset, + &Junction::AccountId32 { network: None, id: to.clone().into() }.into(), + // Put empty message hash here because we are not sending XCM message + &XcmContext::with_message_hash([0; 32]), + ) + .map_err(|_| Error::::TransactFailed)?; + + let sender = match ensure_signed(origin) { + Ok(sender) => sender, + _ => [0u8; 32].into(), + }; + + // Emit WithdrawLiquidity event + Self::deposit_event(Event::WithdrawLiquidity { sender, asset: *asset, to }); + + Ok(()) + } } impl Pallet @@ -860,6 +914,7 @@ pub mod pallet { }; use primitive_types::U256; use sp_core::{ecdsa, Pair}; + use sp_runtime::AccountId32 as SPAccountId32; use sp_std::{boxed::Box, vec}; use sygma_traits::{MpcAddress, TransferType}; use xcm::latest::prelude::*; @@ -2534,5 +2589,109 @@ pub mod pallet { assert_eq!(Balances::free_balance(&BOB), ENDOWED_BALANCE + 200000000); }) } + + #[test] + fn withdraw_liquidity_test() { + new_test_ext().execute_with(|| { + let receiver: SPAccountId32 = SPAccountId32::new([200u8; 32]); + let amount = 200_000_000_000_000u128; // 200 with 12 decimals + + // set mpc address + let test_mpc_addr: MpcAddress = MpcAddress([1u8; 20]); + assert_ok!(SygmaBridge::set_mpc_address(Origin::root(), test_mpc_addr)); + + // make sure receiver balance before withdraw + assert_eq!(Balances::free_balance(receiver.clone()), 0); + + // set 200 native token to token reserved account + assert_ok!(Balances::force_set_balance( + Origin::root(), + BridgeAccount::get(), + amount + )); + assert_eq!(Balances::free_balance(BridgeAccount::get()), amount); + + // try to 100 native token withdraw liquidity to receiver account, failed due to + // permission + assert_noop!( + SygmaBridge::withdraw_liquidity( + Origin::signed(ALICE), + Box::new( + (Concrete(NativeLocation::get()), Fungible(100_000_000_000_000u128)) + .into() + ), + receiver.clone() + ), + bridge::Error::::AccessDenied + ); + + // Grant ALICE the access admin extrinsic + assert_ok!(AccessSegregator::grant_access( + Origin::root(), + BridgePalletIndex::get(), + b"withdraw_liquidity".to_vec(), + ALICE + )); + + assert_ok!(SygmaBridge::withdraw_liquidity( + Origin::signed(ALICE), + Box::new( + (Concrete(NativeLocation::get()), Fungible(100_000_000_000_000u128)).into() + ), + receiver.clone() + )); + + // check receiver balance after withdraw + assert_eq!(Balances::free_balance(receiver.clone()), 100_000_000_000_000u128); + + // should emit WithdrawLiquidity event + assert_events(vec![RuntimeEvent::SygmaBridge( + SygmaBridgeEvent::WithdrawLiquidity { + sender: ALICE, + asset: (Concrete(NativeLocation::get()), Fungible(100_000_000_000_000u128)) + .into(), + to: receiver.clone(), + }, + )]); + + // Register foreign asset (USDC) with asset id 0 + assert_ok!( as FungibleCerate< + ::AccountId, + >>::create(UsdcAssetId::get(), ASSET_OWNER, true, 1,)); + + // Mint some USDC to token reserved account for test + assert_ok!(Assets::mint( + Origin::signed(ASSET_OWNER), + codec::Compact(0), + BridgeAccount::get(), + amount, + )); + assert_eq!(Assets::balance(UsdcAssetId::get(), &BridgeAccount::get()), amount); + + assert_ok!(SygmaBridge::withdraw_liquidity( + Origin::signed(ALICE), + Box::new( + (Concrete(UsdcLocation::get()), Fungible(100_000_000_000_000u128)).into() + ), + receiver.clone() + )); + + // check receiver balance after withdraw + assert_eq!( + Assets::balance(UsdcAssetId::get(), receiver.clone()), + 100_000_000_000_000u128 + ); + + // should emit WithdrawLiquidity event + assert_events(vec![RuntimeEvent::SygmaBridge( + SygmaBridgeEvent::WithdrawLiquidity { + sender: ALICE, + asset: (Concrete(UsdcLocation::get()), Fungible(100_000_000_000_000u128)) + .into(), + to: receiver, + }, + )]); + }) + } } } diff --git a/bridge/src/mock.rs b/bridge/src/mock.rs index 1aaf48b..d7bf2d6 100644 --- a/bridge/src/mock.rs +++ b/bridge/src/mock.rs @@ -167,6 +167,7 @@ parameter_types! { (BridgePalletIndex::get(), b"register_domain".to_vec()), (BridgePalletIndex::get(), b"unregister_domain".to_vec()), (BridgePalletIndex::get(), b"retry".to_vec()), + (BridgePalletIndex::get(), b"withdraw_liquidity".to_vec()), ].to_vec(); } diff --git a/bridge/src/weights.rs b/bridge/src/weights.rs index 3e07558..22957f2 100644 --- a/bridge/src/weights.rs +++ b/bridge/src/weights.rs @@ -23,7 +23,7 @@ // --repeat // 20 // --output -// bridge_weight.rs +// ./bridge/weights.rs #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] @@ -166,4 +166,19 @@ impl super::WeightInfo for SygmaWeightInfo { .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(2)) } + + /// Storage: SygmaBridge MpcAddr (r:1 w:0) + /// Proof: SygmaBridge MpcAddr (max_values: Some(1), max_size: Some(20), added: 515, mode: MaxEncodedLen) + /// Storage: System Account (r:2 w:2) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + fn withdraw_liquidity() -> Weight { + // Proof Size summary in bytes: + // Measured: `189` + // Estimated: `6196` + // Minimum execution time: 84_000_000 picoseconds. + Weight::from_parts(85_000_000, 0) + .saturating_add(Weight::from_parts(0, 6196)) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(2)) + } } diff --git a/substrate-node/runtime/src/lib.rs b/substrate-node/runtime/src/lib.rs index 9d373ae..05fe1a0 100644 --- a/substrate-node/runtime/src/lib.rs +++ b/substrate-node/runtime/src/lib.rs @@ -352,6 +352,7 @@ parameter_types! { (BridgePalletIndex::get(), b"register_domain".to_vec()), (BridgePalletIndex::get(), b"unregister_domain".to_vec()), (BridgePalletIndex::get(), b"retry".to_vec()), + (BridgePalletIndex::get(), b"withdraw_liquidity".to_vec()), (FeeHandlerRouterPalletIndex::get(), b"set_fee_handler".to_vec()), ].to_vec(); }