Skip to content

Commit 9c7b2e6

Browse files
figtracerampagent
andauthored
refactor(lint): improve selected Slither-parity lints (#15086)
* Improve lint parity with Slither * Improve Slither lint parity * test(forge): update lint parity snapshots Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019e9872-f047-7033-acf4-1f985641d25e * fix(lint): tighten divide-before-multiply taint Amp-Thread-ID: https://ampcode.com/threads/T-019e9872-f047-7033-acf4-1f985641d25e Co-authored-by: Amp <amp@ampcode.com> * fix(lint): improve Slither parity for return and timestamp * fix(lint): avoid unused-return branch false positives * fix(lint): avoid stale branch taint false positives --------- Co-authored-by: Amp <amp@ampcode.com>
1 parent 8cf9809 commit 9c7b2e6

34 files changed

Lines changed: 1697 additions & 365 deletions

crates/forge/tests/cli/lint.rs

Lines changed: 9 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ uint256 constant screaming_snake_case_info = 0;
1818
contract ContractWithLints {
1919
uint256 VARIABLE_MIXED_CASE_INFO;
2020
21-
function incorrectShiftHigh() public {
22-
uint256 localValue = 50;
23-
uint256 result = 8 >> localValue;
21+
function incorrectShiftHigh() public pure {
22+
uint256 result = 8;
23+
assembly { result := shr(result, 8) }
2424
}
2525
function divideBeforeMultiplyMedium() public {
2626
(1 / 2) * 3;
@@ -666,10 +666,10 @@ forgetest!(can_override_config_lint, |prj, cmd| {
666666
cmd.arg("lint").args(["--only-lint", "incorrect-shift"]).assert_success().stderr_eq(str![[
667667
r#"
668668
warning[incorrect-shift]: the order of args in a shift operation is incorrect
669-
[FILE]:13:26
669+
[FILE]:13:30
670670
671-
13 │ uint256 result = 8 >> localValue;
672-
━━━━━━━━━━━━━━
671+
13 │ assembly { result := shr(result, 8) }
672+
━━━━━━━━━━━━━━
673673
674674
╰ help: https://getfoundry.sh/forge/linting/incorrect-shift
675675
@@ -707,24 +707,12 @@ warning[divide-before-multiply]: multiplication should occur before division to
707707
[COMPILING_FILES] with [SOLC_VERSION]
708708
[SOLC_VERSION] [ELAPSED]
709709
Compiler run successful with warnings:
710-
Warning (2072): Unused local variable.
711-
[FILE]:13:9:
712-
|
713-
13 | uint256 result = 8 >> localValue;
714-
| ^^^^^^^^^^^^^^
715-
716710
Warning (6133): Statement has no effect.
717711
[FILE]:16:9:
718712
|
719713
16 | (1 / 2) * 3;
720714
| ^^^^^^^^^^^
721715
722-
Warning (2018): Function state mutability can be restricted to pure
723-
[FILE]:11:5:
724-
|
725-
11 | function incorrectShiftHigh() public {
726-
| ^ (Relevant source part starts here and spans across multiple lines).
727-
728716
Warning (2018): Function state mutability can be restricted to pure
729717
[FILE]:15:5:
730718
|
@@ -806,24 +794,12 @@ forgetest!(build_respects_lint_on_build_false, |prj, cmd| {
806794
[COMPILING_FILES] with [SOLC_VERSION]
807795
[SOLC_VERSION] [ELAPSED]
808796
Compiler run successful with warnings:
809-
Warning (2072): Unused local variable.
810-
[FILE]:13:9:
811-
|
812-
13 | uint256 result = 8 >> localValue;
813-
| ^^^^^^^^^^^^^^
814-
815797
Warning (6133): Statement has no effect.
816798
[FILE]:16:9:
817799
|
818800
16 | (1 / 2) * 3;
819801
| ^^^^^^^^^^^
820802
821-
Warning (2018): Function state mutability can be restricted to pure
822-
[FILE]:11:5:
823-
|
824-
11 | function incorrectShiftHigh() public {
825-
| ^ (Relevant source part starts here and spans across multiple lines).
826-
827803
Warning (2018): Function state mutability can be restricted to pure
828804
[FILE]:15:5:
829805
|
@@ -882,24 +858,12 @@ forgetest!(build_no_lint_flag_skips_lint, |prj, cmd| {
882858
[COMPILING_FILES] with [SOLC_VERSION]
883859
[SOLC_VERSION] [ELAPSED]
884860
Compiler run successful with warnings:
885-
Warning (2072): Unused local variable.
886-
[FILE]:13:9:
887-
|
888-
13 | uint256 result = 8 >> localValue;
889-
| ^^^^^^^^^^^^^^
890-
891861
Warning (6133): Statement has no effect.
892862
[FILE]:16:9:
893863
|
894864
16 | (1 / 2) * 3;
895865
| ^^^^^^^^^^^
896866
897-
Warning (2018): Function state mutability can be restricted to pure
898-
[FILE]:11:5:
899-
|
900-
11 | function incorrectShiftHigh() public {
901-
| ^ (Relevant source part starts here and spans across multiple lines).
902-
903867
Warning (2018): Function state mutability can be restricted to pure
904868
[FILE]:15:5:
905869
|
@@ -1427,22 +1391,14 @@ forgetest!(pragma_inconsistent_cross_file, |prj, cmd| {
14271391

14281392
cmd.arg("lint").args(["--only-lint", "pragma-inconsistent"]).assert_success().stderr_eq(str![
14291393
[r#"
1430-
note[pragma-inconsistent]: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: 0.8.20
1394+
note[pragma-inconsistent]: 2 different Solidity pragma version requirements are used: 0.8.20, ^0.8.20
14311395
[FILE]:3:1
14321396
14331397
3 │ pragma solidity ^0.8.20;
14341398
│ ━━━━━━━━━━━━━━━━━━━━━━━━
14351399
14361400
╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent
14371401
1438-
note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: ^0.8.20
1439-
[FILE]:3:1
1440-
1441-
3 │ pragma solidity 0.8.20;
1442-
│ ━━━━━━━━━━━━━━━━━━━━━━━
1443-
1444-
╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent
1445-
14461402
14471403
"#]
14481404
]);
@@ -1538,30 +1494,14 @@ forgetest!(pragma_inconsistent_duplicates_among_conflict, |prj, cmd| {
15381494

15391495
cmd.arg("lint").args(["--only-lint", "pragma-inconsistent"]).assert_success().stderr_eq(str![
15401496
[r#"
1541-
note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: ^0.8.20
1497+
note[pragma-inconsistent]: 2 different Solidity pragma version requirements are used: 0.8.20, ^0.8.20
15421498
[FILE]:3:1
15431499
15441500
3 │ pragma solidity 0.8.20;
15451501
│ ━━━━━━━━━━━━━━━━━━━━━━━
15461502
15471503
╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent
15481504
1549-
note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: ^0.8.20
1550-
[FILE]:3:1
1551-
1552-
3 │ pragma solidity 0.8.20;
1553-
│ ━━━━━━━━━━━━━━━━━━━━━━━
1554-
1555-
╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent
1556-
1557-
note[pragma-inconsistent]: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: 0.8.20
1558-
[FILE]:3:1
1559-
1560-
3 │ pragma solidity ^0.8.20;
1561-
│ ━━━━━━━━━━━━━━━━━━━━━━━━
1562-
1563-
╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent
1564-
15651505
15661506
"#]
15671507
]);
@@ -1578,22 +1518,14 @@ forgetest!(pragma_inconsistent_files_without_pragma, |prj, cmd| {
15781518

15791519
cmd.arg("lint").args(["--only-lint", "pragma-inconsistent"]).assert_success().stderr_eq(str![
15801520
[r#"
1581-
note[pragma-inconsistent]: 'pragma solidity 0.8.20;' conflicts with other version requirements in the project: ^0.8.20
1521+
note[pragma-inconsistent]: 2 different Solidity pragma version requirements are used: 0.8.20, ^0.8.20
15821522
[FILE]:3:1
15831523
15841524
3 │ pragma solidity 0.8.20;
15851525
│ ━━━━━━━━━━━━━━━━━━━━━━━
15861526
15871527
╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent
15881528
1589-
note[pragma-inconsistent]: 'pragma solidity ^0.8.20;' conflicts with other version requirements in the project: 0.8.20
1590-
[FILE]:3:1
1591-
1592-
3 │ pragma solidity ^0.8.20;
1593-
│ ━━━━━━━━━━━━━━━━━━━━━━━━
1594-
1595-
╰ help: https://getfoundry.sh/forge/linting/pragma-inconsistent
1596-
15971529
15981530
"#]
15991531
]);
Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,39 @@
1-
# Incorrect shift order
1+
# Incorrect Yul shift order
22

33
**Severity**: `High`
44
**ID**: `incorrect-shift`
55

6-
Flags shift operations where a literal appears on the left and a non-literal on the right, which
7-
is almost always the wrong operand order.
6+
Flags Yul `shl` and `shr` calls whose operands appear to be reversed.
87

98
## What it does
109

11-
Warns when the left-hand operand of `<<` or `>>` is a numeric literal and the right-hand operand
12-
is a non-literal expression (e.g. a variable, function call, or composite expression).
10+
Warns when the first argument to a Yul `shl` or `shr` call is dynamic and the second argument is a
11+
literal. Yul shift calls take the shift amount first and the value second, so `shr(value, 8)`
12+
shifts the literal `8` by `value`; the usual intended expression is `shr(8, value)`.
1313

1414
## Why is this bad?
1515

16-
Shift expressions like `2 << x` are usually a typo for `x << 2`. In the former, the *value being
17-
shifted* is a tiny constant and the *shift amount* is dynamic — almost never the intended
18-
behavior, and a known source of bugs in production contracts.
16+
Yul's shift argument order is easy to confuse with high-level Solidity operators. Reversing the
17+
arguments silently changes which value is shifted and can produce incorrect arithmetic,
18+
bit-packing, or bounds logic.
1919

2020
## Example
2121

2222
### Bad
2323

2424
```solidity
25-
result = 2 << stateValue; // shift amount comes from state
26-
result = 8 >> localValue; // shift amount comes from a local
27-
result = 16 << (stateValue + 1); // shift amount is a dynamic expression
25+
assembly {
26+
result := shl(value, 8)
27+
result := shr(add(value, 1), 16)
28+
}
2829
```
2930

3031
### Good
3132

3233
```solidity
33-
result = stateValue << 2;
34-
result = localValue >> 3;
35-
result = stateValue << localShiftAmount;
36-
result = 1 << 8; // both literals — fine
34+
assembly {
35+
result := shl(8, value)
36+
result := shr(16, add(value, 1))
37+
result := shl(8, 1) // both literals
38+
}
3739
```

crates/lint/docs/pragma-inconsistent.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ pragmas.
88

99
## What it does
1010

11-
Inspects every `pragma solidity ...;` directive across all input source files and reports when
11+
Inspects every `pragma solidity ...;` directive across all input source files and reports once when
1212
their version requirements are inconsistent (different exact versions, mixed caret/tilde/range
13-
shapes, etc.).
13+
shapes, etc.). The diagnostic lists the distinct requirements seen in the project.
1414

1515
## Why is this bad?
1616

crates/lint/src/sol/analysis/interface.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,22 @@ pub fn is_elementary(hir: &hir::Hir<'_>, id: VariableId, abi: &str) -> bool {
1010
/// Static contract type of a method-call receiver: a contract-typed variable
1111
/// or an `IFoo(addr)` interface wrap.
1212
pub fn receiver_contract_id(hir: &hir::Hir<'_>, recv: &Expr<'_>) -> Option<ContractId> {
13-
match &recv.kind {
13+
match &recv.peel_parens().kind {
1414
ExprKind::Ident([Res::Item(ItemId::Variable(id)), ..]) => {
1515
if let TypeKind::Custom(ItemId::Contract(cid)) = hir.variable(*id).ty.kind {
1616
Some(cid)
1717
} else {
1818
None
1919
}
2020
}
21-
ExprKind::Call(
22-
Expr { kind: ExprKind::Ident([Res::Item(ItemId::Contract(cid))]), .. },
23-
..,
24-
) => Some(*cid),
21+
ExprKind::Call(callee, ..) => {
22+
if let ExprKind::Ident([Res::Item(ItemId::Contract(cid))]) = &callee.peel_parens().kind
23+
{
24+
Some(*cid)
25+
} else {
26+
None
27+
}
28+
}
2529
_ => None,
2630
}
2731
}

crates/lint/src/sol/high/incorrect_shift.rs

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use crate::{
33
linter::{EarlyLintPass, LintContext},
44
sol::{Severity, SolLint},
55
};
6-
use solar::ast::{BinOp, BinOpKind, Expr, ExprKind};
6+
use solar::{
7+
ast::{Stmt, StmtKind, yul},
8+
interface::kw,
9+
};
710

811
declare_forge_lint!(
912
INCORRECT_SHIFT,
@@ -13,28 +16,62 @@ declare_forge_lint!(
1316
);
1417

1518
impl<'ast> EarlyLintPass<'ast> for IncorrectShift {
16-
fn check_expr(&mut self, ctx: &LintContext, expr: &'ast Expr<'ast>) {
17-
if let ExprKind::Binary(
18-
left_expr,
19-
BinOp { kind: BinOpKind::Shl | BinOpKind::Shr, .. },
20-
right_expr,
21-
) = &expr.kind
22-
&& contains_incorrect_shift(left_expr, right_expr)
23-
{
24-
ctx.emit(&INCORRECT_SHIFT, expr.span);
19+
fn check_stmt(&mut self, ctx: &LintContext, stmt: &'ast Stmt<'ast>) {
20+
if let StmtKind::Assembly(assembly) = &stmt.kind {
21+
check_yul_block(ctx, &assembly.block);
2522
}
2623
}
2724
}
2825

29-
// TODO: come up with a better heuristic. Treat initial impl as a PoC.
30-
// Checks if the left operand is a literal and the right operand is not, indicating a potential
31-
// reversed shift operation.
32-
const fn contains_incorrect_shift<'ast>(
33-
left_expr: &'ast Expr<'ast>,
34-
right_expr: &'ast Expr<'ast>,
35-
) -> bool {
36-
let is_left_literal = matches!(left_expr.kind, ExprKind::Lit(..));
37-
let is_right_not_literal = !matches!(right_expr.kind, ExprKind::Lit(..));
26+
fn check_yul_block(ctx: &LintContext, block: &yul::Block<'_>) {
27+
for stmt in block.stmts.iter() {
28+
check_yul_stmt(ctx, stmt);
29+
}
30+
}
3831

39-
is_left_literal && is_right_not_literal
32+
fn check_yul_stmt(ctx: &LintContext, stmt: &yul::Stmt<'_>) {
33+
match &stmt.kind {
34+
yul::StmtKind::Block(block) => check_yul_block(ctx, block),
35+
yul::StmtKind::AssignSingle(_, expr)
36+
| yul::StmtKind::AssignMulti(_, expr)
37+
| yul::StmtKind::Expr(expr) => check_yul_expr(ctx, expr),
38+
yul::StmtKind::If(cond, block) => {
39+
check_yul_expr(ctx, cond);
40+
check_yul_block(ctx, block);
41+
}
42+
yul::StmtKind::For(for_stmt) => {
43+
check_yul_block(ctx, &for_stmt.init);
44+
check_yul_expr(ctx, &for_stmt.cond);
45+
check_yul_block(ctx, &for_stmt.step);
46+
check_yul_block(ctx, &for_stmt.body);
47+
}
48+
yul::StmtKind::Switch(switch) => {
49+
check_yul_expr(ctx, &switch.selector);
50+
for case in switch.cases.iter() {
51+
check_yul_block(ctx, &case.body);
52+
}
53+
}
54+
yul::StmtKind::FunctionDef(func) => check_yul_block(ctx, &func.body),
55+
yul::StmtKind::VarDecl(_, Some(init)) => check_yul_expr(ctx, init),
56+
yul::StmtKind::Leave
57+
| yul::StmtKind::Break
58+
| yul::StmtKind::Continue
59+
| yul::StmtKind::VarDecl(_, None) => {}
60+
}
61+
}
62+
63+
fn check_yul_expr(ctx: &LintContext, expr: &yul::Expr<'_>) {
64+
let yul::ExprKind::Call(call) = &expr.kind else { return };
65+
66+
if matches!(call.name.name, kw::Shl | kw::Shr | kw::Sar)
67+
&& let [left, right] = call.arguments.as_ref()
68+
&& !matches!(left.kind, yul::ExprKind::Lit(_))
69+
&& matches!(right.kind, yul::ExprKind::Lit(_))
70+
{
71+
ctx.emit(&INCORRECT_SHIFT, expr.span);
72+
}
73+
74+
for arg in call.arguments.iter() {
75+
check_yul_expr(ctx, arg);
76+
}
4077
}

crates/lint/src/sol/info/pragma_directive.rs

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,13 @@ impl<'ast> ProjectLintPass<'ast> for PragmaDirective {
4040
return;
4141
}
4242

43-
for (idx, span, req_str) in &entries {
44-
let others = distinct
45-
.iter()
46-
.filter(|v| **v != req_str.as_str())
47-
.copied()
48-
.collect::<Vec<_>>()
49-
.join(", ");
50-
let msg = format!(
51-
"'pragma solidity {req_str};' conflicts with other version requirements in the project: {others}"
52-
);
53-
ctx.emit_with_msg(&sources[*idx], &PRAGMA_INCONSISTENT, *span, msg);
54-
}
43+
let (idx, span, _) = entries[0];
44+
let versions = distinct.join(", ");
45+
let msg = format!(
46+
"{} different Solidity pragma version requirements are used: {versions}",
47+
distinct.len()
48+
);
49+
ctx.emit_with_msg(&sources[idx], &PRAGMA_INCONSISTENT, span, msg);
5550
}
5651
}
5752

0 commit comments

Comments
 (0)