Skip to main content

forge_lint/sol/med/
uninitialized_state_variables.rs

1use super::UninitializedStateVariables;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{
5        Severity, SolLint,
6        analysis::{referenced_item, tuple_elems},
7    },
8};
9use solar::{
10    ast::ContractKind,
11    interface::data_structures::Never,
12    sema::{
13        Gcx, Hir,
14        hir::{
15            Contract, ContractId, DataLocation, Expr, ExprKind, Function, Stmt, StmtKind, TypeKind,
16            VariableId, Visit,
17        },
18    },
19};
20use std::{
21    collections::{HashMap, HashSet},
22    ops::ControlFlow,
23};
24
25declare_forge_lint!(
26    UNINITIALIZED_STATE_VARIABLES,
27    Severity::Med,
28    "uninitialized-state",
29    "state variable is read but never written"
30);
31
32impl<'gcx> LateLintPass<'gcx> for UninitializedStateVariables {
33    fn check_nested_contract(
34        &mut self,
35        ctx: &LintContext,
36        gcx: Gcx<'gcx>,
37        contract_id: ContractId,
38    ) {
39        let contract = gcx.hir.contract(contract_id);
40        // Abstract contracts and interfaces are not deployed; a failed C3 linearization leaves
41        // `linearized_bases` incomplete, so skip rather than produce unsound results.
42        if matches!(contract.kind, ContractKind::Interface | ContractKind::AbstractContract)
43            || contract.linearization_failed()
44        {
45            return;
46        }
47
48        // Every read and write in the whole inheritance chain (`linearized_bases[0]` is the
49        // contract itself) determines whether a variable is ever written.
50        let bases = contract.linearized_bases;
51        let mut collector = Collector {
52            hir: &gcx.hir,
53            gcx,
54            read: HashSet::new(),
55            written: HashSet::new(),
56            aliases: HashMap::new(),
57        };
58        // Inline assembly can write storage directly; bail out conservatively.
59        if bases.iter().any(|&cid| collector.visit_contract_items(gcx.hir.contract(cid)).is_break())
60        {
61            return;
62        }
63
64        for var_id in bases.iter().flat_map(|&cid| gcx.hir.contract(cid).variables()) {
65            let var = gcx.hir.variable(var_id);
66            if !var.is_constant()
67                && !var.is_immutable()
68                && !matches!(var.ty.kind, TypeKind::Mapping(_))
69                && var.initializer.is_none()
70                && collector.read.contains(&var_id)
71                && !collector.written.contains(&var_id)
72            {
73                ctx.emit(&UNINITIALIZED_STATE_VARIABLES, var.span);
74            }
75        }
76    }
77}
78
79struct Collector<'gcx> {
80    hir: &'gcx Hir<'gcx>,
81    gcx: Gcx<'gcx>,
82    read: HashSet<VariableId>,
83    written: HashSet<VariableId>,
84    /// State variables each local `storage` pointer of the current function may reference.
85    aliases: Aliases,
86}
87
88/// Maps local `storage` pointers to the state variables they may reference.
89type Aliases = HashMap<VariableId, HashSet<VariableId>>;
90
91impl<'gcx> Collector<'gcx> {
92    fn visit_contract_items(&mut self, contract: &'gcx Contract<'gcx>) -> ControlFlow<()> {
93        contract.all_functions().try_for_each(|fid| self.visit_nested_function(fid))?;
94        contract.variables().try_for_each(|vid| self.visit_nested_var(vid))?;
95        contract.bases_args.iter().try_for_each(|m| self.visit_modifier(m))
96    }
97
98    /// Marks the variable at the root of an lvalue (through index/slice/member access and tuple
99    /// destructuring) as written; a write through a `storage` pointer writes its targets.
100    fn mark_written(&mut self, expr: &Expr<'_>) {
101        match &expr.peel_parens().kind {
102            ExprKind::Ident(_) => {
103                if let Some(id) = self.gcx.resolved_variable(expr) {
104                    self.written.insert(id);
105                    if let Some(targets) = self.aliases.get(&id) {
106                        self.written.extend(targets);
107                    }
108                }
109            }
110            ExprKind::Tuple(exprs) => exprs.iter().flatten().for_each(|e| self.mark_written(e)),
111            ExprKind::Index(base, _) | ExprKind::Slice(base, ..) | ExprKind::Member(base, _) => {
112                self.mark_written(base)
113            }
114            _ => {}
115        }
116    }
117
118    /// A call taking a `storage` parameter may write through its corresponding argument.
119    fn mark_storage_args(&mut self, call: &'gcx Expr<'gcx>) {
120        if let ExprKind::Call(callee, ..) = &call.kind
121            && let Some(parameters) =
122                self.gcx.type_of_expr(callee.id).and_then(|ty| ty.parameters())
123        {
124            for (index, ty) in parameters.iter().enumerate() {
125                if ty.is_ref_at(DataLocation::Storage)
126                    && let Some(arg) = self.gcx.call_arg(call, index)
127                {
128                    self.mark_written(arg);
129                }
130            }
131        }
132    }
133}
134
135impl<'gcx> Visit<'gcx> for Collector<'gcx> {
136    type BreakValue = ();
137
138    fn hir(&self) -> &'gcx Hir<'gcx> {
139        self.hir
140    }
141
142    fn visit_function(&mut self, func: &'gcx Function<'gcx>) -> ControlFlow<()> {
143        self.aliases = storage_aliases(self.gcx, func);
144        func.modifiers.iter().try_for_each(|m| self.visit_modifier(m))?;
145        func.body.iter().flat_map(|body| body.stmts).try_for_each(|stmt| self.visit_stmt(stmt))
146    }
147
148    fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<()> {
149        match stmt.kind {
150            StmtKind::AssemblyBlock(_) | StmtKind::Switch(_) | StmtKind::Err(_) => {
151                ControlFlow::Break(())
152            }
153            _ => self.walk_stmt(stmt),
154        }
155    }
156
157    fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<()> {
158        match &expr.kind {
159            ExprKind::Ident(_) => self.read.extend(self.gcx.resolved_variable(expr)),
160            // Reassigning a bare storage pointer repoints it rather than writing its target.
161            ExprKind::Assign(lhs, ..) if !is_storage_pointer(self.gcx, lhs) => {
162                self.mark_written(lhs)
163            }
164            ExprKind::Delete(lhs) => self.mark_written(lhs),
165            ExprKind::Unary(op, lhs) if op.kind.has_side_effects() => self.mark_written(lhs),
166            ExprKind::Call(callee, ..) => {
167                // The receiver of a member call covers `push`/`pop` and `using for` library
168                // dispatch with a `T storage self` parameter.
169                if let ExprKind::Member(base, _) = &callee.kind {
170                    self.mark_written(base);
171                }
172                self.mark_storage_args(expr);
173            }
174            _ => {}
175        }
176        self.walk_expr(expr)
177    }
178}
179
180/// Collects, flow-insensitively, the state variables each local `storage` pointer declared in
181/// `func` may reference: every assignment contributes to the pointer's target set, and pointers
182/// assigned from other pointers are resolved transitively.
183fn storage_aliases<'gcx>(gcx: Gcx<'gcx>, func: &'gcx Function<'gcx>) -> Aliases {
184    let hir = &gcx.hir;
185    struct Edges<'gcx> {
186        gcx: Gcx<'gcx>,
187        hir: &'gcx Hir<'gcx>,
188        edges: HashMap<VariableId, HashSet<VariableId>>,
189    }
190
191    impl Edges<'_> {
192        /// Records `lhs = rhs`, matching tuple destructuring element-wise.
193        fn record(&mut self, lhs: &Expr<'_>, rhs: &Expr<'_>) {
194            match (tuple_elems(lhs), tuple_elems(rhs)) {
195                (Some(targets), Some(values)) => {
196                    for (target, value) in targets.iter().zip(values) {
197                        if let (Some(target), Some(value)) = (target, value) {
198                            self.record(target, value);
199                        }
200                    }
201                }
202                _ => {
203                    if let Some(var) =
204                        referenced_item(self.gcx, lhs).and_then(|id| id.as_variable())
205                    {
206                        self.record_var(var, rhs);
207                    }
208                }
209            }
210        }
211
212        fn record_var(&mut self, var: VariableId, rhs: &Expr<'_>) {
213            if is_local_storage_var(self.hir, var) {
214                root_vars(self.gcx, rhs, self.edges.entry(var).or_default());
215            }
216        }
217    }
218
219    impl<'gcx> Visit<'gcx> for Edges<'gcx> {
220        type BreakValue = Never;
221
222        fn hir(&self) -> &'gcx Hir<'gcx> {
223            self.hir
224        }
225
226        fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<Never> {
227            match &stmt.kind {
228                StmtKind::DeclSingle(var) => {
229                    if let Some(init) = self.hir.variable(*var).initializer {
230                        self.record_var(*var, init);
231                    }
232                }
233                StmtKind::DeclMulti(vars, init) => {
234                    for (var, value) in vars.iter().zip(tuple_elems(init).unwrap_or_default()) {
235                        if let (Some(var), Some(value)) = (var, value) {
236                            self.record_var(*var, value);
237                        }
238                    }
239                }
240                _ => {}
241            }
242            self.walk_stmt(stmt)
243        }
244
245        fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Never> {
246            if let ExprKind::Assign(lhs, None, rhs) = &expr.kind {
247                self.record(lhs, rhs);
248            }
249            self.walk_expr(expr)
250        }
251    }
252
253    let mut edges = Edges { gcx, hir, edges: HashMap::new() };
254    let _ = edges.visit_function(func);
255    let edges = edges.edges;
256    let mut aliases = Aliases::new();
257    for &pointer in edges.keys() {
258        let (mut targets, mut seen, mut stack) = (HashSet::new(), HashSet::new(), vec![pointer]);
259        while let Some(var) = stack.pop() {
260            if !seen.insert(var) {
261                continue;
262            }
263            if hir.variable(var).kind.is_state() {
264                targets.insert(var);
265            } else if let Some(roots) = edges.get(&var) {
266                stack.extend(roots);
267            }
268        }
269        if !targets.is_empty() {
270            aliases.insert(pointer, targets);
271        }
272    }
273    aliases
274}
275
276/// The variables an expression is rooted in, through indexing, member access and ternaries.
277fn root_vars(gcx: Gcx<'_>, expr: &Expr<'_>, roots: &mut HashSet<VariableId>) {
278    match &expr.peel_parens().kind {
279        ExprKind::Ident(_) => roots.extend(gcx.resolved_variable(expr)),
280        ExprKind::Index(base, _) | ExprKind::Slice(base, ..) | ExprKind::Member(base, _) => {
281            root_vars(gcx, base, roots)
282        }
283        ExprKind::Ternary(_, then, otherwise) => {
284            root_vars(gcx, then, roots);
285            root_vars(gcx, otherwise, roots);
286        }
287        _ => {}
288    }
289}
290
291/// A bare local `storage` pointer.
292fn is_storage_pointer(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
293    referenced_item(gcx, expr)
294        .and_then(|id| id.as_variable())
295        .is_some_and(|var| is_local_storage_var(&gcx.hir, var))
296}
297
298fn is_local_storage_var(hir: &Hir<'_>, var: VariableId) -> bool {
299    let var = hir.variable(var);
300    !var.kind.is_state() && var.data_location == Some(DataLocation::Storage)
301}