# Security review

**Scope**: `apps/contracts/contracts/**/*.tolk` **Reference**: `elsvv/ton-best-practices-skill` (Tolk v1.2 / TVM 12 audit checklist, 233 vulns from 34 audits) **Date**: 2026-05-12 **Reviewer**: Claude Code (internal pre-audit, not a substitute for a professional audit)

***

## Applied-fixes status (2026-05-12)

The fixes below were applied in-tree. Severity in parens. **None of this replaces a professional external audit**. These are the cheap mechanical fixes; the structural items (Tolk 1.2 migration, RichBounce, prover/contract parity tests) still need follow-up.

| ID  | Title                                                | Status                                | Where                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| --- | ---------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| C-1 | Double-spend via field-element non-uniqueness        | **Fixed**                             | [lib/field.tolk](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/lib/field.tolk), range checks in [pool\_native.tolk:170-184](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L170-L184), [pool\_jetton.tolk:160-176](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_jetton.tolk#L160-L176)                                                                                                       |
| C-2 | Action-phase failure strands nullifier               | **Fixed (atomic)**                    | `+16` flag + bounceable header in `send_native_bounceable` / `send_jetton`; OP\_ADMIN\_RECOVER\_STUCK escape hatch for residual bounce-back case                                                                                                                                                                                                                                                                                                                                                       |
| C-3 | `pool_jetton.handle_jetton_withdraw` is a no-op stub | **Fixed**                             | [pool\_jetton.tolk](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_jetton.tolk) (full impl)                                                                                                                                                                                                                                                                                                                                                                            |
| C-4 | `merkle_tree.zeros` exponential                      | **Fixed (with TODO)**                 | Flat lookup with sentinel + ERR\_ZEROS\_PLACEHOLDER guard in [merkle\_tree.tolk](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/merkle_tree.tolk); precompute script at [scripts/precompute-zeros.ts](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/scripts/precompute-zeros.ts). User must run `pnpm --filter @tonado/contracts run precompute-zeros` to fill the table.                                                                                 |
| H-1 | Counterfeit-jetton + missing admin/pause             | **Fixed**                             | [pool\_jetton.tolk:99-104](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_jetton.tolk#L99-L104) (sender check) + admin/pause + recover handlers                                                                                                                                                                                                                                                                                                                        |
| H-2 | `address_to_field` truncation / format gap           | **Fixed (impl)** + **smoke-verified** | `preloadUint(3) == 0b100` format check in both pools; comment updated. Off-chain prover (`packages/core/src/tonClient.ts :: addressToFieldElement`) updated to match the on-chain encoding `(wc << 240) \| (hash >> 16)` exactly. Step 13 of [shell/testnet.sh](https://github.com/tonadocash/monorepo/blob/main/shell/testnet.sh) is the implicit parity test: if the proof verifies on-chain, the encodings agree. Outstanding: an explicit fuzz unit test that doesn't require a live testnet pool. |
| H-3 | Silent bounce drop                                   | **Partially fixed**                   | `+16` covers our action-phase failures atomically. Receiver-side compute-fail bounces still silently absorbed; OP\_ADMIN\_RECOVER\_STUCK is the manual escape hatch. Long-term fix: Tolk 1.2 RichBounce.                                                                                                                                                                                                                                                                                               |
| H-4 | Malformed log message header                         | **Fixed**                             | New `LOG_PREFIX = 0x30` (`ext_out_msg_info$11` + src=none + dest=none) and body-in-ref in `emit_deposit_log` / `emit_withdraw_log`; resolves the 1023-bit cell overflow on the withdraw log too.                                                                                                                                                                                                                                                                                                       |
| H-5 | Dict access fragility                                | **Fixed**                             | Single `beginParse()` per lookup in [merkle\_tree.tolk :: is\_known\_root](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/merkle_tree.tolk) and `get fun get_root_history()`.                                                                                                                                                                                                                                                                                               |
| M-1 | Bitwise `&` where logical `&&` intended              | **Fixed**                             | [pool\_native.tolk:127](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L127), [pool\_jetton.tolk:97](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_jetton.tolk#L97)                                                                                                                                                                                                                                                       |
| M-2 | `address_equal` via sliceHash                        | **Fixed**                             | Workchain + hash compare via `parse_std_address` pattern in both pools                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| M-3 | Native deposit `>=` vs jetton `==`                   | **Fixed (semantics)**                 | Native deposit now requires `>= denomination + MIN_DEPOSIT_GAS` and keeps surplus in the pool (improves anonymity set, removes the bounce-prone refund send). Jetton stays `==`.                                                                                                                                                                                                                                                                                                                       |
| M-4 | Storage layout doc mismatch                          | **Fixed**                             | [lib/storage.tolk](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/lib/storage.tolk) updated to match concrete layout                                                                                                                                                                                                                                                                                                                                                        |
| M-5 | Unused `ext_info` cell in native state               | **Fixed**                             | Dropped from `State` struct in `pool_native.tolk`                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| M-6 | No upfront gas budget check                          | **Fixed**                             | `MIN_DEPOSIT_GAS`, `MIN_WITHDRAW_GAS`, `MIN_JETTON_DEPOSIT_GAS`, `MIN_JETTON_WITHDRAW_GAS` constants + checks. Values are pessimistic estimates: **rebenchmark in @ton/sandbox before mainnet**.                                                                                                                                                                                                                                                                                                       |
| L-1 | Owner stored as `slice`                              | **Not fixed**                         | Requires Tolk 1.2 typed `address` migration: separate refactor                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| L-2 | Two-step admin transfer                              | **N/A**                               | No ownership transfer op exists; if added later, follow the pattern in [tolk-security.md §6.2](https://github.com/tonadocash/monorepo/blob/main/.agents/skills/ton-best-practices/tolk-security.md)                                                                                                                                                                                                                                                                                                    |
| L-3 | Error code 0xffff collision                          | **Fixed**                             | `ERR_UNKNOWN_OP = 0xfffe`, `ERR_EMPTY_MESSAGE_REJECTED = 0xffff` reserved                                                                                                                                                                                                                                                                                                                                                                                                                              |
| L-4 | Magic numbers in `send_native`                       | **Fixed**                             | `MSG_PREFIX_BOUNCEABLE`, `MSG_TRAILER_BITS`, `SEND_MODE_PAY_GAS_AND_BOUNCE` constants                                                                                                                                                                                                                                                                                                                                                                                                                  |
| I-1 | `verifier.tolk` stub deploy footgun                  | **Not fixed**                         | Deferred to deploy-script-level check (out of contract scope)                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| I-2 | Missing `precompute-zeros.ts`                        | **Fixed**                             | Script written, `pnpm run precompute-zeros` wired up                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| I-3 | No external-message handler                          | **N/A**                               | Confirmed correct (no replay surface)                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| I-4 | Custom Poseidon TVM opcodes                          | **Not fixed**                         | The `asm "POSEIDON3_RC GETPARAM"` opcodes still need verification against the actual TVM-12 instruction set or replacement with a real constants-table read                                                                                                                                                                                                                                                                                                                                            |

### What did NOT change

* `verifier.tolk`: still the auto-generated stub, untouched per project policy.
* The circuits in `apps/contracts/circuits/`: not in scope.
* `apps/relayer/` and `packages/core/`: separate review needed.
* Deploy scripts in `apps/contracts/scripts/*` other than the new `precompute-zeros.ts`.

### Empirical result of the 2026-05-13 e2e attempt

* **C-5 (off-chain Poseidon parity)**: RESOLVED. The F-swap technique was empirically broken (4/4 samples mismatched circom). Replaced with a witness-calculator wrapper over the compiled `poseidonProbe*.wasm`. Now 4/4 samples match (commit 2026-05-13).
* **I-4 (on-chain Poseidon)**: Algorithmically resolved (we ported the optimized variant from circomlibjs to Tolk 1.0, verified output matches the off-chain reference) but **gas-bound on TON testnet workchain 0**. The 1M gas-per-tx hard cap (block config param 21) cannot fit a 20-level merkle insert under any reasonable msg\_value. Three optimization passes (mulDivMod, chunked accessors, cached PoseidonCtx) shaved \~10–30 % but didn't break through the 10× gap. Next-step options documented in `shell/testnet.sh` "Findings from this run".
* **Tolk 0.x → 1.0 migration of `pool_native.tolk`**: DONE. Struct messages, match dispatch, `assert (cond) throw CODE`, `contract.getData/setData`, `createMessage({...}).send(...)`, BounceMode enum. All audit security invariants (C-1 field-range, C-2 bounce-on-action-fail, H-4 log header) re-expressed in Tolk 1.0 form.

### Required before any testnet deploy

1. **Run `pnpm --filter @tonado/contracts run precompute-zeros`**: without it every deposit throws `ERR_ZEROS_PLACEHOLDER = 501` by design (loud failure beats silent OOG). The script writes `build/zeros.json` and patches `merkle_tree.tolk` in place.
2. **Verify the three Poseidon implementations match each other** (see C-5 below). Without this, the precomputed `zeros[]` table and the prover's commitments will not agree with the on-chain merkle tree.
3. Add and run a TS↔Tolk parity test for `address_to_field` (H-2 outstanding).
4. Re-run `pnpm circuits:compile && pnpm verifier:export` so `verifier.tolk` contains a real verifier (and confirm the public-signal order matches `pool_native.tolk` / `pool_jetton.tolk`).
5. Rebenchmark `MIN_*_GAS` constants via `@ton/sandbox printTransactionFees()`.
6. Run **Misti** and **TSA** static analyzers on the patched contracts.
7. External audit (TON-native firm + ZK specialist).

***

## New finding surfaced during the fix iteration

### C-5: Poseidon implementation parity is not established

**Severity**: Critical (correctness / deploy-blocker, not directly exploitable) **Files**: [packages/core/src/poseidon.ts](https://github.com/tonadocash/monorepo/blob/main/packages/core/src/poseidon.ts), [apps/contracts/contracts/poseidon.tolk](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/poseidon.tolk), `apps/contracts/circuits/commitmentHasher.circom`

Three Poseidon implementations must produce identical outputs on every input pair:

1. **The prover** (`@tonado/core`'s `buildPoseidon`): currently builds a BN254 `buildPoseidonOpt` from circomlibjs and **monkey-patches `.F` with a BLS12-381 `ZqField`**.
2. **The circuit** (`circuits/commitmentHasher.circom` compiled with `--prime bls12381`): uses circomlib's `poseidon.circom` template.
3. **The on-chain Tolk Poseidon** ([apps/contracts/contracts/poseidon.tolk](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/poseidon.tolk)): uses placeholder `asm "POSEIDON3_RC GETPARAM"` constants accessors that aren't backed by real TVM instructions yet.

The F-swap approach in (1) is **not** a known-safe technique. Poseidon's round constants and MDS matrix are derived from the field modulus and S-box exponent; circomlibjs's BN254 build embeds BN254-specific round constants. Swapping `.F` causes downstream arithmetic to use BLS12-381 modular reduction but still applies BN254 round constants, yielding a Poseidon-flavored hash that is unlikely to match what circom's BLS12-381 mode produces. The `zeros[]` values just patched into `merkle_tree.tolk` will be **internally consistent** (the precompute and the prover both use this same off-chain hash), but they may not match the circuit's `commitmentHasher` output once it's actually generating proofs.

**Symptoms when this lands**: every deposit succeeds on-chain, but withdraws fail proof verification (`ERR_PROOF_INVALID`) because the circuit's hash of `(secret, nullifier)` doesn't match the leaf the contract inserted.

**Fix paths** (pick one):

* **Recommended**: use a `circom`-compiled-and-run reference (e.g., generate a witness for a trivial circuit that just outputs `poseidon([0, 0])`) and compare against `@tonado/core`'s output. If they don't match, replace the F-swap approach with a from-scratch BLS12-381 Poseidon using **circomlib's `--prime bls12381` round constants** (look in circomlibjs's source for the BLS variant, or recompute from the Poseidon paper's spec).
* **Alternative**: use `ffjavascript`'s native BLS12-381 field operations end-to-end and re-derive round constants from the Poseidon paper's parameter-generation algorithm.

Until this is reconciled, treat the deposit and withdraw flows as **end-to-end untested**. The audit-fix bundle removes specific bug classes but cannot certify that the merkle tree and the proof system agree.

**Also note**: the `BigInt(F.toObject(out))` call in `packages/core/src/poseidon.ts` was failing because the F-swap-altered field returned a `Uint8Array` rather than a `BigInt`. The hot-fix in this iteration adds a `toBigInt()` adapter that handles both representations. The adapter is correct for little-endian byte order (matching `Scalar.fromRprLE`); if the actual output is in Montgomery form, the values are off by a multiplicative factor of `R` mod `p`, which would cause every cross-implementation comparison to fail. **Compare a single `poseidon([1, 2])` output against an independent BLS12-381 Poseidon implementation as the very first check.**

***

## Executive summary

This is a **pre-audit internal review**. The contracts are partial / scaffolded: `pool_jetton.tolk` has stubbed handlers, `verifier.tolk` is an intentional placeholder, and several Tolk-1.2 idioms (`lazy`, typed `address`, `createMessage`, `BounceMode.RichBounce`, `@onBouncedMessage`) are not yet used. The code reads as Tolk-flavored FunC. Treat all findings as **must-fix before any mainnet deploy**.

### Severity counts

| Severity      | Count |
| ------------- | ----- |
| Critical      | 4     |
| High          | 5     |
| Medium        | 6     |
| Low           | 4     |
| Informational | 5     |

***

## Critical

### C-1: Double-spend via field-element non-uniqueness in `nullifier_hash`

**Files**: [pool\_native.tolk:162-200](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L162-L200), [pool\_native.tolk:284-289](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L284-L289)

The Groth16 verifier over BLS12-381 treats public inputs as scalars on a group of order `p` (the BLS12-381 scalar field, `0x73ed…0001`). Curve operations satisfy `s · P == (s + k·p) · P`, so a proof valid for `nullifier_hash = N` is **also valid for `nullifier_hash = N + p`** (as a 256-bit integer, since `N + p < 2^256`).

The on-chain nullifier dict in [`pool_native.tolk:284-289`](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L284-L289) keys nullifiers as **integers**, not field elements, so `dict[N]` and `dict[N + p]` are distinct slots:

```tolk
fun nullifier_spent(d: cell, nh: int): int {
    var v = d.uDictGet(256, nh);   // raw integer key, no mod p
    return v.beginParse().endsEmpty() ? 0 : -1;
}
```

**Attack**: a user generates one legitimate proof, then withdraws twice:

1. Withdraw 1: `nullifier_hash = N`, marks `dict[N]`, receives `denomination − fee`.
2. Withdraw 2: `nullifier_hash = N + p`, same proof. `nullifier_spent` returns `false`. Verifier accepts (same curve point). Funds sent **again**.

For each note, `2^256 ÷ p ≈ 2` aliases fit in uint256, so each note can be spent **twice**. Pool drains.

The same issue applies to **`commitment`** (`commitment` and `commitment + p` produce the same Poseidon leaf in the circuit but different dict keys in [`mark_commitment`](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L280-L283)), though that's a DoS / duplicate-bypass, not theft.

**Fix**: range-check inputs at the message boundary.

```tolk
const FIELD_SIZE: int =
  0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001;

// In handle_withdraw, before any dict lookups or verification:
throw_unless(ERR_FIELD_RANGE, nullifier_hash < FIELD_SIZE);
throw_unless(ERR_FIELD_RANGE, root           < FIELD_SIZE);

// In handle_deposit:
throw_unless(ERR_FIELD_RANGE, commitment < FIELD_SIZE);
```

Add a corresponding `ERR_FIELD_RANGE` to [`lib/errors.tolk`](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/lib/errors.tolk).

***

### C-2: Critical state changes survive action-phase failure (no bounce-on-action-fail)

**Files**: [pool\_native.tolk:62-67](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L62-L67), [pool\_native.tolk:199-251](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L199-L251)

`handle_withdraw` follows this order:

1. Verify proof (compute phase).
2. **Mark nullifier spent + `save_state`** ([pool\_native.tolk:199-200](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L199-L200)).
3. `send_native(recipient, …)` and `send_native(relayer, fee)` ([pool\_native.tolk:204-207](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L204-L207)).
4. Emit log message.

`send_native` ([pool\_native.tolk:243-251](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L243-L251)) uses `SEND_MODE_PAY_GAS = 1` (no `+16` / no `BOUNCE_ON_ACTION_FAIL`), and the message header `0x10` is **non-bounceable**. Per the skill's [tvm-async.md](https://github.com/tonadocash/monorepo/blob/main/.agents/skills/ton-best-practices/tvm-async.md), action-phase failures with neither `+16` nor a bounceable outgoing message:

> Compute success + Action failure → inconsistent state … never allow action phase to fail.

Action-phase failures that can hit here include: malformed recipient address (e.g., `addr_var`, `addr_extern`, unusual workchain), cell-write limits (the log message; see M-1), `>255` actions queued, or fwd-fee/balance edge cases on the third send (log).

If the action phase fails after step 2, the **nullifier is permanently burned** and the funds never leave the pool. The user has no recovery.

Meanwhile, the top-of-handler bounce drop ([pool\_native.tolk:64-67](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L64-L67)) silently swallows any inbound bounces the pool itself receives, so even if a downstream send eventually bounces, the pool will not restore state.

**Fix**:

1. Use `SEND_MODE_PAY_GAS | 16` (`PAY_FEES_SEPARATELY | BOUNCE_ON_ACTION_FAIL`) for both `send_native` calls, and switch the header to bounceable (`0x18` instead of `0x10`).
2. Add `onBouncedMessage` to restore the nullifier (un-mark) on bounce, parsing the bounced body's op-code to identify it as a withdraw-recipient send. With Tolk 1.2 / TVM 12, use `BounceMode.RichBounce` so the full body (including nullifier\_hash) is available.
3. As a complementary protection: re-order so the log is emitted *before* the cash sends, with `+16` on the cash sends, so a log failure can't strand a nullifier.

Same pattern applies to the deposit refund send at [pool\_native.tolk:144-147](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L144-L147).

***

### C-3: `pool_jetton.handle_jetton_withdraw` is a no-op stub

**File**: [pool\_jetton.tolk:129-136](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_jetton.tolk#L129-L136)

```tolk
fun handle_jetton_withdraw(mutate state: State, in_msg: slice, query_id: int): void {
    // Implementation omitted in this stub — copy from pool_native and replace
    // `send_native` with a jetton transfer to `state.pool_jetton_wallet`.
}
```

If deployed in this state, the jetton pool would **accept deposits but silently no-op on withdraw**: no proof check, no nullifier marking, no transfer, no throw. Jettons would be unrecoverable.

**Fix**: Block deploys of `pool_jetton` until this function is implemented and tested. Add a `get fun version(): int` returning a known sentinel for the in-progress build, and gate the deploy script on the production sentinel.

***

### C-4: `merkle_tree.zeros` is exponential. Every deposit OOGs as written

**File**: [merkle\_tree.tolk:126-131](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/merkle_tree.tolk#L126-L131)

```tolk
fun zeros(level: int): int {
    if (level == 0) { return ZERO_VALUE; }
    return poseidon_hash2(zeros(level - 1), zeros(level - 1));  // 2 recursive calls
}
```

This is called inside `insert_leaf`'s loop ([merkle\_tree.tolk:98](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/merkle_tree.tolk#L98)) for every left-child level. The double recursion makes `zeros(level)` cost `2^level` Poseidon hashes. At `MERKLE_TREE_HEIGHT = 20`, the first deposit hash chain alone costs **2^20 ≈ 1M Poseidon-3 permutations**. Each permutation is 8 + 57 + 8 = 73 rounds with field multiplications. Far exceeds any per-tx gas budget. Every deposit fails.

The file comment acknowledges the values are intended to be replaced at build time (`scripts/precompute-zeros.ts`), but that script doesn't exist in the repo (`scripts/` listing shows `compile-circuits.ts`, `deploy-pool.ts`, `export-verifier.ts`, `setup-ptau.ts`, `verify-deployment.ts`; no `precompute-zeros.ts`).

**Fix**: either (a) implement the precompute script and embed the 20 values as a constant table, or (b) inline a hard-coded const array. Example:

```tolk
fun zeros(level: int): int {
    if (level == 0)  { return 0x2fe54c…af6c; }
    if (level == 1)  { return 0x…; }
    // … 18 more …
    if (level == 19) { return 0x…; }
    throw(ERR_INVALID_LEVEL);
    return 0;
}
```

Also: in [`insert_leaf`](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/merkle_tree.tolk#L93-L112), `zeros(level)` is read into `right` even when not used (the else branch overwrites). Move the call inside the `if` branch.

***

## High

### H-1: Counterfeit-jetton attack surface present but admin/pause is unimplemented

**File**: [pool\_jetton.tolk:80-95](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_jetton.tolk#L80-L95)

The check `sender_equals(sender, state.pool_jetton_wallet)` is correct in spirit and matches the project's [`CLAUDE.md`](https://github.com/tonadocash/monorepo/blob/main/CLAUDE.md) safety rail. However:

1. There is **no admin / pause logic** in `pool_jetton.tolk`. The `// ... (admin ops omitted for brevity; mirror pool_native.tolk)` placeholder at [pool\_jetton.tolk:93](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_jetton.tolk#L93) means the jetton pool can't be paused if a wallet-impersonation bypass is later found.
2. The `paused` field is loaded from storage but **never checked** in the dispatch.
3. `sender_equals` uses `sliceHash()` ([pool\_jetton.tolk:138-140](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_jetton.tolk#L138-L140)), same canonicalization concern as L-1 below.

**Fix**: port the pause check + admin handlers from `pool_native`. Verify the jetton wallet address is computed via the canonical TEP-74 derivation (sender's stateInit hash matches the master's `get_wallet_address` result) and treat the stored `pool_jetton_wallet` as the **only** trusted source.

***

### H-2: `address_to_field` truncates address hash with no documented circuit-side parity

**File**: [pool\_native.tolk:258-266](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L258-L266)

```tolk
fun address_to_field(addr: slice): int {
    var s = addr;
    s.loadUint(3);   // skip prefix bits
    var workchain = s.loadInt(8);
    var hash = s.loadUint(256);
    return ((workchain & 0xff) * (1 << 240)) | (hash >> 16);
}
```

The comment says *"For workchain=0 (the common case) this is just the 256-bit hash"*. **This is wrong**. `hash >> 16` keeps the **high 240 bits** of the address hash and discards the low 16 bits. Preimage resistance is still 240 bits, so this is not directly exploitable, but:

* The exact bit packing **must match** the off-chain prover (likely [`packages/core/src/pool.ts`](https://github.com/tonadocash/monorepo/blob/main/packages/core/src/pool.ts)). Any disagreement → all proofs silently fail / mis-verify.
* The `s.loadUint(3)` skip assumes `addr_std$10` layout (3-bit prefix: `100`). For `addr_var`, `addr_extern`, or `addr_none` the parsing is wrong and the resulting field element is meaningless.
* `loadInt(8)` returns a signed integer (`-1` for masterchain). The mask `workchain & 0xff` then gives `255` for masterchain in the field, which is fine for distinguishing, but the prover must agree exactly.

**Fix**:

1. Update the comment to accurately describe the encoding.
2. Add `throw_unless(ERR_BAD_ADDR_FORMAT, addr.preloadUint(3) == 0b100)` to reject non-std addresses.
3. Validate workchain == 0 (or whatever set is supported) in both prover and contract.
4. Cross-check against `packages/core/src/pool.ts :: addressToField` (or whatever the prover-side function is named); ideally factor a single TS+Tolk parity test.

***

### H-3: Bounced messages silently dropped at handler top

**File**: [pool\_native.tolk:62-67](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L62-L67), [pool\_jetton.tolk:72-73](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_jetton.tolk#L72-L73)

```tolk
if (flags & 1) {
    // Bounced message — ignore.
    return;
}
```

Cross-references the skill's *Missing Bounce Handler* (Critical, [tolk-security.md §5.1](https://github.com/tonadocash/monorepo/blob/main/.agents/skills/ton-best-practices/tolk-security.md#L356-L401)) and the *Partial Transaction Execution* category from the arXiv paper (Onton Finance pattern: state debited before downstream send confirms).

Today the only state that can be "in flight" is the nullifier (marked spent before send) and merkle insertion (commit happens before refund send). When a downstream send bounces back, the pool ignores it. Combined with C-2, this is the actual fund-loss path.

**Fix**: implement an `onBouncedMessage` (or whatever Tolk version's equivalent the project actually targets) that:

* For bounced **withdraw recipient/relayer sends**: parse the bounced body, recover the nullifier\_hash (use `BounceMode.RichBounce` so the full body survives), and *unmark* the nullifier.
* For bounced **deposit refund sends**: no state restoration is needed (the deposit already succeeded).

***

### H-4: Log message header is malformed

**Files**: [pool\_native.tolk:127-141](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L127-L141), [pool\_native.tolk:211-227](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L211-L227), [pool\_jetton.tolk:116-126](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_jetton.tolk#L116-L126)

The comment says `ext_out_msg_info$11 minimal flags`, but the prefix written is `storeUint(0x10, 6)` = `0b010000`. That decodes as `int_msg_info$0` + `ihr_disabled=1` + `bounce=0` + `bounced=0` + src tag = `00`. It then stores `dest = addr_none` (2 bits), an **internal message to addr\_none**, which is invalid (`int_msg` requires `MsgAddressInt` dest). It is neither `ext_out_msg_info$11` (which would start with `11`) nor a valid `int_msg`.

This will either be rejected by the action phase or silently dropped, and combined with C-2's lack of `+16` on the surrounding sends, could be the exact action-phase failure that strands a nullifier.

**Fix**: replace the manual header build with the canonical TON emit pattern. For Tolk 1.0+, prefer `createExternalLogMessage(...)` or the Misti-style typed builder. If staying with raw cells, the standard pattern is:

```tolk
var msg = beginCell()
    .storeUint(0x30, 6)        // 11 (ext_out tag) + 00 (src none) + 00 (dest none start)
    .storeUint(0, 64 + 32)     // created_lt + created_at (runtime fills)
    .storeUint(0, 1 + 1)       // no state init, body inline
    .storeUint(OP_LOG_DEPOSIT, 32)
    .storeUint(commitment, 256)
    .storeUint(leaf_index, 32)
    .endCell();
sendRawMessage(msg, 0);
```

Verify in `@ton/sandbox` with `printTransactionFees()` that the log actually appears as an `out_msgs` ext-out entry.

***

### H-5: `is_known_root` silently accepts unknown roots from corrupted slots

**File**: [merkle\_tree.tolk:134-152](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/merkle_tree.tolk#L134-L152)

```tolk
while (checks < ROOT_HISTORY_SIZE) {
    var stored = state.roots.uDictGet(32, i);
    if (!stored.beginParse().endsEmpty()) {     // a
        var v = stored.beginParse().loadUint(256);  // b — re-parse
        if (v == root) { return -1; }
    }
    ...
}
```

Two issues:

* `stored.beginParse()` is called twice (inefficient), and if `uDictGet` returns the *raw* found slice (not a cell) on a Tolk 1.x build, this code won't compile. Verify the actual `uDictGet` return type for your Tolk version (`(cell, int)` tuple vs `slice?` vs `cell`).
* The skip-on-empty branch silently advances past missing ring slots without flagging unexpected gaps. If the dict layout drifts (e.g., a `storeRef` slips into the value), `endsEmpty()` returns true and the entry is skipped, making a "known" root look unknown and DoSing withdraws.

Same return-type concern in [`commitment_exists`](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L275-L279) and [`nullifier_spent`](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L284-L287).

**Fix**: replace ad-hoc `uDictGet().beginParse().endsEmpty()` with the typed Tolk dict API for your version (`map<int32, int256>` + `.exists()` / `.get()` in Tolk 1.1+). Add a unit test that fills 30 roots, checks each is recognized, and confirms the 31st rotation invalidates root #1.

***

## Medium

### M-1: Bitwise `&` where logical `&&` is intended

**File**: [pool\_native.tolk:78](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L78)

```tolk
throw_if(ERR_PAUSED, state.paused == 1 & op != OP_ADMIN_UNPAUSE);
```

Operator precedence in Tolk (inherited from C: `==`/`!=` bind tighter than `&`) makes this parse as `(state.paused == 1) & (op != OP_ADMIN_UNPAUSE)`. Because TVM booleans are `-1`/`0`, bitwise AND of `-1 & -1 = -1` and `... & 0 = 0` *happens* to produce the right answer. But this is a code-smell that matches exactly the [tolk-security.md §11.2 footgun](https://github.com/tonadocash/monorepo/blob/main/.agents/skills/ton-best-practices/tolk-security.md#L1059-L1092) (no short-circuit, precedence trap).

**Fix**: `state.paused == 1 && op != OP_ADMIN_UNPAUSE`.

***

### M-2: `address_equal` / `sender_equals` rely on slice-hash equality

**Files**: [pool\_native.tolk:253-256](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L253-L256), [pool\_jetton.tolk:138-140](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_jetton.tolk#L138-L140)

`sliceHash()` hashes the raw bit pattern. Two slices encoding the *same logical address* in different forms (`addr_std$10` vs `addr_var$11`, or `anycast` bits set/unset) will hash differently. The owner is stored as a raw `slice`, so any deploy script using a slightly different encoding for owner vs sender will break admin operations.

**Fix**: use Tolk 1.2's `address` type (which canonicalizes), or parse both addresses and compare `(workchain, hash)` tuples explicitly.

***

### M-3: `denomination` is read as `coins` from msg\_value but never validates equality

**File**: [pool\_native.tolk:108-109](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L108-L109)

```tolk
throw_unless(ERR_WRONG_DENOMINATION, msg_value >= state.denomination);
```

Native pool uses `>=` (with the surplus refunded), which is fine. But the jetton pool uses `==` ([pool\_jetton.tolk:104](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_jetton.tolk#L104)). The asymmetry isn't documented. For a privacy mixer, **exact denomination is the anonymity-set invariant**. If the native pool accepts any deposit `>= denomination` it weakens the anonymity set (now there's a "user X deposited 1.04 TON, refunded to 1 TON; everyone else deposited exactly 1 TON" leak via the refund).

**Fix**: change native deposit to `==` and require the relayer/sender to attach exactly `denomination + GAS_BUDGET`, with `msg_value - denomination` refunded only if it's positive AND the receipt is via a *bounceable* path that won't leak the surplus.

***

### M-4: Storage layout in code disagrees with `lib/storage.tolk` documentation

**Files**: [lib/storage.tolk:1-30](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/lib/storage.tolk#L1-L30) vs [pool\_native.tolk:25-58](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L25-L58)

`lib/storage.tolk` documents 7 fields (`uint1 paused`, `address owner`, `ref verifier_address`, `coins denomination`, `ref merkle_state`, `ref nullifiers_dict`, `ref extra`). The actual `pool_native.tolk` `State` has 6 fields and no `verifier_address` ref. The off-chain ABI / wrapper code (auto-generated to `apps/contracts/wrappers/`) may rely on either layout.

**Fix**: pick one source of truth and delete the other. Adding `verifier_address` as a separate cell would also help with verifier upgradeability (governance-gated swap).

***

### M-5: Empty `ext_info` cell loaded but never used in native pool

**File**: [pool\_native.tolk:25-58](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L25-L58)

`State.ext_info` is loaded and re-serialized on every save but is unused for native. If a deploy ever passes an oversized `ext_info`, it consumes storage cells silently. Not exploitable but wastes gas.

**Fix**: drop the field for the native pool, or define it as `cell?` and serialize conditionally.

***

### M-6: No upfront gas budget check

**Files**: [pool\_native.tolk:108-148](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L108-L148), [pool\_native.tolk:162-228](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L162-L228)

Per the skill's [audit-checklist.md Phase 4](https://github.com/tonadocash/monorepo/blob/main/.agents/skills/ton-best-practices/audit-checklist.md#L102-L127): every handler should validate `msg_value >= compute_fee + forward_fee + buffer`. Today the only check is `msg_value >= state.denomination`, which lumps gas into the denomination. If denomination is small (e.g., 0.1 TON pool) and the relayer is malicious/cheap, the contract OOGs mid-flow.

**Fix**: hard-code (or `get`-derive) a `MIN_WITHDRAW_GAS` and `MIN_DEPOSIT_GAS` constant, validated up front. Cross-check via `printTransactionFees()` in sandbox tests.

***

## Low

### L-1: Owner stored as `slice`, not typed `address`

**File**: [pool\_native.tolk:27](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L27)

Tolk 1.2 has a proper `address` type that canonicalizes and rejects `addr_none`. Using `slice` defers all validation to runtime. Same applies to `jetton_master` and `pool_jetton_wallet` in [pool\_jetton.tolk:39-40](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_jetton.tolk#L39-L40).

### L-2: No two-step admin transfer

**File**: [pool\_native.tolk:231-235](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L231-L235)

Pause/unpause exist but there's no `OP_TRANSFER_OWNERSHIP`. If/when ownership rotation is added, follow the two-step pattern in [tolk-security.md §6.2](https://github.com/tonadocash/monorepo/blob/main/.agents/skills/ton-best-practices/tolk-security.md#L558-L589).

### L-3: Error code `0xffff` (`ERR_UNKNOWN_OP`) overlaps the empty-message convention

**File**: [lib/errors.tolk:4](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/lib/errors.tolk#L4)

The skill recommends `0xFFFF` for "accept empty message". The contract uses it for "unknown op". Pick distinct codes so monitoring can tell "user paid for an unknown op" apart from "user just topped up the pool".

### L-4: Magic numbers in `send_native`

**File**: [pool\_native.tolk:244-250](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/pool_native.tolk#L244-L250)

`storeUint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)` is hard to verify. Replace with named constants (`CURRENCY_NONE_BITS`, etc.) or use `createMessage()` if your Tolk version supports it.

***

## Informational

### I-1: `verifier.tolk` is intentionally a stub returning `0`

**File**: [verifier.tolk:15-17](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/verifier.tolk#L15-L17)

This is safe-by-construction (no withdraws can succeed) but a deploy-script footgun. Add a `get fun verifier_version(): int` returning `0` for the stub and a real value post-ceremony; gate `deploy-pool.ts` on the production value.

### I-2: `merkle_tree.zeros` precompute script is missing

The `scripts/` directory does not contain the `precompute-zeros.ts` referenced by [merkle\_tree.tolk:125](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/merkle_tree.tolk#L125). Either write it or hard-code the constants.

### I-3: No `@onExternalMessage` handler. Replay protection N/A

The contracts only handle internal messages. The relayer's signed external messages go to the relayer's wallet, not the pool. Good: no seqno/external-replay surface here.

### I-4: `poseidon.tolk` constants loaded via `asm "POSEIDON3_RC GETPARAM"`

**File**: [poseidon.tolk:40-43](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/contracts/poseidon.tolk#L40-L43)

These look like custom TVM-extension opcodes, not stock TVM-12. Confirm either (a) a build-step replaces these with real implementations, or (b) the target TVM version actually has these. If neither, the contract won't compile/run.

### I-5: `denomination` is a per-pool constant but loaded from storage every message

Saves no gas, costs cell reads. Could be embedded in code via a constant + per-denomination contract code. Mostly a gas optimization, wait until benchmarks.

***

## Recommended next steps (in order)

1. **Block any deploy.** C-1 through C-4 each independently make the contracts unsafe; H-4 likely makes every flow fail silently. Treat the contracts as "not ready for testnet" until all Critical and High items are fixed.
2. Add field-range validation (`ERR_FIELD_RANGE` for `nullifier_hash`, `commitment`, `root`). One-line fix, eliminates C-1.
3. Implement the bounce handler + `+16` on outgoing sends. Pair with `BounceMode.RichBounce` (Tolk 1.2 / TVM 12). Eliminates C-2 and H-3.
4. Complete `pool_jetton.handle_jetton_withdraw` and admin handlers, including the existing pause check missing from dispatch.
5. Fix the log-message header (H-4) and verify in `@ton/sandbox` that the `out_msgs` list contains exactly the expected entries.
6. Generate the `zeros[level]` constants and inline them.
7. Add prover/contract parity tests for `address_to_field` (H-2). These are the kind of bugs that go undetected until mainnet.
8. Migrate to Tolk 1.2 idioms (`address` type, struct auto-serialization, `createMessage`) to eliminate entire classes of footguns (M-1, M-2, L-1, L-4).
9. Run **Misti** (`pnpm dlx @nowarp/blueprint-misti`) and **TSA** ([espritoxyz/tsa](https://github.com/espritoxyz/tsa)) once the above is fixed.
10. **External audit**: pair a TON-native firm (TonBit / Zellic / Trail of Bits) for the Tolk side with a ZK-specialist (zkSecurity / Least Authority / ABDK) for the circuits and the generated `verifier.tolk`. This internal review does not replace either.

***

## What this review does NOT cover

* The circom circuits in [`apps/contracts/circuits/`](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/circuits/README.md): needs a ZK auditor.
* The trusted setup ceremony (the `ptau/` files).
* The relayer's mnemonic handling in `apps/relayer/`: flagged in [`CLAUDE.md`](https://github.com/tonadocash/monorepo/blob/main/CLAUDE.md) as the single highest-value secret in the system.
* The TS SDK in `packages/core/`: needs separate review for `parseNote`/`generateProof` correctness and timing-side-channels.
* Deploy scripts in [`apps/contracts/scripts/`](https://github.com/tonadocash/monorepo/blob/main/apps/contracts/scripts/README.md): the `verifier.tolk` and `pool_jetton` stub gates (I-1, C-3) belong here.
* The auto-generated `apps/contracts/wrappers/Verifier.ts` and `verifier.tolk` once regenerated. Re-run this review against the post-ceremony output.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.tonadocash.com/security/security-review.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
