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