forge_lint/sol/low/
block_timestamp.rs1use super::BlockTimestamp;
2use crate::{
3 linter::{EarlyLintPass, LintContext},
4 sol::{Severity, SolLint},
5};
6use solar::ast::{BinOp, BinOpKind, Expr, ExprKind};
7
8declare_forge_lint!(
9 BLOCK_TIMESTAMP,
10 Severity::Low,
11 "block-timestamp",
12 "usage of `block.timestamp` in a comparison may be manipulated by validators"
13);
14
15impl<'ast> EarlyLintPass<'ast> for BlockTimestamp {
16 fn check_expr(&mut self, ctx: &LintContext, expr: &'ast Expr<'ast>) {
17 if let ExprKind::Binary(lhs, BinOp { kind, .. }, rhs) = &expr.kind
18 && is_cmp(*kind)
19 && (contains_block_timestamp(lhs) || contains_block_timestamp(rhs))
20 {
21 ctx.emit(&BLOCK_TIMESTAMP, expr.span);
22 }
23 }
24}
25
26const fn is_cmp(kind: BinOpKind) -> bool {
27 matches!(
28 kind,
29 BinOpKind::Lt
30 | BinOpKind::Le
31 | BinOpKind::Gt
32 | BinOpKind::Ge
33 | BinOpKind::Eq
34 | BinOpKind::Ne
35 )
36}
37
38fn is_block_timestamp(expr: &Expr<'_>) -> bool {
40 matches!(
41 &expr.kind,
42 ExprKind::Member(base, member)
43 if member.as_str() == "timestamp"
44 && matches!(&base.kind, ExprKind::Ident(ident) if ident.as_str() == "block")
45 )
46}
47
48fn contains_block_timestamp(expr: &Expr<'_>) -> bool {
50 if is_block_timestamp(expr) {
51 return true;
52 }
53 match &expr.kind {
54 ExprKind::Unary(_, inner) => contains_block_timestamp(inner),
55 ExprKind::Binary(lhs, _, rhs) => {
56 contains_block_timestamp(lhs) || contains_block_timestamp(rhs)
57 }
58 ExprKind::Tuple(elems) => elems.iter().any(|e| {
59 if let solar::interface::SpannedOption::Some(inner) = e.as_ref() {
60 contains_block_timestamp(inner)
61 } else {
62 false
63 }
64 }),
65 ExprKind::Call(callee, args) => {
66 contains_block_timestamp(callee) || args.exprs().any(|e| contains_block_timestamp(e))
67 }
68 _ => false,
69 }
70}