Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds a new printer for external calls. #2420

Open
wants to merge 7 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions slither/printers/all_printers.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@
from .summary.declaration import Declaration
from .functions.dominator import Dominator
from .summary.martin import Martin
from .summary.external_calls import ExternalCallPrinter
59 changes: 59 additions & 0 deletions slither/printers/summary/external_calls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""
Module printing the high level calls
"""
from slither.printers.abstract_printer import AbstractPrinter
from slither.utils.myprettytable import MyPrettyTable


class ExternalCallPrinter(AbstractPrinter):

ARGUMENT = "external-calls"
HELP = "Print the external calls performed by each function"

WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#external-calls"

def output(self, _):
"""Computes and returns the list of external calls performed."""

all_txt = "External calls"

table = MyPrettyTable(["Source (Line)", "Destination", "Chain"])

# pylint: disable=too-many-nested-blocks
for contract in self.slither.contracts:
if contract.is_interface or contract.is_abstract:
continue

for function in contract.functions:
# Bail out early if this function does not perform high level calls
if not function.all_high_level_calls():
continue

for node in function.nodes:
for target_contract, target_function in node.high_level_calls:

row = [
f"{function.canonical_name} {node.source_mapping.to_detailed_str()}",
f"{target_contract.name}.{target_function}",
]

if function.all_reachable_from_functions:

for source in function.all_reachable_from_functions:
chain = f"{source.canonical_name} -> {function.canonical_name}"
table.add_row(
[
*row,
chain,
]
)
else:
table.add_row([*row, ""])

all_txt += "\n" + str(table)
self.info(all_txt)

res = self.generate_output(all_txt)
res.add_pretty_table(table, "External Calls")

return res
26 changes: 20 additions & 6 deletions slither/solc_parsing/cfg/node.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from itertools import filterfalse
from typing import Union, Optional, Dict, TYPE_CHECKING

from slither.core.cfg.node import Node
from slither.core.cfg.node import NodeType
from slither.core.declarations import SolidityVariable
from slither.core.expressions.assignment_operation import (
AssignmentOperation,
AssignmentOperationType,
)

from slither.core.expressions.identifier import Identifier
from slither.solc_parsing.expressions.expression_parsing import parse_expression
from slither.visitors.expression.find_calls import FindCalls
Expand All @@ -15,6 +18,7 @@
if TYPE_CHECKING:
from slither.solc_parsing.declarations.function import FunctionSolc
from slither.solc_parsing.declarations.modifier import ModifierSolc
from slither.core.expressions.expression import Expression


class NodeSolc:
Expand Down Expand Up @@ -62,9 +66,19 @@ def analyze_expressions(self, caller_context: Union["FunctionSolc", "ModifierSol

find_call = FindCalls(expression)
self._node.calls_as_expression = find_call.result()
self._node.external_calls_as_expressions = [
c for c in self._node.calls_as_expression if not isinstance(c.called, Identifier)
]
self._node.internal_calls_as_expressions = [
c for c in self._node.calls_as_expression if isinstance(c.called, Identifier)
]

def is_external_call(element: "Expression") -> bool:
if not isinstance(element.called, Identifier):
try:
return not isinstance(element.called.expression.value, SolidityVariable)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be able to be isinstance(element.called.value, SolidityVariable) and not require a try/except

Copy link
Contributor Author

@DarkaMaul DarkaMaul Apr 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my initial try but the test failed on the CI :
https://github.com/crytic/slither/actions/runs/8649176961/job/23714782903

    def is_external_call(element: "Expression") -> bool:
        return not isinstance(element.called, Identifier) and not isinstance(
>           element.called.expression.value, SolidityVariable
        )
E       AttributeError: 'TypeConversion' object has no attribute 'value'

except AttributeError:
return True

return False

self._node.external_calls_as_expressions = list(
filter(is_external_call, self._node.calls_as_expression)
)
self._node.internal_calls_as_expressions = list(
filterfalse(is_external_call, self._node.calls_as_expression)
)
30 changes: 30 additions & 0 deletions tests/e2e/printers/test_data/test_external_calls/A.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: GPL3
pragma solidity ^0.8.0;

import "./IERC20.sol";

contract A {
IERC20 private token;

function foo() view internal {
token.balanceOf(address(this));
}

function encodeData(uint256 number, string memory text) public pure returns (bytes memory) {
return abi.encode(number, text);
}
}

contract B is A {
function bar() view public {
foo();
}
}


contract C {
B public b;
function pop() view public {
b.bar();
}
}
79 changes: 79 additions & 0 deletions tests/e2e/printers/test_data/test_external_calls/IERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol)

pragma solidity ^0.8.0;

/**
* @dev Interface of the ERC-20 standard as defined in the ERC.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);

/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);

/**
* @dev Returns the value of tokens in existence.
*/
function totalSupply() external view returns (uint256);

/**
* @dev Returns the value of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);

/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 value) external returns (bool);

/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);

/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 value) external returns (bool);

/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the
* allowance mechanism. `value` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
19 changes: 17 additions & 2 deletions tests/e2e/printers/test_printers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
from collections import Counter
from pathlib import Path

from crytic_compile import CryticCompile
from crytic_compile import CryticCompile, compile_all
from crytic_compile.platform.solc_standard_json import SolcStandardJson

from slither import Slither
from slither.printers.inheritance.inheritance_graph import PrinterInheritanceGraph
from slither.printers.summary.external_calls import ExternalCallPrinter


TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data"
Expand Down Expand Up @@ -35,7 +36,7 @@ def test_inheritance_printer(solc_binary_path) -> None:
assert counter["B -> A"] == 2
assert counter["C -> A"] == 1

# Lets also test the include/exclude interface behavior
# Let also test the include/exclude interface behavior
# Check that the interface is not included
assert "MyInterfaceX" not in content

Expand All @@ -46,3 +47,17 @@ def test_inheritance_printer(solc_binary_path) -> None:

# Remove test generated files
Path("test_printer.dot").unlink(missing_ok=True)


def test_external_call_printers(solc_binary_path) -> None:
solc_path = solc_binary_path("0.8.0")
compilation = compile_all(
(TEST_DATA_DIR / "test_external_calls" / "A.sol").as_posix(), solc=solc_path
).pop()
slither = Slither(compilation)

printer = ExternalCallPrinter(slither, None)
output = printer.output("")

# The test is not great here, but they will soon be moved to a snapshot based system
assert output is not None