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,
8    interface::{Ident, Symbol, sym},
9    sema::{
10        Gcx,
11        hir::{ElementaryType, Expr, ExprKind, Hir, Res, TypeKind},
12        ty::{Ty, TyKind},
13    },
14};
15
16declare_forge_lint!(
17    ENCODE_PACKED_COLLISION,
18    Severity::High,
19    "encode-packed-collision",
20    "`abi.encodePacked()` called with multiple dynamic type arguments; hash collisions possible"
21);
22
23impl<'hir> LateLintPass<'hir> for EncodedPackedCollision {
24    fn check_expr(
25        &mut self,
26        ctx: &LintContext,
27        gcx: Gcx<'hir>,
28        _hir: &'hir Hir<'hir>,
29        expr: &'hir Expr<'hir>,
30    ) {
31        let ExprKind::Call(callee, args, _) = &expr.kind else { return };
32        let ExprKind::Member(base, member) = &callee.peel_parens().kind else { return };
33        if member.name != sym::encodePacked || !is_abi_builtin(base) {
34            return;
35        }
36        // Only non-literal dynamic args count: a top-level string/hex/unicode literal is a
37        // compile-time constant. With at most one non-literal dynamic arg the packed encoding
38        // is still injective, so there is no collision risk.
39        let dynamic_count = args
40            .exprs()
41            .filter(|arg| {
42                !matches!(
43                    arg.peel_parens().kind,
44                    ExprKind::Lit(lit) if matches!(lit.kind, ast::LitKind::Str(..))
45                ) && is_dynamic_arg(gcx, arg)
46            })
47            .count();
48        if dynamic_count >= 2 {
49            ctx.emit(&ENCODE_PACKED_COLLISION, expr.span);
50        }
51    }
52}
53
54fn is_abi_builtin(expr: &Expr<'_>) -> bool {
55    is_builtin_named(expr, sym::abi)
56}
57
58fn is_dynamic_arg<'hir>(gcx: Gcx<'hir>, expr: &'hir Expr<'hir>) -> bool {
59    let expr = expr.peel_parens();
60    match &expr.kind {
61        // String literals (and multi-line/hex string sequences) are always dynamic `string` type.
62        ExprKind::Lit(lit) if matches!(lit.kind, ast::LitKind::Str(..)) => true,
63        // Ternary: dynamic when both branches are dynamic. Handled here so that literal branches
64        // (which have no expr_type) are correctly identified as dynamic.
65        ExprKind::Ternary(_, then, else_) => {
66            is_dynamic_arg(gcx, then) && is_dynamic_arg(gcx, else_)
67        }
68        // Calls: check well-known builtins that return bytes/string, then generic path.
69        ExprKind::Call(callee, _, _) => {
70            is_dynamic_call(callee)
71                || gcx.type_of_expr(expr.id).is_some_and(ty_is_dynamic_bytes_string_or_array)
72        }
73        // Member access: prefer the resolved type so user-defined struct fields named like
74        // builtin properties (e.g. `code`) do not get treated as dynamic bytes.
75        ExprKind::Member(base, member) => {
76            if let Some(ty) = gcx.type_of_expr(expr.id) {
77                ty_is_dynamic_bytes_string_or_array(ty)
78            } else {
79                is_dynamic_builtin_member(base, member)
80            }
81        }
82        _ => gcx.type_of_expr(expr.id).is_some_and(ty_is_dynamic_bytes_string_or_array),
83    }
84}
85
86fn ty_is_dynamic_bytes_string_or_array(ty: Ty<'_>) -> bool {
87    matches!(
88        ty.peel_refs().kind,
89        TyKind::Elementary(ElementaryType::Bytes | ElementaryType::String)
90            | TyKind::DynArray(_)
91            | TyKind::Slice(_)
92    )
93}
94
95/// Returns `true` when `callee(args)` is statically known to return a dynamic type.
96fn is_dynamic_call(callee: &Expr<'_>) -> bool {
97    let callee = callee.peel_parens();
98    match &callee.kind {
99        // `new bytes(n)` / `new string(n)`: dynamic allocation.
100        ExprKind::New(ty) => return is_dynamic_hir_type(&ty.kind),
101        ExprKind::Member(recv, method) => {
102            // abi.encode / abi.encodePacked / abi.encodeWithSelector / … -> bytes
103            if is_abi_builtin(recv) && is_abi_encode_method(method.name) {
104                return true;
105            }
106            // string.concat(…) -> string, bytes.concat(…) -> bytes
107            if method.name == sym::concat
108                && let ExprKind::Type(ty) = &recv.peel_parens().kind
109                && is_dynamic_hir_type(&ty.kind)
110            {
111                return true;
112            }
113        }
114        _ => {}
115    }
116    false
117}
118
119/// Returns `true` when `base.member` is a well-known builtin property of dynamic bytes type.
120fn is_dynamic_builtin_member(base: &Expr<'_>, member: &Ident) -> bool {
121    match member.name {
122        // msg.data -> bytes calldata
123        n if n == sym::data => is_builtin_named(base, sym::msg),
124        // <any>.code, type(C).creationCode, type(C).runtimeCode -> bytes
125        n if n == sym::code || n == sym::creationCode || n == sym::runtimeCode => true,
126        _ => false,
127    }
128}
129
130fn is_abi_encode_method(name: Symbol) -> bool {
131    name == sym::encode
132        || name == sym::encodePacked
133        || name == sym::encodeWithSelector
134        || name == sym::encodeWithSignature
135        || name == sym::encodeCall
136}
137
138fn is_builtin_named(expr: &Expr<'_>, name: Symbol) -> bool {
139    matches!(&expr.peel_parens().kind,
140        ExprKind::Ident(reses) if reses.iter().any(|r| matches!(r, Res::Builtin(b) if b.name() == name))
141    )
142}
143
144const fn is_dynamic_hir_type(kind: &TypeKind<'_>) -> bool {
145    match kind {
146        TypeKind::Elementary(ElementaryType::String | ElementaryType::Bytes) => true,
147        TypeKind::Array(arr) => arr.size.is_none(),
148        _ => false,
149    }
150}