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.
Files changed (54) hide show
  1. certora_cli/CertoraProver/Compiler/CompilerCollectorFactory.py +10 -3
  2. certora_cli/CertoraProver/Compiler/CompilerCollectorVy.py +51 -16
  3. certora_cli/CertoraProver/Compiler/CompilerCollectorYul.py +3 -0
  4. certora_cli/CertoraProver/castingInstrumenter.py +192 -0
  5. certora_cli/CertoraProver/certoraApp.py +52 -0
  6. certora_cli/CertoraProver/certoraBuild.py +694 -207
  7. certora_cli/CertoraProver/certoraBuildCacheManager.py +21 -17
  8. certora_cli/CertoraProver/certoraBuildDataClasses.py +8 -2
  9. certora_cli/CertoraProver/certoraBuildRust.py +88 -54
  10. certora_cli/CertoraProver/certoraBuildSui.py +112 -0
  11. certora_cli/CertoraProver/certoraCloudIO.py +97 -96
  12. certora_cli/CertoraProver/certoraCollectConfigurationLayout.py +230 -84
  13. certora_cli/CertoraProver/certoraCollectRunMetadata.py +52 -6
  14. certora_cli/CertoraProver/certoraCompilerParameters.py +11 -0
  15. certora_cli/CertoraProver/certoraConfigIO.py +43 -35
  16. certora_cli/CertoraProver/certoraContext.py +128 -54
  17. certora_cli/CertoraProver/certoraContextAttributes.py +415 -234
  18. certora_cli/CertoraProver/certoraContextValidator.py +152 -105
  19. certora_cli/CertoraProver/certoraContractFuncs.py +34 -1
  20. certora_cli/CertoraProver/certoraParseBuildScript.py +8 -10
  21. certora_cli/CertoraProver/certoraType.py +10 -1
  22. certora_cli/CertoraProver/certoraVerifyGenerator.py +22 -4
  23. certora_cli/CertoraProver/erc7201.py +45 -0
  24. certora_cli/CertoraProver/splitRules.py +23 -18
  25. certora_cli/CertoraProver/storageExtension.py +351 -0
  26. certora_cli/EquivalenceCheck/Eq_default.conf +0 -1
  27. certora_cli/EquivalenceCheck/Eq_sanity.conf +0 -1
  28. certora_cli/EquivalenceCheck/equivCheck.py +2 -1
  29. certora_cli/Mutate/mutateApp.py +41 -22
  30. certora_cli/Mutate/mutateAttributes.py +11 -0
  31. certora_cli/Mutate/mutateValidate.py +42 -2
  32. certora_cli/Shared/certoraAttrUtil.py +21 -5
  33. certora_cli/Shared/certoraUtils.py +180 -60
  34. certora_cli/Shared/certoraValidateFuncs.py +68 -26
  35. certora_cli/Shared/proverCommon.py +308 -0
  36. certora_cli/certoraCVLFormatter.py +76 -0
  37. certora_cli/certoraConcord.py +39 -0
  38. certora_cli/certoraEVMProver.py +4 -3
  39. certora_cli/certoraRanger.py +39 -0
  40. certora_cli/certoraRun.py +83 -223
  41. certora_cli/certoraSolanaProver.py +40 -128
  42. certora_cli/certoraSorobanProver.py +59 -4
  43. certora_cli/certoraSuiProver.py +93 -0
  44. certora_cli_beta_mirror-8.5.0.dist-info/LICENSE +15 -0
  45. {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-8.5.0.dist-info}/METADATA +21 -5
  46. certora_cli_beta_mirror-8.5.0.dist-info/RECORD +81 -0
  47. {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-8.5.0.dist-info}/WHEEL +1 -1
  48. {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-8.5.0.dist-info}/entry_points.txt +3 -0
  49. certora_jars/ASTExtraction.jar +0 -0
  50. certora_jars/CERTORA-CLI-VERSION-METADATA.json +1 -1
  51. certora_jars/Typechecker.jar +0 -0
  52. certora_cli_beta_mirror-7.28.0.dist-info/LICENSE +0 -22
  53. certora_cli_beta_mirror-7.28.0.dist-info/RECORD +0 -70
  54. {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 typing import Dict, List, Tuple, Set, Any, Union, Optional
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 Attrs.get_attribute_class().attribute_list():
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 Attrs.is_evm_app():
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
- set_wait_for_results_default(context)
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
- if context.solc:
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
- non_vy_paths = [path for path in context.files if not path.endswith(".vy")]
345
- if non_vy_paths:
346
- raise Util.CertoraUserInputError(f"vyper attribute can only be set for Vyper files: {non_vy_paths}")
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.address:
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.struct_link:
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. .conf|.tac|.json file is used with --assert_contracts flags
545
- 2. when both --assert_contracts and --verify flags were given
546
- 3. when the file is not .tac|.conf|.json and neither --assert_contracts nor --verify were used
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 (context.is_verify or context.is_assert or context.is_bytecode):
554
- raise Util.CertoraUserInputError("The 'project_sanity' and 'foundry' options cannot coexist with the 'verify', 'assert_contract' or 'bytecode_jsons' options")
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 and context.is_assert:
560
- raise Util.CertoraUserInputError("only one option of 'assert_contracts' and 'verify' can be used")
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 'project_sanity' or 'foundry' are used")
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 = [f"{file}:{contract}" if file.stem != contract else str(file) for file, contract in file_contract_pairs]
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, so use the auto-dispatcher mode to avoid many unresolved calls
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 if file.stem.endswith(".t") and 'lib' not in file.parents]
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, so use the auto-dispatcher mode to avoid many unresolved calls
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
- if len(context.files) > 1:
621
- raise Util.CertoraUserInputError(
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 not any([context.is_assert, context.is_verify, context.is_bytecode,
628
- special_file_type]) and not context.build_only:
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 _normalize_maps(context: CertoraContext, map_attr_name: str, map_attr: Dict) -> Dict[str, str]:
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, dict):
680
- raise RuntimeError(f"map_attr is not dictionary, got {map_attr}")
681
- map_attr = _normalize_maps(context, map_attr_name, map_attr)
682
- setattr(context, f"{map_attr_name}", map_attr)
683
-
684
- sources_mappings: Dict[str, Optional[str]] = {file_path: None for file_path in context.file_paths} # initially mapping files to None
685
- for file in sources_mappings.keys():
686
- match: Optional[str] = None
687
- for (pattern, value) in map_attr.items():
688
- if fnmatch.fnmatch(file, pattern):
689
- match = pattern
690
- break
691
- if not match:
692
- raise Util.CertoraUserInputError(f"No matching for {file} in {map_attr_name}. Pattens: {map_attr}")
693
- elif not sources_mappings.get(file): # key in file paths but no mapping yet
694
- sources_mappings[file] = match
695
-
696
- # Now we check if all sources were mapped
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
- def set_wait_for_results_default(context: CertoraContext) -> None:
867
- if context.wait_for_results is None:
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.wait_for_results = str(Vf.WaitForResultOptions.ALL)
916
+ setattr(context, attr_name, ci_value)
870
917
  else:
871
- context.wait_for_results = str(Vf.WaitForResultOptions.NONE)
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.is_assert and not context.is_tac
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 run_script_and_parse_json(context: CertoraContext) -> None:
60
- if not context.build_script:
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 from script {context.build_script}")
64
- run_cmd = [context.build_script, '--json']
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.assert_contracts is not None:
51
- contract_to_check_asserts_for = self.context.assert_contracts
52
- self.certora_verify_struct = {"type": "assertion",
53
- "primaryContracts": contract_to_check_asserts_for}
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)