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).
| Contract | Role |
|---|---|
PrivateVault | Shielded 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) |
PoseidonMerkleTree | The note tree, inherited by the vault |
Poseidon2Hash4 | The four-input Poseidon2 hash of token note commitments. Stateless, no owner |
FeeCollector | Splits the HYPE it receives: half to the team, half to buying VOID and burning it |
VoltaSwapper, VoltaMarket | Trading and pricing on VOID's Volta pool, inherited by the fee collector |
HonkVerifier | The 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:
| Part | Amount | Goes to |
|---|---|---|
| Gas fee | ext.gasFee, in HYPE | The vault's EntryPoint deposit (ERC-4337 only) |
| HYPE exit fee | out * EXIT_FEE_BPS / 10_000 (1%, rounded down) | feesAccrued, then the fee collector |
| HYPE exit | out less the exit fee | ext.recipient, at once or after the hold |
| Token exit fee | exitAmount * EXIT_FEE_BPS / 10_000 of exitAsset, when it is a token | tokenBook(token).feesAccrued, then the token fee recipient |
| Token exit | exitAmount less the exit fee | ext.recipient, at once or after the hold |
| Notes | The rest of the inputs | The 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);| Function | Description |
|---|---|
register | Registers 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 |
registerFor | Same 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 |
keysOf | The registered keys; opk == 0 means not registered |
registerNonces | Next 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"| Function | Description |
|---|---|
hasRole | Whether account holds role |
getRoleAdmin | The role that grants and revokes role: DEFAULT_ADMIN_ROLE for all three, itself included |
grantRole / revokeRole | Admin only (AccessControlUnauthorizedAccount). A role can be held by several addresses. Emit RoleGranted / RoleRevoked |
renounceRole | A holder gives up its own role; callerConfirmation must be the caller (AccessControlBadConfirmation) |
pause | Guardian 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) |
unpause | Guardian only. Ends the pause. Emits Unpaused(account); reverts ExpectedPause if not paused |
setEntriesPaused | Guardian only. While true, shield and shieldToken of every token revert EntriesArePaused: one switch closes every asset. Nothing else changes. Emits EntriesPaused |
setTokenEntriesPaused | Guardian 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 |
stopExit | Guardian only. Stops an exit not yet paid, of any asset; its amount stays reserved for it. Reverts NoSuchExit if none is pending. Emits ExitStopped |
resumeExit | Guardian only. Lets a stopped exit be paid again, from its original payableAt. Reverts NoSuchExit or ExitNotStopped. Emits ExitResumed |
setLargeExitThreshold | Admin 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 |
setFeeCollector | Admin only. Changes where sweepFees sends the HYPE fees (ZeroAddress for zero). Emits FeeCollectorSet |
listToken | Admin 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 |
setTokenLimits | Admin only, listed tokens only. Changes a token's threshold and window cap, with the same bounds; exits already created keep their time. Emits TokenLimitsSet |
setTokenFeeRecipient | Admin only. Sets who receives the token exit fees (ZeroAddress for zero). Emits TokenFeeRecipientSet |
upgradeToAndCall | Upgrader 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) |
proxiableUUID | The 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;| Function | Description |
|---|---|
shield | Moves 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 |
shieldToken | Moves 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 |
transact | Spends 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 |
releaseExit | Anyone. 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 |
sweepFees | Anyone. 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 |
sweepTokenFees | Anyone. Sends a token's accrued exit fees to tokenFeeRecipient. Reverts ZeroAddress while no recipient is set; does nothing when there are none. Emits TokenFeesSwept |
receive | Accepts 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;| Function | Description |
|---|---|
validateUserOp | EntryPoint 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 |
execute4337 | EntryPoint 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 |
addStake | Anyone 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.calleris the vault itself (WrongCaller);- no
initCodeand nopaymasterAndData(InvalidUserOp); callDatais exactlyexecute4337(t.nullifiers[0])(InvalidUserOp);nonceisuint256(uint192(t.nullifiers[0])) << 64: one nonce key per first nullifier, so operations of different users never queue behind each other (InvalidUserOp);callGasLimitis at least theminCallGasLimitinext.data(InvalidUserOp);ext.gasFeeis 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,
exitAmountandfeeAmountbelow 2^120,ext.gasFeeat most the HYPE leaving, a recipient for any exit, and for a token exit a listed token, a non-zeroexitAmountandfeeAmount == 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);| Function | Returns |
|---|---|
exits | An exit not yet paid, keyed by its id, the transaction's first nullifier. amount == 0 means none; asset is 0 for HYPE |
exitFeeOf | The exit fee on amount leaving (HYPE after the gas fee, or a token), and what the recipient then receives |
totalBacking | HYPE backing every HYPE note |
pendingExitTotal | HYPE of exits decided and not yet paid; it no longer backs any note |
feesAccrued | HYPE exit fees not yet swept to the fee collector |
tokenConfig | A token's listing, its entries pause and its limits. listed == false for any other address |
tokenBook | A token's backing, pending exits, fees not yet swept, and its current window |
nullifierSpent | Whether a nullifier has been used |
publicInputs | The 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 |
extDataHash | uint256(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.
| Constant | Value |
|---|---|
EXIT_FEE_BPS | 100 (1%) |
LARGE_EXIT_DELAY | 24 hours |
TOKEN_WINDOW | 24 hours |
MIN_LARGE_EXIT_THRESHOLD / MAX_LARGE_EXIT_THRESHOLD | 1 ether / 1_000_000 ether (HYPE only) |
MAX_AMOUNT | 2^120 base units of any asset: the circuit range-checks every amount to 120 bits |
TREE_DEPTH / ROOT_HISTORY_SIZE | 24 / 64 |
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);| Function | Description |
|---|---|
root | Current root |
isKnownRoot | True for any of the last 64 roots and for the final root of every full tree; false for 0 |
nextLeafIndex | Next free leaf in the current tree. Leaves are inserted in pairs, so it is even |
treeNumber | Number of full trees before the current one |
isFinalRoot | Whether a root is the final root of a full tree (valid forever) |
hasher | The 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);| Event | When |
|---|---|
KeysRegistered | register or registerFor |
NoteAdded | A 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 |
Shielded | shield: from is who shielded, index the leaf created, amount the HYPE |
TokenShielded | shieldToken: who shielded, the token, the leaf created and the amount |
NullifierSpent | Three times per spend, once per nullifier |
ExitCreated | A 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) |
ExitPaid | A HYPE exit reached its recipient |
TokenExitCreated | A 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 |
TokenExitPaid | A token exit reached its recipient |
ExitStopped / ExitResumed | The guardian stopped or resumed an exit of any asset |
FeesSwept | sweepFees sent amount HYPE to the fee collector |
TokenFeesSwept | sweepTokenFees sent a token's fees to the token fee recipient |
VaultUpdated | totalBacking and pendingExitTotal after every HYPE change |
TokenBookUpdated | A token's backing and pendingExits after every change |
EntriesPaused | The guardian closed or reopened shields of every asset |
TokenListed / TokenLimitsSet | The admin listed a token or changed its limits |
TokenEntriesPaused | The guardian closed or reopened one token to shields |
TokenFeeRecipientSet | The admin set the token fee recipient |
LargeExitThresholdSet / FeeCollectorSet | At initialization and when the admin changes them |
TreeCompleted | A tree filled up; its final root stays valid and a new tree starts at index 0 |
Paused / Unpaused | The guardian paused or resumed spends and payouts |
RoleGranted / RoleRevoked | A role changed hands; emitted for each role at initialization |
Upgraded | The proxy points at a new implementation (also at deployment) |
Initialized | initialize ran on the proxy (version 1) |
Errors
| Error | Cause |
|---|---|
ZeroAddress | Constructor 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 |
ZeroAmount | shield with no HYPE, shieldToken of 0, a token exit of 0 |
AmountTooLarge | A shield, exitAmount or feeAmount at or above 2^120 base units |
NotAFieldElement | A stub, commitment or nullifier at or above the field modulus, or an opk that is 0 or at or above it |
UnknownRoot | The proof's root is neither in the root history nor a final root |
NullifierAlreadySpent / DuplicateNullifier | A note already spent, or the same nullifier twice among the three of one transaction |
InvalidProof | The verifier rejected the proof (transact only; ERC-4337 returns a failed signature instead) |
InvalidSignature / SignatureExpired | registerFor signature invalid or past its deadline |
WrongCaller | transact with ext.caller set to the vault or to another address than the sender; validateUserOp with ext.caller other than the vault |
FeeExceedsExit | ext.gasFee above the HYPE leaving, feeAmount + (exitAsset == 0 ? exitAmount : 0) |
FeeAmountNotGasFee | A token exit whose feeAmount is not exactly ext.gasFee |
GasFeeNotAllowed | transact with a non-zero ext.gasFee |
GasFeeBelowCost | An operation whose gas fee is below its maximum gas cost |
NotEntryPoint | validateUserOp, execute4337 or a plain HYPE transfer not from the EntryPoint |
InvalidUserOp | A UserOperation that breaks one of the rules above |
EntriesArePaused | shield while entries are paused; shieldToken while the global or the token's entries pause is on |
ThresholdOutOfRange | A HYPE large-exit threshold outside its range |
WrongHasher | A constructor's four-input hasher that does not match Noir's hash_4 |
NotAToken | listToken of an address without code |
TokenNotListed | shieldToken, a token exit, setTokenLimits or setTokenEntriesPaused for a token not listed |
TokenAlreadyListed | listToken of a listed token |
TokenLimitsOutOfRange | A token threshold of 0, or a window cap below the threshold |
TokenAmountMismatch | shieldToken whose transfer did not raise the vault's balance by exactly amount |
NoSuchExit / ExitNotDue / ExitIsStopped / ExitNotStopped | releaseExit, stopExit or resumeExit on an exit that is not pending, not yet payable, stopped, or not stopped |
HypeTransferFailed | A 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| Function | Description |
|---|---|
collect | Anyone. 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 |
payTeam | Anyone. 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 |
setTeamRecipient | Owner only (NotOwner). Sets who receives the team's half from now on and pays it what is owed |
transferOwnership | Owner only. address(0) renounces ownership; the team's recipient can then never change again |
teamOwed | The team's share received and not yet paid. It is never spent on VOID |
toBurn | HYPE set aside to buy VOID and burn it |
lastBuyAt | Timestamp of the latest buy |
totalBurned | VOID 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;| Function | Description |
|---|---|
poolKey / poolId | VOID's pool: native HYPE as currency0, VOID as currency1, fee 0, tick spacing 60, Volta's PositionManager as the hook |
spotPrice | VOID per HYPE, scaled by 1e18, from the pool's slot0; 0 before the pool exists |
unlockCallback | PoolManager only (NotPoolManager) |
receive | Accepts 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 (
tokenIdandmemecoinagree); - 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 undoneVOID
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.
Deployments
The network, every voidcash contract address on Elysium, the listed tokens and their limits, the Volta contracts VOID trades on, the public bundler and indexer, and how to check all of it on-chain.
Integrations
What wallets, exchanges, explorers and analytics tools need to know about the vault and VOID, the indexer API, and building on the voidcash SDK.