Skip to content

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

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion crates/lint/src/sol/gas/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,10 @@ use crate::sol::{EarlyLintPass, SolLint};
mod keccak;
use keccak::ASM_KECCAK256;

register_lints!((AsmKeccak256, (ASM_KECCAK256)));
mod unwrapped_modifier_logic;
use unwrapped_modifier_logic::UNWRAPPED_MODIFIER_LOGIC;

register_lints!(
(AsmKeccak256, (ASM_KECCAK256)),
(UnwrappedModifierLogic, (UNWRAPPED_MODIFIER_LOGIC))
);
59 changes: 59 additions & 0 deletions crates/lint/src/sol/gas/unwrapped_modifier_logic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use super::UnwrappedModifierLogic;
use crate::{
linter::{EarlyLintPass, LintContext},
sol::{Severity, SolLint},
};
use solar_ast::{ExprKind, ItemFunction, Stmt, StmtKind};

declare_forge_lint!(
UNWRAPPED_MODIFIER_LOGIC,
Severity::Gas,
"unwrapped-modifier-logic",
"modifier logic should be wrapped to avoid code duplication and reduce codesize"
);

impl<'ast> EarlyLintPass<'ast> for UnwrappedModifierLogic {
fn check_item_function(&mut self, ctx: &LintContext<'_>, func: &'ast ItemFunction<'ast>) {
// If not a modifier, skip.
if !func.kind.is_modifier() {
return;
}

// If modifier has no contents, skip.
let Some(body) = &func.body else { return };

// If body contains unwrapped logic, emit.
if body.iter().any(|stmt| !is_valid_stmt(stmt))
&& let Some(name) = func.header.name
{
ctx.emit(&UNWRAPPED_MODIFIER_LOGIC, name.span);
}
}
}

fn is_valid_stmt(stmt: &Stmt<'_>) -> bool {
match &stmt.kind {
// If the statement is an expression, emit if not valid.
StmtKind::Expr(expr) => is_valid_expr(expr),

// If the statement is a placeholder, skip.
StmtKind::Placeholder => true,

// Disallow all other statements.
_ => false,
}
}

// TODO: Support library member calls like `Lib.foo` (throws false positives).
fn is_valid_expr(expr: &solar_ast::Expr<'_>) -> bool {
Copy link
Collaborator

Choose a reason for hiding this comment

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

can be simplified to

fn is_valid_expr(expr: &solar_ast::Expr<'_>) -> bool {
    if let ExprKind::Call(func_expr, _) = &expr.kind
        && let ExprKind::Ident(ident) = &func_expr.kind
    {
        return !matches!(ident.name.as_str(), "require" | "assert");
    }
    false
}

Copy link
Author

Choose a reason for hiding this comment

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

// If the expression is a call, continue.
if let ExprKind::Call(func_expr, _) = &expr.kind
&& let ExprKind::Ident(ident) = &func_expr.kind
{
// If the call is a built-in control flow function, emit.
return !matches!(ident.name.as_str(), "require" | "assert");
}

// Disallow all other expressions.
false
}
122 changes: 122 additions & 0 deletions crates/lint/testdata/UnwrappedModifierLogic.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/**
* @title UnwrappedModifierLogicTest
* @notice Test cases for the unwrapped-modifier-logic lint
* @dev This lint helps optimize gas by preventing modifier code duplication.
* Solidity inlines modifier code at each usage point instead of using jumps,
* so any logic in modifiers gets duplicated, increasing deployment costs.
*/
contract UnwrappedModifierLogicTest {
mapping(address => bool) public isOwner;

// Helpers

function checkPublic(address sender) public {
require(isOwner[sender], "Not owner");
}

function checkPrivate(address sender) private {
require(isOwner[sender], "Not owner");
}

function checkInternal(address sender) internal {
require(isOwner[sender], "Not owner");
}

// Good patterns

modifier empty() {
_;
}

modifier publicFn() {
checkPublic(msg.sender);
_;
}

modifier privateFn() {
checkPrivate(msg.sender);
_;
}

modifier internalFn() {
checkInternal(msg.sender);
_;
}

modifier publicPrivateInternal(address owner0, address owner1, address owner2) {
checkPublic(owner0);
checkPrivate(owner1);
checkInternal(owner2);
_;
}

// Bad patterns

modifier requireBuiltIn() { //~NOTE: modifier logic should be wrapped to avoid code duplication and reduce codesize
checkPublic(msg.sender);
require(isOwner[msg.sender], "Not owner");
checkPrivate(msg.sender);
_;
checkInternal(msg.sender);
}

modifier assertBuiltIn() { //~NOTE: modifier logic should be wrapped to avoid code duplication and reduce codesize
checkPublic(msg.sender);
assert(isOwner[msg.sender]);
checkPrivate(msg.sender);
_;
checkInternal(msg.sender);
}

modifier conditionalRevert() { //~NOTE: modifier logic should be wrapped to avoid code duplication and reduce codesize
checkPublic(msg.sender);
if (!isOwner[msg.sender]) {
revert("Not owner");
}
checkPrivate(msg.sender);
_;
checkInternal(msg.sender);
}

modifier assign(address sender) { //~NOTE: modifier logic should be wrapped to avoid code duplication and reduce codesize
checkPublic(sender);
bool _isOwner = true;
checkPrivate(sender);
isOwner[sender] = _isOwner;
_;
checkInternal(sender);
}

modifier assemblyBlock(address sender) { //~NOTE: modifier logic should be wrapped to avoid code duplication and reduce codesize
checkPublic(sender);
assembly {
let x := sender
}
checkPrivate(sender);
_;
checkInternal(sender);
}

modifier uncheckedBlock(address sender) { //~NOTE: modifier logic should be wrapped to avoid code duplication and reduce codesize
checkPublic(sender);
unchecked {
sender;
}
checkPrivate(sender);
_;
checkInternal(sender);
}

event DidSomething(address who);

modifier emitEvent(address sender) { //~NOTE: modifier logic should be wrapped to avoid code duplication and reduce codesize
checkPublic(sender);
emit DidSomething(sender);
checkPrivate(sender);
_;
checkInternal(sender);
}
}
56 changes: 56 additions & 0 deletions crates/lint/testdata/UnwrappedModifierLogic.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
note[unwrapped-modifier-logic]: modifier logic should be wrapped to avoid code duplication and reduce codesize
--> ROOT/testdata/UnwrappedModifierLogic.sol:LL:CC
|
58 | modifier requireBuiltIn() {
| --------------
|
= help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic

note[unwrapped-modifier-logic]: modifier logic should be wrapped to avoid code duplication and reduce codesize
--> ROOT/testdata/UnwrappedModifierLogic.sol:LL:CC
|
66 | modifier assertBuiltIn() {
| -------------
|
= help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic

note[unwrapped-modifier-logic]: modifier logic should be wrapped to avoid code duplication and reduce codesize
--> ROOT/testdata/UnwrappedModifierLogic.sol:LL:CC
|
74 | modifier conditionalRevert() {
| -----------------
|
= help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic

note[unwrapped-modifier-logic]: modifier logic should be wrapped to avoid code duplication and reduce codesize
--> ROOT/testdata/UnwrappedModifierLogic.sol:LL:CC
|
84 | modifier assign(address sender) {
| ------
|
= help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic

note[unwrapped-modifier-logic]: modifier logic should be wrapped to avoid code duplication and reduce codesize
--> ROOT/testdata/UnwrappedModifierLogic.sol:LL:CC
|
93 | modifier assemblyBlock(address sender) {
| -------------
|
= help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic

note[unwrapped-modifier-logic]: modifier logic should be wrapped to avoid code duplication and reduce codesize
--> ROOT/testdata/UnwrappedModifierLogic.sol:LL:CC
|
103 | modifier uncheckedBlock(address sender) {
| --------------
|
= help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic

note[unwrapped-modifier-logic]: modifier logic should be wrapped to avoid code duplication and reduce codesize
--> ROOT/testdata/UnwrappedModifierLogic.sol:LL:CC
|
115 | modifier emitEvent(address sender) {
| ---------
|
= help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic