Skip to content

Cairo: Fix missing use clause in hook for ERC20 votes#637

Merged
ericglau merged 6 commits intoOpenZeppelin:masterfrom
ericglau:cairoerc20votesimport
Aug 20, 2025
Merged

Cairo: Fix missing use clause in hook for ERC20 votes#637
ericglau merged 6 commits intoOpenZeppelin:masterfrom
ericglau:cairoerc20votesimport

Conversation

@ericglau
Copy link
Member

Fixes compile issue identified in #623 (comment)

@ericglau ericglau requested a review from a team August 18, 2025 17:58
@ericglau ericglau requested a review from a team as a code owner August 18, 2025 17:58
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 18, 2025

Walkthrough

Adds missing StarkNet ContractAddress use/import clauses in ERC20 hook generation for pausable and votes paths in Cairo and Cairo Alpha, updates corresponding test snapshots to include the import, and adds a changeset to bump @openzeppelin/wizard-cairo with a patch note.

Changes

Cohort / File(s) Summary
ERC20 hook generation (Cairo)
packages/core/cairo/src/erc20.ts
Insert use clause for starknet::ContractAddress before pausable before_update and votes after_update hook definitions.
ERC20 hook generation (Cairo Alpha)
packages/core/cairo_alpha/src/erc20.ts
Insert use clause for starknet::ContractAddress before pausable before_update and votes after_update hook definitions.
Test snapshots
packages/core/cairo/src/erc20.test.ts.md, packages/core/cairo_alpha/src/erc20.test.ts.md
Add use starknet::ContractAddress; import lines in ERC20 votes-related snippets.
Release metadata
.changeset/old-corners-scream.md
Add changeset for patch release: fix missing use clause in hooks for ERC20 votes.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested reviewers

  • ericnordelo

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@qodo-code-review
Copy link

User description

Fixes compile issue identified in #623 (comment)


PR Type

Bug fix


Description

  • Add missing ContractAddress import for ERC20 hooks

  • Fix compilation errors in pausable and votes features

  • Update test snapshots to reflect import changes


Diagram Walkthrough

flowchart LR
  A["ERC20 Hook Functions"] --> B["Add ContractAddress Import"]
  B --> C["Fix Compilation Errors"]
  C --> D["Update Test Snapshots"]
Loading

File Walkthrough

Relevant files
Bug fix
erc20.ts
Add missing ContractAddress imports to hooks                         

packages/core/cairo/src/erc20.ts

  • Add ContractAddress import for pausable hook function
  • Add ContractAddress import for votes hook function
+2/-0     
erc20.ts
Add missing ContractAddress imports to hooks                         

packages/core/cairo_alpha/src/erc20.ts

  • Add ContractAddress import for pausable hook function
  • Add ContractAddress import for votes hook function
+2/-0     
Documentation
old-corners-scream.md
Document fix for ERC20 votes hooks                                             

.changeset/old-corners-scream.md

  • Add changeset documenting the fix for missing use clause
+5/-0     
Tests
erc20.test.ts.md
Update test snapshot with new import                                         

packages/core/cairo/src/erc20.test.ts.md

  • Update test snapshot to include ContractAddress import
+1/-0     
erc20.test.ts.md
Update test snapshot with new import                                         

packages/core/cairo_alpha/src/erc20.test.ts.md

  • Update test snapshot to include ContractAddress import
+1/-0     

@qodo-code-review
Copy link

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Redundancy

The use clause for starknet::ContractAddress is added in both the pausable and votes branches; ensure it is only added once to avoid duplicate imports or consider adding it unconditionally when usesCustomHooks is true.

  c.addUseClause('starknet', 'ContractAddress');
  const beforeUpdateFn = c.addFunction(hooksTrait, {
    name: 'before_update',
    args: [
      {
        name: 'ref self',
        type: 'ERC20Component::ComponentState<ContractState>',
      },
      { name: 'from', type: 'ContractAddress' },
      { name: 'recipient', type: 'ContractAddress' },
      { name: 'amount', type: 'u256' },
    ],
    code: [],
  });

  beforeUpdateFn.code.push(
    'let contract_state = self.get_contract();',
    'contract_state.pausable.assert_not_paused();',
  );
}

if (allOpts.votes) {
  if (!allOpts.appName) {
    throw new OptionsError({
      appName: 'Application Name is required when Votes are enabled',
    });
  }

  if (!allOpts.appVersion) {
    throw new OptionsError({
      appVersion: 'Application Version is required when Votes are enabled',
    });
  }

  addVotesComponent(
    c,
    toFelt252(allOpts.appName, 'appName'),
    toFelt252(allOpts.appVersion, 'appVersion'),
    'SNIP12 Metadata',
  );

  c.addUseClause('starknet', 'ContractAddress');
Redundancy

Same as above: c.addUseClause('starknet', 'ContractAddress') is added in two places; consider deduplicating or moving it to a common path to prevent duplicate use statements.

  c.addUseClause('starknet', 'ContractAddress');
  const beforeUpdateFn = c.addFunction(hooksTrait, {
    name: 'before_update',
    args: [
      {
        name: 'ref self',
        type: 'ERC20Component::ComponentState<ContractState>',
      },
      { name: 'from', type: 'ContractAddress' },
      { name: 'recipient', type: 'ContractAddress' },
      { name: 'amount', type: 'u256' },
    ],
    code: [],
  });

  beforeUpdateFn.code.push(
    'let contract_state = self.get_contract();',
    'contract_state.pausable.assert_not_paused();',
  );
}

if (allOpts.votes) {
  if (!allOpts.appName) {
    throw new OptionsError({
      appName: 'Application Name is required when Votes are enabled',
    });
  }

  if (!allOpts.appVersion) {
    throw new OptionsError({
      appVersion: 'Application Version is required when Votes are enabled',
    });
  }

  addVotesComponent(
    c,
    toFelt252(allOpts.appName, 'appName'),
    toFelt252(allOpts.appVersion, 'appVersion'),
    'SNIP12 Metadata',
  );

  c.addUseClause('starknet', 'ContractAddress');

@qodo-code-review
Copy link

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Import once for all hooks

Ensure the ContractAddress use clause is added regardless of feature flags when
hooks are enabled. Currently it is only added under pausable, but after_update
for votes also needs it, which can lead to missing imports when votes is true
and pausable is false. Move the addUseClause call to the top of the hooks block
or before both hook functions.

packages/core/cairo/src/erc20.ts [112-114]

-if (allOpts.pausable) {
+const usesCustomHooks = allOpts.pausable || allOpts.votes;
+if (usesCustomHooks) {
+  const hooksTrait = {
+    name: 'ERC20HooksImpl',
+    of: 'ERC20Component::ERC20HooksTrait<ContractState>',
+    tags: [],
+    priority: 1,
+  };
+  c.addImplementedTrait(hooksTrait);
   c.addUseClause('starknet', 'ContractAddress');
-  const beforeUpdateFn = c.addFunction(hooksTrait, {
 
+  if (allOpts.pausable) {
+    const beforeUpdateFn = c.addFunction(hooksTrait, {
+      name: 'before_update',
+      args: [
+        {
+          ...
+        }
+      ],
+      ...
+    });
+    ...
+  }
+
+  if (allOpts.votes) {
+    ...
+    const afterUpdateFn = c.addFunction(hooksTrait, {
+      name: 'after_update',
+      args: [
+        {
+          ...
+        }
+      ],
+      ...
+    });
+    ...
+  }
+}
+

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies that the PR's approach of adding addUseClause in two separate conditional blocks is redundant and proposes a cleaner, more maintainable solution by adding it once.

Low
Centralize the import placement

Mirror the fix in the alpha variant by adding the ContractAddress use clause at
the start of the hooks block. This prevents missing import errors when only
votes is enabled and pausable is disabled.

packages/core/cairo_alpha/src/erc20.ts [112-114]

-if (allOpts.pausable) {
+const usesCustomHooks = allOpts.pausable || allOpts.votes;
+if (usesCustomHooks) {
+  const hooksTrait = {
+    name: 'ERC20HooksImpl',
+    of: 'ERC20Component::ERC20HooksTrait<ContractState>',
+    tags: [],
+    priority: 1,
+  };
+  c.addImplementedTrait(hooksTrait);
   c.addUseClause('starknet', 'ContractAddress');
-  const beforeUpdateFn = c.addFunction(hooksTrait, {
 
+  if (allOpts.pausable) {
+    const beforeUpdateFn = c.addFunction(hooksTrait, {
+      name: 'before_update',
+      args: [
+        {
+          ...
+        }
+      ],
+      ...
+    });
+    ...
+  }
+
+  if (allOpts.votes) {
+    ...
+    const afterUpdateFn = c.addFunction(hooksTrait, {
+      name: 'after_update',
+      args: [
+        {
+          ...
+        }
+      ],
+      ...
+    });
+    ...
+  }
+}
+

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies that the PR's approach of adding addUseClause in two separate conditional blocks is redundant and proposes a cleaner, more maintainable solution by adding it once.

Low
  • More

Copy link
Contributor

@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: 0

🧹 Nitpick comments (3)
.changeset/old-corners-scream.md (1)

5-5: Nit: Reflect that pausable hooks also gained the missing import

The PR also adds the ContractAddress use clause for the pausable hook path. Consider clarifying the changeset description to avoid confusion.

Apply this diff to make the note more complete:

-Fix missing use clause in hooks for ERC20 votes
+Fix missing use clause in hooks for ERC20 votes (and pausable) hooks
packages/core/cairo/src/erc20.ts (1)

101-177: Optional: Add the ContractAddress use clause once per hooks block to avoid duplication

Minor simplification: since both hook variants use ContractAddress in their signatures, import it once after adding the hooks trait instead of in each branch. The builder likely dedupes, but this reduces repetition and intent is clearer.

Apply this refactor:

 function addHooks(c: ContractBuilder, allOpts: Required<ERC20Options>) {
   const usesCustomHooks = allOpts.pausable || allOpts.votes;
   if (usesCustomHooks) {
     const hooksTrait = {
       name: 'ERC20HooksImpl',
       of: 'ERC20Component::ERC20HooksTrait<ContractState>',
       tags: [],
       priority: 1,
     };
     c.addImplementedTrait(hooksTrait);
+    // ContractAddress is required for hook arguments in all custom hook paths
+    c.addUseClause('starknet', 'ContractAddress');

     if (allOpts.pausable) {
-      c.addUseClause('starknet', 'ContractAddress');
       const beforeUpdateFn = c.addFunction(hooksTrait, {
         name: 'before_update',
         args: [
           {
             name: 'ref self',
             type: 'ERC20Component::ComponentState<ContractState>',
           },
           { name: 'from', type: 'ContractAddress' },
           { name: 'recipient', type: 'ContractAddress' },
           { name: 'amount', type: 'u256' },
         ],
         code: [],
       });
       beforeUpdateFn.code.push(
         'let contract_state = self.get_contract();',
         'contract_state.pausable.assert_not_paused();',
       );
     }

     if (allOpts.votes) {
       if (!allOpts.appName) {
         throw new OptionsError({
           appName: 'Application Name is required when Votes are enabled',
         });
       }
       if (!allOpts.appVersion) {
         throw new OptionsError({
           appVersion: 'Application Version is required when Votes are enabled',
         });
       }

       addVotesComponent(
         c,
         toFelt252(allOpts.appName, 'appName'),
         toFelt252(allOpts.appVersion, 'appVersion'),
         'SNIP12 Metadata',
       );

-      c.addUseClause('starknet', 'ContractAddress');
       const afterUpdateFn = c.addFunction(hooksTrait, {
         name: 'after_update',
         args: [
           {
             name: 'ref self',
             type: 'ERC20Component::ComponentState<ContractState>',
           },
           { name: 'from', type: 'ContractAddress' },
           { name: 'recipient', type: 'ContractAddress' },
           { name: 'amount', type: 'u256' },
         ],
         code: [],
       });
       afterUpdateFn.code.push(
         'let mut contract_state = self.get_contract_mut();',
         'contract_state.votes.transfer_voting_units(from, recipient, amount);',
       );
     }
   } else {
     c.addUseClause('openzeppelin::token::erc20', 'ERC20HooksEmptyImpl');
   }
 }
packages/core/cairo_alpha/src/erc20.ts (1)

101-177: Optional: Import ContractAddress once per hooks block

Mirror the small dedup from the Cairo stable file here to keep both generators consistent and slightly cleaner.

Apply this refactor:

 function addHooks(c: ContractBuilder, allOpts: Required<ERC20Options>) {
   const usesCustomHooks = allOpts.pausable || allOpts.votes;
   if (usesCustomHooks) {
     const hooksTrait = {
       name: 'ERC20HooksImpl',
       of: 'ERC20Component::ERC20HooksTrait<ContractState>',
       tags: [],
       priority: 1,
     };
     c.addImplementedTrait(hooksTrait);
+    c.addUseClause('starknet', 'ContractAddress');

     if (allOpts.pausable) {
-      c.addUseClause('starknet', 'ContractAddress');
       const beforeUpdateFn = c.addFunction(hooksTrait, {
         name: 'before_update',
         args: [
           {
             name: 'ref self',
             type: 'ERC20Component::ComponentState<ContractState>',
           },
           { name: 'from', type: 'ContractAddress' },
           { name: 'recipient', type: 'ContractAddress' },
           { name: 'amount', type: 'u256' },
         ],
         code: [],
       });
       beforeUpdateFn.code.push(
         'let contract_state = self.get_contract();',
         'contract_state.pausable.assert_not_paused();',
       );
     }

     if (allOpts.votes) {
       if (!allOpts.appName) {
         throw new OptionsError({
           appName: 'Application Name is required when Votes are enabled',
         });
       }
       if (!allOpts.appVersion) {
         throw new OptionsError({
           appVersion: 'Application Version is required when Votes are enabled',
         });
       }

       addVotesComponent(
         c,
         toFelt252(allOpts.appName, 'appName'),
         toFelt252(allOpts.appVersion, 'appVersion'),
         'SNIP12 Metadata',
       );

-      c.addUseClause('starknet', 'ContractAddress');
       const afterUpdateFn = c.addFunction(hooksTrait, {
         name: 'after_update',
         args: [
           {
             name: 'ref self',
             type: 'ERC20Component::ComponentState<ContractState>',
           },
           { name: 'from', type: 'ContractAddress' },
           { name: 'recipient', type: 'ContractAddress' },
           { name: 'amount', type: 'u256' },
         ],
         code: [],
       });
       afterUpdateFn.code.push(
         'let mut contract_state = self.get_contract_mut();',
         'contract_state.votes.transfer_voting_units(from, recipient, amount);',
       );
     }
   } else {
     c.addUseClause('openzeppelin::token::erc20', 'ERC20HooksEmptyImpl');
   }
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a5e5808 and 1b46b0c.

⛔ Files ignored due to path filters (2)
  • packages/core/cairo/src/erc20.test.ts.snap is excluded by !**/*.snap
  • packages/core/cairo_alpha/src/erc20.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (5)
  • .changeset/old-corners-scream.md (1 hunks)
  • packages/core/cairo/src/erc20.test.ts.md (1 hunks)
  • packages/core/cairo/src/erc20.ts (2 hunks)
  • packages/core/cairo_alpha/src/erc20.test.ts.md (1 hunks)
  • packages/core/cairo_alpha/src/erc20.ts (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build (solidity, default)
  • GitHub Check: validate-cairo-alpha
  • GitHub Check: validate-cairo
🔇 Additional comments (4)
packages/core/cairo/src/erc20.test.ts.md (1)

1114-1114: LGTM: Added ContractAddress import in votes non-upgradeable snapshot

This aligns the snapshot with the generated hooks requiring ContractAddress and fixes the compile issue.

packages/core/cairo_alpha/src/erc20.test.ts.md (1)

1117-1117: LGTM: Added ContractAddress import in votes non-upgradeable snapshot (alpha)

Snapshot now correctly imports ContractAddress for hook argument types.

packages/core/cairo/src/erc20.ts (1)

113-113: LGTM: Importing ContractAddress fixes the hook compile error

Adding c.addUseClause('starknet', 'ContractAddress') before defining before_update/after_update ensures the hook arg types resolve in both pausable and votes flows. Matches the updated snapshots and addresses the reported issue.

Also applies to: 154-154

packages/core/cairo_alpha/src/erc20.ts (1)

113-113: LGTM: Alpha generator now imports ContractAddress for hooks

Same fix as Cairo stable: ensures hook arg types resolve in both pausable and votes paths.

Also applies to: 154-154

Copy link
Member

@ericnordelo ericnordelo left a comment

Choose a reason for hiding this comment

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

Good catch!

@ericglau ericglau requested a review from immrsd August 19, 2025 12:26
Copy link
Contributor

@immrsd immrsd left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Contributor

@CoveMB CoveMB left a comment

Choose a reason for hiding this comment

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

LGTM would compilation tests have catch that?

@ericglau
Copy link
Member Author

ericglau commented Aug 20, 2025

LGTM would compilation tests have catch that?

Yes, it already tests some compilation but missed this combination. Due to compilation times, it's infeasible to test every combination for Cairo.

@ericglau ericglau enabled auto-merge (squash) August 20, 2025 13:35
@ericglau ericglau merged commit 79945e0 into OpenZeppelin:master Aug 20, 2025
20 checks passed
@github-actions github-actions bot locked and limited conversation to collaborators Aug 20, 2025
@ericglau ericglau deleted the cairoerc20votesimport branch August 20, 2025 13:43
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants