Skip to main content

forge_lint/sol/med/
incorrect_erc20_interface.rs

1use 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
15/// ERC20 functions as `(name, parameter types, return types)`.
16const 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        // ERC721 tokens offer functions similar to ERC20 that are not compatible with it.
39        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}