zexus 1.7.1 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/src/__init__.py +7 -0
  4. package/src/zexus/__init__.py +1 -1
  5. package/src/zexus/__pycache__/__init__.cpython-312.pyc +0 -0
  6. package/src/zexus/__pycache__/capability_system.cpython-312.pyc +0 -0
  7. package/src/zexus/__pycache__/debug_sanitizer.cpython-312.pyc +0 -0
  8. package/src/zexus/__pycache__/environment.cpython-312.pyc +0 -0
  9. package/src/zexus/__pycache__/error_reporter.cpython-312.pyc +0 -0
  10. package/src/zexus/__pycache__/input_validation.cpython-312.pyc +0 -0
  11. package/src/zexus/__pycache__/lexer.cpython-312.pyc +0 -0
  12. package/src/zexus/__pycache__/module_cache.cpython-312.pyc +0 -0
  13. package/src/zexus/__pycache__/module_manager.cpython-312.pyc +0 -0
  14. package/src/zexus/__pycache__/object.cpython-312.pyc +0 -0
  15. package/src/zexus/__pycache__/security.cpython-312.pyc +0 -0
  16. package/src/zexus/__pycache__/security_enforcement.cpython-312.pyc +0 -0
  17. package/src/zexus/__pycache__/syntax_validator.cpython-312.pyc +0 -0
  18. package/src/zexus/__pycache__/zexus_ast.cpython-312.pyc +0 -0
  19. package/src/zexus/__pycache__/zexus_token.cpython-312.pyc +0 -0
  20. package/src/zexus/access_control_system/__pycache__/__init__.cpython-312.pyc +0 -0
  21. package/src/zexus/access_control_system/__pycache__/access_control.cpython-312.pyc +0 -0
  22. package/src/zexus/advanced_types.py +17 -2
  23. package/src/zexus/blockchain/__init__.py +411 -0
  24. package/src/zexus/blockchain/accelerator.py +1160 -0
  25. package/src/zexus/blockchain/chain.py +660 -0
  26. package/src/zexus/blockchain/consensus.py +821 -0
  27. package/src/zexus/blockchain/contract_vm.py +1019 -0
  28. package/src/zexus/blockchain/crypto.py +79 -14
  29. package/src/zexus/blockchain/events.py +526 -0
  30. package/src/zexus/blockchain/loadtest.py +721 -0
  31. package/src/zexus/blockchain/monitoring.py +350 -0
  32. package/src/zexus/blockchain/mpt.py +716 -0
  33. package/src/zexus/blockchain/multichain.py +951 -0
  34. package/src/zexus/blockchain/multiprocess_executor.py +338 -0
  35. package/src/zexus/blockchain/network.py +886 -0
  36. package/src/zexus/blockchain/node.py +666 -0
  37. package/src/zexus/blockchain/rpc.py +1203 -0
  38. package/src/zexus/blockchain/rust_bridge.py +421 -0
  39. package/src/zexus/blockchain/storage.py +423 -0
  40. package/src/zexus/blockchain/tokens.py +750 -0
  41. package/src/zexus/blockchain/upgradeable.py +1004 -0
  42. package/src/zexus/blockchain/verification.py +1602 -0
  43. package/src/zexus/blockchain/wallet.py +621 -0
  44. package/src/zexus/cli/__pycache__/main.cpython-312.pyc +0 -0
  45. package/src/zexus/cli/main.py +300 -20
  46. package/src/zexus/cli/zpm.py +1 -1
  47. package/src/zexus/compiler/__pycache__/bytecode.cpython-312.pyc +0 -0
  48. package/src/zexus/compiler/__pycache__/lexer.cpython-312.pyc +0 -0
  49. package/src/zexus/compiler/__pycache__/parser.cpython-312.pyc +0 -0
  50. package/src/zexus/compiler/__pycache__/semantic.cpython-312.pyc +0 -0
  51. package/src/zexus/compiler/__pycache__/zexus_ast.cpython-312.pyc +0 -0
  52. package/src/zexus/compiler/lexer.py +10 -5
  53. package/src/zexus/concurrency_system.py +79 -0
  54. package/src/zexus/config.py +54 -0
  55. package/src/zexus/crypto_bridge.py +244 -8
  56. package/src/zexus/dap/__init__.py +10 -0
  57. package/src/zexus/dap/__main__.py +4 -0
  58. package/src/zexus/dap/dap_server.py +391 -0
  59. package/src/zexus/dap/debug_engine.py +298 -0
  60. package/src/zexus/environment.py +10 -1
  61. package/src/zexus/evaluator/__pycache__/bytecode_compiler.cpython-312.pyc +0 -0
  62. package/src/zexus/evaluator/__pycache__/core.cpython-312.pyc +0 -0
  63. package/src/zexus/evaluator/__pycache__/expressions.cpython-312.pyc +0 -0
  64. package/src/zexus/evaluator/__pycache__/functions.cpython-312.pyc +0 -0
  65. package/src/zexus/evaluator/__pycache__/resource_limiter.cpython-312.pyc +0 -0
  66. package/src/zexus/evaluator/__pycache__/statements.cpython-312.pyc +0 -0
  67. package/src/zexus/evaluator/__pycache__/unified_execution.cpython-312.pyc +0 -0
  68. package/src/zexus/evaluator/__pycache__/utils.cpython-312.pyc +0 -0
  69. package/src/zexus/evaluator/bytecode_compiler.py +441 -37
  70. package/src/zexus/evaluator/core.py +560 -49
  71. package/src/zexus/evaluator/expressions.py +122 -49
  72. package/src/zexus/evaluator/functions.py +417 -16
  73. package/src/zexus/evaluator/statements.py +521 -118
  74. package/src/zexus/evaluator/unified_execution.py +573 -72
  75. package/src/zexus/evaluator/utils.py +14 -2
  76. package/src/zexus/event_loop.py +186 -0
  77. package/src/zexus/lexer.py +742 -486
  78. package/src/zexus/lsp/__init__.py +1 -1
  79. package/src/zexus/lsp/definition_provider.py +163 -9
  80. package/src/zexus/lsp/server.py +22 -8
  81. package/src/zexus/lsp/symbol_provider.py +182 -9
  82. package/src/zexus/module_cache.py +237 -9
  83. package/src/zexus/object.py +64 -6
  84. package/src/zexus/parser/__pycache__/parser.cpython-312.pyc +0 -0
  85. package/src/zexus/parser/__pycache__/strategy_context.cpython-312.pyc +0 -0
  86. package/src/zexus/parser/__pycache__/strategy_structural.cpython-312.pyc +0 -0
  87. package/src/zexus/parser/parser.py +786 -285
  88. package/src/zexus/parser/strategy_context.py +407 -66
  89. package/src/zexus/parser/strategy_structural.py +117 -19
  90. package/src/zexus/persistence.py +15 -1
  91. package/src/zexus/renderer/__init__.py +15 -0
  92. package/src/zexus/renderer/__pycache__/__init__.cpython-312.pyc +0 -0
  93. package/src/zexus/renderer/__pycache__/backend.cpython-312.pyc +0 -0
  94. package/src/zexus/renderer/__pycache__/canvas.cpython-312.pyc +0 -0
  95. package/src/zexus/renderer/__pycache__/color_system.cpython-312.pyc +0 -0
  96. package/src/zexus/renderer/__pycache__/layout.cpython-312.pyc +0 -0
  97. package/src/zexus/renderer/__pycache__/main_renderer.cpython-312.pyc +0 -0
  98. package/src/zexus/renderer/__pycache__/painter.cpython-312.pyc +0 -0
  99. package/src/zexus/renderer/tk_backend.py +208 -0
  100. package/src/zexus/renderer/web_backend.py +260 -0
  101. package/src/zexus/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
  102. package/src/zexus/runtime/__pycache__/async_runtime.cpython-312.pyc +0 -0
  103. package/src/zexus/runtime/__pycache__/load_manager.cpython-312.pyc +0 -0
  104. package/src/zexus/runtime/file_flags.py +137 -0
  105. package/src/zexus/safety/__pycache__/__init__.cpython-312.pyc +0 -0
  106. package/src/zexus/safety/__pycache__/memory_safety.cpython-312.pyc +0 -0
  107. package/src/zexus/security.py +424 -34
  108. package/src/zexus/stdlib/fs.py +23 -18
  109. package/src/zexus/stdlib/http.py +289 -186
  110. package/src/zexus/stdlib/sockets.py +207 -163
  111. package/src/zexus/stdlib/websockets.py +282 -0
  112. package/src/zexus/stdlib_integration.py +369 -2
  113. package/src/zexus/strategy_recovery.py +6 -3
  114. package/src/zexus/type_checker.py +423 -0
  115. package/src/zexus/virtual_filesystem.py +189 -2
  116. package/src/zexus/vm/__init__.py +113 -3
  117. package/src/zexus/vm/__pycache__/async_optimizer.cpython-312.pyc +0 -0
  118. package/src/zexus/vm/__pycache__/bytecode.cpython-312.pyc +0 -0
  119. package/src/zexus/vm/__pycache__/bytecode_converter.cpython-312.pyc +0 -0
  120. package/src/zexus/vm/__pycache__/cache.cpython-312.pyc +0 -0
  121. package/src/zexus/vm/__pycache__/compiler.cpython-312.pyc +0 -0
  122. package/src/zexus/vm/__pycache__/gas_metering.cpython-312.pyc +0 -0
  123. package/src/zexus/vm/__pycache__/jit.cpython-312.pyc +0 -0
  124. package/src/zexus/vm/__pycache__/parallel_vm.cpython-312.pyc +0 -0
  125. package/src/zexus/vm/__pycache__/vm.cpython-312.pyc +0 -0
  126. package/src/zexus/vm/async_optimizer.py +14 -1
  127. package/src/zexus/vm/binary_bytecode.py +659 -0
  128. package/src/zexus/vm/bytecode.py +28 -1
  129. package/src/zexus/vm/bytecode_converter.py +26 -12
  130. package/src/zexus/vm/cabi.c +1985 -0
  131. package/src/zexus/vm/cabi.cpython-312-x86_64-linux-gnu.so +0 -0
  132. package/src/zexus/vm/cabi.h +127 -0
  133. package/src/zexus/vm/cache.py +557 -17
  134. package/src/zexus/vm/compiler.py +703 -5
  135. package/src/zexus/vm/fastops.c +15743 -0
  136. package/src/zexus/vm/fastops.cpython-312-x86_64-linux-gnu.so +0 -0
  137. package/src/zexus/vm/fastops.pyx +288 -0
  138. package/src/zexus/vm/gas_metering.py +50 -9
  139. package/src/zexus/vm/jit.py +83 -2
  140. package/src/zexus/vm/native_jit_backend.py +1816 -0
  141. package/src/zexus/vm/native_runtime.cpp +1388 -0
  142. package/src/zexus/vm/native_runtime.cpython-312-x86_64-linux-gnu.so +0 -0
  143. package/src/zexus/vm/optimizer.py +161 -11
  144. package/src/zexus/vm/parallel_vm.py +118 -42
  145. package/src/zexus/vm/peephole_optimizer.py +82 -4
  146. package/src/zexus/vm/profiler.py +38 -18
  147. package/src/zexus/vm/register_allocator.py +16 -5
  148. package/src/zexus/vm/register_vm.py +8 -5
  149. package/src/zexus/vm/vm.py +3411 -573
  150. package/src/zexus/vm/wasm_compiler.py +658 -0
  151. package/src/zexus/zexus_ast.py +63 -11
  152. package/src/zexus/zexus_token.py +13 -5
  153. package/src/zexus/zpm/installer.py +55 -15
  154. package/src/zexus/zpm/package_manager.py +1 -1
  155. package/src/zexus/zpm/registry.py +257 -28
  156. package/src/zexus.egg-info/PKG-INFO +7 -4
  157. package/src/zexus.egg-info/SOURCES.txt +116 -9
  158. package/src/zexus.egg-info/entry_points.txt +1 -0
  159. package/src/zexus.egg-info/requires.txt +4 -0
@@ -0,0 +1,1602 @@
1
+ """
2
+ Zexus Blockchain — Formal Verification Engine
3
+ ==============================================
4
+
5
+ A static-analysis and symbolic-execution engine that verifies smart
6
+ contract correctness **before** deployment. Operates entirely on the
7
+ AST — never executes user code.
8
+
9
+ Verification Levels
10
+ -------------------
11
+
12
+ Level 1 — **Structural Checks** (fast, always available)
13
+ * Detects missing ``require`` guards on state-mutating actions.
14
+ * Ensures every action that transfers value checks balances.
15
+ * Verifies reentrancy-safe patterns (no external calls after
16
+ state writes).
17
+ * Checks for integer overflow / underflow patterns.
18
+
19
+ Level 2 — **Invariant Verification** (symbolic)
20
+ * User declares ``@invariant`` annotations on contracts.
21
+ * The engine symbolically walks the AST to prove that every
22
+ action preserves the invariant or reports a counterexample.
23
+ * Supports arithmetic constraints (linear inequalities).
24
+
25
+ Level 3 — **Property-Based Verification** (bounded model checking)
26
+ * User declares ``@property`` annotations.
27
+ * The engine explores bounded paths through the action logic
28
+ to verify the property holds for all reachable states.
29
+ * Supports ``@pre`` (precondition) and ``@post`` (postcondition).
30
+
31
+ Integration
32
+ -----------
33
+ * Can be called standalone or wired into the deployment pipeline
34
+ so that ``ContractVM.deploy_contract()`` automatically verifies
35
+ before accepting the contract.
36
+ * Emits ``VerificationReport`` objects with detailed findings.
37
+ * Integrates with the existing ``StaticTypeChecker``.
38
+
39
+ Usage
40
+ -----
41
+ ::
42
+
43
+ from zexus.blockchain.verification import (
44
+ FormalVerifier,
45
+ VerificationLevel,
46
+ )
47
+
48
+ verifier = FormalVerifier(level=VerificationLevel.INVARIANT)
49
+ report = verifier.verify_contract(contract)
50
+ if not report.passed:
51
+ for finding in report.findings:
52
+ print(finding)
53
+ """
54
+
55
+ from __future__ import annotations
56
+
57
+ import copy
58
+ import hashlib
59
+ import itertools
60
+ import math
61
+ import re
62
+ import time
63
+ import logging
64
+ from dataclasses import dataclass, field
65
+ from enum import IntEnum, Enum
66
+ from typing import (
67
+ Any, Callable, Dict, FrozenSet, List, Optional,
68
+ Set, Tuple, Union,
69
+ )
70
+
71
+ logger = logging.getLogger("zexus.blockchain.verification")
72
+
73
+
74
+ # =====================================================================
75
+ # Constants & Enums
76
+ # =====================================================================
77
+
78
+ class VerificationLevel(IntEnum):
79
+ """How deep the verification goes."""
80
+ STRUCTURAL = 1 # Pattern-based checks only
81
+ INVARIANT = 2 # + symbolic invariant proofs
82
+ PROPERTY = 3 # + bounded model checking of @property annotations
83
+ TAINT = 4 # + taint / data-flow analysis
84
+
85
+
86
+ class Severity(str, Enum):
87
+ CRITICAL = "critical"
88
+ HIGH = "high"
89
+ MEDIUM = "medium"
90
+ LOW = "low"
91
+ INFO = "info"
92
+
93
+
94
+ class FindingCategory(str, Enum):
95
+ MISSING_REQUIRE = "missing_require"
96
+ REENTRANCY = "reentrancy"
97
+ OVERFLOW = "overflow"
98
+ UNDERFLOW = "underflow"
99
+ UNCHECKED_TRANSFER = "unchecked_transfer"
100
+ INVARIANT_VIOLATION = "invariant_violation"
101
+ PROPERTY_VIOLATION = "property_violation"
102
+ UNINITIALIZED_STATE = "uninitialized_state"
103
+ UNREACHABLE_CODE = "unreachable_code"
104
+ ACCESS_CONTROL = "access_control"
105
+ DIVISION_BY_ZERO = "division_by_zero"
106
+ STATE_AFTER_CALL = "state_after_call"
107
+ PRECONDITION_VIOLATION = "precondition"
108
+ POSTCONDITION_VIOLATION = "postcondition"
109
+ TAINTED_VALUE = "tainted_value"
110
+ UNSANITIZED_INPUT = "unsanitized_input"
111
+ TAINTED_STORAGE_KEY = "tainted_storage_key"
112
+ PRIVILEGE_ESCALATION = "privilege_escalation"
113
+ UNCHECKED_RETURN = "unchecked_return"
114
+
115
+
116
+ # =====================================================================
117
+ # Findings & Reports
118
+ # =====================================================================
119
+
120
+ @dataclass
121
+ class VerificationFinding:
122
+ """A single issue found during verification."""
123
+ category: FindingCategory
124
+ severity: Severity
125
+ message: str
126
+ action_name: str = ""
127
+ contract_name: str = ""
128
+ line: Optional[int] = None
129
+ suggestion: str = ""
130
+ counterexample: Optional[Dict[str, Any]] = None
131
+
132
+ def to_dict(self) -> Dict[str, Any]:
133
+ d: Dict[str, Any] = {
134
+ "category": self.category.value,
135
+ # Public/serialized form uses the Enum name (UPPERCASE)
136
+ # to provide a stable contract for external tooling.
137
+ "severity": self.severity.name,
138
+ "message": self.message,
139
+ }
140
+ if self.action_name:
141
+ d["action"] = self.action_name
142
+ if self.contract_name:
143
+ d["contract"] = self.contract_name
144
+ if self.line is not None:
145
+ d["line"] = self.line
146
+ if self.suggestion:
147
+ d["suggestion"] = self.suggestion
148
+ if self.counterexample:
149
+ d["counterexample"] = self.counterexample
150
+ return d
151
+
152
+ def __str__(self) -> str:
153
+ loc = f" (line {self.line})" if self.line else ""
154
+ act = f" in {self.action_name}" if self.action_name else ""
155
+ return f"[{self.severity.value.upper()}] {self.category.value}{act}{loc}: {self.message}"
156
+
157
+
158
+ @dataclass
159
+ class VerificationReport:
160
+ """Aggregate result of verifying a contract."""
161
+ level: VerificationLevel
162
+ contract_name: str = ""
163
+ started_at: float = field(default_factory=time.time)
164
+ finished_at: float = 0.0
165
+ findings: List[VerificationFinding] = field(default_factory=list)
166
+ actions_checked: int = 0
167
+ invariants_checked: int = 0
168
+ properties_checked: int = 0
169
+
170
+ @property
171
+ def passed(self) -> bool:
172
+ """True if no critical or high findings."""
173
+ return not any(
174
+ f.severity in (Severity.CRITICAL, Severity.HIGH)
175
+ for f in self.findings
176
+ )
177
+
178
+ @property
179
+ def duration(self) -> float:
180
+ return self.finished_at - self.started_at
181
+
182
+ @property
183
+ def critical_count(self) -> int:
184
+ return sum(1 for f in self.findings if f.severity == Severity.CRITICAL)
185
+
186
+ @property
187
+ def high_count(self) -> int:
188
+ return sum(1 for f in self.findings if f.severity == Severity.HIGH)
189
+
190
+ @property
191
+ def medium_count(self) -> int:
192
+ return sum(1 for f in self.findings if f.severity == Severity.MEDIUM)
193
+
194
+ @property
195
+ def low_count(self) -> int:
196
+ return sum(1 for f in self.findings if f.severity == Severity.LOW)
197
+
198
+ def summary(self) -> str:
199
+ status = "PASS" if self.passed else "FAIL"
200
+ return (
201
+ f"Verification [{status}] for '{self.contract_name}': "
202
+ f"{len(self.findings)} findings "
203
+ f"(C={self.critical_count} H={self.high_count} "
204
+ f"M={self.medium_count} L={self.low_count}) "
205
+ f"in {self.duration:.3f}s"
206
+ )
207
+
208
+ def to_dict(self) -> Dict[str, Any]:
209
+ return {
210
+ # Provide both keys for compatibility across callers.
211
+ "contract": self.contract_name,
212
+ "contract_name": self.contract_name,
213
+ "level": self.level.name,
214
+ "passed": self.passed,
215
+ "duration": round(self.duration, 4),
216
+ "actions_checked": self.actions_checked,
217
+ "invariants_checked": self.invariants_checked,
218
+ "properties_checked": self.properties_checked,
219
+ "findings": [f.to_dict() for f in self.findings],
220
+ "summary": self.summary(),
221
+ }
222
+
223
+
224
+ # =====================================================================
225
+ # Symbolic Value (for invariant / property checking)
226
+ # =====================================================================
227
+
228
+ class SymType(str, Enum):
229
+ INT = "int"
230
+ INTEGER = "int" # alias expected by tests
231
+ FLOAT = "float"
232
+ STRING = "string"
233
+ BOOL = "bool"
234
+ MAP = "map"
235
+ LIST = "list"
236
+ ANY = "any"
237
+ UNKNOWN = "unknown"
238
+
239
+
240
+ @dataclass
241
+ class SymValue:
242
+ """A symbolic value with optional concrete range bounds."""
243
+ name: str = ""
244
+ sym_type: SymType = SymType.ANY
245
+ min_val: Optional[Union[int, float]] = None
246
+ max_val: Optional[Union[int, float]] = None
247
+ concrete: Optional[Any] = None # Known constant value
248
+ is_tainted: bool = False # True if from external input
249
+
250
+ @property
251
+ def is_concrete(self) -> bool:
252
+ return self.concrete is not None
253
+
254
+ @property
255
+ def is_bounded(self) -> bool:
256
+ return self.min_val is not None or self.max_val is not None
257
+
258
+ def could_be_negative(self) -> bool:
259
+ if self.is_concrete:
260
+ return self.concrete < 0
261
+ if self.min_val is not None:
262
+ return self.min_val < 0
263
+ return True # unknown — conservatively yes
264
+
265
+ def could_be_zero(self) -> bool:
266
+ if self.is_concrete:
267
+ return self.concrete == 0
268
+ if self.min_val is not None and self.max_val is not None:
269
+ return self.min_val <= 0 <= self.max_val
270
+ return True
271
+
272
+ def copy(self) -> "SymValue":
273
+ return SymValue(
274
+ name=self.name,
275
+ sym_type=self.sym_type,
276
+ min_val=self.min_val,
277
+ max_val=self.max_val,
278
+ concrete=self.concrete,
279
+ is_tainted=self.is_tainted,
280
+ )
281
+
282
+
283
+ class SymState:
284
+ """Symbolic environment — tracks variable constraints."""
285
+
286
+ def __init__(self, parent: Optional["SymState"] = None):
287
+ self._vars: Dict[str, SymValue] = {}
288
+ self._parent = parent
289
+ self._constraints: List[str] = [] # human-readable constraint log
290
+
291
+ def get(self, name: str) -> Optional[SymValue]:
292
+ v = self._vars.get(name)
293
+ if v is not None:
294
+ return v
295
+ if self._parent:
296
+ return self._parent.get(name)
297
+ return None
298
+
299
+ def set(self, name: str, val: SymValue) -> None:
300
+ self._vars[name] = val
301
+
302
+ def add_constraint(self, desc: str) -> None:
303
+ self._constraints.append(desc)
304
+
305
+ def child(self) -> "SymState":
306
+ return SymState(parent=self)
307
+
308
+ @property
309
+ def constraints(self) -> List[str]:
310
+ return list(self._constraints)
311
+
312
+ @property
313
+ def all_vars(self) -> Dict[str, SymValue]:
314
+ merged: Dict[str, SymValue] = {}
315
+ if self._parent:
316
+ merged.update(self._parent.all_vars)
317
+ merged.update(self._vars)
318
+ return merged
319
+
320
+
321
+ # =====================================================================
322
+ # AST Helpers
323
+ # =====================================================================
324
+
325
+ def _node_type(node: Any) -> str:
326
+ """Get the class name of an AST node."""
327
+ return type(node).__name__
328
+
329
+
330
+ def _get_name(node: Any) -> str:
331
+ """Extract a string name from an Identifier or string."""
332
+ if isinstance(node, str):
333
+ return node
334
+ if hasattr(node, "value"):
335
+ return str(node.value)
336
+ if hasattr(node, "name"):
337
+ return _get_name(node.name)
338
+ return str(node)
339
+
340
+
341
+ def _get_action_name(action_node: Any) -> str:
342
+ """Extract the method/action name from an ActionStatement."""
343
+ if hasattr(action_node, "name"):
344
+ return _get_name(action_node.name)
345
+ return "<anonymous>"
346
+
347
+
348
+ def _walk_ast(node: Any) -> List[Any]:
349
+ """Recursively collect all AST nodes in a subtree."""
350
+ if node is None:
351
+ return []
352
+ result = [node]
353
+ # Walk child attributes
354
+ for attr_name in dir(node):
355
+ if attr_name.startswith("_"):
356
+ continue
357
+ attr = getattr(node, attr_name, None)
358
+ if attr is None or callable(attr):
359
+ continue
360
+ if isinstance(attr, list):
361
+ for item in attr:
362
+ if hasattr(item, "__dict__"):
363
+ result.extend(_walk_ast(item))
364
+ elif hasattr(attr, "__dict__") and not isinstance(attr, type):
365
+ # Heuristic: if it looks like an AST node, recurse
366
+ result.extend(_walk_ast(attr))
367
+ return result
368
+
369
+
370
+ def _contains_node_type(node: Any, type_name: str) -> bool:
371
+ """Check if any descendant node has the given class name."""
372
+ for n in _walk_ast(node):
373
+ if _node_type(n) == type_name:
374
+ return True
375
+ return False
376
+
377
+
378
+ def _collect_nodes_of_type(node: Any, type_name: str) -> List[Any]:
379
+ """Collect all descendant nodes of a specific type."""
380
+ return [n for n in _walk_ast(node) if _node_type(n) == type_name]
381
+
382
+
383
+ def _contains_state_write(node: Any) -> bool:
384
+ """Heuristic: does this action body write to contract state?"""
385
+ for n in _walk_ast(node):
386
+ nt = _node_type(n)
387
+ # Assignment to state variable, indexed assignment, property set
388
+ if nt in ("AssignmentExpression", "IndexExpression",
389
+ "PropertyAssignment"):
390
+ return True
391
+ # Direct store via 'this.x = ...'
392
+ if nt == "PropertyExpression":
393
+ if hasattr(n, "object") and _get_name(getattr(n, "object", "")) == "this":
394
+ return True
395
+ return False
396
+
397
+
398
+ def _contains_external_call(node: Any) -> bool:
399
+ """Heuristic: does this body make external calls (contract_call, transfer)?"""
400
+ for n in _walk_ast(node):
401
+ nt = _node_type(n)
402
+ if nt == "CallExpression":
403
+ callee = getattr(n, "function", getattr(n, "callee", None))
404
+ name = _get_name(callee) if callee else ""
405
+ if name in ("contract_call", "delegate_call", "transfer",
406
+ "static_call", "send"):
407
+ return True
408
+ return False
409
+
410
+
411
+ def _contains_require(node: Any) -> bool:
412
+ """Does the body contain at least one require statement?"""
413
+ return _contains_node_type(node, "RequireStatement")
414
+
415
+
416
+ def _contains_caller_check(node: Any) -> bool:
417
+ """Does the body check TX.caller or msg.sender?"""
418
+ for n in _walk_ast(node):
419
+ nt = _node_type(n)
420
+ if nt == "PropertyExpression":
421
+ obj = getattr(n, "object", None)
422
+ prop = getattr(n, "property", None)
423
+ obj_name = _get_name(obj) if obj else ""
424
+ prop_name = _get_name(prop) if prop else ""
425
+ if obj_name in ("TX", "msg") and prop_name in ("caller", "sender"):
426
+ return True
427
+ return False
428
+
429
+
430
+ def _find_division_ops(node: Any) -> List[Any]:
431
+ """Find all division operations in the subtree."""
432
+ divisions = []
433
+ for n in _walk_ast(node):
434
+ nt = _node_type(n)
435
+ if nt in ("InfixExpression", "BinaryExpression"):
436
+ op = getattr(n, "operator", getattr(n, "op", ""))
437
+ if isinstance(op, str) and op in ("/", "%"):
438
+ divisions.append(n)
439
+ return divisions
440
+
441
+
442
+ def _find_arithmetic_ops(node: Any) -> List[Any]:
443
+ """Find all arithmetic operations."""
444
+ ops = []
445
+ for n in _walk_ast(node):
446
+ nt = _node_type(n)
447
+ if nt in ("InfixExpression", "BinaryExpression"):
448
+ op = getattr(n, "operator", getattr(n, "op", ""))
449
+ if isinstance(op, str) and op in ("+", "-", "*", "**"):
450
+ ops.append(n)
451
+ return ops
452
+
453
+
454
+ def _extract_state_vars(contract: Any) -> List[str]:
455
+ """Extract state variable names from a SmartContract or ContractStatement."""
456
+ names: List[str] = []
457
+ if hasattr(contract, "storage_vars"):
458
+ for var_node in contract.storage_vars:
459
+ if isinstance(var_node, str):
460
+ names.append(var_node)
461
+ elif hasattr(var_node, "name"):
462
+ names.append(_get_name(var_node.name))
463
+ elif isinstance(var_node, dict):
464
+ n = var_node.get("name", "")
465
+ if n:
466
+ names.append(n)
467
+ return names
468
+
469
+
470
+ def _extract_actions(contract: Any) -> Dict[str, Any]:
471
+ """Extract action name -> action object mapping."""
472
+ if hasattr(contract, "actions"):
473
+ actions = contract.actions
474
+ if isinstance(actions, dict):
475
+ return actions
476
+ return {}
477
+
478
+
479
+ # =====================================================================
480
+ # Structural Verifier (Level 1)
481
+ # =====================================================================
482
+
483
+ class StructuralVerifier:
484
+ """Pattern-based checks on contract ASTs.
485
+
486
+ Checks performed:
487
+ * Missing access-control ``require`` on state-mutating actions.
488
+ * Balance checks before transfers.
489
+ * State writes after external calls (reentrancy pattern).
490
+ * Division by zero potential.
491
+ * Integer overflow patterns (unchecked arithmetic).
492
+ * Uninitialized state variable reads.
493
+ """
494
+
495
+ def verify(
496
+ self,
497
+ contract: Any,
498
+ report: VerificationReport,
499
+ ) -> None:
500
+ contract_name = _get_name(getattr(contract, "name", "")) or "Unknown"
501
+ report.contract_name = contract_name
502
+
503
+ state_vars = _extract_state_vars(contract)
504
+ actions = _extract_actions(contract)
505
+
506
+ for action_name, action_obj in actions.items():
507
+ report.actions_checked += 1
508
+ body = getattr(action_obj, "body", None)
509
+ if body is None:
510
+ continue
511
+
512
+ self._check_access_control(
513
+ action_name, body, contract_name, state_vars, report
514
+ )
515
+ self._check_reentrancy(action_name, body, contract_name, report)
516
+ self._check_division_by_zero(action_name, body, contract_name, report)
517
+ self._check_overflow(action_name, body, contract_name, report)
518
+ self._check_transfer_balance(action_name, body, contract_name, report)
519
+
520
+ # ── Individual checks ─────────────────────────────────────────
521
+
522
+ def _check_access_control(
523
+ self,
524
+ action_name: str,
525
+ body: Any,
526
+ contract_name: str,
527
+ state_vars: List[str],
528
+ report: VerificationReport,
529
+ ) -> None:
530
+ """Warn if a state-mutating action has no require/caller check."""
531
+ if not _contains_state_write(body):
532
+ return # Read-only action — no concern
533
+ if _contains_require(body) or _contains_caller_check(body):
534
+ return # Has some access control
535
+
536
+ report.findings.append(VerificationFinding(
537
+ category=FindingCategory.ACCESS_CONTROL,
538
+ severity=Severity.HIGH,
539
+ message=(
540
+ f"State-mutating action '{action_name}' has no "
541
+ f"access-control check (require or caller check)."
542
+ ),
543
+ action_name=action_name,
544
+ contract_name=contract_name,
545
+ suggestion="Add `require(TX.caller == owner, \"Unauthorized\");`",
546
+ ))
547
+
548
+ def _check_reentrancy(
549
+ self,
550
+ action_name: str,
551
+ body: Any,
552
+ contract_name: str,
553
+ report: VerificationReport,
554
+ ) -> None:
555
+ """Detect state writes *after* external calls (CEI violation)."""
556
+ nodes = _walk_ast(body)
557
+ saw_external_call = False
558
+
559
+ for n in nodes:
560
+ if _contains_external_call(n) and not saw_external_call:
561
+ saw_external_call = True
562
+ continue
563
+
564
+ if saw_external_call and _contains_state_write(n):
565
+ report.findings.append(VerificationFinding(
566
+ category=FindingCategory.REENTRANCY,
567
+ severity=Severity.CRITICAL,
568
+ message=(
569
+ f"Action '{action_name}' writes state after an "
570
+ f"external call — potential reentrancy vulnerability."
571
+ ),
572
+ action_name=action_name,
573
+ contract_name=contract_name,
574
+ suggestion=(
575
+ "Follow the Checks-Effects-Interactions pattern: "
576
+ "perform all state writes before external calls."
577
+ ),
578
+ ))
579
+ return # One finding per action is sufficient
580
+
581
+ def _check_division_by_zero(
582
+ self,
583
+ action_name: str,
584
+ body: Any,
585
+ contract_name: str,
586
+ report: VerificationReport,
587
+ ) -> None:
588
+ divisions = _find_division_ops(body)
589
+ for div_node in divisions:
590
+ # Check if the divisor is guarded
591
+ right = getattr(div_node, "right", None)
592
+ if right is None:
593
+ continue
594
+ # If divisor is a literal > 0, it's safe
595
+ if hasattr(right, "value"):
596
+ try:
597
+ val = int(right.value) if not isinstance(right.value, float) else right.value
598
+ if val != 0:
599
+ continue
600
+ except (ValueError, TypeError):
601
+ pass
602
+
603
+ report.findings.append(VerificationFinding(
604
+ category=FindingCategory.DIVISION_BY_ZERO,
605
+ severity=Severity.MEDIUM,
606
+ message=(
607
+ f"Action '{action_name}' contains a division that "
608
+ f"may not guard against zero divisor."
609
+ ),
610
+ action_name=action_name,
611
+ contract_name=contract_name,
612
+ suggestion="Add `require(divisor != 0, \"Division by zero\");` before the division.",
613
+ ))
614
+
615
+ def _check_overflow(
616
+ self,
617
+ action_name: str,
618
+ body: Any,
619
+ contract_name: str,
620
+ report: VerificationReport,
621
+ ) -> None:
622
+ """Flag unchecked arithmetic on values that could overflow."""
623
+ arith_ops = _find_arithmetic_ops(body)
624
+ for op_node in arith_ops:
625
+ op = getattr(op_node, "operator", getattr(op_node, "op", ""))
626
+ if op == "**":
627
+ report.findings.append(VerificationFinding(
628
+ category=FindingCategory.OVERFLOW,
629
+ severity=Severity.MEDIUM,
630
+ message=(
631
+ f"Action '{action_name}' uses exponentiation (**) "
632
+ f"which can cause large-number overflow."
633
+ ),
634
+ action_name=action_name,
635
+ contract_name=contract_name,
636
+ suggestion="Consider adding bounds checks or using safe-math utilities.",
637
+ ))
638
+ elif op == "*":
639
+ # Multiplication of two unbounded values
640
+ left = getattr(op_node, "left", None)
641
+ right = getattr(op_node, "right", None)
642
+ # If both sides are non-literal, flag as potential overflow
643
+ left_is_literal = hasattr(left, "value") and isinstance(
644
+ getattr(left, "value", None), (int, float)
645
+ )
646
+ right_is_literal = hasattr(right, "value") and isinstance(
647
+ getattr(right, "value", None), (int, float)
648
+ )
649
+ if not left_is_literal and not right_is_literal:
650
+ report.findings.append(VerificationFinding(
651
+ category=FindingCategory.OVERFLOW,
652
+ severity=Severity.LOW,
653
+ message=(
654
+ f"Action '{action_name}' multiplies two non-constant "
655
+ f"values without overflow protection."
656
+ ),
657
+ action_name=action_name,
658
+ contract_name=contract_name,
659
+ suggestion="Consider safe-math or bounds-checking patterns.",
660
+ ))
661
+
662
+ def _check_transfer_balance(
663
+ self,
664
+ action_name: str,
665
+ body: Any,
666
+ contract_name: str,
667
+ report: VerificationReport,
668
+ ) -> None:
669
+ """Check that transfers are preceded by balance checks."""
670
+ nodes = _walk_ast(body)
671
+ has_transfer = False
672
+ has_balance_check = False
673
+
674
+ for n in nodes:
675
+ nt = _node_type(n)
676
+ if nt == "CallExpression":
677
+ callee = getattr(n, "function", getattr(n, "callee", None))
678
+ name = _get_name(callee) if callee else ""
679
+ if name == "transfer":
680
+ has_transfer = True
681
+ if name == "get_balance":
682
+ has_balance_check = True
683
+ if nt == "RequireStatement":
684
+ has_balance_check = True # Any require is a proxy
685
+
686
+ if has_transfer and not has_balance_check:
687
+ report.findings.append(VerificationFinding(
688
+ category=FindingCategory.UNCHECKED_TRANSFER,
689
+ severity=Severity.HIGH,
690
+ message=(
691
+ f"Action '{action_name}' calls transfer() without "
692
+ f"a preceding balance check."
693
+ ),
694
+ action_name=action_name,
695
+ contract_name=contract_name,
696
+ suggestion="Add `require(balance >= amount, \"Insufficient funds\");`",
697
+ ))
698
+
699
+
700
+ # =====================================================================
701
+ # Invariant Verifier (Level 2)
702
+ # =====================================================================
703
+
704
+ @dataclass
705
+ class Invariant:
706
+ """A declared contract invariant.
707
+
708
+ Example annotation (in Zexus source)::
709
+
710
+ // @invariant total_supply >= 0
711
+ // @invariant balances_sum == total_supply
712
+ """
713
+ expression: str # Human-readable expression
714
+ variable: str = "" # singular alias (some tooling prefers this)
715
+ variables: List[str] = field(default_factory=list)
716
+ parsed: Optional[Any] = None # Internal parsed representation
717
+
718
+ def __post_init__(self) -> None:
719
+ if self.variable and not self.variables:
720
+ self.variables = [self.variable]
721
+
722
+ def to_dict(self) -> Dict[str, Any]:
723
+ return {"expression": self.expression, "variables": self.variables}
724
+
725
+
726
+ class InvariantVerifier:
727
+ """Symbolically verifies that contract invariants are preserved.
728
+
729
+ For each action, the verifier:
730
+ 1. Sets up an initial symbolic state satisfying the invariant.
731
+ 2. Symbolically executes the action body.
732
+ 3. Checks that the invariant still holds in the post-state.
733
+
734
+ If the invariant *could* be violated, it reports a finding with
735
+ a potential counterexample.
736
+ """
737
+
738
+ # Supported comparison operators for invariant expressions
739
+ _CMP_PATTERN = re.compile(
740
+ r"^(\w+)\s*(>=|<=|>|<|==|!=)\s*(.+)$"
741
+ )
742
+
743
+ def verify(
744
+ self,
745
+ contract: Any,
746
+ invariants: List[Invariant],
747
+ report: VerificationReport,
748
+ ) -> None:
749
+ contract_name = _get_name(getattr(contract, "name", "")) or "Unknown"
750
+ state_vars = _extract_state_vars(contract)
751
+ actions = _extract_actions(contract)
752
+
753
+ for inv in invariants:
754
+ report.invariants_checked += 1
755
+ parsed = self._parse_invariant(inv.expression)
756
+ if parsed is None:
757
+ report.findings.append(VerificationFinding(
758
+ category=FindingCategory.INVARIANT_VIOLATION,
759
+ severity=Severity.INFO,
760
+ message=f"Could not parse invariant: {inv.expression}",
761
+ contract_name=contract_name,
762
+ ))
763
+ continue
764
+
765
+ lhs_var, op, rhs = parsed
766
+ inv.parsed = parsed
767
+ inv.variables = [lhs_var] if lhs_var in state_vars else []
768
+
769
+ # Check each action preserves the invariant
770
+ for action_name, action_obj in actions.items():
771
+ body = getattr(action_obj, "body", None)
772
+ if body is None:
773
+ continue
774
+
775
+ violation = self._check_action_preserves(
776
+ action_name, body, lhs_var, op, rhs, state_vars,
777
+ )
778
+ if violation:
779
+ report.findings.append(VerificationFinding(
780
+ category=FindingCategory.INVARIANT_VIOLATION,
781
+ severity=Severity.HIGH,
782
+ message=violation["message"],
783
+ action_name=action_name,
784
+ contract_name=contract_name,
785
+ counterexample=violation.get("counterexample"),
786
+ suggestion=violation.get("suggestion", ""),
787
+ ))
788
+
789
+ def _parse_invariant(
790
+ self, expr: str
791
+ ) -> Optional[Tuple[str, str, str]]:
792
+ """Parse ``var >= 0`` style invariants."""
793
+ m = self._CMP_PATTERN.match(expr.strip())
794
+ if m:
795
+ return m.group(1), m.group(2), m.group(3).strip()
796
+ return None
797
+
798
+ def _check_action_preserves(
799
+ self,
800
+ action_name: str,
801
+ body: Any,
802
+ lhs_var: str,
803
+ op: str,
804
+ rhs: str,
805
+ state_vars: List[str],
806
+ ) -> Optional[Dict[str, Any]]:
807
+ """Check if an action could violate ``lhs_var <op> rhs``.
808
+
809
+ Uses a conservative static analysis: if the action modifies
810
+ ``lhs_var`` and does NOT contain a require/if guard that
811
+ re-establishes the invariant, it is flagged.
812
+ """
813
+ # Collect all assignments to lhs_var in the body
814
+ assigns = self._collect_assignments_to(body, lhs_var)
815
+ if not assigns:
816
+ return None # Action doesn't touch the invariant variable
817
+
818
+ # Check for subtractions that could violate >= 0
819
+ for assign in assigns:
820
+ rhs_expr = getattr(assign, "value", getattr(assign, "right", None))
821
+ if rhs_expr is None:
822
+ continue
823
+
824
+ # Detect patterns like `total_supply = total_supply - amount`
825
+ if self._is_subtraction_from(rhs_expr, lhs_var):
826
+ # Check if there's a preceding guard
827
+ if not _contains_require(body):
828
+ try:
829
+ rhs_val = int(rhs)
830
+ except (ValueError, TypeError):
831
+ rhs_val = 0
832
+
833
+ return {
834
+ "message": (
835
+ f"Action '{action_name}' decrements '{lhs_var}' "
836
+ f"without a require guard; invariant "
837
+ f"'{lhs_var} {op} {rhs}' may be violated."
838
+ ),
839
+ "counterexample": {
840
+ lhs_var: rhs_val,
841
+ "decremented_by": "unknown_amount",
842
+ },
843
+ "suggestion": (
844
+ f"Add `require({lhs_var} >= amount, "
845
+ f"\"Would violate invariant\");`"
846
+ ),
847
+ }
848
+
849
+ # Detect unchecked assignment (could set to anything)
850
+ if self._is_raw_assignment(assign) and not _contains_require(body):
851
+ return {
852
+ "message": (
853
+ f"Action '{action_name}' assigns to '{lhs_var}' "
854
+ f"without ensuring invariant '{lhs_var} {op} {rhs}'."
855
+ ),
856
+ "suggestion": (
857
+ f"Guard the assignment with "
858
+ f"`require(new_value {op} {rhs});`"
859
+ ),
860
+ }
861
+
862
+ return None
863
+
864
+ def _collect_assignments_to(self, body: Any, var_name: str) -> List[Any]:
865
+ """Find all AST nodes that assign to ``var_name``."""
866
+ results = []
867
+ for n in _walk_ast(body):
868
+ nt = _node_type(n)
869
+ if nt in ("AssignmentExpression", "LetStatement", "ConstStatement"):
870
+ target = getattr(n, "name", getattr(n, "left", None))
871
+ if target and _get_name(target) == var_name:
872
+ results.append(n)
873
+ return results
874
+
875
+ def _is_subtraction_from(self, expr: Any, var_name: str) -> bool:
876
+ """Check if ``expr`` is ``var_name - <something>``."""
877
+ nt = _node_type(expr)
878
+ if nt in ("InfixExpression", "BinaryExpression"):
879
+ op = getattr(expr, "operator", getattr(expr, "op", ""))
880
+ left = getattr(expr, "left", None)
881
+ if op == "-" and left and _get_name(left) == var_name:
882
+ return True
883
+ return False
884
+
885
+ def _is_raw_assignment(self, node: Any) -> bool:
886
+ """True if this looks like a direct assignment (not +=, -=)."""
887
+ nt = _node_type(node)
888
+ return nt in ("AssignmentExpression", "LetStatement")
889
+
890
+
891
+ # =====================================================================
892
+ # Property Verifier (Level 3) — Bounded Model Checking
893
+ # =====================================================================
894
+
895
+ @dataclass
896
+ class ContractProperty:
897
+ """A verifiable property with optional pre/post conditions.
898
+
899
+ Declared as annotations::
900
+
901
+ // @property transfer_preserves_total
902
+ // @pre total_supply > 0
903
+ // @post total_supply == @old(total_supply)
904
+ """
905
+ name: str
906
+ description: str = ""
907
+ precondition: str = "" # @pre expression
908
+ postcondition: str = "" # @post expression
909
+ action: str = "" # Specific action, or "" for all
910
+
911
+ def to_dict(self) -> Dict[str, Any]:
912
+ d: Dict[str, Any] = {"name": self.name}
913
+ if self.description:
914
+ d["description"] = self.description
915
+ if self.precondition:
916
+ d["precondition"] = self.precondition
917
+ if self.postcondition:
918
+ d["postcondition"] = self.postcondition
919
+ if self.action:
920
+ d["action"] = self.action
921
+ return d
922
+
923
+ @property
924
+ def action_scope(self) -> str:
925
+ # Backward-compatible alias for older internal name.
926
+ return self.action
927
+
928
+
929
+ class PropertyVerifier:
930
+ """Performs bounded model checking on contract properties.
931
+
932
+ For each property, the verifier:
933
+ 1. Sets up symbolic initial state satisfying the precondition.
934
+ 2. Explores bounded execution paths through the action.
935
+ 3. Verifies the postcondition holds on every path.
936
+
937
+ Bound depth is configurable (default 3 branches — covers most
938
+ single-action paths).
939
+ """
940
+
941
+ def __init__(self, max_depth: int = 3):
942
+ self._max_depth = max_depth
943
+
944
+ def verify(
945
+ self,
946
+ contract: Any,
947
+ properties: List[ContractProperty],
948
+ report: VerificationReport,
949
+ ) -> None:
950
+ contract_name = _get_name(getattr(contract, "name", "")) or "Unknown"
951
+ state_vars = _extract_state_vars(contract)
952
+ actions = _extract_actions(contract)
953
+
954
+ for prop in properties:
955
+ report.properties_checked += 1
956
+ target_actions = (
957
+ {prop.action: actions[prop.action]}
958
+ if prop.action and prop.action in actions
959
+ else actions
960
+ )
961
+
962
+ for action_name, action_obj in target_actions.items():
963
+ body = getattr(action_obj, "body", None)
964
+ if body is None:
965
+ continue
966
+
967
+ violation = self._check_property(
968
+ prop, action_name, body, state_vars
969
+ )
970
+ if violation:
971
+ sev = (
972
+ Severity.CRITICAL
973
+ if "postcondition" in violation.get("kind", "")
974
+ else Severity.HIGH
975
+ )
976
+ report.findings.append(VerificationFinding(
977
+ category=(
978
+ FindingCategory.POSTCONDITION_VIOLATION
979
+ if "postcondition" in violation.get("kind", "")
980
+ else FindingCategory.PRECONDITION_VIOLATION
981
+ ),
982
+ severity=sev,
983
+ message=violation["message"],
984
+ action_name=action_name,
985
+ contract_name=contract_name,
986
+ counterexample=violation.get("counterexample"),
987
+ ))
988
+
989
+ def _check_property(
990
+ self,
991
+ prop: ContractProperty,
992
+ action_name: str,
993
+ body: Any,
994
+ state_vars: List[str],
995
+ ) -> Optional[Dict[str, Any]]:
996
+ """Bounded model check of a single property on an action."""
997
+
998
+ # Build initial symbolic state
999
+ sym_state = SymState()
1000
+ for sv in state_vars:
1001
+ sym_state.set(sv, SymValue(name=sv, sym_type=SymType.INT, min_val=0))
1002
+
1003
+ # Check postcondition patterns
1004
+ if prop.postcondition:
1005
+ return self._verify_postcondition(
1006
+ prop, action_name, body, state_vars, sym_state
1007
+ )
1008
+
1009
+ return None
1010
+
1011
+ def _verify_postcondition(
1012
+ self,
1013
+ prop: ContractProperty,
1014
+ action_name: str,
1015
+ body: Any,
1016
+ state_vars: List[str],
1017
+ sym_state: SymState,
1018
+ ) -> Optional[Dict[str, Any]]:
1019
+ """Check if postcondition holds after action execution.
1020
+
1021
+ Supports ``@old(var)`` references to pre-state values.
1022
+ """
1023
+ post = prop.postcondition.strip()
1024
+
1025
+ # Parse @old references
1026
+ old_vars = set(re.findall(r"@old\((\w+)\)", post))
1027
+
1028
+ # Look for conservation laws like:
1029
+ # total_supply == @old(total_supply)
1030
+ # balances_sum == @old(balances_sum)
1031
+ for old_var in old_vars:
1032
+ if old_var not in state_vars:
1033
+ continue
1034
+
1035
+ # Check if the action modifies this variable
1036
+ assigns = []
1037
+ for n in _walk_ast(body):
1038
+ nt = _node_type(n)
1039
+ if nt in ("AssignmentExpression", "LetStatement"):
1040
+ target = getattr(n, "name", getattr(n, "left", None))
1041
+ if target and _get_name(target) == old_var:
1042
+ assigns.append(n)
1043
+
1044
+ if assigns:
1045
+ # The action modifies a variable that should be conserved
1046
+ # Check if modifications are balanced (e.g. += and -= same amount)
1047
+ has_increment = False
1048
+ has_decrement = False
1049
+ for assign in assigns:
1050
+ rhs_expr = getattr(assign, "value", getattr(assign, "right", None))
1051
+ if rhs_expr:
1052
+ rhs_nt = _node_type(rhs_expr)
1053
+ if rhs_nt in ("InfixExpression", "BinaryExpression"):
1054
+ op = getattr(rhs_expr, "operator", getattr(rhs_expr, "op", ""))
1055
+ if op == "+":
1056
+ has_increment = True
1057
+ elif op == "-":
1058
+ has_decrement = True
1059
+
1060
+ # If only increment or only decrement, postcondition
1061
+ # `x == @old(x)` is potentially violated
1062
+ if has_increment != has_decrement:
1063
+ return {
1064
+ "kind": "postcondition",
1065
+ "message": (
1066
+ f"Property '{prop.name}': action '{action_name}' "
1067
+ f"may violate postcondition '{post}' — "
1068
+ f"'{old_var}' is modified non-symmetrically."
1069
+ ),
1070
+ "counterexample": {
1071
+ old_var: "modified without balanced inverse",
1072
+ },
1073
+ }
1074
+
1075
+ return None
1076
+
1077
+
1078
+ # =====================================================================
1079
+ # Annotation Parser
1080
+ # =====================================================================
1081
+
1082
+ class AnnotationParser:
1083
+ """Extract @invariant, @property, @pre, @post from contract metadata
1084
+ or source comments.
1085
+ """
1086
+
1087
+ _INV_PATTERN = re.compile(r"@invariant\s+(.+)")
1088
+ _PROP_PATTERN = re.compile(r"@property\s+(\w+)(?:\s+(.+))?")
1089
+ _PRE_PATTERN = re.compile(r"@pre\s+(.+)")
1090
+ _POST_PATTERN = re.compile(r"@post\s+(.+)")
1091
+
1092
+ @classmethod
1093
+ def parse_annotations(
1094
+ cls,
1095
+ source: Union[str, List[str]],
1096
+ ) -> Dict[str, Any]:
1097
+ """Parse verification annotations from source text or comments.
1098
+
1099
+ Returns a dict with keys:
1100
+ - ``invariants``: List[str]
1101
+ - ``properties``: List[Dict[str, Any]]
1102
+ - ``preconditions``: List[str]
1103
+ - ``postconditions``: List[str]
1104
+ """
1105
+ lines = source.splitlines() if isinstance(source, str) else source
1106
+ invariants: List[str] = []
1107
+ properties: List[Dict[str, Any]] = []
1108
+ preconditions: List[str] = []
1109
+ postconditions: List[str] = []
1110
+
1111
+ current_prop: Optional[Dict[str, Any]] = None
1112
+
1113
+ for line in lines:
1114
+ stripped = line.strip().lstrip("/").strip()
1115
+
1116
+ # @invariant
1117
+ m = cls._INV_PATTERN.match(stripped)
1118
+ if m:
1119
+ expr = m.group(1).strip()
1120
+ invariants.append(expr)
1121
+ continue
1122
+
1123
+ # @property
1124
+ m = cls._PROP_PATTERN.match(stripped)
1125
+ if m:
1126
+ # Flush previous property
1127
+ if current_prop is not None:
1128
+ properties.append(current_prop)
1129
+ current_prop = {
1130
+ "name": m.group(1),
1131
+ "description": (m.group(2) or "").strip(),
1132
+ "precondition": "",
1133
+ "postcondition": "",
1134
+ "action": "",
1135
+ }
1136
+ continue
1137
+
1138
+ # @pre
1139
+ m = cls._PRE_PATTERN.match(stripped)
1140
+ if m:
1141
+ expr = m.group(1).strip()
1142
+ preconditions.append(expr)
1143
+ if current_prop is not None:
1144
+ current_prop["precondition"] = expr
1145
+ continue
1146
+
1147
+ # @post
1148
+ m = cls._POST_PATTERN.match(stripped)
1149
+ if m:
1150
+ expr = m.group(1).strip()
1151
+ postconditions.append(expr)
1152
+ if current_prop is not None:
1153
+ current_prop["postcondition"] = expr
1154
+ continue
1155
+
1156
+ # Flush last property
1157
+ if current_prop is not None:
1158
+ properties.append(current_prop)
1159
+
1160
+ return {
1161
+ "invariants": invariants,
1162
+ "properties": properties,
1163
+ "preconditions": preconditions,
1164
+ "postconditions": postconditions,
1165
+ }
1166
+
1167
+ @classmethod
1168
+ def from_contract_metadata(
1169
+ cls,
1170
+ contract: Any,
1171
+ ) -> Dict[str, Any]:
1172
+ """Extract annotations from a contract's metadata.
1173
+
1174
+ Supports either dict-like ``blockchain_config`` or an object
1175
+ with a ``verification`` attribute.
1176
+ """
1177
+ meta = getattr(contract, "blockchain_config", {}) or {}
1178
+ if isinstance(meta, dict):
1179
+ verification = meta.get("verification", {})
1180
+ else:
1181
+ verification = getattr(meta, "verification", {})
1182
+ if not isinstance(verification, dict):
1183
+ verification = {}
1184
+
1185
+ invariants = list(verification.get("invariants", []) or [])
1186
+ props_in = list(verification.get("properties", []) or [])
1187
+ properties: List[Dict[str, Any]] = []
1188
+
1189
+ for p in props_in:
1190
+ if isinstance(p, dict):
1191
+ properties.append({
1192
+ "name": p.get("name", ""),
1193
+ "description": p.get("description", ""),
1194
+ "precondition": p.get("precondition", p.get("pre", "")),
1195
+ "postcondition": p.get("postcondition", p.get("post", "")),
1196
+ "action": p.get("action", ""),
1197
+ })
1198
+
1199
+ return {
1200
+ "invariants": invariants,
1201
+ "properties": properties,
1202
+ "preconditions": [],
1203
+ "postconditions": [],
1204
+ }
1205
+
1206
+
1207
+ # =====================================================================
1208
+ # Taint / Data-Flow Analyzer (Level 4)
1209
+ # =====================================================================
1210
+
1211
+ class TaintLabel(str, Enum):
1212
+ """Labels for data-flow taint tracking."""
1213
+ USER_INPUT = "user_input" # from TX.caller, action args
1214
+ EXTERNAL_CALL = "external_call" # return value of cross-contract call
1215
+ STORAGE_READ = "storage_read" # from persistent state
1216
+ ARITHMETIC = "arithmetic" # derived from tainted operands
1217
+
1218
+ def __str__(self) -> str:
1219
+ return self.value
1220
+
1221
+
1222
+ class TaintAnalyzer:
1223
+ """Data-flow taint analysis for smart contracts.
1224
+
1225
+ Tracks how user-controlled (tainted) values propagate through
1226
+ contract actions and flags dangerous sinks:
1227
+
1228
+ * **Tainted storage key** — using user input as a map/storage key
1229
+ without validation can lead to storage collision attacks.
1230
+ * **Tainted transfer amount** — using unsanitized user input as a
1231
+ transfer value enables arbitrary drain attacks.
1232
+ * **Tainted control flow** — branching on external call results
1233
+ without validation enables oracle manipulation.
1234
+ * **Unchecked external return** — calling another contract and
1235
+ ignoring (or not checking) the return value.
1236
+ * **Privilege escalation** — storing user-supplied data into
1237
+ ``owner`` or ``admin`` state variables.
1238
+
1239
+ This operates on the AST — no code is executed.
1240
+ """
1241
+
1242
+ # Variables considered *inherently tainted* (user-controlled)
1243
+ TAINT_SOURCES = frozenset({
1244
+ "TX.caller", "tx.caller", "TX.value", "tx.value",
1245
+ "TX.origin", "tx.origin", "msg.sender", "msg.value",
1246
+ })
1247
+
1248
+ # State variables that must never receive unsanitized user input
1249
+ SENSITIVE_STATE = frozenset({
1250
+ "owner", "admin", "authority", "minter", "pauser",
1251
+ "operator", "governance",
1252
+ })
1253
+
1254
+ # Sink functions where tainted values are dangerous
1255
+ DANGEROUS_SINKS = frozenset({
1256
+ "transfer", "send", "call", "delegatecall",
1257
+ "selfdestruct", "suicide",
1258
+ })
1259
+
1260
+ def verify(
1261
+ self,
1262
+ contract: Any,
1263
+ report: VerificationReport,
1264
+ ) -> None:
1265
+ contract_name = _get_name(getattr(contract, "name", "")) or "Unknown"
1266
+ actions = _extract_actions(contract)
1267
+
1268
+ for action_name, action_obj in actions.items():
1269
+ body = getattr(action_obj, "body", None)
1270
+ if body is None:
1271
+ continue
1272
+
1273
+ # Build taint set: variables known to carry user-controlled data
1274
+ tainted: Set[str] = set()
1275
+
1276
+ # Action parameters are tainted (they come from the caller)
1277
+ params = getattr(action_obj, "params", getattr(action_obj, "parameters", []))
1278
+ if isinstance(params, (list, tuple)):
1279
+ for p in params:
1280
+ name = _get_name(p) if not isinstance(p, str) else p
1281
+ if name:
1282
+ tainted.add(name)
1283
+
1284
+ # Walk the AST and propagate taint
1285
+ nodes = _walk_ast(body)
1286
+ for node in nodes:
1287
+ nt = _node_type(node)
1288
+
1289
+ # -- Assignments: propagate taint through data flow --
1290
+ if nt in ("AssignmentExpression", "AssignmentStatement", "LetStatement", "VarDeclaration"):
1291
+ target = _get_name(getattr(node, "name", getattr(node, "left", getattr(node, "target", None))))
1292
+ value = getattr(node, "value", getattr(node, "right", None))
1293
+ if target and self._is_tainted_expr(value, tainted):
1294
+ tainted.add(target)
1295
+
1296
+ # Check: assigning tainted value to sensitive state
1297
+ if target.lower() in self.SENSITIVE_STATE:
1298
+ report.findings.append(VerificationFinding(
1299
+ category=FindingCategory.PRIVILEGE_ESCALATION,
1300
+ severity=Severity.CRITICAL,
1301
+ message=(
1302
+ f"Action '{action_name}' assigns user-controlled "
1303
+ f"data to sensitive state variable '{target}'."
1304
+ ),
1305
+ action_name=action_name,
1306
+ contract_name=contract_name,
1307
+ suggestion=(
1308
+ f"Validate the value before assigning to '{target}', "
1309
+ f"or restrict this action to authorised callers only."
1310
+ ),
1311
+ ))
1312
+
1313
+ # -- Call expressions: check dangerous sinks --
1314
+ if nt == "CallExpression":
1315
+ callee = getattr(node, "function", getattr(node, "callee", None))
1316
+ name = _get_name(callee) if callee else ""
1317
+
1318
+ if name.lower() in self.DANGEROUS_SINKS:
1319
+ args = getattr(node, "arguments", getattr(node, "args", []))
1320
+ if isinstance(args, (list, tuple)):
1321
+ for arg in args:
1322
+ if self._is_tainted_expr(arg, tainted):
1323
+ report.findings.append(VerificationFinding(
1324
+ category=FindingCategory.UNSANITIZED_INPUT,
1325
+ severity=Severity.HIGH,
1326
+ message=(
1327
+ f"Action '{action_name}' passes user-controlled "
1328
+ f"data to dangerous sink '{name}()' without sanitization."
1329
+ ),
1330
+ action_name=action_name,
1331
+ contract_name=contract_name,
1332
+ suggestion=(
1333
+ f"Validate/bound the argument before calling '{name}()'."
1334
+ ),
1335
+ ))
1336
+ break # one finding per call
1337
+
1338
+ # -- Storage writes with tainted keys --
1339
+ if nt in ("IndexExpression", "MemberExpression"):
1340
+ idx = getattr(node, "index", getattr(node, "property", None))
1341
+ if idx and self._is_tainted_expr(idx, tainted):
1342
+ parent = getattr(node, "object", None)
1343
+ parent_name = _get_name(parent) if parent else ""
1344
+ report.findings.append(VerificationFinding(
1345
+ category=FindingCategory.TAINTED_STORAGE_KEY,
1346
+ severity=Severity.MEDIUM,
1347
+ message=(
1348
+ f"Action '{action_name}' uses user-controlled data "
1349
+ f"as a storage/map key on '{parent_name}' — "
1350
+ f"potential storage collision."
1351
+ ),
1352
+ action_name=action_name,
1353
+ contract_name=contract_name,
1354
+ suggestion="Hash or validate user input before using as storage key.",
1355
+ ))
1356
+
1357
+ # -- Check for unchecked external call returns --
1358
+ self._check_unchecked_returns(action_name, body, contract_name, report)
1359
+
1360
+ def _is_tainted_expr(self, node: Any, tainted: Set[str]) -> bool:
1361
+ """Return True if the expression is or derives from tainted data."""
1362
+ if node is None:
1363
+ return False
1364
+
1365
+ # Direct variable reference
1366
+ name = _get_name(node)
1367
+ if name:
1368
+ if name in tainted:
1369
+ return True
1370
+ if name in self.TAINT_SOURCES:
1371
+ return True
1372
+
1373
+ # Member access (TX.caller, etc.)
1374
+ nt = _node_type(node)
1375
+ if nt == "MemberExpression":
1376
+ obj_name = _get_name(getattr(node, "object", None))
1377
+ prop_name = _get_name(getattr(node, "property", None))
1378
+ full = f"{obj_name}.{prop_name}" if obj_name and prop_name else ""
1379
+ if full in self.TAINT_SOURCES:
1380
+ return True
1381
+
1382
+ # Binary expression: tainted if either operand is tainted
1383
+ if nt == "BinaryExpression":
1384
+ left = getattr(node, "left", None)
1385
+ right = getattr(node, "right", None)
1386
+ if self._is_tainted_expr(left, tainted) or self._is_tainted_expr(right, tainted):
1387
+ return True
1388
+
1389
+ # Call expression return value
1390
+ if nt == "CallExpression":
1391
+ callee = getattr(node, "function", getattr(node, "callee", None))
1392
+ callee_name = _get_name(callee) if callee else ""
1393
+ # External calls return tainted data
1394
+ if callee_name.lower() in ("call", "delegatecall", "staticcall"):
1395
+ return True
1396
+
1397
+ return False
1398
+
1399
+ def _check_unchecked_returns(
1400
+ self,
1401
+ action_name: str,
1402
+ body: Any,
1403
+ contract_name: str,
1404
+ report: VerificationReport,
1405
+ ) -> None:
1406
+ """Flag external calls whose return value is not assigned or checked."""
1407
+ nodes = _walk_ast(body)
1408
+ for node in nodes:
1409
+ nt = _node_type(node)
1410
+ if nt != "ExpressionStatement":
1411
+ continue
1412
+ expr = getattr(node, "expression", node)
1413
+ if _node_type(expr) == "CallExpression":
1414
+ callee = getattr(expr, "function", getattr(expr, "callee", None))
1415
+ name = _get_name(callee) if callee else ""
1416
+ if name.lower() in ("call", "delegatecall", "staticcall", "send"):
1417
+ report.findings.append(VerificationFinding(
1418
+ category=FindingCategory.UNCHECKED_RETURN,
1419
+ severity=Severity.HIGH,
1420
+ message=(
1421
+ f"Action '{action_name}' calls '{name}()' "
1422
+ f"without checking its return value."
1423
+ ),
1424
+ action_name=action_name,
1425
+ contract_name=contract_name,
1426
+ suggestion=(
1427
+ f"Assign the return value and check for success: "
1428
+ f"`let result = {name}(...); require(result, \"call failed\");`"
1429
+ ),
1430
+ ))
1431
+
1432
+
1433
+ # =====================================================================
1434
+ # Main Verifier
1435
+ # =====================================================================
1436
+
1437
+ class FormalVerifier:
1438
+ """Unified entry point for all verification levels.
1439
+
1440
+ Parameters
1441
+ ----------
1442
+ level : VerificationLevel
1443
+ How deep to verify.
1444
+ annotations : str or list of str, optional
1445
+ Source text containing ``@invariant`` / ``@property`` annotations.
1446
+ invariants : list of Invariant, optional
1447
+ Pre-parsed invariants (overrides parsed annotations).
1448
+ properties : list of ContractProperty, optional
1449
+ Pre-parsed properties (overrides parsed annotations).
1450
+ """
1451
+
1452
+ def __init__(
1453
+ self,
1454
+ level: VerificationLevel = VerificationLevel.STRUCTURAL,
1455
+ annotations: Optional[Union[str, List[str], Dict[str, Any]]] = None,
1456
+ invariants: Optional[List[Invariant]] = None,
1457
+ properties: Optional[List[ContractProperty]] = None,
1458
+ max_depth: int = 3,
1459
+ ):
1460
+ self.level = level
1461
+ self._structural = StructuralVerifier()
1462
+ self._invariant_v = InvariantVerifier()
1463
+ self._property_v = PropertyVerifier(max_depth=max_depth)
1464
+ self._taint = TaintAnalyzer()
1465
+
1466
+ parsed_invariants: List[Invariant] = []
1467
+ parsed_properties: List[ContractProperty] = []
1468
+
1469
+ if isinstance(annotations, dict):
1470
+ for inv_expr in (annotations.get("invariants") or []):
1471
+ if isinstance(inv_expr, str):
1472
+ inv = Invariant(expression=inv_expr)
1473
+ inv.variables = re.findall(r"\b([a-zA-Z_]\w*)\b", inv_expr)
1474
+ parsed_invariants.append(inv)
1475
+ for prop in (annotations.get("properties") or []):
1476
+ if isinstance(prop, dict):
1477
+ parsed_properties.append(ContractProperty(
1478
+ name=prop.get("name", ""),
1479
+ description=prop.get("description", ""),
1480
+ precondition=prop.get("precondition", ""),
1481
+ postcondition=prop.get("postcondition", ""),
1482
+ action=prop.get("action", ""),
1483
+ ))
1484
+ elif annotations is not None:
1485
+ parsed = AnnotationParser.parse_annotations(annotations)
1486
+ for inv_expr in (parsed.get("invariants") or []):
1487
+ inv = Invariant(expression=inv_expr)
1488
+ inv.variables = re.findall(r"\b([a-zA-Z_]\w*)\b", inv_expr)
1489
+ parsed_invariants.append(inv)
1490
+ for prop in (parsed.get("properties") or []):
1491
+ if isinstance(prop, dict):
1492
+ parsed_properties.append(ContractProperty(
1493
+ name=prop.get("name", ""),
1494
+ description=prop.get("description", ""),
1495
+ precondition=prop.get("precondition", ""),
1496
+ postcondition=prop.get("postcondition", ""),
1497
+ action=prop.get("action", ""),
1498
+ ))
1499
+
1500
+ self._invariants = invariants if invariants is not None else parsed_invariants
1501
+ self._properties = properties if properties is not None else parsed_properties
1502
+
1503
+ def verify_contract(
1504
+ self,
1505
+ contract: Any,
1506
+ extra_invariants: Optional[List[Invariant]] = None,
1507
+ extra_properties: Optional[List[ContractProperty]] = None,
1508
+ ) -> VerificationReport:
1509
+ """Run all applicable verification passes on a contract.
1510
+
1511
+ Parameters
1512
+ ----------
1513
+ contract :
1514
+ A ``SmartContract`` instance or AST ``ContractStatement``.
1515
+ extra_invariants :
1516
+ Additional invariants to check beyond those in annotations.
1517
+ extra_properties :
1518
+ Additional properties.
1519
+
1520
+ Returns a ``VerificationReport``.
1521
+ """
1522
+ report = VerificationReport(
1523
+ contract_name=_get_name(getattr(contract, "name", "")),
1524
+ level=self.level,
1525
+ )
1526
+
1527
+ # Also try extracting annotations from contract metadata
1528
+ meta = AnnotationParser.from_contract_metadata(contract)
1529
+ meta_inv: List[Invariant] = []
1530
+ meta_prop: List[ContractProperty] = []
1531
+ for inv_expr in (meta.get("invariants") or []):
1532
+ if isinstance(inv_expr, str):
1533
+ inv = Invariant(expression=inv_expr)
1534
+ inv.variables = re.findall(r"\b([a-zA-Z_]\w*)\b", inv_expr)
1535
+ meta_inv.append(inv)
1536
+ for prop in (meta.get("properties") or []):
1537
+ if isinstance(prop, dict):
1538
+ meta_prop.append(ContractProperty(
1539
+ name=prop.get("name", ""),
1540
+ description=prop.get("description", ""),
1541
+ precondition=prop.get("precondition", ""),
1542
+ postcondition=prop.get("postcondition", ""),
1543
+ action=prop.get("action", ""),
1544
+ ))
1545
+
1546
+ all_inv = list(self._invariants) + (extra_invariants or []) + meta_inv
1547
+ all_prop = list(self._properties) + (extra_properties or []) + meta_prop
1548
+
1549
+ # Level 1: structural
1550
+ if self.level >= VerificationLevel.STRUCTURAL:
1551
+ self._structural.verify(contract, report)
1552
+
1553
+ # Level 2: invariant
1554
+ if self.level >= VerificationLevel.INVARIANT and all_inv:
1555
+ self._invariant_v.verify(contract, all_inv, report)
1556
+
1557
+ # Level 3: property
1558
+ if self.level >= VerificationLevel.PROPERTY and all_prop:
1559
+ self._property_v.verify(contract, all_prop, report)
1560
+
1561
+ # Level 4: taint / data-flow analysis
1562
+ if self.level >= VerificationLevel.TAINT:
1563
+ self._taint.verify(contract, report)
1564
+
1565
+ report.finished_at = time.time()
1566
+ return report
1567
+
1568
+ def verify_multiple(
1569
+ self,
1570
+ contracts: List[Any],
1571
+ ) -> List[VerificationReport]:
1572
+ """Verify a list of contracts and return all reports."""
1573
+ return [self.verify_contract(c) for c in contracts]
1574
+
1575
+ def add_invariant(self, expression: str) -> None:
1576
+ inv = Invariant(expression=expression)
1577
+ inv.variables = re.findall(r"\b([a-zA-Z_]\w*)\b", expression)
1578
+ self._invariants.append(inv)
1579
+
1580
+ def add_property(
1581
+ self,
1582
+ name: str,
1583
+ precondition: str = "",
1584
+ postcondition: str = "",
1585
+ action: str = "",
1586
+ description: str = "",
1587
+ ) -> None:
1588
+ self._properties.append(ContractProperty(
1589
+ name=name,
1590
+ precondition=precondition,
1591
+ postcondition=postcondition,
1592
+ action=action,
1593
+ description=description,
1594
+ ))
1595
+
1596
+ @property
1597
+ def invariant_count(self) -> int:
1598
+ return len(self._invariants)
1599
+
1600
+ @property
1601
+ def property_count(self) -> int:
1602
+ return len(self._properties)