voidcashdocs

Contracts

Every external function, event, struct and error of PrivateVault and FeeCollector, with the Volta trading code and the interfaces they use.

voidcash has two contracts of its own on Elysium. PrivateVault holds the shielded HYPE and listed ERC-20 tokens and runs behind an ERC-1967 proxy, so it can be upgraded. FeeCollector receives the HYPE exit fees and is deployed once, without a proxy. The verifier and the two Poseidon2 hashers are fixed in the vault's implementation. VOID is not a voidcash contract: it is a Volta launch, and the one token the vault lists (see VOID).

ContractRole
PrivateVaultShielded HYPE and listed tokens, notes, nullifiers, the key registry, exits and their hold, the per-token books, and the ERC-4337 account of private actions. Behind an ERC1967Proxy (UUPS)
PoseidonMerkleTreeThe note tree, inherited by the vault
Poseidon2Hash4The four-input Poseidon2 hash of token note commitments. Stateless, no owner
FeeCollectorSplits the HYPE it receives: half to the team, half to buying VOID and burning it
VoltaSwapper, VoltaMarketTrading and pricing on VOID's Volta pool, inherited by the fee collector
HonkVerifierThe UltraHonk verifier of the transact circuit, generated by Barretenberg 5.2.1. Covered here by its interface only

Addresses are on Deployments; how the pieces fit is on Architecture.

Solidity 0.8.38 (pragma ^0.8.26), compiled for the Cancun EVM, with OpenZeppelin Contracts and Contracts Upgradeable 5.7.0. The vault is compiled at 200 optimizer runs to fit the 24,576-byte limit; the verifier and every other contract at 10,000.

PrivateVault

A private balance of HYPE and of listed ERC-20 tokens. Shielding moves an asset into the vault and creates an encrypted note; inside, notes are spent with a zero-knowledge proof and a signature by the owner's wallet; unshielding pays the asset to any address. A note holds an amount of one asset in its base units, HYPE in wei and a token in its smallest unit: the circuit calls it shares. A transaction spends three notes and creates four: a main pair of one private asset, a HYPE fee pair that pays a token operation's gas, and a zero-value HYPE pad, so that every transaction inserts two pairs of leaves. The vault inherits PoseidonMerkleTree, OpenZeppelin EIP712, ReentrancyGuardTransient, UUPSUpgradeable, AccessControlUpgradeable and PausableUpgradeable, and implements ERC-4337's IAccount.

Proxy and initializer

The vault's address, 0x5535fa0d95E4e03783C3A5d693E2f7225e6d23d4, is an OpenZeppelin ERC1967Proxy. It holds all the state and delegates every call to the current implementation (0x45648A8aE69C2c4EAE0dc5727E66F8921F771181 at deployment). Users, integrators and the app only use the proxy's address.

// the implementation
constructor(
    string memory name_,      // EIP-712 domain name: "voidcash"
    IVerifier verifier_,
    IPoseidon2 hasher_,       // the tree's and HYPE notes' hasher
    IPoseidon2Hash4 hasher4_, // token note commitments
    IEntryPoint entryPoint_
);

// the proxy's state, written once through the proxy's constructor
function initialize(
    uint256 largeExitThreshold_,  // 1,000 HYPE at deployment
    address feeCollector_,
    address admin,                // DEFAULT_ADMIN_ROLE
    address upgrader,             // UPGRADER_ROLE
    address guardian_             // GUARDIAN_ROLE
) external;

The constructor reverts ZeroAddress without a verifier, a four-input hasher or an EntryPoint, and WrongHasher unless hasher4_.hash_4(1, 2, 3, 4) returns Noir's value for the same inputs. The tree's hasher answers a four-argument call with a wrong hash instead of reverting, so this check keeps it from being wired in by mistake. The constructor also disables the implementation's initializers, so the implementation itself can never be initialized. verifier(), hasher(), hasher4() and entryPoint() are immutables of the implementation: a new verifier comes with a new implementation.

initialize runs once (InvalidInitialization after that). It reverts ZeroAddress if a role address or the fee collector is zero, and ThresholdOutOfRange if the threshold is outside its range. It writes the empty note tree, sets the threshold (LargeExitThresholdSet) and the fee collector (FeeCollectorSet), and grants the three roles (RoleGranted for each).

An upgrade keeps everything in the proxy's storage: the notes, the tree, the nullifiers, the registered keys, the pending exits and the totals. The EntryPoint deposit and the EIP-712 domain belong to the proxy's address, so signatures stay valid. A new implementation may only append storage variables: v2 appended tokenConfig, tokenBook and tokenFeeRecipient, and the asset of an Exit takes a slot of its own, so exits created before v2 read it as 0, HYPE. The EIP-712 version is "1", and eip712Domain() names the proxy as the verifying contract.

Structs

struct Transaction {
    uint256 root;            // a root of the note tree the proof was made against
    uint256[3] nullifiers;   // the main pair, then the HYPE fee note (dummies included)
    uint256[4] commitments;  // the main pair, the HYPE fee change, the zero-value pad
    address exitAsset;       // 0 = HYPE, or nothing leaves the main pair; otherwise a listed token
    uint256 exitAmount;      // in exitAsset units, out of the main pair
    uint256 feeAmount;       // wei of HYPE out of the fee pair
    ExtData ext;
}

/// Bound to the proof through extDataHash: nobody can change it after the wallet signs.
struct ExtData {
    address recipient;   // receives the exit, less the gas fee and the exit fee
    address caller;      // if non-zero, only this address may submit (the vault itself for ERC-4337)
    uint256 gasFee;      // wei of HYPE leaving: the operation's gas (ERC-4337 only; zero for transact)
    bytes data;          // ERC-4337: abi.encode(uint256 minCallGasLimit)
    bytes ciphertext0;   // encrypted data of output note 0 (main pair)
    bytes ciphertext1;   // output note 1 (main pair)
    bytes ciphertext2;   // output note 2 (the HYPE fee change)
    bytes ciphertext3;   // output note 3 (the pad)
}

struct Keys {
    uint256 opk;       // owner public key, H(nk, wallet key)
    bytes32 viewKey;   // X25519 public key for note encryption
}

/// An exit waiting to be paid: in execution of its UserOperation, or after LARGE_EXIT_DELAY when held.
struct Exit {
    uint128 amount;     // what the recipient receives, in the asset's base units
    uint64 payableAt;   // when it can be paid
    bool stopped;       // stopped by the guardian
    address recipient;
    address asset;      // 0 = HYPE, otherwise the token (added in v2)
}

/// A listed token's settings.
struct TokenConfig {
    bool listed;
    bool entriesPaused;          // closed to new deposits
    uint256 largeExitThreshold;  // a net exit above it waits LARGE_EXIT_DELAY
    uint256 windowCap;           // net exits paid at once per TOKEN_WINDOW; an exit that would exceed it waits too
}

/// A token's books. Invariant: token.balanceOf(vault) >= backing + pendingExits + feesAccrued.
struct TokenBook {
    uint256 backing;       // the token held by notes
    uint256 pendingExits;  // decided, not yet paid
    uint256 feesAccrued;   // exit fees, for the token fee recipient
    uint64 windowStart;    // start of the current window
    uint256 windowOut;     // net exits payable at once, created in the current window
}

What a spend moves. The HYPE leaving the notes is hypeOut = feeAmount + (exitAsset == 0 ? exitAmount : 0); the gas fee comes out of it first, and out = hypeOut - ext.gasFee is the HYPE exit:

PartAmountGoes to
Gas feeext.gasFee, in HYPEThe vault's EntryPoint deposit (ERC-4337 only)
HYPE exit feeout * EXIT_FEE_BPS / 10_000 (1%, rounded down)feesAccrued, then the fee collector
HYPE exitout less the exit feeext.recipient, at once or after the hold
Token exit feeexitAmount * EXIT_FEE_BPS / 10_000 of exitAsset, when it is a tokentokenBook(token).feesAccrued, then the token fee recipient
Token exitexitAmount less the exit feeext.recipient, at once or after the hold
NotesThe rest of the inputsThe four output notes

A HYPE operation pays its gas out of the main pair and leaves the fee pair empty (feeAmount == 0). A token operation pays its gas out of the fee pair: a token exit must have feeAmount == ext.gasFee (FeeAmountNotGasFee), so one operation never pays out two assets. A private send, of any asset, has exitAsset == 0.

Key registry

function register(uint256 opk, bytes32 viewKey) external;
function registerFor(address owner, uint256 opk, bytes32 viewKey, uint256 deadline, bytes calldata signature) external;
function keysOf(address owner) external view returns (uint256 opk, bytes32 viewKey);
function registerNonces(address owner) external view returns (uint256);
FunctionDescription
registerRegisters the caller's owner key and viewing key. opk must be non-zero and below the field modulus (NotAFieldElement). Calling it again replaces the keys
registerForSame for owner, with an EIP-712 signature (EOA or ERC-1271) that anyone can submit. Reverts SignatureExpired after deadline and InvalidSignature for a bad signature
keysOfThe registered keys; opk == 0 means not registered
registerNoncesNext nonce of owner for registerFor

The registerFor typed data is Register(address owner,uint256 opk,bytes32 viewKey,uint256 nonce,uint256 deadline) under the vault's EIP-712 domain. Registering is needed only to receive private sends at your address. See Keys.

Roles, pause and upgrades

function DEFAULT_ADMIN_ROLE() external view returns (bytes32);   // 0x00
function UPGRADER_ROLE() external view returns (bytes32);        // keccak256("UPGRADER_ROLE")
function GUARDIAN_ROLE() external view returns (bytes32);        // keccak256("GUARDIAN_ROLE")

function hasRole(bytes32 role, address account) external view returns (bool);
function getRoleAdmin(bytes32 role) external view returns (bytes32);
function grantRole(bytes32 role, address account) external;
function revokeRole(bytes32 role, address account) external;
function renounceRole(bytes32 role, address callerConfirmation) external;
function supportsInterface(bytes4 interfaceId) external view returns (bool);

// guardian
function pause() external;
function unpause() external;
function paused() external view returns (bool);
function setEntriesPaused(bool paused) external;
function entriesPaused() external view returns (bool);
function setTokenEntriesPaused(address token, bool paused) external;
function stopExit(uint256 exitId) external;
function resumeExit(uint256 exitId) external;

// admin
function setLargeExitThreshold(uint256 threshold) external;
function largeExitThreshold() external view returns (uint256);
function setFeeCollector(address collector) external;
function feeCollector() external view returns (address);
function listToken(address token, uint256 threshold, uint256 windowCap) external;
function setTokenLimits(address token, uint256 threshold, uint256 windowCap) external;
function setTokenFeeRecipient(address recipient) external;
function tokenFeeRecipient() external view returns (address);

// upgrader
function upgradeToAndCall(address newImplementation, bytes memory data) external payable;
function proxiableUUID() external view returns (bytes32);
function UPGRADE_INTERFACE_VERSION() external view returns (string memory);   // "5.0.0"
FunctionDescription
hasRoleWhether account holds role
getRoleAdminThe role that grants and revokes role: DEFAULT_ADMIN_ROLE for all three, itself included
grantRole / revokeRoleAdmin only (AccessControlUnauthorizedAccount). A role can be held by several addresses. Emit RoleGranted / RoleRevoked
renounceRoleA holder gives up its own role; callerConfirmation must be the caller (AccessControlBadConfirmation)
pauseGuardian only. Stops every spend of a note and every payout until unpause, with no time limit: transact, releaseExit, validateUserOp and execute4337 revert EnforcedPause. Exits already decided stay in exits and can be released after the pause. Emits Paused(account)
unpauseGuardian only. Ends the pause. Emits Unpaused(account); reverts ExpectedPause if not paused
setEntriesPausedGuardian only. While true, shield and shieldToken of every token revert EntriesArePaused: one switch closes every asset. Nothing else changes. Emits EntriesPaused
setTokenEntriesPausedGuardian only, listed tokens only (TokenNotListed). Closes or reopens one token to shieldToken; HYPE and other tokens are not affected, and the token's notes stay spendable. Emits TokenEntriesPaused
stopExitGuardian only. Stops an exit not yet paid, of any asset; its amount stays reserved for it. Reverts NoSuchExit if none is pending. Emits ExitStopped
resumeExitGuardian only. Lets a stopped exit be paid again, from its original payableAt. Reverts NoSuchExit or ExitNotStopped. Emits ExitResumed
setLargeExitThresholdAdmin only. Sets the payout above which a HYPE exit waits LARGE_EXIT_DELAY, within [MIN_LARGE_EXIT_THRESHOLD, MAX_LARGE_EXIT_THRESHOLD] (ThresholdOutOfRange). Emits LargeExitThresholdSet. It is the admin's, not the guardian's, so a guardian key alone cannot open the way for large exits
setFeeCollectorAdmin only. Changes where sweepFees sends the HYPE fees (ZeroAddress for zero). Emits FeeCollectorSet
listTokenAdmin only. Lists token with its large-exit threshold and window cap, in its base units. Reverts ZeroAddress for address 0, NotAToken for an address without code, TokenAlreadyListed, and TokenLimitsOutOfRange unless threshold > 0 and windowCap >= threshold. A listed token cannot be removed. Emits TokenListed
setTokenLimitsAdmin only, listed tokens only. Changes a token's threshold and window cap, with the same bounds; exits already created keep their time. Emits TokenLimitsSet
setTokenFeeRecipientAdmin only. Sets who receives the token exit fees (ZeroAddress for zero). Emits TokenFeeRecipientSet
upgradeToAndCallUpgrader only, through the proxy only (UUPSUnauthorizedCallContext otherwise). Points the proxy at newImplementation at once, then delegatecalls data on it if data is not empty. Emits Upgraded(newImplementation)
proxiableUUIDThe ERC-1967 implementation slot. Answers on the implementation only

While the vault is paused, shield, shieldToken, register, registerFor, sweepFees, sweepTokenFees, addStake, the role and admin functions, and every read keep working. The guardian's functions never move funds. The upgrader's do not either, but the implementation it installs decides everything the vault does from then on. See Trust model.

Private side

function shield(uint256 stub, bytes calldata ciphertext) external payable returns (uint32 index);
function shieldToken(address token, uint256 amount, uint256 stub, bytes calldata ciphertext) external returns (uint32 index);
function transact(Transaction calldata t, bytes calldata proof) external;
function releaseExit(uint256 exitId) external;
function sweepFees() external;
function sweepTokenFees(address token) external;
receive() external payable;
FunctionDescription
shieldMoves msg.value HYPE into a note completed with stub = H(opk, rho, r), paired with an empty leaf, and returns its leaf index. No fee. Reverts EntriesArePaused while entries are paused, NotAFieldElement for a stub at or above the field modulus, ZeroAmount with no value and AmountTooLarge at or above 2^120 wei
shieldTokenMoves amount of a listed token from the caller, who approved the vault first, into a note H(3, stub, amount, token) paired with an empty leaf. No fee. Reverts TokenNotListed, EntriesArePaused while the global or the token's entries pause is on, NotAFieldElement, ZeroAmount, AmountTooLarge at or above 2^120 base units, and TokenAmountMismatch unless the vault's balance grew by exactly amount, which refuses fee-on-transfer tokens
transactSpends three notes (real or dummy), creates four and optionally exits HYPE or a listed token to ext.recipient. Anyone can submit it, paying their own gas, unless ext.caller names someone else (WrongCaller). ext.caller may not be the vault and ext.gasFee must be zero (GasFeeNotAllowed): a submitter is never paid from the notes. The exit is paid in the same transaction unless held; if the recipient refuses the HYPE (HypeTransferFailed) or the token transfer fails (SafeERC20FailedOperation), the whole transaction reverts
releaseExitAnyone. Pays a held exit once its payableAt has passed, or an exit its execution step could not pay, to its recipient, in its asset. Reverts NoSuchExit, ExitIsStopped or ExitNotDue, and if the payout fails
sweepFeesAnyone. Withdraws the vault's whole EntryPoint deposit (gas that operations paid and did not use) and sends it, with the HYPE feesAccrued, to feeCollector. Does nothing when both are zero. Emits FeesSwept
sweepTokenFeesAnyone. Sends a token's accrued exit fees to tokenFeeRecipient. Reverts ZeroAddress while no recipient is set; does nothing when there are none. Emits TokenFeesSwept
receiveAccepts HYPE only from the EntryPoint, a withdrawal of the deposit (NotEntryPoint otherwise). HYPE enters the notes only through shield

shield, shieldToken, transact, releaseExit, sweepFees, sweepTokenFees and execute4337 are nonReentrant (OpenZeppelin ReentrancyGuardTransient): a recipient or a token cannot re-enter any of them while it is being paid. Token transfers go through OpenZeppelin SafeERC20.

A shield's amount and asset are public, in Shielded or TokenShielded. A spend's amounts are not, except what leaves the notes: exitAmount, exitAsset and feeAmount are public inputs of the proof. See Notes and The circuit.

ERC-4337 account

function validateUserOp(PackedUserOperation calldata op, bytes32 userOpHash, uint256 missingAccountFunds)
    external returns (uint256 validationData);
function execute4337(uint256 exitId) external;
function addStake(uint32 unstakeDelaySec) external payable;
FunctionDescription
validateUserOpEntryPoint only (NotEntryPoint); reverts EnforcedPause while paused. Decodes abi.encode(Transaction, proof) from op.signature and applies the whole private transaction: nullifiers, new notes, the exit's record. It deposits ext.gasFee into the vault's EntryPoint deposit, which the EntryPoint then takes the prefund from. Returns 1 (failed signature) for an invalid proof without writing anything, so wallets can estimate gas with a placeholder proof
execute4337EntryPoint only, not while paused. Pays the operation's exit, unless it is held, stopped or already paid. Never reverts on a recipient that refuses the HYPE or a token transfer that fails (trySafeTransfer): the exit then stays for releaseExit
addStakeAnyone can add EntryPoint stake for the vault. Nothing can unlock it

The operation must satisfy all of these, in this order, or validation reverts:

  • the vault is not paused (EnforcedPause);
  • the caller is the EntryPoint (NotEntryPoint);
  • ext.caller is the vault itself (WrongCaller);
  • no initCode and no paymasterAndData (InvalidUserOp);
  • callData is exactly execute4337(t.nullifiers[0]) (InvalidUserOp);
  • nonce is uint256(uint192(t.nullifiers[0])) << 64: one nonce key per first nullifier, so operations of different users never queue behind each other (InvalidUserOp);
  • callGasLimit is at least the minCallGasLimit in ext.data (InvalidUserOp);
  • ext.gasFee is at least the operation's maximum cost, (verificationGasLimit + callGasLimit + preVerificationGas) * maxFeePerGas (GasFeeBelowCost);
  • the transaction's own checks: a known root, three pairwise distinct and unspent nullifiers, field elements, exitAmount and feeAmount below 2^120, ext.gasFee at most the HYPE leaving, a recipient for any exit, and for a token exit a listed token, a non-zero exitAmount and feeAmount == ext.gasFee.

Each operation pays its own gas in HYPE from the notes it spends, a token operation from its HYPE fee note; the deposit never pays for anyone. What it pays and does not use stays in the deposit until sweepFees. See Gas.

Exits, views and constants

function exits(uint256 exitId)
    external view returns (uint128 amount, uint64 payableAt, bool stopped, address recipient, address asset);
function exitFeeOf(uint256 amount) external pure returns (uint256 fee, uint256 net);
function totalBacking() external view returns (uint256);
function pendingExitTotal() external view returns (uint256);
function feesAccrued() external view returns (uint256);
function tokenConfig(address token)
    external view returns (bool listed, bool entriesPaused, uint256 largeExitThreshold, uint256 windowCap);
function tokenBook(address token)
    external view returns (uint256 backing, uint256 pendingExits, uint256 feesAccrued, uint64 windowStart, uint256 windowOut);
function nullifierSpent(uint256 nullifier) external view returns (bool);
function publicInputs(Transaction calldata t) external view returns (bytes32[] memory);
function extDataHash(ExtData memory ext) external pure returns (uint256);
function verifier() external view returns (address);
function hasher4() external view returns (address);
function entryPoint() external view returns (address);
function eip712Domain() external view returns (bytes1, string memory, string memory, uint256, address, bytes32, uint256[] memory);
FunctionReturns
exitsAn exit not yet paid, keyed by its id, the transaction's first nullifier. amount == 0 means none; asset is 0 for HYPE
exitFeeOfThe exit fee on amount leaving (HYPE after the gas fee, or a token), and what the recipient then receives
totalBackingHYPE backing every HYPE note
pendingExitTotalHYPE of exits decided and not yet paid; it no longer backs any note
feesAccruedHYPE exit fees not yet swept to the fee collector
tokenConfigA token's listing, its entries pause and its limits. listed == false for any other address
tokenBookA token's backing, pending exits, fees not yet swept, and its current window
nullifierSpentWhether a nullifier has been used
publicInputsThe 14 public inputs of the proof, in circuit order: root, 3 nullifiers, 4 commitments, exitAmount, uint160(exitAsset), feeAmount, extDataHash(ext), and the EIP-712 domain separator as two 128-bit halves
extDataHashuint256(keccak256(abi.encode(ext))) % FIELD, with FIELD the BN254 scalar field

The vault's HYPE balance is totalBacking + pendingExitTotal + feesAccrued. For each listed token, balanceOf(vault) is at least backing + pendingExits + feesAccrued of its book.

The token window is fixed: it restarts at the first token exit at least TOKEN_WINDOW after the window's start, and windowOut counts only exits payable at once. A token exit is held when its net amount is above the token's threshold, or when windowOut + net would exceed the window cap; held exits do not count toward the window, since the guardian can stop them.

ConstantValue
EXIT_FEE_BPS100 (1%)
LARGE_EXIT_DELAY24 hours
TOKEN_WINDOW24 hours
MIN_LARGE_EXIT_THRESHOLD / MAX_LARGE_EXIT_THRESHOLD1 ether / 1_000_000 ether (HYPE only)
MAX_AMOUNT2^120 base units of any asset: the circuit range-checks every amount to 120 bits
TREE_DEPTH / ROOT_HISTORY_SIZE24 / 64

See Constants and The vault.

Note tree

Inherited from PoseidonMerkleTree.

function root() external view returns (uint256);
function isKnownRoot(uint256 root) external view returns (bool);
function nextLeafIndex() external view returns (uint32);
function treeNumber() external view returns (uint32);
function isFinalRoot(uint256 root) external view returns (bool);
function hasher() external view returns (address);
FunctionDescription
rootCurrent root
isKnownRootTrue for any of the last 64 roots and for the final root of every full tree; false for 0
nextLeafIndexNext free leaf in the current tree. Leaves are inserted in pairs, so it is even
treeNumberNumber of full trees before the current one
isFinalRootWhether a root is the final root of a full tree (valid forever)
hasherThe Poseidon2 contract

See The Merkle tree.

Events

event KeysRegistered(address indexed owner, uint256 opk, bytes32 viewKey);
event NoteAdded(uint256 indexed index, uint256 commitment, bytes ciphertext);
event Shielded(address indexed from, uint256 indexed index, uint256 amount);
event NullifierSpent(uint256 indexed nullifier);
event ExitCreated(uint256 indexed exitId, address indexed recipient, uint256 amount, uint256 exitFee, uint256 gasFee, uint64 payableAt);
event ExitPaid(uint256 indexed exitId, address indexed recipient, uint256 amount);
event ExitStopped(uint256 indexed exitId);
event ExitResumed(uint256 indexed exitId);
event FeesSwept(address indexed collector, uint256 amount);
event VaultUpdated(uint256 totalBacking, uint256 pendingExitTotal);
event EntriesPaused(bool paused);
event LargeExitThresholdSet(uint256 threshold);
event FeeCollectorSet(address indexed collector);
event TreeCompleted(uint256 indexed treeNumber, uint256 finalRoot); // from PoseidonMerkleTree

// v2: listed tokens
event TokenListed(address indexed token, uint256 largeExitThreshold, uint256 windowCap);
event TokenLimitsSet(address indexed token, uint256 largeExitThreshold, uint256 windowCap);
event TokenEntriesPaused(address indexed token, bool paused);
event TokenFeeRecipientSet(address indexed recipient);
event TokenShielded(address indexed from, address indexed token, uint256 indexed index, uint256 amount);
event TokenExitCreated(uint256 indexed exitId, address indexed token, address indexed recipient, uint256 amount, uint256 exitFee, uint256 gasFee, uint64 payableAt);
event TokenExitPaid(uint256 indexed exitId, address indexed token, address indexed recipient, uint256 amount);
event TokenFeesSwept(address indexed token, address indexed recipient, uint256 amount);
event TokenBookUpdated(address indexed token, uint256 backing, uint256 pendingExits);

// from OpenZeppelin
event Paused(address account);
event Unpaused(address account);
event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);
event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);
event Upgraded(address indexed implementation);
event Initialized(uint64 version);
EventWhen
KeysRegisteredregister or registerFor
NoteAddedA leaf from a shield or a spend, with its encrypted note data. Four per spend (leaves i to i+3, ciphertexts 0 to 3); one per shield, whose pair is an empty leaf
Shieldedshield: from is who shielded, index the leaf created, amount the HYPE
TokenShieldedshieldToken: who shielded, the token, the leaf created and the amount
NullifierSpentThree times per spend, once per nullifier
ExitCreatedA spend took HYPE out of the notes and no token: amount for recipient after exitFee and gasFee, payable at payableAt. Emitted for every ERC-4337 operation that pays no token out, since each pays a gas fee: one with amount == 0 paid only its gas (a private send of any asset, a link, a collection)
ExitPaidA HYPE exit reached its recipient
TokenExitCreatedA spend took a token out of the notes: amount of it for recipient after exitFee (in the token), gasFee HYPE paid from the fee note, payable at payableAt
TokenExitPaidA token exit reached its recipient
ExitStopped / ExitResumedThe guardian stopped or resumed an exit of any asset
FeesSweptsweepFees sent amount HYPE to the fee collector
TokenFeesSweptsweepTokenFees sent a token's fees to the token fee recipient
VaultUpdatedtotalBacking and pendingExitTotal after every HYPE change
TokenBookUpdatedA token's backing and pendingExits after every change
EntriesPausedThe guardian closed or reopened shields of every asset
TokenListed / TokenLimitsSetThe admin listed a token or changed its limits
TokenEntriesPausedThe guardian closed or reopened one token to shields
TokenFeeRecipientSetThe admin set the token fee recipient
LargeExitThresholdSet / FeeCollectorSetAt initialization and when the admin changes them
TreeCompletedA tree filled up; its final root stays valid and a new tree starts at index 0
Paused / UnpausedThe guardian paused or resumed spends and payouts
RoleGranted / RoleRevokedA role changed hands; emitted for each role at initialization
UpgradedThe proxy points at a new implementation (also at deployment)
Initializedinitialize ran on the proxy (version 1)

Errors

ErrorCause
ZeroAddressConstructor without verifier, four-input hasher or EntryPoint; initialize with a zero role or fee collector; setFeeCollector(0), setTokenFeeRecipient(0), listToken(0, …); sweepTokenFees with no token fee recipient; an exit with no recipient
ZeroAmountshield with no HYPE, shieldToken of 0, a token exit of 0
AmountTooLargeA shield, exitAmount or feeAmount at or above 2^120 base units
NotAFieldElementA stub, commitment or nullifier at or above the field modulus, or an opk that is 0 or at or above it
UnknownRootThe proof's root is neither in the root history nor a final root
NullifierAlreadySpent / DuplicateNullifierA note already spent, or the same nullifier twice among the three of one transaction
InvalidProofThe verifier rejected the proof (transact only; ERC-4337 returns a failed signature instead)
InvalidSignature / SignatureExpiredregisterFor signature invalid or past its deadline
WrongCallertransact with ext.caller set to the vault or to another address than the sender; validateUserOp with ext.caller other than the vault
FeeExceedsExitext.gasFee above the HYPE leaving, feeAmount + (exitAsset == 0 ? exitAmount : 0)
FeeAmountNotGasFeeA token exit whose feeAmount is not exactly ext.gasFee
GasFeeNotAllowedtransact with a non-zero ext.gasFee
GasFeeBelowCostAn operation whose gas fee is below its maximum gas cost
NotEntryPointvalidateUserOp, execute4337 or a plain HYPE transfer not from the EntryPoint
InvalidUserOpA UserOperation that breaks one of the rules above
EntriesArePausedshield while entries are paused; shieldToken while the global or the token's entries pause is on
ThresholdOutOfRangeA HYPE large-exit threshold outside its range
WrongHasherA constructor's four-input hasher that does not match Noir's hash_4
NotATokenlistToken of an address without code
TokenNotListedshieldToken, a token exit, setTokenLimits or setTokenEntriesPaused for a token not listed
TokenAlreadyListedlistToken of a listed token
TokenLimitsOutOfRangeA token threshold of 0, or a window cap below the threshold
TokenAmountMismatchshieldToken whose transfer did not raise the vault's balance by exactly amount
NoSuchExit / ExitNotDue / ExitIsStopped / ExitNotStoppedreleaseExit, stopExit or resumeExit on an exit that is not pending, not yet payable, stopped, or not stopped
HypeTransferFailedA recipient refused HYPE in transact, releaseExit or sweepFees

From OpenZeppelin: AccessControlUnauthorizedAccount(account, neededRole), AccessControlBadConfirmation, EnforcedPause, ExpectedPause, InvalidInitialization, SafeERC20FailedOperation(token), UUPSUnauthorizedCallContext, UUPSUnsupportedProxiableUUID(slot), ERC1967InvalidImplementation(implementation) and ReentrancyGuardReentrantCall.

FeeCollector

Receives the vault's exit fees and the gas that private operations paid and did not use, all in HYPE (0x145618DF0662dCd38a2Cf2AC4d24Dfe1FC8A4991). Anyone can call collect, which splits what has arrived in half: one half is owed to the team's recipient, the other buys VOID on its Volta pool and sends it to the dead address. Nobody can take the HYPE set aside for burning. See Fees.

constructor(
    IPoolManager poolManager_,
    IVoltaPositionManager hook_,   // Volta's PositionManager, VOID's hook
    address token_,                // VOID, a Volta launch paired with native HYPE
    address owner_,                // may change the team's recipient (zero for none)
    address teamRecipient_         // receives the team's half (zero lets it accrue)
);

The constructor (from VoltaSwapper) accepts the pool only if it is exactly the one Volta created for the token, or reverts NotAVoltaPool: see VoltaSwapper. It emits OwnershipTransferred(address(0), owner_) and TeamRecipientChanged(address(0), teamRecipient_).

function DEAD() external view returns (address);              // 0x000000000000000000000000000000000000dEaD
function TEAM_SHARE_BPS() external view returns (uint256);    // 5_000
function MAX_IMPACT_BPS() external view returns (uint256);    // 50
function CALLER_TIP_BPS() external view returns (uint256);    // 50
function GAS_OVERHEAD() external view returns (uint256);      // 25_000
function BUY_INTERVAL() external view returns (uint256);      // 12 seconds

function owner() external view returns (address);
function teamRecipient() external view returns (address);
function teamOwed() external view returns (uint256);
function toBurn() external view returns (uint256);
function lastBuyAt() external view returns (uint256);
function totalBurned() external view returns (uint256);

function collect() external returns (uint256 tokensBurned);
function payTeam() external;
function setTeamRecipient(address newRecipient) external;
function transferOwnership(address newOwner) external;
// and everything of VoltaSwapper, below
FunctionDescription
collectAnyone. Splits the HYPE that has arrived, buys and burns VOID within the limits, refunds the caller's gas if it bought, and pays the team; see below. Returns the VOID burned
payTeamAnyone. Splits what has arrived and pays teamOwed to teamRecipient. Never reverts on the payment: if the recipient is zero or refuses the HYPE, the amount stays owed
setTeamRecipientOwner only (NotOwner). Sets who receives the team's half from now on and pays it what is owed
transferOwnershipOwner only. address(0) renounces ownership; the team's recipient can then never change again
teamOwedThe team's share received and not yet paid. It is never spent on VOID
toBurnHYPE set aside to buy VOID and burn it
lastBuyAtTimestamp of the latest buy
totalBurnedVOID burned through this contract

collect, payTeam and setTeamRecipient are guarded against reentrancy. collect runs in this order:

Split. The HYPE that arrived since the last split (the balance less teamOwed and toBurn) is halved: TEAM_SHARE_BPS (50%) to teamOwed, the rest to toBurn (Collected). HYPE sent here by anyone is split the same way.

Size a buy. At most one buy per BUY_INTERVAL (12 seconds). The size starts at toBurn less a 0.5% reserve for the caller's refund, capped at what moves VOID's price by about MAX_IMPACT_BPS (0.5%), computed from the pool's active liquidity. With no active liquidity there is no buy. The buy must return at least the spot price's worth of VOID after the pool's swap fee, less a quarter of a percent; a size the market does not fill is halved, over up to 8 sizes.

Buy and burn. The VOID bought goes to DEAD and adds to totalBurned (Burned). HYPE that does not fit waits for the next call.

Refund. Only if it bought: the caller is refunded its measured gas plus GAS_OVERHEAD (25,000) at min(tx.gasprice, 2 * block.basefee), capped at 0.5% of the HYPE spent (CallerTipped). A call that buys nothing earns nothing.

Pay the team (TeamPaid).

A buy that small is not worth sandwiching: pushing the price up before it and selling after costs the pool's 1% fee on both legs, about 2%, for at most the 0.5% it moves the price.

event Collected(uint256 amount, uint256 teamShare, uint256 burnShare);
event Burned(uint256 hypeSpent, uint256 tokensBurned);
event TeamPaid(address indexed recipient, uint256 amount);
event TeamRecipientChanged(address indexed previousRecipient, address indexed newRecipient);
event CallerTipped(address indexed caller, uint256 amount);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

error HypeTransferFailed();   // the caller refused its refund
error NotOwner();

VOID transfers go through OpenZeppelin SafeERC20.

VoltaSwapper

The abstract contract the fee collector inherits to trade on VOID's pool through the PoolManager directly (unlock, swap, settle, take), with no router. VoltaMarket is the library it reads the price with.

function VOLTA_TICK_SPACING() external view returns (int24);   // 60
function poolManager() external view returns (address);
function hook() external view returns (address);               // Volta's PositionManager
function token() external view returns (address);              // VOID
function poolId() external view returns (PoolId);
function poolKey() external view returns (PoolKey memory);
function spotPrice() external view returns (uint256);
function unlockCallback(bytes calldata data) external returns (bytes memory);
receive() external payable;
FunctionDescription
poolKey / poolIdVOID's pool: native HYPE as currency0, VOID as currency1, fee 0, tick spacing 60, Volta's PositionManager as the hook
spotPriceVOID per HYPE, scaled by 1e18, from the pool's slot0; 0 before the pool exists
unlockCallbackPoolManager only (NotPoolManager)
receiveAccepts HYPE from anyone

The constructor accepts the pool only if all of these hold, or reverts NotAVoltaPool:

  • the PositionManager names this PoolManager;
  • the PositionManager's creator NFT contract names the PositionManager back;
  • the token is a launch that contract created (tokenId and memecoin agree);
  • the key above hashes to the pool the PositionManager records for the token.

A decoy token or pool therefore never enters trading or pricing. Volta's hook takes the swap fee inside each swap and may fill part of a buy from the VOID it holds; the swap's own deltas, and the quotes taken from them, include both. Quotes run the swap and undo it, carrying the result out in Quote(uint256 amountOut).

error NotPoolManager();
error NotAVoltaPool();
error Quote(uint256 amountOut);   // internal: carries a quote out of a swap that is undone

VOID

VOID is not a voidcash contract. It is a Volta launch: a 44-byte clone of Volta's token implementation, an ERC-20 named voidcash with symbol VOID and 18 decimals, launched with a supply of 1,000,000,000, all of it in its pool but 1 VOID sent to the launching wallet. voidcash adds nothing to it. Volta's buyback destroys the VOID it buys, which lowers the total supply; the fee collector sends what it buys to DEAD. See VOID.

Since 2026-11-05 VOID is the vault's one listed token, with a large-exit threshold of 5,000,000 VOID and a window cap of 25,000,000 VOID per 24 hours (see Deployments). The VOID the vault holds backs VOID notes; the fee collector's burn does not touch it.

Interfaces

// the verifier (Barretenberg's generated HonkVerifier)
interface IVerifier {
    function verify(bytes calldata proof, bytes32[] calldata publicInputs) external view returns (bool);
}

// ERC-4337 v0.8, the parts the vault uses (same ABI as eth-infinitism/account-abstraction)
struct PackedUserOperation {
    address sender;
    uint256 nonce;
    bytes initCode;
    bytes callData;
    bytes32 accountGasLimits;   // verificationGasLimit (high 128 bits) | callGasLimit (low 128 bits)
    uint256 preVerificationGas;
    bytes32 gasFees;            // maxPriorityFeePerGas (high 128 bits) | maxFeePerGas (low 128 bits)
    bytes paymasterAndData;
    bytes signature;
}

interface IAccount {
    function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds)
        external returns (uint256 validationData);
}

// Poseidon2 (poseidon2-evm): the functions the vault calls
interface IPoseidon2 {
    function hash_2(uint256 x, uint256 y) external view returns (uint256);
    function hash_3(uint256 x, uint256 y, uint256 z) external view returns (uint256);
}

// Poseidon2Hash4: Noir's Poseidon2::hash of four inputs, for token note commitments
interface IPoseidon2Hash4 {
    function hash_4(uint256 a, uint256 b, uint256 c, uint256 d) external view returns (uint256);
}

The vault calls transferFrom, transfer and balanceOf on listed tokens, through SafeERC20. A listed token must be a plain ERC-20: no fee on transfer (the vault refuses a shield whose balance increase differs from the amount), no rebasing or other balance changes outside transfers, no transfer hooks into the receiver, and 18 decimals or fewer.

From the EntryPoint the vault calls depositTo, balanceOf, withdrawTo and addStake. From Volta the fee collector calls, on the PositionManager, poolManager, flaunchContract, poolKey and poolFees; on the creator NFT contract, positionManager, tokenId and memecoin. Volta does not publish its Solidity source: these signatures come from the ABI its site ships, its bytecode and on-chain reads, and the fork tests pin the code they ran against.

FallbackHasher, an optional Poseidon2 hasher that tries a Stylus program first and falls back to the EVM hasher, is in the repository but not deployed: the vault uses the Yul hasher.

On this page