mirror of https://github.com/crytic/slither
commit
1da0bdca62
@ -0,0 +1,6 @@ |
|||||||
|
# Demo |
||||||
|
|
||||||
|
This directory contains an example of Slither utility. |
||||||
|
|
||||||
|
See the [utility documentation](https://github.com/crytic/slither/wiki/Adding-a-new-utility) |
||||||
|
|
@ -0,0 +1,252 @@ |
|||||||
|
import argparse |
||||||
|
import logging |
||||||
|
import uuid |
||||||
|
from typing import Optional, Dict, List |
||||||
|
from crytic_compile import cryticparser |
||||||
|
from slither import Slither |
||||||
|
from slither.core.compilation_unit import SlitherCompilationUnit |
||||||
|
from slither.core.declarations import Function |
||||||
|
|
||||||
|
from slither.formatters.utils.patches import create_patch, apply_patch, create_diff |
||||||
|
from slither.utils import codex |
||||||
|
|
||||||
|
logging.basicConfig() |
||||||
|
logging.getLogger("Slither").setLevel(logging.INFO) |
||||||
|
|
||||||
|
logger = logging.getLogger("Slither") |
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace: |
||||||
|
""" |
||||||
|
Parse the underlying arguments for the program. |
||||||
|
:return: Returns the arguments for the program. |
||||||
|
""" |
||||||
|
parser = argparse.ArgumentParser(description="Demo", usage="slither-documentation filename") |
||||||
|
|
||||||
|
parser.add_argument("project", help="The target directory/Solidity file.") |
||||||
|
|
||||||
|
parser.add_argument( |
||||||
|
"--overwrite", help="Overwrite the files (be careful).", action="store_true", default=False |
||||||
|
) |
||||||
|
|
||||||
|
parser.add_argument( |
||||||
|
"--force-answer-parsing", |
||||||
|
help="Apply heuristics to better parse codex output (might lead to incorrect results)", |
||||||
|
action="store_true", |
||||||
|
default=False, |
||||||
|
) |
||||||
|
|
||||||
|
parser.add_argument("--retry", help="Retry failed query (default 1). Each retry increases the temperature by 0.1", action="store", default=1) |
||||||
|
|
||||||
|
# Add default arguments from crytic-compile |
||||||
|
cryticparser.init(parser) |
||||||
|
|
||||||
|
codex.init_parser(parser, always_enable_codex=True) |
||||||
|
|
||||||
|
return parser.parse_args() |
||||||
|
|
||||||
|
|
||||||
|
def _use_tab(char: str) -> Optional[bool]: |
||||||
|
""" |
||||||
|
Check if the char is a tab |
||||||
|
|
||||||
|
Args: |
||||||
|
char: |
||||||
|
|
||||||
|
Returns: |
||||||
|
|
||||||
|
""" |
||||||
|
if char == " ": |
||||||
|
return False |
||||||
|
if char == "\t": |
||||||
|
return True |
||||||
|
return None |
||||||
|
|
||||||
|
|
||||||
|
def _post_processesing( |
||||||
|
answer: str, starting_column: int, use_tab: Optional[bool], force_and_stopped: bool |
||||||
|
) -> Optional[str]: |
||||||
|
""" |
||||||
|
Clean answers from codex |
||||||
|
|
||||||
|
Args: |
||||||
|
answer: |
||||||
|
starting_column: |
||||||
|
|
||||||
|
Returns: |
||||||
|
|
||||||
|
""" |
||||||
|
if answer.count("/**") != 1: |
||||||
|
return None |
||||||
|
# Sometimes codex will miss the */, even if it finished properly the request |
||||||
|
# In this case, we allow slither-documentation to force the */ |
||||||
|
if answer.count("*/") != 1: |
||||||
|
if force_and_stopped: |
||||||
|
answer += "*/" |
||||||
|
else: |
||||||
|
return None |
||||||
|
if answer.find("/**") > answer.find("*/"): |
||||||
|
return None |
||||||
|
answer = answer[answer.find("/**") : answer.find("*/") + 2] |
||||||
|
answer_lines = answer.splitlines() |
||||||
|
# Add indentation to all the lines, aside the first one |
||||||
|
|
||||||
|
space_char = "\t" if use_tab else " " |
||||||
|
|
||||||
|
if len(answer_lines) > 0: |
||||||
|
answer = ( |
||||||
|
answer_lines[0] |
||||||
|
+ "\n" |
||||||
|
+ "\n".join( |
||||||
|
[space_char * (starting_column - 1) + line for line in answer_lines[1:] if line] |
||||||
|
) |
||||||
|
) |
||||||
|
answer += "\n" + space_char * (starting_column - 1) |
||||||
|
return answer |
||||||
|
return answer_lines[0] |
||||||
|
|
||||||
|
|
||||||
|
def _handle_codex( |
||||||
|
answer: Dict, starting_column: int, use_tab: Optional[bool], force: bool |
||||||
|
) -> Optional[str]: |
||||||
|
if "choices" in answer: |
||||||
|
if answer["choices"]: |
||||||
|
if "text" in answer["choices"][0]: |
||||||
|
has_stopped = answer["choices"][0].get("finish_reason", "") == "stop" |
||||||
|
answer_processed = _post_processesing( |
||||||
|
answer["choices"][0]["text"], starting_column, use_tab, force and has_stopped |
||||||
|
) |
||||||
|
if answer_processed is None: |
||||||
|
return None |
||||||
|
return answer_processed |
||||||
|
return None |
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-locals |
||||||
|
def _handle_compilation_unit( |
||||||
|
slither: Slither, |
||||||
|
compilation_unit: SlitherCompilationUnit, |
||||||
|
overwrite: bool, |
||||||
|
force: bool, |
||||||
|
retry: int, |
||||||
|
) -> None: |
||||||
|
|
||||||
|
logging_file = str(uuid.uuid4()) |
||||||
|
|
||||||
|
for scope in compilation_unit.scopes.values(): |
||||||
|
|
||||||
|
# TODO remove hardcoded filtering |
||||||
|
if ( |
||||||
|
".t.sol" in scope.filename.absolute |
||||||
|
or "mock" in scope.filename.absolute.lower() |
||||||
|
or "test" in scope.filename.absolute.lower() |
||||||
|
): |
||||||
|
continue |
||||||
|
|
||||||
|
functions_target: List[Function] = [] |
||||||
|
|
||||||
|
for contract in scope.contracts.values(): |
||||||
|
functions_target += contract.functions_declared |
||||||
|
|
||||||
|
functions_target += list(scope.functions) |
||||||
|
|
||||||
|
all_patches: Dict = {} |
||||||
|
|
||||||
|
for function in functions_target: |
||||||
|
|
||||||
|
if function.source_mapping.is_dependency or function.has_documentation or function.is_constructor_variables: |
||||||
|
continue |
||||||
|
prompt = ( |
||||||
|
"Create a natpsec documentation for this solidity code with only notice and dev.\n" |
||||||
|
) |
||||||
|
src_mapping = function.source_mapping |
||||||
|
content = compilation_unit.core.source_code[src_mapping.filename.absolute] |
||||||
|
start = src_mapping.start |
||||||
|
end = src_mapping.start + src_mapping.length |
||||||
|
prompt += content[start:end] |
||||||
|
|
||||||
|
use_tab = _use_tab(content[start - 1]) |
||||||
|
if use_tab is None and src_mapping.starting_column > 1: |
||||||
|
logger.info(f"Non standard space indentation found {content[start-1:end]}") |
||||||
|
if overwrite: |
||||||
|
logger.info("Disable overwrite to avoid mistakes") |
||||||
|
overwrite = False |
||||||
|
|
||||||
|
openai = codex.openai_module() # type: ignore |
||||||
|
if openai is None: |
||||||
|
return |
||||||
|
|
||||||
|
if slither.codex_log: |
||||||
|
codex.log_codex(logging_file, "Q: " + prompt) |
||||||
|
|
||||||
|
tentative = 0 |
||||||
|
answer_processed: Optional[str] = None |
||||||
|
while tentative < retry: |
||||||
|
tentative += 1 |
||||||
|
|
||||||
|
answer = openai.Completion.create( # type: ignore |
||||||
|
prompt=prompt, |
||||||
|
model=slither.codex_model, |
||||||
|
temperature=min(slither.codex_temperature + tentative*0.1, 1), |
||||||
|
max_tokens=slither.codex_max_tokens, |
||||||
|
) |
||||||
|
|
||||||
|
if slither.codex_log: |
||||||
|
codex.log_codex(logging_file, "A: " + str(answer)) |
||||||
|
|
||||||
|
answer_processed = _handle_codex( |
||||||
|
answer, src_mapping.starting_column, use_tab, force |
||||||
|
) |
||||||
|
if answer_processed: |
||||||
|
break |
||||||
|
|
||||||
|
logger.info( |
||||||
|
f"Codex could not generate a well formatted answer for {function.canonical_name}" |
||||||
|
) |
||||||
|
logger.info(answer) |
||||||
|
|
||||||
|
if not answer_processed: |
||||||
|
continue |
||||||
|
|
||||||
|
create_patch( |
||||||
|
all_patches, src_mapping.filename.absolute, start, start, "", answer_processed |
||||||
|
) |
||||||
|
|
||||||
|
# all_patches["patches"] should have only 1 file |
||||||
|
if "patches" not in all_patches: |
||||||
|
continue |
||||||
|
for file in all_patches["patches"]: |
||||||
|
original_txt = compilation_unit.core.source_code[file].encode("utf8") |
||||||
|
patched_txt = original_txt |
||||||
|
|
||||||
|
patches = all_patches["patches"][file] |
||||||
|
offset = 0 |
||||||
|
patches.sort(key=lambda x: x["start"]) |
||||||
|
|
||||||
|
for patch in patches: |
||||||
|
patched_txt, offset = apply_patch(patched_txt, patch, offset) |
||||||
|
|
||||||
|
if overwrite: |
||||||
|
with open(file, "w", encoding="utf8") as f: |
||||||
|
f.write(patched_txt.decode("utf8")) |
||||||
|
else: |
||||||
|
diff = create_diff(compilation_unit, original_txt, patched_txt, file) |
||||||
|
with open(f"{file}.patch", "w", encoding="utf8") as f: |
||||||
|
f.write(diff) |
||||||
|
|
||||||
|
|
||||||
|
def main() -> None: |
||||||
|
args = parse_args() |
||||||
|
|
||||||
|
logger.info("This tool is a WIP, use it with cautious") |
||||||
|
logger.info("Be aware of OpenAI ToS: https://openai.com/api/policies/terms/") |
||||||
|
slither = Slither(args.project, **vars(args)) |
||||||
|
|
||||||
|
for compilation_unit in slither.compilation_units: |
||||||
|
_handle_compilation_unit( |
||||||
|
slither, compilation_unit, args.overwrite, args.force_answer_parsing, int(args.retry) |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
main() |
Loading…
Reference in new issue