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.nrfiles 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
Before flagging issues, verify patterns using the MCP server:
aztec_search_code({ query: "<pattern-in-question>", filePattern: "*.nr" })
This prevents false positives from outdated knowledge.
Step 5: Review Against Checklist
Contract Structure
- [ ] Proper use of
#[aztec]attribute on contract module - [ ] Storage struct defined with
#[storage]attribute - [ ] Correct state variable types (PublicMutable, Owned, Map, etc.)
- [ ] Constructor/initializer properly defined with
#[initializer] - [ ] Functions have appropriate visibility attributes
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)
- [ ] Notes are properly created with correct owner
- [ ] Only note owners can nullify their notes (critical!)
- [ ] Nullifiers handled correctly to prevent double-spending
- [ ] No iteration over private state (impossible in Aztec)
Private <> Public Boundary
- [ ] Enqueued public calls used correctly from private functions
- [ ] No unintended information leakage between domains
- [ ] Understand that private-to-public is one-way in a transaction
Access Control
- [ ] Sensitive functions have proper access control
- [ ]
self.msg_sender()used correctly — returnsAztecAddressbut panics if sender is None (entrypoints, incognito calls). Useself.context.maybe_msg_sender()when None is possible. - [ ] Admin functions protected appropriately
Cross-Contract Calls
- [ ] Interfaces are auto-generated in v4 (no manual
#[aztec(interface)]needed) - [ ] Correct handling of return values
- [ ] Privacy implications understood
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_codeif 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
senderfield 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 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()returnsAztecAddressdirectly (internally unwraps), but panics if sender is None. This happens at tx entrypoints (account contracts) and in public functions called viaenqueue_incognito(). Useself.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.