Skip to main content

neo_devpack_solidity/solidity/
upgrade.rs

1use once_cell::sync::Lazy;
2use regex::Regex;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
5#[serde(rename_all = "snake_case")]
6pub enum UpgradeSeverity {
7    Info,
8    Warning,
9    Error,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
13#[serde(rename_all = "snake_case")]
14pub enum UpgradeCategory {
15    AutoCompatible,
16    ManualMigration,
17    ManifestReview,
18}
19
20#[derive(Debug, Clone, serde::Serialize)]
21#[serde(rename_all = "camelCase")]
22pub struct UpgradeFinding {
23    pub phase: String,
24    pub severity: UpgradeSeverity,
25    pub category: UpgradeCategory,
26    pub code: Option<String>,
27    pub contract: Option<String>,
28    pub message: String,
29    pub suggestion: Option<String>,
30}
31
32impl UpgradeFinding {
33    #[allow(clippy::too_many_arguments)]
34    pub fn new(
35        phase: impl Into<String>,
36        severity: UpgradeSeverity,
37        category: UpgradeCategory,
38        code: Option<impl Into<String>>,
39        contract: Option<impl Into<String>>,
40        message: impl Into<String>,
41        suggestion: Option<impl Into<String>>,
42    ) -> Self {
43        Self {
44            phase: phase.into(),
45            severity,
46            category,
47            code: code.map(Into::into),
48            contract: contract.map(Into::into),
49            message: message.into(),
50            suggestion: suggestion.map(Into::into),
51        }
52    }
53}
54
55pub fn analyze_upgrade_patterns(source: &str) -> Vec<UpgradeFinding> {
56    static BLOCKHASH_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\bblockhash\s*\(").unwrap());
57    static SELFDESTRUCT_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\bselfdestruct\s*\(").unwrap());
58    static CODEHASH_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\.codehash\b").unwrap());
59    static TX_ORIGIN_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\btx\.origin\b").unwrap());
60    static MSG_SIG_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\bmsg\.sig\b").unwrap());
61    static DELEGATECALL_RE: Lazy<Regex> =
62        Lazy::new(|| Regex::new(r"(?:\.|\b)delegatecall\s*\(").unwrap());
63    static STATICCALL_RE: Lazy<Regex> =
64        Lazy::new(|| Regex::new(r"(?:\.|\b)staticcall\s*\(").unwrap());
65    static LOW_LEVEL_CALL_RE: Lazy<Regex> =
66        Lazy::new(|| Regex::new(r"\.\s*call\s*(?:\(|\{)").unwrap());
67    // M-FE4 fix — require the unit to follow a digit or `)` (the EVM
68    // numeric-literal-suffix shape). The previous `\b(?:...)\b` matched the
69    // word `ether` inside a comment (`// whether we proceed`), a variable
70    // name (`uint ether = msg.value;`), or a function name — all false
71    // positives that escalated to a hard error. Requiring a digit or `)`
72    // before the unit excludes identifier/comment occurrences while still
73    // catching the real `1 ether` / `(a + b) ether` forms.
74    static ETHER_UNIT_RE: Lazy<Regex> = Lazy::new(|| {
75        Regex::new(r"(?:\d|\))\s*(?:wei|gwei|szabo|finney|ether)\b").unwrap()
76    });
77    static SUPPORTS_INTERFACE_RE: Lazy<Regex> =
78        Lazy::new(|| Regex::new(r"\bsupportsInterface\s*\(").unwrap());
79
80    let mut findings = Vec::new();
81
82    if BLOCKHASH_RE.is_match(source) {
83        findings.push(UpgradeFinding::new(
84            "source_scan",
85            UpgradeSeverity::Warning,
86            UpgradeCategory::AutoCompatible,
87            Some("SCAN_BLOCKHASH_AUTO"),
88            None::<String>,
89            "blockhash() is compiler-compatible on Neo N3 and is auto-mapped to Ledger.getBlockHash().",
90            Some("Prefer explicit Ledger.getBlockHash(height) when authoring Neo-native Solidity."),
91        ));
92    }
93
94    if SELFDESTRUCT_RE.is_match(source) {
95        findings.push(UpgradeFinding::new(
96            "source_scan",
97            UpgradeSeverity::Warning,
98            UpgradeCategory::AutoCompatible,
99            Some("SCAN_SELFDESTRUCT_AUTO"),
100            None::<String>,
101            "selfdestruct() is auto-mapped to ContractManagement.destroy() during Neo compilation.",
102            Some(
103                "Review destruction and upgrade semantics before depending on EVM selfdestruct behavior.",
104            ),
105        ));
106    }
107
108    if CODEHASH_RE.is_match(source) {
109        findings.push(UpgradeFinding::new(
110            "source_scan",
111            UpgradeSeverity::Warning,
112            UpgradeCategory::AutoCompatible,
113            Some("SCAN_CODEHASH_AUTO"),
114            None::<String>,
115            "address.codehash is compiler-compatible on Neo N3 and resolves to the script hash identity.",
116            Some("Prefer explicit script-hash based checks in Neo-native code."),
117        ));
118    }
119
120    if TX_ORIGIN_RE.is_match(source) {
121        findings.push(UpgradeFinding::new(
122            "source_scan",
123            UpgradeSeverity::Warning,
124            UpgradeCategory::ManualMigration,
125            Some("SCAN_TX_ORIGIN"),
126            None::<String>,
127            "tx.origin keeps compiling on Neo N3, but its authorization semantics differ from EVM.",
128            Some("Use Runtime.checkWitness(...) or msg.sender-oriented authorization instead."),
129        ));
130    }
131
132    if MSG_SIG_RE.is_match(source) {
133        findings.push(UpgradeFinding::new(
134            "source_scan",
135            UpgradeSeverity::Warning,
136            UpgradeCategory::ManualMigration,
137            Some("SCAN_MSG_SIG"),
138            None::<String>,
139            "msg.sig compiles on Neo N3 as the current function selector, but this differs from EVM semantics across internal calls.",
140            Some("Use explicit method names or interface IDs when you need dispatch identity that survives internal calls."),
141        ));
142    }
143
144    if DELEGATECALL_RE.is_match(source) {
145        findings.push(UpgradeFinding::new(
146            "source_scan",
147            UpgradeSeverity::Error,
148            UpgradeCategory::ManualMigration,
149            Some("SCAN_DELEGATECALL"),
150            None::<String>,
151            "delegatecall-style upgrade and proxy flows do not exist on Neo N3.",
152            Some(
153                "Replace delegatecall proxies with ContractManagement.update() or explicit cross-contract calls.",
154            ),
155        ));
156    }
157
158    if STATICCALL_RE.is_match(source) {
159        findings.push(UpgradeFinding::new(
160            "source_scan",
161            UpgradeSeverity::Warning,
162            UpgradeCategory::ManualMigration,
163            Some("SCAN_STATICCALL"),
164            None::<String>,
165            "staticcall has no direct EVM-equivalent execution mode on Neo N3.",
166            Some("Prefer typed view/pure calls or Syscalls.contractCallWithFlags(..., ReadOnly)."),
167        ));
168    }
169
170    if LOW_LEVEL_CALL_RE.is_match(source) {
171        findings.push(UpgradeFinding::new(
172            "source_scan",
173            UpgradeSeverity::Warning,
174            UpgradeCategory::ManifestReview,
175            Some("SCAN_LOW_LEVEL_CALL"),
176            None::<String>,
177            "Low-level address.call(...) patterns can force broader Neo manifest permissions.",
178            Some(
179                "Prefer typed interfaces or fixed-target wrappers so manifest permissions stay narrow.",
180            ),
181        ));
182    }
183
184    if ETHER_UNIT_RE.is_match(source) {
185        findings.push(UpgradeFinding::new(
186            "source_scan",
187            UpgradeSeverity::Error,
188            UpgradeCategory::ManualMigration,
189            Some("SCAN_ETHER_UNITS"),
190            None::<String>,
191            "Ether-denominated value units are EVM-specific and do not map directly to Neo N3 execution.",
192            Some("Use integer GAS fractions (10^8) or NEP-17 payment flows instead."),
193        ));
194    }
195
196    if SUPPORTS_INTERFACE_RE.is_match(source) {
197        findings.push(UpgradeFinding::new(
198            "source_scan",
199            UpgradeSeverity::Warning,
200            UpgradeCategory::ManualMigration,
201            Some("SCAN_SUPPORTS_INTERFACE"),
202            None::<String>,
203            "supportsInterface(bytes4) is usually unnecessary on Neo N3 because interfaces are advertised in the manifest.",
204            Some(
205                "Prefer manifest.supportedstandards instead of EIP-165 style runtime checks.",
206            ),
207        ));
208    }
209
210    findings
211}