Skip to main content

forge_lint/sol/low/
block_timestamp.rs

1use super::BlockTimestamp;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::{
7    ast,
8    interface::{kw, sym},
9    sema::{
10        Gcx, Hir,
11        builtins::Builtin,
12        hir::{
13            BinOpKind, Block, ContractId, Expr, ExprKind, Function, FunctionId, ItemId, Res, Stmt,
14            StmtKind, VariableId,
15        },
16    },
17};
18use std::collections::HashSet;
19
20declare_forge_lint!(
21    BLOCK_TIMESTAMP,
22    Severity::Low,
23    "block-timestamp",
24    "usage of `block.timestamp` in a comparison may be manipulated by validators"
25);
26
27impl<'hir> LateLintPass<'hir> for BlockTimestamp {
28    fn check_function(
29        &mut self,
30        ctx: &LintContext,
31        _gcx: Gcx<'hir>,
32        hir: &'hir Hir<'hir>,
33        func: &'hir Function<'hir>,
34    ) {
35        if let Some(body) = func.body {
36            let helpers = timestamp_helpers(hir, func.contract);
37            let mut aliases = HashSet::new();
38            check_block(ctx, hir, &helpers, body, &mut aliases);
39        }
40    }
41}
42
43fn check_block<'hir>(
44    ctx: &LintContext,
45    hir: &'hir Hir<'hir>,
46    helpers: &HashSet<FunctionId>,
47    block: Block<'hir>,
48    aliases: &mut HashSet<VariableId>,
49) -> bool {
50    for stmt in block.stmts {
51        if !check_stmt(ctx, hir, helpers, stmt, aliases) {
52            return false;
53        }
54    }
55    true
56}
57
58fn check_stmt<'hir>(
59    ctx: &LintContext,
60    hir: &'hir Hir<'hir>,
61    helpers: &HashSet<FunctionId>,
62    stmt: &'hir Stmt<'hir>,
63    aliases: &mut HashSet<VariableId>,
64) -> bool {
65    match &stmt.kind {
66        StmtKind::DeclSingle(var_id) => {
67            if let Some(init) = hir.variable(*var_id).initializer {
68                check_expr(ctx, hir, helpers, init, aliases);
69                update_alias(
70                    hir,
71                    *var_id,
72                    expr_value_is_timestamp_source(helpers, init, aliases),
73                    aliases,
74                );
75            }
76            true
77        }
78        StmtKind::DeclMulti(vars, expr) => {
79            check_expr(ctx, hir, helpers, expr, aliases);
80            update_multi_aliases(hir, helpers, vars, expr, aliases);
81            true
82        }
83        StmtKind::Expr(expr) => {
84            check_expr(ctx, hir, helpers, expr, aliases);
85            !is_revert_call(expr)
86        }
87        StmtKind::Emit(expr) => {
88            check_expr(ctx, hir, helpers, expr, aliases);
89            true
90        }
91        StmtKind::Revert(expr) | StmtKind::Return(Some(expr)) => {
92            check_expr(ctx, hir, helpers, expr, aliases);
93            false
94        }
95        StmtKind::If(cond, then_stmt, else_stmt) => {
96            check_expr(ctx, hir, helpers, cond, aliases);
97
98            let baseline = aliases.clone();
99            let mut merged = HashSet::new();
100            let mut falls_through = false;
101
102            let mut then_aliases = baseline.clone();
103            if check_stmt(ctx, hir, helpers, then_stmt, &mut then_aliases) {
104                merged.extend(then_aliases);
105                falls_through = true;
106            }
107
108            if let Some(else_stmt) = else_stmt {
109                let mut else_aliases = baseline;
110                if check_stmt(ctx, hir, helpers, else_stmt, &mut else_aliases) {
111                    merged.extend(else_aliases);
112                    falls_through = true;
113                }
114            } else {
115                merged.extend(baseline);
116                falls_through = true;
117            }
118
119            if falls_through {
120                *aliases = merged;
121            }
122            falls_through
123        }
124        StmtKind::Loop(block, _) => {
125            let baseline = aliases.clone();
126            let mut loop_aliases = baseline.clone();
127            *aliases = if check_block(ctx, hir, helpers, *block, &mut loop_aliases) {
128                baseline.union(&loop_aliases).copied().collect()
129            } else {
130                baseline
131            };
132            true
133        }
134        StmtKind::Try(try_stmt) => {
135            check_expr(ctx, hir, helpers, &try_stmt.expr, aliases);
136
137            let baseline = aliases.clone();
138            let mut merged = baseline.clone();
139            for clause in try_stmt.clauses {
140                let mut clause_aliases = baseline.clone();
141                if check_block(ctx, hir, helpers, clause.block, &mut clause_aliases) {
142                    merged.extend(clause_aliases);
143                }
144            }
145            *aliases = merged;
146            true
147        }
148        StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
149            check_block(ctx, hir, helpers, *block, aliases)
150        }
151        StmtKind::AssemblyBlock(block) => check_block(ctx, hir, helpers, *block, aliases),
152        StmtKind::Switch(switch) => {
153            check_expr(ctx, hir, helpers, switch.selector, aliases);
154
155            let baseline = aliases.clone();
156            let mut merged = baseline.clone();
157            for case in switch.cases {
158                let mut case_aliases = baseline.clone();
159                if check_block(ctx, hir, helpers, case.body, &mut case_aliases) {
160                    merged.extend(case_aliases);
161                }
162            }
163            *aliases = merged;
164            true
165        }
166        StmtKind::Return(None) => false,
167        StmtKind::Break | StmtKind::Continue | StmtKind::Placeholder | StmtKind::Err(_) => true,
168    }
169}
170
171fn check_expr<'hir>(
172    ctx: &LintContext,
173    hir: &'hir Hir<'hir>,
174    helpers: &HashSet<FunctionId>,
175    expr: &'hir Expr<'hir>,
176    aliases: &mut HashSet<VariableId>,
177) {
178    match &expr.peel_parens().kind {
179        ExprKind::Assign(lhs, op, rhs) => {
180            check_expr(ctx, hir, helpers, rhs, aliases);
181            let rhs_is_timestamp = expr_value_is_timestamp_source(helpers, rhs, aliases);
182
183            if op.is_some() {
184                check_expr(ctx, hir, helpers, lhs, aliases);
185                update_lhs_aliases(
186                    hir,
187                    lhs,
188                    rhs_is_timestamp || expr_value_is_timestamp_source(helpers, lhs, aliases),
189                    aliases,
190                );
191            } else {
192                update_assignment_aliases(hir, helpers, lhs, rhs, aliases);
193            }
194        }
195        ExprKind::Binary(lhs, op, rhs) => {
196            if is_cmp(op.kind)
197                && (expr_contains_timestamp_source(helpers, lhs, aliases)
198                    || expr_contains_timestamp_source(helpers, rhs, aliases))
199            {
200                ctx.emit(&BLOCK_TIMESTAMP, expr.span);
201            }
202
203            check_expr(ctx, hir, helpers, lhs, aliases);
204            check_expr(ctx, hir, helpers, rhs, aliases);
205        }
206        ExprKind::Call(callee, args, options) => {
207            check_expr(ctx, hir, helpers, callee, aliases);
208            if let Some(options) = options {
209                for arg in options.args {
210                    check_expr(ctx, hir, helpers, &arg.value, aliases);
211                }
212            }
213            for arg in args.exprs() {
214                check_expr(ctx, hir, helpers, arg, aliases);
215            }
216        }
217        ExprKind::Unary(_, inner)
218        | ExprKind::Delete(inner)
219        | ExprKind::Member(inner, _)
220        | ExprKind::Payable(inner)
221        | ExprKind::YulMember(inner, _) => check_expr(ctx, hir, helpers, inner, aliases),
222        ExprKind::Ternary(cond, then_expr, else_expr) => {
223            check_expr(ctx, hir, helpers, cond, aliases);
224
225            let baseline = aliases.clone();
226            let mut then_aliases = baseline.clone();
227            check_expr(ctx, hir, helpers, then_expr, &mut then_aliases);
228            let mut else_aliases = baseline;
229            check_expr(ctx, hir, helpers, else_expr, &mut else_aliases);
230            *aliases = then_aliases.union(&else_aliases).copied().collect();
231        }
232        ExprKind::Tuple(exprs) => {
233            for expr in exprs.iter().flatten() {
234                check_expr(ctx, hir, helpers, expr, aliases);
235            }
236        }
237        ExprKind::Array(exprs) => {
238            for expr in *exprs {
239                check_expr(ctx, hir, helpers, expr, aliases);
240            }
241        }
242        ExprKind::Index(base, index) => {
243            check_expr(ctx, hir, helpers, base, aliases);
244            if let Some(index) = index {
245                check_expr(ctx, hir, helpers, index, aliases);
246            }
247        }
248        ExprKind::Slice(base, start, end) => {
249            check_expr(ctx, hir, helpers, base, aliases);
250            if let Some(start) = start {
251                check_expr(ctx, hir, helpers, start, aliases);
252            }
253            if let Some(end) = end {
254                check_expr(ctx, hir, helpers, end, aliases);
255            }
256        }
257        ExprKind::Ident(_)
258        | ExprKind::Lit(_)
259        | ExprKind::New(_)
260        | ExprKind::TypeCall(_)
261        | ExprKind::Type(_)
262        | ExprKind::Err(_) => {}
263    }
264}
265
266const fn is_cmp(kind: BinOpKind) -> bool {
267    matches!(
268        kind,
269        BinOpKind::Lt
270            | BinOpKind::Le
271            | BinOpKind::Gt
272            | BinOpKind::Ge
273            | BinOpKind::Eq
274            | BinOpKind::Ne
275    )
276}
277
278fn update_multi_aliases(
279    hir: &Hir<'_>,
280    helpers: &HashSet<FunctionId>,
281    vars: &[Option<VariableId>],
282    expr: &Expr<'_>,
283    aliases: &mut HashSet<VariableId>,
284) {
285    if let ExprKind::Tuple(exprs) = &expr.peel_parens().kind
286        && exprs.len() == vars.len()
287    {
288        let rhs_aliases: Vec<_> = exprs
289            .iter()
290            .map(|expr| {
291                expr.is_some_and(|expr| expr_value_is_timestamp_source(helpers, expr, aliases))
292            })
293            .collect();
294        for (var_id, rhs_is_timestamp) in vars.iter().zip(rhs_aliases) {
295            if let Some(var_id) = var_id {
296                update_alias(hir, *var_id, rhs_is_timestamp, aliases);
297            }
298        }
299        return;
300    }
301
302    let rhs_is_timestamp = expr_value_is_timestamp_source(helpers, expr, aliases);
303    for var_id in vars.iter().flatten() {
304        update_alias(hir, *var_id, rhs_is_timestamp, aliases);
305    }
306}
307
308fn update_assignment_aliases(
309    hir: &Hir<'_>,
310    helpers: &HashSet<FunctionId>,
311    lhs: &Expr<'_>,
312    rhs: &Expr<'_>,
313    aliases: &mut HashSet<VariableId>,
314) {
315    if let (ExprKind::Tuple(lhs_exprs), ExprKind::Tuple(rhs_exprs)) =
316        (&lhs.peel_parens().kind, &rhs.peel_parens().kind)
317        && lhs_exprs.len() == rhs_exprs.len()
318    {
319        let rhs_aliases: Vec<_> = rhs_exprs
320            .iter()
321            .map(|rhs| rhs.is_some_and(|rhs| expr_value_is_timestamp_source(helpers, rhs, aliases)))
322            .collect();
323        for (lhs, rhs_is_timestamp) in lhs_exprs.iter().zip(rhs_aliases) {
324            if let Some(lhs) = lhs {
325                update_lhs_aliases(hir, lhs, rhs_is_timestamp, aliases);
326            }
327        }
328        return;
329    }
330
331    let rhs_is_timestamp = expr_value_is_timestamp_source(helpers, rhs, aliases);
332    update_lhs_aliases(hir, lhs, rhs_is_timestamp, aliases);
333}
334
335fn update_lhs_aliases(
336    hir: &Hir<'_>,
337    lhs: &Expr<'_>,
338    is_timestamp: bool,
339    aliases: &mut HashSet<VariableId>,
340) {
341    match &lhs.peel_parens().kind {
342        ExprKind::Ident(resolutions) => {
343            for res in *resolutions {
344                if let Res::Item(ItemId::Variable(var_id)) = res {
345                    update_alias(hir, *var_id, is_timestamp, aliases);
346                }
347            }
348        }
349        ExprKind::Tuple(exprs) => {
350            for expr in exprs.iter().flatten() {
351                update_lhs_aliases(hir, expr, is_timestamp, aliases);
352            }
353        }
354        _ => {}
355    }
356}
357
358fn update_alias(
359    hir: &Hir<'_>,
360    var_id: VariableId,
361    is_timestamp: bool,
362    aliases: &mut HashSet<VariableId>,
363) {
364    if !hir.variable(var_id).is_local_or_return() {
365        return;
366    }
367    if is_timestamp {
368        aliases.insert(var_id);
369    } else {
370        aliases.remove(&var_id);
371    }
372}
373
374fn expr_contains_timestamp_source(
375    helpers: &HashSet<FunctionId>,
376    expr: &Expr<'_>,
377    aliases: &HashSet<VariableId>,
378) -> bool {
379    if expr_value_is_timestamp_source(helpers, expr, aliases) {
380        return true;
381    }
382
383    match &expr.peel_parens().kind {
384        ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
385            expr_contains_timestamp_source(helpers, lhs, aliases)
386                || expr_contains_timestamp_source(helpers, rhs, aliases)
387        }
388        ExprKind::Call(callee, args, options) => {
389            expr_contains_timestamp_source(helpers, callee, aliases)
390                || options.is_some_and(|options| {
391                    options
392                        .args
393                        .iter()
394                        .any(|arg| expr_contains_timestamp_source(helpers, &arg.value, aliases))
395                })
396                || args.exprs().any(|arg| expr_contains_timestamp_source(helpers, arg, aliases))
397        }
398        ExprKind::Unary(_, inner)
399        | ExprKind::Delete(inner)
400        | ExprKind::Member(inner, _)
401        | ExprKind::Payable(inner)
402        | ExprKind::YulMember(inner, _) => expr_contains_timestamp_source(helpers, inner, aliases),
403        ExprKind::Ternary(cond, then_expr, else_expr) => {
404            expr_contains_timestamp_source(helpers, cond, aliases)
405                || expr_contains_timestamp_source(helpers, then_expr, aliases)
406                || expr_contains_timestamp_source(helpers, else_expr, aliases)
407        }
408        ExprKind::Tuple(exprs) => exprs
409            .iter()
410            .flatten()
411            .any(|expr| expr_contains_timestamp_source(helpers, expr, aliases)),
412        ExprKind::Array(exprs) => {
413            exprs.iter().any(|expr| expr_contains_timestamp_source(helpers, expr, aliases))
414        }
415        ExprKind::Index(base, index) => {
416            expr_contains_timestamp_source(helpers, base, aliases)
417                || index
418                    .is_some_and(|index| expr_contains_timestamp_source(helpers, index, aliases))
419        }
420        ExprKind::Slice(base, start, end) => {
421            expr_contains_timestamp_source(helpers, base, aliases)
422                || start
423                    .is_some_and(|start| expr_contains_timestamp_source(helpers, start, aliases))
424                || end.is_some_and(|end| expr_contains_timestamp_source(helpers, end, aliases))
425        }
426        ExprKind::Ident(_)
427        | ExprKind::Lit(_)
428        | ExprKind::New(_)
429        | ExprKind::TypeCall(_)
430        | ExprKind::Type(_)
431        | ExprKind::Err(_) => false,
432    }
433}
434
435fn expr_value_is_timestamp_source(
436    helpers: &HashSet<FunctionId>,
437    expr: &Expr<'_>,
438    aliases: &HashSet<VariableId>,
439) -> bool {
440    if is_block_timestamp(expr) || is_timestamp_helper_call(helpers, expr) {
441        return true;
442    }
443
444    match &expr.peel_parens().kind {
445        ExprKind::Ident(resolutions) => resolutions.iter().any(
446            |res| matches!(res, Res::Item(ItemId::Variable(var_id)) if aliases.contains(var_id)),
447        ),
448        ExprKind::Binary(lhs, op, rhs) if !is_cmp(op.kind) => {
449            expr_value_is_timestamp_source(helpers, lhs, aliases)
450                || expr_value_is_timestamp_source(helpers, rhs, aliases)
451        }
452        ExprKind::Unary(_, inner) | ExprKind::Payable(inner) | ExprKind::YulMember(inner, _) => {
453            expr_value_is_timestamp_source(helpers, inner, aliases)
454        }
455        ExprKind::Ternary(_, then_expr, else_expr) => {
456            expr_value_is_timestamp_source(helpers, then_expr, aliases)
457                || expr_value_is_timestamp_source(helpers, else_expr, aliases)
458        }
459        ExprKind::Tuple([Some(inner)]) => expr_value_is_timestamp_source(helpers, inner, aliases),
460        _ => false,
461    }
462}
463
464fn is_block_timestamp(expr: &Expr<'_>) -> bool {
465    match &expr.peel_parens().kind {
466        ExprKind::Member(base, member) if member.name == kw::Timestamp => {
467            let ExprKind::Ident(reses) = &base.peel_parens().kind else { return false };
468            reses
469                .iter()
470                .any(|res| matches!(res, Res::Builtin(builtin) if builtin.name() == sym::block))
471        }
472        ExprKind::Ident(reses) => {
473            reses.iter().any(|res| matches!(res, Res::Builtin(Builtin::BlockTimestamp)))
474        }
475        _ => false,
476    }
477}
478
479fn timestamp_helpers(hir: &Hir<'_>, contract: Option<ContractId>) -> HashSet<FunctionId> {
480    let Some(contract) = contract else { return HashSet::new() };
481    hir.contract_item_ids(contract)
482        .filter_map(|item| item.as_function())
483        .filter(|fid| {
484            let helper = hir.function(*fid);
485            helper.contract == Some(contract)
486                && matches!(helper.visibility, ast::Visibility::Internal | ast::Visibility::Private)
487                && helper.body.is_some_and(block_directly_returns_timestamp)
488        })
489        .collect()
490}
491
492fn is_timestamp_helper_call(helpers: &HashSet<FunctionId>, expr: &Expr<'_>) -> bool {
493    let ExprKind::Call(callee, _, _) = &expr.peel_parens().kind else { return false };
494    helper_function_ids(callee).into_iter().any(|fid| helpers.contains(&fid))
495}
496
497fn is_revert_call(expr: &Expr<'_>) -> bool {
498    let ExprKind::Call(callee, _, _) = &expr.peel_parens().kind else { return false };
499    let ExprKind::Ident(resolutions) = &callee.peel_parens().kind else { return false };
500    resolutions.iter().any(|res| matches!(res, Res::Builtin(Builtin::Revert | Builtin::RevertMsg)))
501}
502
503fn helper_function_ids(callee: &Expr<'_>) -> Vec<FunctionId> {
504    let ExprKind::Ident(resolutions) = &callee.peel_parens().kind else { return Vec::new() };
505    resolutions
506        .iter()
507        .filter_map(
508            |res| {
509                if let Res::Item(ItemId::Function(fid)) = res { Some(*fid) } else { None }
510            },
511        )
512        .collect()
513}
514
515fn block_directly_returns_timestamp(block: Block<'_>) -> bool {
516    block.stmts.iter().any(stmt_directly_returns_timestamp)
517}
518
519fn stmt_directly_returns_timestamp(stmt: &Stmt<'_>) -> bool {
520    match &stmt.kind {
521        StmtKind::Return(Some(expr)) => expr_contains_direct_block_timestamp(expr),
522        StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
523            block_directly_returns_timestamp(*block)
524        }
525        StmtKind::If(_, then_stmt, else_stmt) => {
526            stmt_directly_returns_timestamp(then_stmt)
527                || else_stmt.is_some_and(stmt_directly_returns_timestamp)
528        }
529        _ => false,
530    }
531}
532
533fn expr_contains_direct_block_timestamp(expr: &Expr<'_>) -> bool {
534    if is_block_timestamp(expr) {
535        return true;
536    }
537
538    match &expr.peel_parens().kind {
539        ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
540            expr_contains_direct_block_timestamp(lhs) || expr_contains_direct_block_timestamp(rhs)
541        }
542        ExprKind::Call(callee, args, options) => {
543            expr_contains_direct_block_timestamp(callee)
544                || options.is_some_and(|options| {
545                    options.args.iter().any(|arg| expr_contains_direct_block_timestamp(&arg.value))
546                })
547                || args.exprs().any(expr_contains_direct_block_timestamp)
548        }
549        ExprKind::Unary(_, inner)
550        | ExprKind::Delete(inner)
551        | ExprKind::Member(inner, _)
552        | ExprKind::Payable(inner)
553        | ExprKind::YulMember(inner, _) => expr_contains_direct_block_timestamp(inner),
554        ExprKind::Ternary(cond, then_expr, else_expr) => {
555            expr_contains_direct_block_timestamp(cond)
556                || expr_contains_direct_block_timestamp(then_expr)
557                || expr_contains_direct_block_timestamp(else_expr)
558        }
559        ExprKind::Tuple(exprs) => {
560            exprs.iter().flatten().any(|expr| expr_contains_direct_block_timestamp(expr))
561        }
562        ExprKind::Array(exprs) => exprs.iter().any(expr_contains_direct_block_timestamp),
563        ExprKind::Index(base, index) => {
564            expr_contains_direct_block_timestamp(base)
565                || index.is_some_and(expr_contains_direct_block_timestamp)
566        }
567        ExprKind::Slice(base, start, end) => {
568            expr_contains_direct_block_timestamp(base)
569                || start.is_some_and(expr_contains_direct_block_timestamp)
570                || end.is_some_and(expr_contains_direct_block_timestamp)
571        }
572        ExprKind::Ident(_)
573        | ExprKind::Lit(_)
574        | ExprKind::New(_)
575        | ExprKind::TypeCall(_)
576        | ExprKind::Type(_)
577        | ExprKind::Err(_) => false,
578    }
579}