HinkalBase
Overview
HinkalBase owns the canonical state that backs private assets: the Merkle tree of commitments, the ledger of spent nullifiers, and the global configuration for helper and logic modules. While higher-level contracts orchestrate flows, this base ensures that when a transaction finishes, the state transition is sound, indexable, and future-proof.
Storage
mapping(uint256 => bool) public nullifiers
— Tracks used nullifiers to prevent double-spend.mapping(uint256 => address) public externalActionMap
— Maps external action IDs to contract addresses.IHinkalHelper public hinkalHelper
— Helper contract for checks and fees.IHinkalInLogic public hinkalInLogic
— Logic contract for complex flows.
Functions
createCommitment
function createCommitment(UTXO memory utxo)
internal view returns (OnChainCommitment memory)
Computes a Poseidon commitment:
Fungible (
tokenId == 0
): Poseidon4(amount, erc20, stealthAddress, timestamp)NFT (
tokenId > 0
): Poseidon5(amount, erc20, stealthAddress, timestamp, tokenId)Returns
OnChainCommitment{ utxo, commitment }
.
A user action often results in new, private notes (UTXOs). Each note is converted into a field element commitment using Poseidon. For fungible tokens, the essence of the note is amount, token address, stealth address, and time. NFTs additionally bind tokenId
, ensuring uniqueness. These commitments are what enter the Merkle tree as leaves, forming the cryptographic backbone of private ownership.
Why two arities?
NFT commitments include
tokenId
to bind the unique asset to the note; fungible commitments omit it to reduce hash arity and gas. Both are deterministic and circuit-compatible.
insertCommitments
function insertCommitments(
uint256[][] memory outCommitments,
bytes[][] memory encryptedOutputs,
OnChainCommitment[] memory onChainCommitments,
bool[] memory onChainCreation
) internal
Flattens commitment leaves, inserts via
insertMany
, and emits events:For off-chain commitments:
NewCommitment(commitment, +index, encryptedOutput)
For on-chain commitments:
NewCommitment(commitment, -index, abi.encode(utxo))
After a successful proof and execution, the protocol crystallizes newly created notes into the Merkle tree. Off-chain generated commitments (from the zk-circuit outputs) and on-chain generated commitments (derived here from UTXO[]
) are flattened and inserted together to preserve ordering. Indexers learn not just the commitment values but also the provenance:
Positive indices → off-chain provided commitments (encrypted outputs accompany them).
Negative indices → on-chain constructed commitments, where the full
UTXO
is ABI-encoded in the event for transparency and recovery.
This dual-path insertion lets the protocol support both fully off-chain note creation and on-chain note creation paths (e.g., proofless deposits or DeFi integrations that mint notes in-process).
insertNullifiers
function insertNullifiers(
uint256[][] calldata inputNullifiers,
bool[] calldata onChainCreation
) internal
Ensures each nullifier is unused, sets it used, and emits Nullified(nullifier)
.
Every spend consumes one or more prior notes. In zero-knowledge, the spender proves knowledge of secrets; on-chain, we only see derived nullifiers. The act of inserting a nullifier is how the chain acknowledges a note as irrevocably spent. Reuse attempts revert, making double-spend attempts impossible within the same root set.
Security and Design Notes
Double-spend prevention via
nullifiers
mapping and strict checks.Event-rich state updates for efficient indexing and monitoring.
Access controls separate admin duties for helper vs. broader protocol admin.
Uses
Transferer
helpers for robust ERC20/ETH/NFT handling and interface support.
Merkle implementation highlights:
Batched insertion (
insertMany
) sorts leaves to minimize hashing and gas.Roots are circular-buffered to allow proofs against recent historical states.
Poseidon hash functions are injected via constructor for flexibility and auditability.
Last updated