ShieldedPool Precompile
Address: 0x0000000000000000000000000000000000000801
The ShieldedPool precompile exposes the three core operations of pallet-shielded-pool to EVM
clients. It allows MetaMask users and Solidity contracts to deposit tokens into the shielded pool,
execute private transfers, and withdraw tokens — all using standard Ethereum transactions.
Overview
| Property | Value |
|---|---|
| Address | 0x0000000000000000000000000000000000000801 |
| Index | 2049 (hash(2049)) |
| Source | frame/evm/precompile/shielded-pool/ |
| Crate | pallet-evm-precompile-shielded-pool |
| Status | Implemented (MVP) |
Caller Identity
The precompile derives the Substrate AccountId32 of the caller from the EVM H160 sender using
AddressMapping. EVM accounts are structurally embedded in Substrate accounts:
AccountId32 = H160 ++ [0x00; 12]
This means the same key controls both the EVM 0xABC… address and the corresponding Substrate
5… address. No explicit linking is required for the precompile to work.
Functions
shield
Deposits public tokens into the shielded pool and creates a commitment.
function shield(
uint32 assetId,
uint256 amount,
bytes32 commitment,
bytes calldata encryptedMemo
) external;
| Parameter | Type | Description |
|---|---|---|
assetId | uint32 | Registered asset identifier |
amount | uint256 | Token amount (native denomination) |
commitment | bytes32 | Poseidon commitment: H(value, assetId, ownerPk, blinding) |
encryptedMemo | bytes | Encrypted note data, max 104 bytes |
The caller's EVM balance is reduced by amount. A new leaf is appended to the Merkle tree.
privateTransfer
Executes a private transfer between shielded notes. Requires a valid Groth16 ZK proof.
function privateTransfer(
bytes calldata proof,
bytes32 root,
bytes32[] calldata inputNullifiers,
bytes32[] calldata outputCommitments,
bytes[] calldata encryptedMemos
) external;
| Parameter | Type | Description |
|---|---|---|
proof | bytes | Groth16 proof bytes |
root | bytes32 | Merkle root at proof generation time |
inputNullifiers | bytes32[] | Nullifiers of consumed notes |
outputCommitments | bytes32[] | Commitments of new output notes |
encryptedMemos | bytes[] | Encrypted data for each output note |
The proof is verified by pallet-zk-verifier. Input nullifiers are marked as spent. Output
commitments are appended to the Merkle tree.
unshield
Withdraws tokens from the shielded pool to a public account. Requires a valid Groth16 ZK proof.
function unshield(
bytes calldata proof,
bytes32 root,
bytes32 nullifier,
uint32 assetId,
uint256 amount,
bytes32 recipient
) external;
| Parameter | Type | Description |
|---|---|---|
proof | bytes | Groth16 unshield proof bytes |
root | bytes32 | Merkle root at proof generation time |
nullifier | bytes32 | Nullifier of the consumed note |
assetId | uint32 | Asset to withdraw |
amount | uint256 | Amount to release |
recipient | bytes32 | AccountId32 of the recipient |
Function Selectors
Computed as bytes4(keccak256("functionSignature")) and verified with ethers.js v6:
ethers.id("shield(uint32,uint256,bytes32,bytes)").slice(0, 10) // 0x781442b9
ethers.id("privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[])").slice(0, 10) // 0xdcd5b898
ethers.id("unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32)").slice(0, 10) // 0xdcf1bff2
| Function | Selector |
|---|---|
shield(uint32,uint256,bytes32,bytes) | 0x781442b9 |
privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[]) | 0xdcd5b898 |
unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32) | 0xdcf1bff2 |
Solidity Interface
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.20;
interface IShieldedPool {
function shield(
uint32 assetId,
uint256 amount,
bytes32 commitment,
bytes calldata encryptedMemo
) external;
function privateTransfer(
bytes calldata proof,
bytes32 root,
bytes32[] calldata inputNullifiers,
bytes32[] calldata outputCommitments,
bytes[] calldata encryptedMemos
) external;
function unshield(
bytes calldata proof,
bytes32 root,
bytes32 nullifier,
uint32 assetId,
uint256 amount,
bytes32 recipient
) external;
}
Usage Example
import { ethers } from "ethers";
// ABI (abbreviated)
const abi = [
"function shield(uint32, uint256, bytes32, bytes)",
"function privateTransfer(bytes, bytes32, bytes32[], bytes32[], bytes[])",
"function unshield(bytes, bytes32, bytes32, uint32, uint256, bytes32)",
];
const SHIELDED_POOL = "0x0000000000000000000000000000000000000801";
const pool = new ethers.Contract(SHIELDED_POOL, abi, signer);
// Shield 1 ORB
const commitment = ethers.hexlify(/* Poseidon(value, assetId, pk, r) */);
const memo = ethers.hexlify(/* encrypted note */);
await pool.shield(0, ethers.parseEther("1"), commitment, memo);
Dispatch Flow
EVM Transaction (H160 sender)
└── precompile address: 0x…0801
└── ShieldedPoolPrecompile::execute(handle)
├── decode selector
├── decode calldata (ABI)
├── H160 → AccountId32 (AddressMapping)
└── dispatch pallet_shielded_pool::Call::shield / private_transfer / unshield
└── pallet-zk-verifier (for transfer/unshield)
Security Considerations
- The caller's
H160is converted toAccountId32via structural mapping. The EVM transaction signature guarantees caller identity. - The precompile does not bypass ZK proof verification. All proofs are still validated by
pallet-zk-verifier. - Double-spend prevention is enforced by the nullifier set in
pallet-shielded-pool. - Historic Merkle roots are retained to allow proofs generated before a tree update to remain valid.
Related
- AccountMapping Precompile (
0x…0800) — link EVM and Substrate accounts - Precompiles Overview — full precompile listing
- Privacy Architecture — ZK proof system overview