Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions prdoc/10971.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
title: "Implement IERC20Metadata for pallet-assets precompiles"

doc:
- audience: Runtime Dev
description: |
Implements the missing ERC20 metadata functions (`name`, `symbol`, `decimals`) for the
pallet-assets precompile to provide full ERC20 compatibility. These functions are essential
for proper EVM wallet and tooling integration.

The precompile implementation reads metadata from pallet-assets storage and returns properly
formatted values with appropriate gas charging using dedicated weight functions. All functions
include proper error handling for missing metadata and invalid UTF-8 encoding.

Benchmarks have been added to measure the weight of metadata reads, and corresponding weight
functions have been implemented in the WeightInfo trait.

The IERC20.sol interface file has been reorganized to clearly separate and document methods
from the base IERC20 interface and the IERC20Metadata extension, with links to the original
OpenZeppelin contracts for better maintainability.

crates:
- name: ethereum-standards
bump: minor
- name: pallet-assets-precompiles
bump: minor
- name: pallet-assets
bump: minor
48 changes: 48 additions & 0 deletions substrate/frame/assets/precompiles/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ where
IERC20Calls::allowance(call) => Self::allowance(asset_id, call, env),
IERC20Calls::approve(call) => Self::approve(asset_id, call, env),
IERC20Calls::transferFrom(call) => Self::transfer_from(asset_id, call, env),
IERC20Calls::name(_) => Self::name(asset_id, env),
IERC20Calls::symbol(_) => Self::symbol(asset_id, env),
IERC20Calls::decimals(_) => Self::decimals(asset_id, env),
}
}
}
Expand Down Expand Up @@ -322,4 +325,49 @@ where

return Ok(IERC20::transferFromCall::abi_encode_returns(&true));
}

/// Execute the name call.
fn name(
asset_id: <Runtime as Config<Instance>>::AssetId,
env: &mut impl Ext<T = Runtime>,
) -> Result<Vec<u8>, Error> {
env.charge(<Runtime as Config<Instance>>::WeightInfo::get_name())?;

let metadata = pallet_assets::Pallet::<Runtime, Instance>::get_metadata(asset_id)
.ok_or(Error::Revert(Revert { reason: "Metadata not found".into() }))?;

let name = alloc::string::String::from_utf8(metadata.name.to_vec())
.map_err(|_| Error::Revert(Revert { reason: "Invalid UTF-8 in name".into() }))?;

Ok(IERC20::nameCall::abi_encode_returns(&name))
}

/// Execute the symbol call.
fn symbol(
asset_id: <Runtime as Config<Instance>>::AssetId,
env: &mut impl Ext<T = Runtime>,
) -> Result<Vec<u8>, Error> {
env.charge(<Runtime as Config<Instance>>::WeightInfo::get_symbol())?;

let metadata = pallet_assets::Pallet::<Runtime, Instance>::get_metadata(asset_id)
.ok_or(Error::Revert(Revert { reason: "Metadata not found".into() }))?;

let symbol = alloc::string::String::from_utf8(metadata.symbol.to_vec())
.map_err(|_| Error::Revert(Revert { reason: "Invalid UTF-8 in symbol".into() }))?;

Ok(IERC20::symbolCall::abi_encode_returns(&symbol))
}

/// Execute the decimals call.
fn decimals(
asset_id: <Runtime as Config<Instance>>::AssetId,
env: &mut impl Ext<T = Runtime>,
) -> Result<Vec<u8>, Error> {
env.charge(<Runtime as Config<Instance>>::WeightInfo::get_decimals())?;

let metadata = pallet_assets::Pallet::<Runtime, Instance>::get_metadata(asset_id)
.ok_or(Error::Revert(Revert { reason: "Metadata not found".into() }))?;

Ok(IERC20::decimalsCall::abi_encode_returns(&metadata.decimals))
}
}
45 changes: 45 additions & 0 deletions substrate/frame/assets/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -641,5 +641,50 @@ benchmarks_instance_pallet! {
assert_eq!(Reserves::<T, I>::get(id)[0], reserve);
}

get_name {

@pgherveou pgherveou Feb 9, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are identical we should probably only have one and just call it for both methods in the precompile
@0xRVE can you do that in a follow up, so we can merge this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do

let (asset_id, caller, _) = create_default_asset::<T, I>(true);
T::Currency::make_free_balance_be(&caller, DepositBalanceOf::<T, I>::max_value());
let name_bytes = vec![0u8; T::StringLimit::get() as usize];
let symbol_bytes = vec![0u8; T::StringLimit::get() as usize];
let origin = SystemOrigin::Signed(caller).into();
Assets::<T, I>::set_metadata(origin, asset_id.clone(), name_bytes.clone(), symbol_bytes, 12)?;
}: {
let _ = Pallet::<T, I>::get_metadata(asset_id.clone().into());
} verify {
let metadata = Pallet::<T, I>::get_metadata(asset_id.into());
assert!(metadata.is_some());
assert_eq!(metadata.unwrap().name.to_vec(), name_bytes);
}

get_symbol {
let (asset_id, caller, _) = create_default_asset::<T, I>(true);
T::Currency::make_free_balance_be(&caller, DepositBalanceOf::<T, I>::max_value());
let name_bytes = vec![0u8; T::StringLimit::get() as usize];
let symbol_bytes = vec![0u8; T::StringLimit::get() as usize];
let origin = SystemOrigin::Signed(caller).into();
Assets::<T, I>::set_metadata(origin, asset_id.clone(), name_bytes, symbol_bytes.clone(), 12)?;
}: {
let _ = Pallet::<T, I>::get_metadata(asset_id.clone().into());
} verify {
let metadata = Pallet::<T, I>::get_metadata(asset_id.into());
assert!(metadata.is_some());
assert_eq!(metadata.unwrap().symbol.to_vec(), symbol_bytes);
}

get_decimals {
let (asset_id, caller, _) = create_default_asset::<T, I>(true);
T::Currency::make_free_balance_be(&caller, DepositBalanceOf::<T, I>::max_value());
let name_bytes = vec![0u8; T::StringLimit::get() as usize];
let symbol_bytes = vec![0u8; T::StringLimit::get() as usize];
let origin = SystemOrigin::Signed(caller).into();
Assets::<T, I>::set_metadata(origin, asset_id.clone(), name_bytes, symbol_bytes, 12)?;
}: {
let _ = Pallet::<T, I>::get_metadata(asset_id.clone().into());
} verify {
let metadata = Pallet::<T, I>::get_metadata(asset_id.into());
assert!(metadata.is_some());
assert_eq!(metadata.unwrap().decimals, 12);
}

impl_benchmark_test_suite!(Assets, crate::mock::new_test_ext(), crate::mock::Test)
}
Loading
Loading