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::interface::is_elementary},
5};
6use solar::sema::hir;
7
8declare_forge_lint!(
9    INCORRECT_ERC20_INTERFACE,
10    Severity::Med,
11    "incorrect-erc20-interface",
12    "incorrect ERC20 function interface"
13);
14
15impl<'hir> LateLintPass<'hir> for IncorrectERC20Interface {
16    fn check_contract(
17        &mut self,
18        ctx: &LintContext,
19        hir: &'hir hir::Hir<'hir>,
20        contract: &'hir hir::Contract<'hir>,
21    ) {
22        // Check if the contract is a possible ERC20 by name or inheritance.
23        let is_erc20 = contract.linearized_bases.iter().any(|base_id| {
24            let name = hir.contract(*base_id).name.as_str();
25            name == "ERC20" || name == "IERC20"
26        });
27
28        if !is_erc20 {
29            return;
30        }
31
32        // If this contract implements a function from ERC721, we can assume it is an ERC721 token.
33        // These tokens offer functions which are similar to ERC20, but are not compatible.
34        let is_erc721 = contract.linearized_bases.iter().any(|base_id| {
35            let name = hir.contract(*base_id).name.as_str();
36            name == "ERC721" || name == "IERC721"
37        });
38
39        if is_erc721 {
40            return;
41        }
42
43        // Check each function in the contract for incorrect ERC20 signatures.
44        for item_id in contract.items {
45            let Some(fid) = item_id.as_function() else { continue };
46            let func = hir.function(fid);
47
48            if !func.kind.is_function() {
49                continue;
50            }
51
52            let Some(name) = func.name else { continue };
53
54            if has_incorrect_erc20_signature(hir, name.as_str(), func.parameters, func.returns) {
55                ctx.emit(&INCORRECT_ERC20_INTERFACE, func.span);
56            }
57        }
58    }
59}
60
61/// Checks if a function signature does not match the expected ERC20 specification.
62///
63/// Returns `true` if the function name and parameter types match an ERC20 function but the return
64/// types are incorrect.
65fn has_incorrect_erc20_signature(
66    hir: &hir::Hir<'_>,
67    name: &str,
68    parameters: &[hir::VariableId],
69    returns: &[hir::VariableId],
70) -> bool {
71    let sig_match = |vars: &[hir::VariableId], expected: &[&str]| -> bool {
72        vars.len() == expected.len()
73            && vars.iter().zip(expected).all(|(&id, &ty)| is_elementary(hir, id, ty))
74    };
75    let params_match = sig_match;
76    let returns_match = sig_match;
77
78    match name {
79        // function transfer(address,uint256) external returns (bool)
80        "transfer" if params_match(parameters, &["address", "uint256"]) => {
81            !returns_match(returns, &["bool"])
82        }
83        // function transferFrom(address,address,uint256) external returns (bool)
84        "transferFrom" if params_match(parameters, &["address", "address", "uint256"]) => {
85            !returns_match(returns, &["bool"])
86        }
87        // function approve(address,uint256) external returns (bool)
88        "approve" if params_match(parameters, &["address", "uint256"]) => {
89            !returns_match(returns, &["bool"])
90        }
91        // function allowance(address,address) external view returns (uint256)
92        "allowance" if params_match(parameters, &["address", "address"]) => {
93            !returns_match(returns, &["uint256"])
94        }
95        // function balanceOf(address) external view returns (uint256)
96        "balanceOf" if params_match(parameters, &["address"]) => {
97            !returns_match(returns, &["uint256"])
98        }
99        // function totalSupply() external view returns (uint256)
100        "totalSupply" if params_match(parameters, &[]) => !returns_match(returns, &["uint256"]),
101        _ => false,
102    }
103}