Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optional ERC20-XCM gas limit override and tests #2422

Merged
merged 16 commits into from
Aug 28, 2023
45 changes: 37 additions & 8 deletions pallets/erc20-xcm-bridge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ pub mod pallet {
use sp_core::{H160, H256, U256};
use sp_std::vec::Vec;
use xcm::latest::{
Error as XcmError, MultiAsset, MultiLocation, Result as XcmResult, XcmContext,
AssetId, Error as XcmError, Junction, MultiAsset, MultiLocation, Result as XcmResult,
XcmContext,
};
use xcm_executor::traits::{Convert, Error as MatchError, MatchesFungibles};
use xcm_executor::Assets;
Expand All @@ -65,14 +66,33 @@ pub mod pallet {
pub fn is_erc20_asset(asset: &MultiAsset) -> bool {
Erc20Matcher::<T::Erc20MultilocationPrefix>::is_erc20_asset(asset)
}
pub fn weight_of_erc20_transfer() -> Weight {
T::GasWeightMapping::gas_to_weight(T::Erc20TransferGasLimit::get(), true)
pub fn gas_limit_of_erc20_transfer(asset_id: &AssetId) -> u64 {
if let AssetId::Concrete(multilocation) = asset_id {
if let Some(Junction::GeneralKey {
length: _,
ref data,
}) = multilocation.interior().into_iter().next_back()
{
if let Ok(content) = core::str::from_utf8(&data[0..10]) {
if content == "gas_limit:" {
let mut bytes: [u8; 8] = Default::default();
bytes.copy_from_slice(&data[10..18]);
return u64::from_le_bytes(bytes);
}
}
}
};
T::Erc20TransferGasLimit::get()
}
pub fn weight_of_erc20_transfer(asset_id: &AssetId) -> Weight {
T::GasWeightMapping::gas_to_weight(Self::gas_limit_of_erc20_transfer(asset_id), true)
}
fn erc20_transfer(
erc20_contract_address: H160,
from: H160,
to: H160,
amount: U256,
gas_limit: u64,
) -> Result<(), Erc20TransferError> {
let mut input = Vec::with_capacity(ERC20_TRANSFER_CALL_DATA_SIZE);
// ERC20.transfer method hash
Expand All @@ -82,15 +102,14 @@ pub mod pallet {
// append amount to be transferred
input.extend_from_slice(H256::from_uint(&amount).as_bytes());

let weight_limit =
T::GasWeightMapping::gas_to_weight(T::Erc20TransferGasLimit::get(), true);
let weight_limit: Weight = T::GasWeightMapping::gas_to_weight(gas_limit, true);

let exec_info = T::EvmRunner::call(
from,
erc20_contract_address,
input,
U256::default(),
T::Erc20TransferGasLimit::get(),
gas_limit,
None,
None,
None,
Expand Down Expand Up @@ -140,6 +159,8 @@ pub mod pallet {
let beneficiary = T::AccountIdConverter::convert_ref(who)
.map_err(|()| MatchError::AccountIdConversionFailed)?;

let gas_limit = Self::gas_limit_of_erc20_transfer(&what.id);

// Get the global context to recover accounts origins.
XcmHoldingErc20sOrigins::with(|erc20s_origins| {
match erc20s_origins.drain(contract_address, amount) {
Expand All @@ -149,7 +170,13 @@ pub mod pallet {
tokens_to_transfer
.into_iter()
.try_for_each(|(from, subamount)| {
Self::erc20_transfer(contract_address, from, beneficiary, subamount)
Self::erc20_transfer(
contract_address,
from,
beneficiary,
subamount,
gas_limit,
)
})
})
.map_err(Into::into),
Expand Down Expand Up @@ -182,10 +209,12 @@ pub mod pallet {
let to = T::AccountIdConverter::convert_ref(to)
.map_err(|()| MatchError::AccountIdConversionFailed)?;

let gas_limit = Self::gas_limit_of_erc20_transfer(&asset.id);

// We perform the evm transfers in a storage transaction to ensure that if it fail
// any contract storage changes are rolled back.
frame_support::storage::with_storage_layer(|| {
Self::erc20_transfer(contract_address, from, to, amount)
Self::erc20_transfer(contract_address, from, to, amount, gas_limit)
})?;

Ok(asset.clone().into())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,22 @@ pub struct WeightInfo<T>(PhantomData<T>);
impl<T: frame_system::Config + pallet_erc20_xcm_bridge::Config> WeightInfo<T> {
pub(crate) fn withdraw_asset(asset: &MultiAsset) -> Weight {
if pallet_erc20_xcm_bridge::Pallet::<T>::is_erc20_asset(asset) {
pallet_erc20_xcm_bridge::Pallet::<T>::weight_of_erc20_transfer()
pallet_erc20_xcm_bridge::Pallet::<T>::weight_of_erc20_transfer(&asset.id)

} else {
Weight::from_parts(200_000_000 as u64, ASSET_BURN_MAX_PROOF_SIZE)
}
}
pub(crate) fn transfer_asset(asset: &MultiAsset) -> Weight {
if pallet_erc20_xcm_bridge::Pallet::<T>::is_erc20_asset(asset) {
pallet_erc20_xcm_bridge::Pallet::<T>::weight_of_erc20_transfer()
pallet_erc20_xcm_bridge::Pallet::<T>::weight_of_erc20_transfer(&asset.id)
} else {
Weight::from_parts(200_000_000 as u64, ASSET_TRANSFER_MAX_PROOF_SIZE)
}
}
pub(crate) fn transfer_reserve_asset(asset: &MultiAsset) -> Weight {
if pallet_erc20_xcm_bridge::Pallet::<T>::is_erc20_asset(asset) {
pallet_erc20_xcm_bridge::Pallet::<T>::weight_of_erc20_transfer()
pallet_erc20_xcm_bridge::Pallet::<T>::weight_of_erc20_transfer(&asset.id)
} else {
Weight::from_parts(200_000_000 as u64, ASSET_TRANSFER_MAX_PROOF_SIZE)
}
Expand Down
173 changes: 90 additions & 83 deletions test/suites/dev/test-xcm-v3/test-xcm-erc20-excess-gas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
sovereignAccountOfSibling,
} from "../../../helpers/xcm.js";
import { parseEther } from "ethers";
import { stringToU8a } from "@polkadot/util";

export const ERC20_TOTAL_SUPPLY = 1_000_000_000n;

Expand Down Expand Up @@ -178,95 +179,101 @@ describeSuite({
},
});

// UNCOMMENT when https://github.com/moonbeam-foundation/moonbeam/pull/2422 is merged

// it({
// id: "T03",
// title: "Incoming ERC20 transfer should succeed if setting a custom gas limit",
// test: async function () {
// const amountTransferred = 1_000_000n;
// const paraSovereign = sovereignAccountOfSibling(context, paraId);
it({
id: "T03",
title: "Incoming ERC20 transfer should succeed if setting a custom gas limit",
test: async function () {
const amountTransferred = 1_000_000n;
const paraSovereign = sovereignAccountOfSibling(context, paraId);

// // Deploy contract
// const { contractAddress, status } = await context.deployContract!("ERC20ExcessGas", {
// args: ["ERC20", "20S", paraSovereign, ERC20_TOTAL_SUPPLY],
// });
// expect(status).eq("success");
// Deploy contract
const { contractAddress, status } = await context.deployContract!("ERC20ExcessGas", {
args: ["ERC20", "20S", paraSovereign, ERC20_TOTAL_SUPPLY],
});
expect(status).eq("success");

// // Get pallet indices
// const metadata = await polkadotJs.rpc.state.getMetadata();
// const balancesPalletIndex = metadata.asLatest.pallets
// .find(({ name }) => name.toString() == "Balances")!
// .index.toNumber();
// const erc20XcmPalletIndex = metadata.asLatest.pallets
// .find(({ name }) => name.toString() == "Erc20XcmBridge")!
// .index.toNumber();
// Get pallet indices
const metadata = await polkadotJs.rpc.state.getMetadata();
const balancesPalletIndex = metadata.asLatest.pallets
.find(({ name }) => name.toString() == "Balances")!
.index.toNumber();
const erc20XcmPalletIndex = metadata.asLatest.pallets
.find(({ name }) => name.toString() == "Erc20XcmBridge")!
.index.toNumber();

// // Send some native tokens to the sovereign account of paraId (to pay fees)
// await polkadotJs.tx.balances.transfer(paraSovereign, parseEther("1")).signAndSend(alith);
// await context.createBlock();
// Send some native tokens to the sovereign account of paraId (to pay fees)
await polkadotJs.tx.balances.transfer(paraSovereign, parseEther("1")).signAndSend(alith);
await context.createBlock();

// // Create the incoming xcm message
// const config: XcmFragmentConfig = {
// assets: [
// {
// multilocation: {
// parents: 0,
// interior: {
// X1: { PalletInstance: Number(balancesPalletIndex) },
// },
// },
// fungible: 100_000_000_000_000_000n,
// },
// {
// multilocation: {
// parents: 0,
// interior: {
// X3: [
// {
// PalletInstance: erc20XcmPalletIndex,
// },
// {
// AccountKey20: {
// network: "Any",
// key: contractAddress,
// },
// },
// {
// GeneralIndex: 300_000n,
// },
// ],
// },
// },
// fungible: amountTransferred,
// },
// ],
// beneficiary: CHARLETH_ADDRESS,
// };
// Create the incoming xcm message
const config: XcmFragmentConfig = {
assets: [
{
multilocation: {
parents: 0,
interior: {
X1: { PalletInstance: Number(balancesPalletIndex) },
},
},
fungible: 100_000_000_000_000_000n,
},
{
multilocation: {
parents: 0,
interior: {
X3: [
{
PalletInstance: erc20XcmPalletIndex,
},
{
AccountKey20: {
network: null,
key: contractAddress,
},
},
// Override default gas limit with optional GeneralKey.
// b'gas_limit:' + 300000(little endian) + zeros padding
{
GeneralKey: {
data: [
103, 97, 115, 95, 108, 105, 109, 105, 116, 58, 224, 147, 4, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
],
length: 32,
},
},
],
},
},
fungible: amountTransferred,
},
],
beneficiary: CHARLETH_ADDRESS,
};

// const xcmMessage = new XcmFragment(config)
// .withdraw_asset()
// .clear_origin()
// .buy_execution()
// .deposit_asset_v3(2n)
// .as_v3();
const xcmMessage = new XcmFragment(config)
.withdraw_asset()
.clear_origin()
.buy_execution()
.deposit_asset_v3(2n)
.as_v3();

// // Mock the reception of the xcm message
// await injectHrmpMessageAndSeal(context, paraId, {
// type: "XcmVersionedXcm",
// payload: xcmMessage,
// });
// Mock the reception of the xcm message
await injectHrmpMessageAndSeal(context, paraId, {
type: "XcmVersionedXcm",
payload: xcmMessage,
});

// // Charleth should have received the tokens
// expect(
// await context.readContract!({
// contractName: "ERC20ExcessGas",
// contractAddress: contractAddress as `0x${string}`,
// functionName: "balanceOf",
// args: [CHARLETH_ADDRESS],
// })
// ).equals(amountTransferred);
// },
// });
// Charleth should have received the tokens
expect(
await context.readContract!({
contractName: "ERC20ExcessGas",
contractAddress: contractAddress as `0x${string}`,
functionName: "balanceOf",
args: [CHARLETH_ADDRESS],
})
).equals(amountTransferred);
},
});
},
});
Loading