From 402da1c0e2b8f5486eb095ce2eaecd405446819a Mon Sep 17 00:00:00 2001 From: Shady Khalifa Date: Fri, 6 Dec 2024 15:57:28 +0200 Subject: [PATCH] feat: using ERC20 and Assets works on Both EVM and Substrate --- pallets/services/src/lib.rs | 9 +- pallets/services/src/mock.rs | 10 +- pallets/services/src/tests.rs | 54 ++++++++ precompiles/services/src/lib.rs | 2 +- precompiles/services/src/mock.rs | 145 ++++++++++++++++++++- precompiles/services/src/tests.rs | 171 ++++++++++++++++++++++++- runtime/mainnet/src/tangle_services.rs | 1 + runtime/testnet/src/tangle_services.rs | 1 + 8 files changed, 379 insertions(+), 14 deletions(-) diff --git a/pallets/services/src/lib.rs b/pallets/services/src/lib.rs index 17cf8f3e..e6fe8cf4 100644 --- a/pallets/services/src/lib.rs +++ b/pallets/services/src/lib.rs @@ -902,12 +902,12 @@ pub mod module { }; } - let service_id = Self::next_instance_id(); + let request_id = NextServiceRequestId::::get(); let (allowed, _weight) = Self::on_request_hook( &blueprint, blueprint_id, &caller, - service_id, + request_id, &preferences, &request_args, &permitted_callers, @@ -918,14 +918,11 @@ pub mod module { native_value, )?; - ensure!(allowed, Error::::InvalidRequestInput); - let permitted_callers = BoundedVec::<_, MaxPermittedCallersOf>::try_from(permitted_callers) .map_err(|_| Error::::MaxPermittedCallersExceeded)?; let assets = BoundedVec::<_, MaxAssetsPerServiceOf>::try_from(assets) .map_err(|_| Error::::MaxAssetsPerServiceExceeded)?; - let request_id = NextServiceRequestId::::get(); let operators = pending_approvals .iter() .cloned() @@ -948,6 +945,8 @@ pub mod module { permitted_callers, operators_with_approval_state, }; + + ensure!(allowed, Error::::InvalidRequestInput); ServiceRequests::::insert(request_id, service_request); NextServiceRequestId::::set(request_id.saturating_add(1)); diff --git a/pallets/services/src/mock.rs b/pallets/services/src/mock.rs index 5f19f914..44d97c26 100644 --- a/pallets/services/src/mock.rs +++ b/pallets/services/src/mock.rs @@ -22,7 +22,7 @@ use frame_election_provider_support::{ onchain, SequentialPhragmen, }; use frame_support::{ - assert_ok, construct_runtime, parameter_types, + construct_runtime, parameter_types, traits::{ConstU128, ConstU32, OneSessionHandler}, }; use frame_support::{derive_impl, traits::AsEnsureOriginWithArg}; @@ -643,12 +643,12 @@ pub fn new_test_ext_raw_authorities(authorities: Vec) -> sp_io::TestE ]) .unwrap(), Default::default(), - 30_000, + 300_000, true, false, ); - assert_ok!(call); + assert_eq!(call.map(|info| info.exit_reason.is_succeed()).ok(), Some(true)); // Mint for i in 1..=authorities.len() { let call = ::EvmRunner::call( @@ -678,12 +678,12 @@ pub fn new_test_ext_raw_authorities(authorities: Vec) -> sp_io::TestE ]) .unwrap(), Default::default(), - 30_000, + 300_000, true, false, ); - assert_ok!(call); + assert_eq!(call.map(|info| info.exit_reason.is_succeed()).ok(), Some(true)); } }); diff --git a/pallets/services/src/tests.rs b/pallets/services/src/tests.rs index 014cf360..d46ad41f 100644 --- a/pallets/services/src/tests.rs +++ b/pallets/services/src/tests.rs @@ -566,6 +566,60 @@ fn request_service_with_payment_asset() { }); } +#[test] +fn request_service_with_payment_token() { + new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { + System::set_block_number(1); + assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); + let alice = mock_pub_key(ALICE); + let blueprint = cggmp21_blueprint(); + + assert_ok!(Services::create_blueprint(RuntimeOrigin::signed(alice.clone()), blueprint)); + let bob = mock_pub_key(BOB); + assert_ok!(Services::register( + RuntimeOrigin::signed(bob.clone()), + 0, + OperatorPreferences { key: zero_key(), price_targets: Default::default() }, + Default::default(), + 0, + )); + + let charlie = mock_pub_key(CHARLIE); + assert_ok!(Services::request( + RuntimeOrigin::signed(charlie.clone()), + 0, + vec![], + vec![bob.clone()], + Default::default(), + vec![TNT, USDC, WETH], + 100, + Asset::Erc20(USDC_ERC20), + 5 * 10u128.pow(6), // 5 USDC + )); + + assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 1); + + // The Pallet address now has 5 USDC + assert_ok!( + Services::query_erc20_balance_of(USDC_ERC20, Services::address()).map(|(b, _)| b), + U256::from(5 * 10u128.pow(6)) + ); + + // Bob approves the request + assert_ok!(Services::approve( + RuntimeOrigin::signed(bob.clone()), + 0, + Percent::from_percent(10) + )); + + // The request is now fully approved + assert_eq!(ServiceRequests::::iter_keys().collect::>().len(), 0); + + // Now the service should be initiated + assert!(Instances::::contains_key(0)); + }); +} + #[test] fn job_calls() { new_test_ext(vec![ALICE, BOB, CHARLIE, DAVE, EVE]).execute_with(|| { diff --git a/precompiles/services/src/lib.rs b/precompiles/services/src/lib.rs index 1e156241..04fdbdd2 100644 --- a/precompiles/services/src/lib.rs +++ b/precompiles/services/src/lib.rs @@ -114,7 +114,7 @@ where /// Request a new service. #[precompile::public( - "requestService(uint256,uint256[],bytes,bytes,bytes,uint256,address,uint256)" + "requestService(uint256,uint256[],bytes,bytes,bytes,uint256,uint256,address,uint256)" )] fn request_service( handle: &mut impl PrecompileHandle, diff --git a/precompiles/services/src/mock.rs b/precompiles/services/src/mock.rs index f591d157..4a0bf273 100644 --- a/precompiles/services/src/mock.rs +++ b/precompiles/services/src/mock.rs @@ -15,6 +15,8 @@ // along with Tangle. If not, see . #![allow(clippy::all)] use super::*; +use core::ops::Mul; +use ethabi::Uint; use frame_election_provider_support::{ bounds::{ElectionBounds, ElectionBoundsBuilder}, onchain, SequentialPhragmen, @@ -29,12 +31,14 @@ use frame_support::{derive_impl, traits::AsEnsureOriginWithArg}; use frame_system::EnsureRoot; use mock_evm::MockedEvmRunner; use pallet_evm::GasWeightMapping; +use pallet_services::traits::EvmRunner; use pallet_services::{EvmAddressMapping, EvmGasWeightMapping}; use pallet_session::historical as pallet_session_historical; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use serde::Deserialize; use serde::Serialize; +use serde_json::json; use sp_core::{self, sr25519, sr25519::Public as sr25519Public, ConstU32, RuntimeDebug, H160}; use sp_keystore::{testing::MemoryKeystore, KeystoreExt, KeystorePtr}; use sp_runtime::{ @@ -358,6 +362,21 @@ impl From for AccountId32 { } } +impl From for TestAccount { + fn from(x: AccountId32) -> Self { + let bytes: [u8; 32] = x.into(); + match bytes { + a if a == [1u8; 32] => TestAccount::Alex, + a if a == [2u8; 32] => TestAccount::Bob, + a if a == [3u8; 32] => TestAccount::Charlie, + a if a == [4u8; 32] => TestAccount::Dave, + a if a == [5u8; 32] => TestAccount::Eve, + a if a == PRECOMPILE_ADDRESS_BYTES => TestAccount::PrecompileAddress, + _ => TestAccount::Empty, + } + } +} + impl From for sp_core::sr25519::Public { fn from(x: TestAccount) -> Self { match x { @@ -577,6 +596,7 @@ pub fn mock_authorities(vec: Vec) -> Vec { pub const MBSM: H160 = H160([0x12; 20]); pub const CGGMP21_BLUEPRINT: H160 = H160([0x21; 20]); +pub const USDC_ERC20: H160 = H160([0x23; 20]); pub const TNT: AssetId = 0; pub const USDC: AssetId = 1; @@ -600,7 +620,7 @@ impl ExtBuilder { pub fn new_test_ext_raw_authorities(authorities: Vec) -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); // We use default for brevity, but you can configure as desired if needed. - let balances: Vec<_> = authorities.iter().map(|i| (i.clone(), 20_000_u128)).collect(); + let balances: Vec<_> = authorities.iter().map(|i| (i.clone(), 20_000_000_u128)).collect(); pallet_balances::GenesisConfig:: { balances } .assimilate_storage(&mut t) .unwrap(); @@ -661,11 +681,58 @@ pub fn new_test_ext_raw_authorities(authorities: Vec) -> sp_io::TestE MBSM, ); + create_contract( + include_str!("../../../pallets/services/src/test-artifacts/MockERC20.hex"), + USDC_ERC20, + ); + + // Add some initial balance to the authorities in the EVM pallet + for a in authorities.iter().cloned() { + evm_accounts.insert( + TestAccount::from(a).into(), + fp_evm::GenesisAccount { + code: vec![], + storage: Default::default(), + nonce: Default::default(), + balance: Uint::from(1_000).mul(Uint::from(10).pow(Uint::from(18))), + }, + ); + } + let evm_config = pallet_evm::GenesisConfig:: { accounts: evm_accounts, ..Default::default() }; evm_config.assimilate_storage(&mut t).unwrap(); + let assets_config = pallet_assets::GenesisConfig:: { + assets: vec![ + (USDC, authorities[0].clone(), true, 100_000), // 1 cent. + (WETH, authorities[1].clone(), true, 100), // 100 wei. + (WBTC, authorities[2].clone(), true, 100), // 100 satoshi. + ], + metadata: vec![ + (USDC, Vec::from(b"USD Coin"), Vec::from(b"USDC"), 6), + (WETH, Vec::from(b"Wrapped Ether"), Vec::from(b"WETH"), 18), + (WBTC, Vec::from(b"Wrapped Bitcoin"), Vec::from(b"WBTC"), 18), + ], + accounts: vec![ + (USDC, authorities[0].clone(), 1_000_000 * 10u128.pow(6)), + (WETH, authorities[0].clone(), 100 * 10u128.pow(18)), + (WBTC, authorities[0].clone(), 50 * 10u128.pow(18)), + // + (USDC, authorities[1].clone(), 1_000_000 * 10u128.pow(6)), + (WETH, authorities[1].clone(), 100 * 10u128.pow(18)), + (WBTC, authorities[1].clone(), 50 * 10u128.pow(18)), + // + (USDC, authorities[2].clone(), 1_000_000 * 10u128.pow(6)), + (WETH, authorities[2].clone(), 100 * 10u128.pow(18)), + (WBTC, authorities[2].clone(), 50 * 10u128.pow(18)), + ], + next_asset_id: Some(4), + }; + + assets_config.assimilate_storage(&mut t).unwrap(); + let mut ext = sp_io::TestExternalities::new(t); ext.register_extension(KeystoreExt(Arc::new(MemoryKeystore::new()) as KeystorePtr)); ext.execute_with(|| System::set_block_number(1)); @@ -673,6 +740,82 @@ pub fn new_test_ext_raw_authorities(authorities: Vec) -> sp_io::TestE System::set_block_number(1); Session::on_initialize(1); >::on_initialize(1); + + let call = ::EvmRunner::call( + Services::address(), + USDC_ERC20, + serde_json::from_value::(json!({ + "name": "initialize", + "inputs": [ + { + "name": "name_", + "type": "string", + "internalType": "string" + }, + { + "name": "symbol_", + "type": "string", + "internalType": "string" + }, + { + "name": "decimals_", + "type": "uint8", + "internalType": "uint8" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + })) + .unwrap() + .encode_input(&[ + ethabi::Token::String("USD Coin".to_string()), + ethabi::Token::String("USDC".to_string()), + ethabi::Token::Uint(6.into()), + ]) + .unwrap(), + Default::default(), + 300_000, + true, + false, + ); + + assert_eq!(call.map(|info| info.exit_reason.is_succeed()).ok(), Some(true)); + // Mint + for a in authorities { + let call = ::EvmRunner::call( + Services::address(), + USDC_ERC20, + serde_json::from_value::(json!({ + "name": "mint", + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + })) + .unwrap() + .encode_input(&[ + ethabi::Token::Address(TestAccount::from(a).into()), + ethabi::Token::Uint(Uint::from(100_000).mul(Uint::from(10).pow(Uint::from(6)))), + ]) + .unwrap(), + Default::default(), + 300_000, + true, + false, + ); + + assert_eq!(call.map(|info| info.exit_reason.is_succeed()).ok(), Some(true)); + } }); ext diff --git a/precompiles/services/src/tests.rs b/precompiles/services/src/tests.rs index 0405c7e2..447eae2f 100644 --- a/precompiles/services/src/tests.rs +++ b/precompiles/services/src/tests.rs @@ -1,3 +1,5 @@ +use core::ops::Mul; + use crate::mock::*; use crate::mock_evm::PCall; use crate::mock_evm::PrecompilesValue; @@ -26,8 +28,6 @@ fn zero_key() -> ecdsa::Public { ecdsa::Public::from([0; 33]) } -const WETH: AssetId = 1; - #[allow(dead_code)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum MachineKind { @@ -219,6 +219,173 @@ fn test_request_service() { }); } +#[test] +fn test_request_service_with_erc20() { + ExtBuilder.build().execute_with(|| { + assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); + // First create the blueprint + let blueprint_data = cggmp21_blueprint(); + + PrecompilesValue::get() + .prepare_test( + TestAccount::Alex, + H160::from_low_u64_be(1), + PCall::create_blueprint { + blueprint_data: UnboundedBytes::from(blueprint_data.encode()), + }, + ) + .execute_returns(()); + + // Now register operator + let preferences_data = OperatorPreferences { + key: zero_key(), + price_targets: price_targets(MachineKind::Large), + } + .encode(); + + PrecompilesValue::get() + .prepare_test( + TestAccount::Bob, + H160::from_low_u64_be(1), + PCall::register_operator { + blueprint_id: U256::from(0), + preferences: UnboundedBytes::from(preferences_data), + registration_args: UnboundedBytes::from(vec![0u8]), + }, + ) + .execute_returns(()); + + assert_ok!( + Services::query_erc20_balance_of(USDC_ERC20, Services::address()) + .map(|(balance, _)| balance), + U256::zero(), + ); + // Finally, request the service + let permitted_callers_data: Vec = vec![TestAccount::Alex.into()]; + let service_providers_data: Vec = vec![TestAccount::Bob.into()]; + let request_args_data = vec![0u8]; + + let payment_amount = U256::from(5).mul(U256::from(10).pow(6.into())); // 5 USDC + + PrecompilesValue::get() + .prepare_test( + TestAccount::Alex, + H160::from_low_u64_be(1), + PCall::request_service { + blueprint_id: U256::from(0), // Use the first blueprint + permitted_callers_data: UnboundedBytes::from(permitted_callers_data.encode()), + service_providers_data: UnboundedBytes::from(service_providers_data.encode()), + request_args_data: UnboundedBytes::from(request_args_data), + assets: [TNT, WETH].into_iter().map(Into::into).collect(), + ttl: U256::from(1000), + payment_asset_id: U256::from(0), + payment_token_address: USDC_ERC20.into(), + amount: payment_amount, + }, + ) + .execute_returns(()); + + // Services pallet address now should have 5 USDC + assert_ok!( + Services::query_erc20_balance_of(USDC_ERC20, Services::address()) + .map(|(balance, _)| balance), + payment_amount + ); + + // Approve the service request by the operator(s) + PrecompilesValue::get() + .prepare_test( + TestAccount::Bob, + H160::from_low_u64_be(1), + PCall::approve { request_id: U256::from(0), restaking_percent: 10 }, + ) + .execute_returns(()); + + // Ensure the service instance is created + assert!(Instances::::contains_key(0)); + }); +} + +#[test] +fn test_request_service_with_asset() { + ExtBuilder.build().execute_with(|| { + assert_ok!(Services::update_master_blueprint_service_manager(RuntimeOrigin::root(), MBSM)); + // First create the blueprint + let blueprint_data = cggmp21_blueprint(); + + PrecompilesValue::get() + .prepare_test( + TestAccount::Alex, + H160::from_low_u64_be(1), + PCall::create_blueprint { + blueprint_data: UnboundedBytes::from(blueprint_data.encode()), + }, + ) + .execute_returns(()); + + // Now register operator + let preferences_data = OperatorPreferences { + key: zero_key(), + price_targets: price_targets(MachineKind::Large), + } + .encode(); + + PrecompilesValue::get() + .prepare_test( + TestAccount::Bob, + H160::from_low_u64_be(1), + PCall::register_operator { + blueprint_id: U256::from(0), + preferences: UnboundedBytes::from(preferences_data), + registration_args: UnboundedBytes::from(vec![0u8]), + }, + ) + .execute_returns(()); + + assert_eq!(Assets::balance(USDC, Services::account_id()), 0); + + // Finally, request the service + let permitted_callers_data: Vec = vec![TestAccount::Alex.into()]; + let service_providers_data: Vec = vec![TestAccount::Bob.into()]; + let request_args_data = vec![0u8]; + + let payment_amount = U256::from(5).mul(U256::from(10).pow(6.into())); // 5 USDC + + PrecompilesValue::get() + .prepare_test( + TestAccount::Alex, + H160::from_low_u64_be(1), + PCall::request_service { + blueprint_id: U256::from(0), // Use the first blueprint + permitted_callers_data: UnboundedBytes::from(permitted_callers_data.encode()), + service_providers_data: UnboundedBytes::from(service_providers_data.encode()), + request_args_data: UnboundedBytes::from(request_args_data), + assets: [TNT, WETH].into_iter().map(Into::into).collect(), + ttl: U256::from(1000), + payment_asset_id: U256::from(USDC), + payment_token_address: Default::default(), + amount: payment_amount, + }, + ) + .execute_returns(()); + + // Services pallet address now should have 5 USDC + assert_eq!(Assets::balance(USDC, Services::account_id()), payment_amount.as_u128()); + + // Approve the service request by the operator(s) + PrecompilesValue::get() + .prepare_test( + TestAccount::Bob, + H160::from_low_u64_be(1), + PCall::approve { request_id: U256::from(0), restaking_percent: 10 }, + ) + .execute_returns(()); + + // Ensure the service instance is created + assert!(Instances::::contains_key(0)); + }); +} + #[test] fn test_unregister_operator() { ExtBuilder.build().execute_with(|| { diff --git a/runtime/mainnet/src/tangle_services.rs b/runtime/mainnet/src/tangle_services.rs index de7b6b27..1cf4c43a 100644 --- a/runtime/mainnet/src/tangle_services.rs +++ b/runtime/mainnet/src/tangle_services.rs @@ -151,6 +151,7 @@ impl pallet_services::Config for Runtime { type RuntimeEvent = RuntimeEvent; type ForceOrigin = EnsureRootOrHalfCouncil; type Currency = Balances; + type Fungibles = Assets; type PalletEVMAddress = ServicesEVMAddress; type EvmRunner = PalletEvmRunner; type EvmGasWeightMapping = PalletEVMGasWeightMapping; diff --git a/runtime/testnet/src/tangle_services.rs b/runtime/testnet/src/tangle_services.rs index c7a44766..0cd11350 100644 --- a/runtime/testnet/src/tangle_services.rs +++ b/runtime/testnet/src/tangle_services.rs @@ -148,6 +148,7 @@ impl pallet_services::Config for Runtime { type RuntimeEvent = RuntimeEvent; type ForceOrigin = EnsureRootOrHalfCouncil; type Currency = Balances; + type Fungibles = Assets; type PalletEVMAddress = ServicesEVMAddress; type EvmRunner = PalletEvmRunner; type EvmGasWeightMapping = PalletEVMGasWeightMapping;