certora-cli-beta-mirror 7.28.0__py3-none-any.whl → 8.5.0__py3-none-any.whl
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.
- certora_cli/CertoraProver/Compiler/CompilerCollectorFactory.py +10 -3
- certora_cli/CertoraProver/Compiler/CompilerCollectorVy.py +51 -16
- certora_cli/CertoraProver/Compiler/CompilerCollectorYul.py +3 -0
- certora_cli/CertoraProver/castingInstrumenter.py +192 -0
- certora_cli/CertoraProver/certoraApp.py +52 -0
- certora_cli/CertoraProver/certoraBuild.py +694 -207
- certora_cli/CertoraProver/certoraBuildCacheManager.py +21 -17
- certora_cli/CertoraProver/certoraBuildDataClasses.py +8 -2
- certora_cli/CertoraProver/certoraBuildRust.py +88 -54
- certora_cli/CertoraProver/certoraBuildSui.py +112 -0
- certora_cli/CertoraProver/certoraCloudIO.py +97 -96
- certora_cli/CertoraProver/certoraCollectConfigurationLayout.py +230 -84
- certora_cli/CertoraProver/certoraCollectRunMetadata.py +52 -6
- certora_cli/CertoraProver/certoraCompilerParameters.py +11 -0
- certora_cli/CertoraProver/certoraConfigIO.py +43 -35
- certora_cli/CertoraProver/certoraContext.py +128 -54
- certora_cli/CertoraProver/certoraContextAttributes.py +415 -234
- certora_cli/CertoraProver/certoraContextValidator.py +152 -105
- certora_cli/CertoraProver/certoraContractFuncs.py +34 -1
- certora_cli/CertoraProver/certoraParseBuildScript.py +8 -10
- certora_cli/CertoraProver/certoraType.py +10 -1
- certora_cli/CertoraProver/certoraVerifyGenerator.py +22 -4
- certora_cli/CertoraProver/erc7201.py +45 -0
- certora_cli/CertoraProver/splitRules.py +23 -18
- certora_cli/CertoraProver/storageExtension.py +351 -0
- certora_cli/EquivalenceCheck/Eq_default.conf +0 -1
- certora_cli/EquivalenceCheck/Eq_sanity.conf +0 -1
- certora_cli/EquivalenceCheck/equivCheck.py +2 -1
- certora_cli/Mutate/mutateApp.py +41 -22
- certora_cli/Mutate/mutateAttributes.py +11 -0
- certora_cli/Mutate/mutateValidate.py +42 -2
- certora_cli/Shared/certoraAttrUtil.py +21 -5
- certora_cli/Shared/certoraUtils.py +180 -60
- certora_cli/Shared/certoraValidateFuncs.py +68 -26
- certora_cli/Shared/proverCommon.py +308 -0
- certora_cli/certoraCVLFormatter.py +76 -0
- certora_cli/certoraConcord.py +39 -0
- certora_cli/certoraEVMProver.py +4 -3
- certora_cli/certoraRanger.py +39 -0
- certora_cli/certoraRun.py +83 -223
- certora_cli/certoraSolanaProver.py +40 -128
- certora_cli/certoraSorobanProver.py +59 -4
- certora_cli/certoraSuiProver.py +93 -0
- certora_cli_beta_mirror-8.5.0.dist-info/LICENSE +15 -0
- {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-8.5.0.dist-info}/METADATA +21 -5
- certora_cli_beta_mirror-8.5.0.dist-info/RECORD +81 -0
- {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-8.5.0.dist-info}/WHEEL +1 -1
- {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-8.5.0.dist-info}/entry_points.txt +3 -0
- certora_jars/ASTExtraction.jar +0 -0
- certora_jars/CERTORA-CLI-VERSION-METADATA.json +1 -1
- certora_jars/Typechecker.jar +0 -0
- certora_cli_beta_mirror-7.28.0.dist-info/LICENSE +0 -22
- certora_cli_beta_mirror-7.28.0.dist-info/RECORD +0 -70
- {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-8.5.0.dist-info}/top_level.txt +0 -0
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
# You should have received a copy of the GNU General Public License
|
|
14
14
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
15
|
|
|
16
|
+
|
|
16
17
|
import logging
|
|
17
18
|
import os
|
|
18
19
|
import re
|
|
@@ -21,7 +22,8 @@ import itertools
|
|
|
21
22
|
import tempfile
|
|
22
23
|
import fnmatch
|
|
23
24
|
from pathlib import Path
|
|
24
|
-
from
|
|
25
|
+
from wcmatch import glob
|
|
26
|
+
from typing import Dict, List, Tuple, Set, Any, Union, OrderedDict
|
|
25
27
|
|
|
26
28
|
import CertoraProver.certoraContext as Ctx
|
|
27
29
|
import CertoraProver.certoraContextAttributes as Attrs
|
|
@@ -30,6 +32,7 @@ from CertoraProver.certoraContextClass import CertoraContext
|
|
|
30
32
|
from Shared import certoraUtils as Util
|
|
31
33
|
from Shared import certoraAttrUtil as AttrUtil
|
|
32
34
|
from CertoraProver.certoraProjectScanner import scan_project
|
|
35
|
+
import CertoraProver.certoraApp as App
|
|
33
36
|
|
|
34
37
|
scripts_dir_path = Path(__file__).parent.resolve() # containing directory
|
|
35
38
|
sys.path.insert(0, str(scripts_dir_path))
|
|
@@ -43,9 +46,60 @@ class CertoraContextValidator:
|
|
|
43
46
|
def __init__(self, context: CertoraContext):
|
|
44
47
|
self.context = context
|
|
45
48
|
|
|
49
|
+
def handle_concord_attrs(self) -> None:
|
|
50
|
+
if self.context.app == App.ConcordApp:
|
|
51
|
+
for attr in (Attrs.ConcordAttributes.unsupported_attributes()):
|
|
52
|
+
attr_name = attr.get_conf_key()
|
|
53
|
+
if getattr(self.context, attr_name):
|
|
54
|
+
if attr.arg_type == AttrUtil.AttrArgType.BOOLEAN:
|
|
55
|
+
setattr(self.context, attr_name, False)
|
|
56
|
+
else:
|
|
57
|
+
setattr(self.context, attr_name, None)
|
|
58
|
+
raise Util.CertoraUserInputError(f"Concord does not support {attr_name}")
|
|
59
|
+
if not self.context.check_method:
|
|
60
|
+
raise Util.CertoraUserInputError("'check_method' attribute/flag is mandatory when running Concord")
|
|
61
|
+
contracts = getattr(self.context, 'contracts', None)
|
|
62
|
+
if not contracts:
|
|
63
|
+
raise Util.CertoraUserInputError("No contracts were specified for Concord")
|
|
64
|
+
if len(contracts) != 2:
|
|
65
|
+
raise Util.CertoraUserInputError(f"expecting 2 contracts for Concord, got {len(contracts)}: {contracts}")
|
|
66
|
+
|
|
67
|
+
def handle_ranger_attrs(self) -> None:
|
|
68
|
+
# unset unsupported attributes
|
|
69
|
+
if self.context.app == App.RangerApp:
|
|
70
|
+
for attr in Attrs.RangerAttributes.unsupported_attributes():
|
|
71
|
+
attr_name = attr.get_conf_key()
|
|
72
|
+
if getattr(self.context, attr_name):
|
|
73
|
+
if attr.arg_type == AttrUtil.AttrArgType.BOOLEAN:
|
|
74
|
+
setattr(self.context, attr_name, False)
|
|
75
|
+
else:
|
|
76
|
+
setattr(self.context, attr_name, None)
|
|
77
|
+
validation_logger.info(f"Ranger does not support {attr_name}, ignoring this attribute")
|
|
78
|
+
|
|
79
|
+
# setting the default Ranger attributes
|
|
80
|
+
|
|
81
|
+
self.context.range = self.context.range or Util.DEFAULT_RANGER_RANGE
|
|
82
|
+
self.context.ranger_failure_limit = self.context.ranger_failure_limit or Util.DEFAULT_RANGER_FAILURE_LIMIT
|
|
83
|
+
if self.context.loop_iter and self.context.loop_iter != Util.DEFAULT_RANGER_LOOP_ITER:
|
|
84
|
+
validation_logger.info(f"While running Ranger, loop iter is {Util.DEFAULT_RANGER_LOOP_ITER} "
|
|
85
|
+
f"ignoring the set value of {self.context.loop_iter}")
|
|
86
|
+
self.context.loop_iter = self.context.loop_iter or Util.DEFAULT_RANGER_LOOP_ITER
|
|
87
|
+
|
|
88
|
+
for attr in Attrs.RangerAttributes.true_by_default_attributes():
|
|
89
|
+
attr_name = attr.get_conf_key()
|
|
90
|
+
setattr(self.context, attr_name, True)
|
|
91
|
+
|
|
92
|
+
else:
|
|
93
|
+
if self.context.range:
|
|
94
|
+
self.context.range = None
|
|
95
|
+
validation_logger.info("the 'range' attribute is ignored when not running from the Ranger App")
|
|
96
|
+
if self.context.ranger_failure_limit:
|
|
97
|
+
self.context.ranger_failure_limit = None
|
|
98
|
+
validation_logger.info("the 'ranger_failure_limit' is ignored when not running from the Ranger App")
|
|
99
|
+
|
|
46
100
|
def validate(self) -> None:
|
|
47
101
|
|
|
48
|
-
for attr_def in
|
|
102
|
+
for attr_def in self.context.app.attr_class.attribute_list():
|
|
49
103
|
conf_key = attr_def.get_conf_key()
|
|
50
104
|
attr = getattr(self.context, conf_key, None)
|
|
51
105
|
if attr_def.deprecation_msg and attr:
|
|
@@ -106,9 +160,9 @@ class CertoraContextValidator:
|
|
|
106
160
|
|
|
107
161
|
if value is None:
|
|
108
162
|
raise RuntimeError(f"calling validate_type_string with null value {conf_key}")
|
|
109
|
-
if not isinstance(value, str):
|
|
110
|
-
raise Util.CertoraUserInputError(f"value of {conf_key} {value} is not a string")
|
|
111
|
-
attr.validate_value(value)
|
|
163
|
+
if not isinstance(value, (str, int)):
|
|
164
|
+
raise Util.CertoraUserInputError(f"value of {conf_key} {value} is not a string or an integer")
|
|
165
|
+
attr.validate_value(str(value))
|
|
112
166
|
|
|
113
167
|
def validate_type_boolean(self, attr: AttrUtil.AttributeDefinition) -> None:
|
|
114
168
|
conf_key, value = self.__get_key_and_value(attr)
|
|
@@ -121,6 +175,20 @@ class CertoraContextValidator:
|
|
|
121
175
|
elif value not in [True, False]:
|
|
122
176
|
raise Util.CertoraUserInputError(f"value of {conf_key} {value} is not a boolean (true/false)")
|
|
123
177
|
|
|
178
|
+
def check_rust_args_post_argparse(self) -> None:
|
|
179
|
+
context = self.context
|
|
180
|
+
if context.files:
|
|
181
|
+
if context.build_script:
|
|
182
|
+
raise Util.CertoraUserInputError("'files' and 'build_script' cannot be both set for Rust projects")
|
|
183
|
+
if len(context.files) > 1:
|
|
184
|
+
raise Util.CertoraUserInputError("Rust projects must specify exactly one executable in 'files'.")
|
|
185
|
+
|
|
186
|
+
if context.url_visibility and context.local:
|
|
187
|
+
validation_logger.info("'url_visibility' has no effect in local tool runs")
|
|
188
|
+
set_attr_default(context, attr_name=Attrs.CommonAttributes.URL_VISIBILITY.name.lower(),
|
|
189
|
+
ci_value=str(Vf.UrlVisibilityOptions.PUBLIC),
|
|
190
|
+
default_value=str(Vf.UrlVisibilityOptions.PRIVATE))
|
|
191
|
+
|
|
124
192
|
def check_args_post_argparse(self) -> None:
|
|
125
193
|
"""
|
|
126
194
|
Performs checks over the arguments after basic argparse parsing
|
|
@@ -158,7 +226,7 @@ class CertoraContextValidator:
|
|
|
158
226
|
check_contract_name_arg_inputs(context) # Here context.contracts is set
|
|
159
227
|
Util.check_packages_arguments(context)
|
|
160
228
|
convert_to_compiler_map(context)
|
|
161
|
-
if
|
|
229
|
+
if Ctx.is_evm_app_class(context):
|
|
162
230
|
if context.compiler_map is None and context.bytecode_jsons is None: # xxx - we would need to reiterate here
|
|
163
231
|
# once we allow combining raw bytecode with Solidity
|
|
164
232
|
context.solc = Vf.is_solc_file_valid(context.solc)
|
|
@@ -175,10 +243,18 @@ class CertoraContextValidator:
|
|
|
175
243
|
if context.compilation_steps_only and context.build_only:
|
|
176
244
|
raise Util.CertoraUserInputError("cannot use both 'compilation_steps_only' and 'build_only'")
|
|
177
245
|
|
|
178
|
-
|
|
246
|
+
set_attr_default(context, attr_name=Attrs.CommonAttributes.WAIT_FOR_RESULTS.name.lower(),
|
|
247
|
+
ci_value=str(Vf.WaitForResultOptions.ALL),
|
|
248
|
+
default_value=str(Vf.WaitForResultOptions.NONE))
|
|
179
249
|
if context.wait_for_results and context.wait_for_results != str(Vf.WaitForResultOptions.NONE) and context.local:
|
|
180
250
|
validation_logger.warning("'wait_for_results' has no effect in local tool runs")
|
|
181
251
|
|
|
252
|
+
if context.url_visibility and context.local:
|
|
253
|
+
validation_logger.info("'url_visibility' has no effect in local tool runs")
|
|
254
|
+
set_attr_default(context, attr_name=Attrs.CommonAttributes.URL_VISIBILITY.name.lower(),
|
|
255
|
+
ci_value=str(Vf.UrlVisibilityOptions.PUBLIC),
|
|
256
|
+
default_value=str(Vf.UrlVisibilityOptions.PRIVATE))
|
|
257
|
+
|
|
182
258
|
# packages must be in a normal form (no unneeded . or ..)
|
|
183
259
|
if context.packages:
|
|
184
260
|
for idx, el in enumerate(context.packages):
|
|
@@ -334,19 +410,13 @@ def convert_to_compiler_map(context: CertoraContext) -> None:
|
|
|
334
410
|
if context.solc_map:
|
|
335
411
|
context.compiler_map = context.solc_map
|
|
336
412
|
context.solc_map = None
|
|
337
|
-
|
|
338
|
-
context.compiler_map = {'*.sol': f'{context.solc}'}
|
|
339
|
-
if context.vyper:
|
|
340
|
-
context.compiler_map = {'*.vy': f'{context.vyper}'}
|
|
413
|
+
|
|
341
414
|
|
|
342
415
|
def check_vyper_flag(context: CertoraContext) -> None:
|
|
343
416
|
if context.vyper:
|
|
344
|
-
|
|
345
|
-
if
|
|
346
|
-
|
|
347
|
-
if context.solc:
|
|
348
|
-
raise Util.CertoraUserInputError("cannot set both vyper attribute and solc attribute")
|
|
349
|
-
context.solc = context.vyper
|
|
417
|
+
vy_paths = [path for path in context.files if path.endswith(".vy")]
|
|
418
|
+
if not vy_paths:
|
|
419
|
+
validation_logger.warning("vyper attribute was set but no Vyper files were set")
|
|
350
420
|
|
|
351
421
|
def check_contract_name_arg_inputs(context: CertoraContext) -> None:
|
|
352
422
|
"""
|
|
@@ -388,14 +458,6 @@ def check_contract_name_arg_inputs(context: CertoraContext) -> None:
|
|
|
388
458
|
check_conflicting_link_args(context)
|
|
389
459
|
|
|
390
460
|
context.verified_contract_files = []
|
|
391
|
-
if context.assert_contracts is not None:
|
|
392
|
-
for assert_arg in context.assert_contracts:
|
|
393
|
-
contract = Util.get_trivial_contract_name(assert_arg)
|
|
394
|
-
if contract not in contract_names:
|
|
395
|
-
__suggest_contract_name(f"'assert' argument, {contract}, doesn't match any contract name", contract,
|
|
396
|
-
contract_names, contract_to_file)
|
|
397
|
-
else:
|
|
398
|
-
context.verified_contract_files.append(contract_to_file[contract])
|
|
399
461
|
|
|
400
462
|
context.spec_file = None
|
|
401
463
|
|
|
@@ -409,7 +471,7 @@ def check_contract_name_arg_inputs(context: CertoraContext) -> None:
|
|
|
409
471
|
context.spec_file = spec
|
|
410
472
|
|
|
411
473
|
contract_to_address = dict()
|
|
412
|
-
if context
|
|
474
|
+
if getattr(context, 'address', None):
|
|
413
475
|
for address_str in context.address:
|
|
414
476
|
contract = address_str.split(':')[0]
|
|
415
477
|
if contract not in contract_names:
|
|
@@ -425,7 +487,7 @@ def check_contract_name_arg_inputs(context: CertoraContext) -> None:
|
|
|
425
487
|
all_warnings.add(f'address {number} for contract {contract} defined twice')
|
|
426
488
|
context.address = contract_to_address
|
|
427
489
|
|
|
428
|
-
if context
|
|
490
|
+
if getattr(context, 'struct_link', None):
|
|
429
491
|
contract_slot_to_contract = dict()
|
|
430
492
|
for link in context.struct_link:
|
|
431
493
|
location = link.split('=')[0]
|
|
@@ -541,30 +603,32 @@ def check_mode_of_operation(context: CertoraContext) -> None:
|
|
|
541
603
|
|
|
542
604
|
@param context: A namespace including all CLI arguments provided
|
|
543
605
|
@raise an CertoraUserInputError when:
|
|
544
|
-
1.
|
|
545
|
-
2. when
|
|
546
|
-
3.
|
|
547
|
-
4. If either --bytecode_jsons or --bytecode_spec was used without the other.
|
|
606
|
+
1. when both --equivalence_contracts and --verify flags were given
|
|
607
|
+
2. when the file is not .tac|.conf|.json and neither --equivalence_contracts nor --verify were used
|
|
608
|
+
3. If either --bytecode_jsons or --bytecode_spec was used without the other.
|
|
548
609
|
"""
|
|
549
610
|
context.is_verify = context.verify is not None and len(context.verify) > 0
|
|
550
|
-
context.is_assert = context.assert_contracts is not None and len(context.assert_contracts) > 0
|
|
551
611
|
context.is_bytecode = context.bytecode_jsons is not None and len(context.bytecode_jsons) > 0
|
|
612
|
+
context.is_equivalence = context.equivalence_contracts is not None or context.app == App.ConcordApp
|
|
552
613
|
|
|
553
|
-
if (context.project_sanity or context.foundry) and
|
|
554
|
-
|
|
614
|
+
if (context.project_sanity or context.foundry) and \
|
|
615
|
+
(context.is_verify or context.is_bytecode or context.is_equivalence):
|
|
616
|
+
raise Util.CertoraUserInputError("The 'project_sanity' and 'foundry' options cannot coexist with the 'verify', "
|
|
617
|
+
"'assert_contract' or 'bytecode_jsons' options")
|
|
555
618
|
|
|
556
619
|
if context.project_sanity and context.foundry:
|
|
557
620
|
raise Util.CertoraUserInputError("The 'project_sanity' and 'foundry' options cannot coexist")
|
|
558
621
|
|
|
559
|
-
if context.is_verify
|
|
560
|
-
raise Util.CertoraUserInputError("only one option of '
|
|
622
|
+
if len(list(filter(None, [context.is_verify, context.is_equivalence]))) > 1:
|
|
623
|
+
raise Util.CertoraUserInputError("only one option of 'verify', 'equivalence' can be used")
|
|
561
624
|
|
|
562
625
|
has_bytecode_spec = context.bytecode_spec is not None
|
|
563
626
|
if has_bytecode_spec != context.is_bytecode:
|
|
564
627
|
raise Util.CertoraUserInputError("Must use 'bytecode' together with 'bytecode_spec'")
|
|
565
628
|
|
|
566
629
|
if not context.files and not any((context.is_bytecode, context.project_sanity, context.foundry)):
|
|
567
|
-
raise Util.CertoraUserInputError("Should always provide input files, unless 'bytecode_jsons' or
|
|
630
|
+
raise Util.CertoraUserInputError("Should always provide input files, unless 'bytecode_jsons' or "
|
|
631
|
+
"'project_sanity' or 'foundry' are used")
|
|
568
632
|
|
|
569
633
|
if context.is_bytecode and context.files:
|
|
570
634
|
raise Util.CertoraUserInputError("Cannot use 'bytecode_jsons' with other files")
|
|
@@ -578,7 +642,8 @@ def check_mode_of_operation(context: CertoraContext) -> None:
|
|
|
578
642
|
if not context.files:
|
|
579
643
|
file_contract_pairs = _get_project_file_contract_pairs(context)
|
|
580
644
|
main_contract = file_contract_pairs[0][1] # any contract
|
|
581
|
-
context.files =
|
|
645
|
+
context.files = \
|
|
646
|
+
[f"{file}:{contract}" if file.stem != contract else str(file) for file, contract in file_contract_pairs]
|
|
582
647
|
else:
|
|
583
648
|
first = context.files[0]
|
|
584
649
|
if ':' in first:
|
|
@@ -590,12 +655,14 @@ def check_mode_of_operation(context: CertoraContext) -> None:
|
|
|
590
655
|
|
|
591
656
|
context.verify = f"{main_contract}:{spec_file_name}"
|
|
592
657
|
context.is_verify = True
|
|
593
|
-
# In this mode we want to make life easy for the user,
|
|
658
|
+
# In this mode we want to make life easy for the user,
|
|
659
|
+
# so use the auto-dispatcher mode to avoid many unresolved calls
|
|
594
660
|
context.auto_dispatcher = True
|
|
595
661
|
|
|
596
662
|
if context.foundry:
|
|
597
663
|
file_contract_pairs = _get_project_file_contract_pairs(context)
|
|
598
|
-
test_pairs = [(file, contract) for file, contract in file_contract_pairs
|
|
664
|
+
test_pairs = [(file, contract) for file, contract in file_contract_pairs
|
|
665
|
+
if file.stem.endswith(".t") and 'lib' not in file.parents]
|
|
599
666
|
context.files = [f"{file}:{contract}" for file, contract in test_pairs]
|
|
600
667
|
main_contract = test_pairs[0][1] # any contract
|
|
601
668
|
spec_file_name = _generate_temp_spec(context, "use builtin rule verifyFoundryFuzzTestsNoRevert;\n")
|
|
@@ -603,7 +670,8 @@ def check_mode_of_operation(context: CertoraContext) -> None:
|
|
|
603
670
|
context.foundry_tests_mode = True
|
|
604
671
|
context.verify = f"{main_contract}:{spec_file_name}"
|
|
605
672
|
context.is_verify = True
|
|
606
|
-
# In this mode we want to make life easy for the user,
|
|
673
|
+
# In this mode we want to make life easy for the user,
|
|
674
|
+
# so use the auto-dispatcher mode to avoid many unresolved calls
|
|
607
675
|
context.auto_dispatcher = True
|
|
608
676
|
|
|
609
677
|
if context.files:
|
|
@@ -616,49 +684,30 @@ def check_mode_of_operation(context: CertoraContext) -> None:
|
|
|
616
684
|
for input_file in context.files:
|
|
617
685
|
special_file_type = next((suffix for suffix in special_file_suffixes if input_file.endswith(suffix)), None)
|
|
618
686
|
|
|
619
|
-
if special_file_type:
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
f"No other files are allowed with a file of type {special_file_type}")
|
|
623
|
-
if context.is_assert:
|
|
624
|
-
raise Util.CertoraUserInputError(
|
|
625
|
-
f"Option 'assert_contracts' cannot be used with a {special_file_type} file {input_file}")
|
|
687
|
+
if special_file_type and len(context.files) > 1:
|
|
688
|
+
raise Util.CertoraUserInputError(
|
|
689
|
+
f"No other files are allowed with a file of type {special_file_type}")
|
|
626
690
|
|
|
627
|
-
if
|
|
628
|
-
|
|
691
|
+
if (
|
|
692
|
+
not context.app == App.ConcordApp and
|
|
693
|
+
not any([context.is_verify, context.is_bytecode,
|
|
694
|
+
context.equivalence_contracts, special_file_type]) and not context.build_only):
|
|
629
695
|
raise Util.CertoraUserInputError("You must use 'verify' when running the Certora Prover")
|
|
630
696
|
|
|
697
|
+
def find_matching_contracts(context: CertoraContext, pattern: str) -> List[str]:
|
|
698
|
+
result = []
|
|
699
|
+
for contract in context.contract_to_file:
|
|
700
|
+
if fnmatch.fnmatch(contract, pattern):
|
|
701
|
+
result.append(context.contract_to_file[contract])
|
|
702
|
+
return result
|
|
631
703
|
|
|
632
|
-
def
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
:param context:
|
|
636
|
-
:param map_attr_name: name of the attribute e.g. solc_optimize_map
|
|
637
|
-
:param map_attr: the value of the map attribute, a dictionary mapping contract/file to a value
|
|
638
|
-
:return:
|
|
639
|
-
|
|
640
|
-
all solc_XXX_map attributes are used to set attributes for the Solidity compiler. The value is based on the
|
|
641
|
-
Solidity file to be compiled, therefore it translates all keys to a relative path to a file
|
|
642
|
-
"""
|
|
643
|
-
|
|
644
|
-
def find_matching_files(context: CertoraContext, pattern: str) -> List[str]:
|
|
645
|
-
file_part = pattern.split(':')[0]
|
|
646
|
-
if Path(file_part).suffix == "": # no suffix means the key is a contract
|
|
647
|
-
matching_contracts = fnmatch.filter(context.contracts, pattern) # find matching contracts
|
|
648
|
-
return [context.contract_to_file[contract] for contract in matching_contracts]
|
|
649
|
-
else:
|
|
650
|
-
return [file_part]
|
|
651
|
-
|
|
652
|
-
new_map_attr: Dict[str, str] = {}
|
|
653
|
-
for (pattern, value) in map_attr.copy().items():
|
|
654
|
-
matching_files = find_matching_files(context, pattern)
|
|
655
|
-
for file in matching_files:
|
|
656
|
-
file = str(Path(file).resolve(strict=False).relative_to(Path.cwd()))
|
|
657
|
-
attr_value = new_map_attr.get(file)
|
|
658
|
-
if attr_value is None: # key in file paths but no mapping yet
|
|
659
|
-
new_map_attr[file] = value
|
|
660
|
-
return new_map_attr
|
|
704
|
+
def find_matching_files(context: CertoraContext, pattern: str) -> List[str]:
|
|
705
|
+
result = []
|
|
661
706
|
|
|
707
|
+
for path in context.file_paths:
|
|
708
|
+
if glob.globmatch(path, pattern, flags=glob.GLOBSTAR):
|
|
709
|
+
result.append(path)
|
|
710
|
+
return result
|
|
662
711
|
|
|
663
712
|
def check_map_attributes(context: CertoraContext) -> None:
|
|
664
713
|
|
|
@@ -676,27 +725,24 @@ def check_map_attributes(context: CertoraContext) -> None:
|
|
|
676
725
|
# we also check the map value was not set to False explicitly in the conf file
|
|
677
726
|
if base_attr and map_attr and not (base_attr is False and context.conf_options.get(base_attr) is not None):
|
|
678
727
|
raise Util.CertoraUserInputError(f"You cannot use both '{attr_prefix}' and '{map_attr_name}' arguments")
|
|
679
|
-
if not isinstance(map_attr,
|
|
680
|
-
raise RuntimeError(f"map_attr is not dictionary, got {map_attr}")
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
for
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
paths_not_set = [path for path, value in sources_mappings.items() if value is None]
|
|
698
|
-
if paths_not_set:
|
|
699
|
-
raise Util.CertoraUserInputError(f"{' '.join(paths_not_set)} was not set in {map_attr_name}")
|
|
728
|
+
if not isinstance(map_attr, OrderedDict):
|
|
729
|
+
raise RuntimeError(f"`map_attr` is not an ordered dictionary, got {map_attr}")
|
|
730
|
+
|
|
731
|
+
# will check that all the contract files are matched
|
|
732
|
+
file_list: Dict[str, bool] = {path: False for path in context.file_paths}
|
|
733
|
+
|
|
734
|
+
for key, value in map_attr.items():
|
|
735
|
+
pattern = key.split(':')[0] # ignore the contract part
|
|
736
|
+
if Path(pattern).suffix == "":
|
|
737
|
+
for path in find_matching_contracts(context, pattern):
|
|
738
|
+
file_list[path] = True
|
|
739
|
+
else:
|
|
740
|
+
for path in find_matching_files(context, pattern):
|
|
741
|
+
file_list[path] = True
|
|
742
|
+
|
|
743
|
+
none_keys = [k for k, v in file_list.items() if v is False]
|
|
744
|
+
if none_keys:
|
|
745
|
+
raise Util.CertoraUserInputError(f"The following files are not matched in {map_attr_name}: {none_keys}")
|
|
700
746
|
|
|
701
747
|
|
|
702
748
|
def check_parametric_contracts(context: CertoraContext) -> None:
|
|
@@ -863,16 +909,17 @@ def check_files_input(file_list: List[str]) -> None:
|
|
|
863
909
|
if file.endswith(Util.SOROBAN_EXEC_EXTENSION):
|
|
864
910
|
raise Util.CertoraUserInputError(f'The Soroban file {file} cannot be accompanied with other files')
|
|
865
911
|
|
|
866
|
-
|
|
867
|
-
|
|
912
|
+
|
|
913
|
+
def set_attr_default(context: CertoraContext, attr_name: str, ci_value: str, default_value: str) -> None:
|
|
914
|
+
if getattr(context, attr_name, None) is None:
|
|
868
915
|
if Util.is_ci_or_git_action():
|
|
869
|
-
context
|
|
916
|
+
setattr(context, attr_name, ci_value)
|
|
870
917
|
else:
|
|
871
|
-
context
|
|
918
|
+
setattr(context, attr_name, default_value)
|
|
872
919
|
|
|
873
920
|
|
|
874
921
|
def mode_has_spec_file(context: CertoraContext) -> bool:
|
|
875
|
-
return not context.
|
|
922
|
+
return not (context.is_tac or context.is_equivalence)
|
|
876
923
|
|
|
877
924
|
|
|
878
925
|
def to_relative_paths(paths: Union[str, List[str]]) -> Union[str, List[str]]:
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
# You should have received a copy of the GNU General Public License
|
|
14
14
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
15
|
|
|
16
|
-
from typing import List, Optional, Tuple, Dict, Any
|
|
16
|
+
from typing import List, Optional, Tuple, Dict, Any, cast
|
|
17
17
|
from CertoraProver import certoraType as CT
|
|
18
18
|
from Shared import certoraUtils as Util
|
|
19
19
|
|
|
@@ -46,6 +46,30 @@ class SourceBytes:
|
|
|
46
46
|
except (KeyError, ValueError):
|
|
47
47
|
return None
|
|
48
48
|
|
|
49
|
+
class VyperMetadata:
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
frame_size: Optional[int] = None,
|
|
53
|
+
frame_start: Optional[int] = None,
|
|
54
|
+
venom_via_stack: Optional[List[str]] = None,
|
|
55
|
+
venom_return_via_stack: bool = False,
|
|
56
|
+
runtime_start_pc: Optional[int] = None,
|
|
57
|
+
):
|
|
58
|
+
self.frame_size = frame_size
|
|
59
|
+
self.frame_start = frame_start
|
|
60
|
+
self.venom_via_stack = venom_via_stack
|
|
61
|
+
self.venom_return_via_stack = venom_return_via_stack
|
|
62
|
+
self.runtime_start_pc = runtime_start_pc
|
|
63
|
+
|
|
64
|
+
def as_dict(self) -> Dict[str, Any]:
|
|
65
|
+
return {
|
|
66
|
+
"frame_size": self.frame_size,
|
|
67
|
+
"frame_start": self.frame_start,
|
|
68
|
+
"venom_via_stack": self.venom_via_stack,
|
|
69
|
+
"venom_return_via_stack": self.venom_return_via_stack,
|
|
70
|
+
"runtime_start_pc": self.runtime_start_pc,
|
|
71
|
+
}
|
|
72
|
+
|
|
49
73
|
|
|
50
74
|
class Func:
|
|
51
75
|
def __init__(self,
|
|
@@ -57,15 +81,19 @@ class Func:
|
|
|
57
81
|
notpayable: bool,
|
|
58
82
|
fromLib: bool, # actually serialized
|
|
59
83
|
isConstructor: bool, # not serialized
|
|
84
|
+
is_free_func: bool,
|
|
60
85
|
stateMutability: str,
|
|
61
86
|
visibility: str,
|
|
62
87
|
implemented: bool, # does this function have a body? (false for interface functions)
|
|
63
88
|
overrides: bool, # does this function override an interface declaration or super-contract definition?
|
|
89
|
+
virtual: bool,
|
|
64
90
|
contractName: str,
|
|
65
91
|
source_bytes: Optional[SourceBytes],
|
|
66
92
|
ast_id: Optional[int],
|
|
67
93
|
original_file: Optional[str],
|
|
94
|
+
location: Optional[str],
|
|
68
95
|
body_location: Optional[str],
|
|
96
|
+
vyper_metadata: Optional[VyperMetadata] = None
|
|
69
97
|
):
|
|
70
98
|
self.name = name
|
|
71
99
|
self.fullArgs = fullArgs
|
|
@@ -75,15 +103,19 @@ class Func:
|
|
|
75
103
|
self.notpayable = notpayable
|
|
76
104
|
self.fromLib = fromLib
|
|
77
105
|
self.isConstructor = isConstructor
|
|
106
|
+
self.is_free_func = is_free_func
|
|
78
107
|
self.stateMutability = stateMutability
|
|
79
108
|
self.visibility = visibility
|
|
80
109
|
self.original_file = original_file
|
|
110
|
+
self.location = location
|
|
81
111
|
self.body_location = body_location
|
|
82
112
|
self.implemented = implemented
|
|
83
113
|
self.ast_id = ast_id
|
|
84
114
|
self.overrides = overrides
|
|
115
|
+
self.virtual = virtual
|
|
85
116
|
self.contractName = contractName
|
|
86
117
|
self.source_bytes = source_bytes
|
|
118
|
+
self.vyper_metadata = vyper_metadata
|
|
87
119
|
|
|
88
120
|
def as_dict(self) -> Dict[str, Any]:
|
|
89
121
|
return {
|
|
@@ -99,6 +131,7 @@ class Func:
|
|
|
99
131
|
"contractName": self.contractName,
|
|
100
132
|
"sourceBytes": None if self.source_bytes is None else self.source_bytes.as_dict(),
|
|
101
133
|
"originalFile": None if self.original_file is None else Util.resolve_original_file(self.original_file),
|
|
134
|
+
"vyper_metadata": cast(VyperMetadata, self.vyper_metadata).as_dict() if self.vyper_metadata is not None else None
|
|
102
135
|
}
|
|
103
136
|
|
|
104
137
|
def name_for_contract(self, contract_name: str) -> str:
|
|
@@ -17,6 +17,7 @@ import subprocess
|
|
|
17
17
|
import json
|
|
18
18
|
import logging
|
|
19
19
|
import os
|
|
20
|
+
from typing import List
|
|
20
21
|
|
|
21
22
|
from CertoraProver.certoraContextClass import CertoraContext
|
|
22
23
|
from Shared import certoraUtils as Util
|
|
@@ -56,17 +57,11 @@ def add_solana_files_to_context(context: CertoraContext, json_obj: dict) -> None
|
|
|
56
57
|
update_metadata(context, solana_files_attr)
|
|
57
58
|
|
|
58
59
|
|
|
59
|
-
def
|
|
60
|
-
|
|
61
|
-
return
|
|
60
|
+
def run_rust_build(context: CertoraContext, build_cmd: List[str]) -> None:
|
|
61
|
+
|
|
62
62
|
try:
|
|
63
|
-
build_script_logger.info(f"Building
|
|
64
|
-
|
|
65
|
-
if context.cargo_features is not None:
|
|
66
|
-
run_cmd.append('--cargo_features')
|
|
67
|
-
for feature in context.cargo_features:
|
|
68
|
-
run_cmd.append(feature)
|
|
69
|
-
result = subprocess.run(run_cmd, capture_output=True, text=True)
|
|
63
|
+
build_script_logger.info(f"Building by calling `{' '.join(build_cmd)}`")
|
|
64
|
+
result = subprocess.run(build_cmd, capture_output=True, text=True)
|
|
70
65
|
|
|
71
66
|
# Check if the script executed successfully
|
|
72
67
|
if result.returncode != 0:
|
|
@@ -95,6 +90,9 @@ def run_script_and_parse_json(context: CertoraContext) -> None:
|
|
|
95
90
|
|
|
96
91
|
add_solana_files_to_context(context, json_obj)
|
|
97
92
|
|
|
93
|
+
assert not context.files, f"run_rust_build: expecting files to be empty, got: {context.files}"
|
|
94
|
+
context.files = [os.path.join(context.rust_project_directory, context.rust_executables)]
|
|
95
|
+
|
|
98
96
|
if context.test == str(Util.TestValue.AFTER_BUILD_RUST):
|
|
99
97
|
raise Util.TestResultsReady(context)
|
|
100
98
|
|
|
@@ -158,6 +158,15 @@ class Type(ABC):
|
|
|
158
158
|
ast_logger.fatal(f"unexpected AST Type Name Node: {name}")
|
|
159
159
|
return ret
|
|
160
160
|
|
|
161
|
+
def contains_mapping(self) -> bool:
|
|
162
|
+
if isinstance(self, MappingType):
|
|
163
|
+
return True
|
|
164
|
+
if isinstance(self, StructType):
|
|
165
|
+
return any(member.contains_mapping() for member in self.members)
|
|
166
|
+
if isinstance(self, ArrayType):
|
|
167
|
+
return self.elementType.contains_mapping()
|
|
168
|
+
return False
|
|
169
|
+
|
|
161
170
|
|
|
162
171
|
class PrimitiveType(Type):
|
|
163
172
|
allowed_primitive_type_names = {
|
|
@@ -204,7 +213,7 @@ class PrimitiveType(Type):
|
|
|
204
213
|
return PrimitiveType.canonical_primitive_name(self.name)
|
|
205
214
|
|
|
206
215
|
def get_source_str(self) -> str:
|
|
207
|
-
return self.name
|
|
216
|
+
return self.type_string # this is different from `self.name` in the case of a payable address
|
|
208
217
|
|
|
209
218
|
|
|
210
219
|
class StringType(Type):
|
|
@@ -23,6 +23,7 @@ from typing import Dict, Any, Set, List, Tuple, Optional
|
|
|
23
23
|
from CertoraProver.certoraMiniSpecParser import SpecImportLexer, SpecImportParser, SpecWithImports
|
|
24
24
|
from CertoraProver.certoraContextClass import CertoraContext
|
|
25
25
|
from Shared import certoraUtils as Util
|
|
26
|
+
import CertoraProver.certoraApp as App
|
|
26
27
|
|
|
27
28
|
build_logger = logging.getLogger("build_conf")
|
|
28
29
|
|
|
@@ -47,10 +48,27 @@ class CertoraVerifyGenerator:
|
|
|
47
48
|
# we need to build once because of the early typechecking...
|
|
48
49
|
self.update_certora_verify_struct(False)
|
|
49
50
|
|
|
50
|
-
elif self.context.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
elif self.context.equivalence_contracts is not None:
|
|
52
|
+
if self.context.method is None:
|
|
53
|
+
raise Util.CertoraUserInputError("Argument `method` is required for equivalence checks")
|
|
54
|
+
if len(self.context.method) != 1:
|
|
55
|
+
raise Util.CertoraUserInputError("Can only check equivalence on one method")
|
|
56
|
+
equivalence_query = context.equivalence_contracts
|
|
57
|
+
equiv_contracts = equivalence_query.split("=")
|
|
58
|
+
self.certora_verify_struct = {
|
|
59
|
+
"type": "equivalence",
|
|
60
|
+
"primary_contract": equiv_contracts[0],
|
|
61
|
+
"secondary_contract": equiv_contracts[1]
|
|
62
|
+
}
|
|
63
|
+
elif context.app == App.ConcordApp:
|
|
64
|
+
if len(context.contracts) != 2:
|
|
65
|
+
raise Util.CertoraUserInputError(f"Expecting 2 contracts but got {len(context.contracts)}: {context.contracts}")
|
|
66
|
+
contract_list = list(context.contracts)
|
|
67
|
+
self.certora_verify_struct = {
|
|
68
|
+
"type": "equivalence",
|
|
69
|
+
"primary_contract": contract_list[0],
|
|
70
|
+
"secondary_contract": contract_list[1]
|
|
71
|
+
}
|
|
54
72
|
|
|
55
73
|
def update_certora_verify_struct(self, in_certora_sources: bool) -> None:
|
|
56
74
|
"""
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# The Certora Prover
|
|
2
|
+
# Copyright (C) 2025 Certora Ltd.
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation, version 3 of the License.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
|
+
|
|
16
|
+
from Crypto.Hash import keccak
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def calculate_keccak_hash(x: bytes) -> int:
|
|
20
|
+
"""
|
|
21
|
+
Calculates the Keccak-256 hash of the input bytes and returns it as an integer.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
x (bytes): The input bytes to be hashed.
|
|
25
|
+
Returns:
|
|
26
|
+
int: The Keccak-256 hash value as an integer.
|
|
27
|
+
"""
|
|
28
|
+
k = keccak.new(digest_bits=256)
|
|
29
|
+
k.update(x)
|
|
30
|
+
return int(k.hexdigest(), base=16)
|
|
31
|
+
|
|
32
|
+
def erc7201(x: bytes) -> int:
|
|
33
|
+
"""
|
|
34
|
+
Hashes the input bytes using the Keccak-256 algorithm and returns
|
|
35
|
+
the result as an integer. The input is first hashed, then decremented
|
|
36
|
+
by 1, and the resulting hash is used to compute the final hash.
|
|
37
|
+
The final hash is masked to 256 bits and the last byte is cleared
|
|
38
|
+
(set to zero).
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
x (bytes): The input bytes to be hashed.
|
|
42
|
+
Returns:
|
|
43
|
+
int: The final hash value as an integer.
|
|
44
|
+
"""
|
|
45
|
+
return calculate_keccak_hash((calculate_keccak_hash(x) - 1).to_bytes(32, byteorder='big')) & (~0xff)
|