-
Notifications
You must be signed in to change notification settings - Fork 2k
feat(forge-lint): [claude] check for unwrapped modifiers #10967
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
feat(forge-lint): [claude] check for unwrapped modifiers #10967
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thank you, makes sense. left some nits, pending @0xrusowsky @DaniPopes review
overall the PR looks good, however i'd like to hold it back until we merge: and then make it do a fix suggestion |
\ |
that's merged now so we can rebuild this PR on top of it |
@0xClandestine if u want to see an example of how to generate snippets, you can check:
note that in that case, rather than using the AST, i used the HIR cause i needed semantic info (i think that for you case the AST should be enough) |
41fc4f8
to
bed9ce0
Compare
bed9ce0
to
d7a6829
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please improve the code suggestions, and let's wait for others to voice their opinions on whether to use fn post_source_unit
or not
@@ -14,141 +14,129 @@ declare_forge_lint!( | |||
|
|||
impl<'ast> EarlyLintPass<'ast> for UnwrappedModifierLogic { | |||
fn check_item_function(&mut self, ctx: &LintContext<'_>, func: &'ast ItemFunction<'ast>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
while i expect this to be true most of the time, technically speaking the lint will only reduce codesize if the modifier is used more than once.
@grandizzy @DaniPopes should we use fn post_source_unit
to also account for the modifier usage?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
makes sense to warn only if used more than once
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. However, I think it's still a good idea to emit the lint regardless if it the modifier is used more than once in the scope of the current contract. Abstract contracts like openzeppelin
's ReentrancyGuard
don't use their own modifiers directly; instead, they're intended for use by inheriting contracts.
Alternatively, could we simply note that the lint only results in bytecode size savings when the modifier it flags is used more than once?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds good, let's make sure to add it to the foundry book when documenting this lint
fn check_stmts(&self, stmts: &[Stmt<'_>]) -> bool { | ||
let mut total_valid = 0; | ||
for stmt in stmts { | ||
if !self.is_valid_stmt(stmt) { | ||
return true; | ||
} | ||
if let StmtKind::Expr(expr) = &stmt.kind | ||
&& self.is_valid_expr(expr) | ||
{ | ||
total_valid += 1; | ||
} | ||
} | ||
total_valid > 1 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since we check that total_valid > 1
, can we instead turn it into a bool
and early exit when the 2nd valid is found?
fn check_stmts(&self, stmts: &[Stmt<'_>]) -> bool { | |
let mut total_valid = 0; | |
for stmt in stmts { | |
if !self.is_valid_stmt(stmt) { | |
return true; | |
} | |
if let StmtKind::Expr(expr) = &stmt.kind | |
&& self.is_valid_expr(expr) | |
{ | |
total_valid += 1; | |
} | |
} | |
total_valid > 1 | |
} | |
fn check_stmts(&self, stmts: &[Stmt<'_>]) -> bool { | |
let mut has_valid = false; | |
for stmt in stmts { | |
if !self.is_valid_stmt(stmt) { | |
return true; | |
} | |
if let StmtKind::Expr(expr) = &stmt.kind | |
&& self.is_valid_expr(expr) | |
{ | |
if has_valid { return true; } | |
has_valid = true; | |
} | |
} | |
false | |
} |
total_valid > 1 | ||
} | ||
|
||
fn get_indent_and_span( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note for other reviewers: this fn is weird but kinda needed cause the diff is produced against the code span (which is printed indented).
i'll do a follow-up PR to remove the indentation of the original span, so that fixes don't need to care about spacing:
once that is implemented, this fn would only need to return the whole fn span
} | ||
|
||
impl UnwrappedModifierLogic { | ||
// TODO: Support library member calls like `Lib.foo` (throws false positives). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why would it throw false positives @0xClandestine?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's not a good way to differentiate between library member calls (which are valid) and other member calls (e.g. external calls) thus we simply mark all member calls as invalid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, i see.
if that's the case i'd encourage you to move your lint to LateLintPass
, as there you'll have the semantic information required to know if the the contract being called is a library or not... i think it's best if we avoid throwing false positives now that we have the tools 🙂 (you'll see that using the HIR is quite similar to using the AST)
also if you need help or don't feel like doing the change yourself, i can give u a hand, just lmk!
Motivation
Coauthored by Claude 4 Opus Max
Modifiers in Solidity that contain logic directly in their body can lead to code duplication when used across multiple functions. Since modifiers are inlined at compile time, any logic within them is duplicated for each function that uses the modifier increasing contract size and deployment cost. A common best practice is to extract modifier logic into internal functions that can be called from within the modifier, reducing code duplication and contract size.
Solution
This PR adds a new gas optimization lint
unwrapped-modifier-logic
that detects when modifiers contain logic directly in their body instead of usinginternal/private/public
functions.The lint checks for modifiers that contain any statements other than:
internal/private/public
functions._;
Example of code that triggers the lint:
Recommended pattern:
PR Checklist
testdata/ModifierLogic.sol
andtestdata/ModifierLogic.stderr
with comprehensive test cases