diff --git a/Cargo.lock b/Cargo.lock index dab3f8879110f..fedc51be796d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1190,6 +1190,7 @@ dependencies = [ name = "asset-hub-westend-runtime" version = "0.15.0" dependencies = [ + "alloy-core", "asset-test-utils", "assets-common", "bp-asset-hub-rococo", @@ -1233,6 +1234,7 @@ dependencies = [ "pallet-nfts-runtime-api", "pallet-proxy", "pallet-revive", + "pallet-revive-fixtures", "pallet-session", "pallet-state-trie-migration", "pallet-timestamp", @@ -1313,15 +1315,20 @@ name = "assets-common" version = "0.7.0" dependencies = [ "cumulus-primitives-core", + "ethereum-standards", "frame-support", + "frame-system", "impl-trait-for-tuples", "pallet-asset-conversion", "pallet-assets", + "pallet-revive", + "pallet-revive-uapi", "pallet-xcm", "parachains-common", "parity-scale-codec", "scale-info", "sp-api 26.0.0", + "sp-core 28.0.0", "sp-runtime 31.0.1", "staging-xcm", "staging-xcm-builder", @@ -12771,6 +12778,7 @@ dependencies = [ "assert_matches", "derive_more 0.99.17", "environmental", + "ethereum-standards", "ethereum-types", "frame-benchmarking", "frame-support", diff --git a/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-westend/src/lib.rs b/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-westend/src/lib.rs index 81f59c93c0f90..0ae7067025fa4 100644 --- a/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-westend/src/lib.rs +++ b/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-westend/src/lib.rs @@ -52,6 +52,7 @@ decl_test_parachains! { PoolAssets: asset_hub_westend_runtime::PoolAssets, AssetConversion: asset_hub_westend_runtime::AssetConversion, SnowbridgeSystemFrontend: asset_hub_westend_runtime::SnowbridgeSystemFrontend, + Revive: asset_hub_westend_runtime::Revive, } }, } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml b/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml index 79afbb00b4615..bc84e439cea43 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml @@ -150,6 +150,7 @@ runtime-benchmarks = [ "xcm/runtime-benchmarks", ] try-runtime = [ + "assets-common/try-runtime", "cumulus-pallet-aura-ext/try-runtime", "cumulus-pallet-parachain-system/try-runtime", "cumulus-pallet-weight-reclaim/try-runtime", diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml b/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml index c3a0683b9d4df..818942b3e83a3 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml @@ -108,7 +108,9 @@ snowbridge-pallet-system-frontend = { workspace = true } snowbridge-runtime-common = { workspace = true } [dev-dependencies] +alloy-core = { workspace = true, features = ["sol-types"] } asset-test-utils = { workspace = true, default-features = true } +pallet-revive-fixtures = { workspace = true, default-features = true } parachains-runtimes-test-utils = { workspace = true, default-features = true } [build-dependencies] @@ -163,6 +165,7 @@ runtime-benchmarks = [ "xcm/runtime-benchmarks", ] try-runtime = [ + "assets-common/try-runtime", "cumulus-pallet-aura-ext/try-runtime", "cumulus-pallet-parachain-system/try-runtime", "cumulus-pallet-weight-reclaim/try-runtime", @@ -204,6 +207,7 @@ try-runtime = [ "sp-runtime/try-runtime", ] std = [ + "alloy-core/std", "assets-common/std", "bp-asset-hub-rococo/std", "bp-asset-hub-westend/std", diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/xcm/mod.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/xcm/mod.rs index a6b5d3d0fd08a..27532ac431e7a 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/xcm/mod.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/xcm/mod.rs @@ -16,9 +16,13 @@ mod pallet_xcm_benchmarks_fungible; mod pallet_xcm_benchmarks_generic; -use crate::{xcm_config::MaxAssetsIntoHolding, Runtime}; +use crate::{ + xcm_config::{ERC20TransferGasLimit, MaxAssetsIntoHolding}, + Runtime, +}; use alloc::vec::Vec; -use frame_support::weights::Weight; +use assets_common::IsLocalAccountKey20; +use frame_support::{traits::Contains, weights::Weight}; use pallet_xcm_benchmarks_fungible::WeightInfo as XcmFungibleWeight; use pallet_xcm_benchmarks_generic::WeightInfo as XcmGeneric; use sp_runtime::BoundedVec; @@ -54,9 +58,33 @@ impl WeighAssets for AssetFilter { } } +trait WeighAsset { + /// Return one worst-case estimate: `weight`, or another. + fn weigh_asset(&self, weight: Weight) -> Weight; +} + +impl WeighAsset for Asset { + fn weigh_asset(&self, weight: Weight) -> Weight { + // If the asset is a smart contract ERC20, then we know the gas limit, + // else we return the weight that was passed in, that's already + // the worst case for non-ERC20 assets. + if IsLocalAccountKey20::contains(&self.id.0) { + ERC20TransferGasLimit::get() + } else { + weight + } + } +} + impl WeighAssets for Assets { fn weigh_assets(&self, weight: Weight) -> Weight { - weight.saturating_mul(self.inner().iter().count() as u64) + // We start with zero. + let mut final_weight = Weight::zero(); + // For each asset, we add weight depending on the type of asset. + for asset in self.inner().iter() { + final_weight = final_weight.saturating_add(asset.weigh_asset(weight)); + } + final_weight } } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs index 5187e0e69a4fb..b6a53ad2612b8 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs @@ -30,6 +30,7 @@ use frame_support::{ tokens::imbalance::{ResolveAssetTo, ResolveTo}, ConstU32, Contains, Equals, Everything, LinearStoragePrice, PalletInfoAccess, }, + PalletId, }; use frame_system::EnsureRoot; use pallet_xcm::{AuthorizedAliasers, XcmPassthrough}; @@ -209,6 +210,31 @@ pub type PoolFungiblesTransactor = FungiblesAdapter< CheckingAccount, >; +parameter_types! { + /// Taken from the real gas and deposits of a standard ERC20 transfer call. + pub const ERC20TransferGasLimit: Weight = Weight::from_parts(700_000_000, 200_000); + pub const ERC20TransferStorageDepositLimit: Balance = 10_200_000_000; + pub ERC20TransfersCheckingAccount: AccountId = PalletId(*b"py/revch").into_account_truncating(); +} + +/// Transactor for ERC20 tokens. +pub type ERC20Transactor = assets_common::ERC20Transactor< + // We need this for accessing pallet-revive. + Runtime, + // The matcher for smart contracts. + assets_common::ERC20Matcher, + // How to convert from a location to an account id. + LocationToAccountId, + // The maximum gas that can be used by a standard ERC20 transfer. + ERC20TransferGasLimit, + // The maximum storage deposit that can be used by a standard ERC20 transfer. + ERC20TransferStorageDepositLimit, + // We're generic over this so we can't escape specifying it. + AccountId, + // Checking account for ERC20 transfers. + ERC20TransfersCheckingAccount, +>; + /// Means for transacting assets on this chain. pub type AssetTransactors = ( FungibleTransactor, @@ -216,6 +242,7 @@ pub type AssetTransactors = ( ForeignFungiblesTransactor, PoolFungiblesTransactor, UniquesTransactor, + ERC20Transactor, ); /// This is the type we use to convert an (incoming) XCM origin into a local `Origin` instance, diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/tests/tests.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/tests/tests.rs index 6a6a62f6f7f81..305f248f2e840 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/tests/tests.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/tests/tests.rs @@ -17,6 +17,10 @@ //! Tests for the Westmint (Westend Assets Hub) chain. +use alloy_core::{ + primitives::U256, + sol_types::{sol_data, SolType}, +}; use asset_hub_westend_runtime::{ xcm_config, xcm_config::{ @@ -25,7 +29,7 @@ use asset_hub_westend_runtime::{ }, AllPalletsWithoutSystem, Assets, Balances, Block, ExistentialDeposit, ForeignAssets, ForeignAssetsInstance, MetadataDepositBase, MetadataDepositPerByte, ParachainSystem, - PolkadotXcm, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, SessionKeys, + PolkadotXcm, Revive, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, SessionKeys, ToRococoXcmRouterInstance, TrustBackedAssetsInstance, XcmpQueue, }; pub use asset_hub_westend_runtime::{AssetConversion, AssetDeposit, CollatorSelection, System}; @@ -37,30 +41,36 @@ use codec::{Decode, Encode}; use frame_support::{ assert_err, assert_noop, assert_ok, parameter_types, traits::{ - fungible::{Inspect, Mutate}, + fungible::{self, Inspect, Mutate}, fungibles::{ - Create, Inspect as FungiblesInspect, InspectEnumerable, Mutate as FungiblesMutate, + self, Create, Inspect as FungiblesInspect, InspectEnumerable, Mutate as FungiblesMutate, }, ContainsPair, }, weights::{Weight, WeightToFee as WeightToFeeT}, }; use hex_literal::hex; +use pallet_revive::{Code, DepositLimit, InstantiateReturnValue, NonceAlreadyIncremented}; +use pallet_revive_fixtures::compile_module; use parachains_common::{AccountId, AssetIdForTrustBackedAssets, AuraId, Balance}; use sp_consensus_aura::SlotDuration; use sp_core::crypto::Ss58Codec; use sp_runtime::{traits::MaybeEquivalence, Either}; use std::convert::Into; use testnet_parachains_constants::westend::{consensus::*, currency::UNITS, fee::WeightToFee}; -use xcm::latest::{ - prelude::{Assets as XcmAssets, *}, - ROCOCO_GENESIS_HASH, +use xcm::{ + latest::{ + prelude::{Assets as XcmAssets, *}, + ROCOCO_GENESIS_HASH, + }, + VersionedXcm, }; use xcm_builder::WithLatestLocationConverter; use xcm_executor::traits::{ConvertLocation, JustTry, WeightTrader}; use xcm_runtime_apis::conversions::LocationToAccountHelper; const ALICE: [u8; 32] = [1u8; 32]; +const BOB: [u8; 32] = [2u8; 32]; const SOME_ASSET_ADMIN: [u8; 32] = [5u8; 32]; parameter_types! { @@ -1447,3 +1457,328 @@ fn governance_authorize_upgrade_works() { RuntimeOrigin, >(GovernanceOrigin::Location(GovernanceLocation::get()))); } + +#[test] +fn weight_of_message_increases_when_dealing_with_erc20s() { + use xcm::VersionedXcm; + use xcm_runtime_apis::fees::runtime_decl_for_xcm_payment_api::XcmPaymentApiV1; + let message = Xcm::<()>::builder_unsafe().withdraw_asset((Parent, 100u128)).build(); + let versioned = VersionedXcm::<()>::V5(message); + let regular_asset_weight = Runtime::query_xcm_weight(versioned).unwrap(); + + let message = Xcm::<()>::builder_unsafe() + .withdraw_asset((AccountKey20 { network: None, key: [1u8; 20] }, 100u128)) + .build(); + let versioned = VersionedXcm::<()>::V5(message); + let weight = Runtime::query_xcm_weight(versioned).unwrap(); + assert!( + weight.ref_time() > regular_asset_weight.ref_time() + // The proof size really blows up. + && weight.proof_size() > 10 * regular_asset_weight.proof_size() + ); + assert_eq!(weight, crate::xcm_config::ERC20TransferGasLimit::get()); +} + +#[test] +fn withdraw_and_deposit_erc20s() { + let sender: AccountId = ALICE.into(); + let beneficiary: AccountId = BOB.into(); + let checking_account = + asset_hub_westend_runtime::xcm_config::ERC20TransfersCheckingAccount::get(); + let initial_wnd_amount = 10_000_000_000_000u128; + + ExtBuilder::::default().build().execute_with(|| { + // We need to give enough funds for every account involved so they + // can call `Revive::map_account`. + assert_ok!(Balances::mint_into(&sender, initial_wnd_amount)); + assert_ok!(Balances::mint_into(&beneficiary, initial_wnd_amount)); + assert_ok!(Balances::mint_into(&checking_account, initial_wnd_amount)); + + // We need to map all accounts. + assert_ok!(Revive::map_account(RuntimeOrigin::signed(checking_account.clone()))); + assert_ok!(Revive::map_account(RuntimeOrigin::signed(sender.clone()))); + assert_ok!(Revive::map_account(RuntimeOrigin::signed(beneficiary.clone()))); + + let code = include_bytes!( + "../../../../../../substrate/frame/revive/fixtures/contracts/erc20.polkavm" + ) + .to_vec(); + + let initial_amount_u256 = U256::from(1_000_000_000_000u128); + let constructor_data = sol_data::Uint::<256>::abi_encode(&initial_amount_u256); + let result = Revive::bare_instantiate( + RuntimeOrigin::signed(sender.clone()), + 0, + Weight::from_parts(2_000_000_000, 200_000), + DepositLimit::Balance(Balance::MAX), + Code::Upload(code), + constructor_data, + None, + NonceAlreadyIncremented::Yes, + ); + let Ok(InstantiateReturnValue { addr: erc20_address, .. }) = result.result else { + unreachable!("contract should initialize") + }; + + let sender_balance_before = >::balance(&sender); + + let erc20_transfer_amount = 100u128; + let wnd_amount_for_fees = 1_000_000_000_000u128; + // Actual XCM to execute locally. + let message = Xcm::::builder() + .withdraw_asset((Parent, wnd_amount_for_fees)) + .pay_fees((Parent, wnd_amount_for_fees)) + .withdraw_asset(( + AccountKey20 { key: erc20_address.into(), network: None }, + erc20_transfer_amount, + )) + .deposit_asset(AllCounted(1), beneficiary.clone()) + .refund_surplus() + .deposit_asset(AllCounted(1), sender.clone()) + .build(); + assert_ok!(PolkadotXcm::execute( + RuntimeOrigin::signed(sender.clone()), + Box::new(VersionedXcm::V5(message)), + Weight::from_parts(2_500_000_000, 220_000), + )); + + // Revive is not taking any fees. + let sender_balance_after = >::balance(&sender); + // Balance after is larger than the difference between balance before and transferred + // amount because of the refund. + assert!(sender_balance_after > sender_balance_before - wnd_amount_for_fees); + + // Beneficiary receives the ERC20. + let beneficiary_amount = + >::balance(erc20_address, &beneficiary); + assert_eq!(beneficiary_amount, erc20_transfer_amount); + }); +} + +#[test] +fn non_existent_erc20_will_error() { + let sender: AccountId = ALICE.into(); + let beneficiary: AccountId = BOB.into(); + let checking_account = + asset_hub_westend_runtime::xcm_config::ERC20TransfersCheckingAccount::get(); + let initial_wnd_amount = 10_000_000_000_000u128; + // We try to withdraw an ERC20 token but the address doesn't exist. + let non_existent_contract_address = [1u8; 20]; + + ExtBuilder::::default().build().execute_with(|| { + // We need to give enough funds for every account involved so they + // can call `Revive::map_account`. + assert_ok!(Balances::mint_into(&sender, initial_wnd_amount)); + assert_ok!(Balances::mint_into(&beneficiary, initial_wnd_amount)); + assert_ok!(Balances::mint_into(&checking_account, initial_wnd_amount)); + + // We need to map all accounts. + assert_ok!(Revive::map_account(RuntimeOrigin::signed(checking_account.clone()))); + assert_ok!(Revive::map_account(RuntimeOrigin::signed(sender.clone()))); + assert_ok!(Revive::map_account(RuntimeOrigin::signed(beneficiary.clone()))); + + let wnd_amount_for_fees = 1_000_000_000_000u128; + let erc20_transfer_amount = 100u128; + let message = Xcm::::builder() + .withdraw_asset((Parent, wnd_amount_for_fees)) + .pay_fees((Parent, wnd_amount_for_fees)) + .withdraw_asset(( + AccountKey20 { key: non_existent_contract_address, network: None }, + erc20_transfer_amount, + )) + .deposit_asset(AllCounted(1), beneficiary.clone()) + .build(); + // Execution fails but doesn't panic. + assert!(PolkadotXcm::execute( + RuntimeOrigin::signed(sender.clone()), + Box::new(VersionedXcm::V5(message)), + Weight::from_parts(2_500_000_000, 120_000), + ) + .is_err()); + }); +} + +#[test] +fn smart_contract_not_erc20_will_error() { + let sender: AccountId = ALICE.into(); + let beneficiary: AccountId = BOB.into(); + let checking_account = + asset_hub_westend_runtime::xcm_config::ERC20TransfersCheckingAccount::get(); + let initial_wnd_amount = 10_000_000_000_000u128; + + ExtBuilder::::default().build().execute_with(|| { + // We need to give enough funds for every account involved so they + // can call `Revive::map_account`. + assert_ok!(Balances::mint_into(&sender, initial_wnd_amount)); + assert_ok!(Balances::mint_into(&beneficiary, initial_wnd_amount)); + assert_ok!(Balances::mint_into(&checking_account, initial_wnd_amount)); + + // We need to map all accounts. + assert_ok!(Revive::map_account(RuntimeOrigin::signed(checking_account.clone()))); + assert_ok!(Revive::map_account(RuntimeOrigin::signed(sender.clone()))); + assert_ok!(Revive::map_account(RuntimeOrigin::signed(beneficiary.clone()))); + + let (code, _) = compile_module("dummy").unwrap(); + + let result = Revive::bare_instantiate( + RuntimeOrigin::signed(sender.clone()), + 0, + Weight::from_parts(2_000_000_000, 200_000), + DepositLimit::Balance(Balance::MAX), + Code::Upload(code), + Vec::new(), + None, + NonceAlreadyIncremented::Yes, + ); + let Ok(InstantiateReturnValue { addr: non_erc20_address, .. }) = result.result else { + unreachable!("contract should initialize") + }; + + let wnd_amount_for_fees = 1_000_000_000_000u128; + let erc20_transfer_amount = 100u128; + let message = Xcm::::builder() + .withdraw_asset((Parent, wnd_amount_for_fees)) + .pay_fees((Parent, wnd_amount_for_fees)) + .withdraw_asset(( + AccountKey20 { key: non_erc20_address.into(), network: None }, + erc20_transfer_amount, + )) + .deposit_asset(AllCounted(1), beneficiary.clone()) + .build(); + // Execution fails but doesn't panic. + assert!(PolkadotXcm::execute( + RuntimeOrigin::signed(sender.clone()), + Box::new(VersionedXcm::V5(message)), + Weight::from_parts(2_500_000_000, 120_000), + ) + .is_err()); + }); +} + +// Here the contract returns a number but because it can be cast to true +// it still succeeds. +#[test] +fn smart_contract_does_not_return_bool_fails() { + let sender: AccountId = ALICE.into(); + let beneficiary: AccountId = BOB.into(); + let checking_account = + asset_hub_westend_runtime::xcm_config::ERC20TransfersCheckingAccount::get(); + let initial_wnd_amount = 10_000_000_000_000u128; + + ExtBuilder::::default().build().execute_with(|| { + // We need to give enough funds for every account involved so they + // can call `Revive::map_account`. + assert_ok!(Balances::mint_into(&sender, initial_wnd_amount)); + assert_ok!(Balances::mint_into(&beneficiary, initial_wnd_amount)); + assert_ok!(Balances::mint_into(&checking_account, initial_wnd_amount)); + + // We need to map all accounts. + assert_ok!(Revive::map_account(RuntimeOrigin::signed(checking_account.clone()))); + assert_ok!(Revive::map_account(RuntimeOrigin::signed(sender.clone()))); + assert_ok!(Revive::map_account(RuntimeOrigin::signed(beneficiary.clone()))); + + // This contract implements the ERC20 interface for `transfer` except it returns a uint256. + let code = include_bytes!( + "../../../../../../substrate/frame/revive/fixtures/contracts/fake_erc20.polkavm" + ) + .to_vec(); + + let initial_amount_u256 = U256::from(1_000_000_000_000u128); + let constructor_data = sol_data::Uint::<256>::abi_encode(&initial_amount_u256); + let result = Revive::bare_instantiate( + RuntimeOrigin::signed(sender.clone()), + 0, + Weight::from_parts(2_000_000_000, 200_000), + DepositLimit::Balance(Balance::MAX), + Code::Upload(code), + constructor_data, + None, + NonceAlreadyIncremented::Yes, + ); + let Ok(InstantiateReturnValue { addr: non_erc20_address, .. }) = result.result else { + unreachable!("contract should initialize") + }; + + let wnd_amount_for_fees = 1_000_000_000_000u128; + let erc20_transfer_amount = 100u128; + let message = Xcm::::builder() + .withdraw_asset((Parent, wnd_amount_for_fees)) + .pay_fees((Parent, wnd_amount_for_fees)) + .withdraw_asset(( + AccountKey20 { key: non_erc20_address.into(), network: None }, + erc20_transfer_amount, + )) + .deposit_asset(AllCounted(1), beneficiary.clone()) + .build(); + // Execution fails but doesn't panic. + assert!(PolkadotXcm::execute( + RuntimeOrigin::signed(sender.clone()), + Box::new(VersionedXcm::V5(message)), + Weight::from_parts(2_500_000_000, 220_000), + ) + .is_err()); + }); +} + +#[test] +fn expensive_erc20_runs_out_of_gas() { + let sender: AccountId = ALICE.into(); + let beneficiary: AccountId = BOB.into(); + let checking_account = + asset_hub_westend_runtime::xcm_config::ERC20TransfersCheckingAccount::get(); + let initial_wnd_amount = 10_000_000_000_000u128; + + ExtBuilder::::default().build().execute_with(|| { + // We need to give enough funds for every account involved so they + // can call `Revive::map_account`. + assert_ok!(Balances::mint_into(&sender, initial_wnd_amount)); + assert_ok!(Balances::mint_into(&beneficiary, initial_wnd_amount)); + assert_ok!(Balances::mint_into(&checking_account, initial_wnd_amount)); + + // We need to map all accounts. + assert_ok!(Revive::map_account(RuntimeOrigin::signed(checking_account.clone()))); + assert_ok!(Revive::map_account(RuntimeOrigin::signed(sender.clone()))); + assert_ok!(Revive::map_account(RuntimeOrigin::signed(beneficiary.clone()))); + + // This contract does a lot more storage writes in `transfer`. + let code = include_bytes!( + "../../../../../../substrate/frame/revive/fixtures/contracts/expensive_erc20.polkavm" + ) + .to_vec(); + + let initial_amount_u256 = U256::from(1_000_000_000_000u128); + let constructor_data = sol_data::Uint::<256>::abi_encode(&initial_amount_u256); + let result = Revive::bare_instantiate( + RuntimeOrigin::signed(sender.clone()), + 0, + Weight::from_parts(2_000_000_000, 200_000), + DepositLimit::Balance(Balance::MAX), + Code::Upload(code), + constructor_data, + None, + NonceAlreadyIncremented::Yes, + ); + let Ok(InstantiateReturnValue { addr: non_erc20_address, .. }) = result.result else { + unreachable!("contract should initialize") + }; + + let wnd_amount_for_fees = 1_000_000_000_000u128; + let erc20_transfer_amount = 100u128; + let message = Xcm::::builder() + .withdraw_asset((Parent, wnd_amount_for_fees)) + .pay_fees((Parent, wnd_amount_for_fees)) + .withdraw_asset(( + AccountKey20 { key: non_erc20_address.into(), network: None }, + erc20_transfer_amount, + )) + .deposit_asset(AllCounted(1), beneficiary.clone()) + .build(); + // Execution fails but doesn't panic. + assert!(PolkadotXcm::execute( + RuntimeOrigin::signed(sender.clone()), + Box::new(VersionedXcm::V5(message)), + Weight::from_parts(2_500_000_000, 120_000), + ) + .is_err()); + }); +} diff --git a/cumulus/parachains/runtimes/assets/common/Cargo.toml b/cumulus/parachains/runtimes/assets/common/Cargo.toml index 92b582d52972b..c64a9c1875b7f 100644 --- a/cumulus/parachains/runtimes/assets/common/Cargo.toml +++ b/cumulus/parachains/runtimes/assets/common/Cargo.toml @@ -19,9 +19,13 @@ tracing = { workspace = true } # Substrate frame-support = { workspace = true } +frame-system = { workspace = true } pallet-asset-conversion = { workspace = true } pallet-assets = { workspace = true } +pallet-revive = { workspace = true } +pallet-revive-uapi = { workspace = true, features = ["scale"] } sp-api = { workspace = true } +sp-core = { workspace = true } sp-runtime = { workspace = true } # Polkadot @@ -32,6 +36,7 @@ xcm-executor = { workspace = true } # Cumulus cumulus-primitives-core = { workspace = true } +ethereum-standards = { workspace = true } parachains-common = { workspace = true } [features] @@ -40,12 +45,15 @@ std = [ "codec/std", "cumulus-primitives-core/std", "frame-support/std", + "frame-system/std", "pallet-asset-conversion/std", "pallet-assets/std", + "pallet-revive/std", "pallet-xcm/std", "parachains-common/std", "scale-info/std", "sp-api/std", + "sp-core/std", "sp-runtime/std", "tracing/std", "xcm-builder/std", @@ -55,8 +63,10 @@ std = [ runtime-benchmarks = [ "cumulus-primitives-core/runtime-benchmarks", "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", "pallet-asset-conversion/runtime-benchmarks", "pallet-assets/runtime-benchmarks", + "pallet-revive/runtime-benchmarks", "pallet-xcm/runtime-benchmarks", "parachains-common/runtime-benchmarks", "sp-runtime/runtime-benchmarks", @@ -64,3 +74,12 @@ runtime-benchmarks = [ "xcm-executor/runtime-benchmarks", "xcm/runtime-benchmarks", ] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-asset-conversion/try-runtime", + "pallet-assets/try-runtime", + "pallet-revive/try-runtime", + "pallet-xcm/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/cumulus/parachains/runtimes/assets/common/src/erc20_transactor.rs b/cumulus/parachains/runtimes/assets/common/src/erc20_transactor.rs new file mode 100644 index 0000000000000..ec4e3adad3bae --- /dev/null +++ b/cumulus/parachains/runtimes/assets/common/src/erc20_transactor.rs @@ -0,0 +1,222 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Cumulus. +// 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 ERC20 Asset Transactor. + +use core::marker::PhantomData; +use ethereum_standards::IERC20; +use frame_support::{ + pallet_prelude::Zero, + traits::{fungible::Inspect, OriginTrait}, +}; +use pallet_revive::{ + precompiles::alloy::{ + primitives::{Address, U256 as EU256}, + sol_types::SolCall, + }, + AddressMapper, ContractResult, DepositLimit, MomentOf, +}; +use sp_core::{Get, H160, H256, U256}; +use sp_runtime::Weight; +use xcm::latest::prelude::*; +use xcm_executor::{ + traits::{ConvertLocation, Error as MatchError, MatchesFungibles, TransactAsset}, + AssetsInHolding, +}; + +type BalanceOf = <::Currency as Inspect< + ::AccountId, +>>::Balance; + +/// An Asset Transactor that deals with ERC20 tokens. +pub struct ERC20Transactor< + T, + Matcher, + AccountIdConverter, + GasLimit, + StorageDepositLimit, + AccountId, + TransfersCheckingAccount, +>( + PhantomData<( + T, + Matcher, + AccountIdConverter, + GasLimit, + StorageDepositLimit, + AccountId, + TransfersCheckingAccount, + )>, +); + +impl< + AccountId: Eq + Clone, + T: pallet_revive::Config, + AccountIdConverter: ConvertLocation, + Matcher: MatchesFungibles, + GasLimit: Get, + StorageDepositLimit: Get>, + TransfersCheckingAccount: Get, + > TransactAsset + for ERC20Transactor< + T, + Matcher, + AccountIdConverter, + GasLimit, + StorageDepositLimit, + AccountId, + TransfersCheckingAccount, + > +where + BalanceOf: Into + TryFrom, + MomentOf: Into, + T::Hash: frame_support::traits::IsType, +{ + fn can_check_in(_origin: &Location, _what: &Asset, _context: &XcmContext) -> XcmResult { + // We don't support teleports. + Err(XcmError::Unimplemented) + } + + fn check_in(_origin: &Location, _what: &Asset, _context: &XcmContext) { + // We don't support teleports. + } + + fn can_check_out(_destination: &Location, _what: &Asset, _context: &XcmContext) -> XcmResult { + // We don't support teleports. + Err(XcmError::Unimplemented) + } + + fn check_out(_destination: &Location, _what: &Asset, _context: &XcmContext) { + // We don't support teleports. + } + + fn withdraw_asset_with_surplus( + what: &Asset, + who: &Location, + _context: Option<&XcmContext>, + ) -> Result<(AssetsInHolding, Weight), XcmError> { + tracing::trace!( + target: "xcm::transactor::erc20::withdraw", + ?what, ?who, + ); + let (asset_id, amount) = Matcher::matches_fungibles(what)?; + let who = AccountIdConverter::convert_location(who) + .ok_or(MatchError::AccountIdConversionFailed)?; + // We need to map the 32 byte checking account to a 20 byte account. + let checking_account_eth = T::AddressMapper::to_address(&TransfersCheckingAccount::get()); + let checking_address = Address::from(Into::<[u8; 20]>::into(checking_account_eth)); + let gas_limit = GasLimit::get(); + // To withdraw, we actually transfer to the checking account. + // We do this using the solidity ERC20 interface. + let data = + IERC20::transferCall { to: checking_address, value: EU256::from(amount) }.abi_encode(); + let ContractResult { result, gas_consumed, storage_deposit, .. } = + pallet_revive::Pallet::::bare_call( + T::RuntimeOrigin::signed(who.clone()), + asset_id, + BalanceOf::::zero(), + gas_limit, + DepositLimit::Balance(StorageDepositLimit::get()), + data, + ); + // We need to return this surplus for the executor to allow refunding it. + let surplus = gas_limit.saturating_sub(gas_consumed); + tracing::trace!(target: "xcm::transactor::erc20::withdraw", ?gas_consumed, ?surplus, ?storage_deposit); + if let Ok(return_value) = result { + tracing::trace!(target: "xcm::transactor::erc20::withdraw", ?return_value, "Return value by withdraw_asset"); + if return_value.did_revert() { + tracing::debug!(target: "xcm::transactor::erc20::withdraw", "ERC20 contract reverted"); + Err(XcmError::FailedToTransactAsset("ERC20 contract reverted")) + } else { + let is_success = IERC20::transferCall::abi_decode_returns_validate(&return_value.data).map_err(|error| { + tracing::debug!(target: "xcm::transactor::erc20::withdraw", ?error, "ERC20 contract result couldn't decode"); + XcmError::FailedToTransactAsset("ERC20 contract result couldn't decode") + })?; + if is_success { + tracing::trace!(target: "xcm::transactor::erc20::withdraw", "ERC20 contract was successful"); + Ok((what.clone().into(), surplus)) + } else { + tracing::debug!(target: "xcm::transactor::erc20::withdraw", "contract transfer failed"); + Err(XcmError::FailedToTransactAsset("ERC20 contract transfer failed")) + } + } + } else { + tracing::debug!(target: "xcm::transactor::erc20::withdraw", ?result, "Error"); + // This error could've been duplicate smart contract, out of gas, etc. + // If the issue is gas, there's nothing the user can change in the XCM + // that will make this work since there's a hardcoded gas limit. + Err(XcmError::FailedToTransactAsset("ERC20 contract execution errored")) + } + } + + fn deposit_asset_with_surplus( + what: &Asset, + who: &Location, + _context: Option<&XcmContext>, + ) -> Result { + tracing::trace!( + target: "xcm::transactor::erc20::deposit", + ?what, ?who, + ); + let (asset_id, amount) = Matcher::matches_fungibles(what)?; + let who = AccountIdConverter::convert_location(who) + .ok_or(MatchError::AccountIdConversionFailed)?; + // We need to map the 32 byte beneficiary account to a 20 byte account. + let eth_address = T::AddressMapper::to_address(&who); + let address = Address::from(Into::<[u8; 20]>::into(eth_address)); + // To deposit, we actually transfer from the checking account to the beneficiary. + // We do this using the solidity ERC20 interface. + let data = IERC20::transferCall { to: address, value: EU256::from(amount) }.abi_encode(); + let gas_limit = GasLimit::get(); + let ContractResult { result, gas_consumed, storage_deposit, .. } = + pallet_revive::Pallet::::bare_call( + T::RuntimeOrigin::signed(TransfersCheckingAccount::get()), + asset_id, + BalanceOf::::zero(), + gas_limit, + DepositLimit::Balance(StorageDepositLimit::get()), + data, + ); + // We need to return this surplus for the executor to allow refunding it. + let surplus = gas_limit.saturating_sub(gas_consumed); + tracing::trace!(target: "xcm::transactor::erc20::deposit", ?gas_consumed, ?surplus, ?storage_deposit); + if let Ok(return_value) = result { + tracing::trace!(target: "xcm::transactor::erc20::deposit", ?return_value, "Return value"); + if return_value.did_revert() { + tracing::debug!(target: "xcm::transactor::erc20::deposit", "Contract reverted"); + Err(XcmError::FailedToTransactAsset("ERC20 contract reverted")) + } else { + let is_success = IERC20::transferCall::abi_decode_returns_validate(&return_value.data).map_err(|error| { + tracing::debug!(target: "xcm::transactor::erc20::deposit", ?error, "ERC20 contract result couldn't decode"); + XcmError::FailedToTransactAsset("ERC20 contract result couldn't decode") + })?; + if is_success { + tracing::trace!(target: "xcm::transactor::erc20::deposit", "ERC20 contract was successful"); + Ok(surplus) + } else { + tracing::debug!(target: "xcm::transactor::erc20::deposit", "contract transfer failed"); + Err(XcmError::FailedToTransactAsset("ERC20 contract transfer failed")) + } + } + } else { + tracing::debug!(target: "xcm::transactor::erc20::deposit", ?result, "Error"); + // This error could've been duplicate smart contract, out of gas, etc. + // If the issue is gas, there's nothing the user can change in the XCM + // that will make this work since there's a hardcoded gas limit. + Err(XcmError::FailedToTransactAsset("ERC20 contract execution errored")) + } + } +} diff --git a/cumulus/parachains/runtimes/assets/common/src/lib.rs b/cumulus/parachains/runtimes/assets/common/src/lib.rs index da024b1c94562..87c173a990bfd 100644 --- a/cumulus/parachains/runtimes/assets/common/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/common/src/lib.rs @@ -17,11 +17,13 @@ #[cfg(feature = "runtime-benchmarks")] pub mod benchmarks; +mod erc20_transactor; pub mod foreign_creators; pub mod fungible_conversion; pub mod local_and_foreign_assets; pub mod matching; pub mod runtime_api; +pub use erc20_transactor::ERC20Transactor; extern crate alloc; extern crate core; @@ -30,13 +32,15 @@ use crate::matching::{LocalLocationPattern, ParentLocation}; use alloc::vec::Vec; use codec::{Decode, EncodeLike}; use core::{cmp::PartialEq, marker::PhantomData}; -use frame_support::traits::{Equals, EverythingBut}; +use frame_support::traits::{Contains, Equals, EverythingBut}; use parachains_common::{AssetIdForTrustBackedAssets, CollectionId, ItemId}; -use sp_runtime::traits::TryConvertInto; +use sp_core::H160; +use sp_runtime::traits::{MaybeEquivalence, TryConvertInto}; use xcm::prelude::*; use xcm_builder::{ AsPrefixedGeneralIndex, MatchedConvertedConcreteId, StartsWith, WithLatestLocationConverter, }; +use xcm_executor::traits::JustTry; /// `Location` vs `AssetIdForTrustBackedAssets` converter for `TrustBackedAssets` pub type AssetIdForTrustBackedAssetsConvert = @@ -124,6 +128,36 @@ pub type ForeignAssetsConvertedConcreteId< BalanceConverter, >; +/// `Contains` implementation that matches locations with no parents, +/// a `PalletInstance` and an `AccountKey20` junction. +pub struct IsLocalAccountKey20; +impl Contains for IsLocalAccountKey20 { + fn contains(location: &Location) -> bool { + matches!(location.unpack(), (0, [AccountKey20 { .. }])) + } +} + +/// Fallible converter from a location to a `H160` that matches any location ending with +/// an `AccountKey20` junction. +pub struct AccountKey20ToH160; +impl MaybeEquivalence for AccountKey20ToH160 { + fn convert(location: &Location) -> Option { + match location.unpack() { + (0, [AccountKey20 { key, .. }]) => Some((*key).into()), + _ => None, + } + } + + fn convert_back(key: &H160) -> Option { + Some(Location::new(0, [AccountKey20 { key: (*key).into(), network: None }])) + } +} + +/// [`xcm_executor::traits::MatchesFungibles`] implementation that matches +/// ERC20 tokens. +pub type ERC20Matcher = + MatchedConvertedConcreteId; + pub type AssetIdForPoolAssets = u32; /// `Location` vs `AssetIdForPoolAssets` converter for `PoolAssets`. diff --git a/cumulus/parachains/runtimes/testing/penpal/Cargo.toml b/cumulus/parachains/runtimes/testing/penpal/Cargo.toml index 16838b9ed126a..95a68b3864577 100644 --- a/cumulus/parachains/runtimes/testing/penpal/Cargo.toml +++ b/cumulus/parachains/runtimes/testing/penpal/Cargo.toml @@ -185,6 +185,7 @@ runtime-benchmarks = [ ] try-runtime = [ + "assets-common/try-runtime", "cumulus-pallet-aura-ext/try-runtime", "cumulus-pallet-parachain-system/try-runtime", "cumulus-pallet-xcm/try-runtime", diff --git a/polkadot/xcm/xcm-executor/src/lib.rs b/polkadot/xcm/xcm-executor/src/lib.rs index a14c417217634..eb3f7718908e8 100644 --- a/polkadot/xcm/xcm-executor/src/lib.rs +++ b/polkadot/xcm/xcm-executor/src/lib.rs @@ -898,20 +898,25 @@ impl XcmExecutor { WithdrawAsset(assets) => { let origin = self.origin_ref().ok_or(XcmError::BadOrigin)?; self.ensure_can_subsume_assets(assets.len())?; + let mut total_surplus = Weight::zero(); Config::TransactionalProcessor::process(|| { // Take `assets` from the origin account (on-chain)... for asset in assets.inner() { - Config::AssetTransactor::withdraw_asset( + let (_, surplus) = Config::AssetTransactor::withdraw_asset_with_surplus( asset, origin, Some(&self.context), )?; + // If we have some surplus, aggregate it. + total_surplus.saturating_accrue(surplus); } Ok(()) }) .and_then(|_| { // ...and place into holding. self.holding.subsume_assets(assets.into()); + // Credit the total surplus. + self.total_surplus.saturating_accrue(total_surplus); Ok(()) }) }, @@ -933,28 +938,36 @@ impl XcmExecutor { Config::TransactionalProcessor::process(|| { // Take `assets` from the origin account (on-chain) and place into dest account. let origin = self.origin_ref().ok_or(XcmError::BadOrigin)?; + let mut total_surplus = Weight::zero(); for asset in assets.inner() { - Config::AssetTransactor::transfer_asset( + let (_, surplus) = Config::AssetTransactor::transfer_asset_with_surplus( &asset, origin, &beneficiary, &self.context, )?; + // If we have some surplus, aggregate it. + total_surplus.saturating_accrue(surplus); } + // Credit the total surplus. + self.total_surplus.saturating_accrue(total_surplus); Ok(()) }) }, TransferReserveAsset { mut assets, dest, xcm } => { Config::TransactionalProcessor::process(|| { let origin = self.origin_ref().ok_or(XcmError::BadOrigin)?; + let mut total_surplus = Weight::zero(); // Take `assets` from the origin account (on-chain) and place into dest account. for asset in assets.inner() { - Config::AssetTransactor::transfer_asset( + let (_, surplus) = Config::AssetTransactor::transfer_asset_with_surplus( asset, origin, &dest, &self.context, )?; + // If we have some surplus, aggregate it. + total_surplus.saturating_accrue(surplus); } let reanchor_context = Config::UniversalLocation::get(); assets @@ -963,6 +976,8 @@ impl XcmExecutor { let mut message = vec![ReserveAssetDeposited(assets), ClearOrigin]; message.extend(xcm.0.into_iter()); self.send(dest, Xcm(message), FeeReason::TransferReserveAsset)?; + // Credit the total surplus. + self.total_surplus.saturating_accrue(total_surplus); Ok(()) }) }, @@ -1113,7 +1128,9 @@ impl XcmExecutor { let old_holding = self.holding.clone(); let result = Config::TransactionalProcessor::process(|| { let deposited = self.holding.saturating_take(assets); - Self::deposit_assets_with_retry(&deposited, &beneficiary, Some(&self.context)) + let surplus = Self::deposit_assets_with_retry(&deposited, &beneficiary, Some(&self.context))?; + self.total_surplus.saturating_accrue(surplus); + Ok(()) }); if Config::TransactionalProcessor::IS_TRANSACTIONAL && result.is_err() { self.holding = old_holding; @@ -1763,36 +1780,46 @@ impl XcmExecutor { to_deposit: &AssetsInHolding, beneficiary: &Location, context: Option<&XcmContext>, - ) -> Result<(), XcmError> { + ) -> Result { + let mut total_surplus = Weight::zero(); let mut failed_deposits = Vec::with_capacity(to_deposit.len()); - for asset in to_deposit.assets_iter() { - // if deposit failed for asset, mark it for retry. - if Config::AssetTransactor::deposit_asset(&asset, &beneficiary, context).is_err() { - failed_deposits.push(asset); + match Config::AssetTransactor::deposit_asset_with_surplus(&asset, &beneficiary, context) + { + Ok(surplus) => { + total_surplus.saturating_accrue(surplus); + }, + Err(_) => { + // if deposit failed for asset, mark it for retry. + failed_deposits.push(asset); + }, } } - tracing::trace!( target: "xcm::deposit_assets_with_retry", ?failed_deposits, "First‐pass failures, about to retry" ); - // retry previously failed deposits, this time short-circuiting on any error. for asset in failed_deposits { - if let Err(e) = Config::AssetTransactor::deposit_asset(&asset, &beneficiary, context) { - // Ignore dust deposit errors. - if !matches!( - e, - XcmError::FailedToTransactAsset(s) - if *s == *<&'static str>::from(sp_runtime::TokenError::BelowMinimum) - ) { - return Err(e); - } - } + match Config::AssetTransactor::deposit_asset_with_surplus(&asset, &beneficiary, context) + { + Ok(surplus) => { + total_surplus.saturating_accrue(surplus); + }, + Err(error) => { + // Ignore dust deposit errors. + if !matches!( + error, + XcmError::FailedToTransactAsset(string) + if *string == *<&'static str>::from(sp_runtime::TokenError::BelowMinimum) + ) { + return Err(error); + } + }, + }; } - Ok(()) + Ok(total_surplus) } /// Take from transferred `assets` the delivery fee required to send an onward transfer message diff --git a/polkadot/xcm/xcm-executor/src/traits/transact_asset.rs b/polkadot/xcm/xcm-executor/src/traits/transact_asset.rs index c2331f805b4bd..c54280ab93d57 100644 --- a/polkadot/xcm/xcm-executor/src/traits/transact_asset.rs +++ b/polkadot/xcm/xcm-executor/src/traits/transact_asset.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . -use crate::AssetsInHolding; +use crate::{AssetsInHolding, Weight}; use core::result::Result; use xcm::latest::{Asset, Error as XcmError, Location, Result as XcmResult, XcmContext}; @@ -83,8 +83,21 @@ pub trait TransactAsset { Err(XcmError::Unimplemented) } - /// Withdraw the given asset from the consensus system. Return the actual asset(s) withdrawn, - /// which should always be equal to `_what`. + /// Identical to `deposit_asset` but returning the surplus, if any. + /// + /// Return the difference between the worst-case weight and the actual weight consumed. + /// This can be zero most of the time unless there's some metering involved. + fn deposit_asset_with_surplus( + what: &Asset, + who: &Location, + context: Option<&XcmContext>, + ) -> Result { + Self::deposit_asset(what, who, context).map(|()| Weight::zero()) + } + + /// Withdraw the given asset from the consensus system. + /// + /// Return the actual asset(s) withdrawn, which should always be equal to `_what`. /// /// The XCM `_maybe_context` parameter may be `None` when the caller of `withdraw_asset` is /// outside of the context of a currently-executing XCM. An example will be the `charge_fees` @@ -99,6 +112,18 @@ pub trait TransactAsset { Err(XcmError::Unimplemented) } + /// Withdraw assets returning surplus. + /// + /// The surplus is the difference between the worst-case weight and the actual weight consumed. + /// This can be zero most of the time unless there's some metering involved. + fn withdraw_asset_with_surplus( + what: &Asset, + who: &Location, + maybe_context: Option<&XcmContext>, + ) -> Result<(AssetsInHolding, Weight), XcmError> { + Self::withdraw_asset(what, who, maybe_context).map(|assets| (assets, Weight::zero())) + } + /// Move an `asset` `from` one location in `to` another location. /// /// Returns `XcmError::FailedToTransactAsset` if transfer failed. @@ -117,6 +142,21 @@ pub trait TransactAsset { Err(XcmError::Unimplemented) } + /// Identical to `internal_transfer_asset` but returning the surplus, if any. + /// + /// The surplus is the difference between the worst-case weight and the actual + /// consumed weight. + /// This can be zero usually if there's no metering involved. + fn internal_transfer_asset_with_surplus( + asset: &Asset, + from: &Location, + to: &Location, + context: &XcmContext, + ) -> Result<(AssetsInHolding, Weight), XcmError> { + Self::internal_transfer_asset(asset, from, to, context) + .map(|assets| (assets, Weight::zero())) + } + /// Move an `asset` `from` one location in `to` another location. /// /// Attempts to use `internal_transfer_asset` and if not available then falls back to using a @@ -130,13 +170,35 @@ pub trait TransactAsset { match Self::internal_transfer_asset(asset, from, to, context) { Err(XcmError::AssetNotFound | XcmError::Unimplemented) => { let assets = Self::withdraw_asset(asset, from, Some(context))?; - // Not a very forgiving attitude; once we implement roll-backs then it'll be nicer. Self::deposit_asset(asset, to, Some(context))?; Ok(assets) }, result => result, } } + + /// Identical to `transfer_asset` but returning the surplus, if any. + /// + /// The surplus is the difference between the worst-case weight and the actual + /// consumed weight. + /// This can be zero usually if there's no metering involved. + fn transfer_asset_with_surplus( + asset: &Asset, + from: &Location, + to: &Location, + context: &XcmContext, + ) -> Result<(AssetsInHolding, Weight), XcmError> { + match Self::internal_transfer_asset_with_surplus(asset, from, to, context) { + Err(XcmError::AssetNotFound | XcmError::Unimplemented) => { + let (assets, withdraw_surplus) = + Self::withdraw_asset_with_surplus(asset, from, Some(context))?; + let deposit_surplus = Self::deposit_asset_with_surplus(asset, to, Some(context))?; + let total_surplus = withdraw_surplus.saturating_add(deposit_surplus); + Ok((assets, total_surplus)) + }, + result => result, + } + } } #[impl_trait_for_tuples::impl_for_tuples(30)] @@ -204,6 +266,27 @@ impl TransactAsset for Tuple { Err(XcmError::AssetNotFound) } + fn deposit_asset_with_surplus( + what: &Asset, + who: &Location, + context: Option<&XcmContext>, + ) -> Result { + for_tuples!( #( + match Tuple::deposit_asset_with_surplus(what, who, context) { + Err(XcmError::AssetNotFound) | Err(XcmError::Unimplemented) => (), + r => return r, + } + )* ); + tracing::trace!( + target: "xcm::TransactAsset::deposit_asset", + ?what, + ?who, + ?context, + "did not deposit asset", + ); + Err(XcmError::AssetNotFound) + } + fn withdraw_asset( what: &Asset, who: &Location, @@ -225,6 +308,27 @@ impl TransactAsset for Tuple { Err(XcmError::AssetNotFound) } + fn withdraw_asset_with_surplus( + what: &Asset, + who: &Location, + maybe_context: Option<&XcmContext>, + ) -> Result<(AssetsInHolding, Weight), XcmError> { + for_tuples!( #( + match Tuple::withdraw_asset_with_surplus(what, who, maybe_context) { + Err(XcmError::AssetNotFound) | Err(XcmError::Unimplemented) => (), + r => return r, + } + )* ); + tracing::trace!( + target: "xcm::TransactAsset::withdraw_asset", + ?what, + ?who, + ?maybe_context, + "did not withdraw asset", + ); + Err(XcmError::AssetNotFound) + } + fn internal_transfer_asset( what: &Asset, from: &Location, @@ -247,6 +351,29 @@ impl TransactAsset for Tuple { ); Err(XcmError::AssetNotFound) } + + fn internal_transfer_asset_with_surplus( + what: &Asset, + from: &Location, + to: &Location, + context: &XcmContext, + ) -> Result<(AssetsInHolding, Weight), XcmError> { + for_tuples!( #( + match Tuple::internal_transfer_asset_with_surplus(what, from, to, context) { + Err(XcmError::AssetNotFound) | Err(XcmError::Unimplemented) => (), + r => return r, + } + )* ); + tracing::trace!( + target: "xcm::TransactAsset::internal_transfer_asset", + ?what, + ?from, + ?to, + ?context, + "did not transfer asset", + ); + Err(XcmError::AssetNotFound) + } } #[cfg(test)] diff --git a/prdoc/pr_7762.prdoc b/prdoc/pr_7762.prdoc new file mode 100644 index 0000000000000..770e0624904d6 --- /dev/null +++ b/prdoc/pr_7762.prdoc @@ -0,0 +1,41 @@ +# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0 +# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json + +title: ERC20 Asset Transactor + +doc: + - audience: Runtime Dev + description: | + This PR introduces an Asset Transactor for dealing with ERC20 tokens and adds it to Asset Hub + Westend. + This means asset ids of the form `{ parents: 0, interior: X1(AccountKey20 { key, network }) }` will be + matched by this transactor and the corresponding `transfer` function will be called in the + smart contract whose address is `key`. + If your chain uses `pallet-revive`, you can support ERC20s as well by adding the transactor, which lives + in `assets-common`. + - audience: Runtime User + description: | + This PR allows ERC20 tokens on Asset Hub to be referenced in XCM via their smart contract address. + This is the first step towards cross-chain transferring ERC20s created on the Hub. + +crates: +- name: assets-common + bump: minor +- name: asset-hub-westend-runtime + bump: minor +- name: pallet-revive + bump: minor +- name: penpal-runtime + bump: patch +- name: staging-xcm-executor + bump: minor +- name: polkadot-sdk + bump: minor +- name: ethereum-standards + bump: minor +- name: pallet-revive-fixtures + bump: minor +- name: asset-hub-rococo-runtime + bump: patch +- name: pallet-staking-async-parachain-runtime + bump: patch diff --git a/substrate/frame/revive/Cargo.toml b/substrate/frame/revive/Cargo.toml index 7c88a0a5c41db..a96035a12e048 100644 --- a/substrate/frame/revive/Cargo.toml +++ b/substrate/frame/revive/Cargo.toml @@ -21,6 +21,7 @@ alloy-core = { workspace = true, features = ["sol-types"] } codec = { features = ["derive", "max-encoded-len"], workspace = true } derive_more = { workspace = true } environmental = { workspace = true } +ethereum-standards = { workspace = true } ethereum-types = { workspace = true, features = ["codec", "rlp", "serialize"] } hex-literal = { workspace = true } humantime-serde = { optional = true, workspace = true } diff --git a/substrate/frame/revive/fixtures/contracts/dummy.polkavm b/substrate/frame/revive/fixtures/contracts/dummy.polkavm new file mode 100644 index 0000000000000..d970e700ce564 Binary files /dev/null and b/substrate/frame/revive/fixtures/contracts/dummy.polkavm differ diff --git a/substrate/frame/revive/fixtures/contracts/dummy.sol b/substrate/frame/revive/fixtures/contracts/dummy.sol new file mode 100644 index 0000000000000..e64031b0e0210 --- /dev/null +++ b/substrate/frame/revive/fixtures/contracts/dummy.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.2 <0.9.0; + +contract Simple { + uint256 number; + + function store(uint256 num) public { + number = num; + } + + function retrieve() public view returns (uint256) { + return number; + } +} \ No newline at end of file diff --git a/substrate/frame/revive/fixtures/contracts/erc20.polkavm b/substrate/frame/revive/fixtures/contracts/erc20.polkavm new file mode 100644 index 0000000000000..e545040080bf6 Binary files /dev/null and b/substrate/frame/revive/fixtures/contracts/erc20.polkavm differ diff --git a/substrate/frame/revive/fixtures/contracts/erc20.sol b/substrate/frame/revive/fixtures/contracts/erc20.sol new file mode 100644 index 0000000000000..14a21998f0ca6 --- /dev/null +++ b/substrate/frame/revive/fixtures/contracts/erc20.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MyToken is ERC20 { + constructor(uint256 total) ERC20("TestToken1", "TT1") { + // We mint `total` tokens to the creator of this contract, as + // a sort of genesis. + _mint(msg.sender, total); + } + + function mint(uint256 amount) public { + _mint(msg.sender, amount); + } + + function burn(uint256 amount) public { + _burn(msg.sender, amount); + } +} diff --git a/substrate/frame/revive/fixtures/contracts/expensive_erc20.polkavm b/substrate/frame/revive/fixtures/contracts/expensive_erc20.polkavm new file mode 100644 index 0000000000000..b72214b86c6fb Binary files /dev/null and b/substrate/frame/revive/fixtures/contracts/expensive_erc20.polkavm differ diff --git a/substrate/frame/revive/fixtures/contracts/expensive_erc20.sol b/substrate/frame/revive/fixtures/contracts/expensive_erc20.sol new file mode 100644 index 0000000000000..a1363845bd9e3 --- /dev/null +++ b/substrate/frame/revive/fixtures/contracts/expensive_erc20.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MyToken is ERC20 { + constructor(uint256 total) ERC20("TestToken1", "TT1") { + // We mint `total` tokens to the creator of this contract, as + // a sort of genesis. + _mint(msg.sender, total); + } + + function transfer(address to, uint256 value) public override returns (bool) { + address owner = msg.sender; + _transfer(owner, to, value); + for (uint256 i = 0; i < 1000000; i++) { + keccak256(abi.encode(i)); + } + return true; + } +} diff --git a/substrate/frame/revive/fixtures/contracts/fake_erc20.polkavm b/substrate/frame/revive/fixtures/contracts/fake_erc20.polkavm new file mode 100644 index 0000000000000..932bbcaf61735 Binary files /dev/null and b/substrate/frame/revive/fixtures/contracts/fake_erc20.polkavm differ diff --git a/substrate/frame/revive/fixtures/contracts/fake_erc20.sol b/substrate/frame/revive/fixtures/contracts/fake_erc20.sol new file mode 100644 index 0000000000000..1c6d0aca5c8c2 --- /dev/null +++ b/substrate/frame/revive/fixtures/contracts/fake_erc20.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract MyToken { + mapping(address account => uint256) private _balances; + + uint256 private _totalSupply; + + constructor(uint256 total) { + // We mint `total` tokens to the creator of this contract, as + // a sort of genesis. + _mint(msg.sender, total); + } + + function transfer(address to, uint256 value) public virtual returns (uint256) { + address owner = msg.sender; + _transfer(owner, to, value); + return 1243657816489523; + } + + function _transfer(address from, address to, uint256 value) internal { + _update(from, to, value); + } + + function _update(address from, address to, uint256 value) internal virtual { + if (from == address(0)) { + // Overflow check required: The rest of the code assumes that totalSupply never overflows + _totalSupply += value; + } else { + uint256 fromBalance = _balances[from]; + unchecked { + // Overflow not possible: value <= fromBalance <= totalSupply. + _balances[from] = fromBalance - value; + } + } + + if (to == address(0)) { + unchecked { + // Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply. + _totalSupply -= value; + } + } else { + unchecked { + // Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256. + _balances[to] += value; + } + } + } + + function _mint(address account, uint256 value) internal { + _update(address(0), account, value); + } +} + diff --git a/substrate/frame/revive/src/impl_fungibles.rs b/substrate/frame/revive/src/impl_fungibles.rs new file mode 100644 index 0000000000000..75c3ae59e98d1 --- /dev/null +++ b/substrate/frame/revive/src/impl_fungibles.rs @@ -0,0 +1,437 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) 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. + +//! Implementation of the `fungibles::*` family of traits for `pallet-revive`. +//! +//! This is meant to allow ERC20 tokens stored on this pallet to be used with +//! the fungibles traits. +//! This is only meant for tests since gas limits are not taken into account, +//! the feature flags make sure of that. + +#![cfg(any(feature = "std", feature = "runtime-benchmarks", test))] + +use alloy_core::{ + primitives::{Address, U256 as EU256}, + sol_types::*, +}; +use frame_support::{ + traits::{ + tokens::{ + fungible, fungibles, DepositConsequence, Fortitude, Precision, Preservation, + Provenance, WithdrawConsequence, + }, + OriginTrait, + }, + PalletId, +}; +use sp_core::{H160, H256, U256}; +use sp_runtime::{ + traits::{AccountIdConversion, Zero}, + DispatchError, +}; + +use super::{ + address::AddressMapper, pallet, BalanceOf, Bounded, Config, ContractResult, DepositLimit, + MomentOf, Pallet, Weight, +}; +use ethereum_standards::IERC20; + +const GAS_LIMIT: Weight = Weight::from_parts(1_000_000_000, 100_000); + +impl Pallet { + // Test checking account for the `fungibles::*` implementation. + // + // Still needs to be mapped in tests for it to be usable. + pub fn checking_account() -> ::AccountId { + PalletId(*b"py/revch").into_account_truncating() + } +} + +impl fungibles::Inspect<::AccountId> for Pallet +where + BalanceOf: Into + TryFrom + Bounded, + MomentOf: Into, + T::Hash: frame_support::traits::IsType, +{ + // The asset id of an ERC20 is its origin contract's address. + type AssetId = H160; + // The balance is always u128. + type Balance = u128; + + // Need to call a view function here. + fn total_issuance(asset_id: Self::AssetId) -> Self::Balance { + let data = IERC20::totalSupplyCall {}.abi_encode(); + let ContractResult { result, .. } = Self::bare_call( + T::RuntimeOrigin::signed(Self::checking_account()), + asset_id, + BalanceOf::::zero(), + GAS_LIMIT, + DepositLimit::Balance( + <::Currency as fungible::Inspect<_>>::total_issuance(), + ), + data, + ); + if let Ok(return_value) = result { + if let Ok(eu256) = EU256::abi_decode_validate(&return_value.data) { + eu256.to::() + } else { + 0 + } + } else { + 0 + } + } + + fn minimum_balance(_: Self::AssetId) -> Self::Balance { + // ERC20s don't have this concept. + 1 + } + + fn total_balance(asset_id: Self::AssetId, account_id: &T::AccountId) -> Self::Balance { + // Since ERC20s don't have the concept of freezes and locks, + // total balance is the same as balance. + Self::balance(asset_id, account_id) + } + + fn balance(asset_id: Self::AssetId, account_id: &T::AccountId) -> Self::Balance { + let eth_address = T::AddressMapper::to_address(account_id); + let address = Address::from(Into::<[u8; 20]>::into(eth_address)); + let data = IERC20::balanceOfCall { account: address }.abi_encode(); + let ContractResult { result, .. } = Self::bare_call( + T::RuntimeOrigin::signed(account_id.clone()), + asset_id, + BalanceOf::::zero(), + GAS_LIMIT, + DepositLimit::Balance( + <::Currency as fungible::Inspect<_>>::total_issuance(), + ), + data, + ); + if let Ok(return_value) = result { + if let Ok(eu256) = EU256::abi_decode_validate(&return_value.data) { + eu256.to::() + } else { + 0 + } + } else { + 0 + } + } + + fn reducible_balance( + asset_id: Self::AssetId, + account_id: &T::AccountId, + _: Preservation, + _: Fortitude, + ) -> Self::Balance { + // Since ERC20s don't have minimum amounts, this is the same + // as balance. + Self::balance(asset_id, account_id) + } + + fn can_deposit( + _: Self::AssetId, + _: &T::AccountId, + _: Self::Balance, + _: Provenance, + ) -> DepositConsequence { + DepositConsequence::Success + } + + fn can_withdraw( + _: Self::AssetId, + _: &T::AccountId, + _: Self::Balance, + ) -> WithdrawConsequence { + WithdrawConsequence::Success + } + + fn asset_exists(_: Self::AssetId) -> bool { + false + } +} + +// We implement `fungibles::Mutate` to override `burn_from` and `mint_to`. +// +// These functions are used in [`xcm_builder::FungiblesAdapter`]. +impl fungibles::Mutate<::AccountId> for Pallet +where + BalanceOf: Into + TryFrom + Bounded, + MomentOf: Into, + T::Hash: frame_support::traits::IsType, +{ + fn burn_from( + asset_id: Self::AssetId, + who: &T::AccountId, + amount: Self::Balance, + _: Preservation, + _: Precision, + _: Fortitude, + ) -> Result { + let checking_account_eth = T::AddressMapper::to_address(&Self::checking_account()); + let checking_address = Address::from(Into::<[u8; 20]>::into(checking_account_eth)); + let data = + IERC20::transferCall { to: checking_address, value: EU256::from(amount) }.abi_encode(); + let ContractResult { result, gas_consumed, .. } = Self::bare_call( + T::RuntimeOrigin::signed(who.clone()), + asset_id, + BalanceOf::::zero(), + GAS_LIMIT, + DepositLimit::Balance( + <::Currency as fungible::Inspect<_>>::total_issuance(), + ), + data, + ); + log::trace!(target: "whatiwant", "{gas_consumed}"); + if let Ok(return_value) = result { + if return_value.did_revert() { + Err("Contract reverted".into()) + } else { + let is_success = + bool::abi_decode_validate(&return_value.data).expect("Failed to ABI decode"); + if is_success { + let balance = >::balance(asset_id, who); + Ok(balance) + } else { + Err("Contract transfer failed".into()) + } + } + } else { + Err("Contract out of gas".into()) + } + } + + fn mint_into( + asset_id: Self::AssetId, + who: &T::AccountId, + amount: Self::Balance, + ) -> Result { + let eth_address = T::AddressMapper::to_address(who); + let address = Address::from(Into::<[u8; 20]>::into(eth_address)); + let data = IERC20::transferCall { to: address, value: EU256::from(amount) }.abi_encode(); + let ContractResult { result, .. } = Self::bare_call( + T::RuntimeOrigin::signed(Self::checking_account()), + asset_id, + BalanceOf::::zero(), + GAS_LIMIT, + DepositLimit::Balance( + <::Currency as fungible::Inspect<_>>::total_issuance(), + ), + data, + ); + if let Ok(return_value) = result { + if return_value.did_revert() { + Err("Contract reverted".into()) + } else { + let is_success = + bool::abi_decode_validate(&return_value.data).expect("Failed to ABI decode"); + if is_success { + let balance = >::balance(asset_id, who); + Ok(balance) + } else { + Err("Contract transfer failed".into()) + } + } + } else { + Err("Contract out of gas".into()) + } + } +} + +// This impl is needed for implementing `fungibles::Mutate`. +// However, we don't have this type of access to smart contracts. +// Withdraw and deposit happen via the custom `fungibles::Mutate` impl above. +// Because of this, all functions here return an error, when possible. +impl fungibles::Unbalanced<::AccountId> for Pallet +where + BalanceOf: Into + TryFrom + Bounded, + MomentOf: Into, + T::Hash: frame_support::traits::IsType, +{ + fn handle_raw_dust(_: Self::AssetId, _: Self::Balance) {} + fn handle_dust(_: fungibles::Dust) {} + fn write_balance( + _: Self::AssetId, + _: &T::AccountId, + _: Self::Balance, + ) -> Result, DispatchError> { + Err(DispatchError::Unavailable) + } + fn set_total_issuance(_id: Self::AssetId, _amount: Self::Balance) { + // Empty. + } + + fn decrease_balance( + _: Self::AssetId, + _: &T::AccountId, + _: Self::Balance, + _: Precision, + _: Preservation, + _: Fortitude, + ) -> Result { + Err(DispatchError::Unavailable) + } + + fn increase_balance( + _: Self::AssetId, + _: &T::AccountId, + _: Self::Balance, + _: Precision, + ) -> Result { + Err(DispatchError::Unavailable) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + test_utils::{builder::*, ALICE}, + tests::{Contracts, ExtBuilder, RuntimeOrigin, Test}, + Code, ContractInfoOf, + }; + use frame_support::assert_ok; + + #[test] + fn call_erc20_contract() { + ExtBuilder::default().existential_deposit(1).build().execute_with(|| { + let _ = + <::Currency as fungible::Mutate<_>>::set_balance(&ALICE, 1_000_000); + let code = include_bytes!("../fixtures/contracts/erc20.polkavm").to_vec(); + let amount = EU256::from(1000); + let constructor_data = sol_data::Uint::<256>::abi_encode(&amount); + let Contract { addr, .. } = BareInstantiateBuilder::::bare_instantiate( + RuntimeOrigin::signed(ALICE), + Code::Upload(code), + ) + .data(constructor_data) + .build_and_unwrap_contract(); + let result = BareCallBuilder::::bare_call(RuntimeOrigin::signed(ALICE), addr) + .data(IERC20::totalSupplyCall {}.abi_encode()) + .build_and_unwrap_result(); + let balance = + EU256::abi_decode_validate(&result.data).expect("Failed to decode ABI response"); + assert_eq!(balance, EU256::from(amount)); + // Contract is uploaded. + assert_eq!(ContractInfoOf::::contains_key(&addr), true); + }); + } + + #[test] + fn total_issuance_works() { + ExtBuilder::default().existential_deposit(1).build().execute_with(|| { + let _ = + <::Currency as fungible::Mutate<_>>::set_balance(&ALICE, 1_000_000); + let code = include_bytes!("../fixtures/contracts/erc20.polkavm").to_vec(); + let amount = 1000; + let constructor_data = sol_data::Uint::<256>::abi_encode(&EU256::from(amount)); + let Contract { addr, .. } = BareInstantiateBuilder::::bare_instantiate( + RuntimeOrigin::signed(ALICE), + Code::Upload(code), + ) + .data(constructor_data) + .build_and_unwrap_contract(); + + let total_issuance = >::total_issuance(addr); + assert_eq!(total_issuance, amount); + }); + } + + #[test] + fn get_balance_of_erc20() { + ExtBuilder::default().existential_deposit(1).build().execute_with(|| { + let _ = + <::Currency as fungible::Mutate<_>>::set_balance(&ALICE, 1_000_000); + let code = include_bytes!("../fixtures/contracts/erc20.polkavm").to_vec(); + let amount = 1000; + let constructor_data = sol_data::Uint::<256>::abi_encode(&EU256::from(amount)); + let Contract { addr, .. } = BareInstantiateBuilder::::bare_instantiate( + RuntimeOrigin::signed(ALICE), + Code::Upload(code), + ) + .data(constructor_data) + .build_and_unwrap_contract(); + assert_eq!(>::balance(addr, &ALICE), amount); + }); + } + + #[test] + fn burn_from_impl_works() { + ExtBuilder::default().existential_deposit(1).build().execute_with(|| { + let _ = + <::Currency as fungible::Mutate<_>>::set_balance(&ALICE, 1_000_000); + let code = include_bytes!("../fixtures/contracts/erc20.polkavm").to_vec(); + let amount = 1000; + let constructor_data = sol_data::Uint::<256>::abi_encode(&(EU256::from(amount * 2))); + let Contract { addr, .. } = BareInstantiateBuilder::::bare_instantiate( + RuntimeOrigin::signed(ALICE), + Code::Upload(code), + ) + .data(constructor_data) + .build_and_unwrap_contract(); + let _ = BareCallBuilder::::bare_call(RuntimeOrigin::signed(ALICE), addr) + .build_and_unwrap_result(); + assert_eq!(>::balance(addr, &ALICE), amount * 2); + + // Use `fungibles::Mutate<_>::burn_from`. + assert_ok!(>::burn_from( + addr, + &ALICE, + amount, + Preservation::Expendable, + Precision::Exact, + Fortitude::Polite + )); + assert_eq!(>::balance(addr, &ALICE), amount); + }); + } + + #[test] + fn mint_into_impl_works() { + ExtBuilder::default().existential_deposit(1).build().execute_with(|| { + let checking_account = Pallet::::checking_account(); + let _ = + <::Currency as fungible::Mutate<_>>::set_balance(&ALICE, 1_000_000); + let _ = <::Currency as fungible::Mutate<_>>::set_balance( + &checking_account, + 1_000_000, + ); + let code = include_bytes!("../fixtures/contracts/erc20.polkavm").to_vec(); + let amount = 1000; + let constructor_data = sol_data::Uint::<256>::abi_encode(&EU256::from(amount)); + // We're instantiating the contract with the `CheckingAccount` so it has `amount` in it. + let Contract { addr, .. } = BareInstantiateBuilder::::bare_instantiate( + RuntimeOrigin::signed(checking_account.clone()), + Code::Upload(code), + ) + .storage_deposit_limit((1_000_000_000_000).into()) + .data(constructor_data) + .build_and_unwrap_contract(); + assert_eq!( + >::balance(addr, &checking_account), + amount + ); + assert_eq!(>::balance(addr, &ALICE), 0); + + // We use `mint_into` to transfer assets from the checking account to `ALICE`. + assert_ok!(>::mint_into(addr, &ALICE, amount)); + // Balances changed accordingly. + assert_eq!(>::balance(addr, &checking_account), 0); + assert_eq!(>::balance(addr, &ALICE), amount); + }); + } +} diff --git a/substrate/frame/revive/src/lib.rs b/substrate/frame/revive/src/lib.rs index 04d09d6eb8341..d19d464831ea8 100644 --- a/substrate/frame/revive/src/lib.rs +++ b/substrate/frame/revive/src/lib.rs @@ -27,14 +27,14 @@ mod benchmarking; mod call_builder; mod exec; mod gas; +mod impl_fungibles; mod limits; mod primitives; mod storage; -mod transient_storage; -mod wasm; - #[cfg(test)] mod tests; +mod transient_storage; +mod wasm; pub mod evm; pub mod precompiles; diff --git a/substrate/frame/revive/src/tests.rs b/substrate/frame/revive/src/tests.rs index fd2d4a376b357..b9251e1168b32 100644 --- a/substrate/frame/revive/src/tests.rs +++ b/substrate/frame/revive/src/tests.rs @@ -352,6 +352,7 @@ where } parameter_types! { pub static UnstableInterface: bool = true; + pub CheckingAccount: AccountId32 = BOB.clone(); } impl FindAuthor<::AccountId> for Test { @@ -423,9 +424,13 @@ impl ExtBuilder { sp_tracing::try_init_simple(); self.set_associated_consts(); let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); - pallet_balances::GenesisConfig:: { balances: vec![], ..Default::default() } - .assimilate_storage(&mut t) - .unwrap(); + let checking_account = Pallet::::checking_account(); + pallet_balances::GenesisConfig:: { + balances: vec![(checking_account.clone(), 1_000_000_000_000)], + ..Default::default() + } + .assimilate_storage(&mut t) + .unwrap(); let mut ext = sp_io::TestExternalities::new(t); ext.register_extension(KeystoreExt::new(MemoryKeystore::new())); ext.execute_with(|| { @@ -442,6 +447,9 @@ impl ExtBuilder { CodeInfoOf::::insert(code_hash, crate::CodeInfo::new(ALICE)); } }); + ext.execute_with(|| { + assert_ok!(Pallet::::map_account(RuntimeOrigin::signed(checking_account))); + }); ext } } diff --git a/substrate/frame/staking-async/runtimes/parachain/Cargo.toml b/substrate/frame/staking-async/runtimes/parachain/Cargo.toml index 202a1f31a2b44..a4df251d7c63e 100644 --- a/substrate/frame/staking-async/runtimes/parachain/Cargo.toml +++ b/substrate/frame/staking-async/runtimes/parachain/Cargo.toml @@ -197,6 +197,7 @@ runtime-benchmarks = [ "xcm/runtime-benchmarks", ] try-runtime = [ + "assets-common/try-runtime", "cumulus-pallet-aura-ext/try-runtime", "cumulus-pallet-parachain-system/try-runtime", "cumulus-pallet-weight-reclaim/try-runtime", diff --git a/umbrella/Cargo.toml b/umbrella/Cargo.toml index 740048c980c1f..22577c2493aa3 100644 --- a/umbrella/Cargo.toml +++ b/umbrella/Cargo.toml @@ -357,6 +357,7 @@ runtime-benchmarks = [ "xcm-runtime-apis?/runtime-benchmarks", ] try-runtime = [ + "assets-common?/try-runtime", "cumulus-pallet-aura-ext?/try-runtime", "cumulus-pallet-dmp-queue?/try-runtime", "cumulus-pallet-parachain-system?/try-runtime",