Skip to content

Commit 0449d9e

Browse files
committed
New detector and test case
1 parent 97918c5 commit 0449d9e

File tree

8 files changed

+349
-4
lines changed

8 files changed

+349
-4
lines changed

.vscode/settings.json

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
{
22
"rust-analyzer.rustfmt.extraArgs": ["+nightly"],
33
"rust-analyzer.showUnlinkedFileNotification": false,
4-
"rust-analyzer.linkedProjects": [
5-
"apps/cargo-scout-audit/Cargo.toml",
6-
"detectors/Cargo.toml"
7-
]
4+
"rust-analyzer.linkedProjects": ["detectors/Cargo.toml"]
85
}
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "vec-could-be-mapping"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[lib]
7+
crate-type = ["cdylib"]
8+
9+
[dependencies]
10+
scout-audit-clippy-utils = { workspace = true }
11+
dylint_linting = { workspace = true }
12+
if_chain = { workspace = true }
13+
itertools = {workspace = true}
14+
15+
[dev-dependencies]
16+
dylint_testing = { workspace = true }
17+
18+
[package.metadata.rust-analyzer]
19+
rustc_private = true
+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
#![feature(rustc_private)]
2+
#![recursion_limit = "256"]
3+
#![feature(let_chains)]
4+
extern crate rustc_ast;
5+
extern crate rustc_hir;
6+
extern crate rustc_middle;
7+
extern crate rustc_span;
8+
9+
use std::collections::HashMap;
10+
11+
use itertools::Itertools;
12+
use rustc_hir::intravisit::{walk_expr, FnKind, Visitor};
13+
use rustc_hir::{Body, Expr, ExprKind, FnDecl, GenericArg, GenericArgs, PathSegment, QPath};
14+
use rustc_hir::{Ty, TyKind};
15+
use rustc_lint::{LateContext, LateLintPass};
16+
use rustc_span::def_id::LocalDefId;
17+
use rustc_span::Span;
18+
use scout_audit_clippy_utils::diagnostics::span_lint_and_help;
19+
20+
const LINT_MESSAGE: &str =
21+
"You are iterating over a vector of tuples using `find`. Consider using a mapping instead.";
22+
23+
const ITERABLE_METHODS: [&str; 1] = ["find"];
24+
25+
dylint_linting::impl_late_lint! {
26+
pub VEC_COULD_BE_MAPPING,
27+
Warn,
28+
LINT_MESSAGE,
29+
VecCouldBeMapping::default(),
30+
{
31+
name: "Vec could be Mapping",
32+
long_message: "This vector could be a mapping. Consider changing it, because you are using `find` method in a vector of tuples",
33+
severity: "Enhancement",
34+
help: "https://coinfabrik.github.io/scout/docs/vulnerabilities/vec-could-be-mapping",
35+
vulnerability_class: "Gas Usage",
36+
}
37+
}
38+
39+
#[derive(Default)]
40+
pub struct VecCouldBeMapping {
41+
storage_names: HashMap<String, Span>,
42+
}
43+
44+
impl<'tcx> LateLintPass<'tcx> for VecCouldBeMapping {
45+
fn check_fn(
46+
&mut self,
47+
cx: &LateContext<'tcx>,
48+
_: FnKind<'tcx>,
49+
_: &'tcx FnDecl<'_>,
50+
body: &'tcx Body<'_>,
51+
_: Span,
52+
_: LocalDefId,
53+
) {
54+
let mut vec_mapping_storage = VecMappingStorage {
55+
storage_names: self.storage_names.clone(),
56+
uses_as_hashmap: Vec::new(),
57+
};
58+
59+
walk_expr(&mut vec_mapping_storage, body.value);
60+
61+
vec_mapping_storage
62+
.uses_as_hashmap
63+
.iter()
64+
.for_each(|(span, field)| {
65+
let field_sp = self.storage_names.get(field).copied();
66+
67+
span_lint_and_help(
68+
cx,
69+
VEC_COULD_BE_MAPPING,
70+
*span,
71+
LINT_MESSAGE,
72+
field_sp,
73+
"Change this to a `ink::storage::Mapping<...>`",
74+
);
75+
});
76+
}
77+
78+
fn check_field_def(&mut self, _: &LateContext<'tcx>, fdef: &'tcx rustc_hir::FieldDef<'tcx>) {
79+
println!("{:#?}", fdef);
80+
if is_vec_type_with_tuple_of_2_elems(fdef) {
81+
self.storage_names
82+
.insert(fdef.ident.name.to_string(), fdef.span);
83+
}
84+
}
85+
}
86+
87+
struct VecMappingStorage {
88+
storage_names: HashMap<String, Span>,
89+
uses_as_hashmap: Vec<(Span, String)>,
90+
}
91+
92+
impl VecMappingStorage {
93+
fn call_storage_and_any_or_find(&self, methods: &[String]) -> bool {
94+
methods.first() == Some(&"self".to_string())
95+
&& methods
96+
.iter()
97+
.any(|method| ITERABLE_METHODS.contains(&method.as_str()))
98+
&& methods
99+
.iter()
100+
.any(|method| self.storage_names.keys().contains(method))
101+
}
102+
}
103+
104+
impl<'tcx> Visitor<'tcx> for VecMappingStorage {
105+
fn visit_expr(&mut self, expr: &'tcx Expr<'_>) {
106+
let (methods, field) = get_method_path_names(expr);
107+
108+
if !methods.is_empty() && self.call_storage_and_any_or_find(&methods) {
109+
self.uses_as_hashmap.push((expr.span, field));
110+
} else {
111+
walk_expr(self, expr);
112+
}
113+
}
114+
}
115+
116+
fn get_method_path_names(expr: &Expr<'_>) -> (Vec<String>, String) {
117+
let mut full_method_ident = Vec::new();
118+
let mut expr = expr;
119+
let mut fields = Vec::new();
120+
121+
'names: loop {
122+
match expr.kind {
123+
ExprKind::MethodCall(PathSegment { ident, .. }, rec, ..) => {
124+
full_method_ident.push(ident.name.to_string());
125+
expr = rec;
126+
}
127+
ExprKind::Field(rec, ident) => {
128+
full_method_ident.push(ident.name.to_string());
129+
fields.push(ident.name.to_string());
130+
expr = rec;
131+
}
132+
ExprKind::Path(QPath::Resolved(_, b)) => {
133+
if !full_method_ident.is_empty() {
134+
for seg in b.segments.iter().rev() {
135+
full_method_ident.push(seg.ident.name.to_string());
136+
}
137+
}
138+
break 'names;
139+
}
140+
_ => {
141+
break 'names;
142+
}
143+
}
144+
}
145+
full_method_ident.reverse();
146+
147+
let field = if full_method_ident.len() > 1
148+
&& full_method_ident[0] == "self"
149+
&& fields.contains(&full_method_ident[1])
150+
{
151+
full_method_ident[1].clone()
152+
} else {
153+
"".to_string()
154+
};
155+
156+
(full_method_ident, field)
157+
}
158+
159+
fn is_vec_type_with_tuple_of_2_elems(fdef: &'_ rustc_hir::FieldDef<'_>) -> bool {
160+
if let TyKind::Path(QPath::Resolved(Some(Ty { kind, .. }), ..)) = fdef.ty.kind
161+
&& let TyKind::Path(QPath::Resolved(_, b)) = *kind
162+
&& let Some(last) = b.segments.iter().last()
163+
&& last.ident.name.to_string() == "Vec"
164+
&& let Some(GenericArgs { args, .. }) = last.args
165+
{
166+
for arg in args.iter() {
167+
if let GenericArg::Type(ins) = *arg
168+
&& let TyKind::Tup(insides) = ins.kind
169+
&& insides.len() == 2
170+
{
171+
return true;
172+
}
173+
}
174+
}
175+
false
176+
}
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[workspace]
2+
exclude = [".cargo", "target"]
3+
members = ["vec-could-be-mapping-*/*"]
4+
resolver = "2"
5+
6+
[workspace.dependencies]
7+
ink = { version = "5.0.0", default-features = false }
8+
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
9+
scale-info = { version = "2.6", default-features = false, features = ["derive"] }
10+
ink_e2e = { version = "=5.0.0" }
11+
12+
[profile.release]
13+
codegen-units = 1
14+
debug = 0
15+
debug-assertions = false
16+
lto = true
17+
opt-level = "z"
18+
overflow-checks = false
19+
panic = "abort"
20+
strip = "symbols"
21+
22+
[profile.release-with-logs]
23+
debug-assertions = true
24+
inherits = "release"
25+
26+
[workspace.metadata.dylint]
27+
libraries = [
28+
{ path = "/home/flerena/coinfabrik/scout/detectors/vec-could-be-mapping" },
29+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[package]
2+
name = "vec-could-be-mapping-remediated"
3+
version = "0.1.0"
4+
edition = "2021"
5+
authors = ["[your_name] <[your_email]>"]
6+
7+
[lib]
8+
path = "src/lib.rs"
9+
10+
[features]
11+
default = ["std"]
12+
std = [
13+
"ink/std",
14+
"scale/std",
15+
"scale-info/std",
16+
]
17+
ink-as-dependency = []
18+
e2e-tests = []
19+
20+
[dependencies]
21+
ink = { workspace = true }
22+
scale = { workspace = true }
23+
scale-info = { workspace = true }
24+
25+
26+
[dev-dependencies]
27+
ink_e2e = { workspace = true }
28+
29+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#![cfg_attr(not(feature = "std"), no_std, no_main)]
2+
3+
#[ink::contract]
4+
mod vec_could_be_mapping {
5+
6+
use ink::storage::Mapping;
7+
8+
#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
9+
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
10+
pub enum Error {
11+
NotFound,
12+
}
13+
#[ink(storage)]
14+
pub struct VecCouldBeMapping {
15+
balances: Mapping<AccountId, Balance>,
16+
}
17+
18+
impl VecCouldBeMapping {
19+
/// Creates a new instance of the contract.
20+
#[ink(constructor)]
21+
pub fn new() -> Self {
22+
Self {
23+
balances: Mapping::new(),
24+
}
25+
}
26+
/// Returns the percentage difference between two values.
27+
#[ink(message)]
28+
pub fn get_percentage_difference(&mut self, acc: AccountId) -> Result<Balance, Error> {
29+
self.balances.get(&acc).ok_or(Error::NotFound)
30+
}
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[package]
2+
name = "vec-could-be-mapping-vulnerable"
3+
version = "0.1.0"
4+
edition = "2021"
5+
authors = ["[your_name] <[your_email]>"]
6+
7+
[lib]
8+
path = "src/lib.rs"
9+
10+
[features]
11+
default = ["std"]
12+
std = [
13+
"ink/std",
14+
"scale/std",
15+
"scale-info/std",
16+
]
17+
ink-as-dependency = []
18+
e2e-tests = []
19+
20+
[dependencies]
21+
ink = { workspace = true }
22+
scale = { workspace = true }
23+
scale-info = { workspace = true }
24+
25+
26+
[dev-dependencies]
27+
ink_e2e = { workspace = true }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#![cfg_attr(not(feature = "std"), no_std, no_main)]
2+
3+
#[ink::contract]
4+
mod vec_could_be_mapping {
5+
6+
use ink::prelude::vec::Vec;
7+
8+
#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
9+
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
10+
pub enum Error {
11+
NotFound,
12+
}
13+
#[ink(storage)]
14+
pub struct VecCouldBeMapping {
15+
balances: Vec<(AccountId, Balance)>,
16+
}
17+
18+
impl VecCouldBeMapping {
19+
/// Creates a new instance of the contract.
20+
#[ink(constructor)]
21+
pub fn new() -> Self {
22+
Self {
23+
balances: Vec::new(),
24+
}
25+
}
26+
/// Returns the percentage difference between two values.
27+
#[ink(message)]
28+
pub fn get_percentage_difference(&mut self, acc: AccountId) -> Result<Balance, Error> {
29+
self.balances
30+
.iter()
31+
.find(|(a, _)| *a == acc)
32+
.map(|(_, b)| *b)
33+
.ok_or(Error::NotFound)
34+
}
35+
}
36+
}

0 commit comments

Comments
 (0)