# How to Interact with Native Substrate Functions from Solidity on 3DPass

> Learn to interact with native Substrate functions from Solidity on 3DPass using precompile contracts for atomic EVM execution. Unlock powerful cross-chain interactions seamlessly.

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

---

**You interact with native Substrate functions from Solidity on 3DPass by using precompile contracts—fixed-address Solidity interfaces that map directly to Substrate pallet extrinsics, allowing atomic execution within the EVM environment.**

The 3DPass blockchain (repository: `3dpass/3dp`) bridges the gap between Ethereum-compatible smart contracts and native Substrate runtime logic through a specialized precompile architecture. This enables Solidity developers to leverage powerful native pallets—such as Identity and SerialNumbers—without leaving the EVM environment or managing complex cross-chain messaging.

## Understanding Precompile Contracts on 3DPass

Precompiles on 3DPass are system contracts deployed at hard-coded H160 addresses (20-byte Ethereum-style addresses). Each precompile corresponds to a specific Substrate pallet and exposes that pallet's extrinsics as Solidity functions. When you call a precompile function from Solidity, the 3DPass runtime intercepts the call, translates it into the corresponding Substrate extrinsic, executes it atomically with your EVM transaction, and returns the result to your contract.

### How Precompile Addresses Work

Each precompile occupies a fixed address derived from the pallet's module ID. For example, the Identity pallet precompile resides at `0x0000000000000000000000000000000000000904`, while the SerialNumbers pallet uses its own distinct constant address. These addresses are exported as constants in the Solidity interface files:

- `IDENTITY_ADDRESS` in `precompiles/identity/Identity.sol`
- `SERIAL_NUMBERS_ADDRESS` in `precompiles/serial-numbers/SerialNumbers.sol`

### Custom Selectors and Runtime Routing

Every function in a precompile interface is annotated with a `@custom:selector` tag followed by a hexadecimal value. This selector serves as the function discriminator that the 3DPass runtime uses to route your Solidity call to the correct pallet extrinsic. For instance, when you call `setIdentity` in Solidity, the runtime recognizes the specific selector and invokes the corresponding `set_identity` extrinsic in the Substrate Identity pallet.

## Step-by-Step: Interact with Native Substrate Functions from Solidity

To interact with native Substrate functions from Solidity on 3DPass, follow this workflow:

1. **Import the precompile interface** – Include the appropriate Solidity file from the `precompiles/` directory (e.g., `Identity.sol` or `SerialNumbers.sol`).
2. **Use the constant contract instance** – The interface files define ready-to-use constants like `IDENTITY_CONTRACT` and `SERIAL_NUMBERS_CONTRACT` that point to the fixed precompile addresses.
3. **Call Solidity functions** – Invoke the functions defined in the interface; these map directly to Substrate extrinsics using the custom selectors.
4. **Handle return values and events** – Process the returned structs or emitted events exactly as you would with standard Solidity contracts.

## Practical Code Examples

### Querying Identity Information

To read identity data from the native Substrate Identity pallet, import `Identity.sol` and call the `identity` function using the pre-defined contract constant:

```solidity
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.3;

import "precompiles/identity/Identity.sol";

contract MyIdentityReader {
    function getMyIdentity() external view returns (Identity.Registration memory reg) {
        // Calls the precompile at IDENTITY_ADDRESS (0x000...0904)
        reg = IDENTITY_CONTRACT.identity(msg.sender);
    }
}

```

This call retrieves the full `Registration` struct from the pallet's storage, including all identity fields and judgments, without requiring an off-chain worker or oracle.

### Creating Serial Numbers

To write data to the SerialNumbers pallet—creating a new serial number entry—use the `SerialNumbers.sol` precompile:

```solidity
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.3;

import "precompiles/serial-numbers/SerialNumbers.sol";

contract SerialNumberMinter {
    function mint(uint32 blockIdx) external returns (bytes16 snHash, uint64 snIdx) {
        // Maps to the create_serial_number extrinsic via custom selector
        (snHash, snIdx, ) = SERIAL_NUMBERS_CONTRACT.createSerialNumber(blockIdx);
    }
}

```

The `createSerialNumber` function translates into the native `create_serial_number` extrinsic. The transaction executes atomically—if the EVM transaction reverts, the Substrate state change rolls back as well.

### Setting Identity Data

To update identity information on-chain, construct the `IdentityInfo` struct and pass it to `setIdentity`:

```solidity
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.3;

import "precompiles/identity/Identity.sol";

contract IdentityWriter {
    function setMyIdentity(Identity.IdentityInfo calldata info) external {
        // Invokes the set_identity extrinsic in the Substrate runtime
        IDENTITY_CONTRACT.setIdentity(info);
    }
}

```

The Solidity struct layout mirrors the Substrate storage struct, ensuring type compatibility between the EVM and native runtime environments.

### Listening to Precompile Events

Precompiles emit events that correspond to pallet events. You can listen to these in your Solidity contracts or dApp frontends:

```solidity
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.3;

import "precompiles/identity/Identity.sol";

contract IdentityWatcher {
    // Mirrors the IdentitySet event from the pallet
    event IdentitySet(address indexed who);
    
    constructor() {
        // Subscribe to events emitted by IDENTITY_CONTRACT
    }
}

```

The `IdentitySet` event selector matches the pallet's event definition, allowing standard Ethereum tooling like Ethers.js or Web3.js to capture state changes originating from native Substrate logic.

## Key Precompile Interfaces and File Paths

The 3DPass repository organizes precompiles under the `precompiles/` directory. Here are the critical interfaces for interacting with native Substrate functions:

| File | Role | Key Constants |
|------|------|---------------|
| `precompiles/identity/Identity.sol` | Interface to the native **Identity** pallet | `IDENTITY_ADDRESS`, `IDENTITY_CONTRACT` |
| `precompiles/serial-numbers/SerialNumbers.sol` | Interface to the **SerialNumbers** pallet | `SERIAL_NUMBERS_ADDRESS`, `SERIAL_NUMBERS_CONTRACT` |
| `precompiles/assets-erc20/ERC20.sol` | Standard ERC‑20 interface for asset pallets | Standard ERC-20 methods |
| `precompiles/proxy/Proxy.sol` | Generic proxy pattern for forwarding calls | Proxy delegation methods |
| `precompiles/utils/tests/solidity_test.sol` | Reference for custom selectors and complex args | Selector examples |

## Gas Accounting and Performance Considerations

When you interact with native Substrate functions from Solidity on 3DPass, gas costs reflect the underlying pallet's computational weight. The runtime converts Substrate weight units to EVM gas at the precompile boundary, ensuring fair pricing for native operations. Because precompile calls execute synchronously within the same block, they offer significant performance advantages over cross-chain messaging or off-chain oracle patterns—state changes commit atomically with your EVM transaction, eliminating latency and bridge risks.

## Summary

- **Precompile contracts** bridge Solidity and Substrate by exposing pallet APIs at fixed H160 addresses like `0x0000000000000000000000000000000000000904`.
- **Import interfaces** from `precompiles/identity/Identity.sol` or `precompiles/serial-numbers/SerialNumbers.sol` to access native functions.
- **Use constant instances** such as `IDENTITY_CONTRACT` and `SERIAL_NUMBERS_CONTRACT` to call methods that map to Substrate extrinsics via `@custom:selector` annotations.
- **Execute atomically**—precompile calls run within the EVM transaction context, ensuring state consistency between Solidity and Substrate without cross-chain latency.

## Frequently Asked Questions

### How do I find the correct precompile address for a specific Substrate pallet?

Each precompile address is hard-coded in the corresponding Solidity interface file as a constant. For example, `Identity.sol` defines `IDENTITY_ADDRESS` as `0x0000000000000000000000000000000000000904`, while `SerialNumbers.sol` defines `SERIAL_NUMBERS_ADDRESS` at a different fixed address. Check the specific interface file in the `precompiles/` directory to obtain the correct H160 address for your target pallet.

### What is the purpose of the `@custom:selector` annotation in precompile interfaces?

The `@custom:selector` annotation specifies the hexadecimal function selector that the 3DPass runtime uses to route your Solidity call to the correct Substrate extrinsic. When you call a function like `setIdentity` in Solidity, the runtime inspects the selector defined in the annotation (e.g., `@custom:selector 0x12345678`) and invokes the corresponding `set_identity` extrinsic in the Identity pallet. This mapping ensures type-safe translation between EVM calls and native runtime functions.

### Can I revert a Substrate state change if my Solidity transaction fails?

Yes. Precompile calls execute atomically within the EVM transaction context. If your Solidity contract reverts due to a `require` statement, out-of-gas error, or explicit `revert()`, the entire transaction rolls back—including any state changes made by the precompile to the Substrate pallet storage. This atomicity guarantees consistency between your EVM contract state and the native Substrate state, eliminating the need for manual compensation logic or two-phase commit patterns.

### How are gas costs calculated for precompile calls?

Gas costs for precompile calls are derived from the underlying Substrate pallet's computational weight. The 3DPass runtime converts the Substrate weight units (representing CPU time and storage I/O) to EVM gas units at the precompile boundary. This ensures that computationally expensive native operations (like complex identity verification or serial number generation) cost more gas than simple storage reads, while maintaining compatibility with standard Ethereum gas estimation tools.