Skip to content

fix(circuits): add range constraints and isMint burn path#99

Merged
iap merged 2 commits into
devfrom
fix/circuit-range-constraints
May 11, 2026
Merged

fix(circuits): add range constraints and isMint burn path#99
iap merged 2 commits into
devfrom
fix/circuit-range-constraints

Conversation

@iap
Copy link
Copy Markdown
Contributor

@iap iap commented May 11, 2026

Adds range constraints to UTXOSettlement circuit to prevent malformed private inputs.

Changes:

  • amount: Num2Bits(64) — prevents values > 2^64
  • recipient: Num2Bits(160) — prevents values > 2^160 (Ethereum address range)
  • settlementModule: Num2Bits(160) — same
  • chainId: Num2Bits(64) — prevents values > 2^64

MARKPool.withdraw now accepts isMint param: true mints RYLA to recipient, false burns RYLA from recipient (pre-approved).

Trusted setup rerun for updated circuit (1050 constraints, pot11).

Scope: circuits, contracts

Verification: 9 witness tests passing. 84 Solidity unit tests passing.

Risk: Low. Circuit change requires trusted setup rerun (done). No deployed contracts affected.

Summary by CodeRabbit

  • New Features

    • withdraw() now accepts an isMint flag to support both mint and burn settlement flows.
  • Bug Fixes / Chores

    • Updated on-chain proof verification parameters to align with the current proving artifact.
  • Tests

    • Circuit and contract tests updated to use the new proof inputs and direct witness calculation.
  • Refactor

    • Strengthened input validity checks in the UTXO settlement circuit (explicit bit-length constraints).

Review Change Stack

UTXOSettlement circuit:
- Add Num2Bits(64) constraint on amount (prevents >64-bit values)
- Add Num2Bits(160) constraint on recipient and settlementModule
- Add Num2Bits(64) constraint on chainId
- 1050 non-linear constraints (up from 602)

MARKPool:
- withdraw() now accepts isMint param (true=mint to recipient, false=burn from recipient)
- Document settlementModule binding is circuit-enforced

Trusted setup rerun for updated circuit. Verification key updated.
9 witness tests passing. 84 Solidity unit tests passing.
@iap iap requested a review from a team as a code owner May 11, 2026 12:43
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 11, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0036d832c1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread contracts/src/pool/MARKPool.sol
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

Walkthrough

The PR adds formal bit-size constraints to the UTXOSettlement circuit, regenerates Groth16 verification artifacts and verifier constants, extends MARKPool.withdraw with an isMint flag to select mint vs burn behavior, and refactors circuit tests to use a single instantiated witness calculator and updated call signatures.

Changes

UTXO Settlement Constraints & Mint/Burn Mode

Layer / File(s) Summary
Circuit Input Constraints
circuits/utxo/UTXOSettlement.circom
Added bitify.circom import and explicit bit-decomposition constraints: amount (64 bits, non-zero), recipient (160 bits), chainId (64 bits), settlementModule (160 bits). Preserved Poseidon-based nullifierHash and commitmentHash constraints.
Verification Key & Verifier Constants
circuits/artifacts/utxo_verification_key.json, contracts/src/pool/verifier/UTXOVerifier.sol
Groth16 BN128 verification key updated: vk_delta_2 and initial IC coordinates changed in the JSON artifact; verifier deltax/deltay and IC0–IC4 constants in UTXOVerifier.sol were replaced to match the regenerated key.
Contract Withdraw Signature & Docs
contracts/src/pool/MARKPool.sol
withdraw() signature extended with bool isMint (inserted between amount and nullifierHash) and documentation updated to describe mint vs burn flows and commitment replay protection.
Proof Signals & Token Handling
contracts/src/pool/MARKPool.sol
Proof public signals now include isMint as signals[3]. After proof verification the contract marks the nullifier used, deletes the commitment, and conditionally mints to recipient when isMint=true or transfers-from-recipient and burns when isMint=false.
Witness Test Harness
circuits/test/UTXOSettlement.test.mjs
Witness calculator loaded from build artifacts and instantiated once; expectPass/expectFail call wc.calculateWitness() directly; test vectors refactored to use a valid base object with per-case overrides.
Contract Unit Tests: Signature Updates
contracts/test/unit/pool/MARKPool.t.sol
Unit tests updated to include the new isMint boolean argument in withdraw() calls across success and revert-path tests.

Sequence Diagram

sequenceDiagram
  participant Caller
  participant MARKPool
  participant UTXOVerifier
  participant Groth16
  Caller->>MARKPool: withdraw(recipient, amount, isMint, nullifierHash, commitmentHash, proof)
  MARKPool->>MARKPool: Build signals [nullifierHash, commitmentHash, amount, isMint]
  MARKPool->>UTXOVerifier: verifyProof(signals, proof)
  UTXOVerifier->>Groth16: perform pairing checks with IC/delta constants
  Groth16-->>UTXOVerifier: verification result
  UTXOVerifier-->>MARKPool: boolean valid
  MARKPool->>MARKPool: mark nullifierHash as spent
  MARKPool->>MARKPool: delete commitment
  MARKPool->>MARKPool: if isMint == 1 then mint amount to recipient
  MARKPool->>MARKPool: if isMint == 0 then transfer amount from recipient and burn
  MARKPool-->>Caller: Done
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • trade/mark#96: Directly related—overlaps UTXOSettlement.circom constraint changes and witness test adjustments.
  • trade/mark#97: Related—also updates verification artifacts, verifier constants, and MARKPool.withdraw integration.
  • trade/mark#98: Related—regenerates Groth16 key and syncs verifier constants and withdraw signal wiring.

Poem

I checked each bit and hopped through every gate,
Keys remade anew so proofs line up straight,
The pool learned to mint or to burn with one flag,
Tests hum with a witness and circuits wag their tail. 🐰✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers the key technical changes and verification results, but is missing several required template sections including explicit scope checkboxes, verification checklist items, risk review items, and governance checklist items. Fill out all required template sections with appropriate checkboxes and details: complete the Scope checklist, add detailed Verification checklist results, complete Risk Review items, and confirm Governance requirements (target branch, CODEOWNER review, docs updates).
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the two main changes: adding range constraints to the circuit and implementing the isMint burn path functionality.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/circuit-range-constraints

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@circuits/test/UTXOSettlement.test.mjs`:
- Around line 74-88: Add a failing test case that mirrors the other overflow
checks: call expectFail with a descriptive message like "settlementModule
exceeds 160 bits" and pass a test vector spreading valid but setting
settlementModule to 2n ** 160n, and compute commitmentHash using
poseidonHash(secret, amount, isMint, recipient, chainId, 2n ** 160n); place this
new expectFail in UTXOSettlement.test.mjs next to the other overflow tests so
the Num2Bits(160) constraint for settlementModule is validated.

In `@contracts/src/pool/MARKPool.sol`:
- Around line 149-154: Add a unit test exercising the burn branch when isMint ==
false: use the MARKPool flow that calls TOKEN.safeTransferFrom(recipient,
address(this), amount) and TOKEN.burn(amount) to assert the recipient has
approved the pool beforehand (approve(poolAddress, amount)), that
safeTransferFrom transfers tokens into the pool, and that burn reduces total
supply/balance accordingly; also update documentation/comments around the
function that processes commitments (reference commitmentHash and proof) to
state clearly that access control is enforced by the zero-knowledge proof (proof
must bind to the recipient’s shielded secret) and not by msg.sender, and note
the explicit requirement that the recipient must pre-approve the pool contract
for the burn path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c526499e-458a-4c08-a33f-e5238575c53f

📥 Commits

Reviewing files that changed from the base of the PR and between fc29df6 and 0036d83.

📒 Files selected for processing (5)
  • circuits/artifacts/utxo_verification_key.json
  • circuits/test/UTXOSettlement.test.mjs
  • circuits/utxo/UTXOSettlement.circom
  • contracts/src/pool/MARKPool.sol
  • contracts/src/pool/verifier/UTXOVerifier.sol

Comment thread circuits/test/UTXOSettlement.test.mjs
Comment thread contracts/src/pool/MARKPool.sol
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
contracts/test/unit/pool/MARKPool.t.sol (1)

79-125: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add explicit isMint = false tests for the new burn branch.

From Line 83 through Line 124, every updated withdraw call passes true, so this suite validates only mint mode. Please add burn-path coverage (isMint = false) with:

  1. a success case (recipient has balance + approves pool), and
  2. a failure case (missing approval or insufficient balance).

Without this, the newly introduced branch can regress silently.

Proposed test additions
+    function testWithdrawBurnsFromRecipientWhenIsMintFalse() public {
+        vm.prank(operator);
+        pool.commit(COMMITMENT, AMOUNT);
+
+        // Prepare recipient balance and approval for burn path
+        vm.prank(admin);
+        token.mint(recipient, AMOUNT);
+        vm.prank(recipient);
+        token.approve(address(pool), AMOUNT);
+
+        pool.withdraw(recipient, AMOUNT, false, NULLIFIER, COMMITMENT, A, B, C);
+
+        assertEq(token.balanceOf(recipient), 0);
+        assertTrue(pool.usedNullifiers(NULLIFIER));
+        assertEq(pool.commitments(COMMITMENT), 0);
+    }
+
+    function testWithdrawBurnRevertsWithoutApproval() public {
+        vm.prank(operator);
+        pool.commit(COMMITMENT, AMOUNT);
+
+        vm.prank(admin);
+        token.mint(recipient, AMOUNT);
+
+        vm.expectRevert(); // replace with concrete selector if exposed by token/pool
+        pool.withdraw(recipient, AMOUNT, false, NULLIFIER, COMMITMENT, A, B, C);
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/test/unit/pool/MARKPool.t.sol` around lines 79 - 125, Add two tests
exercising the new burn branch of Pool.withdraw (isMint = false): one success
and one failure. Create a success test (e.g.,
testWithdrawBurnsFromRecipientSuccess) that mints or assigns AMOUNT to
recipient, has recipient approve the pool for AMOUNT, commits the commitment as
operator, then calls pool.withdraw(recipient, AMOUNT, false, NULLIFIER,
COMMITMENT, A, B, C) and asserts recipient balance decreased by AMOUNT,
pool.usedNullifiers(NULLIFIER) is true and pool.commitments(COMMITMENT) == 0.
Create a failure test (e.g.,
testWithdrawBurnsFromRecipientRevertsOnInsufficientApprovalOrBalance) that sets
up recipient without sufficient balance or without approving pool and then
vm.expectRevert with the appropriate PoolErrors (Allowance/transfer failure or
CommitmentInvalid if applicable) before calling pool.withdraw(...) with isMint =
false to ensure the burn path reverts correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@contracts/test/unit/pool/MARKPool.t.sol`:
- Around line 79-125: Add two tests exercising the new burn branch of
Pool.withdraw (isMint = false): one success and one failure. Create a success
test (e.g., testWithdrawBurnsFromRecipientSuccess) that mints or assigns AMOUNT
to recipient, has recipient approve the pool for AMOUNT, commits the commitment
as operator, then calls pool.withdraw(recipient, AMOUNT, false, NULLIFIER,
COMMITMENT, A, B, C) and asserts recipient balance decreased by AMOUNT,
pool.usedNullifiers(NULLIFIER) is true and pool.commitments(COMMITMENT) == 0.
Create a failure test (e.g.,
testWithdrawBurnsFromRecipientRevertsOnInsufficientApprovalOrBalance) that sets
up recipient without sufficient balance or without approving pool and then
vm.expectRevert with the appropriate PoolErrors (Allowance/transfer failure or
CommitmentInvalid if applicable) before calling pool.withdraw(...) with isMint =
false to ensure the burn path reverts correctly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3e9f43a4-429f-44bb-acc5-f7c9611bf955

📥 Commits

Reviewing files that changed from the base of the PR and between 0036d83 and 348837d.

📒 Files selected for processing (1)
  • contracts/test/unit/pool/MARKPool.t.sol

@iap iap merged commit e0a8937 into dev May 11, 2026
19 checks passed
@iap iap deleted the fix/circuit-range-constraints branch May 11, 2026 12:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant