neo_solidity/solidity/validate/contract/
erc_nep_patterns.rs

1/// ERC → NEP pattern adaptation diagnostics.
2///
3/// Detects Ethereum-style patterns in Solidity contracts and emits warnings
4/// with actionable guidance for migrating to Neo N3 equivalents.
5///
6/// Checked patterns:
7/// - ERC-20 `transfer(to, amount)` → NEP-17 `transfer(from, to, amount, data)`
8/// - ERC-20 `approve`/`allowance`/`transferFrom` → not in NEP-17 spec
9/// - ERC-721 `transferFrom(from, to, tokenId)` → NEP-11 `transfer(to, tokenId, data)`
10/// - `receive()` / `fallback()` → `onNEP17Payment()` callback
11/// - `supportsInterface(bytes4)` → manifest `supportedstandards`
12fn validate_erc_nep_patterns(metadata: &ContractMetadata, diagnostics: &mut Vec<Diagnostic>) {
13    let public_methods: Vec<&FunctionMetadata> = metadata
14        .methods
15        .iter()
16        .filter(|m| {
17            !matches!(m.kind, FunctionKind::Constructor)
18                && matches!(
19                    m.visibility,
20                    VisibilityKind::Public | VisibilityKind::External
21                )
22        })
23        .collect();
24
25    let names_lower: std::collections::HashSet<String> = public_methods
26        .iter()
27        .map(|m| m.name.to_ascii_lowercase())
28        .collect();
29
30    check_erc20_transfer_pattern(&public_methods, &names_lower, diagnostics);
31    check_erc20_approve_pattern(&public_methods, &names_lower, diagnostics);
32    check_erc721_transfer_from_pattern(&public_methods, &names_lower, diagnostics);
33    check_receive_fallback_pattern(&metadata.methods, diagnostics);
34    check_supports_interface_pattern(&public_methods, diagnostics);
35    check_bn254_precompile_usage(&public_methods, diagnostics);
36    check_erc1155_pattern(&public_methods, diagnostics);
37    check_erc2612_permit_pattern(&public_methods, diagnostics);
38    check_erc4626_vault_pattern(&public_methods, &names_lower, diagnostics);
39}
40
41/// Detect ERC-20 style `transfer(address, uint256)` and suggest NEP-17 4-param form.
42fn check_erc20_transfer_pattern(
43    public_methods: &[&FunctionMetadata],
44    names: &std::collections::HashSet<String>,
45    diagnostics: &mut Vec<Diagnostic>,
46) {
47    let has_ownerof = names.contains("ownerof");
48    // Only flag for fungible-token-like contracts (no ownerOf → not NFT)
49    if has_ownerof {
50        return;
51    }
52
53    if let Some(transfer) = public_methods
54        .iter()
55        .find(|m| m.name.eq_ignore_ascii_case("transfer"))
56    {
57        let param_count = transfer.parameters.len();
58        if param_count == 2 {
59            // Classic ERC-20: transfer(address to, uint256 amount)
60            diagnostics.push(Diagnostic::warning(
61                "function 'transfer' has 2 parameters (ERC-20 pattern). \
62                 NEP-17 requires 4 parameters: transfer(from, to, amount, data). \
63                 The `from` address is verified via Runtime.checkWitness() and \
64                 `data` (type Any) is forwarded to the recipient's onNEP17Payment callback."
65            ).with_code("W101")
66             .with_suggestion("Add `from` and `data` parameters: `transfer(address from, address to, uint256 amount, bytes data)`"));
67        } else if param_count == 3 {
68            // Partial migration: transfer(from, to, amount) — missing `data`
69            diagnostics.push(Diagnostic::warning(
70                "function 'transfer' has 3 parameters, but NEP-17 requires 4: \
71                 transfer(from, to, amount, data). The `data` parameter (type Any) \
72                 is forwarded to the recipient's onNEP17Payment callback."
73            ).with_code("W102")
74             .with_suggestion("Add `data` parameter for NEP-17 compliance: `transfer(address from, address to, uint256 amount, bytes data)`"));
75        }
76    }
77}
78
79/// Detect ERC-20 approve/allowance/transferFrom and note they are not in NEP-17.
80fn check_erc20_approve_pattern(
81    public_methods: &[&FunctionMetadata],
82    names: &std::collections::HashSet<String>,
83    diagnostics: &mut Vec<Diagnostic>,
84) {
85    let erc20_extras: Vec<&str> = ["approve", "allowance", "transferfrom"]
86        .iter()
87        .filter(|n| names.contains(**n))
88        .copied()
89        .collect();
90
91    if !erc20_extras.is_empty() {
92        // Only warn if this still looks like an ERC-20-style token surface.
93        // If a contract already exposes canonical NEP-17 transfer(from,to,amount,data)
94        // or is NFT-shaped (`ownerOf`), treat approve/allowance/transferFrom as
95        // compatibility extensions instead of migration warnings.
96        let has_token_signal = names.contains("balanceof") || names.contains("transfer");
97        let has_ownerof = names.contains("ownerof");
98        let has_nep17_transfer = public_methods
99            .iter()
100            .any(|m| m.name.eq_ignore_ascii_case("transfer") && m.parameters.len() == 4);
101
102        if has_token_signal && !has_ownerof && !has_nep17_transfer {
103            diagnostics.push(Diagnostic::warning(format!(
104                "ERC-20 method(s) [{}] detected. These are not part of the NEP-17 spec; \
105                 Neo uses Runtime.checkWitness() for authorization instead of the \
106                 approve/allowance pattern. You may keep them as extensions, but they \
107                 will not contribute to NEP-17 standard detection.",
108                erc20_extras.join(", ")
109            )).with_code("W103")
110             .with_suggestion("Remove approve/allowance or keep as optional extension alongside NEP-17 transfer"));
111        }
112    }
113}
114
115/// Detect ERC-721 `transferFrom(from, to, tokenId)` and suggest NEP-11 `transfer(to, tokenId, data)`.
116fn check_erc721_transfer_from_pattern(
117    public_methods: &[&FunctionMetadata],
118    names: &std::collections::HashSet<String>,
119    diagnostics: &mut Vec<Diagnostic>,
120) {
121    let has_ownerof = names.contains("ownerof");
122    if !has_ownerof {
123        return; // Not an NFT contract
124    }
125
126    let has_nep11_transfer = public_methods
127        .iter()
128        .any(|m| m.name.eq_ignore_ascii_case("transfer") && m.parameters.len() == 3);
129
130    if has_nep11_transfer {
131        return;
132    }
133
134    if let Some(xfer_from) = public_methods
135        .iter()
136        .find(|m| m.name.eq_ignore_ascii_case("transferfrom"))
137    {
138        let param_count = xfer_from.parameters.len();
139        if param_count == 3 {
140            diagnostics.push(Diagnostic::warning(
141                "function 'transferFrom' with 3 parameters (ERC-721 pattern) detected. \
142                 NEP-11 uses transfer(to, tokenId, data) with 3 parameters instead. \
143                 Authorization is via Runtime.checkWitness(owner), not msg.sender."
144            ).with_code("W104")
145             .with_suggestion("Replace `transferFrom(from, to, id)` with `transfer(to, id, data)`"));
146        }
147    }
148}
149
150/// Detect `receive()` / `fallback()` and suggest `onNEP17Payment()`.
151fn check_receive_fallback_pattern(
152    all_methods: &[FunctionMetadata],
153    diagnostics: &mut Vec<Diagnostic>,
154) {
155    let has_onnep17 = all_methods
156        .iter()
157        .any(|m| m.name.eq_ignore_ascii_case("onnep17payment"));
158
159    for method in all_methods {
160        let name_lower = method.name.to_ascii_lowercase();
161        if name_lower == "receive" || name_lower == "fallback" {
162            if has_onnep17 {
163                diagnostics.push(Diagnostic::warning(format!(
164                    "function '{}' has no effect on Neo N3. The contract already defines \
165                     onNEP17Payment which is the correct Neo callback for receiving tokens.",
166                    method.name
167                )).with_code("W105")
168                 .with_suggestion("Remove — the existing onNEP17Payment handler is sufficient"));
169            } else {
170                diagnostics.push(Diagnostic::warning(format!(
171                    "function '{}' has no effect on Neo N3. Use onNEP17Payment(address from, \
172                     uint256 amount, bytes data) to handle incoming token payments.",
173                    method.name
174                )).with_code("W105")
175                 .with_suggestion("Replace with `function onNEP17Payment(address from, uint256 amount, bytes data)`"));
176            }
177        }
178    }
179}
180
181/// Detect `supportsInterface(bytes4)` and note that Neo uses manifest instead.
182fn check_supports_interface_pattern(
183    public_methods: &[&FunctionMetadata],
184    diagnostics: &mut Vec<Diagnostic>,
185) {
186    if let Some(si) = public_methods
187        .iter()
188        .find(|m| m.name == "supportsInterface")
189    {
190        if si.parameters.len() == 1 {
191            diagnostics.push(Diagnostic::warning(
192                "function 'supportsInterface' (EIP-165) is unnecessary on Neo N3. \
193                 Neo uses the manifest 'supportedstandards' array for interface \
194                 detection, which the compiler populates automatically."
195            ).with_code("W106")
196             .with_suggestion("Remove — Neo N3 uses manifest-based interface discovery"));
197        }
198    }
199}
200
201/// Detect ERC-1155 multi-token pattern and note Neo N3 has no direct equivalent.
202fn check_erc1155_pattern(
203    public_methods: &[&FunctionMetadata],
204    diagnostics: &mut Vec<Diagnostic>,
205) {
206    let has_safe_transfer = public_methods.iter().any(|m| {
207        m.name.eq_ignore_ascii_case("safeTransferFrom") && m.parameters.len() == 5
208    });
209    let has_batch_transfer = public_methods.iter().any(|m| {
210        m.name.eq_ignore_ascii_case("safeBatchTransferFrom") && m.parameters.len() == 5
211    });
212
213    if has_safe_transfer || has_batch_transfer {
214        diagnostics.push(Diagnostic::warning(
215            "ERC-1155 multi-token pattern detected. Neo N3 does not have a direct \
216             NEP equivalent for multi-token contracts."
217        ).with_code("W107")
218         .with_suggestion("Split into separate NEP-17 (fungible) and NEP-11 (non-fungible) contracts"));
219    }
220}
221
222/// Detect ERC-2612 permit pattern and note Neo uses checkWitness instead.
223fn check_erc2612_permit_pattern(
224    public_methods: &[&FunctionMetadata],
225    diagnostics: &mut Vec<Diagnostic>,
226) {
227    if let Some(permit) = public_methods
228        .iter()
229        .find(|m| m.name.eq_ignore_ascii_case("permit"))
230    {
231        if permit.parameters.len() == 7 {
232            diagnostics.push(Diagnostic::warning(
233                "ERC-2612 permit pattern detected (7-parameter permit function). \
234                 Neo N3 uses Runtime.checkWitness() for authorization; off-chain \
235                 signature permits are not needed."
236            ).with_code("W108")
237             .with_suggestion("Use `Runtime.checkWitness()` instead of off-chain signatures"));
238        }
239    }
240}
241
242/// Detect ERC-4626 tokenized vault pattern and suggest NEP-17 replacement.
243fn check_erc4626_vault_pattern(
244    public_methods: &[&FunctionMetadata],
245    names: &std::collections::HashSet<String>,
246    diagnostics: &mut Vec<Diagnostic>,
247) {
248    let has_deposit = public_methods
249        .iter()
250        .any(|m| m.name.eq_ignore_ascii_case("deposit") && m.parameters.len() == 2);
251    let has_withdraw = public_methods
252        .iter()
253        .any(|m| m.name.eq_ignore_ascii_case("withdraw") && m.parameters.len() == 3);
254    let has_convert_shares = names.contains("converttoshares");
255    let has_convert_assets = names.contains("converttoassets");
256
257    if has_deposit && has_withdraw && (has_convert_shares || has_convert_assets) {
258        diagnostics.push(Diagnostic::warning(
259            "ERC-4626 tokenized vault pattern detected. The vault logic compiles \
260             correctly, but replace ERC-20 token interactions with NEP-17 equivalents."
261        ).with_code("W109")
262         .with_suggestion("Replace ERC-20 interactions with NEP-17 equivalents; use Runtime.checkWitness() for authorization"));
263    }
264}
265
266/// Detect BN254 elliptic curve precompile usage and suggest Neo alternatives.
267fn check_bn254_precompile_usage(
268    public_methods: &[&FunctionMetadata],
269    diagnostics: &mut Vec<Diagnostic>,
270) {
271    let bn254_indicators = ["ecadd", "ecmul", "ecpairing", "bn256add", "bn256scalarmul", "bn256pairing"];
272    for method in public_methods {
273        let name_lower = method.name.to_ascii_lowercase();
274        if bn254_indicators.iter().any(|ind| name_lower.contains(ind)) {
275            diagnostics.push(Diagnostic::warning(format!(
276                "function '{}' appears to use BN254 elliptic curve operations (ecAdd/ecMul/ecPairing). \
277                 These precompiles (addresses 0x06, 0x07, 0x08) are not available on Neo N3.",
278                method.name
279            )).with_code("W110")
280             .with_suggestion("Use CryptoLib BLS12-381 operations instead"));
281        }
282    }
283}