forge_lint/sol/med/
incorrect_erc20_interface.rs1use super::IncorrectERC20Interface;
2use crate::{
3 linter::{LateLintPass, LintContext},
4 sol::{Severity, SolLint, analysis::is_elementary},
5};
6use solar::sema::{Gcx, hir};
7
8declare_forge_lint!(
9 INCORRECT_ERC20_INTERFACE,
10 Severity::Med,
11 "incorrect-erc20-interface",
12 "incorrect ERC20 function interface"
13);
14
15const ERC20_FUNCTIONS: &[(&str, &[&str], &[&str])] = &[
17 ("transfer", &["address", "uint256"], &["bool"]),
18 ("transferFrom", &["address", "address", "uint256"], &["bool"]),
19 ("approve", &["address", "uint256"], &["bool"]),
20 ("allowance", &["address", "address"], &["uint256"]),
21 ("balanceOf", &["address"], &["uint256"]),
22 ("totalSupply", &[], &["uint256"]),
23];
24
25impl<'gcx> LateLintPass<'gcx> for IncorrectERC20Interface {
26 fn check_contract(
27 &mut self,
28 ctx: &LintContext,
29 gcx: Gcx<'gcx>,
30 contract: &'gcx hir::Contract<'gcx>,
31 ) {
32 let inherits = |names: &[&str]| {
33 contract
34 .linearized_bases
35 .iter()
36 .any(|base| names.contains(&gcx.hir.contract(*base).name.as_str()))
37 };
38 if !inherits(&["ERC20", "IERC20"]) || inherits(&["ERC721", "IERC721"]) {
40 return;
41 }
42 let matches = |vars: &[hir::VariableId], expected: &[&str]| {
43 vars.len() == expected.len()
44 && vars.iter().zip(expected).all(|(&id, &ty)| is_elementary(&gcx.hir, id, ty))
45 };
46 let functions = contract.items.iter().filter_map(|id| id.as_function());
47 for func in functions.map(|id| gcx.hir.function(id)) {
48 let Some(name) = func.name.filter(|_| func.kind.is_function()) else { continue };
49 if ERC20_FUNCTIONS.iter().any(|(n, params, returns)| {
50 *n == name.as_str()
51 && matches(func.parameters, params)
52 && !matches(func.returns, returns)
53 }) {
54 ctx.emit(&INCORRECT_ERC20_INTERFACE, func.span);
55 }
56 }
57 }
58}