| name | review-contract |
| description | Review Aztec smart contracts for correctness, security, and best practices. Use proactively after writing or modifying Aztec contracts. |
| allowed-tools | Read, Grep, Glob, Bash |
Aztec Contract Review Skill
Review Noir contracts written for the Aztec Network, focusing on correctness, security, and best practices.
Usage
/review-contract [file-path]
Examples:
/review-contract # Review contract in current context
/review-contract contracts/token/src/main.nr # Review specific file
/review-contract contracts/ # Review all contracts in directory
Workflow
Step 1: Identify Contract(s) to Review
If file path provided:
- Use the provided path directly
- If directory, find all
main.nr files within
If no path (use context):
- Check if a contract file was recently read or edited in conversation
- If not, search for contracts:
Glob: **/src/main.nr
Step 2: Sync Aztec Version (if needed)
Ensure MCP server has the correct version for accurate pattern matching:
aztec_status()
If repos not synced or version mismatch with project's Nargo.toml, run:
aztec_sync_repos({ version: "<detected-version>" })
Step 3: Read and Understand the Contract
- Read the contract file(s)
- Identify the contract's purpose from code and any comments
- If purpose is unclear, ask the user what the contract is intended to do
Step 4: Verify Patterns Against Current API
MANDATORY: Before flagging ANY issue as Critical or High severity, verify the pattern against the current Aztec source using the MCP server. Your training data may be stale — the MCP server has the actual current code.
aztec_search_code({ query: "<pattern-in-question>", filePattern: "*.nr" })
Also check the "Common False Positives" section below — if your finding matches one of those, do NOT include it.
Step 5: Review Against Checklist
Contract Structure
Function Visibility
| Attribute | Use Case |
|---|
#[external("private")] | Executes in PXE, reads/writes private state |
#[external("public")] | Executes on sequencer, visible to everyone |
#[external("utility")] + unconstrained | Off-chain reads without proofs |
#[view] | Read-only, doesn't modify state |
#[only_self] | Only callable by the contract itself |
#[internal("private")] | Internal function callable only within the contract (private domain) |
#[internal("public")] | Internal function callable only within the contract (public domain) |
#[authorize_once] | Requires one-time authorization (authwit) to call |
#[allow_phase_change] | Allows function to be called across phase boundaries |
Private State (Notes)
Private <> Public Boundary
Access Control
Cross-Contract Calls
Step 6: Flag Issues by Severity
Critical - Could cause loss of funds or privacy breaches:
- Privacy leaks (private data exposed in public functions)
- Incorrect note ownership allowing unauthorized spending
- Missing nullifier checks enabling double-spend
High - Significant bugs or security concerns:
- Missing access control on sensitive functions
- Incorrect msg_sender handling
- State inconsistencies between private and public
Medium - Best practice violations:
- Inefficient patterns
- Missing view annotations
- Unclear function purposes
Low - Code style or minor improvements:
- Naming conventions
- Code organization
- Documentation gaps
Step 7: Provide Recommendations
For each issue:
- Explain why it's a problem
- Show the current code
- Provide corrected code
- Reference similar patterns from
aztec_search_code if helpful
Output Format
## Contract Review: [ContractName]
### Summary
Brief overview of the contract's purpose and overall quality.
### Issues Found
#### Critical
- **[Issue Title]**: Description
- Location: `file:line`
- Current: `code snippet`
- Suggested: `fixed code`
#### High
...
#### Medium
...
#### Low
...
### Recommendations
Specific suggestions for improving the contract beyond fixing issues.
### What's Done Well
Highlight good practices observed in the contract.
Interactive Review
During review, you may ask the user clarifying questions:
- "This function transfers notes but has no access control. Is this intentional?"
- "The
sender field on this note cannot be used for authorization. Did you intend for the sender to be able to modify this note?"
- "This public function exposes the recipient address. Is this privacy tradeoff acceptable for your use case?"
Common False Positives — Do NOT Flag These
These are things Claude frequently gets wrong when reviewing Aztec contracts. Check this list BEFORE writing any finding:
-
Noir integer overflow is NOT a vulnerability. Noir u8, u64, u128 types PANIC on overflow — they do NOT wrap. Only Field arithmetic wraps. Do NOT flag u64 or u128 addition/multiplication as missing overflow protection. Do NOT suggest adding overflow guards for unsigned integer math. The only type that needs overflow caution is Field.
-
Notes do NOT need manual randomness fields. The #[note] macro automatically injects a NoteHeader with a nonce for commitment uniqueness. Do NOT flag notes as "missing randomness" or "predictable commitments."
-
Double .at() on Owned<PrivateSet<T>> is correct, not a bug. For Map<AztecAddress, Owned<PrivateSet<NoteType>>>, the first .at() indexes the Map, the second authenticates the Owned wrapper. Do NOT flag self.storage.balances.at(owner).at(owner) as redundant or incorrect.
If you are uncertain whether a pattern is correct, use aztec_search_code() to verify before flagging.
Common Aztec Pitfalls to Check
-
Storing addresses on notes for "access control" - Only the note owner can nullify. Fields are just data.
-
Trying to iterate over private state - Notes can't be enumerated. Use different patterns.
-
Exposing private data in public function parameters - Once public, always public.
-
Race conditions between private and public state - Private reads stale public state.
-
Misunderstanding msg_sender() behavior - self.msg_sender() returns AztecAddress directly (internally unwraps), but panics if sender is None. This happens at tx entrypoints (account contracts) and in public functions called via enqueue_incognito(). Use self.context.maybe_msg_sender() → Option<AztecAddress> when None is possible. Also note: msg_sender in enqueued public calls is visible on-chain, which can leak privacy.