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
@@ -20,22 +20,24 @@ import os
20
20
  import re
21
21
  import shutil
22
22
  import sys
23
+ import tempfile
23
24
  import typing
24
25
  from collections import OrderedDict, defaultdict
25
26
  from enum import Enum
26
27
  from functools import lru_cache
27
28
  from pathlib import Path
28
29
  from typing import Any, Dict, List, Tuple, Optional, Set, Iterator, NoReturn
30
+
29
31
  from Crypto.Hash import keccak
30
32
 
33
+ from CertoraProver.castingInstrumenter import generate_casting_instrumentation
31
34
  from CertoraProver.certoraBuildCacheManager import CertoraBuildCacheManager, CachedFiles
32
35
  from CertoraProver.certoraBuildDataClasses import CONTRACTS, ImmutableReference, ContractExtension, ContractInSDC, SDC, \
33
36
  Instrumentation, InsertBefore, InsertAfter, UnspecializedSourceFinder, instrumentation_logger
34
37
  from CertoraProver.certoraCompilerParameters import SolcParameters
38
+ from CertoraProver.certoraContractFuncs import Func, InternalFunc, STATEMUT, SourceBytes, VyperMetadata
35
39
  from CertoraProver.certoraSourceFinders import add_source_finders
36
40
  from CertoraProver.certoraVerifyGenerator import CertoraVerifyGenerator
37
- from CertoraProver.certoraContractFuncs import Func, InternalFunc, STATEMUT, SourceBytes
38
- from Shared.certoraUtils import is_relative_to
39
41
 
40
42
  scripts_dir_path = Path(__file__).parent.parent.resolve() # containing directory
41
43
  sys.path.insert(0, str(scripts_dir_path))
@@ -54,7 +56,11 @@ from Shared import certoraValidateFuncs as Vf
54
56
  from CertoraProver import certoraContextValidator as Cv
55
57
  from Shared import certoraUtils as Util
56
58
  import CertoraProver.certoraContext as Ctx
57
-
59
+ from CertoraProver import storageExtension
60
+ from CertoraProver.storageExtension import (
61
+ NameSpacedStorage,
62
+ NewStorageInfo,
63
+ )
58
64
 
59
65
  BUILD_IS_LIBRARY = False
60
66
  AUTO_FINDER_PREFIX = "autoFinder_"
@@ -72,6 +78,7 @@ MUTANTS_LOCATION = "mutants_location"
72
78
 
73
79
  FunctionSig = Tuple[str, List[str], List[str], str]
74
80
 
81
+
75
82
  # logger for building the abstract syntax tree
76
83
  ast_logger = logging.getLogger("ast")
77
84
  # logger for issues calling/shelling out to external functions
@@ -84,12 +91,10 @@ build_logger = logging.getLogger("build_conf")
84
91
  # logger of the build cache
85
92
  build_cache_logger = logging.getLogger("build_cache")
86
93
 
87
-
88
94
  def fatal_error(logger: logging.Logger, msg: str) -> NoReturn:
89
95
  logger.fatal(msg)
90
96
  raise Exception(msg)
91
97
 
92
-
93
98
  class InputConfig:
94
99
  def __init__(self, context: CertoraContext) -> None:
95
100
  """
@@ -198,11 +203,18 @@ class PresetImmutableReference(ImmutableReference):
198
203
  # this function is Solidity specific.
199
204
  # todo: create certoraBuildUtilsSol.py file, where such solidity specific functions will be.
200
205
  def generate_finder_body(f: Func, internal_id: int, sym: int, compiler_collector: CompilerCollectorSol,
201
- compressed: bool = False) -> Optional[Tuple[List[int], str]]:
206
+ compressed: bool = False,
207
+ should_generate_inlining: bool = True) -> Optional[Tuple[List[int], str]]:
202
208
  if compressed:
203
209
  return generate_compressed_finder(
204
210
  f, internal_id, sym, compiler_collector
205
211
  )
212
+ elif not should_generate_inlining:
213
+ # We should not generate inlining as reported in CERT-9399, only when compressed=False.
214
+ # used_symbols is being generated the exact same way as in generate_full_finder
215
+ used_symbols = [i for i in range(len(f.fullArgs))]
216
+ return used_symbols, ''
217
+
206
218
  else:
207
219
  return generate_full_finder(
208
220
  f, internal_id, sym, compiler_collector
@@ -398,8 +410,9 @@ def get_modifier_param_type_name(ind: int, def_node: Dict[str, Any], f: Func) ->
398
410
 
399
411
  def generate_modifier_finder(f: Func, internal_id: int, sym: int,
400
412
  compiler_collector: CompilerCollectorSol, def_node: Dict[str, Any],
401
- compress: bool) -> Optional[Tuple[str, str]]:
402
- compressed = generate_finder_body(f, internal_id, sym, compiler_collector, compressed=compress)
413
+ compress: bool, should_generate_inlining: bool) -> Optional[Tuple[str, str]]:
414
+ compressed = generate_finder_body(f, internal_id, sym, compiler_collector, compressed=compress,
415
+ should_generate_inlining=should_generate_inlining)
403
416
  if compressed is None:
404
417
  return None
405
418
  modifier_name = f"logInternal{internal_id}"
@@ -411,6 +424,8 @@ def generate_modifier_finder(f: Func, internal_id: int, sym: int,
411
424
  formal_strings = []
412
425
  arg_strings = []
413
426
  for (logged_ty, logged_name) in zip(loggable_types, loggable_names):
427
+ if logged_name == "":
428
+ continue
414
429
  arg_strings.append(logged_name)
415
430
  formal_strings.append(f"{logged_ty} {logged_name}")
416
431
  modifier_body = f"modifier {modifier_name}"
@@ -420,9 +435,10 @@ def generate_modifier_finder(f: Func, internal_id: int, sym: int,
420
435
  return f'{modifier_name}({",".join(arg_strings)})', modifier_body
421
436
 
422
437
 
423
- def generate_inline_finder(f: Func, internal_id: int, sym: int,
424
- compiler_collector: CompilerCollectorSol, should_compress: bool) -> Optional[str]:
425
- finder = generate_finder_body(f, internal_id, sym, compiler_collector, compressed=should_compress)
438
+ def generate_inline_finder(f: Func, internal_id: int, sym: int, compiler_collector: CompilerCollectorSol,
439
+ should_compress: bool, should_generate_inlining: bool) -> Optional[str]:
440
+ finder = generate_finder_body(f, internal_id, sym, compiler_collector, compressed=should_compress,
441
+ should_generate_inlining=should_generate_inlining)
426
442
  if finder is None:
427
443
  return None
428
444
  return finder[1]
@@ -441,8 +457,12 @@ def convert_pathname_to_posix(json_dict: Dict[str, Any], entry: str, smart_contr
441
457
  if path_obj.is_file():
442
458
  json_dict_posix_paths[path_obj.as_posix()] = json_dict[entry][file_path]
443
459
  else:
444
- fatal_error(compiler_logger, f"The path of the source file {file_path}"
445
- f"in the standard json file {json_dict} does not exist")
460
+ json_dict_str = str(json_dict)
461
+ # protecting against long strings
462
+ if len(json_dict_str) > 200:
463
+ json_dict_str = json_dict_str[:200] + '...'
464
+ fatal_error(compiler_logger, f"The path of the source file {file_path} "
465
+ f"in the standard json file does not exist!\n{json_dict_str} ")
446
466
  json_dict[entry] = json_dict_posix_paths
447
467
 
448
468
 
@@ -823,6 +843,7 @@ class CertoraBuildGenerator:
823
843
  body_location = body_node["src"]
824
844
  elif body_node is None and func_def["implemented"]:
825
845
  ast_logger.debug(f"No body for {func_def} but ast claims it is implemented")
846
+ location: Optional[str] = func_def.get("src", None)
826
847
 
827
848
  if original_contract is not None:
828
849
  if method := original_contract.find_method(func_name, solidity_type_args):
@@ -847,14 +868,17 @@ class CertoraBuildGenerator:
847
868
  func_def[STATEMUT] in ["nonpayable", "view", "pure"],
848
869
  c_is_lib,
849
870
  is_constructor,
871
+ func_def.get("kind") == CertoraBuildGenerator.FREEFUNCTION_STRING,
850
872
  func_def[STATEMUT],
851
873
  func_visibility,
852
874
  func_def["implemented"],
853
875
  func_def.get("overrides", None) is not None,
876
+ func_def.get("virtual", False),
854
877
  contract_name,
855
878
  source_bytes,
856
879
  ast_id=func_def.get("id", None),
857
880
  original_file=original_file,
881
+ location=location,
858
882
  body_location=body_location,
859
883
  )
860
884
 
@@ -906,15 +930,18 @@ class CertoraBuildGenerator:
906
930
  notpayable=is_not_payable,
907
931
  fromLib=c_is_lib,
908
932
  isConstructor=False,
933
+ is_free_func=False,
909
934
  stateMutability=state_mutability,
910
935
  implemented=True,
911
936
  overrides=public_state_var.get("overrides", None) is not None,
937
+ virtual=False,
912
938
  # according to Solidity docs, getter functions have external visibility
913
939
  visibility="external",
914
940
  contractName=contract_name,
915
941
  source_bytes=SourceBytes.from_ast_node(public_state_var),
916
942
  ast_id=None,
917
943
  original_file=c_file,
944
+ location=None,
918
945
  body_location=None,
919
946
  )
920
947
  )
@@ -948,6 +975,8 @@ class CertoraBuildGenerator:
948
975
  ret = solc_type == "address"
949
976
  elif isinstance(ct_type, CT.StructType):
950
977
  ret = solc_type == "tuple"
978
+ elif isinstance(ct_type, CT.EnumType):
979
+ ret = solc_type == "uint8"
951
980
  return ret
952
981
 
953
982
  fs = [f for f in fs if all(compareTypes(a.type, i)
@@ -968,7 +997,7 @@ class CertoraBuildGenerator:
968
997
  assert len(f.returns) == len(fabi["outputs"]), \
969
998
  f"function collected for {fabi['name']} has the wrong number of return values"
970
999
  assert all(compareTypes(a.type, i) for a, i in zip(f.returns, fabi["outputs"])), \
971
- f"function collected for {fabi['name']} has the wrong types of return values"
1000
+ f"function collected for {fabi['name']} has the wrong types of return values. {[t.type.type_string for t in f.returns]} vs {fabi['outputs']}"
972
1001
 
973
1002
  verify_collected_all_abi_funcs(
974
1003
  [f for f in data["abi"] if f["type"] == "function"],
@@ -1198,10 +1227,15 @@ class CertoraBuildGenerator:
1198
1227
  return x[TYPE] == FUNCTION or x[TYPE] == CertoraBuildGenerator.CONSTRUCTOR_STRING
1199
1228
 
1200
1229
  @staticmethod
1201
- def collect_srcmap(data: Dict[str, Any]) -> Any:
1230
+ def collect_srcmap(data: Dict[str, Any]) -> Tuple[str, str]:
1202
1231
  # no source map object in vyper
1203
- return (data["evm"]["deployedBytecode"].get("sourceMap", ""),
1204
- data["evm"]["bytecode"].get("sourceMap", ""))
1232
+ deployed = data["evm"]["deployedBytecode"].get("sourceMap", "")
1233
+ if isinstance(deployed, dict):
1234
+ deployed = deployed.get("pc_pos_map_compressed", "")
1235
+ regular = data["evm"]["bytecode"].get("sourceMap", "")
1236
+ if isinstance(regular, dict):
1237
+ regular = regular.get("pc_pos_map_compressed", "")
1238
+ return deployed, regular
1205
1239
 
1206
1240
  @staticmethod
1207
1241
  def collect_varmap(contract: str, data: Dict[str, Any]) -> Any:
@@ -1347,49 +1381,63 @@ class CertoraBuildGenerator:
1347
1381
 
1348
1382
  return bytecode
1349
1383
 
1350
- def _handle_via_ir(self, contract_file_path: str, settings_dict: Dict[str, Any]) -> None:
1351
- if not self.context.solc_via_ir_map and not self.context.solc_via_ir:
1352
- return
1353
- if self.context.solc_via_ir_map:
1354
- match = Util.match_path_to_mapping_key(Path(contract_file_path), self.context.solc_via_ir_map)
1355
- if match:
1356
- settings_dict["viaIR"] = match
1357
- elif self.context.solc_via_ir:
1384
+ def get_map_bool_attribute_value(self, contract_file_path: Path, attr_name: str) -> bool:
1385
+ match = Ctx.get_map_attribute_value(self.context, contract_file_path, attr_name)
1386
+ assert isinstance(match, (bool, type(None))), f"Expected {attr_name} to be bool or None, got {type(match)}"
1387
+ return bool(match)
1388
+
1389
+ def get_solc_via_ir_value(self, contract_file_path: Path) -> bool:
1390
+ return self.get_map_bool_attribute_value(contract_file_path, 'solc_via_ir')
1391
+
1392
+ def get_vyper_venom_value(self, contract_file_path: Path) -> bool:
1393
+ return self.get_map_bool_attribute_value(contract_file_path, 'vyper_venom')
1394
+
1395
+ def get_solc_evm_version_value(self, contract_file_path: Path) -> Optional[str]:
1396
+ match = Ctx.get_map_attribute_value(self.context, contract_file_path, 'solc_evm_version')
1397
+ assert isinstance(match, (str, type(None))), f"Expected solc_evm_version to be string or None, got {type(match)}"
1398
+ return match
1399
+
1400
+ def get_solc_optimize_value(self, contract_file_path: Path) -> Optional[str]:
1401
+ match = Ctx.get_map_attribute_value(self.context, contract_file_path, 'solc_optimize')
1402
+ assert isinstance(match, (str, int, type(None))), f"Expected solc_optimize to be string, integer , got {type(match)}"
1403
+ if isinstance(match, int):
1404
+ match = str(match)
1405
+ return match
1406
+
1407
+ def _handle_venom(self, contract_file_path: Path, settings_dict: Dict[str, Any]) -> None:
1408
+ if self.get_vyper_venom_value(contract_file_path):
1409
+ settings_dict["experimentalCodegen"] = True
1410
+
1411
+ def _handle_via_ir(self, contract_file_path: Path, settings_dict: Dict[str, Any]) -> None:
1412
+ if self.get_solc_via_ir_value(contract_file_path):
1358
1413
  settings_dict["viaIR"] = True
1359
1414
 
1360
- def _handle_evm_version(self, contract_file_path: str, settings_dict: Dict[str, Any]) -> None:
1361
- if self.context.solc_evm_version_map:
1362
- match = Util.match_path_to_mapping_key(Path(contract_file_path), self.context.solc_evm_version_map)
1363
- if match:
1364
- settings_dict["evmVersion"] = match
1365
- elif self.context.solc_evm_version:
1366
- settings_dict["evmVersion"] = self.context.solc_evm_version
1415
+ def _handle_evm_version(self, contract_file_path: Path, settings_dict: Dict[str, Any]) -> None:
1416
+ match = self.get_solc_evm_version_value(contract_file_path)
1417
+ if match:
1418
+ settings_dict["evmVersion"] = match
1367
1419
 
1368
- def _handle_optimize(self, contract_file_path: str, settings_dict: Dict[str, Any],
1420
+ def _handle_optimize(self, contract_file_path: Path, settings_dict: Dict[str, Any],
1369
1421
  compiler_collector: CompilerCollector) -> None:
1370
1422
  """
1371
1423
  @param contract_file_path: the contract that we are working on
1372
1424
  @param settings_dict: data structure for sending to the solc compiler
1373
1425
  """
1374
- if self.context.solc_optimize_map:
1375
- match = Util.match_path_to_mapping_key(Path(contract_file_path), self.context.solc_optimize_map)
1376
- if match and int(match) > 0:
1377
- settings_dict["optimizer"] = {"enabled": True}
1378
- settings_dict["optimizer"]['runs'] = int(match)
1379
- elif self.context.solc_optimize:
1426
+ match = self.get_solc_optimize_value(contract_file_path)
1427
+ if match:
1380
1428
  settings_dict["optimizer"] = {"enabled": True}
1381
- if int(self.context.solc_optimize) > 0:
1382
- settings_dict["optimizer"]['runs'] = int(self.context.solc_optimize)
1429
+ if int(match) > 0:
1430
+ settings_dict["optimizer"]['runs'] = int(match)
1383
1431
 
1384
1432
  # if optimizer is true, we should also define settings_dict["optimizer"]["details"]
1385
1433
  # for both optimize map and optimize
1386
1434
  optimizer = settings_dict.get("optimizer")
1387
1435
  if optimizer and isinstance(optimizer, dict) and optimizer.get('enabled'):
1388
1436
  # if we are not disabling finder friendly optimizer specifically, enable it whenever viaIR is also enabled
1389
- if not self.context.strict_solc_optimizer and self.context.solc_via_ir:
1437
+ if not self.context.strict_solc_optimizer and self.get_solc_via_ir_value(contract_file_path):
1390
1438
  # The default optimizer steps (taken from libsolidity/interface/OptimiserSettings.h) but with the
1391
1439
  # full inliner step removed
1392
- solc0_8_26_to_0_8_29 = ("dhfoDgvulfnTUtnIfxa[r]EscLMVcul[j]Trpeulxa[r]cLCTUca[r]LSsTFOtfDnca[r]" +
1440
+ solc0_8_26_to_0_8_30 = ("dhfoDgvulfnTUtnIfxa[r]EscLMVcul[j]Trpeulxa[r]cLCTUca[r]LSsTFOtfDnca[r]" +
1393
1441
  "IulcscCTUtx[scCTUt]TOntnfDIuljmul[jul]VcTOculjmul")
1394
1442
  solc0_8_13_to_0_8_25 = "dhfoDgvulfnTUtnIf[xa[r]EscLMcCTUtTOntnfDIulLculVcul[j]T" + \
1395
1443
  "peulxa[rul]xa[r]cLgvifCTUca[r]LSsTFOtfDnca[r]Iulc]jmul[jul]VcTOculjmul"
@@ -1412,13 +1460,18 @@ class CertoraBuildGenerator:
1412
1460
  compiler_version = compiler_collector.compiler_version
1413
1461
  major, minor, patch = compiler_version
1414
1462
 
1415
- err_msg = "--finder_friendly_optimizer option supports solc versions 0.6.7 - 0.8.25 only, " \
1416
- f"got {major}.{minor}.{patch}"
1463
+ err_msg = \
1464
+ f"Unsupported solc version {major}.{minor}.{patch} for `solc_via_ir`. " \
1465
+ f"Supported versions: 0.6.7 – 0.8.25.\n" \
1466
+ f"Use `solc_via_ir_map` to disable `solc_via_ir` for specific files with older compiler versions."
1467
+
1417
1468
  yul_optimizer_steps = None
1418
1469
  if major != 0:
1419
1470
  raise Util.CertoraUserInputError(err_msg)
1420
1471
  elif minor < 6 or (minor == 6 and patch < 7):
1421
1472
  raise Util.CertoraUserInputError(err_msg)
1473
+ elif self.context.yul_optimizer_steps:
1474
+ yul_optimizer_steps = self.context.yul_optimizer_steps
1422
1475
  elif (minor == 6 and patch >= 7) or (minor == 7 and 0 <= patch <= 1):
1423
1476
  yul_optimizer_steps = solc0_6_7_to_0_7_1
1424
1477
  elif minor == 7 and 2 <= patch <= 5:
@@ -1433,8 +1486,8 @@ class CertoraBuildGenerator:
1433
1486
  yul_optimizer_steps = solc0_8_12
1434
1487
  elif minor == 8 and 13 <= patch <= 25:
1435
1488
  yul_optimizer_steps = solc0_8_13_to_0_8_25
1436
- elif minor == 8 and 26 <= patch <= 29:
1437
- yul_optimizer_steps = solc0_8_26_to_0_8_29
1489
+ elif minor == 8 and 26 <= patch <= 30:
1490
+ yul_optimizer_steps = solc0_8_26_to_0_8_30
1438
1491
  assert yul_optimizer_steps is not None, \
1439
1492
  'Yul Optimizer steps missing for requested Solidity version. Please contact Certora team.'
1440
1493
 
@@ -1456,7 +1509,7 @@ class CertoraBuildGenerator:
1456
1509
  for opt_pass in self.context.disable_solc_optimizers:
1457
1510
  settings_dict["optimizer"]["details"][opt_pass] = False
1458
1511
 
1459
- def _fill_codegen_related_options(self, contract_file_path: str, settings_dict: Dict[str, Any],
1512
+ def _fill_codegen_related_options(self, contract_file_path: Path, settings_dict: Dict[str, Any],
1460
1513
  compiler_collector: CompilerCollector) -> None:
1461
1514
  """
1462
1515
  Fills options that control how we call solc and affect the bytecode in some way
@@ -1467,6 +1520,9 @@ class CertoraBuildGenerator:
1467
1520
  self._handle_via_ir(contract_file_path, settings_dict)
1468
1521
  self._handle_evm_version(contract_file_path, settings_dict)
1469
1522
  self._handle_optimize(contract_file_path, settings_dict, compiler_collector)
1523
+ compiler_lang = compiler_collector.smart_contract_lang
1524
+ if compiler_lang == CompilerLangVy():
1525
+ self._handle_venom(contract_file_path, settings_dict)
1470
1526
 
1471
1527
  @staticmethod
1472
1528
  def solc_setting_optimizer_runs(settings_dict: Dict[str, Any]) -> Tuple[bool, Optional[int]]:
@@ -1500,7 +1556,8 @@ class CertoraBuildGenerator:
1500
1556
  contract_file_posix_abs: Path,
1501
1557
  contract_file_as_provided: str,
1502
1558
  remappings: List[str],
1503
- compiler_collector: CompilerCollector) -> Dict[str, Any]:
1559
+ compiler_collector: CompilerCollector,
1560
+ compile_wd: Path) -> Dict[str, Any]:
1504
1561
  """
1505
1562
  when calling solc with the standard_json api, instead of passing it flags, we pass it json to request what we
1506
1563
  want -- currently we only use this to retrieve storage layout as this is the only way to do that,
@@ -1511,18 +1568,22 @@ class CertoraBuildGenerator:
1511
1568
  @param compiler_collector: Solidity or Vyper compiler collector
1512
1569
  @return:
1513
1570
  """
1571
+ solc_json_contract_key = os.path.relpath(contract_file_as_provided, compile_wd) if self.context.use_relpaths_for_solc_json else contract_file_posix_abs
1514
1572
  compiler_collector_lang = compiler_collector.smart_contract_lang
1515
1573
  if compiler_collector_lang == CompilerLangSol() or compiler_collector_lang == CompilerLangYul():
1516
- sources_dict = {str(contract_file_posix_abs): {
1574
+ sources_dict = {str(solc_json_contract_key): {
1517
1575
  "urls": [str(contract_file_posix_abs)]}} # type: Dict[str, Dict[str, Any]]
1518
1576
  output_selection = ["transientStorageLayout", "storageLayout", "abi", "evm.bytecode",
1519
- "evm.deployedBytecode", "evm.methodIdentifiers", "evm.assembly"]
1577
+ "evm.deployedBytecode", "evm.methodIdentifiers", "evm.assembly",
1578
+ "evm.bytecode.functionDebugData"]
1520
1579
  ast_selection = ["id", "ast"]
1521
1580
  elif compiler_collector_lang == CompilerLangVy():
1522
1581
  with open(contract_file_posix_abs) as f:
1523
1582
  contents = f.read()
1524
1583
  sources_dict = {str(contract_file_posix_abs): {"content": contents}}
1525
1584
  output_selection = ["abi", "evm.bytecode", "evm.deployedBytecode", "evm.methodIdentifiers"]
1585
+ if compiler_collector.compiler_version >= (0, 4, 4):
1586
+ output_selection += ["metadata", "evm.deployedBytecode.symbolMap"]
1526
1587
  ast_selection = ["ast"]
1527
1588
 
1528
1589
  settings_dict: Dict[str, Any] = \
@@ -1536,7 +1597,7 @@ class CertoraBuildGenerator:
1536
1597
  }
1537
1598
  }
1538
1599
 
1539
- self._fill_codegen_related_options(contract_file_as_provided, settings_dict, compiler_collector)
1600
+ self._fill_codegen_related_options(Path(contract_file_as_provided), settings_dict, compiler_collector)
1540
1601
 
1541
1602
  result_dict = {"language": compiler_collector_lang.name, "sources": sources_dict, "settings": settings_dict}
1542
1603
  # debug_print("Standard json input")
@@ -1739,6 +1800,10 @@ class CertoraBuildGenerator:
1739
1800
  sdc_name = f"{Path(build_arg_contract_file).name}_{file_index}"
1740
1801
  compilation_path = self.get_compilation_path(sdc_name)
1741
1802
  self.file_to_sdc_name[Util.abs_norm_path(build_arg_contract_file)] = sdc_name
1803
+
1804
+ compiler_collector = self.compiler_coll_factory \
1805
+ .get_compiler_collector(Path(path_for_compiler_collector_file))
1806
+
1742
1807
  # update remappings and collect_cmd:
1743
1808
  if not is_vyper:
1744
1809
  Util.safe_create_dir(compilation_path)
@@ -1786,8 +1851,10 @@ class CertoraBuildGenerator:
1786
1851
  compiler_logger.debug(f"collect_cmd: {collect_cmd}\n")
1787
1852
  else:
1788
1853
  compiler_ver_to_run = get_relevant_compiler(Path(build_arg_contract_file), self.context)
1789
-
1790
- collect_cmd = f'{compiler_ver_to_run} -p "{self.context.solc_allow_path}" -o "{compilation_path}" ' \
1854
+ path_string = ""
1855
+ if compiler_collector.compiler_version[1] < 4:
1856
+ path_string = f' -p "{self.context.solc_allow_path}"'
1857
+ collect_cmd = f'{compiler_ver_to_run}{path_string} -o "{compilation_path}" ' \
1791
1858
  f'--standard-json'
1792
1859
 
1793
1860
  # Make sure compilation artifacts are always deleted
@@ -1800,13 +1867,10 @@ class CertoraBuildGenerator:
1800
1867
  # (we do not try to save a big chain history of changes, just a previous and current)
1801
1868
  self.backup_compiler_outputs(sdc_name, smart_contract_lang, "prev")
1802
1869
 
1803
- compiler_collector = self.compiler_coll_factory \
1804
- .get_compiler_collector(Path(path_for_compiler_collector_file))
1805
-
1806
1870
  # Standard JSON
1807
1871
  remappings = [] if isinstance(compiler_collector, CompilerCollectorYul) else self.context.remappings
1808
1872
  input_for_solc = self.standard_json(Path(file_abs_path), build_arg_contract_file, remappings,
1809
- compiler_collector)
1873
+ compiler_collector, compile_wd)
1810
1874
  standard_json_input = json.dumps(input_for_solc).encode("utf-8")
1811
1875
  compiler_logger.debug(f"about to run in {compile_wd} the command: {collect_cmd}")
1812
1876
  compiler_logger.debug(f"solc input = {json.dumps(input_for_solc, indent=4)}")
@@ -1862,7 +1926,7 @@ class CertoraBuildGenerator:
1862
1926
  contract_file_abs = str(Util.abs_norm_path(contract_file))
1863
1927
 
1864
1928
  # using os.path.relpath because Path.relative_to cannot go up the directory tree (no ..)
1865
- contract_file_rel = os.path.relpath(Path(contract_file_abs), Path.cwd())
1929
+ contract_file_rel = os.path.relpath(Path(contract_file_abs), compile_wd)
1866
1930
 
1867
1931
  build_logger.debug(f"available keys: {data['contracts'].keys()}")
1868
1932
  if contract_file_rel in data[CONTRACTS]:
@@ -1961,7 +2025,15 @@ class CertoraBuildGenerator:
1961
2025
  srclist = {"0": file_abs_path}
1962
2026
  report_srclist = {"0": file_abs_path}
1963
2027
 
1964
- report_source_file = report_srclist[[idx for idx in srclist if srclist[idx] == file_abs_path][0]]
2028
+ # Annoyingly, this is currently used just for... presentation?!
2029
+ # We should clean up report_source_file from all places...
2030
+
2031
+ build_logger.debug(f"Finding report source file, abs path {file_abs_path} relative to {compile_wd}")
2032
+ if self.context.use_relpaths_for_solc_json:
2033
+ orig_report_source_file = Util.abs_posix_path(os.path.relpath(file_abs_path, compile_wd))
2034
+ else:
2035
+ orig_report_source_file = file_abs_path
2036
+ report_source_file = report_srclist[[idx for idx in srclist if Util.abs_posix_path(srclist[idx]) == orig_report_source_file][0]]
1965
2037
 
1966
2038
  # all "contracts in SDC" are the same for every primary contract of the compiled file.
1967
2039
  # we can therefore compute those just once...
@@ -1992,8 +2064,9 @@ class CertoraBuildGenerator:
1992
2064
 
1993
2065
  build_logger.debug(f"finding primary contract address of {file_compiler_path}:{primary_contract} in "
1994
2066
  f"{contracts_with_chosen_addresses}")
2067
+ path_to_find = os.path.relpath(file_compiler_path, compile_wd) if self.context.use_relpaths_for_solc_json else file_compiler_path
1995
2068
  primary_contract_address = \
1996
- self.find_contract_address_str(file_compiler_path,
2069
+ self.find_contract_address_str(path_to_find,
1997
2070
  primary_contract,
1998
2071
  contracts_with_chosen_addresses)
1999
2072
  build_logger.debug(f"Contracts in SDC {sdc_name}: {[contract.name for contract in contracts_in_sdc]}")
@@ -2039,7 +2112,7 @@ class CertoraBuildGenerator:
2039
2112
  if new_path is None:
2040
2113
  file_abs_path = Util.abs_posix_path(build_arg_contract_file)
2041
2114
  # for vyper, because there are no autofinders, at least find the main file
2042
- if (orig_file == file_abs_path and
2115
+ if (Util.abs_posix_path(orig_file) == file_abs_path and
2043
2116
  ((Util.get_certora_sources_dir() / build_arg_contract_file).exists() or
2044
2117
  Path(build_arg_contract_file).exists())):
2045
2118
  new_srclist_map[idx] = build_arg_contract_file
@@ -2054,6 +2127,7 @@ class CertoraBuildGenerator:
2054
2127
  report_srclist = srclist
2055
2128
  else:
2056
2129
  report_srclist = srclist
2130
+ build_logger.debug(f"Report source list={report_srclist}")
2057
2131
  return report_srclist
2058
2132
 
2059
2133
  def get_bytecode(self,
@@ -2061,7 +2135,7 @@ class CertoraBuildGenerator:
2061
2135
  contract_name: str,
2062
2136
  primary_contracts: List[str],
2063
2137
  contracts_with_chosen_addresses: List[Tuple[int, Any]],
2064
- fail_if_no_bytecode: bool
2138
+ is_deployed_bytecode: bool
2065
2139
  ) -> str:
2066
2140
  """
2067
2141
  Computes the linked bytecode object from the Solidity compiler output.
@@ -2072,20 +2146,31 @@ class CertoraBuildGenerator:
2072
2146
  @param primary_contracts - the names of the primary contracts we check to have a bytecode
2073
2147
  @param contracts_with_chosen_addresses - a list of tuples of addresses and the
2074
2148
  associated contract identifier
2075
- @param fail_if_no_bytecode - true if the function should fail if bytecode object is missing,
2149
+ @param is_deployed_bytecode - true if we deal with deployed (runtime) bytecode,
2150
+ thus the function should fail if bytecode object is missing,
2076
2151
  false otherwise
2077
2152
  @returns linked bytecode object
2078
2153
  """
2079
2154
  # TODO: Only contract_name should be necessary. This requires a lot more test cases to make sure we're not
2080
2155
  # missing any weird solidity outputs.
2081
2156
  bytecode_ = bytecode_object["object"]
2082
- bytecode = self.collect_and_link_bytecode(contract_name, contracts_with_chosen_addresses,
2083
- bytecode_, bytecode_object.get("linkReferences", {}))
2157
+ try:
2158
+ bytecode = self.collect_and_link_bytecode(contract_name, contracts_with_chosen_addresses,
2159
+ bytecode_, bytecode_object.get("linkReferences", {}))
2160
+ except Util.CertoraUserInputError as e:
2161
+ if is_deployed_bytecode:
2162
+ raise e
2163
+ else:
2164
+ prefix_msg = f"Failed to find a dependency library while building the "\
2165
+ f"constructor bytecode of {contract_name}. "\
2166
+ f"If you need the contract, import the missing library directly and add a dummy usage:\n"
2167
+ orig_msg = str(e)
2168
+ raise Util.CertoraUserInputError(prefix_msg + orig_msg)
2084
2169
  if contract_name in primary_contracts and len(bytecode) == 0:
2085
2170
  msg = f"Contract {contract_name} has no bytecode. " \
2086
2171
  f"It may be caused because the contract is abstract, " \
2087
2172
  f"or is missing constructor code. Please check the output of the Solidity compiler."
2088
- if fail_if_no_bytecode:
2173
+ if is_deployed_bytecode:
2089
2174
  raise Util.CertoraUserInputError(msg)
2090
2175
  else:
2091
2176
  build_logger.warning(msg)
@@ -2155,6 +2240,40 @@ class CertoraBuildGenerator:
2155
2240
  storage_slot['descriptor'] = type_descriptor.as_dict()
2156
2241
  return storage_data
2157
2242
 
2243
+ @staticmethod
2244
+ def add_vyper_internal_function_data(internal_funcs: Set[Func], contract_data: Dict[str, Any]) -> None:
2245
+ metadata = contract_data["metadata"]
2246
+ symbol_map = contract_data["evm"]["deployedBytecode"]["symbolMap"]
2247
+
2248
+ for internal_func in internal_funcs:
2249
+ # find metadata entry
2250
+ func_name = internal_func.name
2251
+ func_info = metadata['function_info']
2252
+ metadata_func_info = None
2253
+ for value in func_info.values():
2254
+ if value.get("name") == func_name:
2255
+ metadata_func_info = value
2256
+ break
2257
+ assert metadata_func_info is not None, f"Could not find metadata for internal function {func_name}"
2258
+ vyper_metadata = VyperMetadata()
2259
+ if metadata_func_info.get('frame_info'):
2260
+ vyper_metadata.frame_size = metadata_func_info['frame_info']['frame_size']
2261
+ vyper_metadata.frame_start = metadata_func_info['frame_info']['frame_start']
2262
+ if metadata_func_info.get('venom_via_stack'):
2263
+ vyper_metadata.venom_via_stack = metadata_func_info['venom_via_stack']
2264
+ if metadata_func_info.get('venom_return_via_stack'):
2265
+ vyper_metadata.venom_return_via_stack = metadata_func_info['venom_return_via_stack']
2266
+ pattern_in_symbol_map = re.compile(fr"{func_name}\(.*\)_runtime$")
2267
+ matches = [k for k in symbol_map if pattern_in_symbol_map.search(k)]
2268
+ if len(matches) == 0:
2269
+ build_logger.warning(f"Could not find symbol map entry for {func_name} probably was inlined")
2270
+ continue
2271
+ elif len(matches) > 1:
2272
+ raise RuntimeError(f"Found multiple matches for {func_name} in symbol map: {matches}")
2273
+ else:
2274
+ vyper_metadata.runtime_start_pc = symbol_map[matches[0]]
2275
+ internal_func.vyper_metadata = vyper_metadata
2276
+
2158
2277
  def get_contract_in_sdc(self,
2159
2278
  source_code_file: str,
2160
2279
  contract_name: str,
@@ -2190,14 +2309,17 @@ class CertoraBuildGenerator:
2190
2309
  notpayable=x.notpayable,
2191
2310
  fromLib=x.fromLib,
2192
2311
  isConstructor=x.isConstructor,
2312
+ is_free_func=False,
2193
2313
  stateMutability=x.stateMutability,
2194
2314
  visibility=x.visibility,
2195
2315
  implemented=x.implemented,
2196
2316
  overrides=x.overrides,
2317
+ virtual=False,
2197
2318
  contractName=x.contractName,
2198
2319
  ast_id=x.ast_id,
2199
2320
  source_bytes=x.source_bytes,
2200
2321
  original_file=x.original_file,
2322
+ location=None,
2201
2323
  body_location=x.body_location,
2202
2324
  ) for x in clfuncs]
2203
2325
  elif compiler_lang == CompilerLangYul():
@@ -2246,6 +2368,14 @@ class CertoraBuildGenerator:
2246
2368
  build_arg_contract_file)
2247
2369
  immutables = self.collect_immutables(contract_data, build_arg_contract_file, compiler_lang)
2248
2370
 
2371
+ internal_function_entrypoints = set([])
2372
+
2373
+ if compiler_lang == CompilerLangSol() and "functionDebugData" in contract_data["evm"]["deployedBytecode"]:
2374
+ debug = contract_data["evm"]["deployedBytecode"]["functionDebugData"]
2375
+ for (_, v) in debug.items():
2376
+ if "entryPoint" in v and v["entryPoint"] is not None:
2377
+ internal_function_entrypoints.add(v["entryPoint"])
2378
+
2249
2379
  if self.context.internal_funcs is not None:
2250
2380
  all_internal_functions: Dict[str, Any] = \
2251
2381
  Util.read_json_file(self.context.internal_funcs)
@@ -2260,7 +2390,7 @@ class CertoraBuildGenerator:
2260
2390
 
2261
2391
  if compiler_lang == CompilerLangSol():
2262
2392
  settings_dict: Dict[str, Any] = {}
2263
- self._fill_codegen_related_options(build_arg_contract_file, settings_dict,
2393
+ self._fill_codegen_related_options(Path(build_arg_contract_file), settings_dict,
2264
2394
  compiler_collector_for_contract_file)
2265
2395
  solc_optimizer_on, solc_optimizer_runs = self.solc_setting_optimizer_runs(settings_dict)
2266
2396
  solc_via_ir = self.solc_setting_via_ir(settings_dict)
@@ -2268,6 +2398,9 @@ class CertoraBuildGenerator:
2268
2398
  else:
2269
2399
  compiler_parameters = None
2270
2400
 
2401
+ if compiler_lang == CompilerLangVy() and compiler_collector_for_contract_file.compiler_version >= (0, 4, 4):
2402
+ self.add_vyper_internal_function_data(internal_funcs, contract_data)
2403
+
2271
2404
  return ContractInSDC(contract_name,
2272
2405
  # somehow make sure this is an absolute path which obeys the autofinder remappings
2273
2406
  # (i.e. make sure this file doesn't start with autoFinder_)
@@ -2296,7 +2429,8 @@ class CertoraBuildGenerator:
2296
2429
  extension_contracts=list(),
2297
2430
  local_assignments={},
2298
2431
  branches={},
2299
- requires={}
2432
+ requires={},
2433
+ internal_starts=list(internal_function_entrypoints)
2300
2434
  )
2301
2435
 
2302
2436
  @staticmethod
@@ -2389,7 +2523,7 @@ class CertoraBuildGenerator:
2389
2523
  # if no function finder mode set, determine based on viaIR enabled or not:
2390
2524
  if self.context.function_finder_mode is None:
2391
2525
  # in via-ir, should not compress
2392
- if self.context.solc_via_ir:
2526
+ if self.get_solc_via_ir_value(Path(contract_file)):
2393
2527
  should_compress = False
2394
2528
  else:
2395
2529
  should_compress = True
@@ -2398,6 +2532,19 @@ class CertoraBuildGenerator:
2398
2532
  # (deprecate this option later)
2399
2533
  should_compress = self.context.function_finder_mode == Vf.FunctionFinderMode.DEFAULT.name
2400
2534
 
2535
+ if self.context.solc_optimize and self.get_solc_via_ir_value(Path(contract_file)) and \
2536
+ any(param_name == "" for param_name in f.paramNames):
2537
+ # As reported in CERT-9399, we should not generate function finder for this edge case
2538
+
2539
+ instrumentation_logger.warning(f"Failed to generate auto finder for {f.name} @ {f.where()}.")
2540
+ instrumentation_logger.warning(
2541
+ "Cannot apply summaries for internal functions with unnamed argument when compiling "
2542
+ "using solc_optimize and solc_via_ir"
2543
+ )
2544
+ should_generate_inlining = False
2545
+ else:
2546
+ should_generate_inlining = True
2547
+
2401
2548
  if len(mods) > 0:
2402
2549
  # we need to add the instrumentation in a modifer because solidity modifiers will (potentially)
2403
2550
  # appear before any instrumentation we add to the literal source body, which will tank the detection
@@ -2409,8 +2556,10 @@ class CertoraBuildGenerator:
2409
2556
  # where in the source such modifiers will go. In order to insert a modifier, we have to have at
2410
2557
  # least one modifier already present, and then insert before the first modifier's location in the
2411
2558
  # source code
2412
- mod_inst = generate_modifier_finder(f, internal_id, function_symbol, sdc.compiler_collector,
2413
- def_node, compress=should_compress)
2559
+ mod_inst = generate_modifier_finder(
2560
+ f, internal_id, function_symbol, sdc.compiler_collector, def_node,
2561
+ compress=should_compress, should_generate_inlining=should_generate_inlining
2562
+ )
2414
2563
  if mod_inst is None:
2415
2564
  instrumentation_logger.debug(f"Modifier generation for {f.name} @ {f.where()} failed")
2416
2565
  return None
@@ -2444,8 +2593,8 @@ class CertoraBuildGenerator:
2444
2593
  per_file_inst[first_mod_offset] = Instrumentation(expected=bytes(modifier_name[0:1], "utf-8"),
2445
2594
  to_ins=modifier_invocation, mut=InsertBefore())
2446
2595
  else:
2447
- finder_res = generate_inline_finder(f, internal_id, function_symbol,
2448
- sdc.compiler_collector, should_compress)
2596
+ finder_res = generate_inline_finder(f, internal_id, function_symbol, sdc.compiler_collector,
2597
+ should_compress, should_generate_inlining)
2449
2598
  if finder_res is None:
2450
2599
  instrumentation_logger.debug(f"Generating auto finder for {f.name} @ {f.where()}"
2451
2600
  f" failed, giving up generation")
@@ -2455,6 +2604,114 @@ class CertoraBuildGenerator:
2455
2604
  mut=InsertAfter())
2456
2605
  return function_finder_by_contract, function_finder_instrumentation
2457
2606
 
2607
+ def add_internal_func_harnesses(self, contract_file: str, sdc: SDC, spec_calls: List[str]) -> Optional[Tuple[Dict[str, str], Dict[str, Dict[int, Instrumentation]]]]:
2608
+ # contract file -> byte offset -> to insert
2609
+ harness_function_instrumentation: Dict[str, Dict[int, Instrumentation]] = defaultdict(dict)
2610
+ # internal function name -> harness fuction name
2611
+ harness_function_names: Dict[str, str] = {}
2612
+
2613
+ if not isinstance(sdc.compiler_collector, CompilerCollectorSol):
2614
+ raise Exception(f"Encountered a compiler collector that is not solc for file {contract_file}"
2615
+ " when trying to add function autofinders")
2616
+ instrumentation_logger.debug(f"Using {sdc.compiler_collector} compiler to "
2617
+ f"add external function harnesses to contract {sdc.primary_contract}")
2618
+
2619
+ for c in sdc.contracts:
2620
+ for f in c.internal_funcs:
2621
+ if f"{sdc.primary_contract}.{f.name}" not in spec_calls:
2622
+ continue
2623
+
2624
+ if f.fromLib:
2625
+ # Even external library functions can't be called directly from spec,
2626
+ # so skip harnessing internal ones.
2627
+ continue
2628
+
2629
+ if f.isConstructor:
2630
+ continue
2631
+
2632
+ if f.is_free_func:
2633
+ # Free functions (declared outside of any contract/library) don't have visibility modifiers
2634
+ continue
2635
+
2636
+ orig_file = f.original_file
2637
+ if not orig_file:
2638
+ instrumentation_logger.debug(f"missing file location for {f.name}")
2639
+ continue
2640
+
2641
+ loc = f.location
2642
+ if not loc:
2643
+ instrumentation_logger.debug(f"Found a function {f.name} in "
2644
+ f"{c.name} that doesn't have a location")
2645
+ continue
2646
+
2647
+ if f.implemented:
2648
+ expected_end_char = b"}"
2649
+ else:
2650
+ # This is a virtual function with no implementation. Declare also a virtual harness so that when
2651
+ # Implementing a harness for the override we can just add the `override` keyword to the harness
2652
+ # function as well without needing to have any extra logic at that point.
2653
+ expected_end_char = b";"
2654
+
2655
+ if len(f.fullArgs) != len(f.paramNames):
2656
+ instrumentation_logger.debug(f"Do not have argument names for {f.name} in"
2657
+ f" {c.name}, giving up internal function harnessing")
2658
+ continue
2659
+
2660
+ if any(ty.location == CT.TypeLocation.STORAGE for ty in f.fullArgs + f.returns):
2661
+ instrumentation_logger.debug(f"Function {f.name} has input arguments with 'storage' location - cannot harness it")
2662
+ continue
2663
+
2664
+ instrumentation_path = str(Util.abs_norm_path(orig_file))
2665
+ per_file_inst = harness_function_instrumentation[instrumentation_path]
2666
+
2667
+ start, size, _ = loc.split(":")
2668
+ body_end_byte = int(start) + int(size) - 1
2669
+
2670
+ def get_local_type_name(ty: CT.TypeInstance) -> str:
2671
+ # Handles imports that use 'as'. E.g. `import {A as B} from "A.sol";`
2672
+ ret = ty.get_source_str()
2673
+ assert orig_file
2674
+ for node in self.asts[sdc.sdc_origin_file][orig_file].values():
2675
+ if node["nodeType"] != "ImportDirective":
2676
+ continue
2677
+ for alias in node["symbolAliases"]:
2678
+ if alias["foreign"]["name"] == ty.get_source_str() and "local" in alias:
2679
+ ret = alias["local"]
2680
+ break
2681
+
2682
+ # Now add the location
2683
+ return ret + (f" {ty.location.value}" if ty.location != CT.TypeLocation.STACK else "")
2684
+
2685
+ harness_name = f"{f.name}_external_harness"
2686
+ harness_string = f" function {harness_name}({', '.join(f'{get_local_type_name(ty)} {n}' for ty, n in zip(f.fullArgs, f.paramNames))}) external"
2687
+
2688
+ if f.stateMutability in ["pure", "view"]:
2689
+ harness_string += f" {f.stateMutability}"
2690
+
2691
+ if f.virtual:
2692
+ harness_string += " virtual"
2693
+
2694
+ if f.overrides:
2695
+ harness_string += " override"
2696
+
2697
+ if f.returns:
2698
+ harness_string += f" returns ({', '.join(get_local_type_name(r) for r in f.returns)})"
2699
+
2700
+ if f.implemented:
2701
+ harness_call = f"{f.name}({', '.join(f.paramNames)})"
2702
+ if f.returns:
2703
+ harness_string += f" {{ return {harness_call}; }}"
2704
+ else:
2705
+ harness_string += f" {{ {harness_call}; }}"
2706
+ else:
2707
+ harness_string += ";"
2708
+
2709
+ per_file_inst[body_end_byte] = Instrumentation(expected=expected_end_char, to_ins=harness_string,
2710
+ mut=InsertAfter())
2711
+ harness_function_names[f.name] = harness_name
2712
+
2713
+ return harness_function_names, harness_function_instrumentation
2714
+
2458
2715
  def cleanup(self) -> None:
2459
2716
  for sdc_name, smart_contract_lang in self.__compiled_artifacts_to_clean:
2460
2717
  self.cleanup_compiler_outputs(sdc_name, smart_contract_lang)
@@ -2510,6 +2767,16 @@ class CertoraBuildGenerator:
2510
2767
 
2511
2768
  def build(self, certora_verify_generator: CertoraVerifyGenerator) -> None:
2512
2769
  context = self.context
2770
+
2771
+ spec_calls: List[str] = []
2772
+ if context.verify and not context.disallow_internal_function_calls:
2773
+ with tempfile.NamedTemporaryFile("r", dir=Util.get_build_dir()) as tmp_file:
2774
+ try:
2775
+ Ctx.run_local_spec_check(False, context, ["-listCalls", tmp_file.name], print_errors=False)
2776
+ spec_calls = tmp_file.read().split("\n")
2777
+ except Exception as e:
2778
+ instrumentation_logger.debug(f"Failed to get calls from spec\n{e}")
2779
+
2513
2780
  self.context.remappings = []
2514
2781
  for i, build_arg_contract_file in enumerate(sorted(self.input_config.sorted_files)):
2515
2782
  build_logger.debug(f"\nbuilding file {build_arg_contract_file}")
@@ -2519,7 +2786,6 @@ class CertoraBuildGenerator:
2519
2786
  Util.print_progress_message(f"Compiling {orig_file_name}...")
2520
2787
  sdc_pre_finders = self.collect_for_file(build_arg_contract_file, i, compiler_lang, Path(os.getcwd()),
2521
2788
  path_for_compiler_collector_file, original_sdc=None)
2522
-
2523
2789
  # Build sources tree
2524
2790
  build_logger.debug("Building source tree")
2525
2791
  sources_from_pre_finder_SDCs = set()
@@ -2527,7 +2793,7 @@ class CertoraBuildGenerator:
2527
2793
  sources_from_pre_finder_SDCs |= sdc.all_contract_files
2528
2794
  sources = self.collect_sources(context, certora_verify_generator, sources_from_pre_finder_SDCs)
2529
2795
  try:
2530
- self.cwd_rel_in_sources = build_source_tree(sources, context)
2796
+ build_source_tree(sources, context)
2531
2797
  except Exception as e:
2532
2798
  build_logger.debug(f"build_source_tree failed. Sources: {sources}", exc_info=e)
2533
2799
  raise
@@ -2562,7 +2828,7 @@ class CertoraBuildGenerator:
2562
2828
  # We start by trying to instrument _all_ finders, both autofinders and source finders
2563
2829
  added_finders, all_finders_success, src_finders_gen_success, post_backup_dir = self.finders_compilation_round(
2564
2830
  build_arg_contract_file, i, ignore_patterns, path_for_compiler_collector_file, pre_backup_dir,
2565
- sdc_pre_finders, not context.disable_source_finders)
2831
+ sdc_pre_finders, not context.disable_source_finders, spec_calls)
2566
2832
 
2567
2833
  # we could have a case where source finders failed but regular finders succeeded.
2568
2834
  # e.g. if we processed the AST wrong and skipped source finders generation
@@ -2574,21 +2840,21 @@ class CertoraBuildGenerator:
2574
2840
  # let's try just the function autofinders
2575
2841
  added_finders, function_autofinder_success, _, post_backup_dir = self.finders_compilation_round(
2576
2842
  build_arg_contract_file, i, ignore_patterns, path_for_compiler_collector_file, pre_backup_dir,
2577
- sdc_pre_finders, False)
2843
+ sdc_pre_finders, False, spec_calls)
2578
2844
 
2579
2845
  if not function_autofinder_success:
2580
2846
  self.auto_finders_failed = True
2581
2847
 
2582
2848
  if not self.auto_finders_failed or not self.source_finders_failed:
2583
2849
  # setup source_dir. note that post_backup_dir must include the finders in this case
2584
- for _, _, sdc in added_finders:
2850
+ for _, _, _, sdc in added_finders:
2585
2851
  sdc.source_dir = str(post_backup_dir.relative_to(Util.get_certora_sources_dir()))
2586
2852
  sdc.orig_source_dir = str(pre_backup_dir.relative_to(Util.get_certora_sources_dir()))
2587
2853
  else:
2588
2854
  # no point in running autofinders in vyper right now
2589
- added_finders = [({}, {}, sdc_pre_finder) for sdc_pre_finder in sdc_pre_finders]
2855
+ added_finders = [({}, {}, {}, sdc_pre_finder) for sdc_pre_finder in sdc_pre_finders]
2590
2856
 
2591
- for added_func_finders, added_source_finders, sdc in added_finders:
2857
+ for added_func_finders, added_source_finders, added_internal_function_harnesses, sdc in added_finders:
2592
2858
  for contract in sdc.contracts:
2593
2859
  all_functions: List[Func] = list()
2594
2860
  for k, v in added_func_finders.items():
@@ -2596,6 +2862,8 @@ class CertoraBuildGenerator:
2596
2862
  contract.function_finders[k] = v
2597
2863
  for source_key, source_value in added_source_finders.items():
2598
2864
  contract.local_assignments[source_key] = source_value
2865
+ if contract.name == sdc.primary_contract:
2866
+ contract.internal_function_harnesses = added_internal_function_harnesses
2599
2867
  all_functions.extend(contract.methods)
2600
2868
  all_functions.extend(contract.internal_funcs)
2601
2869
  functions_unique_by_internal_rep = list() # type: List[Func]
@@ -2649,11 +2917,182 @@ class CertoraBuildGenerator:
2649
2917
 
2650
2918
  self.SDCs[self.get_sdc_key(sdc.primary_contract, sdc.primary_contract_address)] = sdc
2651
2919
 
2920
+ if self.context.dump_asts:
2921
+ self.dump_asts()
2652
2922
  self.handle_links()
2653
2923
  self.handle_struct_links()
2654
2924
  self.handle_contract_extensions()
2925
+ if self.context.storage_extension_annotation:
2926
+ self.handle_erc7201_annotations()
2655
2927
  self.handle_storage_extension_harnesses()
2656
2928
 
2929
+ def extract_slayout(self, original_file: str, ns_storage: Set[NameSpacedStorage], compiler_version: str, target_file: str) -> NewStorageInfo:
2930
+ """
2931
+ Given a file containing a contract with namespaced storage, extract the storage information
2932
+ corresponding to the namespaced types.
2933
+
2934
+ Args:
2935
+ original_file: Path to the Solidity file containing namespaced storage declarations
2936
+ ns_storage: Set of tuples (type_name, namespace) for each namespaced storage
2937
+
2938
+ Returns:
2939
+ NewStorageInfo: A tuple (fields, types) where:
2940
+ - fields: List of new fields added by the namespaced storage
2941
+ - types: Dictionary of types referenced by the fields
2942
+ """
2943
+ file_dir = Path(original_file).parent
2944
+
2945
+ # Generate a unique name for the harness contract based on the contract names in the file
2946
+ harness_name = storageExtension.generate_harness_name(original_file)
2947
+
2948
+ with tempfile.NamedTemporaryFile(mode="w+t", suffix=".sol", dir=file_dir, delete=True) as tmp_file:
2949
+ # Import the original file.
2950
+ # Note we import and don't inline the file's contents since that's how the
2951
+ # original file is accessed also in the actual code, and compiling it
2952
+ # directly can cause issues.
2953
+ tmp_file.write(f"import \"{original_file}\";\n\n")
2954
+ rel_path = os.path.relpath(tmp_file.name, Path.cwd())
2955
+ if self.context.compiler_map:
2956
+ self.context.compiler_map.update({rel_path: compiler_version}, last=False)
2957
+ # Write the harness contract with dummy fields for each namespaced storage
2958
+ var_to_slot = storageExtension.write_harness_contract(tmp_file, harness_name, ns_storage)
2959
+ tmp_file.flush()
2960
+
2961
+ if self.context.extract_storage_extension_annotation:
2962
+ # If the flag is set, save the storage extension contract
2963
+ build_dir = Util.get_build_dir()
2964
+ shutil.copyfile(
2965
+ Path(tmp_file.name),
2966
+ build_dir / f"{Path(original_file).stem}_storage_extension.sol"
2967
+ )
2968
+
2969
+ # normalize the path exactly the way collect_for_file expects it:
2970
+ abs_path = Util.abs_posix_path(tmp_file.name)
2971
+ self.context.file_to_contract[abs_path] = {harness_name}
2972
+
2973
+ def attempt_compilation() -> NewStorageInfo:
2974
+ """Helper function to compile and extract layout"""
2975
+ compile_idx = storageExtension.get_next_file_index(self.file_to_sdc_name)
2976
+ sdcs = self.collect_for_file(tmp_file.name, compile_idx, CompilerLangSol(), Path.cwd(), Util.abs_posix_path(tmp_file.name), None)
2977
+ if not sdcs:
2978
+ raise RuntimeError(f"Failed to compile harness contract for {tmp_file}")
2979
+ layout = storageExtension.extract_harness_contract_layout(sdcs, harness_name)
2980
+
2981
+ # Remap each slot according to the ERC-7201 namespace
2982
+ remapped_fields = storageExtension.remapped_fields_from_layout(layout, var_to_slot)
2983
+ return (remapped_fields, layout.get('types', {}))
2984
+
2985
+ # First attempt with original content
2986
+ try:
2987
+ return attempt_compilation()
2988
+ except Exception as e:
2989
+ if not target_file:
2990
+ build_logger.error(f"Error extracting storage layout for {original_file}: {str(e)}")
2991
+ raise
2992
+
2993
+ # Retry with target file import
2994
+ build_logger.info("First attempt failed, retrying with import of target file")
2995
+
2996
+ # Read current content
2997
+ tmp_file.seek(0)
2998
+ current_content = tmp_file.read()
2999
+
3000
+ # Prepare modified content with target import at the beginning
3001
+ rel_target_path = os.path.relpath(target_file, Path.cwd())
3002
+ modified_content = f'import "{rel_target_path}";\n{current_content}'
3003
+
3004
+ # Write modified content
3005
+ tmp_file.seek(0)
3006
+ tmp_file.truncate()
3007
+ tmp_file.write(modified_content)
3008
+ tmp_file.flush()
3009
+
3010
+ try:
3011
+ return attempt_compilation()
3012
+ except Exception as retry_e:
3013
+ build_logger.error(f"Retry also failed for {original_file}: {str(retry_e)}")
3014
+ raise retry_e
3015
+ finally:
3016
+ # Delete the key from the context
3017
+ self.context.file_to_contract.pop(abs_path, None)
3018
+
3019
+ def handle_erc7201_annotations(self) -> None:
3020
+ """
3021
+ Look for contracts that use erc-7201 namespaced storage layout
3022
+ (see https://eips.ethereum.org/EIPS/eip-7201).
3023
+
3024
+ Find contracts A s.t. A contain a type declaration with such an annotation, e.g.
3025
+ /** @custom:storage-location erc-7201:some.name.space */
3026
+ struct T { ... }
3027
+
3028
+ Then, for any contract C that has A as a base contract, _extend_ C's storage layout
3029
+ information such that it contains the information for a `T` at the slot
3030
+ erc-7201(some.name.space) as defined in the EIP.
3031
+ """
3032
+ # Find all erc7201-like contracts, generate+compile a harness & extract layout information
3033
+ # maps (path,contract) -> new storage info added by (path,contract)
3034
+ slayouts: Dict[Tuple[str, str], NewStorageInfo] = {}
3035
+
3036
+ # Scan all of the contracts (including dependencies of targets) for namespaced storage
3037
+ # layout information
3038
+ for target_file in self.context.file_paths:
3039
+ if target_file not in self.asts:
3040
+ # No AST for this file, so we can't do anything
3041
+ continue
3042
+ for (imported_file, imported_file_ast) in self.asts[target_file].items():
3043
+ for def_node in imported_file_ast.values():
3044
+ if def_node.get("nodeType") != "ContractDefinition":
3045
+ continue
3046
+
3047
+ # Construct a key for the contract definition node
3048
+ contract_name = def_node.get("name")
3049
+ key = (imported_file, contract_name)
3050
+ if key in slayouts:
3051
+ # We already have this contract's storage layout information
3052
+ continue
3053
+
3054
+ # Collect any @custom:storage-location annotations
3055
+ ns_storage = storageExtension.get_namespace_storage_from_ast(def_node)
3056
+
3057
+ if not ns_storage:
3058
+ # No namespaced storage found in this contract
3059
+ continue
3060
+
3061
+ # Now that we have all the storage layout information, extract it once
3062
+ slayouts[key] = self.extract_slayout(imported_file, ns_storage,
3063
+ get_relevant_compiler(Path(target_file), self.context),
3064
+ target_file)
3065
+
3066
+ if self.context.test == str(Util.TestValue.STORAGE_EXTENSION_LAYOUT):
3067
+ raise Util.TestResultsReady(slayouts)
3068
+
3069
+ if not slayouts:
3070
+ # No contracts with namespaced storage found
3071
+ return
3072
+
3073
+ # Finally, extend each target contract with the storage layout info from
3074
+ # all of its base contracts
3075
+ for target in self.get_primary_contracts_from_sdcs():
3076
+ if target.name not in self.context.contract_to_file:
3077
+ # This is a contract that was not compiled, so we don't have a file for it
3078
+ continue
3079
+ target_file = self.context.contract_to_file[target.name]
3080
+ base_contracts = self.retrieve_base_contracts_list(
3081
+ target_file,
3082
+ target_file if self.context.use_relpaths_for_solc_json else Util.abs_posix_path(target_file),
3083
+ target.name
3084
+ )
3085
+ extensions: Set[str] = set()
3086
+ harnesses: Dict[str, NewStorageInfo] = {}
3087
+ for base in base_contracts:
3088
+ layout = slayouts.get((base[0], base[1]))
3089
+ if layout is not None:
3090
+ extensions.add(base[1])
3091
+ harnesses[base[1]] = layout
3092
+ else:
3093
+ build_logger.warning(f"Could not find storage layout for {base[1]} in {base[0]}")
3094
+ storageExtension.apply_extensions(target, extensions, harnesses)
3095
+
2657
3096
  def handle_storage_extension_harnesses(self) -> None:
2658
3097
  def new_field_of_node(ext_instance: Any, node: Dict[str, Any]) -> Optional[Dict[str, Any]]:
2659
3098
  """
@@ -2698,8 +3137,8 @@ class CertoraBuildGenerator:
2698
3137
  return None
2699
3138
  cloned_field = storage_field_info.copy()
2700
3139
  cloned_field["slot"] = str(link_slot)
2701
- # Try to uniquify the name
2702
- cloned_field["label"] = f"certoralink_{ext_instance.name}_{var_name}"
3140
+ # Don't bother trying to uniquify the name
3141
+ cloned_field["label"] = var_name
2703
3142
  return cloned_field
2704
3143
 
2705
3144
  def handle_one_extension(storage_ext: str) -> tuple[Any, str, List[Dict[str, Any]]] :
@@ -2716,8 +3155,9 @@ class CertoraBuildGenerator:
2716
3155
 
2717
3156
  # let's try to find the AST now
2718
3157
  build_file_ast = None
3158
+ orig_path = os.path.relpath(ext_instance.original_file, Path.cwd()) if self.context.use_relpaths_for_solc_json else ext_instance.original_file
2719
3159
  for (k, v) in self.asts.items():
2720
- if ext_instance.original_file in v:
3160
+ if orig_path in v:
2721
3161
  build_file_ast = k
2722
3162
  break
2723
3163
  if build_file_ast is None:
@@ -2725,10 +3165,10 @@ class CertoraBuildGenerator:
2725
3165
 
2726
3166
  contract_def_node = self.get_contract_def_node_ref(
2727
3167
  build_file_ast,
2728
- ext_instance.original_file,
3168
+ orig_path,
2729
3169
  storage_ext
2730
3170
  )
2731
- def_node = self.asts[build_file_ast][ext_instance.original_file][contract_def_node]
3171
+ def_node = self.asts[build_file_ast][orig_path][contract_def_node]
2732
3172
  nodes = def_node.get("nodes")
2733
3173
  if not isinstance(nodes, list):
2734
3174
  raise RuntimeError(f"Couldn't find nodes for body of {storage_ext}")
@@ -2740,41 +3180,6 @@ class CertoraBuildGenerator:
2740
3180
  new_fields.append(new_field)
2741
3181
 
2742
3182
  return (ext_instance, extension_sdc_name, new_fields)
2743
-
2744
- def apply_extensions(target_contract: Any, extensions: Set[str], to_add: Dict[str, Tuple[List[Dict[str, Any]], Dict[str, Any]]]) -> None:
2745
- """
2746
- Apply the fields from each extension to the target contract,
2747
- """
2748
- if target_contract.storage_layout.get("storage") is None:
2749
- target_contract.storage_layout["storage"] = []
2750
- if target_contract.storage_layout.get("types") is None:
2751
- target_contract.storage_layout["types"] = {}
2752
- target_slots = {storage["slot"] for storage in target_contract.storage_layout["storage"]}
2753
- # Keep track of slots we've added, and error if we
2754
- # find two extensions extending the same slot
2755
- added_slots: Dict[str, str] = {}
2756
- for ext in extensions:
2757
- (new_fields, new_types) = to_add[ext]
2758
-
2759
- for f in new_fields:
2760
- # See if any of the new fields is a slot we've already added
2761
- slot = f["slot"]
2762
- if slot in added_slots:
2763
- seen = added_slots[slot]
2764
- raise Util.CertoraUserInputError(f"Slot {slot} added to {target_contract.name} by {ext} already added by {seen}")
2765
-
2766
- if slot in target_slots:
2767
- raise Util.CertoraUserInputError(f"Slot {slot} added to {target_contract.name} by {ext} already mapped by {target_contract.name}")
2768
-
2769
- added_slots[slot] = ext
2770
-
2771
- target_contract.storage_layout["storage"].extend(new_fields)
2772
-
2773
- for (new_id, new_ty) in new_types.items():
2774
- if new_id in target_contract.storage_layout["types"]:
2775
- continue
2776
- target_contract.storage_layout["types"][new_id] = new_ty
2777
-
2778
3183
  extension_contracts: Set[str] = set()
2779
3184
  storage_extensions: Dict[str, Set[str]] = defaultdict(set)
2780
3185
  storage_ext = self.context.storage_extension_harnesses
@@ -2803,7 +3208,7 @@ class CertoraBuildGenerator:
2803
3208
  sdc = self.SDCs[target_sdc]
2804
3209
  target_contract = sdc.find_contract(target)
2805
3210
  assert target_contract is not None, f"could not find contract for {target}"
2806
- apply_extensions(target_contract, extensions, extension_to_fields_and_types)
3211
+ storageExtension.apply_extensions(target_contract, extensions, extension_to_fields_and_types)
2807
3212
 
2808
3213
  def finders_compilation_round(self,
2809
3214
  build_arg_contract_file: str,
@@ -2812,12 +3217,13 @@ class CertoraBuildGenerator:
2812
3217
  path_for_compiler_collector_file: str,
2813
3218
  pre_backup_dir: Path,
2814
3219
  sdc_pre_finders: List[SDC],
2815
- with_source_finders: bool) -> Tuple[
2816
- List[Tuple[Dict[str, InternalFunc], Dict[str, UnspecializedSourceFinder], SDC]], bool, bool, Path]:
3220
+ with_source_finders: bool,
3221
+ spec_calls: List[str]) -> Tuple[
3222
+ List[Tuple[Dict[str, InternalFunc], Dict[str, UnspecializedSourceFinder], Dict[str, str], SDC]], bool, bool, Path]:
2817
3223
  added_finders_to_sdc, finders_compilation_success, source_finders_gen_success = \
2818
3224
  self.instrument_auto_finders(
2819
3225
  build_arg_contract_file, i, sdc_pre_finders,
2820
- path_for_compiler_collector_file, with_source_finders)
3226
+ path_for_compiler_collector_file, with_source_finders, spec_calls)
2821
3227
  # successful or not, we backup current .certora_sources for either debuggability, or for availability
2822
3228
  # of sources.
2823
3229
  post_backup_dir = self.get_fresh_backupdir(Util.POST_AUTOFINDER_BACKUP_DIR)
@@ -2889,11 +3295,12 @@ class CertoraBuildGenerator:
2889
3295
  def instrument_auto_finders(self, build_arg_contract_file: str, i: int,
2890
3296
  sdc_pre_finders: List[SDC],
2891
3297
  path_for_compiler_collector_file: str,
2892
- instrument_source_finders: bool) -> Tuple[
2893
- List[Tuple[Dict[str, InternalFunc], Dict[str, UnspecializedSourceFinder], SDC]], bool, bool]:
3298
+ instrument_source_finders: bool,
3299
+ spec_calls: List[str]) -> Tuple[
3300
+ List[Tuple[Dict[str, InternalFunc], Dict[str, UnspecializedSourceFinder], Dict[str, str], SDC]], bool, bool]:
2894
3301
 
2895
3302
  # initialization
2896
- ret = [] # type: List[Tuple[Dict[str, InternalFunc], Dict[str, UnspecializedSourceFinder], SDC]]
3303
+ ret: List[Tuple[Dict[str, InternalFunc], Dict[str, UnspecializedSourceFinder], Dict[str, str], SDC]] = []
2897
3304
  instrumentation_logger.debug(f"Instrumenting auto finders in {build_arg_contract_file}")
2898
3305
  # all of the [SDC]s inside [sdc_pre_finders] have the same list of [ContractInSDC]s
2899
3306
  # (generated in the [collect_from_file] function).
@@ -2902,10 +3309,19 @@ class CertoraBuildGenerator:
2902
3309
  if added_function_finders_tuple is None:
2903
3310
  instrumentation_logger.warning(
2904
3311
  f"Computing function finder instrumentation failed for {build_arg_contract_file}")
2905
- return [({}, {}, old_sdc) for old_sdc in sdc_pre_finders], False, False
3312
+ return [({}, {}, {}, old_sdc) for old_sdc in sdc_pre_finders], False, False
2906
3313
 
2907
3314
  (added_function_finders, function_instr) = added_function_finders_tuple
2908
3315
 
3316
+ instr = function_instr
3317
+
3318
+ added_internal_function_harnesses: Dict[str, str] = {}
3319
+ if not self.context.disallow_internal_function_calls:
3320
+ added_internal_func_harness_tuple = self.add_internal_func_harnesses(build_arg_contract_file, sdc_pre_finder, spec_calls)
3321
+ if added_internal_func_harness_tuple:
3322
+ instr = CertoraBuildGenerator.merge_dicts_instrumentation(function_instr, added_internal_func_harness_tuple[1])
3323
+ added_internal_function_harnesses = added_internal_func_harness_tuple[0]
3324
+
2909
3325
  source_finders_gen_succeeded = False
2910
3326
  if instrument_source_finders:
2911
3327
  try:
@@ -2914,17 +3330,24 @@ class CertoraBuildGenerator:
2914
3330
  (added_source_finders, source_instr) = added_source_finders_tuple
2915
3331
  # Update instr with additional instrumentations. Recall it is a map file -> offset -> instr.
2916
3332
  # Function finders take precedence
2917
- instr = CertoraBuildGenerator.merge_dicts_instrumentation(function_instr, source_instr)
3333
+ instr = CertoraBuildGenerator.merge_dicts_instrumentation(instr, source_instr)
2918
3334
  source_finders_gen_succeeded = True
2919
3335
  except: # noqa: E722
2920
3336
  instrumentation_logger.warning(
2921
3337
  f"Computing source finder instrumentation failed for {build_arg_contract_file}")
2922
- instr = function_instr
2923
3338
  added_source_finders = {}
2924
3339
  else:
2925
- instr = function_instr
2926
3340
  added_source_finders = {}
2927
3341
 
3342
+ try:
3343
+ casting_instrumentations, casting_types = generate_casting_instrumentation(self.asts, build_arg_contract_file, sdc_pre_finder)
3344
+ except Exception as e:
3345
+ instrumentation_logger.warning(
3346
+ f"Computing casting instrumentation failed for {build_arg_contract_file}: {e}", exc_info=True)
3347
+ casting_instrumentations, casting_types = {}, {}
3348
+
3349
+ instr = CertoraBuildGenerator.merge_dicts_instrumentation(instr, casting_instrumentations)
3350
+
2928
3351
  abs_build_arg_contract_file = Util.abs_posix_path(build_arg_contract_file)
2929
3352
  if abs_build_arg_contract_file not in instr:
2930
3353
  instrumentation_logger.debug(
@@ -2942,7 +3365,7 @@ class CertoraBuildGenerator:
2942
3365
  # instrumentation should be keyed only using absolute paths
2943
3366
  instrumentation_logger.warning(f"Already generated autofinder for {new_name}, "
2944
3367
  f"cannot instrument again for {contract_file}")
2945
- return [({}, {}, old_sdc) for old_sdc in sdc_pre_finders], False, False
3368
+ return [({}, {}, {}, old_sdc) for old_sdc in sdc_pre_finders], False, False
2946
3369
 
2947
3370
  autofinder_remappings[new_name] = contract_file
2948
3371
 
@@ -2971,19 +3394,26 @@ class CertoraBuildGenerator:
2971
3394
  instrumentation_logger.warning("Skipping source finder generation!")
2972
3395
  else:
2973
3396
  instrumentation_logger.warning("Skipping internal function finder generation!")
2974
- return [({}, {}, old_sdc) for old_sdc in sdc_pre_finders], False, False
3397
+ return [({}, {}, {}, old_sdc) for old_sdc in sdc_pre_finders], False, False
2975
3398
  to_skip = to_insert.mut.insert(to_insert.to_ins, to_insert.expected, output)
2976
3399
  if to_skip != 0:
2977
3400
  in_file.read(to_skip)
2978
3401
  read_so_far += amt + 1 + to_skip
2979
3402
  output.write(in_file.read(-1))
2980
3403
 
3404
+ library_name, funcs = casting_types.get(contract_file, ("", list()))
3405
+ if len(funcs) > 0:
3406
+ output.write(bytes(f"\nlibrary {library_name}" + "{\n", "utf8"))
3407
+ for f in funcs:
3408
+ output.write(bytes(f, "utf8"))
3409
+ output.write(bytes("}\n", "utf8"))
3410
+
2981
3411
  new_file = self.to_autofinder_file(build_arg_contract_file)
2982
3412
  self.context.file_to_contract[new_file] = self.context.file_to_contract[
2983
3413
  build_arg_contract_file]
2984
3414
 
2985
3415
  # add generated file to map attributes
2986
- for map_attr in Attrs.get_attribute_class().all_map_attrs():
3416
+ for map_attr in self.context.app.attr_class.all_map_attrs():
2987
3417
  map_attr_value = getattr(self.context, map_attr)
2988
3418
  if map_attr_value and build_arg_contract_file in map_attr_value:
2989
3419
  map_attr_value[new_file] = map_attr_value[build_arg_contract_file]
@@ -3003,13 +3433,13 @@ class CertoraBuildGenerator:
3003
3433
  for k, v in autofinder_remappings.items():
3004
3434
  self.function_finder_file_remappings[Util.abs_posix_path(k)] = Util.abs_posix_path(v)
3005
3435
  new_sdcs = self.collect_for_file(new_file, i, get_compiler_lang(build_arg_contract_file),
3006
- Util.get_certora_sources_dir() / self.cwd_rel_in_sources,
3436
+ Util.get_certora_sources_dir() / self.context.cwd_rel_in_sources,
3007
3437
  path_for_compiler_collector_file,
3008
3438
  sdc_pre_finder,
3009
3439
  fail_on_compilation_error=False,
3010
3440
  reroute_main_path=True)
3011
3441
  for new_sdc in new_sdcs:
3012
- ret.append((added_function_finders, added_source_finders, new_sdc))
3442
+ ret.append((added_function_finders, added_source_finders, added_internal_function_harnesses, new_sdc))
3013
3443
 
3014
3444
  except Util.SolcCompilationException as e:
3015
3445
  print(f"Encountered an exception generating autofinder {new_file} ({e}), falling back to original "
@@ -3018,7 +3448,7 @@ class CertoraBuildGenerator:
3018
3448
  f"falling back to the original file {Path(build_arg_contract_file).name}", exc_info=e)
3019
3449
  # clean up mutation
3020
3450
  self.function_finder_file_remappings = {}
3021
- return [({}, {}, sdc_pre_finder) for sdc_pre_finder in sdc_pre_finders], False, False
3451
+ return [({}, {}, {}, sdc_pre_finder) for sdc_pre_finder in sdc_pre_finders], False, False
3022
3452
  return ret, True, source_finders_gen_succeeded
3023
3453
 
3024
3454
  def to_autofinder_file(self, contract_file: str) -> str:
@@ -3031,7 +3461,7 @@ class CertoraBuildGenerator:
3031
3461
  contract_path = Util.abs_posix_path_obj(contract_file)
3032
3462
  rel_directory = Path(os.path.relpath(contract_file, '.')).parent
3033
3463
  contract_filename = contract_path.name
3034
- new_path = Util.get_certora_sources_dir() / self.cwd_rel_in_sources / rel_directory / contract_filename
3464
+ new_path = Util.get_certora_sources_dir() / self.context.cwd_rel_in_sources / rel_directory / contract_filename
3035
3465
  new_path.parent.mkdir(parents=True, exist_ok=True)
3036
3466
  return str(new_path)
3037
3467
 
@@ -3041,7 +3471,7 @@ class CertoraBuildGenerator:
3041
3471
  This assumes those paths can be related to cwd.
3042
3472
  """
3043
3473
  rel_to_cwd_path = Path(os.path.relpath(path, '.'))
3044
- new_path = Util.get_certora_sources_dir() / self.cwd_rel_in_sources / rel_to_cwd_path
3474
+ new_path = Util.get_certora_sources_dir() / self.context.cwd_rel_in_sources / rel_to_cwd_path
3045
3475
  return str(new_path.absolute())
3046
3476
 
3047
3477
  def handle_links(self) -> None:
@@ -3405,13 +3835,23 @@ class CertoraBuildGenerator:
3405
3835
  raise Util.CertoraUserInputError(f"collect_sources: {path_to_file} does not exist cwd - {Path.cwd()}."
3406
3836
  f"abs - {os.path.normpath(Path.cwd() / path_to_file)}")
3407
3837
 
3408
- sources = set()
3409
- sources |= sources_from_SDCs
3838
+ sources = set(sources_from_SDCs)
3839
+ if context.files:
3840
+ sources.update(Path(p) for p in context.files) # all files in "files" attribute are uploaded
3410
3841
  sources |= certora_verify_generator.get_spec_files()
3411
3842
  if Util.PACKAGE_FILE.exists():
3412
3843
  add_to_sources(Util.PACKAGE_FILE)
3413
3844
  if Util.REMAPPINGS_FILE.exists():
3414
3845
  add_to_sources(Util.REMAPPINGS_FILE)
3846
+ foundry_toml = Util.find_nearest_foundry_toml()
3847
+ if foundry_toml:
3848
+ # if we find foundry.toml we add it to source tree and, if exists, the remappings.txt file from the
3849
+ # root directory
3850
+ foundry_root = foundry_toml.parent
3851
+ add_to_sources(foundry_root / Util.FOUNDRY_TOML_FILE)
3852
+ remappings_file_in_root = foundry_root / Util.REMAPPINGS_FILE
3853
+ if remappings_file_in_root.exists():
3854
+ add_to_sources(remappings_file_in_root)
3415
3855
  if context.bytecode_jsons:
3416
3856
  for file in context.bytecode_jsons:
3417
3857
  add_to_sources(Path(file))
@@ -3437,7 +3877,21 @@ class CertoraBuildGenerator:
3437
3877
  return sources
3438
3878
 
3439
3879
  def __del__(self) -> None:
3440
- self.cleanup()
3880
+ try:
3881
+ self.cleanup()
3882
+ except ImportError:
3883
+ # Avoiding Python interpreter shutdown exceptions which are safe to ignore
3884
+ pass
3885
+
3886
+ def dump_asts(self) -> None:
3887
+ asts_dump_file = Util.get_asts_file()
3888
+ filtered_asts = {k: v for k, v in self.asts.items() if not str(k).startswith(f"{Util.get_build_dir()}")}
3889
+ with asts_dump_file.open("w+") as output_file:
3890
+ try:
3891
+ json.dump(filtered_asts, output_file, indent=4, sort_keys=True)
3892
+ except Exception as e:
3893
+ ast_logger.debug(f"Couldn't dump ASTs to {asts_dump_file}", exc_info=e)
3894
+ raise
3441
3895
 
3442
3896
 
3443
3897
  # make sure each source file exists and its path is in absolute format
@@ -3449,20 +3903,19 @@ def sources_to_abs(sources: Set[Path]) -> Set[Path]:
3449
3903
  return result
3450
3904
 
3451
3905
 
3452
- def build_source_tree(sources: Set[Path], context: CertoraContext, overwrite: bool = False) -> Path:
3906
+ def build_source_tree(sources: Set[Path], context: CertoraContext, overwrite: bool = False) -> None:
3453
3907
  """
3454
3908
  Copies files to .certora_sources
3455
- @returns the cwd relative in sources
3456
3909
  """
3457
3910
  sources = sources_to_abs(sources)
3458
- cwd_rel_in_sources, common_path = CertoraBuildGenerator.get_cwd_rel_in_sources(sources)
3911
+ context.cwd_rel_in_sources, context.common_path = CertoraBuildGenerator.get_cwd_rel_in_sources(sources)
3459
3912
 
3460
3913
  for source_path in sources:
3461
3914
  is_dir = source_path.is_dir()
3462
3915
  # copy file to the path of the file from the common root under the sources directory
3463
3916
 
3464
3917
  # make sure directory exists
3465
- target_path = Util.get_certora_sources_dir() / source_path.relative_to(common_path)
3918
+ target_path = Util.get_certora_sources_dir() / source_path.relative_to(context.common_path)
3466
3919
  target_directory = target_path if is_dir else target_path.parent
3467
3920
  try:
3468
3921
  target_directory.mkdir(parents=True, exist_ok=True)
@@ -3477,7 +3930,7 @@ def build_source_tree(sources: Set[Path], context: CertoraContext, overwrite: bo
3477
3930
 
3478
3931
  try:
3479
3932
  if overwrite:
3480
- # expecting target path to exist.
3933
+ # expecting the target path to exist.
3481
3934
  if target_path.exists():
3482
3935
  build_logger.debug(f"Overwriting {target_path} by copying from {source_path}")
3483
3936
  else:
@@ -3491,16 +3944,24 @@ def build_source_tree(sources: Set[Path], context: CertoraContext, overwrite: bo
3491
3944
  raise
3492
3945
 
3493
3946
  # the empty file .cwd is written in the source tree to denote the current working directory
3494
- cwd_file_path = Util.get_certora_sources_dir() / cwd_rel_in_sources / Util.CWD_FILE
3947
+ cwd_file_path = Util.get_certora_sources_dir() / context.cwd_rel_in_sources / Util.CWD_FILE
3495
3948
  cwd_file_path.parent.mkdir(parents=True, exist_ok=True)
3496
3949
  cwd_file_path.touch()
3497
3950
 
3498
- # the empty file .project_directory is written in the source tree to denote the projecct directory
3951
+ # copy context.forge_remappings to remappings.txt in the source tree, overwriting any existing remappings.txt
3952
+ forge_remappings = getattr(context, 'forge_remappings', None)
3953
+ if forge_remappings:
3954
+ remappings_file_path = Util.get_certora_sources_dir() / context.cwd_rel_in_sources / Util.REMAPPINGS_FILE
3955
+ with remappings_file_path.open("w") as remap_file:
3956
+ for remap in context.forge_remappings:
3957
+ remap_file.write(remap + "\n")
3958
+
3959
+ # the empty file .project_directory is written in the source tree to denote the project directory
3499
3960
  rust_proj_dir = getattr(context, 'rust_project_directory', None)
3500
3961
  if rust_proj_dir:
3501
3962
  proj_dir_parent_relative = os.path.relpath(rust_proj_dir, os.getcwd())
3502
3963
  assert Path(rust_proj_dir).is_dir(), f"build_source_tree: not a directory {rust_proj_dir}"
3503
- proj_dir_parent_file = (Util.get_certora_sources_dir() / cwd_rel_in_sources / proj_dir_parent_relative /
3964
+ proj_dir_parent_file = (Util.get_certora_sources_dir() / context.cwd_rel_in_sources / proj_dir_parent_relative /
3504
3965
  Util.PROJECT_DIR_FILE)
3505
3966
  proj_dir_parent_file.parent.mkdir(parents=True, exist_ok=True)
3506
3967
  proj_dir_parent_file.touch()
@@ -3516,10 +3977,8 @@ def build_source_tree(sources: Set[Path], context: CertoraContext, overwrite: bo
3516
3977
  build_logger.debug("Couldn't copy repro conf to certora sources.", exc_info=e)
3517
3978
  raise
3518
3979
 
3519
- return cwd_rel_in_sources
3520
-
3521
-
3522
- def build_from_scratch(certora_build_generator: CertoraBuildGenerator,
3980
+ def build_from_scratch(context: CertoraContext,
3981
+ certora_build_generator: CertoraBuildGenerator,
3523
3982
  certora_verify_generator: CertoraVerifyGenerator,
3524
3983
  build_cache_enabled: bool) -> CachedFiles:
3525
3984
  """
@@ -3545,10 +4004,17 @@ def build_from_scratch(certora_build_generator: CertoraBuildGenerator,
3545
4004
  may_store_in_build_cache = True
3546
4005
  absolute_sources_dir = Util.get_certora_sources_dir().absolute()
3547
4006
  for sdc in certora_build_generator.SDCs.values():
4007
+
4008
+ # add to cache also source files that were found in the SDCs (e.g., storage extensions)
4009
+ paths_set = sdc.all_contract_files
4010
+ for f in context.files:
4011
+ path = f.split(':')[0] # `f` is either 'path/to/file.sol' or 'path/to/file.sol:ContractName'
4012
+ paths_set.add(Path(path).absolute())
4013
+
3548
4014
  # the contract files in SDCs are relative to .certora_sources. Which isn't good for us here.
3549
4015
  # Need to be relative to original paths
3550
- for f in sdc.all_contract_files:
3551
- if is_relative_to(f, absolute_sources_dir):
4016
+ for f in paths_set:
4017
+ if f.is_relative_to(absolute_sources_dir):
3552
4018
  rel_f = f.relative_to(absolute_sources_dir)
3553
4019
  else:
3554
4020
  # may be an absolute path already outside .certora_sources, so we can keep it.
@@ -3583,61 +4049,84 @@ def build_from_scratch(certora_build_generator: CertoraBuildGenerator,
3583
4049
 
3584
4050
  def build_from_cache_or_scratch(context: CertoraContext,
3585
4051
  certora_build_generator: CertoraBuildGenerator,
3586
- certora_verify_generator: CertoraVerifyGenerator,
3587
- certora_build_cache_manager: CertoraBuildCacheManager) \
3588
- -> Tuple[bool, bool, CachedFiles]:
4052
+ certora_verify_generator: CertoraVerifyGenerator) \
4053
+ -> Tuple[bool, CachedFiles]:
3589
4054
  """
3590
4055
  Builds either from cache (fast path) or from scratch (slow path)
3591
- @returns 1st tuple element whether there was a cache hit or not
3592
- @returns 2nd tuple element whether the build cache is enabled and applicable
3593
- @returns 3rd tuple element the artifacts of the build to potentially be cached
4056
+ @returns 1st tuple element whether the cache should be saved
4057
+ @returns 2nd tuple element the artifacts of the build to potentially be cached
3594
4058
  """
3595
- cache_hit = False
3596
4059
  cached_files: Optional[CachedFiles] = None
3597
4060
 
3598
4061
  if not context.build_cache:
3599
- cached_files = build_from_scratch(certora_build_generator,
4062
+ cached_files = build_from_scratch(context, certora_build_generator,
3600
4063
  certora_verify_generator,
3601
4064
  False)
3602
- return cache_hit, False, cached_files
4065
+ return False, cached_files
3603
4066
 
3604
- build_cache_applicable = certora_build_cache_manager.cache_is_applicable(context)
4067
+ build_cache_applicable = CertoraBuildCacheManager.cache_is_applicable(context)
3605
4068
 
3606
4069
  if not build_cache_applicable:
3607
- build_cache_disabling_options = certora_build_cache_manager.cache_disabling_options(context)
4070
+ build_cache_disabling_options = CertoraBuildCacheManager.cache_disabling_options(context)
3608
4071
  build_logger.warning("Requested to enable the build cache, but the build cache is not applicable "
3609
4072
  f"to this run because of the given options: {build_cache_disabling_options}")
3610
- cached_files = build_from_scratch(certora_build_generator,
4073
+ cached_files = build_from_scratch(context, certora_build_generator,
3611
4074
  certora_verify_generator,
3612
4075
  False)
3613
- return cache_hit, False, cached_files
3614
-
3615
- cached_files = certora_build_cache_manager.build_from_cache(context)
3616
- # if no match, will rebuild from scratch
3617
- if cached_files is not None:
3618
- # write to .certora_build.json
3619
- shutil.copyfile(cached_files.certora_build_file, Util.get_certora_build_file())
3620
- # write build_output_props file
3621
- shutil.copyfile(cached_files.build_output_props_file, Util.get_built_output_props_file())
3622
- # write build_cache indicator file
3623
- with open(Util.get_build_cache_indicator_file(), "w+") as indicator_handle:
3624
- json.dump({"build_cache_hit": True}, indicator_handle)
3625
- # write in sources all the additional paths found
3626
- for p in cached_files.path_with_additional_included_files.glob("*"):
3627
- if p.is_dir():
3628
- Util.safe_copy_folder(p,
3629
- Util.get_certora_sources_dir() / p.name,
3630
- shutil.ignore_patterns())
3631
- else:
3632
- shutil.copyfile(p, Util.get_certora_sources_dir() / p.name)
3633
- cache_hit = True
3634
- else:
3635
- # rebuild
3636
- cached_files = build_from_scratch(certora_build_generator,
4076
+ return False, cached_files
4077
+
4078
+ cached_files = CertoraBuildCacheManager.build_from_cache(context)
4079
+
4080
+ if not cached_files:
4081
+ # No match, rebuild
4082
+ cached_files = build_from_scratch(context, certora_build_generator,
3637
4083
  certora_verify_generator,
3638
4084
  True)
4085
+ return True, cached_files
4086
+
4087
+ # Cache hit!
4088
+
4089
+ # write to .certora_build.json
4090
+ # This must happen _before_ searching for new internal function calls - the typechecker needs this file.
4091
+ shutil.copyfile(cached_files.certora_build_file, Util.get_certora_build_file())
4092
+
4093
+ # Check whether there are any new internal function calls in the spec file - if there are then we need to build
4094
+ # from scratch in order to create the relevant harness function.
4095
+ if context.verify and not context.disallow_internal_function_calls:
4096
+ with tempfile.NamedTemporaryFile("r", dir=Util.get_build_dir()) as tmp_file:
4097
+ internal_calls = []
4098
+ try:
4099
+ Ctx.run_local_spec_check(True, context, ["-listCalls", tmp_file.name], print_errors=False)
4100
+ output = tmp_file.read().strip()
4101
+ if output:
4102
+ internal_calls = output.split("\n")
4103
+ except Exception as e:
4104
+ instrumentation_logger.debug(f"Failed to get calls from spec\n{e}")
4105
+
4106
+ if internal_calls:
4107
+ build_logger.info("Found new internal calls in the spec file, need to recompile anyway")
4108
+ # There are new internal calls in the spec, we need to rebuild in order to generate the
4109
+ # external harness functions for them.
4110
+ cached_files = build_from_scratch(context, certora_build_generator,
4111
+ certora_verify_generator,
4112
+ True)
4113
+ return True, cached_files
4114
+
4115
+ # write build_output_props file
4116
+ shutil.copyfile(cached_files.build_output_props_file, Util.get_built_output_props_file())
4117
+ # write build_cache indicator file
4118
+ with open(Util.get_build_cache_indicator_file(), "w+") as indicator_handle:
4119
+ json.dump({"build_cache_hit": True}, indicator_handle)
4120
+ # write in sources all the additional paths found
4121
+ for p in cached_files.path_with_additional_included_files.glob("*"):
4122
+ if p.is_dir():
4123
+ Util.safe_copy_folder(p,
4124
+ Util.get_certora_sources_dir() / p.name,
4125
+ shutil.ignore_patterns())
4126
+ else:
4127
+ shutil.copyfile(p, Util.get_certora_sources_dir() / p.name)
3639
4128
 
3640
- return cache_hit, True, cached_files
4129
+ return False, cached_files
3641
4130
 
3642
4131
 
3643
4132
  def build(context: CertoraContext, ignore_spec_syntax_check: bool = False) -> None:
@@ -3651,7 +4140,7 @@ def build(context: CertoraContext, ignore_spec_syntax_check: bool = False) -> No
3651
4140
 
3652
4141
  try:
3653
4142
  input_config = InputConfig(context)
3654
-
4143
+ context.main_cache_key = CertoraBuildCacheManager.get_main_cache_key(context)
3655
4144
  # Create generators
3656
4145
  certora_build_generator = CertoraBuildGenerator(input_config, context)
3657
4146
 
@@ -3661,19 +4150,17 @@ def build(context: CertoraContext, ignore_spec_syntax_check: bool = False) -> No
3661
4150
 
3662
4151
  # Start by syntax checking, if we're in the right mode
3663
4152
  if Cv.mode_has_spec_file(context) and not context.build_only and not ignore_spec_syntax_check:
3664
- attr = context.disable_local_typechecking
3665
- if attr:
3666
- build_logger.warning(
3667
- "Local checks of CVL specification files disabled. It is recommended to enable the checks.")
3668
- else:
4153
+ if Ctx.should_run_local_speck_check(context):
3669
4154
  Ctx.run_local_spec_check(False, context)
3670
4155
 
3671
- certora_build_cache_manager = CertoraBuildCacheManager()
4156
+ should_save_cache, cached_files = build_from_cache_or_scratch(context,
4157
+ certora_build_generator,
4158
+ certora_verify_generator)
3672
4159
 
3673
- cache_hit, build_cache_enabled, cached_files = build_from_cache_or_scratch(context,
3674
- certora_build_generator,
3675
- certora_verify_generator,
3676
- certora_build_cache_manager)
4160
+ # avoid running the same test over and over again for each split run, context.split_rules is true only for
4161
+ # the first run and is set to [] for split runs
4162
+ if Ctx.should_run_local_speck_check(context) and context.split_rules:
4163
+ Ctx.run_local_spec_check(True, context)
3677
4164
 
3678
4165
  # .certora_verify.json is always constructed even if build cache is enabled
3679
4166
  # Sources construction should only happen when rebuilding
@@ -3687,8 +4174,8 @@ def build(context: CertoraContext, ignore_spec_syntax_check: bool = False) -> No
3687
4174
  build_logger.debug("build_source_tree failed", exc_info=e)
3688
4175
 
3689
4176
  # save in build cache
3690
- if not cache_hit and build_cache_enabled and cached_files.may_store_in_build_cache:
3691
- certora_build_cache_manager.save_build_cache(context, cached_files)
4177
+ if should_save_cache and cached_files.may_store_in_build_cache:
4178
+ CertoraBuildCacheManager.save_build_cache(context, cached_files)
3692
4179
 
3693
4180
  certora_verify_generator.update_certora_verify_struct(True)
3694
4181
  certora_verify_generator.dump() # second dump with properly rooted specs