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.
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/__init__.py +7 -0
- package/src/zexus/__init__.py +1 -1
- package/src/zexus/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/capability_system.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/debug_sanitizer.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/environment.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/error_reporter.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/input_validation.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/lexer.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/module_cache.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/module_manager.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/object.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/security.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/security_enforcement.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/syntax_validator.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/zexus_ast.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/zexus_token.cpython-312.pyc +0 -0
- package/src/zexus/access_control_system/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/access_control_system/__pycache__/access_control.cpython-312.pyc +0 -0
- package/src/zexus/advanced_types.py +17 -2
- package/src/zexus/blockchain/__init__.py +411 -0
- package/src/zexus/blockchain/accelerator.py +1160 -0
- package/src/zexus/blockchain/chain.py +660 -0
- package/src/zexus/blockchain/consensus.py +821 -0
- package/src/zexus/blockchain/contract_vm.py +1019 -0
- package/src/zexus/blockchain/crypto.py +79 -14
- package/src/zexus/blockchain/events.py +526 -0
- package/src/zexus/blockchain/loadtest.py +721 -0
- package/src/zexus/blockchain/monitoring.py +350 -0
- package/src/zexus/blockchain/mpt.py +716 -0
- package/src/zexus/blockchain/multichain.py +951 -0
- package/src/zexus/blockchain/multiprocess_executor.py +338 -0
- package/src/zexus/blockchain/network.py +886 -0
- package/src/zexus/blockchain/node.py +666 -0
- package/src/zexus/blockchain/rpc.py +1203 -0
- package/src/zexus/blockchain/rust_bridge.py +421 -0
- package/src/zexus/blockchain/storage.py +423 -0
- package/src/zexus/blockchain/tokens.py +750 -0
- package/src/zexus/blockchain/upgradeable.py +1004 -0
- package/src/zexus/blockchain/verification.py +1602 -0
- package/src/zexus/blockchain/wallet.py +621 -0
- package/src/zexus/cli/__pycache__/main.cpython-312.pyc +0 -0
- package/src/zexus/cli/main.py +300 -20
- package/src/zexus/cli/zpm.py +1 -1
- package/src/zexus/compiler/__pycache__/bytecode.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/lexer.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/parser.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/semantic.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/zexus_ast.cpython-312.pyc +0 -0
- package/src/zexus/compiler/lexer.py +10 -5
- package/src/zexus/concurrency_system.py +79 -0
- package/src/zexus/config.py +54 -0
- package/src/zexus/crypto_bridge.py +244 -8
- package/src/zexus/dap/__init__.py +10 -0
- package/src/zexus/dap/__main__.py +4 -0
- package/src/zexus/dap/dap_server.py +391 -0
- package/src/zexus/dap/debug_engine.py +298 -0
- package/src/zexus/environment.py +10 -1
- package/src/zexus/evaluator/__pycache__/bytecode_compiler.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/core.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/expressions.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/functions.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/resource_limiter.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/statements.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/unified_execution.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/utils.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/bytecode_compiler.py +441 -37
- package/src/zexus/evaluator/core.py +560 -49
- package/src/zexus/evaluator/expressions.py +122 -49
- package/src/zexus/evaluator/functions.py +417 -16
- package/src/zexus/evaluator/statements.py +521 -118
- package/src/zexus/evaluator/unified_execution.py +573 -72
- package/src/zexus/evaluator/utils.py +14 -2
- package/src/zexus/event_loop.py +186 -0
- package/src/zexus/lexer.py +742 -486
- package/src/zexus/lsp/__init__.py +1 -1
- package/src/zexus/lsp/definition_provider.py +163 -9
- package/src/zexus/lsp/server.py +22 -8
- package/src/zexus/lsp/symbol_provider.py +182 -9
- package/src/zexus/module_cache.py +237 -9
- package/src/zexus/object.py +64 -6
- package/src/zexus/parser/__pycache__/parser.cpython-312.pyc +0 -0
- package/src/zexus/parser/__pycache__/strategy_context.cpython-312.pyc +0 -0
- package/src/zexus/parser/__pycache__/strategy_structural.cpython-312.pyc +0 -0
- package/src/zexus/parser/parser.py +786 -285
- package/src/zexus/parser/strategy_context.py +407 -66
- package/src/zexus/parser/strategy_structural.py +117 -19
- package/src/zexus/persistence.py +15 -1
- package/src/zexus/renderer/__init__.py +15 -0
- package/src/zexus/renderer/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/backend.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/canvas.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/color_system.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/layout.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/main_renderer.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/painter.cpython-312.pyc +0 -0
- package/src/zexus/renderer/tk_backend.py +208 -0
- package/src/zexus/renderer/web_backend.py +260 -0
- package/src/zexus/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/runtime/__pycache__/async_runtime.cpython-312.pyc +0 -0
- package/src/zexus/runtime/__pycache__/load_manager.cpython-312.pyc +0 -0
- package/src/zexus/runtime/file_flags.py +137 -0
- package/src/zexus/safety/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/safety/__pycache__/memory_safety.cpython-312.pyc +0 -0
- package/src/zexus/security.py +424 -34
- package/src/zexus/stdlib/fs.py +23 -18
- package/src/zexus/stdlib/http.py +289 -186
- package/src/zexus/stdlib/sockets.py +207 -163
- package/src/zexus/stdlib/websockets.py +282 -0
- package/src/zexus/stdlib_integration.py +369 -2
- package/src/zexus/strategy_recovery.py +6 -3
- package/src/zexus/type_checker.py +423 -0
- package/src/zexus/virtual_filesystem.py +189 -2
- package/src/zexus/vm/__init__.py +113 -3
- package/src/zexus/vm/__pycache__/async_optimizer.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/bytecode.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/bytecode_converter.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/cache.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/compiler.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/gas_metering.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/jit.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/parallel_vm.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/vm.cpython-312.pyc +0 -0
- package/src/zexus/vm/async_optimizer.py +14 -1
- package/src/zexus/vm/binary_bytecode.py +659 -0
- package/src/zexus/vm/bytecode.py +28 -1
- package/src/zexus/vm/bytecode_converter.py +26 -12
- package/src/zexus/vm/cabi.c +1985 -0
- package/src/zexus/vm/cabi.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/cabi.h +127 -0
- package/src/zexus/vm/cache.py +557 -17
- package/src/zexus/vm/compiler.py +703 -5
- package/src/zexus/vm/fastops.c +15743 -0
- package/src/zexus/vm/fastops.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/fastops.pyx +288 -0
- package/src/zexus/vm/gas_metering.py +50 -9
- package/src/zexus/vm/jit.py +83 -2
- package/src/zexus/vm/native_jit_backend.py +1816 -0
- package/src/zexus/vm/native_runtime.cpp +1388 -0
- package/src/zexus/vm/native_runtime.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/optimizer.py +161 -11
- package/src/zexus/vm/parallel_vm.py +118 -42
- package/src/zexus/vm/peephole_optimizer.py +82 -4
- package/src/zexus/vm/profiler.py +38 -18
- package/src/zexus/vm/register_allocator.py +16 -5
- package/src/zexus/vm/register_vm.py +8 -5
- package/src/zexus/vm/vm.py +3411 -573
- package/src/zexus/vm/wasm_compiler.py +658 -0
- package/src/zexus/zexus_ast.py +63 -11
- package/src/zexus/zexus_token.py +13 -5
- package/src/zexus/zpm/installer.py +55 -15
- package/src/zexus/zpm/package_manager.py +1 -1
- package/src/zexus/zpm/registry.py +257 -28
- package/src/zexus.egg-info/PKG-INFO +7 -4
- package/src/zexus.egg-info/SOURCES.txt +116 -9
- package/src/zexus.egg-info/entry_points.txt +1 -0
- 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)
|