Skip to main content

forge_lint/sol/high/
encode_packed_collision.rs

1use super::EncodedPackedCollision;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::{
7    ast::LitKind,
8    sema::{
9        Gcx,
10        builtins::Builtin,
11        hir::{Expr, ExprKind},
12    },
13};
14
15declare_forge_lint!(
16    ENCODE_PACKED_COLLISION,
17    Severity::High,
18    "encode-packed-collision",
19    "`abi.encodePacked()` called with multiple dynamic type arguments; hash collisions possible"
20);
21
22impl<'gcx> LateLintPass<'gcx> for EncodedPackedCollision {
23    fn check_expr(&mut self, ctx: &LintContext, gcx: Gcx<'gcx>, expr: &'gcx Expr<'gcx>) {
24        let ExprKind::Call(callee, args, _) = &expr.kind else { return };
25        if gcx.resolved_builtin(callee) != Some(Builtin::AbiEncodePacked) {
26            return;
27        }
28        // Only non-literal dynamic args count: a top-level string/hex/unicode literal is a
29        // compile-time constant. With at most one non-literal dynamic arg the packed encoding
30        // is still injective, so there is no collision risk.
31        let dynamic_count =
32            args.exprs().filter(|arg| !is_str_lit(arg) && is_dynamic_arg(gcx, arg)).count();
33        if dynamic_count >= 2 {
34            ctx.emit(&ENCODE_PACKED_COLLISION, expr.span);
35        }
36    }
37}
38
39fn is_str_lit(expr: &Expr<'_>) -> bool {
40    matches!(expr.peel_parens().kind, ExprKind::Lit(lit) if matches!(lit.kind, LitKind::Str(..)))
41}
42
43fn is_dynamic_arg<'gcx>(gcx: Gcx<'gcx>, expr: &Expr<'gcx>) -> bool {
44    gcx.type_of_expr(expr.peel_parens().id).is_some_and(|ty| ty.peel_refs().is_dynamically_sized())
45}