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 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
45struct Checker<'a, 's, 'c, 'gcx> {
48 ctx: &'a LintContext<'s, 'c>,
49 gcx: Gcx<'gcx>,
50 helpers: Vec<FunctionId>,
51 aliases: HashSet<VariableId>,
53}
54
55impl<'gcx> Checker<'_, '_, '_, 'gcx> {
56 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 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 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 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 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 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 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 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 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
280fn is_block_timestamp(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
282 gcx.resolved_builtin(expr) == Some(Builtin::BlockTimestamp)
283}
284
285fn 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}