Skip to main content

forge_lint/sol/med/
incorrect_erc721_interface.rs

1use super::IncorrectERC721Interface;
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_ERC721_INTERFACE,
10    Severity::Med,
11    "incorrect-erc721-interface",
12    "incorrect ERC721 function interface"
13);
14
15impl<'hir> LateLintPass<'hir> for IncorrectERC721Interface {
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 ERC721 by name or inheritance.
23        let is_erc721 = contract.linearized_bases.iter().any(|base_id| {
24            let name = hir.contract(*base_id).name.as_str();
25            name == "ERC721" || name == "IERC721"
26        });
27
28        if !is_erc721 {
29            return;
30        }
31
32        // Check each function in the contract for incorrect ERC721 signatures.
33        for item_id in contract.items {
34            let Some(fid) = item_id.as_function() else { continue };
35            let func = hir.function(fid);
36
37            if !func.kind.is_function() {
38                continue;
39            }
40
41            let Some(name) = func.name else { continue };
42
43            if has_incorrect_erc721_signature(hir, name.as_str(), func.parameters, func.returns) {
44                ctx.emit(&INCORRECT_ERC721_INTERFACE, func.span);
45            }
46        }
47    }
48}
49
50/// Checks if a function signature does not match the expected ERC721 (or ERC165) specification.
51///
52/// Returns `true` if the function name and parameter types match an ERC721 function but the return
53/// types are incorrect.
54fn has_incorrect_erc721_signature(
55    hir: &hir::Hir<'_>,
56    name: &str,
57    parameters: &[hir::VariableId],
58    returns: &[hir::VariableId],
59) -> bool {
60    let sig_match = |vars: &[hir::VariableId], expected: &[&str]| -> bool {
61        vars.len() == expected.len()
62            && vars.iter().zip(expected).all(|(&id, &ty)| is_elementary(hir, id, ty))
63    };
64    let params_match = sig_match;
65    let returns_match = sig_match;
66
67    match name {
68        // function balanceOf(address) external view returns (uint256)
69        "balanceOf" if params_match(parameters, &["address"]) => {
70            !returns_match(returns, &["uint256"])
71        }
72        // function ownerOf(uint256) external view returns (address)
73        "ownerOf" if params_match(parameters, &["uint256"]) => {
74            !returns_match(returns, &["address"])
75        }
76        // function safeTransferFrom(address,address,uint256,bytes) external
77        "safeTransferFrom"
78            if params_match(parameters, &["address", "address", "uint256", "bytes"]) =>
79        {
80            !returns_match(returns, &[])
81        }
82        // function safeTransferFrom(address,address,uint256) external
83        "safeTransferFrom" if params_match(parameters, &["address", "address", "uint256"]) => {
84            !returns_match(returns, &[])
85        }
86        // function transferFrom(address,address,uint256) external
87        "transferFrom" if params_match(parameters, &["address", "address", "uint256"]) => {
88            !returns_match(returns, &[])
89        }
90        // function approve(address,uint256) external
91        "approve" if params_match(parameters, &["address", "uint256"]) => {
92            !returns_match(returns, &[])
93        }
94        // function setApprovalForAll(address,bool) external
95        "setApprovalForAll" if params_match(parameters, &["address", "bool"]) => {
96            !returns_match(returns, &[])
97        }
98        // function getApproved(uint256) external view returns (address)
99        "getApproved" if params_match(parameters, &["uint256"]) => {
100            !returns_match(returns, &["address"])
101        }
102        // function isApprovedForAll(address,address) external view returns (bool)
103        "isApprovedForAll" if params_match(parameters, &["address", "address"]) => {
104            !returns_match(returns, &["bool"])
105        }
106        // ERC165: function supportsInterface(bytes4) external view returns (bool)
107        "supportsInterface" if params_match(parameters, &["bytes4"]) => {
108            !returns_match(returns, &["bool"])
109        }
110        _ => false,
111    }
112}