neo_solidity/solidity/validate/contract/
erc_nep_patterns.rs1fn 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
41fn 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 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 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 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
79fn 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 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
115fn 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; }
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
150fn 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
181fn 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
201fn 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
222fn 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
242fn 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
266fn 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}