Skip to main content

forge_lint/sol/low/
block_timestamp.rs

1use super::BlockTimestamp;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{
5        Severity, SolLint,
6        analysis::{any_subexpr, branch_always_exits, loop_stmts, tuple_elems},
7    },
8};
9use solar::{
10    ast::Visibility,
11    sema::{
12        Gcx, Hir,
13        builtins::Builtin,
14        hir::{BinOpKind, Expr, ExprKind, Function, FunctionId, Stmt, StmtKind, VariableId, Visit},
15    },
16};
17use std::{collections::HashSet, convert::Infallible, ops::ControlFlow};
18
19declare_forge_lint!(
20    BLOCK_TIMESTAMP,
21    Severity::Low,
22    "block-timestamp",
23    "usage of `block.timestamp` in a comparison may be manipulated by validators"
24);
25
26impl<'gcx> LateLintPass<'gcx> for BlockTimestamp {
27    fn check_function(&mut self, ctx: &LintContext, gcx: Gcx<'gcx>, func: &'gcx Function<'gcx>) {
28        let Some(body) = func.body else { return };
29        // The contract's own internal helpers that return `block.timestamp` directly.
30        let helpers = func
31            .contract
32            .map(|c| gcx.hir.contract(c).functions())
33            .into_iter()
34            .flatten()
35            .filter(|&id| {
36                let helper = gcx.hir.function(id);
37                matches!(helper.visibility, Visibility::Internal | Visibility::Private)
38                    && helper.body.is_some_and(|body| returns_timestamp(gcx, body.stmts))
39            })
40            .collect();
41        Checker { ctx, gcx, helpers, aliases: HashSet::new() }.block(body.stmts);
42    }
43}
44
45/// Flow-sensitive walk reporting comparisons involving `block.timestamp`, a helper returning it,
46/// or a local holding a value derived from either.
47struct Checker<'a, 's, 'c, 'gcx> {
48    ctx: &'a LintContext<'s, 'c>,
49    gcx: Gcx<'gcx>,
50    helpers: Vec<FunctionId>,
51    /// Locals currently holding a timestamp-derived value.
52    aliases: HashSet<VariableId>,
53}
54
55impl<'gcx> Checker<'_, '_, '_, 'gcx> {
56    /// Walks statements in order, stopping at the first one control cannot continue past.
57    fn block(&mut self, stmts: impl IntoIterator<Item = &'gcx Stmt<'gcx>>) {
58        for stmt in stmts {
59            let _ = self.visit_stmt(stmt);
60            if branch_always_exits(self.gcx, stmt) {
61                break;
62            }
63        }
64    }
65
66    /// Walks an alternative arm on a copy of the current aliases; when control can continue past
67    /// `stmts`, the aliases the arm leaves are added to `merged`.
68    fn arm(
69        &mut self,
70        merged: &mut HashSet<VariableId>,
71        stmts: impl IntoIterator<Item = &'gcx Stmt<'gcx>>,
72        walk: impl FnOnce(&mut Self),
73    ) {
74        let saved = self.aliases.clone();
75        walk(self);
76        let aliases = std::mem::replace(&mut self.aliases, saved);
77        if !stmts.into_iter().any(|expr| branch_always_exits(self.gcx, expr)) {
78            merged.extend(aliases);
79        }
80    }
81
82    /// Binds the variables of an lvalue (tuple targets included) to whether they now hold a
83    /// timestamp-derived value.
84    fn bind(&mut self, lhs: &Expr<'_>, is_source: bool) {
85        match &lhs.peel_parens().kind {
86            ExprKind::Tuple(elems) => elems.iter().flatten().for_each(|e| self.bind(e, is_source)),
87            ExprKind::Ident(_) => {
88                if let Some(var) = self.gcx.resolved_variable(lhs) {
89                    self.set_alias(var, is_source);
90                }
91            }
92            _ => {}
93        }
94    }
95
96    fn set_alias(&mut self, var: VariableId, is_source: bool) {
97        if self.gcx.hir.variable(var).is_local_or_return() {
98            if is_source {
99                self.aliases.insert(var);
100            } else {
101                self.aliases.remove(&var);
102            }
103        }
104    }
105
106    /// Whether each of `n` targets receives a timestamp-derived value from `rhs`: element-wise
107    /// for a matching tuple, otherwise `rhs` as a whole for every target.
108    fn source_values(&self, rhs: &Expr<'_>, n: usize) -> Vec<bool> {
109        match tuple_elems(rhs) {
110            Some(elems) if elems.len() == n => {
111                elems.iter().map(|e| e.is_some_and(|e| self.is_source_value(e))).collect()
112            }
113            _ => vec![self.is_source_value(rhs); n],
114        }
115    }
116
117    /// True if the value of `expr` derives from a timestamp source: the source itself, or one
118    /// flowing through arithmetic, unary operators, a ternary arm or a parenthesized tuple.
119    fn is_source_value(&self, expr: &Expr<'_>) -> bool {
120        self.is_source(expr)
121            || match &expr.peel_parens().kind {
122                ExprKind::Binary(lhs, op, rhs) if !is_cmp(op.kind) => {
123                    self.is_source_value(lhs) || self.is_source_value(rhs)
124                }
125                ExprKind::Unary(_, inner)
126                | ExprKind::Payable(inner)
127                | ExprKind::YulMember(inner, _) => self.is_source_value(inner),
128                ExprKind::Ternary(_, then_expr, else_expr) => {
129                    self.is_source_value(then_expr) || self.is_source_value(else_expr)
130                }
131                ExprKind::Tuple([Some(inner)]) => self.is_source_value(inner),
132                _ => false,
133            }
134    }
135
136    /// `block.timestamp`, a call to a helper returning it, or an alias of either.
137    fn is_source(&self, expr: &Expr<'_>) -> bool {
138        is_block_timestamp(self.gcx, expr)
139            || self.gcx.resolved_variable(expr).is_some_and(|var| self.aliases.contains(&var))
140            || matches!(&expr.peel_parens().kind, ExprKind::Call(callee, ..)
141                if self.gcx.resolved_function(callee).is_some_and(|id| self.helpers.contains(&id)))
142    }
143}
144
145impl<'gcx> Visit<'gcx> for Checker<'_, '_, '_, 'gcx> {
146    type BreakValue = Infallible;
147
148    fn hir(&self) -> &'gcx Hir<'gcx> {
149        &self.gcx.hir
150    }
151
152    fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<Infallible> {
153        match &stmt.kind {
154            StmtKind::DeclSingle(var) => {
155                if let Some(init) = self.gcx.hir.variable(*var).initializer {
156                    self.visit_expr(init)?;
157                    let is_source = self.is_source_value(init);
158                    self.set_alias(*var, is_source);
159                }
160            }
161            StmtKind::DeclMulti(vars, expr) => {
162                self.visit_expr(expr)?;
163                for (var, is_source) in vars.iter().zip(self.source_values(expr, vars.len())) {
164                    if let Some(var) = var {
165                        self.set_alias(*var, is_source);
166                    }
167                }
168            }
169            StmtKind::Block(block)
170            | StmtKind::UncheckedBlock(block)
171            | StmtKind::AssemblyBlock(block) => {
172                self.block(block.stmts);
173            }
174            // Only the arms control can continue past contribute their aliases.
175            StmtKind::If(cond, then_stmt, else_stmt) => {
176                self.visit_expr(cond)?;
177                let mut merged = HashSet::new();
178                self.arm(&mut merged, std::slice::from_ref(*then_stmt), |s| {
179                    let _ = s.visit_stmt(then_stmt);
180                });
181                match else_stmt {
182                    Some(else_stmt) => {
183                        self.arm(&mut merged, std::slice::from_ref(*else_stmt), |s| {
184                            let _ = s.visit_stmt(else_stmt);
185                        })
186                    }
187                    None => merged.extend(self.aliases.iter().copied()),
188                }
189                self.aliases = merged;
190            }
191            StmtKind::Loop(block, source) => {
192                let mut merged = self.aliases.clone();
193                let stmts = loop_stmts(*block, *source);
194                self.arm(&mut merged, stmts.clone(), |s| s.block(stmts));
195                self.aliases = merged;
196            }
197            StmtKind::Try(try_stmt) => {
198                self.visit_expr(&try_stmt.expr)?;
199                let mut merged = self.aliases.clone();
200                for clause in try_stmt.clauses {
201                    self.arm(&mut merged, clause.block.stmts, |s| s.block(clause.block.stmts));
202                }
203                self.aliases = merged;
204            }
205            StmtKind::Switch(switch) => {
206                self.visit_expr(switch.selector)?;
207                let mut merged = self.aliases.clone();
208                for case in switch.cases {
209                    self.arm(&mut merged, case.body.stmts, |s| s.block(case.body.stmts));
210                }
211                self.aliases = merged;
212            }
213            _ => self.walk_stmt(stmt)?,
214        }
215        ControlFlow::Continue(())
216    }
217
218    fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Infallible> {
219        match &expr.peel_parens().kind {
220            // The right-hand side is evaluated first, against the aliases before the write.
221            ExprKind::Assign(lhs, op, rhs) => {
222                self.visit_expr(rhs)?;
223                if op.is_some() {
224                    self.visit_expr(lhs)?;
225                    let is_source = self.is_source_value(rhs) || self.is_source_value(lhs);
226                    self.bind(lhs, is_source);
227                } else if let Some(elems) = tuple_elems(lhs) {
228                    for (elem, is_source) in elems.iter().zip(self.source_values(rhs, elems.len()))
229                    {
230                        if let Some(elem) = elem {
231                            self.bind(elem, is_source);
232                        }
233                    }
234                } else {
235                    let is_source = self.is_source_value(rhs);
236                    self.bind(lhs, is_source);
237                }
238            }
239            ExprKind::Binary(lhs, op, rhs) => {
240                if is_cmp(op.kind) && (self.contains_source(lhs) || self.contains_source(rhs)) {
241                    self.ctx.emit(&BLOCK_TIMESTAMP, expr.span);
242                }
243                self.visit_expr(lhs)?;
244                self.visit_expr(rhs)?;
245            }
246            ExprKind::Ternary(cond, then_expr, else_expr) => {
247                self.visit_expr(cond)?;
248                let mut merged = HashSet::new();
249                self.arm(&mut merged, &[], |s| {
250                    let _ = s.visit_expr(then_expr);
251                });
252                self.visit_expr(else_expr)?;
253                self.aliases.extend(merged);
254            }
255            _ => self.walk_expr(expr)?,
256        }
257        ControlFlow::Continue(())
258    }
259}
260
261impl<'gcx> Checker<'_, '_, '_, 'gcx> {
262    /// True if `expr` or any subexpression is a timestamp source.
263    fn contains_source(&self, expr: &'gcx Expr<'gcx>) -> bool {
264        any_subexpr(expr, |e| self.is_source(e))
265    }
266}
267
268const fn is_cmp(kind: BinOpKind) -> bool {
269    matches!(
270        kind,
271        BinOpKind::Lt
272            | BinOpKind::Le
273            | BinOpKind::Gt
274            | BinOpKind::Ge
275            | BinOpKind::Eq
276            | BinOpKind::Ne
277    )
278}
279
280/// `block.timestamp`, or the Yul `timestamp()` builtin.
281fn is_block_timestamp(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
282    gcx.resolved_builtin(expr) == Some(Builtin::BlockTimestamp)
283}
284
285/// True if a `return` reachable through plain blocks and `if` arms mentions `block.timestamp`.
286fn returns_timestamp(gcx: Gcx<'_>, stmts: &[Stmt<'_>]) -> bool {
287    stmts.iter().any(|stmt| match &stmt.kind {
288        StmtKind::Return(Some(expr)) => any_subexpr(expr, |expr| is_block_timestamp(gcx, expr)),
289        StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
290            returns_timestamp(gcx, block.stmts)
291        }
292        StmtKind::If(_, then_stmt, else_stmt) => {
293            returns_timestamp(gcx, std::slice::from_ref(*then_stmt))
294                || else_stmt.is_some_and(|e| returns_timestamp(gcx, std::slice::from_ref(e)))
295        }
296        _ => false,
297    })
298}