How to Interact with the Native P3D Token from an ERC20 Perspective on 3DPass
The 3DPass runtime exposes the native P3D token through an ERC20-compatible pre-compile at address 0x0000000000000000000000000000000000000802, enabling standard Web3 libraries and Solidity contracts to query balances, execute transfers, and manage allowances exactly like any Ethereum ERC20 token.
The 3DPass blockchain (repository 3dpass/3dp) implements a Frontier-based EVM compatibility layer that bridges Substrate's native asset handling with Ethereum standards. For developers looking to interact with the native P3D token from an ERC20 perspective, the runtime provides a dedicated pre-compile that maps standard ERC20 methods to the underlying pallet_balances storage and dispatch logic.
ERC20 Metadata and Pre-compile Registration
NativeErc20Metadata Implementation
In runtime/src/precompiles.rs, the NativeErc20Metadata struct implements the Erc20Metadata trait to define the token's static properties. This configuration identifies the native currency and supplies the metadata required by the ERC20 specification.
// runtime/src/precompiles.rs
pub struct NativeErc20Metadata;
impl Erc20Metadata for NativeErc20Metadata {
fn name() -> &'static str { "P3D token" }
fn symbol() -> &'static str { "P3D" }
fn decimals() -> u8 { 18 }
fn is_native_currency() -> bool { true }
}
Source: runtime/src/precompiles.rs#L38-L62
Pre-compile Address Registration
The ERC20 pre-compile is registered within the FrontierPrecompiles set at the deterministic address 0x0000000000000000000000000000000000000802 (numeric ID 2050). This registration occurs in the same runtime/src/precompiles.rs file, linking the metadata to the balance-handling logic.
// runtime/src/precompiles.rs
PrecompileAt<AddressU64<2050>, Erc20BalancesPrecompile<R, NativeErc20Metadata>>,
Source: runtime/src/precompiles.rs#L105-L106
Any EVM call targeting this address is routed to Erc20BalancesPrecompile, which implements the full ERC20 API by wrapping pallet_balances operations.
The ERC20 API Implementation
The Erc20BalancesPrecompile struct in precompiles/balances-erc20/src/lib.rs provides a complete ERC20 interface. The implementation maps standard Ethereum calls to Substrate storage reads and dispatches, while maintaining a separate ApprovesStorage map for allowances since pallet_balances does not natively support them.
| ERC20 Method | Implementation Location | Description |
|---|---|---|
totalSupply() |
total_supply (~lines 94-100) |
Returns total issuance from pallet_balances |
balanceOf(address) |
balance_of (~lines 102-110) |
Returns usable balance for an EVM-mapped address |
allowance(address,address) |
allowance (~lines 113-131) |
Reads from internal ApprovesStorage double-map |
approve(address,uint256) |
approve (~lines 133-166) |
Writes allowance to ApprovesStorage |
transfer(address,uint256) |
transfer (~lines 168-200) |
Dispatches pallet_balances::transfer |
transferFrom(address,address,uint256) |
transfer_from (~lines 202-264) |
Validates allowance, then dispatches transfer |
name(), symbol(), decimals() |
Metadata trait methods | Returns static values from NativeErc20Metadata |
deposit() (payable) |
deposit (~lines 384-401) |
Wraps incoming native currency to caller's balance |
Source for implementations: precompiles/balances-erc20/src/lib.rs – see function definitions at specified line ranges.
Key Implementation Details:
- Storage: Native balances are stored by
pallet_balances. The ERC20 pre-compile reads this pallet directly and maintains a separateApprovesStoragedouble-map for allowances. - Dispatch: Calls such as
transferandtransferFromuseRuntimeHelper::try_dispatchto invoke the underlying Substrate extrinsic (pallet_balances::Call::transfer). This ensures consistent weight, fee, and event handling with standard Substrate transactions. - Gas Accounting: Each read/write incurs gas costs calculated by
RuntimeHelper::db_read_gas_cost()anddb_write_gas_cost(). Log events (Transfer,Approval,Deposit) are charged usingrecord_log_costs_manual.
Practical Integration Examples
Because the pre-compile lives at a fixed address, any Ethereum-compatible client can treat P3D as a regular ERC20 contract. The following examples demonstrate Solidity and JavaScript integration patterns.
Solidity Contract Interaction
Treat the pre-compile as a standard ERC20 contract at address 0x0000000000000000000000000000000000000802. The following Solidity example demonstrates balance queries, transfers, and the native deposit mechanism.
pragma solidity ^0.8.0;
interface IP3D {
function totalSupply() external view returns (uint256);
function balanceOf(address) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
}
address constant P3D_PRECOMPILE = 0x0000000000000000000000000000000000000802;
contract P3DIntegration {
IP3D private constant p3d = IP3D(P3D_PRECOMPILE);
function checkBalance(address account) external view returns (uint256) {
return p3d.balanceOf(account);
}
function sendP3D(address recipient, uint256 amount) external returns (bool) {
return p3d.transfer(recipient, amount);
}
function depositNative() external payable {
require(msg.value > 0, "Must send P3D");
(bool success, ) = P3D_PRECOMPILE.call{value: msg.value}("");
require(success, "Deposit failed");
}
}
Key Implementation Points:
- The contract uses the static address
0x...0802defined inruntime/src/precompiles.rs. - The
depositNativefunction demonstrates forwarding native currency to the pre-compile; the runtime'sdepositfunction (lines 384-401 inprecompiles/balances-erc20/src/lib.rs) validatesis_native_currency()and credits the amount.
JavaScript/ethers.js Integration
Connect to a 3DPass node exposing the Frontier RPC layer to query balances and execute transfers using standard ethers.js patterns.
import { ethers } from "ethers";
// Connect to 3DPass Frontier RPC endpoint
const provider = new ethers.WebSocketProvider("ws://localhost:8546");
// Pre-compile address for P3D (2050 in decimal)
const P3D_ADDRESS = "0x0000000000000000000000000000000000000802";
// Standard ERC20 ABI
const ERC20_ABI = [
"function name() view returns (string)",
"function symbol() view returns (string)",
"function decimals() view returns (uint8)",
"function totalSupply() view returns (uint256)",
"function balanceOf(address) view returns (uint256)",
"function transfer(address,uint256) returns (bool)",
"function approve(address,uint256) returns (bool)",
"function allowance(address,address) view returns (uint256)"
];
const p3d = new ethers.Contract(P3D_ADDRESS, ERC20_ABI, provider);
async function getTokenInfo() {
const [name, symbol, decimals, totalSupply] = await Promise.all([
p3d.name(),
p3d.symbol(),
p3d.decimals(),
p3d.totalSupply()
]);
console.log(`Token: ${name} (${symbol})`);
console.log(`Decimals: ${decimals}`);
console.log(`Total Supply: ${ethers.formatUnits(totalSupply, decimals)}`);
}
// Address conversion helper (Substrate SS58 to EVM H160)
import { decodeAddress } from "@polkadot/util-crypto";
function ss58ToEvm(ss58Address) {
const publicKey = decodeAddress(ss58Address);
// Take last 20 bytes of the 32-byte public key
return "0x" + Buffer.from(publicKey.slice(-20)).toString("hex");
}
// Example usage
const substrateAddress = "5F3sa2TJAWMqDhXG6jhV4N8ko9XJ5v2v9p5e9Z5xK4kK9J9y";
const evmAddress = ss58ToEvm(substrateAddress);
getTokenInfo();
Important Integration Notes:
- The provider must connect to a Frontier RPC endpoint (provided by
pallet-evm). - EVM addresses are derived from Substrate public keys; the conversion shown uses the last 20 bytes of the 32-byte Substrate public key.
Depositing Native P3D via Payable Transactions
The pre-compile supports wrapping native P3D into the ERC20 balance system through a payable deposit function. This is particularly useful for contracts that receive native currency and need to account for it within the ERC20 interface.
async function depositNativeP3D(signer, amountInP3D) {
const weiAmount = ethers.parseUnits(amountInP3D, 18);
const tx = await signer.sendTransaction({
to: P3D_ADDRESS,
value: weiAmount,
data: "0x" // Empty calldata triggers the deposit fallback
});
await tx.wait();
console.log(`Successfully deposited ${amountInP3D} P3D`);
}
// Usage with a signer
const signer = provider.getSigner();
depositNativeP3D(signer, "10");
When the transaction reaches the pre-compile, the deposit function (lines 384-401 in precompiles/balances-erc20/src/lib.rs) validates that the token is native via Metadata::is_native_currency() and credits the amount to the sender's balance in pallet_balances.
Summary
- The P3D native token is exposed as an ERC20-compatible pre-compile at address 0x0000000000000000000000000000000000000802 (ID 2050).
- The
NativeErc20Metadatastruct inruntime/src/precompiles.rsdefines the token name ("P3D token"), symbol ("P3D"), and 18 decimals. - The
Erc20BalancesPrecompileinprecompiles/balances-erc20/src/lib.rsimplements the full ERC20 interface by wrappingpallet_balancesfor storage and dispatch. - All standard methods (
totalSupply,balanceOf,transfer,approve,transferFrom) are supported, plus adepositpayable fallback for wrapping native P3D. - Gas costs are calculated using
RuntimeHelpermethods to align Substrate storage costs with EVM gas accounting.
Frequently Asked Questions
What is the contract address for the P3D ERC20 token?
The P3D ERC20 pre-compile is deployed at the deterministic address 0x0000000000000000000000000000000000000802 (decimal 2050). This address is hard-coded in the runtime at runtime/src/precompiles.rs and is accessible from any Ethereum-compatible client connected to the 3DPass network.
Can I use standard Web3 libraries like ethers.js or web3.js to interact with P3D?
Yes. Because the pre-compile implements the standard ERC20 interface, you can use any Ethereum library that supports ERC20 tokens. Instantiate a contract object using the address 0x0000000000000000000000000000000000000802 and the standard ERC20 ABI. The library will treat P3D exactly like a native Ethereum ERC20 token, including support for transfer, approve, and event logs.
How does the deposit function work for wrapping native P3D?
The deposit function is a payable fallback implemented in precompiles/balances-erc20/src/lib.rs (lines 384-401). When you send a transaction with value to the pre-compile address with empty calldata (or specifically calling the deposit function), the pre-compile validates that the token is the native currency via Metadata::is_native_currency(). It then credits the sent amount to the sender's balance in pallet_balances. This allows smart contracts to receive and account for native P3D within the ERC20 balance system.
Is there a difference between P3D held in a Substrate account versus an EVM address?
P3D balances are ultimately stored in pallet_balances, which is the canonical Substrate balance pallet. When accessing P3D through the ERC20 pre-compile, the EVM address is mapped to the corresponding Substrate account (typically via the last 20 bytes of the public key or via the H160 mapping in pallet-evm). Therefore, the balance is the same underlying native balance, just accessed through different interfaces. Transfers via the ERC20 interface dispatch the same pallet_balances::transfer extrinsic as native Substrate transfers.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →