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}