Skip to main content

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

PropertyValue
Address0x0000000000000000000000000000000000000801
Index2049 (hash(2049))
Sourceframe/evm/precompile/shielded-pool/
Cratepallet-evm-precompile-shielded-pool
StatusImplemented (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;
ParameterTypeDescription
assetIduint32Registered asset identifier
amountuint256Token amount (native denomination)
commitmentbytes32Poseidon commitment: H(value, assetId, ownerPk, blinding)
encryptedMemobytesEncrypted 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;
ParameterTypeDescription
proofbytesGroth16 proof bytes
rootbytes32Merkle root at proof generation time
inputNullifiersbytes32[]Nullifiers of consumed notes
outputCommitmentsbytes32[]Commitments of new output notes
encryptedMemosbytes[]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;
ParameterTypeDescription
proofbytesGroth16 unshield proof bytes
rootbytes32Merkle root at proof generation time
nullifierbytes32Nullifier of the consumed note
assetIduint32Asset to withdraw
amountuint256Amount to release
recipientbytes32AccountId32 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
FunctionSelector
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 H160 is converted to AccountId32 via 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.