As of 30 Oct 2026, Hinkal has 1 audit report on Trustblock, by Quantstamp, dated 28 Aug 2026. That report lists no open critical findings.
Record last updated . It holds audits and findings, contracts and incidents up to Oct 2026. Trustblock does not review code.
Newest first
| Audit | Security firm | Date | Scope | Critical findings | Report |
|---|---|---|---|---|---|
| Hinkal Protocol - 2 | Security firm: Quantstamp | Date: | Scope: Not recorded (see the report) | No open critical findings | Open · certificate.quantstamp.com |
Statuses are the security firm’s own, from each report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 0 | 0 | 0 |
| Medium | 0 | 0 | 1 |
| Low | 2 | 3 | 8 |
`prooflessDeposit` is permissionless and takes no proof, fee or allowance (`Hinkal.sol:263-270`). Its only validation, `performProoflessDepositChecks` (`HinkalHelper.sol:35-53`), checks that the four arrays have equal length and that each encrypted-output blob is non-empty. It never requires `amounts[i] > 0` and never caps the number of entries. Each entry mints one leaf (`Hinkal.sol:336-348`), and a zero-value UTXO hashes to an ordinary non-zero commitment, so it occupies a slot like any other note. Capacity is fixed at `2**(LEVELS-1)` (`MerkleBase.sol:25-26`), `m_index` only ever increases, `LEVELS` is immutable, and there is no prune, expand or migrate anywhere in the tree. Once `m_index` reaches `2**LEVELS` both insertion paths revert `"Tree is full."` (`Merkle.sol:24` and `:44`) and keeps reverting. Deposits stop, and so does every withdrawal that produces a change note, which is most of them. There is no in-place recovery, the pool is dead where it stands and the funds already inside it can only be moved by whatever spends need no new leaf. Cost scales with the deployed `LEVELS`, which is a constructor argument. The cost of filling the tree depends a lot of the gas costs, on cheap L2s it might be a strong possibility depending on the `LEVELS`
Open the reportThe `RelayStore` contract allows the owner to add and remove whitelisted relayer addresses. It stored both a boolean mapping for onchain checks and a list to enable frontend retrieval. Adding relays using the `addRelay()` function does not check if that address has already been added, and thus potentially adds a duplicate to the relayStore. During the removal process, the `removeRelay()` function iterates through the store to remove the first matching entry, then breaks. Apart from this being a loop of unbounded size, this also results in leftover artifacts of relayers that are still listed in the `RelayStore`, but will not pass onchain verification since the `RelayList` mapping correctly returns `false`.
Open the report`verifyWallet` returns immediately when `stack.signerAddress` is zero (`EmporiumUpgradeable.sol:292-294`), before it consumes `usedMessages` and before it recovers the EIP-712 signature. `signerAddress` is not an `ecrecover` result that happened to fail. It is a plaintext field in the `EmporiumStack` that `runAction` decodes out of `externalActionMetadata` (`EmporiumUpgradeable.sol:71-78`), so setting it to zero is a choice the caller makes. With it zero, every operation falls to CASE 2 and executes as `op.endpoint.call{value: op.value}(op.callData)` from Emporium's own context (`EmporiumUpgradeable.sol:107`). The accompanying proof is the minimal shape, `dimensionsCheck` constrains nothing at `tokenNumber == 0`, no nullifier is burned and no message is consumed, so the transaction leaves no trace that would stop it running again in the next block for the price of gas. Anyone can do this without holding a UTXO, being whitelisted or going through a relayer.
Open the reportIn the circom template `ConditionalOverflowPreventer` the signal `enabled` is not constrained to be 0 or 1, despite the intention being that it represents a boolean true or false value. Since this value is later multiplied against a real value, it may change that value in problematic ways. The severity of this issue is lower as within the current architecture the template is only used within `OverflowPreventer.circom` where the value of `enabled` is always hardcoded to 1. It would be safer for future updates to ensure the input is constrained within the template itself rather than assuming it has already been validated outside of the template.
Open the report`SignatureVerifier` passes the `spendingPublicKey` (`A`) and `eddsaSignature` (`R8`) to the inherited `EdDSAPoseidonVerifier` template without first validating that they are valid Baby-Jubjub points. The verifier performs curve arithmetic using components that assume valid inputs, but these components do not independently enforce curve membership. For nonzero input commitments, `A` is indirectly bound to the commitment through the stealth address. This prevents substituting a malformed `A` for an honestly created note without breaking the underlying Poseidon binding, but it does not ensure that the originally committed `A` was a valid prime-order point. Similarly, an off-curve `R8` is not explicitly rejected before being passed to the `BabyAdd` template. No practical signature forgery against an honest, commitment-bound `A` has been demonstrated, but accepting malformed points violates the verifier’s cryptographic assumptions and may permit unexpected behavior in exceptional cases.
Open the report`EMPORIUM_SIGNATURE_TYPEHASH` covers `(uint256 message, EmporiumOperation[] ops)` and nothing else (`EmporiumUpgradeable.sol:36-39`). The ops are the instructions; the fields that decide where the resulting value goes all travel unsigned in `circomData`: - **Destination.** `handleOut` mints the output note to `circomData.stealthAddressStructure`. - **Fee.** The `payRelayFees` fallback (`:233-245`) pays `flatFee` of `feeToken` out of the user's wallet, guarded only by `flatFee != 0` and `signerAddress != 0`. The main loop's `require(sumAbs >= flatFee)` (`:215-218`) and the `variableRate <= 10000` cap do not apply here, and `feeToken == address(0)s` routes native ETH so the fee is bounded by the user's balance, not by what they received. - **Token list.** `erc20TokenAddresses = []` selects the minimal circuit (`CircomDataBuilder.sol:125-133`) and makes all four reconciliation loops iterate zero times. There is also no deadline and no cancellable nonce,`usedMessages[emporiumMessage]` (`:298-302`) makes a signature single use but leaves it bankable indefinitely, and 7702 delegation persists until revoked.
Open the report`RelayStore.calculateRelayFee()` calculates the relay fee as follows: ```solidity relayFee = flatFee + (balance * variableRate) / 10000; ``` However, `Hinkal._internalTransact()` and `EmporiumUpgradeable.payRelayFees()` use the following calculation: ```solidity uint256 recipientAmount = ((10000 - circomData.feeStructure.variableRate) * (sumAbs - flatFee)) / 10000; relayFee = sumAbs - recipientAmount; ``` Ignoring integer-rounding differences, this simplifies to: ```solidity relayFee = flatFee + ((sumAbs - flatFee) * variableRate) / 10000; ``` `RelayStore.calculateRelayFee()` applies the variable rate to the full balance, while the other implementations apply it only after deducting the flat fee. `RelayStore.calculateRelayFee()` returns a higher fee whenever both fee components are nonzero. This can cause fee quotes to differ from the amount charged during transaction execution.
Open the reportSwaps executed through Hinkal are exposed to ordinary sandwich MEV. Shielding hides who is swapping and what they hold but it does not hide the swap. Every parameter that governs the fill is a public input carried in plaintext in the transaction calldata (`CircomData.sol:26-33`): - `erc20TokenAddresses`, the pair, - `amountChanges`, the exact input size, which `ExternalActionSwap.sol:43` converts directly into `inputAmount`, - `externalActionMetadata`, the target pool's fee tier, - `slippageValues`, the exact worst-case output the victim will accept. A MEV bot decoding a pending Hinkal transaction therefore reads the pair, the size, and the victim's floor. On typical aggregator flow the last of these has to be inferred, here it is stated outright. Nothing at the venue narrows the window, both Uniswap adapters hard-code `amountOutMinimum: 0` and `sqrtPriceLimitX96: 0` (`UniswapExternalAction.sol:44-45`, `Uniswap2ExternalAction.sol:44-45`), and the Odos adapter forwards prover-supplied calldata to the router without inspecting it (`OdosExternalAction.sol:32-34`). The single price constraint in the system is `balanceDif >= slippageValues[i]` (`Hinkal.sol:111-114`) it's an absolute number fixed when the proof was built, unrelated to the market price at execution and never sanity-checked against the input amount. Extraction is capped at the gap between the true quote and the victim's floor, since the check at L#113 is atomic and a bot that pushes past the floor reverts the victim and loses its own gas. The cap only holds if clients set the floor tightly, and nothing on-chain requires that. A client that leaves `slippageValues` at zero publishes a transaction that can be drained of its entire output.
Open the reportThe `signedMessageHash` does not include the chain ID or verifying contract address. Although the signed message includes `rootHashHinkal`, a hard fork may initially preserve identical contract state, accepted Merkle roots, and contract addresses across both chains. Consequently, a proof that was unspent at the time of the fork could be submitted on one chain and replayed on the other while its root remains accepted and its nullifiers remain unused there.
Open the reportThe reports name a repository or files. Open a report to see its scope.
Is this your project? Show this record on your own website. The label states what the record holds, links to this page and updates by itself. It is free and needs no sign-in.
Automatic: the three strongest facts of this project’s own record (its last audit and two more), highlighted. They update with the record. Picking a fact turns Automatic off.
<head><script src="https://widget.trustblock.run/project-widget.min.js" defer></script></head><body><divdata-tb-project-widgetdata-tb-project-slug="hinkal-1"></div></body>
Paste the code where the label should appear. It uses no API credit and updates by itself.
Something wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.
Tell us