diff --git a/crates/lint/src/sol/gas/mod.rs b/crates/lint/src/sol/gas/mod.rs index 69bc9422f57f3..7fac2bf1c465e 100644 --- a/crates/lint/src/sol/gas/mod.rs +++ b/crates/lint/src/sol/gas/mod.rs @@ -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)) +); diff --git a/crates/lint/src/sol/gas/unwrapped_modifier_logic.rs b/crates/lint/src/sol/gas/unwrapped_modifier_logic.rs new file mode 100644 index 0000000000000..ec96f45ea2399 --- /dev/null +++ b/crates/lint/src/sol/gas/unwrapped_modifier_logic.rs @@ -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", + "wrap modifier logic to reduce code size" +); + +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 { + // 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 +} diff --git a/crates/lint/testdata/UnwrappedModifierLogic.sol b/crates/lint/testdata/UnwrappedModifierLogic.sol new file mode 100644 index 0000000000000..8f7dfc689295a --- /dev/null +++ b/crates/lint/testdata/UnwrappedModifierLogic.sol @@ -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: wrap modifier logic to reduce code size + checkPublic(msg.sender); + require(isOwner[msg.sender], "Not owner"); + checkPrivate(msg.sender); + _; + checkInternal(msg.sender); + } + + modifier assertBuiltIn() { //~NOTE: wrap modifier logic to reduce code size + checkPublic(msg.sender); + assert(isOwner[msg.sender]); + checkPrivate(msg.sender); + _; + checkInternal(msg.sender); + } + + modifier conditionalRevert() { //~NOTE: wrap modifier logic to reduce code size + checkPublic(msg.sender); + if (!isOwner[msg.sender]) { + revert("Not owner"); + } + checkPrivate(msg.sender); + _; + checkInternal(msg.sender); + } + + modifier assign(address sender) { //~NOTE: wrap modifier logic to reduce code size + checkPublic(sender); + bool _isOwner = true; + checkPrivate(sender); + isOwner[sender] = _isOwner; + _; + checkInternal(sender); + } + + modifier assemblyBlock(address sender) { //~NOTE: wrap modifier logic to reduce code size + checkPublic(sender); + assembly { + let x := sender + } + checkPrivate(sender); + _; + checkInternal(sender); + } + + modifier uncheckedBlock(address sender) { //~NOTE: wrap modifier logic to reduce code size + checkPublic(sender); + unchecked { + sender; + } + checkPrivate(sender); + _; + checkInternal(sender); + } + + event DidSomething(address who); + + modifier emitEvent(address sender) { //~NOTE: wrap modifier logic to reduce code size + checkPublic(sender); + emit DidSomething(sender); + checkPrivate(sender); + _; + checkInternal(sender); + } +} \ No newline at end of file diff --git a/crates/lint/testdata/UnwrappedModifierLogic.stderr b/crates/lint/testdata/UnwrappedModifierLogic.stderr new file mode 100644 index 0000000000000..3ec35fa9304dc --- /dev/null +++ b/crates/lint/testdata/UnwrappedModifierLogic.stderr @@ -0,0 +1,56 @@ +note[unwrapped-modifier-logic]: wrap modifier logic to reduce code size + --> 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]: wrap modifier logic to reduce code size + --> 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]: wrap modifier logic to reduce code size + --> 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]: wrap modifier logic to reduce code size + --> 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]: wrap modifier logic to reduce code size + --> 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]: wrap modifier logic to reduce code size + --> 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]: wrap modifier logic to reduce code size + --> ROOT/testdata/UnwrappedModifierLogic.sol:LL:CC + | +115 | modifier emitEvent(address sender) { + | --------- + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#unwrapped-modifier-logic +