# How to Interact with the Native P3D Token from an ERC20 Perspective on 3DPass

> Interact with the native P3D token from an ERC20 perspective on 3DPass. Use standard Web3 libraries to query balances, execute transfers, and manage allowances easily.

- Repository: [3Dpass/3dp](https://github.com/3dpass/3dp)
- Tags: how-to-guide
- Published: 2026-02-23

---

**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`](https://github.com/3dpass/3dp/blob/main/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.

```rust
// 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](https://github.com/3dpass/3dp/blob/main/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`](https://github.com/3dpass/3dp/blob/main/runtime/src/precompiles.rs) file, linking the metadata to the balance-handling logic.

```rust
// runtime/src/precompiles.rs
PrecompileAt<AddressU64<2050>, Erc20BalancesPrecompile<R, NativeErc20Metadata>>,

```

*Source: [runtime/src/precompiles.rs#L105-L106](https://github.com/3dpass/3dp/blob/main/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`](https://github.com/3dpass/3dp/blob/main/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`](https://github.com/3dpass/3dp/blob/main/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 separate `ApprovesStorage` double-map for allowances.
- **Dispatch**: Calls such as `transfer` and `transferFrom` use `RuntimeHelper::try_dispatch` to 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()` and `db_write_gas_cost()`. Log events (`Transfer`, `Approval`, `Deposit`) are charged using `record_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.

```solidity
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...0802` defined in [`runtime/src/precompiles.rs`](https://github.com/3dpass/3dp/blob/main/runtime/src/precompiles.rs).
- The `depositNative` function demonstrates forwarding native currency to the pre-compile; the runtime's `deposit` function (lines 384-401 in [`precompiles/balances-erc20/src/lib.rs`](https://github.com/3dpass/3dp/blob/main/precompiles/balances-erc20/src/lib.rs)) validates `is_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.

```javascript
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.

```javascript
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`](https://github.com/3dpass/3dp/blob/main/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 `NativeErc20Metadata` struct in [`runtime/src/precompiles.rs`](https://github.com/3dpass/3dp/blob/main/runtime/src/precompiles.rs) defines the token name ("P3D token"), symbol ("P3D"), and 18 decimals.
- The `Erc20BalancesPrecompile` in [`precompiles/balances-erc20/src/lib.rs`](https://github.com/3dpass/3dp/blob/main/precompiles/balances-erc20/src/lib.rs) implements the full ERC20 interface by wrapping `pallet_balances` for storage and dispatch.
- All standard methods (`totalSupply`, `balanceOf`, `transfer`, `approve`, `transferFrom`) are supported, plus a `deposit` payable fallback for wrapping native P3D.
- Gas costs are calculated using `RuntimeHelper` methods 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`](https://github.com/3dpass/3dp/blob/main/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`](https://github.com/3dpass/3dp/blob/main/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.