Skip to main content
The Wraith Protocol EVM contract suite handles every on-chain operation involved in stealth payments: announcing stealth addresses, registering meta-addresses, sending assets atomically, resolving human-readable names, and sponsoring gas for recipients. The SDK wraps all of these contracts — most integrations never call them directly — but understanding their interfaces helps you build custom integrations or verify protocol behavior.

Contract set

ContractPurpose
ERC5564AnnouncerEmits stealth address announcements so recipients can scan for payments
ERC6538RegistryMaps wallet addresses to stealth meta-addresses (spend + view key pairs)
WraithSenderAtomically transfers assets and publishes announcements in one transaction
WraithNamesRegisters human-readable .wraith names that resolve to stealth meta-addresses
WraithWithdrawerEIP-7702 delegation target for gas-sponsored withdrawals from stealth addresses

Deployed addresses (Horizen Testnet)

ContractAddress
ERC5564Announcer0x8AE65c05E7eb48B9bA652781Bc0a3DBA09A484F3
ERC6538Registry0x953E6cEdcdfAe321796e7637d33653F6Ce05c527
WraithSender0x226C5eb4e139D9fa01cc09eA318638b090b12095
WraithNames0x3d46f709a99A3910f52bD292211Eb5D557F882D6
WraithWithdrawer is optional and only required on chains with EIP-7702 support. It is not deployed on Horizen Testnet by default.

ERC5564Announcer

The announcer is a minimal singleton with no storage and no access control. Its only job is to emit the Announcement event whenever a stealth payment is made. Recipients scan these events to discover payments sent to them.

Event

event Announcement(
    uint256 indexed schemeId,
    address indexed stealthAddress,
    address indexed caller,
    bytes ephemeralPubKey,
    bytes metadata  // first byte = view tag
);

Function

function announce(
    uint256 schemeId,
    address stealthAddress,
    bytes memory ephemeralPubKey,
    bytes memory metadata
) external;
The schemeId identifies the cryptographic scheme. Use the SCHEME_ID constant exported by the SDK — do not hardcode a value.

Usage

import { SCHEME_ID } from "@wraith-protocol/sdk/chains/evm";

// After generating a stealth address for a recipient, announce it on-chain
await announcer.write.announce([
  SCHEME_ID,
  stealthAddress,
  ephemeralPubKey,
  metadata, // view tag as first byte
]);
Use WraithSender instead of calling the announcer directly. It combines the token transfer and announcement into one atomic transaction, which prevents announcements without actual payments.

ERC6538Registry

The registry stores stealth meta-addresses per account. A meta-address is a pair of compressed public keys (spending key + viewing key) that senders use to generate stealth addresses for a recipient. The registry supports direct registration and delegated registration via EIP-712 signatures.

Functions

// Register your own meta-address
function registerKeys(uint256 schemeId, bytes calldata stealthMetaAddress) external;

// Register on behalf of another address using an EIP-712 signature
function registerKeysOnBehalf(
    address registrant,
    uint256 schemeId,
    bytes calldata stealthMetaAddress,
    bytes calldata signature
) external;

// Look up a registered meta-address
function stealthMetaAddressOf(
    address registrant,
    uint256 schemeId
) external view returns (bytes memory);

// Replay protection for delegated registration
function incrementNonce(address registrant) external;
function nonceOf(address registrant) external view returns (uint256);

Usage

import { SCHEME_ID, metaAddressToBytes } from "@wraith-protocol/sdk/chains/evm";

// Encode your meta-address to the 66-byte on-chain format
const metaBytes = metaAddressToBytes(metaAddress);

// Register directly
await registry.write.registerKeys([SCHEME_ID, metaBytes]);

WraithSender

WraithSender combines asset transfer and announcement into a single atomic transaction. This guarantees that every announced stealth address actually received funds — and that senders can’t announce without transferring. The contract uses ReentrancyGuard.

Functions

// Send native ETH to a stealth address
function sendETH(
    uint256 schemeId,
    address stealthAddress,
    bytes memory ephemeralPubKey,
    bytes memory metadata
) external payable;

// Send ERC-20 tokens with an optional ETH tip for recipient gas
function sendERC20(
    uint256 schemeId,
    address stealthAddress,
    bytes memory ephemeralPubKey,
    bytes memory metadata,
    address token,
    uint256 amount,
    uint256 gasTip  // optional ETH tip deposited at stealth address
) external payable;

// Batch send native ETH to multiple stealth addresses
function batchSendETH(
    uint256 schemeId,
    address[] calldata stealthAddresses,
    bytes[] calldata ephemeralPubKeys,
    bytes[] calldata metadatas,
    uint256[] calldata amounts
) external payable;
The constructor takes the announcer contract address. When deploying a custom instance, pass the ERC5564Announcer address.

Usage

import { generateStealthAddress, SCHEME_ID } from "@wraith-protocol/sdk/chains/evm";

// Generate a stealth address for the recipient
const stealth = generateStealthAddress(spendingPubKey, viewingPubKey);

// Encode the view tag as the first byte of metadata
const metadata = `0x${stealth.viewTag.toString(16).padStart(2, "0")}`;

// Send 0.1 ETH and emit the announcement atomically
await sender.write.sendETH(
  [SCHEME_ID, stealth.stealthAddress, stealth.ephemeralPubKey, metadata],
  { value: parseEther("0.1") }
);

WraithNames

WraithNames maps human-readable names (like alice) to stealth meta-addresses. Ownership is proven by an ECDSA signature from the spending key embedded in the meta-address. The contract performs on-chain public key decompression and signature verification, so no trusted third party is involved.

Functions

function register(
    string calldata name,
    bytes calldata metaAddress,
    bytes calldata signature
) external;

function registerOnBehalf(
    string calldata name,
    bytes calldata metaAddress,
    bytes calldata signature
) external;

function update(
    string calldata name,
    bytes calldata newMetaAddress,
    bytes calldata signature
) external;

function release(
    string calldata name,
    bytes calldata signature
) external;

// Forward lookup: name → meta-address
function resolve(string calldata name) external view returns (bytes memory);

// Reverse lookup: meta-address → name
function nameOf(bytes calldata metaAddress) external view returns (string memory);

Name rules

Names must follow these constraints:
  • Length: 3–32 characters
  • Characters: Lowercase alphanumeric (a-z, 0-9) and hyphens (-)
  • Hyphens: Not allowed at the start or end of the name
Names that violate these rules will cause the register call to revert. Validate names client-side before submitting a transaction.

Signature verification

The contract decompresses the spending public key from the first 33 bytes of the meta-address and verifies an ECDSA signature over keccak256(name || metaAddress) with the Ethereum signed message prefix. This includes on-chain elliptic curve point decompression (y = sqrt(x³ + 7) mod p).

Usage

import {
  signNameRegistration,
  metaAddressToBytes,
} from "@wraith-protocol/sdk/chains/evm";

// Encode and sign
const metaBytes = metaAddressToBytes(metaAddress);
const sig = signNameRegistration("alice", metaBytes, keys.spendingKey);

// Register on-chain
await names.write.register(["alice", metaBytes, sig]);

WraithWithdrawer

WraithWithdrawer is an EIP-7702 delegation target that allows a sponsor to pay gas on behalf of a stealth address. The stealth address owner authorizes the contract via an EIP-7702 delegation, and a relayer submits the withdrawal, paying gas and optionally collecting a fee.
EIP-7702 is required for sponsored withdrawals. Check that your target chain supports EIP-7702 before using WraithWithdrawer.

Functions

// Sponsored: a relayer pays gas and collects sponsorFee from the stealth balance
function withdrawETH(address payable to, uint256 sponsorFee) external;
function withdrawERC20(address token, address to, uint256 sponsorFee) external;

// Direct: the stealth address pays its own gas
function withdrawETHDirect(address payable to) external;
function withdrawERC20Direct(address token, address to) external;

Deployment

Deploy contracts in this order. Each step depends on the previous one completing successfully.
1

Deploy ERC5564Announcer

No constructor arguments. Deterministic — deploy the same bytecode on every chain.
npx hardhat run scripts/deploy.ts --network <chain>
2

Deploy ERC6538Registry

No constructor arguments.
3

Deploy WraithSender

Pass the ERC5564Announcer address as the constructor argument.
4

Deploy WraithNames

No constructor arguments.
5

Deploy WraithWithdrawer (optional)

Only required on chains with EIP-7702 support. No constructor arguments.

Indexing announcements

Set up a subgraph to index Announcement events and make them queryable for payment scanning:
type Announcement @entity {
  id: ID!
  schemeId: BigInt!
  stealthAddress: Bytes!
  caller: Bytes!
  ephemeralPubKey: Bytes!
  metadata: Bytes!
  blockNumber: BigInt!
  transactionHash: Bytes!
}