diff --git a/Cargo.lock b/Cargo.lock index aec0fd2d..60696745 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6115,6 +6115,8 @@ dependencies = [ "pallet-state-coprocessor", "pallet-sudo", "pallet-timestamp", + "pallet-token-gateway", + "pallet-token-gateway-inspector", "pallet-token-governor", "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", @@ -11615,6 +11617,7 @@ dependencies = [ "frame-system", "ismp", "pallet-ismp", + "pallet-token-gateway", "pallet-token-governor", "pallet-xcm", "parity-scale-codec", @@ -12350,6 +12353,8 @@ dependencies = [ "pallet-mmr 0.1.1", "pallet-sudo", "pallet-timestamp", + "pallet-token-gateway", + "pallet-token-gateway-inspector", "pallet-token-governor", "pallet-xcm", "parachains-common", @@ -12936,6 +12941,51 @@ dependencies = [ "sp-runtime 39.0.0", ] +[[package]] +name = "pallet-token-gateway" +version = "1.15.0" +dependencies = [ + "alloy-primitives", + "alloy-sol-macro", + "alloy-sol-types", + "anyhow", + "frame-support 37.0.0", + "frame-system", + "ismp", + "log", + "pallet-ismp", + "pallet-token-governor", + "parity-scale-codec", + "primitive-types", + "scale-info", + "sp-core 34.0.0", + "sp-io 38.0.0", + "sp-runtime 39.0.0", +] + +[[package]] +name = "pallet-token-gateway-inspector" +version = "0.1.0" +dependencies = [ + "alloy-primitives", + "alloy-sol-macro", + "alloy-sol-types", + "anyhow", + "frame-support 37.0.0", + "frame-system", + "ismp", + "log", + "pallet-ismp", + "pallet-token-gateway", + "pallet-token-governor", + "parity-scale-codec", + "primitive-types", + "scale-info", + "sp-core 34.0.0", + "sp-io 38.0.0", + "sp-runtime 39.0.0", +] + [[package]] name = "pallet-token-governor" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 27c9c2e1..eab71588 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,8 @@ members = [ "modules/ismp/pallets/call-decompressor", "modules/ismp/pallets/asset-gateway", "modules/ismp/pallets/token-governor", + "modules/ismp/pallets/token-gateway", + "modules/ismp/pallets/token-gateway-inspector", "modules/ismp/pallets/hyperbridge", "modules/ismp/pallets/state-coprocessor", "modules/ismp/testsuite", @@ -304,6 +306,8 @@ pallet-asset-gateway = { path = "modules/ismp/pallets/asset-gateway", default-fe pallet-token-governor = { path = "modules/ismp/pallets/token-governor", default-features = false } pallet-state-coprocessor = { path = "modules/ismp/pallets/state-coprocessor", default-features = false } pallet-mmr = { path = "modules/trees/mmr/pallet", default-features = false } +pallet-token-gateway = { version = "1.15.0", path = "modules/ismp/pallets/token-gateway", default-features = false } +pallet-token-gateway-inspector = { path = "modules/ismp/pallets/token-gateway-inspector", default-features = false } # merkle trees pallet-mmr-runtime-api = { path = "modules/trees/mmr/pallet/runtime-api", default-features = false } diff --git a/modules/ismp/pallets/asset-gateway/Cargo.toml b/modules/ismp/pallets/asset-gateway/Cargo.toml index 49f60f2f..b540a172 100644 --- a/modules/ismp/pallets/asset-gateway/Cargo.toml +++ b/modules/ismp/pallets/asset-gateway/Cargo.toml @@ -35,6 +35,7 @@ pallet-xcm = { workspace = true } staging-xcm = { workspace = true } staging-xcm-builder = { workspace = true } staging-xcm-executor = { workspace = true } +pallet-token-gateway = { workspace = true } [features] default = ["std"] @@ -56,6 +57,7 @@ std = [ "pallet-token-governor/std", "alloy-sol-types/std", "alloy-primitives/std", - "anyhow/std" + "anyhow/std", + "pallet-token-gateway/std" ] try-runtime = [] diff --git a/modules/ismp/pallets/asset-gateway/src/lib.rs b/modules/ismp/pallets/asset-gateway/src/lib.rs index 8f4ea9c8..9734ab8c 100644 --- a/modules/ismp/pallets/asset-gateway/src/lib.rs +++ b/modules/ismp/pallets/asset-gateway/src/lib.rs @@ -20,6 +20,10 @@ extern crate alloc; use alloc::{boxed::Box, string::ToString, vec}; use alloy_sol_types::SolType; use core::marker::PhantomData; +use pallet_token_gateway::{ + impls::{convert_to_balance, convert_to_erc20}, + types::Body, +}; use pallet_token_governor::TokenGatewayParams; use frame_support::{ @@ -273,22 +277,6 @@ where } } -alloy_sol_macro::sol! { - #![sol(all_derives)] - struct Body { - // Amount of the asset to be sent - uint256 amount; - // The asset identifier - bytes32 asset_id; - // Flag to redeem the erc20 asset on the destination - bool redeem; - // Sender address - bytes32 from; - // Recipient address - bytes32 to; - } -} - #[derive(Clone)] pub struct Module(PhantomData); @@ -514,63 +502,3 @@ where } } } - -/// Converts an ERC20 U256 to a DOT u128 -pub fn convert_to_balance(value: U256) -> Result { - let dec_str = (value / U256::from(100_000_000u128)).to_string(); - dec_str.parse().map_err(|e| anyhow::anyhow!("{e:?}")) -} - -/// Converts a DOT u128 to an Erc20 denomination -pub fn convert_to_erc20(value: u128) -> U256 { - U256::from(value) * U256::from(100_000_000u128) -} - -#[cfg(test)] -mod tests { - use sp_core::U256; - use sp_runtime::Permill; - use std::ops::Mul; - - use crate::{convert_to_balance, convert_to_erc20}; - #[test] - fn test_per_mill() { - let per_mill = Permill::from_parts(1_000); - - println!("{}", per_mill.mul(20_000_000u128)); - } - - #[test] - fn balance_conversions() { - let supposedly_small_u256 = U256::from_dec_str("1000000000000000000").unwrap(); - // convert erc20 value to dot value - let converted_balance = convert_to_balance(supposedly_small_u256).unwrap(); - println!("{}", converted_balance); - - let dot = 10_000_000_000u128; - - assert_eq!(converted_balance, dot); - - // Convert 1 dot to erc20 - - let dot = 10_000_000_000u128; - let erc_20_val = convert_to_erc20(dot); - assert_eq!(erc_20_val, U256::from_dec_str("1000000000000000000").unwrap()); - } - - #[test] - fn max_value_check() { - let max = U256::MAX; - - let converted_balance = convert_to_balance(max); - assert!(converted_balance.is_err()) - } - - #[test] - fn min_value_check() { - let min = U256::from(1u128); - - let converted_balance = convert_to_balance(min).unwrap(); - assert_eq!(converted_balance, 0); - } -} diff --git a/modules/ismp/pallets/testsuite/Cargo.toml b/modules/ismp/pallets/testsuite/Cargo.toml index 1ea103cc..c0f0cab5 100644 --- a/modules/ismp/pallets/testsuite/Cargo.toml +++ b/modules/ismp/pallets/testsuite/Cargo.toml @@ -45,10 +45,12 @@ pallet-ismp-relayer = { workspace = true, default-features = true } pallet-fishermen = { workspace = true, default-features = true } pallet-call-decompressor = { workspace = true, default-features = true } pallet-asset-gateway = { workspace = true, default-features = true } +pallet-token-gateway = { workspace = true, default-features = true } sp-state-machine = { workspace = true, default-features = true } mmr-primitives = { workspace = true, default-features = true } pallet-mmr = { workspace = true, default-features = true } pallet-token-governor = { workspace = true, default-features = true } +pallet-token-gateway-inspector = { workspace = true, default-features = true } # Polkadot pallet-xcm = { workspace = true, default-features = true } diff --git a/modules/ismp/pallets/testsuite/src/runtime.rs b/modules/ismp/pallets/testsuite/src/runtime.rs index 068c1f0d..95d112fd 100644 --- a/modules/ismp/pallets/testsuite/src/runtime.rs +++ b/modules/ismp/pallets/testsuite/src/runtime.rs @@ -16,6 +16,8 @@ extern crate alloc; +use std::collections::BTreeSet; + use alloc::collections::BTreeMap; use cumulus_pallet_parachain_system::ParachainSetCode; use frame_support::{ @@ -39,16 +41,21 @@ use ismp::{ }; use ismp_sync_committee::constants::sepolia::Sepolia; use pallet_ismp::{mmr::Leaf, ModuleId}; +use pallet_token_governor::GatewayParams; use sp_core::{ crypto::AccountId32, offchain::{testing::TestOffchainExt, OffchainDbExt, OffchainWorkerExt}, - H256, + H160, H256, }; use sp_runtime::{ traits::{IdentityLookup, Keccak256}, BuildStorage, }; +use staging_xcm::prelude::Location; use substrate_state_machine::SubstrateStateMachine; +use xcm_simulator_example::ALICE; + +pub const INITIAL_BALANCE: u128 = 1_000_000_000_000_000_000; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; @@ -75,6 +82,8 @@ frame_support::construct_runtime!( TokenGovernor: pallet_token_governor, Sudo: pallet_sudo, IsmpSyncCommittee: ismp_sync_committee::pallet, + TokenGateway: pallet_token_gateway, + TokenGatewayInspector: pallet_token_gateway_inspector, } ); @@ -203,6 +212,22 @@ impl pallet_hyperbridge::Config for Test { type IsmpHost = Ismp; } +parameter_types! { + pub const NativeAssetId: Location = Location::here(); +} + +impl pallet_token_gateway::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Dispatcher = Ismp; + type Assets = Assets; + type Currency = Balances; + type NativeAssetId = NativeAssetId; +} + +impl pallet_token_gateway_inspector::Config for Test { + type RuntimeEvent = RuntimeEvent; +} + impl ismp_sync_committee::pallet::Config for Test { type AdminOrigin = EnsureRoot; type IsmpHost = Ismp; @@ -424,7 +449,10 @@ where pub fn new_test_ext() -> sp_io::TestExternalities { let _ = env_logger::builder().is_test(true).try_init(); - let storage = frame_system::GenesisConfig::::default().build_storage().unwrap(); + let mut storage = frame_system::GenesisConfig::::default().build_storage().unwrap(); + pallet_balances::GenesisConfig:: { balances: vec![(ALICE, INITIAL_BALANCE)] } + .assimilate_storage(&mut storage) + .unwrap(); let mut ext = sp_io::TestExternalities::new(storage); register_offchain_ext(&mut ext); @@ -435,6 +463,23 @@ pub fn new_test_ext() -> sp_io::TestExternalities { pallet_token_governor::Params:: { registration_fee: Default::default() }; pallet_token_governor::ProtocolParams::::put(protocol_params); + pallet_token_gateway::SupportedAssets::::insert(Location::here(), H256::zero()); + pallet_token_gateway::LocalAssets::::insert(H256::zero(), Location::here()); + pallet_token_gateway::TokenGatewayAddresses::::insert( + StateMachine::Evm(1), + H160::zero().0.to_vec(), + ); + pallet_token_gateway_inspector::StandaloneChainAssets::::insert( + StateMachine::Kusama(100), + vec![H256::zero()].into_iter().collect::>(), + ); + + let params = GatewayParams { + address: H160::zero(), + host: H160::zero(), + call_dispatcher: H160::random(), + }; + pallet_token_governor::TokenGatewayParams::::insert(StateMachine::Evm(1), params); }); ext } diff --git a/modules/ismp/pallets/testsuite/src/tests/mod.rs b/modules/ismp/pallets/testsuite/src/tests/mod.rs index cf467107..15b9530b 100644 --- a/modules/ismp/pallets/testsuite/src/tests/mod.rs +++ b/modules/ismp/pallets/testsuite/src/tests/mod.rs @@ -8,3 +8,5 @@ mod pallet_ismp_host_executive; mod pallet_ismp_relayer; mod xcm_integration_test; + +mod pallet_token_gateway; diff --git a/modules/ismp/pallets/testsuite/src/tests/pallet_asset_gateway.rs b/modules/ismp/pallets/testsuite/src/tests/pallet_asset_gateway.rs index 53f66bf8..bbccfb10 100644 --- a/modules/ismp/pallets/testsuite/src/tests/pallet_asset_gateway.rs +++ b/modules/ismp/pallets/testsuite/src/tests/pallet_asset_gateway.rs @@ -14,7 +14,8 @@ use ismp::{ module::IsmpModule, router::{PostRequest, Request, Timeout}, }; -use pallet_asset_gateway::{convert_to_erc20, Body, Module}; +use pallet_asset_gateway::Module; +use pallet_token_gateway::{impls::convert_to_erc20, types::Body}; use sp_core::{ByteArray, H160}; use staging_xcm::v4::{Junction, Junctions, Location, NetworkId, WeightLimit}; use xcm_simulator::TestExt; diff --git a/modules/ismp/pallets/testsuite/src/tests/pallet_token_gateway.rs b/modules/ismp/pallets/testsuite/src/tests/pallet_token_gateway.rs new file mode 100644 index 00000000..eaa5d372 --- /dev/null +++ b/modules/ismp/pallets/testsuite/src/tests/pallet_token_gateway.rs @@ -0,0 +1,297 @@ +#![cfg(test)] + +use alloy_sol_types::SolValue; +use ismp::{ + host::StateMachine, + router::{PostRequest, Request, Timeout}, +}; +use pallet_token_gateway::{ + impls::{convert_to_erc20, module_id}, + Body, TeleportParams, +}; +use sp_core::{ByteArray, H160, H256, U256}; +use staging_xcm::prelude::Location; +use xcm_simulator_example::ALICE; + +use crate::runtime::{ + new_test_ext, RuntimeOrigin, Test, TokenGateway, TokenGatewayInspector, INITIAL_BALANCE, +}; +use ismp::module::IsmpModule; + +const SEND_AMOUNT: u128 = 1000_000_000_0000; + +#[test] +fn should_teleport_asset_correctly() { + new_test_ext().execute_with(|| { + let params = TeleportParams { + asset_id: Location::here(), + destination: StateMachine::Evm(1), + recepient: H256::random(), + timeout: 0, + amount: SEND_AMOUNT, + token_gateway: H160::zero().0.to_vec(), + relayer_fee: Default::default(), + }; + + TokenGateway::teleport(RuntimeOrigin::signed(ALICE), params).unwrap(); + + let new_balance = pallet_balances::Pallet::::free_balance(ALICE); + + assert_eq!(new_balance, INITIAL_BALANCE - SEND_AMOUNT); + }) +} + +#[test] +fn should_receive_asset_correctly() { + new_test_ext().execute_with(|| { + let params = TeleportParams { + asset_id: Location::here(), + destination: StateMachine::Evm(1), + recepient: H256::random(), + timeout: 0, + amount: SEND_AMOUNT, + token_gateway: H160::zero().0.to_vec(), + relayer_fee: Default::default(), + }; + + TokenGateway::teleport(RuntimeOrigin::signed(ALICE), params).unwrap(); + + let new_balance = pallet_balances::Pallet::::free_balance(ALICE); + + assert_eq!(new_balance, INITIAL_BALANCE - SEND_AMOUNT); + + let module = TokenGateway::default(); + let post = PostRequest { + source: StateMachine::Evm(1), + dest: StateMachine::Kusama(100), + nonce: 0, + from: H160::zero().0.to_vec(), + to: H160::zero().0.to_vec(), + timeout_timestamp: 1000, + body: { + let body = Body { + amount: { + let mut bytes = [0u8; 32]; + // Module callback will convert to ten decimals + convert_to_erc20(SEND_AMOUNT).to_big_endian(&mut bytes); + alloy_primitives::U256::from_be_bytes(bytes) + }, + asset_id: H256::zero().0.into(), + redeem: false, + from: alloy_primitives::B256::from_slice(ALICE.as_slice()), + to: alloy_primitives::B256::from_slice(ALICE.as_slice()), + }; + + let encoded = vec![vec![0], Body::abi_encode(&body)].concat(); + encoded + }, + }; + + module.on_accept(post).unwrap(); + let new_balance = pallet_balances::Pallet::::free_balance(ALICE); + + assert_eq!(new_balance, INITIAL_BALANCE); + }); +} + +#[test] +fn should_timeout_request_correctly() { + new_test_ext().execute_with(|| { + let params = TeleportParams { + asset_id: Location::here(), + destination: StateMachine::Evm(1), + recepient: H256::random(), + timeout: 0, + amount: SEND_AMOUNT, + token_gateway: H160::zero().0.to_vec(), + relayer_fee: Default::default(), + }; + + TokenGateway::teleport(RuntimeOrigin::signed(ALICE), params).unwrap(); + + let new_balance = pallet_balances::Pallet::::free_balance(ALICE); + + assert_eq!(new_balance, INITIAL_BALANCE - SEND_AMOUNT); + + let module = TokenGateway::default(); + let post = PostRequest { + source: StateMachine::Evm(1), + dest: StateMachine::Kusama(100), + nonce: 0, + from: H160::zero().0.to_vec(), + to: H160::zero().0.to_vec(), + timeout_timestamp: 1000, + body: { + let body = Body { + amount: { + let mut bytes = [0u8; 32]; + // Module callback will convert to ten decimals + convert_to_erc20(SEND_AMOUNT).to_big_endian(&mut bytes); + alloy_primitives::U256::from_be_bytes(bytes) + }, + asset_id: H256::zero().0.into(), + redeem: false, + from: alloy_primitives::B256::from_slice(ALICE.as_slice()), + to: alloy_primitives::B256::from_slice(ALICE.as_slice()), + }; + + let encoded = vec![vec![0], Body::abi_encode(&body)].concat(); + encoded + }, + }; + + module.on_timeout(Timeout::Request(Request::Post(post))).unwrap(); + let new_balance = pallet_balances::Pallet::::free_balance(ALICE); + + assert_eq!(new_balance, INITIAL_BALANCE); + }); +} + +#[test] +fn inspector_should_intercept_illegal_request() { + new_test_ext().execute_with(|| { + let asset_id: H256 = [1u8; 32].into(); + let post = PostRequest { + source: StateMachine::Kusama(100), + dest: StateMachine::Evm(1), + nonce: 0, + from: module_id().0.to_vec(), + to: H160::zero().0.to_vec(), + timeout_timestamp: 1000, + body: { + let body = Body { + amount: { + let mut bytes = [0u8; 32]; + // Module callback will convert to ten decimals + convert_to_erc20(SEND_AMOUNT).to_big_endian(&mut bytes); + alloy_primitives::U256::from_be_bytes(bytes) + }, + asset_id: asset_id.0.into(), + redeem: false, + from: alloy_primitives::B256::from_slice(ALICE.as_slice()), + to: alloy_primitives::B256::from_slice(ALICE.as_slice()), + }; + + let encoded = vec![vec![0], Body::abi_encode(&body)].concat(); + encoded + }, + }; + + let result = TokenGatewayInspector::inspect_request(&post); + println!("{result:?}"); + assert!(result.is_err()); + + pallet_token_gateway_inspector::InflowBalances::::insert( + StateMachine::Kusama(100), + asset_id, + convert_to_erc20(SEND_AMOUNT), + ); + + let result = TokenGatewayInspector::inspect_request(&post); + assert!(result.is_ok()); + let inflow = pallet_token_gateway_inspector::InflowBalances::::get( + StateMachine::Kusama(100), + asset_id, + ); + assert_eq!(inflow, U256::zero()); + }); +} + +#[test] +fn inspector_should_record_asset_inflow() { + new_test_ext().execute_with(|| { + let asset_id: H256 = [1u8; 32].into(); + let post = PostRequest { + source: StateMachine::Evm(1), + dest: StateMachine::Kusama(100), + nonce: 0, + from: H160::zero().0.to_vec(), + to: H160::zero().0.to_vec(), + timeout_timestamp: 1000, + body: { + let body = Body { + amount: { + let mut bytes = [0u8; 32]; + // Module callback will convert to ten decimals + convert_to_erc20(SEND_AMOUNT).to_big_endian(&mut bytes); + alloy_primitives::U256::from_be_bytes(bytes) + }, + asset_id: asset_id.0.into(), + redeem: false, + from: alloy_primitives::B256::from_slice(ALICE.as_slice()), + to: alloy_primitives::B256::from_slice(ALICE.as_slice()), + }; + + let encoded = vec![vec![0], Body::abi_encode(&body)].concat(); + encoded + }, + }; + + let result = TokenGatewayInspector::inspect_request(&post); + println!("{result:?}"); + assert!(result.is_ok()); + + let inflow = pallet_token_gateway_inspector::InflowBalances::::get( + StateMachine::Kusama(100), + asset_id, + ); + + assert_eq!(convert_to_erc20(SEND_AMOUNT), inflow); + }); +} + +#[test] +fn inspector_should_handle_timeout_correctly() { + new_test_ext().execute_with(|| { + let asset_id: H256 = [1u8; 32].into(); + let post = PostRequest { + source: StateMachine::Kusama(100), + dest: StateMachine::Evm(1), + nonce: 0, + from: module_id().0.to_vec(), + to: H160::zero().0.to_vec(), + timeout_timestamp: 1000, + body: { + let body = Body { + amount: { + let mut bytes = [0u8; 32]; + // Module callback will convert to ten decimals + convert_to_erc20(SEND_AMOUNT).to_big_endian(&mut bytes); + alloy_primitives::U256::from_be_bytes(bytes) + }, + asset_id: asset_id.0.into(), + redeem: false, + from: alloy_primitives::B256::from_slice(ALICE.as_slice()), + to: alloy_primitives::B256::from_slice(ALICE.as_slice()), + }; + + let encoded = vec![vec![0], Body::abi_encode(&body)].concat(); + encoded + }, + }; + + let inflow = pallet_token_gateway_inspector::InflowBalances::::get( + StateMachine::Kusama(100), + asset_id, + ); + + assert_eq!(inflow, U256::zero()); + + pallet_token_gateway_inspector::InflowBalances::::insert( + StateMachine::Evm(1), + asset_id, + convert_to_erc20(SEND_AMOUNT), + ); + + let result = TokenGatewayInspector::handle_timeout(&post); + println!("{result:?}"); + assert!(result.is_ok()); + + let inflow = pallet_token_gateway_inspector::InflowBalances::::get( + StateMachine::Kusama(100), + asset_id, + ); + + assert_eq!(convert_to_erc20(SEND_AMOUNT), inflow); + }); +} diff --git a/modules/ismp/pallets/testsuite/src/tests/xcm_integration_test.rs b/modules/ismp/pallets/testsuite/src/tests/xcm_integration_test.rs index edd698c4..bcdf1068 100644 --- a/modules/ismp/pallets/testsuite/src/tests/xcm_integration_test.rs +++ b/modules/ismp/pallets/testsuite/src/tests/xcm_integration_test.rs @@ -128,7 +128,8 @@ async fn should_dispatch_ismp_request_when_xcm_is_received() -> anyhow::Result<( _ => None, }) { let body = - pallet_asset_gateway::Body::abi_decode(&mut &post.body[1..], true).unwrap(); + pallet_token_gateway::types::Body::abi_decode(&mut &post.body[1..], true) + .unwrap(); let to = alloy_primitives::FixedBytes::<32>::from_slice( &vec![vec![0u8; 12], vec![1u8; 20]].concat(), ); diff --git a/modules/ismp/pallets/token-gateway-inspector/Cargo.toml b/modules/ismp/pallets/token-gateway-inspector/Cargo.toml new file mode 100644 index 00000000..c4249530 --- /dev/null +++ b/modules/ismp/pallets/token-gateway-inspector/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "pallet-token-gateway-inspector" +version = "0.1.0" +edition = "2021" +description = "The token gateway inspector ensures the validity of token gateway messages coming from standalone chains" +authors = ["Polytope Labs "] +publish = false + +[dependencies] +frame-support = { workspace = true } +frame-system = { workspace = true } +sp-runtime = { workspace = true } +sp-core = { workspace = true } +sp-io = { workspace = true } +primitive-types = { workspace = true } + +ismp = { workspace = true } +pallet-ismp = { workspace = true } + +log = { workspace = true } +codec = { workspace = true } +scale-info = { workspace = true } +anyhow = { workspace = true } + +alloy-primitives = { workspace = true } +alloy-sol-macro = { workspace = true } +alloy-sol-types = { workspace = true } + +pallet-token-gateway = { workspace = true } +pallet-token-governor = { workspace = true } + +[features] +default = ["std"] +std = [ + "frame-support/std", + "frame-system/std", + "sp-runtime/std", + "sp-core/std", + "sp-io/std", + "primitive-types/std", + "ismp/std", + "pallet-ismp/std", + "log/std", + "scale-info/std", + "anyhow/std", + "alloy-primitives/std", + "pallet-token-gateway/std", + "pallet-token-governor/std", +] +try-runtime = [] diff --git a/modules/ismp/pallets/token-gateway-inspector/src/lib.rs b/modules/ismp/pallets/token-gateway-inspector/src/lib.rs new file mode 100644 index 00000000..1ec040e8 --- /dev/null +++ b/modules/ismp/pallets/token-gateway-inspector/src/lib.rs @@ -0,0 +1,275 @@ +// Copyright (C) Polytope Labs Ltd. +// 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. + +//! The token governor handles asset registration as well as tracks the metadata of multi-chain +//! native tokens across all connected chains +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use alloy_sol_types::SolValue; +use frame_support::pallet_prelude::Weight; +use ismp::router::PostRequest; + +use alloc::{format, string::ToString, vec, vec::Vec}; +use primitive_types::{H256, U256}; + +// Re-export pallet items so that they can be accessed from the crate namespace. +pub use pallet::*; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use alloc::collections::{BTreeMap, BTreeSet}; + use frame_support::{pallet_prelude::*, Blake2_128Concat}; + use frame_system::pallet_prelude::*; + use ismp::{events::Meta, host::StateMachine}; + use pallet_token_gateway::Body; + use pallet_token_governor::TokenGatewayParams; + + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(_); + + /// The pallet's configuration trait. + #[pallet::config] + pub trait Config: + frame_system::Config + pallet_ismp::Config + pallet_token_governor::Config + { + /// The overarching runtime event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + } + + /// Native asset ids for standalone chains connected to token gateway. + #[pallet::storage] + pub type StandaloneChainAssets = + StorageMap<_, Twox64Concat, StateMachine, BTreeSet, OptionQuery>; + + /// Balances for net inflow of non native assets into a standalone chain + #[pallet::storage] + pub type InflowBalances = + StorageDoubleMap<_, Blake2_128Concat, StateMachine, Twox64Concat, H256, U256, ValueQuery>; + + /// Pallet events that functions in this pallet can emit. + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Illegal request has been intercepted + IllegalRequest { source: StateMachine }, + /// Native asset IDs have been registered + NativeAssetsRegistered { assets: BTreeMap> }, + /// Native asset IDs have been deregistered + NativeAssetsDeregistered { assets: BTreeMap> }, + } + + /// Errors that can be returned by this pallet. + #[pallet::error] + pub enum Error {} + + #[pallet::call] + impl Pallet + where + ::AccountId: From<[u8; 32]>, + ::Balance: Default, + { + /// Register the native token asset ids for standalone chains + #[pallet::call_index(0)] + #[pallet::weight(weight())] + pub fn register_standalone_chain_native_assets( + origin: OriginFor, + assets: BTreeMap>, + ) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin)?; + + for (state_machine, mut new_asset_ids) in assets.clone() { + let _ = StandaloneChainAssets::::try_mutate(state_machine, |asset_ids| { + if let Some(set) = asset_ids { + set.append(&mut new_asset_ids); + } else { + *asset_ids = Some(new_asset_ids); + }; + + Ok::<(), ()>(()) + }); + } + + Self::deposit_event(Event::::NativeAssetsRegistered { assets }); + + Ok(()) + } + + /// Deregister the native token asset ids for standalone chains + #[pallet::call_index(1)] + #[pallet::weight(weight())] + pub fn deregister_standalone_chain_native_assets( + origin: OriginFor, + assets: BTreeMap>, + ) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin)?; + + for (state_machine, new_asset_ids) in assets.clone() { + let _ = StandaloneChainAssets::::try_mutate(state_machine, |asset_ids| { + if let Some(set) = asset_ids { + for id in new_asset_ids { + set.remove(&id); + } + if set.is_empty() { + *asset_ids = None; + } + } + Ok::<(), ()>(()) + }); + } + + Self::deposit_event(Event::::NativeAssetsDeregistered { assets }); + + Ok(()) + } + } + + // Hack for implementing the [`Default`] bound needed for + // [`IsmpDispatcher`](ismp::dispatcher::IsmpDispatcher) and + // [`IsmpModule`](ismp::module::IsmpModule) + impl Default for Pallet { + fn default() -> Self { + Self(PhantomData) + } + } + + impl Pallet { + pub fn is_token_gateway_request( + from: Vec, + to: Vec, + source: StateMachine, + dest: StateMachine, + ) -> bool { + from == pallet_token_gateway::impls::module_id().0.to_vec() || + TokenGatewayParams::::get(source) + .map(|params| params.address.0.to_vec() == from) + .unwrap_or_default() || + to == pallet_token_gateway::impls::module_id().0.to_vec() || + TokenGatewayParams::::get(dest) + .map(|params| params.address.0.to_vec() == to) + .unwrap_or_default() + } + + pub fn inspect_request(post: &PostRequest) -> Result<(), ismp::Error> { + let PostRequest { body, from, to, source, dest, nonce, .. } = post.clone(); + + // Token Gateway contracts on EVM chains are immutable and non upgradeable + // As long as the initial deployment is valid + // it's impossible to send malicious requests + if source.is_evm() && dest.is_evm() { + return Ok(()) + } + + if Self::is_token_gateway_request(from.clone(), to.clone(), source, dest) { + let body = Body::abi_decode(&mut &body[1..], true).map_err(|_| { + ismp::error::Error::ModuleDispatchError { + msg: "Token Gateway: Failed to decode request body".to_string(), + meta: Meta { source, dest, nonce }, + } + })?; + + if !dest.is_evm() { + InflowBalances::::try_mutate(dest, H256::from(body.asset_id.0), |val| { + let amount = U256::from_big_endian(&body.amount.to_be_bytes::<32>()); + *val += amount; + Ok::<_, ()>(()) + }) + .map_err(|_| { + ismp::Error::Custom(format!( + "Failed to record inflow while inspecting packet" + )) + })?; + } + + let native_asset_ids = StandaloneChainAssets::::get(source).unwrap_or_default(); + if !native_asset_ids.contains(&H256::from(body.asset_id.0)) && !source.is_evm() { + let balance = InflowBalances::::get(source, H256::from(body.asset_id.0)); + let amount = U256::from_big_endian(&body.amount.to_be_bytes::<32>()); + if amount > balance { + Err(ismp::Error::Custom(format!("Illegal Token Gateway request")))?; + Pallet::::deposit_event(Event::::IllegalRequest { source }) + } + + InflowBalances::::try_mutate(source, H256::from(body.asset_id.0), |val| { + *val -= amount; + Ok::<_, ()>(()) + }) + .map_err(|_| { + ismp::Error::Custom(format!( + "Failed to record inflow while inspecting packet" + )) + })?; + } + } + + Ok(()) + } + + pub fn handle_timeout(post: &PostRequest) -> Result<(), ismp::Error> { + let PostRequest { body, from, to, source, dest, nonce, .. } = post.clone(); + // Token Gateway contracts on EVM chains are immutable and non upgradeable + // As long as the initial deployment is valid + // it's impossible to send malicious requests + if source.is_evm() && dest.is_evm() { + return Ok(()) + } + + if Self::is_token_gateway_request(from.clone(), to.clone(), source, dest) { + let body = Body::abi_decode(&mut &body[1..], true).map_err(|_| { + ismp::error::Error::ModuleDispatchError { + msg: "Token Gateway: Failed to decode request body".to_string(), + meta: Meta { source, dest, nonce }, + } + })?; + + let native_asset_ids = StandaloneChainAssets::::get(source).unwrap_or_default(); + if !native_asset_ids.contains(&H256::from(body.asset_id.0)) && !source.is_evm() { + InflowBalances::::try_mutate(source, H256::from(body.asset_id.0), |val| { + let amount = U256::from_big_endian(&body.amount.to_be_bytes::<32>()); + *val += amount; + Ok::<_, ()>(()) + }) + .map_err(|_| { + ismp::Error::Custom(format!( + "Failed to record inflow while inspecting packet" + )) + })?; + } + + if !dest.is_evm() { + InflowBalances::::try_mutate(dest, H256::from(body.asset_id.0), |val| { + let amount = U256::from_big_endian(&body.amount.to_be_bytes::<32>()); + *val -= amount; + Ok::<_, ()>(()) + }) + .map_err(|_| { + ismp::Error::Custom(format!( + "Failed to record inflow while inspecting packet" + )) + })?; + } + } + Ok(()) + } + } +} + +/// Static weights because benchmarks suck, and we'll be getting PolkaVM soon anyways +fn weight() -> Weight { + Weight::from_parts(300_000_000, 0) +} diff --git a/modules/ismp/pallets/token-gateway/Cargo.toml b/modules/ismp/pallets/token-gateway/Cargo.toml new file mode 100644 index 00000000..bbf5bd6f --- /dev/null +++ b/modules/ismp/pallets/token-gateway/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "pallet-token-gateway" +version = "1.15.0" +edition = "2021" +description = "The token gateway is a susbtrate implementation of the token gateway protocol" +authors = ["Polytope Labs "] +publish = false + +[dependencies] +frame-support = { workspace = true } +frame-system = { workspace = true } +sp-runtime = { workspace = true } +sp-core = { workspace = true } +sp-io = { workspace = true } +primitive-types = { workspace = true } + +ismp = { workspace = true } +pallet-ismp = { workspace = true } + +log = { workspace = true } +codec = { workspace = true } +scale-info = { workspace = true } +anyhow = { workspace = true } + +alloy-primitives = { workspace = true } +alloy-sol-macro = { workspace = true } +alloy-sol-types = { workspace = true } + +pallet-token-governor = { workspace = true } + +[features] +default = ["std"] +std = [ + "frame-support/std", + "frame-system/std", + "sp-runtime/std", + "sp-core/std", + "sp-io/std", + "primitive-types/std", + "ismp/std", + "pallet-ismp/std", + "log/std", + "scale-info/std", + "anyhow/std", + "alloy-primitives/std", + "pallet-token-governor/std" +] +try-runtime = [] diff --git a/modules/ismp/pallets/token-gateway/README.md b/modules/ismp/pallets/token-gateway/README.md new file mode 100644 index 00000000..5ad0ebb8 --- /dev/null +++ b/modules/ismp/pallets/token-gateway/README.md @@ -0,0 +1,70 @@ +# Pallet Token Gateway + +This allows standalone chains or parachains make asset transfers to and from EVM token gateway deployments. + + +## Overview + +The Pallet allows the [`AdminOrigin`](https://docs.rs/pallet-ismp/latest/pallet_ismp/pallet/trait.Config.html#associatedtype.AdminOrigin) configured in [`pallet-ismp`](https://docs.rs/pallet-ismp/latest/pallet_ismp) to dispatch calls for registering asset Ids +and also requesting the token gateway addresses from Hyperbridge. + +## Adding to Runtime + +The first step is to implement the pallet config for the runtime. + +```rust,ignore +use frame_support::parameter_types; +use ismp::Error; +use ismp::host::StateMachine; +use ismp::module::IsmpModule; +use ismp::router::{IsmpRouter, Post, Response, Timeout}; + +parameter_types! { + // The Native asset Id for the native currency, for parachains this would be the XCM location for the parachain + // For standalone chains, any constant of your choosing + pub const NativeAssetId: StateMachine = Location::here(); +} + +impl pallet_ismp::Config for Runtime { + // configure the runtime event + type RuntimeEvent = RuntimeEvent; + // Pallet Ismp + type Dispatcher = Ismp; + // Pallet Assets + type Assets = Assets; + // Pallet balances + type Currency = Balances; + // The Native asset Id + type NativeAssetId = NativeAssetId; +} + +#[derive(Default)] +struct Router; +impl IsmpRouter for Router { + fn module_for_id(&self, id: Vec) -> Result, Error> { + let module = match id.as_slice() { + id if TokenGateway::is_token_gateway(&id) => Box::new(TokenGateway::default()), + _ => Err(Error::ModuleNotFound(id))? + }; + Ok(module) + } +} +``` + +## Setting up + +The pallet requires some setting up before the teleport function is available for use in the runtime. + +1. Register your native assets directly on `Hyperbridge` by dispatching `create_erc6160_asset`. +3. Set token gateway addresses for the EVM chains of interest by dispatching the `set_token_gateway_addresses` extrinsic. + This allows us validate incoming requests. + + +## Dispatchable Functions + +- `teleport` - This function is used to bridge assets to EVM chains through Hyperbridge. +- `set_token_gateway_addresses` - This call allows the `AdminOrigin` origin to set the token gateway address for EVM chains. +- `create_erc6160_asset` - This call dispatches a request to Hyperbridge to create multi chain native assets on token gateway deployments +## License + +This library is licensed under the Apache 2.0 License, Copyright (c) 2024 Polytope Labs. diff --git a/modules/ismp/pallets/token-gateway/src/impls.rs b/modules/ismp/pallets/token-gateway/src/impls.rs new file mode 100644 index 00000000..d2ec37e2 --- /dev/null +++ b/modules/ismp/pallets/token-gateway/src/impls.rs @@ -0,0 +1,99 @@ +// Copyright (C) Polytope Labs Ltd. +// 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 Implementations + +use alloc::string::ToString; +use sp_core::{H160, U256}; +use sp_runtime::traits::AccountIdConversion; + +use crate::{Config, Pallet, PALLET_ID}; + +impl Pallet { + pub fn pallet_account() -> T::AccountId { + PALLET_ID.into_account_truncating() + } + + pub fn is_token_gateway(id: &[u8]) -> bool { + id == &module_id().0 + } +} + +/// Module Id is the last 20 bytes of the keccak hash of the pallet id +pub fn module_id() -> H160 { + let hash = sp_io::hashing::keccak_256(&PALLET_ID.0); + H160::from_slice(&hash[12..32]) +} + +/// Converts an ERC20 U256 to a DOT u128 +pub fn convert_to_balance(value: U256) -> Result { + let dec_str = (value / U256::from(100_000_000u128)).to_string(); + dec_str.parse().map_err(|e| anyhow::anyhow!("{e:?}")) +} + +/// Converts a DOT u128 to an Erc20 denomination +pub fn convert_to_erc20(value: u128) -> U256 { + U256::from(value) * U256::from(100_000_000u128) +} + +#[cfg(test)] +mod tests { + use sp_core::U256; + use sp_runtime::Permill; + use std::ops::Mul; + + use super::{convert_to_balance, convert_to_erc20}; + + #[test] + fn test_per_mill() { + let per_mill = Permill::from_parts(1_000); + + println!("{}", per_mill.mul(20_000_000u128)); + } + + #[test] + fn balance_conversions() { + let supposedly_small_u256 = U256::from_dec_str("1000000000000000000").unwrap(); + // convert erc20 value to dot value + let converted_balance = convert_to_balance(supposedly_small_u256).unwrap(); + println!("{}", converted_balance); + + let dot = 10_000_000_000u128; + + assert_eq!(converted_balance, dot); + + // Convert 1 dot to erc20 + + let dot = 10_000_000_000u128; + let erc_20_val = convert_to_erc20(dot); + assert_eq!(erc_20_val, U256::from_dec_str("1000000000000000000").unwrap()); + } + + #[test] + fn max_value_check() { + let max = U256::MAX; + + let converted_balance = convert_to_balance(max); + assert!(converted_balance.is_err()) + } + + #[test] + fn min_value_check() { + let min = U256::from(1u128); + + let converted_balance = convert_to_balance(min).unwrap(); + assert_eq!(converted_balance, 0); + } +} diff --git a/modules/ismp/pallets/token-gateway/src/lib.rs b/modules/ismp/pallets/token-gateway/src/lib.rs new file mode 100644 index 00000000..fc449d19 --- /dev/null +++ b/modules/ismp/pallets/token-gateway/src/lib.rs @@ -0,0 +1,474 @@ +// Copyright (C) Polytope Labs Ltd. +// 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. + +//! The token gateway enables asset transfers to EVM instances of token gateway +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +pub mod impls; +pub mod types; +use crate::impls::{convert_to_balance, convert_to_erc20, module_id}; +use alloy_sol_types::SolValue; +use frame_support::{ + ensure, + pallet_prelude::Weight, + traits::{ + fungibles::{self, Mutate}, + tokens::Preservation, + Currency, ExistenceRequirement, + }, + PalletId, +}; +use ismp::{ + events::Meta, + router::{PostRequest, Request, Response, Timeout}, +}; +use sp_core::{Get, U256}; +pub use types::*; + +use alloc::{string::ToString, vec, vec::Vec}; +use ismp::module::IsmpModule; +use primitive_types::H256; + +// Re-export pallet items so that they can be accessed from the crate namespace. +pub use pallet::*; + +/// The module id for this pallet +pub const PALLET_ID: PalletId = PalletId(*b"tokengtw"); + +#[frame_support::pallet] +pub mod pallet { + use alloc::collections::BTreeMap; + + use super::*; + use frame_support::{ + pallet_prelude::*, + traits::{tokens::Preservation, Currency, ExistenceRequirement}, + }; + use frame_system::pallet_prelude::*; + use ismp::{ + dispatcher::{DispatchPost, DispatchRequest, FeeMetadata, IsmpDispatcher}, + host::StateMachine, + }; + use pallet_token_governor::RemoteERC6160AssetRegistration; + + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(_); + + /// The pallet's configuration trait. + #[pallet::config] + pub trait Config: frame_system::Config + pallet_ismp::Config { + /// The overarching runtime event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// The [`IsmpDispatcher`] for dispatching cross-chain requests + type Dispatcher: IsmpDispatcher; + + /// A currency implementation for interacting with the native asset + type Currency: Currency; + + /// Fungible asset implementation + type Assets: fungibles::Mutate + fungibles::Inspect; + + /// The native asset ID + type NativeAssetId: Get>; + } + + /// Assets supported by this instance of token gateway + /// A map of the local asset id to the token gateway asset id + #[pallet::storage] + pub type SupportedAssets = StorageMap<_, Identity, AssetId, H256, OptionQuery>; + + /// Assets supported by this instance of token gateway + /// A map of the token gateway asset id to the local asset id + #[pallet::storage] + pub type LocalAssets = StorageMap<_, Identity, H256, AssetId, OptionQuery>; + + /// The token gateway adresses on different chains + #[pallet::storage] + pub type TokenGatewayAddresses = + StorageMap<_, Identity, StateMachine, Vec, OptionQuery>; + + /// Pallet events that functions in this pallet can emit. + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// An asset has been teleported + AssetTeleported { + /// Source account on the relaychain + from: T::AccountId, + /// beneficiary account on destination + to: H256, + /// Amount transferred + amount: <::Currency as Currency>::Balance, + /// Destination chain + dest: StateMachine, + /// Request commitment + commitment: H256, + }, + + /// An asset has been received and transferred to the beneficiary's account + AssetReceived { + /// beneficiary account on relaychain + beneficiary: T::AccountId, + /// Amount transferred + amount: <::Currency as Currency>::Balance, + /// Destination chain + source: StateMachine, + }, + + /// An asset has been refunded and transferred to the beneficiary's account + AssetRefunded { + /// beneficiary account on relaychain + beneficiary: T::AccountId, + /// Amount transferred + amount: <::Currency as Currency>::Balance, + /// Destination chain + source: StateMachine, + }, + + /// ERC6160 asset creation request dispatched to hyperbridge + ERC6160AssetRegistrationDispatched { + /// Request commitment + commitment: H256, + }, + } + + /// Errors that can be returned by this pallet. + #[pallet::error] + pub enum Error { + /// A asset that has not been registered + UnregisteredAsset, + /// Error while teleporting asset + AssetTeleportError, + /// Coprocessor was not configured in the runtime + CoprocessorNotConfigured, + } + + #[pallet::call] + impl Pallet + where + ::AccountId: From<[u8; 32]>, + u128: From<<::Currency as Currency>::Balance>, + ::Balance: + From<<::Currency as Currency>::Balance>, + <::Assets as fungibles::Inspect>::Balance: + From<<::Currency as Currency>::Balance>, + [u8; 32]: From<::AccountId>, + { + /// Teleports a registered asset + /// locks the asset and dispatches a request to token gateway on the destination + #[pallet::call_index(0)] + #[pallet::weight(weight())] + pub fn teleport( + origin: OriginFor, + params: TeleportParams< + AssetId, + <::Currency as Currency>::Balance, + >, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + let dispatcher = ::Dispatcher::default(); + let asset_id = SupportedAssets::::get(params.asset_id.clone()) + .ok_or_else(|| Error::::UnregisteredAsset)?; + if params.asset_id == T::NativeAssetId::get() { + // Custody funds in pallet + ::Currency::transfer( + &who, + &Self::pallet_account(), + params.amount, + ExistenceRequirement::KeepAlive, + )?; + } else { + ::Assets::transfer( + params.asset_id, + &who, + &Self::pallet_account(), + params.amount.into(), + Preservation::Protect, + )?; + } + + // Dispatch Ismp request + // Token gateway expected abi encoded address + let to = params.recepient.0; + let from: [u8; 32] = who.clone().into(); + + let body = Body { + amount: { + let amount: u128 = params.amount.into(); + let mut bytes = [0u8; 32]; + convert_to_erc20(amount).to_big_endian(&mut bytes); + alloy_primitives::U256::from_be_bytes(bytes) + }, + asset_id: asset_id.0.into(), + redeem: false, + from: from.into(), + to: to.into(), + }; + + let dispatch_post = DispatchPost { + dest: params.destination, + from: module_id().0.to_vec(), + to: params.token_gateway, + timeout: params.timeout, + body: { + // Prefix with the handleIncomingAsset enum variant + let mut encoded = vec![0]; + encoded.extend_from_slice(&Body::abi_encode(&body)); + encoded + }, + }; + + let metadata = FeeMetadata { payer: who.clone(), fee: params.relayer_fee.into() }; + let commitment = dispatcher + .dispatch_request(DispatchRequest::Post(dispatch_post), metadata) + .map_err(|_| Error::::AssetTeleportError)?; + + Self::deposit_event(Event::::AssetTeleported { + from: who, + to: params.recepient, + dest: params.destination, + amount: params.amount, + commitment, + }); + Ok(()) + } + + /// Set the token gateway address for specified chains + #[pallet::call_index(1)] + #[pallet::weight(weight())] + pub fn set_token_gateway_addresses( + origin: OriginFor, + addresses: BTreeMap>, + ) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin)?; + for (chain, address) in addresses { + TokenGatewayAddresses::::insert(chain, address.clone()); + } + Ok(()) + } + + /// Registers a multi-chain ERC6160 asset. The asset should not already exist. + /// + /// This works by dispatching a request to the TokenGateway module on each requested chain + /// to create the asset. + #[pallet::call_index(2)] + #[pallet::weight(weight())] + pub fn create_erc6160_asset( + origin: OriginFor, + owner: T::AccountId, + assets: AssetRegistration>, + ) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin)?; + + for asset_map in assets.assets.clone() { + let asset_id: H256 = + sp_io::hashing::keccak_256(asset_map.reg.symbol.as_ref()).into(); + SupportedAssets::::insert(asset_map.local_id.clone(), asset_id.clone()); + LocalAssets::::insert(asset_id, asset_map.local_id); + } + + let dispatcher = ::Dispatcher::default(); + let dispatch_post = DispatchPost { + dest: T::Coprocessor::get().ok_or_else(|| Error::::CoprocessorNotConfigured)?, + from: module_id().0.to_vec(), + to: pallet_token_governor::PALLET_ID.to_vec(), + timeout: 0, + body: { + RemoteERC6160AssetRegistration { + assets: assets.assets.into_iter().map(|asset_map| asset_map.reg).collect(), + owner: owner.clone(), + } + .encode() + }, + }; + + let metadata = FeeMetadata { payer: owner.into(), fee: Default::default() }; + + let commitment = dispatcher + .dispatch_request(DispatchRequest::Post(dispatch_post), metadata) + .map_err(|_| Error::::AssetTeleportError)?; + Self::deposit_event(Event::::ERC6160AssetRegistrationDispatched { commitment }); + + Ok(()) + } + } + + // Hack for implementing the [`Default`] bound needed for + // [`IsmpDispatcher`](ismp::dispatcher::IsmpDispatcher) and + // [`IsmpModule`](ismp::module::IsmpModule) + impl Default for Pallet { + fn default() -> Self { + Self(PhantomData) + } + } +} + +impl IsmpModule for Pallet +where + ::AccountId: From<[u8; 32]>, + <::Currency as Currency>::Balance: From, + <::Assets as fungibles::Inspect>::Balance: From, +{ + fn on_accept( + &self, + PostRequest { body, from, source, dest, nonce, .. }: PostRequest, + ) -> Result<(), ismp::error::Error> { + ensure!( + from == TokenGatewayAddresses::::get(source).unwrap_or_default().to_vec() || + from == module_id().0.to_vec(), + ismp::error::Error::ModuleDispatchError { + msg: "Token Gateway: Unknown source contract address".to_string(), + meta: Meta { source, dest, nonce }, + } + ); + + let body = Body::abi_decode(&mut &body[1..], true).map_err(|_| { + ismp::error::Error::ModuleDispatchError { + msg: "Token Gateway: Failed to decode request body".to_string(), + meta: Meta { source, dest, nonce }, + } + })?; + let amount = convert_to_balance(U256::from_big_endian(&body.amount.to_be_bytes::<32>())) + .map_err(|_| ismp::error::Error::ModuleDispatchError { + msg: "Token Gateway: Trying to withdraw Invalid amount".to_string(), + meta: Meta { source, dest, nonce }, + })?; + + let local_asset_id = + LocalAssets::::get(H256::from(body.asset_id.0)).ok_or_else(|| { + ismp::error::Error::ModuleDispatchError { + msg: "Token Gateway: Unknown asset".to_string(), + meta: Meta { source, dest, nonce }, + } + })?; + let beneficiary: T::AccountId = body.to.0.into(); + if local_asset_id == T::NativeAssetId::get() { + ::Currency::transfer( + &Pallet::::pallet_account(), + &beneficiary, + amount.into(), + ExistenceRequirement::AllowDeath, + ) + .map_err(|_| ismp::error::Error::ModuleDispatchError { + msg: "Token Gateway: Failed to complete asset transfer".to_string(), + meta: Meta { source, dest, nonce }, + })?; + } else { + ::Assets::transfer( + local_asset_id, + &Pallet::::pallet_account(), + &beneficiary, + amount.into(), + Preservation::Protect, + ) + .map_err(|_| ismp::error::Error::ModuleDispatchError { + msg: "Token Gateway: Failed to complete asset transfer".to_string(), + meta: Meta { source, dest, nonce }, + })?; + } + + Self::deposit_event(Event::::AssetReceived { + beneficiary, + amount: amount.into(), + source, + }); + Ok(()) + } + + fn on_response(&self, _response: Response) -> Result<(), ismp::error::Error> { + Err(ismp::error::Error::Custom("Module does not accept responses".to_string())) + } + + fn on_timeout(&self, request: Timeout) -> Result<(), ismp::error::Error> { + match request { + Timeout::Request(Request::Post(PostRequest { body, source, dest, nonce, .. })) => { + let body = Body::abi_decode(&mut &body[1..], true).map_err(|_| { + ismp::error::Error::ModuleDispatchError { + msg: "Token Gateway: Failed to decode request body".to_string(), + meta: Meta { source, dest, nonce }, + } + })?; + let beneficiary = body.from.0.into(); + + let amount = + convert_to_balance(U256::from_big_endian(&body.amount.to_be_bytes::<32>())) + .map_err(|_| ismp::error::Error::ModuleDispatchError { + msg: "Token Gateway: Trying to withdraw Invalid amount".to_string(), + meta: Meta { source, dest, nonce }, + })?; + let local_asset_id = LocalAssets::::get(H256::from(body.asset_id.0)) + .ok_or_else(|| ismp::error::Error::ModuleDispatchError { + msg: "Token Gateway: Unknown asset".to_string(), + meta: Meta { source, dest, nonce }, + })?; + + if local_asset_id == T::NativeAssetId::get() { + ::Currency::transfer( + &Pallet::::pallet_account(), + &beneficiary, + amount.into(), + ExistenceRequirement::AllowDeath, + ) + .map_err(|_| ismp::error::Error::ModuleDispatchError { + msg: "Token Gateway: Failed to complete asset transfer".to_string(), + meta: Meta { source, dest, nonce }, + })?; + } else { + ::Assets::transfer( + local_asset_id, + &Pallet::::pallet_account(), + &beneficiary, + amount.into(), + Preservation::Protect, + ) + .map_err(|_| ismp::error::Error::ModuleDispatchError { + msg: "Token Gateway: Failed to complete asset transfer".to_string(), + meta: Meta { source, dest, nonce }, + })?; + } + + Pallet::::deposit_event(Event::::AssetRefunded { + beneficiary, + amount: amount.into(), + source: dest, + }); + }, + Timeout::Request(Request::Get(get)) => Err(ismp::error::Error::ModuleDispatchError { + msg: "Tried to timeout unsupported request type".to_string(), + meta: Meta { source: get.source, dest: get.dest, nonce: get.nonce }, + })?, + + Timeout::Response(response) => Err(ismp::error::Error::ModuleDispatchError { + msg: "Tried to timeout unsupported request type".to_string(), + meta: Meta { + source: response.source_chain(), + dest: response.dest_chain(), + nonce: response.nonce(), + }, + })?, + } + Ok(()) + } +} + +/// Static weights because benchmarks suck, and we'll be getting PolkaVM soon anyways +fn weight() -> Weight { + Weight::from_parts(300_000_000, 0) +} diff --git a/modules/ismp/pallets/token-gateway/src/types.rs b/modules/ismp/pallets/token-gateway/src/types.rs new file mode 100644 index 00000000..1da61b68 --- /dev/null +++ b/modules/ismp/pallets/token-gateway/src/types.rs @@ -0,0 +1,78 @@ +// Copyright (C) Polytope Labs Ltd. +// 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 types + +use alloc::vec::Vec; +use frame_support::{pallet_prelude::*, traits::fungibles}; +use ismp::host::StateMachine; +use pallet_token_governor::ERC6160AssetRegistration; +use primitive_types::H256; + +use crate::Config; + +pub type AssetId = + <::Assets as fungibles::Inspect<::AccountId>>::AssetId; + +/// Asset teleportation parameters +#[derive(Debug, Clone, Encode, Decode, scale_info::TypeInfo, PartialEq, Eq)] +pub struct TeleportParams { + /// Asset Id registered on Hyperbridge + pub asset_id: AssetId, + /// Destination state machine + pub destination: StateMachine, + /// Receiving account on destination + pub recepient: H256, + /// Amount to be sent + pub amount: Balance, + /// Request timeout + pub timeout: u64, + /// Token gateway address + pub token_gateway: Vec, + /// Relayer fee + pub relayer_fee: Balance, +} + +/// Local asset Id and its corresponding token gateway asset id +#[derive(Clone, Encode, Decode, scale_info::TypeInfo, PartialEq, Eq, RuntimeDebug)] +pub struct AssetMap { + /// Local Asset Id + pub local_id: AssetId, + /// MNT Asset registration details + pub reg: ERC6160AssetRegistration, +} + +/// A struct for registering some assets +#[derive(Clone, Encode, Decode, scale_info::TypeInfo, PartialEq, Eq, RuntimeDebug)] +#[scale_info(skip_type_params(T))] +pub struct AssetRegistration { + pub assets: BoundedVec, ConstU32<10>>, +} + +alloy_sol_macro::sol! { + #![sol(all_derives)] + struct Body { + // Amount of the asset to be sent + uint256 amount; + // The asset identifier + bytes32 asset_id; + // Flag to redeem the erc20 asset on the destination + bool redeem; + // Sender address + bytes32 from; + // Recipient address + bytes32 to; + } +} diff --git a/modules/ismp/pallets/token-governor/src/impls.rs b/modules/ismp/pallets/token-governor/src/impls.rs index 99a8e4be..212c6d73 100644 --- a/modules/ismp/pallets/token-governor/src/impls.rs +++ b/modules/ismp/pallets/token-governor/src/impls.rs @@ -85,7 +85,7 @@ where .ok_or_else(|| Error::::UnknownTokenGateway)?; let dispatcher = T::Dispatcher::default(); - dispatcher + let commitment = dispatcher .dispatch_request( DispatchRequest::Post(DispatchPost { dest: chain.clone(), @@ -99,11 +99,11 @@ where .map_err(|_| Error::::DispatchFailed)?; // tracks which chains the asset is deployed on SupportedChains::::insert(asset_id, chain, true); + Self::deposit_event(Event::::AssetRegistered { asset_id, commitment, dest: chain }); } AssetMetadatas::::insert(asset_id, metadata); AssetOwners::::insert(asset_id, who); - Self::deposit_event(Event::::AssetRegistered { asset_id }); Ok(()) } @@ -388,7 +388,7 @@ where .ok_or_else(|| Error::::UnknownTokenGateway)?; let dispatcher = T::Dispatcher::default(); - dispatcher + let commitment = dispatcher .dispatch_request( DispatchRequest::Post(DispatchPost { dest: chain.clone(), @@ -402,13 +402,13 @@ where .map_err(|_| Error::::DispatchFailed)?; // tracks which chains the asset is deployed on SupportedChains::::insert(asset_id, chain, true); + Self::deposit_event(Event::::AssetRegistered { asset_id, commitment, dest: chain }); } AssetMetadatas::::insert(asset_id, metadata); let who: T::AccountId = PalletId(PALLET_ID).into_account_truncating(); AssetOwners::::insert(asset_id, who); - Self::deposit_event(Event::::AssetRegistered { asset_id }); Ok(()) } } diff --git a/modules/ismp/pallets/token-governor/src/lib.rs b/modules/ismp/pallets/token-governor/src/lib.rs index 6041bdd8..3ff651e1 100644 --- a/modules/ismp/pallets/token-governor/src/lib.rs +++ b/modules/ismp/pallets/token-governor/src/lib.rs @@ -24,6 +24,7 @@ mod types; use alloy_sol_types::SolValue; use frame_support::pallet_prelude::Weight; use ismp::router::{PostRequest, Response, Timeout}; + pub use types::*; use alloc::{format, vec}; @@ -111,6 +112,10 @@ pub mod pallet { AssetRegistered { /// The asset identifier asset_id: H256, + /// Request commitment + commitment: H256, + /// Destination chain + dest: StateMachine, }, /// A new pending asset has been registered NewPendingAsset { @@ -366,11 +371,26 @@ pub mod pallet { } } -impl IsmpModule for Pallet { +impl IsmpModule for Pallet +where + T::AccountId: From<[u8; 32]>, +{ fn on_accept( &self, PostRequest { body: data, from, source, .. }: PostRequest, ) -> Result<(), ismp::error::Error> { + // Only substrate chains are allowed to fully register assets remotely + if source.is_substrate() { + let remote_reg: RemoteERC6160AssetRegistration = + codec::Decode::decode(&mut &*data) + .map_err(|_| ismp::error::Error::Custom(format!("Failed to decode data")))?; + for asset in remote_reg.assets { + Pallet::::register_asset(asset, remote_reg.owner.clone()).map_err(|e| { + ismp::error::Error::Custom(format!("Failed create asset {e:?}")) + })?; + } + return Ok(()) + } let RegistrarParams { address, .. } = TokenRegistrarParams::::get(&source) .ok_or_else(|| ismp::error::Error::Custom(format!("Pallet is not initialized")))?; if from != address.as_bytes().to_vec() { diff --git a/modules/ismp/pallets/token-governor/src/types.rs b/modules/ismp/pallets/token-governor/src/types.rs index daad26a9..bfeea058 100644 --- a/modules/ismp/pallets/token-governor/src/types.rs +++ b/modules/ismp/pallets/token-governor/src/types.rs @@ -101,6 +101,15 @@ pub struct ERC6160AssetRegistration { pub chains: Vec, } +/// Holds data required for multi-chain native asset registration +#[derive(Debug, Clone, Encode, Decode, scale_info::TypeInfo, PartialEq, Eq)] +pub struct RemoteERC6160AssetRegistration { + /// Owner of these assets + pub owner: AccountId, + /// Assets + pub assets: Vec, +} + /// Holds data required for multi-chain native asset registration (unsigned) #[derive(Debug, Clone, Encode, Decode, scale_info::TypeInfo, PartialEq, Eq)] pub struct UnsignedERC6160AssetRegistration { diff --git a/parachain/runtimes/gargantua/Cargo.toml b/parachain/runtimes/gargantua/Cargo.toml index fa30f214..695ee1c7 100644 --- a/parachain/runtimes/gargantua/Cargo.toml +++ b/parachain/runtimes/gargantua/Cargo.toml @@ -101,6 +101,8 @@ pallet-mmr-runtime-api = { workspace = true } sp-mmr-primitives = { workspace = true } simnode-runtime-api = { workspace = true } hyperbridge-client-machine = { workspace = true } +pallet-token-gateway-inspector = { workspace = true } +pallet-token-gateway = { workspace = true } [features] default = [ @@ -173,6 +175,8 @@ std = [ "pallet-call-decompressor/std", "pallet-state-coprocessor/std", "pallet-asset-gateway/std", + "pallet-token-gateway-inspector/std", + "pallet-token-gateway/std", "pallet-token-governor/std", "pallet-assets/std", "pallet-mmr/std", diff --git a/parachain/runtimes/gargantua/src/ismp.rs b/parachain/runtimes/gargantua/src/ismp.rs index f3986b95..eec9769f 100644 --- a/parachain/runtimes/gargantua/src/ismp.rs +++ b/parachain/runtimes/gargantua/src/ismp.rs @@ -16,7 +16,7 @@ use crate::{ alloc::{boxed::Box, string::ToString}, weights, AccountId, Assets, Balance, Balances, Gateway, Ismp, IsmpParachain, Mmr, - ParachainInfo, Runtime, RuntimeEvent, Timestamp, EXISTENTIAL_DEPOSIT, + ParachainInfo, Runtime, RuntimeEvent, Timestamp, TokenGatewayInspector, EXISTENTIAL_DEPOSIT, }; use frame_support::{ pallet_prelude::{ConstU32, Get}, @@ -154,6 +154,10 @@ impl pallet_asset_gateway::Config for Runtime { type IsmpHost = Ismp; } +impl pallet_token_gateway_inspector::Config for Runtime { + type RuntimeEvent = RuntimeEvent; +} + #[cfg(feature = "runtime-benchmarks")] pub struct XcmBenchmarkHelper; #[cfg(feature = "runtime-benchmarks")] @@ -198,10 +202,7 @@ impl pallet_assets::Config for Runtime { impl IsmpModule for ProxyModule { fn on_accept(&self, request: PostRequest) -> Result<(), Error> { if request.dest != HostStateMachine::get() { - let token_gateway = Gateway::token_gateway_address(&request.dest); - if request.source.is_substrate() && request.from == token_gateway.0.to_vec() { - Err(Error::Custom("Illegal request!".into()))? - } + TokenGatewayInspector::inspect_request(&request)?; Ismp::dispatch_request( Request::Post(request), @@ -250,7 +251,12 @@ impl IsmpModule for ProxyModule { fn on_timeout(&self, timeout: Timeout) -> Result<(), Error> { let (from, source) = match &timeout { - Timeout::Request(Request::Post(post)) => (&post.from, &post.source), + Timeout::Request(Request::Post(post)) => { + if post.source != HostStateMachine::get() { + TokenGatewayInspector::handle_timeout(post)?; + } + (&post.from, &post.source) + }, Timeout::Request(Request::Get(get)) => (&get.from, &get.source), Timeout::Response(res) => (&res.post.to, &res.post.dest), }; diff --git a/parachain/runtimes/gargantua/src/lib.rs b/parachain/runtimes/gargantua/src/lib.rs index 49e3bad0..f7403f56 100644 --- a/parachain/runtimes/gargantua/src/lib.rs +++ b/parachain/runtimes/gargantua/src/lib.rs @@ -232,7 +232,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("gargantua"), impl_name: create_runtime_str!("gargantua"), authoring_version: 1, - spec_version: 1130, + spec_version: 1140, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -708,6 +708,7 @@ construct_runtime!( TokenGovernor: pallet_token_governor = 59, StateCoprocessor: pallet_state_coprocessor = 60, Fishermen: pallet_fishermen = 61, + TokenGatewayInspector: pallet_token_gateway_inspector = 62, // Governance TechnicalCollective: pallet_collective = 80,