certora-cli 8.3.1__tar.gz → 8.5.1__tar.gz

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 (89) hide show
  1. {certora_cli-8.3.1/certora_cli.egg-info → certora_cli-8.5.1}/PKG-INFO +2 -2
  2. certora_cli-8.5.1/README.md +1 -0
  3. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/Compiler/CompilerCollectorVy.py +3 -3
  4. certora_cli-8.5.1/certora_cli/CertoraProver/castingInstrumenter.py +192 -0
  5. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraBuild.py +129 -23
  6. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraBuildCacheManager.py +6 -3
  7. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraBuildDataClasses.py +1 -0
  8. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraCloudIO.py +4 -1
  9. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraCollectRunMetadata.py +29 -0
  10. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraCompilerParameters.py +11 -0
  11. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraContext.py +2 -2
  12. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraContextAttributes.py +63 -0
  13. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraContextValidator.py +0 -2
  14. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraContractFuncs.py +28 -1
  15. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/Shared/certoraUtils.py +23 -3
  16. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/Shared/certoraValidateFuncs.py +14 -8
  17. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/Shared/proverCommon.py +4 -0
  18. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/certoraRun.py +9 -1
  19. certora_cli-8.5.1/certora_cli/certoraSuiProver.py +93 -0
  20. {certora_cli-8.3.1 → certora_cli-8.5.1/certora_cli.egg-info}/PKG-INFO +2 -2
  21. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli.egg-info/SOURCES.txt +2 -0
  22. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli.egg-info/entry_points.txt +1 -0
  23. certora_cli-8.3.1/certora_jars/Typechecker.jar → certora_cli-8.5.1/certora_jars/ASTExtraction.jar +0 -0
  24. certora_cli-8.5.1/certora_jars/CERTORA-CLI-VERSION-METADATA.json +1 -0
  25. certora_cli-8.3.1/certora_jars/ASTExtraction.jar → certora_cli-8.5.1/certora_jars/Typechecker.jar +0 -0
  26. {certora_cli-8.3.1 → certora_cli-8.5.1}/setup.py +3 -2
  27. certora_cli-8.3.1/README.md +0 -1
  28. certora_cli-8.3.1/certora_jars/CERTORA-CLI-VERSION-METADATA.json +0 -1
  29. {certora_cli-8.3.1 → certora_cli-8.5.1}/LICENSE +0 -0
  30. {certora_cli-8.3.1 → certora_cli-8.5.1}/MANIFEST.in +0 -0
  31. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_bins/__init__.py +0 -0
  32. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/Compiler/CompilerCollector.py +0 -0
  33. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/Compiler/CompilerCollectorFactory.py +0 -0
  34. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/Compiler/CompilerCollectorSol.py +0 -0
  35. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/Compiler/CompilerCollectorSolBased.py +0 -0
  36. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/Compiler/CompilerCollectorYul.py +0 -0
  37. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/Compiler/__init__.py +0 -0
  38. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/__init__.py +0 -0
  39. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraApp.py +0 -0
  40. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraBuildRust.py +0 -0
  41. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraBuildSui.py +0 -0
  42. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraCollectConfigurationLayout.py +0 -0
  43. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraConfigIO.py +0 -0
  44. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraContextClass.py +0 -0
  45. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraExtensionInfo.py +0 -0
  46. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraJobList.py +0 -0
  47. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraMiniSpecParser.py +0 -0
  48. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraNodeFilters.py +0 -0
  49. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraParseBuildScript.py +0 -0
  50. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraProjectScanner.py +0 -0
  51. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraSourceFinders.py +0 -0
  52. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraType.py +0 -0
  53. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/certoraVerifyGenerator.py +0 -0
  54. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/erc7201.py +0 -0
  55. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/splitRules.py +0 -0
  56. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/CertoraProver/storageExtension.py +0 -0
  57. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/EquivalenceCheck/Eq_default.conf +0 -0
  58. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/EquivalenceCheck/Eq_mc_no_out_template.spec +0 -0
  59. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/EquivalenceCheck/Eq_mc_template.spec +0 -0
  60. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/EquivalenceCheck/Eq_sanity.conf +0 -0
  61. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/EquivalenceCheck/Eq_template.spec +0 -0
  62. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/EquivalenceCheck/__init__.py +0 -0
  63. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/EquivalenceCheck/equivCheck.py +0 -0
  64. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/EquivalenceCheck/sanity.spec +0 -0
  65. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/Mutate/__init__.py +0 -0
  66. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/Mutate/mutateApp.py +0 -0
  67. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/Mutate/mutateAttributes.py +0 -0
  68. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/Mutate/mutateConstants.py +0 -0
  69. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/Mutate/mutateUtil.py +0 -0
  70. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/Mutate/mutateValidate.py +0 -0
  71. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/Shared/ExpectedComparator.py +0 -0
  72. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/Shared/__init__.py +0 -0
  73. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/Shared/certoraAttrUtil.py +0 -0
  74. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/Shared/certoraLogging.py +0 -0
  75. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/__init__.py +0 -0
  76. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/certoraCVLFormatter.py +0 -0
  77. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/certoraConcord.py +0 -0
  78. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/certoraEVMProver.py +0 -0
  79. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/certoraEqCheck.py +0 -0
  80. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/certoraMutate.py +0 -0
  81. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/certoraRanger.py +0 -0
  82. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/certoraSolanaProver.py +0 -0
  83. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/certoraSorobanProver.py +0 -0
  84. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli/rustMutator.py +0 -0
  85. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli.egg-info/dependency_links.txt +0 -0
  86. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli.egg-info/requires.txt +0 -0
  87. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_cli.egg-info/top_level.txt +0 -0
  88. {certora_cli-8.3.1 → certora_cli-8.5.1}/certora_jars/__init__.py +0 -0
  89. {certora_cli-8.3.1 → certora_cli-8.5.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: certora-cli
3
- Version: 8.3.1
3
+ Version: 8.5.1
4
4
  Summary: Runner for the Certora Prover
5
5
  Home-page: https://pypi.org/project/certora-cli
6
6
  Author: Certora
@@ -39,4 +39,4 @@ Dynamic: requires-dist
39
39
  Dynamic: requires-python
40
40
  Dynamic: summary
41
41
 
42
- Commit 33ef152. Build and Run scripts for executing the Certora Prover on Solidity smart contracts.
42
+ Commit 1738e5b. Build and Run scripts for executing the Certora Prover on Solidity smart contracts.
@@ -0,0 +1 @@
1
+ Commit 1738e5b. Build and Run scripts for executing the Certora Prover on Solidity smart contracts.
@@ -659,7 +659,7 @@ class CompilerLangVy(CompilerLang, metaclass=Singleton):
659
659
 
660
660
  @staticmethod
661
661
  def extract_type_from_enum_def(ast_enumdef_node: Dict[str, Any]) -> VyperType:
662
- fields = [n['target']['id'] for n in ast_enumdef_node['body']]
662
+ fields = [n['target']['id'] if "target" in n else n["value"]["id"] for n in ast_enumdef_node['body']]
663
663
  return CompilerLangVy.VyperTypeEnum(ast_enumdef_node['name'], fields)
664
664
 
665
665
  @staticmethod
@@ -1273,7 +1273,7 @@ class Collector:
1273
1273
  self._collect_const(v)
1274
1274
 
1275
1275
  # Extract and resolve types
1276
- type_asts = {'EnumDef', 'StructDef', 'InterfaceDef', 'Import', 'Import', 'ImportFrom'}
1276
+ type_asts = {'EnumDef', 'StructDef', 'InterfaceDef', 'Import', 'Import', 'ImportFrom', 'FlagDef'}
1277
1277
  types = [e for e in module_node['body'] if e['ast_type'] in type_asts]
1278
1278
  for t in types:
1279
1279
  self._collect_type_decl(t)
@@ -1300,7 +1300,7 @@ class Collector:
1300
1300
 
1301
1301
  def _collect_type_decl(self, type_decl_node: Dict[str, Any]) -> None:
1302
1302
  """Update self.types to add type definition from `type_decl_node`. Expects self.consts to be initialized"""
1303
- if type_decl_node['ast_type'] == 'EnumDef':
1303
+ if type_decl_node['ast_type'] in ('EnumDef', 'FlagDef'):
1304
1304
  vy_type = CompilerLangVy.extract_type_from_enum_def(type_decl_node)
1305
1305
  elif type_decl_node['ast_type'] == 'StructDef':
1306
1306
  vy_type = CompilerLangVy.extract_type_from_struct_def(type_decl_node, self.consts)
@@ -0,0 +1,192 @@
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
+
17
+ from dataclasses import dataclass
18
+ from typing import Dict, Any, Optional, Generator
19
+
20
+ from CertoraProver.Compiler.CompilerCollectorSol import CompilerCollectorSol
21
+ from CertoraProver.certoraBuildDataClasses import SDC, Instrumentation, Replace
22
+ from Shared import certoraUtils as Util
23
+
24
+
25
+ @dataclass
26
+ class IntType:
27
+ is_signed: bool
28
+ bitwidth: int
29
+
30
+ def encode(self) -> Optional[int]:
31
+ """
32
+ Encodes into a 9 bit number: lowest bit is on if [IntType] is true, and upper 8 bits are the width.
33
+ returns none if width is out of bounds.
34
+ """
35
+ if self.bitwidth <= 0 or self.bitwidth > 256:
36
+ return None
37
+ return self.bitwidth * 2 + int(self.is_signed)
38
+
39
+
40
+ def parse_int_type(type_string: str) -> Optional[IntType]:
41
+ """
42
+ Parse an integer type string of the form 'int<bitwidth>' or 'uint<bitwidth>'.
43
+ Returns IntType or None if the string doesn't match the pattern.
44
+ """
45
+ if type_string == "int":
46
+ return IntType(True, 256)
47
+ elif type_string == "uint":
48
+ return IntType(False, 256)
49
+ elif type_string.startswith("int"):
50
+ is_signed = True
51
+ bitwidth_str = type_string[3:]
52
+ elif type_string.startswith("uint"):
53
+ is_signed = False
54
+ bitwidth_str = type_string[4:]
55
+ else:
56
+ return None
57
+
58
+ try:
59
+ bitwidth = int(bitwidth_str)
60
+ return IntType(is_signed, bitwidth)
61
+ except ValueError:
62
+ return None
63
+
64
+
65
+ def is_int_type(type_string: str) -> bool:
66
+ return parse_int_type(type_string) is not None
67
+
68
+
69
+ def encode_type(type_string: str) -> int:
70
+ int_type = parse_int_type(type_string)
71
+ if int_type is None:
72
+ raise Exception(f"Invalid type string: {type_string}")
73
+ encoded = int_type.encode()
74
+ if encoded is None:
75
+ raise Exception(f"Invalid type string: {type_string}")
76
+ return encoded
77
+
78
+
79
+ @dataclass
80
+ class CastInfo:
81
+ arg_type_str: str
82
+ res_type_str: str
83
+ expr_id: int
84
+
85
+
86
+ def find_casts(ast: Dict[int, Any]) -> list[CastInfo]:
87
+ """
88
+ Search all nodes that are in functions for kind "typeConversion" with one argument.
89
+ Returns a list of CastInfo instances.
90
+ """
91
+ conversions = []
92
+ function_nodes = [node for node in ast.values() if node.get('nodeType') == 'FunctionDefinition']
93
+
94
+ for func in function_nodes:
95
+ for node in iter_all_nodes(func):
96
+ if isinstance(node, dict) and node.get("kind") == "typeConversion":
97
+ arguments = node.get("arguments", [])
98
+ if len(arguments) == 1 and isinstance(arguments[0], dict):
99
+ expression = node["expression"]
100
+ expr_node_id = expression["id"]
101
+ arg_type_str = expression["argumentTypes"][0]["typeString"]
102
+ if not is_int_type(arg_type_str):
103
+ continue
104
+ if "typeName" not in expression:
105
+ continue
106
+ expr_type_node = expression["typeName"]
107
+ if isinstance(expr_type_node, str):
108
+ expr_type_str = expr_type_node
109
+ else:
110
+ expr_type_str = expr_type_node["name"]
111
+ if not is_int_type(expr_type_str):
112
+ continue
113
+ conversions.append(CastInfo(arg_type_str, expr_type_str, expr_node_id))
114
+
115
+ return conversions
116
+
117
+
118
+ def casting_func_name(counter: int) -> str:
119
+ return f"cast_{counter}"
120
+
121
+
122
+ def generate_casting_function(assembly_prefix: str, cast_info: CastInfo, counter: int) -> str:
123
+ """
124
+ returns the text of a solidity function that does casting according to CastInfo. It also has an encoded mload
125
+ call, to be decoded later on the kotlin side if we run the `safeCasting` builtin rule.
126
+ """
127
+ conversion_string = assembly_prefix + \
128
+ "{ mstore(0xffffff6e4604afefe123321beef1b03fffffffffffffffffffff" + \
129
+ f'{"%0.4x" % counter}{"%0.4x" % encode_type(cast_info.arg_type_str)}{"%0.4x" % encode_type(cast_info.res_type_str)}, x)' + "}"
130
+ function_head = f"function {casting_func_name(counter)}({cast_info.arg_type_str} x) internal pure returns ({cast_info.res_type_str})"
131
+ return function_head + "{\n" + conversion_string + f"return {cast_info.res_type_str}(x);\n" "}\n"
132
+
133
+
134
+ def generate_casting_instrumentation(asts: Dict[str, Dict[str, Dict[int, Any]]], contract_file: str, sdc: SDC) \
135
+ -> tuple[Dict[str, Dict[int, Instrumentation]], Dict[str, tuple[str, list[str]]]]:
136
+ """
137
+ Generate instrumentation for integer type casts in Solidity code.
138
+
139
+ Returns a tuple of:
140
+ - casting_instrumentation: Maps file paths to offset->Instrumentation mappings that replace type names with
141
+ library calls to casting functions we generate.
142
+ - casting_types: Maps file paths to library_name and the text for our new casting functions. This library is added
143
+ to the end of the file.
144
+
145
+ It's not hundred precent sure this works well in combination with `compiler_map`, because the counter for library
146
+ names may reset?
147
+ """
148
+ casting_instrumentation: Dict[str, Dict[int, Instrumentation]] = dict()
149
+
150
+ if not isinstance(sdc.compiler_collector, CompilerCollectorSol):
151
+ raise Exception(f"Encountered a compiler collector that is not solc for file {contract_file}"
152
+ " when trying to add casting instrumentation")
153
+ assembly_prefix = sdc.compiler_collector.gen_memory_safe_assembly_prefix()
154
+
155
+ casting_funcs : Dict[str, tuple[str, list[str]]] = dict()
156
+ counter = 0
157
+ original_files = sorted({Util.convert_path_for_solc_import(c.original_file) for c in sdc.contracts})
158
+ for file_count, solfile in enumerate(original_files, start=1):
159
+ main_ast = asts[contract_file]
160
+ curr_file_ast = main_ast.get(solfile, dict())
161
+
162
+ per_file_inst = casting_instrumentation.setdefault(solfile, dict())
163
+ libname, per_file_casts = casting_funcs.setdefault(solfile, (f"CertoraCastingLib{file_count}", []))
164
+
165
+ casts = find_casts(curr_file_ast)
166
+ for cast_info in casts:
167
+ start_offset, src_len, file = curr_file_ast[cast_info.expr_id]["src"].split(":")
168
+ counter += 1
169
+ per_file_inst[int(start_offset)] = Instrumentation(expected=bytes(cast_info.res_type_str[0], 'utf-8'),
170
+ to_ins=f"{libname}.{casting_func_name(counter)}",
171
+ mut=Replace(len(cast_info.res_type_str)))
172
+ new_func = generate_casting_function(assembly_prefix, cast_info, counter)
173
+ per_file_casts.append(new_func)
174
+
175
+ return casting_instrumentation, casting_funcs
176
+
177
+
178
+ def iter_all_nodes(node: Any) -> Generator[Any, Optional[Any], None]:
179
+ """
180
+ Yield a node and all its subnodes in depth-first order.
181
+ Works with dict nodes that may contain nested dicts and lists.
182
+ """
183
+ yield node
184
+
185
+ if isinstance(node, dict):
186
+ for value in node.values():
187
+ if isinstance(value, (dict, list)):
188
+ yield from iter_all_nodes(value)
189
+ elif isinstance(node, list):
190
+ for item in node:
191
+ if isinstance(item, (dict, list)):
192
+ yield from iter_all_nodes(item)
@@ -20,24 +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
- from Crypto.Hash import keccak
29
- import tempfile
30
-
31
29
  from typing import Any, Dict, List, Tuple, Optional, Set, Iterator, NoReturn
32
30
 
31
+ from Crypto.Hash import keccak
33
32
 
33
+ from CertoraProver.castingInstrumenter import generate_casting_instrumentation
34
34
  from CertoraProver.certoraBuildCacheManager import CertoraBuildCacheManager, CachedFiles
35
35
  from CertoraProver.certoraBuildDataClasses import CONTRACTS, ImmutableReference, ContractExtension, ContractInSDC, SDC, \
36
36
  Instrumentation, InsertBefore, InsertAfter, UnspecializedSourceFinder, instrumentation_logger
37
37
  from CertoraProver.certoraCompilerParameters import SolcParameters
38
+ from CertoraProver.certoraContractFuncs import Func, InternalFunc, STATEMUT, SourceBytes, VyperMetadata
38
39
  from CertoraProver.certoraSourceFinders import add_source_finders
39
40
  from CertoraProver.certoraVerifyGenerator import CertoraVerifyGenerator
40
- from CertoraProver.certoraContractFuncs import Func, InternalFunc, STATEMUT, SourceBytes
41
41
 
42
42
  scripts_dir_path = Path(__file__).parent.parent.resolve() # containing directory
43
43
  sys.path.insert(0, str(scripts_dir_path))
@@ -1381,11 +1381,17 @@ class CertoraBuildGenerator:
1381
1381
 
1382
1382
  return bytecode
1383
1383
 
1384
- def get_solc_via_ir_value(self, contract_file_path: Path) -> bool:
1385
- match = Ctx.get_map_attribute_value(self.context, contract_file_path, 'solc_via_ir')
1386
- assert isinstance(match, (bool, type(None))), f"Expected solc_via_ir to be bool or None, got {type(match)}"
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
1387
  return bool(match)
1388
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
+
1389
1395
  def get_solc_evm_version_value(self, contract_file_path: Path) -> Optional[str]:
1390
1396
  match = Ctx.get_map_attribute_value(self.context, contract_file_path, 'solc_evm_version')
1391
1397
  assert isinstance(match, (str, type(None))), f"Expected solc_evm_version to be string or None, got {type(match)}"
@@ -1398,6 +1404,10 @@ class CertoraBuildGenerator:
1398
1404
  match = str(match)
1399
1405
  return match
1400
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
+
1401
1411
  def _handle_via_ir(self, contract_file_path: Path, settings_dict: Dict[str, Any]) -> None:
1402
1412
  if self.get_solc_via_ir_value(contract_file_path):
1403
1413
  settings_dict["viaIR"] = True
@@ -1510,6 +1520,9 @@ class CertoraBuildGenerator:
1510
1520
  self._handle_via_ir(contract_file_path, settings_dict)
1511
1521
  self._handle_evm_version(contract_file_path, settings_dict)
1512
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)
1513
1526
 
1514
1527
  @staticmethod
1515
1528
  def solc_setting_optimizer_runs(settings_dict: Dict[str, Any]) -> Tuple[bool, Optional[int]]:
@@ -1543,7 +1556,8 @@ class CertoraBuildGenerator:
1543
1556
  contract_file_posix_abs: Path,
1544
1557
  contract_file_as_provided: str,
1545
1558
  remappings: List[str],
1546
- compiler_collector: CompilerCollector) -> Dict[str, Any]:
1559
+ compiler_collector: CompilerCollector,
1560
+ compile_wd: Path) -> Dict[str, Any]:
1547
1561
  """
1548
1562
  when calling solc with the standard_json api, instead of passing it flags, we pass it json to request what we
1549
1563
  want -- currently we only use this to retrieve storage layout as this is the only way to do that,
@@ -1554,9 +1568,10 @@ class CertoraBuildGenerator:
1554
1568
  @param compiler_collector: Solidity or Vyper compiler collector
1555
1569
  @return:
1556
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
1557
1572
  compiler_collector_lang = compiler_collector.smart_contract_lang
1558
1573
  if compiler_collector_lang == CompilerLangSol() or compiler_collector_lang == CompilerLangYul():
1559
- sources_dict = {str(contract_file_posix_abs): {
1574
+ sources_dict = {str(solc_json_contract_key): {
1560
1575
  "urls": [str(contract_file_posix_abs)]}} # type: Dict[str, Dict[str, Any]]
1561
1576
  output_selection = ["transientStorageLayout", "storageLayout", "abi", "evm.bytecode",
1562
1577
  "evm.deployedBytecode", "evm.methodIdentifiers", "evm.assembly",
@@ -1567,6 +1582,8 @@ class CertoraBuildGenerator:
1567
1582
  contents = f.read()
1568
1583
  sources_dict = {str(contract_file_posix_abs): {"content": contents}}
1569
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"]
1570
1587
  ast_selection = ["ast"]
1571
1588
 
1572
1589
  settings_dict: Dict[str, Any] = \
@@ -1853,7 +1870,7 @@ class CertoraBuildGenerator:
1853
1870
  # Standard JSON
1854
1871
  remappings = [] if isinstance(compiler_collector, CompilerCollectorYul) else self.context.remappings
1855
1872
  input_for_solc = self.standard_json(Path(file_abs_path), build_arg_contract_file, remappings,
1856
- compiler_collector)
1873
+ compiler_collector, compile_wd)
1857
1874
  standard_json_input = json.dumps(input_for_solc).encode("utf-8")
1858
1875
  compiler_logger.debug(f"about to run in {compile_wd} the command: {collect_cmd}")
1859
1876
  compiler_logger.debug(f"solc input = {json.dumps(input_for_solc, indent=4)}")
@@ -1909,7 +1926,7 @@ class CertoraBuildGenerator:
1909
1926
  contract_file_abs = str(Util.abs_norm_path(contract_file))
1910
1927
 
1911
1928
  # using os.path.relpath because Path.relative_to cannot go up the directory tree (no ..)
1912
- 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)
1913
1930
 
1914
1931
  build_logger.debug(f"available keys: {data['contracts'].keys()}")
1915
1932
  if contract_file_rel in data[CONTRACTS]:
@@ -2008,7 +2025,15 @@ class CertoraBuildGenerator:
2008
2025
  srclist = {"0": file_abs_path}
2009
2026
  report_srclist = {"0": file_abs_path}
2010
2027
 
2011
- 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]]
2012
2037
 
2013
2038
  # all "contracts in SDC" are the same for every primary contract of the compiled file.
2014
2039
  # we can therefore compute those just once...
@@ -2039,8 +2064,9 @@ class CertoraBuildGenerator:
2039
2064
 
2040
2065
  build_logger.debug(f"finding primary contract address of {file_compiler_path}:{primary_contract} in "
2041
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
2042
2068
  primary_contract_address = \
2043
- self.find_contract_address_str(file_compiler_path,
2069
+ self.find_contract_address_str(path_to_find,
2044
2070
  primary_contract,
2045
2071
  contracts_with_chosen_addresses)
2046
2072
  build_logger.debug(f"Contracts in SDC {sdc_name}: {[contract.name for contract in contracts_in_sdc]}")
@@ -2086,7 +2112,7 @@ class CertoraBuildGenerator:
2086
2112
  if new_path is None:
2087
2113
  file_abs_path = Util.abs_posix_path(build_arg_contract_file)
2088
2114
  # for vyper, because there are no autofinders, at least find the main file
2089
- if (orig_file == file_abs_path and
2115
+ if (Util.abs_posix_path(orig_file) == file_abs_path and
2090
2116
  ((Util.get_certora_sources_dir() / build_arg_contract_file).exists() or
2091
2117
  Path(build_arg_contract_file).exists())):
2092
2118
  new_srclist_map[idx] = build_arg_contract_file
@@ -2101,6 +2127,7 @@ class CertoraBuildGenerator:
2101
2127
  report_srclist = srclist
2102
2128
  else:
2103
2129
  report_srclist = srclist
2130
+ build_logger.debug(f"Report source list={report_srclist}")
2104
2131
  return report_srclist
2105
2132
 
2106
2133
  def get_bytecode(self,
@@ -2108,7 +2135,7 @@ class CertoraBuildGenerator:
2108
2135
  contract_name: str,
2109
2136
  primary_contracts: List[str],
2110
2137
  contracts_with_chosen_addresses: List[Tuple[int, Any]],
2111
- fail_if_no_bytecode: bool
2138
+ is_deployed_bytecode: bool
2112
2139
  ) -> str:
2113
2140
  """
2114
2141
  Computes the linked bytecode object from the Solidity compiler output.
@@ -2119,20 +2146,31 @@ class CertoraBuildGenerator:
2119
2146
  @param primary_contracts - the names of the primary contracts we check to have a bytecode
2120
2147
  @param contracts_with_chosen_addresses - a list of tuples of addresses and the
2121
2148
  associated contract identifier
2122
- @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,
2123
2151
  false otherwise
2124
2152
  @returns linked bytecode object
2125
2153
  """
2126
2154
  # TODO: Only contract_name should be necessary. This requires a lot more test cases to make sure we're not
2127
2155
  # missing any weird solidity outputs.
2128
2156
  bytecode_ = bytecode_object["object"]
2129
- bytecode = self.collect_and_link_bytecode(contract_name, contracts_with_chosen_addresses,
2130
- 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)
2131
2169
  if contract_name in primary_contracts and len(bytecode) == 0:
2132
2170
  msg = f"Contract {contract_name} has no bytecode. " \
2133
2171
  f"It may be caused because the contract is abstract, " \
2134
2172
  f"or is missing constructor code. Please check the output of the Solidity compiler."
2135
- if fail_if_no_bytecode:
2173
+ if is_deployed_bytecode:
2136
2174
  raise Util.CertoraUserInputError(msg)
2137
2175
  else:
2138
2176
  build_logger.warning(msg)
@@ -2202,6 +2240,40 @@ class CertoraBuildGenerator:
2202
2240
  storage_slot['descriptor'] = type_descriptor.as_dict()
2203
2241
  return storage_data
2204
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
+
2205
2277
  def get_contract_in_sdc(self,
2206
2278
  source_code_file: str,
2207
2279
  contract_name: str,
@@ -2326,6 +2398,9 @@ class CertoraBuildGenerator:
2326
2398
  else:
2327
2399
  compiler_parameters = None
2328
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
+
2329
2404
  return ContractInSDC(contract_name,
2330
2405
  # somehow make sure this is an absolute path which obeys the autofinder remappings
2331
2406
  # (i.e. make sure this file doesn't start with autoFinder_)
@@ -2842,6 +2917,8 @@ class CertoraBuildGenerator:
2842
2917
 
2843
2918
  self.SDCs[self.get_sdc_key(sdc.primary_contract, sdc.primary_contract_address)] = sdc
2844
2919
 
2920
+ if self.context.dump_asts:
2921
+ self.dump_asts()
2845
2922
  self.handle_links()
2846
2923
  self.handle_struct_links()
2847
2924
  self.handle_contract_extensions()
@@ -3002,7 +3079,7 @@ class CertoraBuildGenerator:
3002
3079
  target_file = self.context.contract_to_file[target.name]
3003
3080
  base_contracts = self.retrieve_base_contracts_list(
3004
3081
  target_file,
3005
- Util.abs_posix_path(target_file),
3082
+ target_file if self.context.use_relpaths_for_solc_json else Util.abs_posix_path(target_file),
3006
3083
  target.name
3007
3084
  )
3008
3085
  extensions: Set[str] = set()
@@ -3078,8 +3155,9 @@ class CertoraBuildGenerator:
3078
3155
 
3079
3156
  # let's try to find the AST now
3080
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
3081
3159
  for (k, v) in self.asts.items():
3082
- if ext_instance.original_file in v:
3160
+ if orig_path in v:
3083
3161
  build_file_ast = k
3084
3162
  break
3085
3163
  if build_file_ast is None:
@@ -3087,10 +3165,10 @@ class CertoraBuildGenerator:
3087
3165
 
3088
3166
  contract_def_node = self.get_contract_def_node_ref(
3089
3167
  build_file_ast,
3090
- ext_instance.original_file,
3168
+ orig_path,
3091
3169
  storage_ext
3092
3170
  )
3093
- 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]
3094
3172
  nodes = def_node.get("nodes")
3095
3173
  if not isinstance(nodes, list):
3096
3174
  raise RuntimeError(f"Couldn't find nodes for body of {storage_ext}")
@@ -3261,6 +3339,16 @@ class CertoraBuildGenerator:
3261
3339
  else:
3262
3340
  added_source_finders = {}
3263
3341
 
3342
+ if self.context.safe_casting_builtin:
3343
+ try:
3344
+ casting_instrumentations, casting_types = generate_casting_instrumentation(self.asts, build_arg_contract_file, sdc_pre_finder)
3345
+ except Exception as e:
3346
+ instrumentation_logger.warning(
3347
+ f"Computing casting instrumentation failed for {build_arg_contract_file}: {e}", exc_info=True)
3348
+ casting_instrumentations, casting_types = {}, {}
3349
+
3350
+ instr = CertoraBuildGenerator.merge_dicts_instrumentation(instr, casting_instrumentations)
3351
+
3264
3352
  abs_build_arg_contract_file = Util.abs_posix_path(build_arg_contract_file)
3265
3353
  if abs_build_arg_contract_file not in instr:
3266
3354
  instrumentation_logger.debug(
@@ -3314,6 +3402,14 @@ class CertoraBuildGenerator:
3314
3402
  read_so_far += amt + 1 + to_skip
3315
3403
  output.write(in_file.read(-1))
3316
3404
 
3405
+ if self.context.safe_casting_builtin:
3406
+ library_name, funcs = casting_types.get(contract_file, ("", list()))
3407
+ if len(funcs) > 0:
3408
+ output.write(bytes(f"\nlibrary {library_name}" + "{\n", "utf8"))
3409
+ for f in funcs:
3410
+ output.write(bytes(f, "utf8"))
3411
+ output.write(bytes("}\n", "utf8"))
3412
+
3317
3413
  new_file = self.to_autofinder_file(build_arg_contract_file)
3318
3414
  self.context.file_to_contract[new_file] = self.context.file_to_contract[
3319
3415
  build_arg_contract_file]
@@ -3789,6 +3885,16 @@ class CertoraBuildGenerator:
3789
3885
  # Avoiding Python interpreter shutdown exceptions which are safe to ignore
3790
3886
  pass
3791
3887
 
3888
+ def dump_asts(self) -> None:
3889
+ asts_dump_file = Util.get_asts_file()
3890
+ filtered_asts = {k: v for k, v in self.asts.items() if not str(k).startswith(f"{Util.get_build_dir()}")}
3891
+ with asts_dump_file.open("w+") as output_file:
3892
+ try:
3893
+ json.dump(filtered_asts, output_file, indent=4, sort_keys=True)
3894
+ except Exception as e:
3895
+ ast_logger.debug(f"Couldn't dump ASTs to {asts_dump_file}", exc_info=e)
3896
+ raise
3897
+
3792
3898
 
3793
3899
  # make sure each source file exists and its path is in absolute format
3794
3900
  def sources_to_abs(sources: Set[Path]) -> Set[Path]:
@@ -179,9 +179,12 @@ class CertoraBuildCacheManager:
179
179
  trg = trg_path_with_additional_included_files / post_autofinder_dir.name
180
180
  if post_autofinder_dir != trg:
181
181
  if post_autofinder_dir.is_dir():
182
- if trg.exists():
183
- shutil.rmtree(trg)
184
- Util.safe_copy_folder(post_autofinder_dir, trg, shutil.ignore_patterns())
182
+ # if we match on the cache, and we run a few certoraRun-s in parallel,
183
+ # it could be this folder already exists. But the exact contents do not matter and are likely to
184
+ # agree with the exception of spec files. So let's merge the folders so that the
185
+ # sources are in any case runnable
186
+ if not trg.exists():
187
+ Util.safe_merge_folder(post_autofinder_dir, trg, shutil.ignore_patterns())
185
188
  else:
186
189
  # highly unlikely .post_autofinder.[digit] will be a file and not a directory,
187
190
  # but would rather not crash and future-proof instead
@@ -189,6 +189,7 @@ class ContractInSDC:
189
189
  # why are we using this and not the normal list of functions?
190
190
  "internalFunctions": {key: method.as_dict() for key, method in self.function_finders.items()},
191
191
  # this doesn't even have all functions
192
+ "internalFuncs": [f.as_dict() for f in self.internal_funcs],
192
193
  "allMethods": [f.as_dict() for f in self.all_funcs],
193
194
  "solidityTypes": [x.as_dict() for x in self.types],
194
195
  "compilerName": "" if not self.compiler_collector else self.compiler_collector.compiler_name,
@@ -689,8 +689,11 @@ class CloudVerification:
689
689
  auth_data["mutationTestId"] = self.context.mutation_test_id
690
690
  return auth_data
691
691
 
692
+ def get_group_id_url(self) -> str:
693
+ return f"{self.get_domain()}/?groupIds={self.context.group_id}"
694
+
692
695
  def print_group_id_url(self) -> None:
693
- group_id_url = f"{self.get_domain()}/?groupIds={self.context.group_id}"
696
+ group_id_url = self.get_group_id_url()
694
697
  print(f"See all jobs generated from splitting the rules at {group_id_url}", flush=True)
695
698
 
696
699
  def print_output_links(self) -> None: