From 114dc2eecaff3262472645f19f961971ee448de5 Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 8 Apr 2024 11:43:42 +0200 Subject: [PATCH 01/71] Fix #2266 where the type of functions when used as RValues was not properly printed. --- slither/slithir/operations/assignment.py | 13 +++++++++++-- .../test_data/test_printer_slithir/bug-2266.sol | 13 +++++++++++++ tests/e2e/printers/test_printers.py | 16 ++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 tests/e2e/printers/test_data/test_printer_slithir/bug-2266.sol diff --git a/slither/slithir/operations/assignment.py b/slither/slithir/operations/assignment.py index 1f29ceb7b1..ab6637faa0 100644 --- a/slither/slithir/operations/assignment.py +++ b/slither/slithir/operations/assignment.py @@ -45,10 +45,19 @@ def rvalue(self) -> Union[RVALUE, Function, TupleVariable]: def __str__(self) -> str: lvalue = self.lvalue + + # When rvalues are functions, we want to properly display their return type + # Fix: https://github.com/crytic/slither/issues/2266 + if isinstance(self.rvalue.type, list): + rvalue_type = ",".join(f"{rvalue_type}" for rvalue_type in self.rvalue.type) + else: + rvalue_type = f"{self.rvalue.type}" + assert lvalue if lvalue and isinstance(lvalue, ReferenceVariable): points = lvalue.points_to while isinstance(points, ReferenceVariable): points = points.points_to - return f"{lvalue}({lvalue.type}) (->{points}) := {self.rvalue}({self.rvalue.type})" - return f"{lvalue}({lvalue.type}) := {self.rvalue}({self.rvalue.type})" + return f"{lvalue}({lvalue.type}) (->{points}) := {self.rvalue}({rvalue_type})" + + return f"{lvalue}({lvalue.type}) := {self.rvalue}({rvalue_type})" diff --git a/tests/e2e/printers/test_data/test_printer_slithir/bug-2266.sol b/tests/e2e/printers/test_data/test_printer_slithir/bug-2266.sol new file mode 100644 index 0000000000..5c11a29141 --- /dev/null +++ b/tests/e2e/printers/test_data/test_printer_slithir/bug-2266.sol @@ -0,0 +1,13 @@ +pragma solidity ^0.8.0; + +contract A { + function add(uint256 a, uint256 b) public returns (uint256) { + return a + b; + } +} + +contract B is A { + function assignFunction() public { + function(uint256, uint256) returns (uint256) myFunction = super.add; + } +} \ No newline at end of file diff --git a/tests/e2e/printers/test_printers.py b/tests/e2e/printers/test_printers.py index 26429d3381..b7cd4d3b6b 100644 --- a/tests/e2e/printers/test_printers.py +++ b/tests/e2e/printers/test_printers.py @@ -7,6 +7,7 @@ from slither import Slither from slither.printers.inheritance.inheritance_graph import PrinterInheritanceGraph +from slither.printers.summary.slithir import PrinterSlithIR TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" @@ -34,3 +35,18 @@ def test_inheritance_printer(solc_binary_path) -> None: assert counter["B -> A"] == 2 assert counter["C -> A"] == 1 + + +def test_slithir_printer(solc_binary_path) -> None: + solc_path = solc_binary_path("0.8.0") + standard_json = SolcStandardJson() + standard_json.add_source_file( + Path(TEST_DATA_DIR, "test_printer_slithir", "bug-2266.sol").as_posix() + ) + compilation = CryticCompile(standard_json, solc=solc_path) + slither = Slither(compilation) + + printer = PrinterSlithIR(slither, logger=None) + output = printer.output("test_printer_slithir.dot") + + assert "slither.core.solidity_types" not in output.data["description"] From 46d0cb13dbdef2789a1094bfdf4ce22b4249ebae Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 8 Apr 2024 14:58:30 +0200 Subject: [PATCH 02/71] Init Cheatcode printer --- slither/printers/summary/cheatcodes.py | 19 ++++++ .../test_printer_cheatcode/README.md | 66 +++++++++++++++++++ .../test_printer_cheatcode/foundry.toml | 6 ++ .../test_printer_cheatcode/src/Vault.sol | 8 +++ .../test_printer_cheatcode/test/Vault.t.sol | 32 +++++++++ tests/e2e/printers/test_printers.py | 12 ++++ 6 files changed, 143 insertions(+) create mode 100644 slither/printers/summary/cheatcodes.py create mode 100644 tests/e2e/printers/test_data/test_printer_cheatcode/README.md create mode 100644 tests/e2e/printers/test_data/test_printer_cheatcode/foundry.toml create mode 100644 tests/e2e/printers/test_data/test_printer_cheatcode/src/Vault.sol create mode 100644 tests/e2e/printers/test_data/test_printer_cheatcode/test/Vault.t.sol diff --git a/slither/printers/summary/cheatcodes.py b/slither/printers/summary/cheatcodes.py new file mode 100644 index 0000000000..8c84da8e65 --- /dev/null +++ b/slither/printers/summary/cheatcodes.py @@ -0,0 +1,19 @@ +""" + Cheatcode printer + + This printer prints the usage of cheatcode in the code. +""" +from slither.printers.abstract_printer import AbstractPrinter +from slither.utils import output + + +class CheatcodePrinter(AbstractPrinter): + + ARGUMENT = "cheatcode" + + HELP = """""" + + WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#cheatcode" + + def output(self, filename: str) -> output.Output: + pass diff --git a/tests/e2e/printers/test_data/test_printer_cheatcode/README.md b/tests/e2e/printers/test_data/test_printer_cheatcode/README.md new file mode 100644 index 0000000000..9265b45584 --- /dev/null +++ b/tests/e2e/printers/test_data/test_printer_cheatcode/README.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/tests/e2e/printers/test_data/test_printer_cheatcode/foundry.toml b/tests/e2e/printers/test_data/test_printer_cheatcode/foundry.toml new file mode 100644 index 0000000000..e6810b2b58 --- /dev/null +++ b/tests/e2e/printers/test_data/test_printer_cheatcode/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = 'src' +out = 'out' +libs = ['lib'] + +# See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file diff --git a/tests/e2e/printers/test_data/test_printer_cheatcode/src/Vault.sol b/tests/e2e/printers/test_data/test_printer_cheatcode/src/Vault.sol new file mode 100644 index 0000000000..2aaf6653cc --- /dev/null +++ b/tests/e2e/printers/test_data/test_printer_cheatcode/src/Vault.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +contract Vault { + constructor(){ + + } +} diff --git a/tests/e2e/printers/test_data/test_printer_cheatcode/test/Vault.t.sol b/tests/e2e/printers/test_data/test_printer_cheatcode/test/Vault.t.sol new file mode 100644 index 0000000000..6a598136df --- /dev/null +++ b/tests/e2e/printers/test_data/test_printer_cheatcode/test/Vault.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "../src/Vault.sol"; + +contract VaultTest is Test { + + Vault public vault; + + address public alice = address(0x42); + + function broadcast() pure public {} + + function setUp() public { + vm.prank(alice); + vm.deal(alice, 1000 ether); + + uint256 value = 123; + + vm.warp(1641070800); + vm.roll(100); + vm.fee(25 gwei); + + // Not a cheatcode + broadcast(); + } + + function test_deal() public { + + } +} diff --git a/tests/e2e/printers/test_printers.py b/tests/e2e/printers/test_printers.py index 26429d3381..8d9c5587c5 100644 --- a/tests/e2e/printers/test_printers.py +++ b/tests/e2e/printers/test_printers.py @@ -1,4 +1,6 @@ import re +import shutil +import pytest from collections import Counter from pathlib import Path @@ -11,6 +13,9 @@ TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" +foundry_available = shutil.which("forge") is not None +project_ready = Path(TEST_DATA_DIR, "test_printer_cheatcode/lib/forge-std").exists() + def test_inheritance_printer(solc_binary_path) -> None: solc_path = solc_binary_path("0.8.0") @@ -34,3 +39,10 @@ def test_inheritance_printer(solc_binary_path) -> None: assert counter["B -> A"] == 2 assert counter["C -> A"] == 1 + + +@pytest.mark.skipif( + not foundry_available or not project_ready, reason="requires Foundry and project setup" +) +def test_printer_cheatcode(): + slither = Slither(Path(TEST_DATA_DIR, "test_printer_cheatcode").as_posix()) \ No newline at end of file From 7045cb82d88adeaddd2c04947d7a9a315bc7b1cf Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 8 Apr 2024 18:31:19 +0200 Subject: [PATCH 03/71] Update Cheatcode Printer --- slither/printers/all_printers.py | 1 + slither/printers/summary/cheatcodes.py | 55 ++++++++++++++- .../test_printer_cheatcode/README.md | 68 ++----------------- .../test_printer_cheatcode/src/Counter.sol | 14 ++++ .../test_printer_cheatcode/src/Vault.sol | 8 --- .../test_printer_cheatcode/test/Counter.t.sol | 36 ++++++++++ .../test_printer_cheatcode/test/Vault.t.sol | 32 --------- tests/e2e/printers/test_printers.py | 15 +++- 8 files changed, 121 insertions(+), 108 deletions(-) create mode 100644 tests/e2e/printers/test_data/test_printer_cheatcode/src/Counter.sol delete mode 100644 tests/e2e/printers/test_data/test_printer_cheatcode/src/Vault.sol create mode 100644 tests/e2e/printers/test_data/test_printer_cheatcode/test/Counter.t.sol delete mode 100644 tests/e2e/printers/test_data/test_printer_cheatcode/test/Vault.t.sol diff --git a/slither/printers/all_printers.py b/slither/printers/all_printers.py index 3edd5325b6..034578f528 100644 --- a/slither/printers/all_printers.py +++ b/slither/printers/all_printers.py @@ -24,3 +24,4 @@ from .summary.declaration import Declaration from .functions.dominator import Dominator from .summary.martin import Martin +from .summary.cheatcodes import CheatcodePrinter diff --git a/slither/printers/summary/cheatcodes.py b/slither/printers/summary/cheatcodes.py index 8c84da8e65..50dcdc1583 100644 --- a/slither/printers/summary/cheatcodes.py +++ b/slither/printers/summary/cheatcodes.py @@ -4,6 +4,7 @@ This printer prints the usage of cheatcode in the code. """ from slither.printers.abstract_printer import AbstractPrinter +from slither.slithir.operations import HighLevelCall from slither.utils import output @@ -11,9 +12,59 @@ class CheatcodePrinter(AbstractPrinter): ARGUMENT = "cheatcode" - HELP = """""" + HELP = """ + Print the usage of (Foundry) cheatcodes in the code. + For the complete list of Cheatcodes, see https://book.getfoundry.sh/cheatcodes/ + """ WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#cheatcode" def output(self, filename: str) -> output.Output: - pass + + info: str = "" + + vm = self.slither.get_contract_from_name("Vm").pop() + + for contract in self.slither.contracts_derived: + # Check that the IS_TEST variable is set. (Only works for Foundry) + is_test_var = contract.variables_as_dict.get("IS_TEST", None) + is_test = False + if is_test_var is not None: + try: + is_test = is_test_var.expression.value == "true" + except AttributeError: + pass + + if not is_test: + continue + + found_contract: bool = False + contract_info: str = "" + for func in contract.functions_declared: + function_info = f"\t{func}\n" + found_function: bool = False + for node in func.nodes: + for op in node.all_slithir_operations(): + if ( + isinstance(op, HighLevelCall) + and op.function.contract == vm + and op.function.visibility == "external" + ): + found_function = True + function_info += ( + f"\t\tL{node.source_mapping.lines}: {op.function.name}\n" + ) + + if found_function: + if found_contract is False: + contract_info = f"{contract} ({contract.source_mapping.filename.short})\n" + found_contract = True + + contract_info += function_info + + if found_contract: + info += contract_info + + self.info(info) + res = output.Output(info) + return res diff --git a/tests/e2e/printers/test_data/test_printer_cheatcode/README.md b/tests/e2e/printers/test_data/test_printer_cheatcode/README.md index 9265b45584..71bbaff380 100644 --- a/tests/e2e/printers/test_data/test_printer_cheatcode/README.md +++ b/tests/e2e/printers/test_data/test_printer_cheatcode/README.md @@ -1,66 +1,6 @@ -## Foundry - -**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** - -Foundry consists of: - -- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). -- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. -- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. -- **Chisel**: Fast, utilitarian, and verbose solidity REPL. - -## Documentation - -https://book.getfoundry.sh/ - -## Usage - -### Build - -```shell -$ forge build -``` - -### Test - -```shell -$ forge test -``` - -### Format - -```shell -$ forge fmt -``` - -### Gas Snapshots - -```shell -$ forge snapshot -``` - -### Anvil - -```shell -$ anvil -``` - -### Deploy - -```shell -$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key -``` - -### Cast - -```shell -$ cast -``` - -### Help +# Counter +Init using : ```shell -$ forge --help -$ anvil --help -$ cast --help -``` +forge install foundry-rs/forge-std +``` \ No newline at end of file diff --git a/tests/e2e/printers/test_data/test_printer_cheatcode/src/Counter.sol b/tests/e2e/printers/test_data/test_printer_cheatcode/src/Counter.sol new file mode 100644 index 0000000000..aded7997b0 --- /dev/null +++ b/tests/e2e/printers/test_data/test_printer_cheatcode/src/Counter.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract Counter { + uint256 public number; + + function setNumber(uint256 newNumber) public { + number = newNumber; + } + + function increment() public { + number++; + } +} diff --git a/tests/e2e/printers/test_data/test_printer_cheatcode/src/Vault.sol b/tests/e2e/printers/test_data/test_printer_cheatcode/src/Vault.sol deleted file mode 100644 index 2aaf6653cc..0000000000 --- a/tests/e2e/printers/test_data/test_printer_cheatcode/src/Vault.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; - -contract Vault { - constructor(){ - - } -} diff --git a/tests/e2e/printers/test_data/test_printer_cheatcode/test/Counter.t.sol b/tests/e2e/printers/test_data/test_printer_cheatcode/test/Counter.t.sol new file mode 100644 index 0000000000..529a7ce088 --- /dev/null +++ b/tests/e2e/printers/test_data/test_printer_cheatcode/test/Counter.t.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "../src/Counter.sol"; + +contract CounterTest is Test { + Counter public counter; + + address public alice = address(0x42); + address public bob = address(0x43); + + function difficulty(uint256 value) public { + // empty + } + + function setUp() public { + counter = new Counter(); + counter.setNumber(0); + + vm.deal(alice, 1 ether); + vm.deal(bob, 2 ether); + + difficulty(1); + } + + function testIncrement() public { + vm.prank(alice); + counter.increment(); + assertEq(counter.number(), 1); + + vm.prank(bob); + counter.increment(); + assertEq(counter.number(), 2); + } +} diff --git a/tests/e2e/printers/test_data/test_printer_cheatcode/test/Vault.t.sol b/tests/e2e/printers/test_data/test_printer_cheatcode/test/Vault.t.sol deleted file mode 100644 index 6a598136df..0000000000 --- a/tests/e2e/printers/test_data/test_printer_cheatcode/test/Vault.t.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; - -import "forge-std/Test.sol"; -import "../src/Vault.sol"; - -contract VaultTest is Test { - - Vault public vault; - - address public alice = address(0x42); - - function broadcast() pure public {} - - function setUp() public { - vm.prank(alice); - vm.deal(alice, 1000 ether); - - uint256 value = 123; - - vm.warp(1641070800); - vm.roll(100); - vm.fee(25 gwei); - - // Not a cheatcode - broadcast(); - } - - function test_deal() public { - - } -} diff --git a/tests/e2e/printers/test_printers.py b/tests/e2e/printers/test_printers.py index 8d9c5587c5..121c330351 100644 --- a/tests/e2e/printers/test_printers.py +++ b/tests/e2e/printers/test_printers.py @@ -1,14 +1,15 @@ import re import shutil -import pytest from collections import Counter from pathlib import Path +import pytest from crytic_compile import CryticCompile 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.cheatcodes import CheatcodePrinter TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" @@ -45,4 +46,14 @@ def test_inheritance_printer(solc_binary_path) -> None: not foundry_available or not project_ready, reason="requires Foundry and project setup" ) def test_printer_cheatcode(): - slither = Slither(Path(TEST_DATA_DIR, "test_printer_cheatcode").as_posix()) \ No newline at end of file + slither = Slither( + Path(TEST_DATA_DIR, "test_printer_cheatcode").as_posix(), foundry_compile_all=True + ) + + printer = CheatcodePrinter(slither=slither, logger=None) + output = printer.output("cheatcode.out") + + assert ( + output + == "CounterTest (test/Counter.t.sol)\n\tsetUp\n\t\tL[21]: deal\n\t\tL[22]: deal\n\ttestIncrement\n\t\tL[28]: prank\n\t\tL[30]: assertEq\n\t\tL[32]: prank\n\t\tL[34]: assertEq\n" + ) From 7b9f543918883eaddf555d1e502c1efd2bb990b2 Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 9 Apr 2024 09:37:29 +0200 Subject: [PATCH 04/71] Fix test --- tests/e2e/printers/test_printers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/printers/test_printers.py b/tests/e2e/printers/test_printers.py index 121c330351..1825f5d5c6 100644 --- a/tests/e2e/printers/test_printers.py +++ b/tests/e2e/printers/test_printers.py @@ -54,6 +54,6 @@ def test_printer_cheatcode(): output = printer.output("cheatcode.out") assert ( - output + output.data["description"] == "CounterTest (test/Counter.t.sol)\n\tsetUp\n\t\tL[21]: deal\n\t\tL[22]: deal\n\ttestIncrement\n\t\tL[28]: prank\n\t\tL[30]: assertEq\n\t\tL[32]: prank\n\t\tL[34]: assertEq\n" ) From 2f0f8ccb47f15470a434d5e7a2091b858e12b290 Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 9 Apr 2024 09:38:28 +0200 Subject: [PATCH 05/71] Prevent failure when executed with a project not using `Vm` --- slither/printers/summary/cheatcodes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/slither/printers/summary/cheatcodes.py b/slither/printers/summary/cheatcodes.py index 50dcdc1583..24315a7e20 100644 --- a/slither/printers/summary/cheatcodes.py +++ b/slither/printers/summary/cheatcodes.py @@ -23,7 +23,10 @@ def output(self, filename: str) -> output.Output: info: str = "" - vm = self.slither.get_contract_from_name("Vm").pop() + try: + vm = self.slither.get_contract_from_name("Vm").pop() + except IndexError: + return output.Output("No contract named VM found") for contract in self.slither.contracts_derived: # Check that the IS_TEST variable is set. (Only works for Foundry) From b5766dc27dd86489ebfdede9f348d672f80b02a9 Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 9 Apr 2024 09:44:43 +0200 Subject: [PATCH 06/71] Add cheatcode printer to test files --- scripts/ci_test_printers.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci_test_printers.sh b/scripts/ci_test_printers.sh index 3306c134e2..d811c19f91 100755 --- a/scripts/ci_test_printers.sh +++ b/scripts/ci_test_printers.sh @@ -5,7 +5,7 @@ cd tests/e2e/solc_parsing/test_data/compile/ || exit # Do not test the evm printer,as it needs a refactoring -ALL_PRINTERS="cfg,constructor-calls,contract-summary,data-dependency,echidna,function-id,function-summary,modifiers,call-graph,halstead,human-summary,inheritance,inheritance-graph,loc,martin,slithir,slithir-ssa,vars-and-auth,require,variable-order,declaration,ck" +ALL_PRINTERS="cfg,constructor-calls,contract-summary,data-dependency,echidna,function-id,function-summary,modifiers,call-graph,halstead,human-summary,inheritance,inheritance-graph,loc,martin,slithir,slithir-ssa,vars-and-auth,require,variable-order,declaration,ck,cheatcode" # Only test 0.5.17 to limit test time for file in *0.5.17-compact.zip; do From 90373fbe6e653fb8dfc2f9b860aef69858c7d189 Mon Sep 17 00:00:00 2001 From: Alexis Date: Fri, 26 Apr 2024 13:42:53 +1100 Subject: [PATCH 07/71] Upgrade cheatcode printer --- slither/printers/summary/cheatcodes.py | 3 ++- tests/e2e/printers/test_printers.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/slither/printers/summary/cheatcodes.py b/slither/printers/summary/cheatcodes.py index 24315a7e20..857ec2da9f 100644 --- a/slither/printers/summary/cheatcodes.py +++ b/slither/printers/summary/cheatcodes.py @@ -55,7 +55,8 @@ def output(self, filename: str) -> output.Output: ): found_function = True function_info += ( - f"\t\tL{node.source_mapping.lines}: {op.function.name}\n" + f"\t\t{op.function.name} - ({node.source_mapping.to_detailed_str()})\n" + f"\t\t{node.expression}\n\n" ) if found_function: diff --git a/tests/e2e/printers/test_printers.py b/tests/e2e/printers/test_printers.py index 1825f5d5c6..c5b82b2572 100644 --- a/tests/e2e/printers/test_printers.py +++ b/tests/e2e/printers/test_printers.py @@ -51,9 +51,9 @@ def test_printer_cheatcode(): ) printer = CheatcodePrinter(slither=slither, logger=None) - output = printer.output("cheatcode.out") + output = printer.output("") assert ( output.data["description"] - == "CounterTest (test/Counter.t.sol)\n\tsetUp\n\t\tL[21]: deal\n\t\tL[22]: deal\n\ttestIncrement\n\t\tL[28]: prank\n\t\tL[30]: assertEq\n\t\tL[32]: prank\n\t\tL[34]: assertEq\n" + == "CounterTest (test/Counter.t.sol)\n\tsetUp\n\t\tdeal - (test/Counter.t.sol#21 (9 - 32)\n\t\tvm.deal(alice,1000000000000000000)\n\n\t\tdeal - (test/Counter.t.sol#22 (9 - 30)\n\t\tvm.deal(bob,2000000000000000000)\n\n\ttestIncrement\n\t\tprank - (test/Counter.t.sol#28 (9 - 24)\n\t\tvm.prank(alice)\n\n\t\tassertEq - (test/Counter.t.sol#30 (9 - 38)\n\t\tassertEq(counter.number(),1)\n\n\t\tprank - (test/Counter.t.sol#32 (9 - 22)\n\t\tvm.prank(bob)\n\n\t\tassertEq - (test/Counter.t.sol#34 (9 - 38)\n\t\tassertEq(counter.number(),2)\n\n" ) From ee46caecff72f5faebe72fe2d446013fbf6b9346 Mon Sep 17 00:00:00 2001 From: Alexis Date: Thu, 2 May 2024 07:40:56 +1100 Subject: [PATCH 08/71] Add missing blank line. --- tests/e2e/printers/test_data/test_printer_cheatcode/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/printers/test_data/test_printer_cheatcode/README.md b/tests/e2e/printers/test_data/test_printer_cheatcode/README.md index 71bbaff380..f1a26a296c 100644 --- a/tests/e2e/printers/test_data/test_printer_cheatcode/README.md +++ b/tests/e2e/printers/test_data/test_printer_cheatcode/README.md @@ -1,6 +1,7 @@ # Counter Init using : + ```shell forge install foundry-rs/forge-std ``` \ No newline at end of file From f3f4b2084c053c37099a4f56021aeeb5cd76d9c4 Mon Sep 17 00:00:00 2001 From: dm Date: Wed, 5 Jun 2024 16:37:48 +0200 Subject: [PATCH 09/71] Update tests/e2e/printers/test_printers.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- tests/e2e/printers/test_printers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/e2e/printers/test_printers.py b/tests/e2e/printers/test_printers.py index fc7a22378c..aa5d7f8a46 100644 --- a/tests/e2e/printers/test_printers.py +++ b/tests/e2e/printers/test_printers.py @@ -35,7 +35,6 @@ def test_inheritance_printer(solc_binary_path) -> None: assert counter["B -> A"] == 2 assert counter["C -> A"] == 1 - # Let also test the include/exclude interface behavior # Check that the interface is not included assert "MyInterfaceX" not in content From ecca8da74ec9cee207b2bbce2dc87a8e3adbfeea Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 11 Jun 2024 12:05:50 +0200 Subject: [PATCH 10/71] Improve performances of offsets references. --- slither/core/slither_core.py | 134 +++++++++++------- slither/core/source_mapping/source_mapping.py | 6 +- 2 files changed, 87 insertions(+), 53 deletions(-) diff --git a/slither/core/slither_core.py b/slither/core/slither_core.py index 1206e564bc..f5e36ace31 100644 --- a/slither/core/slither_core.py +++ b/slither/core/slither_core.py @@ -8,7 +8,7 @@ import posixpath import re from collections import defaultdict -from typing import Optional, Dict, List, Set, Union, Tuple +from typing import Optional, Dict, List, Set, Union, Tuple, TypeVar from crytic_compile import CryticCompile from crytic_compile.utils.naming import Filename @@ -88,6 +88,7 @@ def __init__(self) -> None: self._contracts: List[Contract] = [] self._contracts_derived: List[Contract] = [] + self._offset_to_min_offset: Optional[Dict[Filename, Dict[int, Set[int]]]] = None self._offset_to_objects: Optional[Dict[Filename, Dict[int, Set[SourceMapping]]]] = None self._offset_to_references: Optional[Dict[Filename, Dict[int, Set[Source]]]] = None self._offset_to_implementations: Optional[Dict[Filename, Dict[int, Set[Source]]]] = None @@ -195,69 +196,70 @@ def print_functions(self, d: str): for f in c.functions: f.cfg_to_dot(os.path.join(d, f"{c.name}.{f.name}.dot")) - def offset_to_objects(self, filename_str: str, offset: int) -> Set[SourceMapping]: - if self._offset_to_objects is None: - self._compute_offsets_to_ref_impl_decl() - filename: Filename = self.crytic_compile.filename_lookup(filename_str) - return self._offset_to_objects[filename][offset] - def _compute_offsets_from_thing(self, thing: SourceMapping): definition = get_definition(thing, self.crytic_compile) references = get_references(thing) implementations = get_all_implementations(thing, self.contracts) + # Create the offset mapping for offset in range(definition.start, definition.end + 1): - if ( - isinstance(thing, (TopLevel, Contract)) - or ( - isinstance(thing, FunctionContract) - and thing.contract_declarer == thing.contract - ) - or (isinstance(thing, ContractLevel) and not isinstance(thing, FunctionContract)) - ): + self._offset_to_min_offset[definition.filename][offset].add(definition.start) - self._offset_to_objects[definition.filename][offset].add(thing) + is_declared_function = ( + isinstance(thing, FunctionContract) and thing.contract_declarer == thing.contract + ) - self._offset_to_definitions[definition.filename][offset].add(definition) - self._offset_to_implementations[definition.filename][offset].update(implementations) - self._offset_to_references[definition.filename][offset] |= set(references) + should_add_to_objects = ( + isinstance(thing, (TopLevel, Contract)) + or is_declared_function + or (isinstance(thing, ContractLevel) and not isinstance(thing, FunctionContract)) + ) + + if should_add_to_objects: + self._offset_to_objects[definition.filename][definition.start].add(thing) + + self._offset_to_definitions[definition.filename][definition.start].add(definition) + self._offset_to_implementations[definition.filename][definition.start].update( + implementations + ) + self._offset_to_references[definition.filename][definition.start] |= set(references) + + # For references + should_add_to_objects = ( + isinstance(thing, TopLevel) + or is_declared_function + or (isinstance(thing, ContractLevel) and not isinstance(thing, FunctionContract)) + ) for ref in references: for offset in range(ref.start, ref.end + 1): - is_declared_function = ( - isinstance(thing, FunctionContract) - and thing.contract_declarer == thing.contract - ) + self._offset_to_min_offset[definition.filename][offset].add(ref.start) + + if should_add_to_objects: + self._offset_to_objects[definition.filename][ref.start].add(thing) + + if is_declared_function: + # Only show the nearest lexical definition for declared contract-level functions if ( - isinstance(thing, TopLevel) - or is_declared_function - or ( - isinstance(thing, ContractLevel) and not isinstance(thing, FunctionContract) - ) + thing.contract.source_mapping.start + < ref.start + < thing.contract.source_mapping.end ): - self._offset_to_objects[definition.filename][offset].add(thing) - - if is_declared_function: - # Only show the nearest lexical definition for declared contract-level functions - if ( - thing.contract.source_mapping.start - < offset - < thing.contract.source_mapping.end - ): - self._offset_to_definitions[ref.filename][offset].add(definition) + self._offset_to_definitions[ref.filename][ref.start].add(definition) - else: - self._offset_to_definitions[ref.filename][offset].add(definition) + else: + self._offset_to_definitions[ref.filename][ref.start].add(definition) - self._offset_to_implementations[ref.filename][offset].update(implementations) - self._offset_to_references[ref.filename][offset] |= set(references) + self._offset_to_implementations[ref.filename][ref.start].update(implementations) + self._offset_to_references[ref.filename][ref.start] |= set(references) def _compute_offsets_to_ref_impl_decl(self): # pylint: disable=too-many-branches self._offset_to_references = defaultdict(lambda: defaultdict(lambda: set())) self._offset_to_definitions = defaultdict(lambda: defaultdict(lambda: set())) self._offset_to_implementations = defaultdict(lambda: defaultdict(lambda: set())) self._offset_to_objects = defaultdict(lambda: defaultdict(lambda: set())) + self._offset_to_min_offset = defaultdict(lambda: defaultdict(lambda: set())) for compilation_unit in self._compilation_units: for contract in compilation_unit.contracts: @@ -308,23 +310,59 @@ def _compute_offsets_to_ref_impl_decl(self): # pylint: disable=too-many-branche for pragma in compilation_unit.pragma_directives: self._compute_offsets_from_thing(pragma) + T = TypeVar("T", Source, SourceMapping) + + def _get_offset( + self, mapping: Dict[Filename, Dict[int, Set[T]]], filename_str: str, offset: int + ) -> Set[T]: + """Get the Source/SourceMapping referenced by the offset. + + For performance reasons, references are only stored once at the lowest offset. + It uses the _offset_to_min_offset mapping to retrieve the correct offsets. + As multiple definitions can be related to the same offset, we retrieve all of them. + + :param mapping: Mapping to search for (objects. references, ...) + :param filename_str: Filename to consider + :param offset: Look-up offset + :raises IndexError: When the start offset is not found + :return: The corresponding set of Source/SourceMapping + """ + filename: Filename = self.crytic_compile.filename_lookup(filename_str) + + start_offsets = self._offset_to_min_offset[filename][offset] + if not start_offsets: + msg = f"Unable to find reference for offset {offset}" + raise IndexError(msg) + + results = set() + for start_offset in start_offsets: + results |= mapping[filename][start_offset] + + return results + def offset_to_references(self, filename_str: str, offset: int) -> Set[Source]: if self._offset_to_references is None: self._compute_offsets_to_ref_impl_decl() - filename: Filename = self.crytic_compile.filename_lookup(filename_str) - return self._offset_to_references[filename][offset] + + return self._get_offset(self._offset_to_references, filename_str, offset) def offset_to_implementations(self, filename_str: str, offset: int) -> Set[Source]: if self._offset_to_implementations is None: self._compute_offsets_to_ref_impl_decl() - filename: Filename = self.crytic_compile.filename_lookup(filename_str) - return self._offset_to_implementations[filename][offset] + + return self._get_offset(self._offset_to_implementations, filename_str, offset) def offset_to_definitions(self, filename_str: str, offset: int) -> Set[Source]: if self._offset_to_definitions is None: self._compute_offsets_to_ref_impl_decl() - filename: Filename = self.crytic_compile.filename_lookup(filename_str) - return self._offset_to_definitions[filename][offset] + + return self._get_offset(self._offset_to_definitions, filename_str, offset) + + def offset_to_objects(self, filename_str: str, offset: int) -> Set[SourceMapping]: + if self._offset_to_objects is None: + self._compute_offsets_to_ref_impl_decl() + + return self._get_offset(self._offset_to_objects, filename_str, offset) # endregion ################################################################################### diff --git a/slither/core/source_mapping/source_mapping.py b/slither/core/source_mapping/source_mapping.py index 41841f1e80..355aa55386 100644 --- a/slither/core/source_mapping/source_mapping.py +++ b/slither/core/source_mapping/source_mapping.py @@ -112,12 +112,8 @@ def __eq__(self, other: Any) -> bool: try: return ( self.start == other.start - and self.length == other.length - and self.filename == other.filename + and self.filename.relative == other.filename.relative and self.is_dependency == other.is_dependency - and self.lines == other.lines - and self.starting_column == other.starting_column - and self.ending_column == other.ending_column and self.end == other.end ) except AttributeError: From 8f8bfeff6ae16e28559d9d493d36f1a2f9a696a7 Mon Sep 17 00:00:00 2001 From: Jean Cvllr <31145285+CJ42@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:51:31 +0100 Subject: [PATCH 11/71] Add instruction for how to upgrade slither in the CLI --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index f1e491103f..34932b3b49 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,12 @@ Slither requires Python 3.6+ and [solc](https://github.com/ethereum/solidity/), pip3 install slither-analyzer ``` +If your slither version is behind the latest available release, you can upgrade slither to stay up to date with the latest features and bug fixes using the following command: + +```console +pip install --upgrade slither-analyzer +``` + ### Using Git ```bash From 1512fb0740a6c4d70bc5268adb613c69da37ac13 Mon Sep 17 00:00:00 2001 From: withbest Date: Mon, 29 Jul 2024 00:44:55 +0800 Subject: [PATCH 12/71] chore: fix some comments Signed-off-by: withbest --- Makefile | 2 +- slither/core/declarations/function.py | 4 ++-- slither/core/declarations/solidity_variables.py | 2 +- slither/detectors/reentrancy/reentrancy.py | 2 +- slither/slithir/convert.py | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index a94c3eeb87..17697385ed 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ TEST_MODULE := tests ALL_PY_SRCS := $(shell find $(PY_MODULE) -name '*.py') \ $(shell find test -name '*.py') -# Optionally overriden by the user, if they're using a virtual environment manager. +# Optionally overridden by the user, if they're using a virtual environment manager. VENV ?= env # On Windows, venv scripts/shims are under `Scripts` instead of `bin`. diff --git a/slither/core/declarations/function.py b/slither/core/declarations/function.py index 6e8968dfb2..6f9a8ac407 100644 --- a/slither/core/declarations/function.py +++ b/slither/core/declarations/function.py @@ -776,14 +776,14 @@ def add_explicit_base_constructor_calls_statements(self, modif: ModifierStatemen def variables(self) -> List[LocalVariable]: """ Return all local variables - Include paramters and return values + Include parameters and return values """ return list(self._variables.values()) @property def local_variables(self) -> List[LocalVariable]: """ - Return all local variables (dont include paramters and return values) + Return all local variables (dont include parameters and return values) """ return list(set(self.variables) - set(self.returns) - set(self.parameters)) diff --git a/slither/core/declarations/solidity_variables.py b/slither/core/declarations/solidity_variables.py index 8094ab7c35..2ff8260900 100644 --- a/slither/core/declarations/solidity_variables.py +++ b/slither/core/declarations/solidity_variables.py @@ -82,7 +82,7 @@ "abi.encodeCall()": ["bytes"], "bytes.concat()": ["bytes"], "string.concat()": ["string"], - # abi.decode returns an a list arbitrary types + # abi.decode returns a list arbitrary types "abi.decode()": [], "type(address)": [], "type()": [], # 0.6.8 changed type(address) to type() diff --git a/slither/detectors/reentrancy/reentrancy.py b/slither/detectors/reentrancy/reentrancy.py index 8dd9aecc05..6545615cc9 100644 --- a/slither/detectors/reentrancy/reentrancy.py +++ b/slither/detectors/reentrancy/reentrancy.py @@ -195,7 +195,7 @@ def does_not_bring_new_info(self, new_info: "AbstractState") -> bool: def _filter_if(node: Node) -> bool: """ - Check if the node is a condtional node where + Check if the node is a conditional node where there is an external call checked Heuristic: - The call is a IF node diff --git a/slither/slithir/convert.py b/slither/slithir/convert.py index 7d8aa543bf..93f1524dcc 100644 --- a/slither/slithir/convert.py +++ b/slither/slithir/convert.py @@ -995,7 +995,7 @@ def extract_tmp_call(ins: TmpCall, contract: Optional[Contract]) -> Union[Call, # Support for library call where the parameter is a function # We could merge this with the standard library handling # Except that we will have some troubles with using_for - # As the type of the funciton will not match function() + # As the type of the function will not match function() # Additionally we do not have a correct view on the parameters of the tmpcall # At this level # @@ -1237,7 +1237,7 @@ def convert_to_low_level( ) -> Union[Send, LowLevelCall, Transfer,]: """ Convert to a transfer/send/or low level call - The funciton assume to receive a correct IR + The function assume to receive a correct IR The checks must be done by the caller Must be called after can_be_low_level From cadcf1e90c64f839cdbeea17dba25c3f7633f158 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Fri, 23 Aug 2024 07:54:36 -0500 Subject: [PATCH 13/71] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 34932b3b49..bf0f3e4692 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ Slither requires Python 3.6+ and [solc](https://github.com/ethereum/solidity/), pip3 install slither-analyzer ``` -If your slither version is behind the latest available release, you can upgrade slither to stay up to date with the latest features and bug fixes using the following command: +#### How to upgrade ```console pip install --upgrade slither-analyzer From 7aa0367748e9d453d8becf9474f9173058cbbab9 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Fri, 23 Aug 2024 07:54:49 -0500 Subject: [PATCH 14/71] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bf0f3e4692..174760b6e4 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ pip3 install slither-analyzer #### How to upgrade ```console -pip install --upgrade slither-analyzer +python3 -m pip install --upgrade slither-analyzer ``` ### Using Git From 7bfa87ad76e39f7a046b20a7342e1e4c8239ddce Mon Sep 17 00:00:00 2001 From: Simone Date: Mon, 9 Sep 2024 10:38:28 +0200 Subject: [PATCH 15/71] Add support custom errors in require --- .../core/declarations/solidity_variables.py | 1 + .../require-error.sol-0.8.27-compact.zip | Bin 0 -> 3685 bytes .../require-error.sol-0.8.27-compact.json | 5 +++++ .../solc_parsing/test_data/require-error.sol | 20 ++++++++++++++++++ 4 files changed, 26 insertions(+) create mode 100644 tests/e2e/solc_parsing/test_data/compile/require-error.sol-0.8.27-compact.zip create mode 100644 tests/e2e/solc_parsing/test_data/expected/require-error.sol-0.8.27-compact.json create mode 100644 tests/e2e/solc_parsing/test_data/require-error.sol diff --git a/slither/core/declarations/solidity_variables.py b/slither/core/declarations/solidity_variables.py index 8094ab7c35..ce8477ab23 100644 --- a/slither/core/declarations/solidity_variables.py +++ b/slither/core/declarations/solidity_variables.py @@ -50,6 +50,7 @@ "assert(bool)": [], "require(bool)": [], "require(bool,string)": [], + "require(bool,error)": [], # Solidity 0.8.26 via-ir and Solidity >= 0.8.27 "revert()": [], "revert(string)": [], "revert ": [], diff --git a/tests/e2e/solc_parsing/test_data/compile/require-error.sol-0.8.27-compact.zip b/tests/e2e/solc_parsing/test_data/compile/require-error.sol-0.8.27-compact.zip new file mode 100644 index 0000000000000000000000000000000000000000..63aa223b37ffa0e9d4e552aa106161a54fd79272 GIT binary patch literal 3685 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 literal 0 HcmV?d00001 diff --git a/tests/e2e/solc_parsing/test_data/expected/require-error.sol-0.8.27-compact.json b/tests/e2e/solc_parsing/test_data/expected/require-error.sol-0.8.27-compact.json new file mode 100644 index 0000000000..3c3089c048 --- /dev/null +++ b/tests/e2e/solc_parsing/test_data/expected/require-error.sol-0.8.27-compact.json @@ -0,0 +1,5 @@ +{ + "TestToken": { + "transferWithRequireError(address,uint256)": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n2->3;\n3[label=\"Node Type: EXPRESSION 3\n\"];\n}\n" + } +} \ No newline at end of file diff --git a/tests/e2e/solc_parsing/test_data/require-error.sol b/tests/e2e/solc_parsing/test_data/require-error.sol new file mode 100644 index 0000000000..97c8ecac4f --- /dev/null +++ b/tests/e2e/solc_parsing/test_data/require-error.sol @@ -0,0 +1,20 @@ +pragma solidity 0.8.27; + +/// Insufficient balance for transfer. Needed `required` but only +/// `available` available. +/// @param available balance available. +/// @param required requested amount to transfer. +error InsufficientBalance(uint256 available, uint256 required); + +contract TestToken { + mapping(address => uint) balance; + function transferWithRequireError(address to, uint256 amount) public { + require( + balance[msg.sender] >= amount, + InsufficientBalance(balance[msg.sender], amount) + ); + balance[msg.sender] -= amount; + balance[to] += amount; + } + // ... +} From 5d55bc5752070f28330acafe890100b48b59aff0 Mon Sep 17 00:00:00 2001 From: Simone Date: Mon, 9 Sep 2024 10:44:29 +0200 Subject: [PATCH 16/71] Add test --- tests/e2e/solc_parsing/test_ast_parsing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/solc_parsing/test_ast_parsing.py b/tests/e2e/solc_parsing/test_ast_parsing.py index ca3872f8c5..6ec7b6fbd2 100644 --- a/tests/e2e/solc_parsing/test_ast_parsing.py +++ b/tests/e2e/solc_parsing/test_ast_parsing.py @@ -475,6 +475,7 @@ def make_version(minor: int, patch_min: int, patch_max: int) -> List[str]: Test("solidity-0.8.24.sol", ["0.8.24"], solc_args="--evm-version cancun"), Test("scope/inherited_function_scope.sol", ["0.8.24"]), Test("using_for_global_user_defined_operator_1.sol", ["0.8.24"]), + Test("require-error.sol", ["0.8.27"]), ] # create the output folder if needed try: From 109c31adcf0b31f4a45c1ce63ab97db63496844e Mon Sep 17 00:00:00 2001 From: Simone Date: Wed, 11 Sep 2024 14:01:59 +0200 Subject: [PATCH 17/71] Add require with error --- slither/core/cfg/node.py | 3 ++- slither/printers/summary/require_calls.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/slither/core/cfg/node.py b/slither/core/cfg/node.py index 87d0e16a2e..f600d0f43d 100644 --- a/slither/core/cfg/node.py +++ b/slither/core/cfg/node.py @@ -529,7 +529,8 @@ def contains_require_or_assert(self) -> bool: bool: True if the node has a require or assert call """ return any( - c.name in ["require(bool)", "require(bool,string)", "assert(bool)"] + c.name + in ["require(bool)", "require(bool,string)", "require(bool,error)", "assert(bool)"] for c in self.internal_calls ) diff --git a/slither/printers/summary/require_calls.py b/slither/printers/summary/require_calls.py index 7823de1600..ae79e9ed66 100644 --- a/slither/printers/summary/require_calls.py +++ b/slither/printers/summary/require_calls.py @@ -11,6 +11,7 @@ SolidityFunction("assert(bool)"), SolidityFunction("require(bool)"), SolidityFunction("require(bool,string)"), + SolidityFunction("require(bool,error)"), ] From 7fd0e5883cfe4b0d4ed843209226fcc05f7653bd Mon Sep 17 00:00:00 2001 From: Simone Date: Fri, 13 Sep 2024 14:30:07 +0200 Subject: [PATCH 18/71] Revisit *Calls API --- slither/core/cfg/node.py | 69 ++++++-------- slither/core/declarations/contract.py | 22 +++-- slither/core/declarations/function.py | 92 ++++++++++--------- .../detectors/assembly/incorrect_return.py | 3 +- slither/detectors/attributes/locked_ether.py | 2 +- .../erc/erc20/arbitrary_send_erc20.py | 8 +- slither/detectors/functions/dead_code.py | 6 +- slither/detectors/functions/modifier.py | 2 +- .../functions/out_of_order_retryable.py | 6 +- .../detectors/functions/protected_variable.py | 4 +- slither/detectors/functions/suicidal.py | 2 +- slither/detectors/reentrancy/reentrancy.py | 9 +- .../statements/assert_state_change.py | 2 +- .../statements/divide_before_multiply.py | 2 +- .../statements/unprotected_upgradeable.py | 4 +- slither/printers/call/call_graph.py | 25 ++--- slither/printers/functions/authorization.py | 6 +- slither/printers/guidance/echidna.py | 4 +- slither/printers/summary/modifier_calls.py | 12 +-- slither/printers/summary/when_not_paused.py | 4 +- slither/tools/flattening/flattening.py | 5 +- .../tools/possible_paths/possible_paths.py | 10 +- slither/utils/upgradeability.py | 13 ++- 23 files changed, 168 insertions(+), 144 deletions(-) diff --git a/slither/core/cfg/node.py b/slither/core/cfg/node.py index 87d0e16a2e..06868ab827 100644 --- a/slither/core/cfg/node.py +++ b/slither/core/cfg/node.py @@ -46,12 +46,6 @@ if TYPE_CHECKING: from slither.slithir.variables.variable import SlithIRVariable from slither.core.compilation_unit import SlitherCompilationUnit - from slither.utils.type_helpers import ( - InternalCallType, - HighLevelCallType, - LibraryCallType, - LowLevelCallType, - ) from slither.core.cfg.scope import Scope from slither.core.scope.scope import FileScope @@ -153,11 +147,11 @@ def __init__( self._ssa_vars_written: List["SlithIRVariable"] = [] self._ssa_vars_read: List["SlithIRVariable"] = [] - self._internal_calls: List[Union["Function", "SolidityFunction"]] = [] - self._solidity_calls: List[SolidityFunction] = [] - self._high_level_calls: List["HighLevelCallType"] = [] # contains library calls - self._library_calls: List["LibraryCallType"] = [] - self._low_level_calls: List["LowLevelCallType"] = [] + self._internal_calls: List[InternalCall] = [] # contains solidity calls + self._solidity_calls: List[SolidityCall] = [] + self._high_level_calls: List[Tuple[Contract, HighLevelCall]] = [] # contains library calls + self._library_calls: List[LibraryCall] = [] + self._low_level_calls: List[LowLevelCall] = [] self._external_calls_as_expressions: List[Expression] = [] self._internal_calls_as_expressions: List[Expression] = [] self._irs: List[Operation] = [] @@ -226,8 +220,9 @@ def type(self, new_type: NodeType) -> None: @property def will_return(self) -> bool: if not self.sons and self.type != NodeType.THROW: - if SolidityFunction("revert()") not in self.solidity_calls: - if SolidityFunction("revert(string)") not in self.solidity_calls: + solidity_calls = [ir.function for ir in self.solidity_calls] + if SolidityFunction("revert()") not in solidity_calls: + if SolidityFunction("revert(string)") not in solidity_calls: return True return False @@ -373,44 +368,38 @@ def variables_written_as_expression(self, exprs: List[Expression]) -> None: ################################################################################### @property - def internal_calls(self) -> List["InternalCallType"]: + def internal_calls(self) -> List[InternalCall]: """ - list(Function or SolidityFunction): List of internal/soldiity function calls + list(InternalCall): List of IR operations with internal/solidity function calls """ return list(self._internal_calls) @property - def solidity_calls(self) -> List[SolidityFunction]: + def solidity_calls(self) -> List[SolidityCall]: """ - list(SolidityFunction): List of Soldity calls + list(SolidityCall): List of IR operations with solidity calls """ return list(self._solidity_calls) @property - def high_level_calls(self) -> List["HighLevelCallType"]: + def high_level_calls(self) -> List[HighLevelCall]: """ - list((Contract, Function|Variable)): - List of high level calls (external calls). - A variable is called in case of call to a public state variable + list(HighLevelCall): List of IR operations with high level calls (external calls). Include library calls """ return list(self._high_level_calls) @property - def library_calls(self) -> List["LibraryCallType"]: + def library_calls(self) -> List[LibraryCall]: """ - list((Contract, Function)): - Include library calls + list(LibraryCall): List of IR operations with library calls. """ return list(self._library_calls) @property - def low_level_calls(self) -> List["LowLevelCallType"]: + def low_level_calls(self) -> List[LowLevelCall]: """ - list((Variable|SolidityVariable, str)): List of low_level call - A low level call is defined by - - the variable called - - the name of the function (call/delegatecall/codecall) + list(LowLevelCall): List of IR operations with low_level call """ return list(self._low_level_calls) @@ -529,8 +518,8 @@ def contains_require_or_assert(self) -> bool: bool: True if the node has a require or assert call """ return any( - c.name in ["require(bool)", "require(bool,string)", "assert(bool)"] - for c in self.internal_calls + ir.function.name in ["require(bool)", "require(bool,string)", "assert(bool)"] + for ir in self.internal_calls ) def contains_if(self, include_loop: bool = True) -> bool: @@ -894,11 +883,11 @@ def _find_read_write_call(self) -> None: # pylint: disable=too-many-statements self._vars_written.append(var) if isinstance(ir, InternalCall): - self._internal_calls.append(ir.function) + self._internal_calls.append(ir) if isinstance(ir, SolidityCall): # TODO: consider removing dependancy of solidity_call to internal_call - self._solidity_calls.append(ir.function) - self._internal_calls.append(ir.function) + self._solidity_calls.append(ir) + self._internal_calls.append(ir) if ( isinstance(ir, SolidityCall) and ir.function == SolidityFunction("sstore(uint256,uint256)") @@ -916,22 +905,22 @@ def _find_read_write_call(self) -> None: # pylint: disable=too-many-statements self._vars_read.append(ir.arguments[0]) if isinstance(ir, LowLevelCall): assert isinstance(ir.destination, (Variable, SolidityVariable)) - self._low_level_calls.append((ir.destination, str(ir.function_name.value))) + self._low_level_calls.append(ir) elif isinstance(ir, HighLevelCall) and not isinstance(ir, LibraryCall): # Todo investigate this if condition # It does seem right to compare against a contract # This might need a refactoring if isinstance(ir.destination.type, Contract): - self._high_level_calls.append((ir.destination.type, ir.function)) + self._high_level_calls.append((ir.destination.type, ir)) elif ir.destination == SolidityVariable("this"): func = self.function # Can't use this in a top level function assert isinstance(func, FunctionContract) - self._high_level_calls.append((func.contract, ir.function)) + self._high_level_calls.append((func.contract, ir)) else: try: # Todo this part needs more tests and documentation - self._high_level_calls.append((ir.destination.type.type, ir.function)) + self._high_level_calls.append((ir.destination.type.type, ir)) except AttributeError as error: # pylint: disable=raise-missing-from raise SlitherException( @@ -940,8 +929,8 @@ def _find_read_write_call(self) -> None: # pylint: disable=too-many-statements elif isinstance(ir, LibraryCall): assert isinstance(ir.destination, Contract) assert isinstance(ir.function, Function) - self._high_level_calls.append((ir.destination, ir.function)) - self._library_calls.append((ir.destination, ir.function)) + self._high_level_calls.append((ir.destination, ir)) + self._library_calls.append(ir) self._vars_read = list(set(self._vars_read)) self._state_vars_read = [v for v in self._vars_read if isinstance(v, StateVariable)] diff --git a/slither/core/declarations/contract.py b/slither/core/declarations/contract.py index 3f97a33ed2..a2f24fc6cd 100644 --- a/slither/core/declarations/contract.py +++ b/slither/core/declarations/contract.py @@ -29,7 +29,6 @@ # pylint: disable=too-many-lines,too-many-instance-attributes,import-outside-toplevel,too-many-nested-blocks if TYPE_CHECKING: - from slither.utils.type_helpers import LibraryCallType, HighLevelCallType, InternalCallType from slither.core.declarations import ( Enum, EventContract, @@ -39,6 +38,7 @@ FunctionContract, CustomErrorContract, ) + from slither.slithir.operations import HighLevelCall, LibraryCall from slither.slithir.variables.variable import SlithIRVariable from slither.core.variables import Variable, StateVariable from slither.core.compilation_unit import SlitherCompilationUnit @@ -106,7 +106,7 @@ def __init__(self, compilation_unit: "SlitherCompilationUnit", scope: "FileScope self._is_incorrectly_parsed: bool = False self._available_functions_as_dict: Optional[Dict[str, "Function"]] = None - self._all_functions_called: Optional[List["InternalCallType"]] = None + self._all_functions_called: Optional[List["Function"]] = None self.compilation_unit: "SlitherCompilationUnit" = compilation_unit self.file_scope: "FileScope" = scope @@ -1023,15 +1023,21 @@ def get_functions_overridden_by(self, function: "Function") -> List["Function"]: ################################################################################### @property - def all_functions_called(self) -> List["InternalCallType"]: + def all_functions_called(self) -> List["Function"]: """ list(Function): List of functions reachable from the contract Includes super, and private/internal functions not shadowed """ + from slither.slithir.operations import Operation + if self._all_functions_called is None: all_functions = [f for f in self.functions + self.modifiers if not f.is_shadowed] # type: ignore all_callss = [f.all_internal_calls() for f in all_functions] + [list(all_functions)] - all_calls = [item for sublist in all_callss for item in sublist] + all_calls = [ + item.function if isinstance(item, Operation) else item + for sublist in all_callss + for item in sublist + ] all_calls = list(set(all_calls)) all_constructors = [c.constructor for c in self.inheritance if c.constructor] @@ -1069,18 +1075,18 @@ def all_state_variables_read(self) -> List["StateVariable"]: return list(set(all_state_variables_read)) @property - def all_library_calls(self) -> List["LibraryCallType"]: + def all_library_calls(self) -> List["LibraryCall"]: """ - list((Contract, Function): List all of the libraries func called + list(LibraryCall): List all of the libraries func called """ all_high_level_callss = [f.all_library_calls() for f in self.functions + self.modifiers] # type: ignore all_high_level_calls = [item for sublist in all_high_level_callss for item in sublist] return list(set(all_high_level_calls)) @property - def all_high_level_calls(self) -> List["HighLevelCallType"]: + def all_high_level_calls(self) -> List[Tuple["Contract", "HighLevelCall"]]: """ - list((Contract, Function|Variable)): List all of the external high level calls + list(Tuple("Contract", "HighLevelCall")): List all of the external high level calls """ all_high_level_callss = [f.all_high_level_calls() for f in self.functions + self.modifiers] # type: ignore all_high_level_calls = [item for sublist in all_high_level_callss for item in sublist] diff --git a/slither/core/declarations/function.py b/slither/core/declarations/function.py index 6e8968dfb2..0b44a9e255 100644 --- a/slither/core/declarations/function.py +++ b/slither/core/declarations/function.py @@ -31,19 +31,20 @@ # pylint: disable=import-outside-toplevel,too-many-instance-attributes,too-many-statements,too-many-lines if TYPE_CHECKING: - from slither.utils.type_helpers import ( - InternalCallType, - LowLevelCallType, - HighLevelCallType, - LibraryCallType, - ) from slither.core.declarations import Contract, FunctionContract from slither.core.cfg.node import Node, NodeType from slither.core.variables.variable import Variable from slither.slithir.variables.variable import SlithIRVariable from slither.slithir.variables import LocalIRVariable from slither.core.expressions.expression import Expression - from slither.slithir.operations import Operation + from slither.slithir.operations import ( + HighLevelCall, + InternalCall, + LibraryCall, + LowLevelCall, + SolidityCall, + Operation, + ) from slither.core.compilation_unit import SlitherCompilationUnit from slither.core.scope.scope import FileScope @@ -149,11 +150,11 @@ def __init__(self, compilation_unit: "SlitherCompilationUnit") -> None: self._vars_read_or_written: List["Variable"] = [] self._solidity_vars_read: List["SolidityVariable"] = [] self._state_vars_written: List["StateVariable"] = [] - self._internal_calls: List["InternalCallType"] = [] - self._solidity_calls: List["SolidityFunction"] = [] - self._low_level_calls: List["LowLevelCallType"] = [] - self._high_level_calls: List["HighLevelCallType"] = [] - self._library_calls: List["LibraryCallType"] = [] + self._internal_calls: List["InternalCall"] = [] + self._solidity_calls: List["SolidityCall"] = [] + self._low_level_calls: List["LowLevelCall"] = [] + self._high_level_calls: List[Tuple["Contract", "HighLevelCall"]] = [] + self._library_calls: List["LibraryCall"] = [] self._external_calls_as_expressions: List["Expression"] = [] self._expression_vars_read: List["Expression"] = [] self._expression_vars_written: List["Expression"] = [] @@ -169,11 +170,11 @@ def __init__(self, compilation_unit: "SlitherCompilationUnit") -> None: self._all_expressions: Optional[List["Expression"]] = None self._all_slithir_operations: Optional[List["Operation"]] = None - self._all_internals_calls: Optional[List["InternalCallType"]] = None - self._all_high_level_calls: Optional[List["HighLevelCallType"]] = None - self._all_library_calls: Optional[List["LibraryCallType"]] = None - self._all_low_level_calls: Optional[List["LowLevelCallType"]] = None - self._all_solidity_calls: Optional[List["SolidityFunction"]] = None + self._all_internals_calls: Optional[List["InternalCall"]] = None + self._all_high_level_calls: Optional[List[Tuple["Contract", "HighLevelCall"]]] = None + self._all_library_calls: Optional[List["LibraryCall"]] = None + self._all_low_level_calls: Optional[List["LowLevelCall"]] = None + self._all_solidity_calls: Optional[List["SolidityCall"]] = None self._all_variables_read: Optional[List["Variable"]] = None self._all_variables_written: Optional[List["Variable"]] = None self._all_state_variables_read: Optional[List["StateVariable"]] = None @@ -857,43 +858,42 @@ def slithir_variables(self) -> List["SlithIRVariable"]: ################################################################################### @property - def internal_calls(self) -> List["InternalCallType"]: + def internal_calls(self) -> List["InternalCall"]: """ - list(Function or SolidityFunction): List of function calls (that does not create a transaction) + list(InternalCall): List of IR operations for internal calls """ return list(self._internal_calls) @property - def solidity_calls(self) -> List[SolidityFunction]: + def solidity_calls(self) -> List["SolidityCall"]: """ - list(SolidityFunction): List of Soldity calls + list(SolidityCall): List of IR operations for Solidity calls """ return list(self._solidity_calls) @property - def high_level_calls(self) -> List["HighLevelCallType"]: + def high_level_calls(self) -> List[Tuple["Contract", "HighLevelCall"]]: """ - list((Contract, Function|Variable)): - List of high level calls (external calls). + list(Tuple(Contract, "HighLevelCall")): List of call target contract and IR of the high level call A variable is called in case of call to a public state variable Include library calls """ return list(self._high_level_calls) @property - def library_calls(self) -> List["LibraryCallType"]: + def library_calls(self) -> List["LibraryCall"]: """ - list((Contract, Function)): + list(LibraryCall): List of IR operations for library calls """ return list(self._library_calls) @property - def low_level_calls(self) -> List["LowLevelCallType"]: + def low_level_calls(self) -> List["LowLevelCall"]: """ - list((Variable|SolidityVariable, str)): List of low_level call + list(LowLevelCall): List of IR operations for low level calls A low level call is defined by - the variable called - - the name of the function (call/delegatecall/codecall) + - the name of the function (call/delegatecall/callcode) """ return list(self._low_level_calls) @@ -1121,10 +1121,14 @@ def _explore_functions(self, f_new_values: Callable[["Function"], List]) -> List values = f_new_values(self) explored = [self] to_explore = [ - c for c in self.internal_calls if isinstance(c, Function) and c not in explored + ir.function + for ir in self.internal_calls + if isinstance(ir.function, Function) and ir.function not in explored ] to_explore += [ - c for (_, c) in self.library_calls if isinstance(c, Function) and c not in explored + ir.function + for ir in self.library_calls + if isinstance(ir.function, Function) and ir.function not in explored ] to_explore += [m for m in self.modifiers if m not in explored] @@ -1138,14 +1142,18 @@ def _explore_functions(self, f_new_values: Callable[["Function"], List]) -> List values += f_new_values(f) to_explore += [ - c - for c in f.internal_calls - if isinstance(c, Function) and c not in explored and c not in to_explore + ir.function + for ir in f.internal_calls + if isinstance(ir.function, Function) + and ir.function not in explored + and ir.function not in to_explore ] to_explore += [ - c - for (_, c) in f.library_calls - if isinstance(c, Function) and c not in explored and c not in to_explore + ir.function + for ir in f.library_calls + if isinstance(ir.function, Function) + and ir.function not in explored + and ir.function not in to_explore ] to_explore += [m for m in f.modifiers if m not in explored and m not in to_explore] @@ -1210,31 +1218,31 @@ def all_state_variables_written(self) -> List[StateVariable]: ) return self._all_state_variables_written - def all_internal_calls(self) -> List["InternalCallType"]: + def all_internal_calls(self) -> List["InternalCall"]: """recursive version of internal_calls""" if self._all_internals_calls is None: self._all_internals_calls = self._explore_functions(lambda x: x.internal_calls) return self._all_internals_calls - def all_low_level_calls(self) -> List["LowLevelCallType"]: + def all_low_level_calls(self) -> List["LowLevelCall"]: """recursive version of low_level calls""" if self._all_low_level_calls is None: self._all_low_level_calls = self._explore_functions(lambda x: x.low_level_calls) return self._all_low_level_calls - def all_high_level_calls(self) -> List["HighLevelCallType"]: + def all_high_level_calls(self) -> List[Tuple["Contract", "HighLevelCall"]]: """recursive version of high_level calls""" if self._all_high_level_calls is None: self._all_high_level_calls = self._explore_functions(lambda x: x.high_level_calls) return self._all_high_level_calls - def all_library_calls(self) -> List["LibraryCallType"]: + def all_library_calls(self) -> List["LibraryCall"]: """recursive version of library calls""" if self._all_library_calls is None: self._all_library_calls = self._explore_functions(lambda x: x.library_calls) return self._all_library_calls - def all_solidity_calls(self) -> List[SolidityFunction]: + def all_solidity_calls(self) -> List["SolidityCall"]: """recursive version of solidity calls""" if self._all_solidity_calls is None: self._all_solidity_calls = self._explore_functions(lambda x: x.solidity_calls) diff --git a/slither/detectors/assembly/incorrect_return.py b/slither/detectors/assembly/incorrect_return.py index bd5a6d8449..48ecab1789 100644 --- a/slither/detectors/assembly/incorrect_return.py +++ b/slither/detectors/assembly/incorrect_return.py @@ -73,7 +73,8 @@ def _detect(self) -> List[Output]: for node in f.nodes: if node.sons: - for function_called in node.internal_calls: + for ir in node.internal_calls: + function_called = ir.function if isinstance(function_called, Function): found = _assembly_node(function_called) if found: diff --git a/slither/detectors/attributes/locked_ether.py b/slither/detectors/attributes/locked_ether.py index 91ec686503..efb376e229 100644 --- a/slither/detectors/attributes/locked_ether.py +++ b/slither/detectors/attributes/locked_ether.py @@ -59,7 +59,7 @@ def do_no_send_ether(contract: Contract) -> bool: explored += to_explore to_explore = [] for function in functions: - calls = [c.name for c in function.internal_calls] + calls = [ir.function.name for ir in function.internal_calls] if "suicide(address)" in calls or "selfdestruct(address)" in calls: return False for node in function.nodes: diff --git a/slither/detectors/erc/erc20/arbitrary_send_erc20.py b/slither/detectors/erc/erc20/arbitrary_send_erc20.py index f060054590..046b53ccbf 100644 --- a/slither/detectors/erc/erc20/arbitrary_send_erc20.py +++ b/slither/detectors/erc/erc20/arbitrary_send_erc20.py @@ -31,11 +31,11 @@ def permit_results(self) -> List[Node]: def _detect_arbitrary_from(self, contract: Contract) -> None: for f in contract.functions: all_high_level_calls = [ - f_called[1].solidity_signature - for f_called in f.high_level_calls - if isinstance(f_called[1], Function) + ir.function.solidity_signature + for _, ir in f.high_level_calls + if isinstance(ir.function, Function) ] - all_library_calls = [f_called[1].solidity_signature for f_called in f.library_calls] + all_library_calls = [ir.function.solidity_signature for ir in f.library_calls] if ( "transferFrom(address,address,uint256)" in all_high_level_calls or "safeTransferFrom(address,address,address,uint256)" in all_library_calls diff --git a/slither/detectors/functions/dead_code.py b/slither/detectors/functions/dead_code.py index 5cafa16504..3628d10a2c 100644 --- a/slither/detectors/functions/dead_code.py +++ b/slither/detectors/functions/dead_code.py @@ -48,13 +48,15 @@ def _detect(self) -> List[Output]: all_functionss_called = [ f.all_internal_calls() for f in contract.functions_entry_points ] - all_functions_called = [item for sublist in all_functionss_called for item in sublist] + all_functions_called = [ + item.function for sublist in all_functionss_called for item in sublist + ] functions_used |= { f.canonical_name for f in all_functions_called if isinstance(f, Function) } all_libss_called = [f.all_library_calls() for f in contract.functions_entry_points] all_libs_called: List[Tuple[Contract, Function]] = [ - item for sublist in all_libss_called for item in sublist + item.function for sublist in all_libss_called for item in sublist ] functions_used |= { lib[1].canonical_name for lib in all_libs_called if isinstance(lib, tuple) diff --git a/slither/detectors/functions/modifier.py b/slither/detectors/functions/modifier.py index 7f14872663..a888d5b703 100644 --- a/slither/detectors/functions/modifier.py +++ b/slither/detectors/functions/modifier.py @@ -17,7 +17,7 @@ def is_revert(node: Node) -> bool: return node.type == NodeType.THROW or any( - c.name in ["revert()", "revert(string"] for c in node.internal_calls + ir.function.name in ["revert()", "revert(string"] for ir in node.internal_calls ) diff --git a/slither/detectors/functions/out_of_order_retryable.py b/slither/detectors/functions/out_of_order_retryable.py index db9096f95f..a11e31ef45 100644 --- a/slither/detectors/functions/out_of_order_retryable.py +++ b/slither/detectors/functions/out_of_order_retryable.py @@ -101,9 +101,9 @@ def _detect_multiple_tickets( # include ops from internal function calls internal_ops = [] - for internal_call in node.internal_calls: - if isinstance(internal_call, Function): - internal_ops += internal_call.all_slithir_operations() + for ir in node.internal_calls: + if isinstance(ir.function, Function): + internal_ops += ir.function.all_slithir_operations() # analyze node for retryable tickets for ir in node.irs + internal_ops: diff --git a/slither/detectors/functions/protected_variable.py b/slither/detectors/functions/protected_variable.py index 5796729262..b9260abd61 100644 --- a/slither/detectors/functions/protected_variable.py +++ b/slither/detectors/functions/protected_variable.py @@ -61,7 +61,9 @@ def _analyze_function(self, function: Function, contract: Contract) -> List[Outp if not function_protection: self.logger.error(f"{function_sig} not found") continue - if function_protection not in function.all_internal_calls(): + if function_protection not in [ + ir.function for ir in function.all_internal_calls() + ]: info: DETECTOR_INFO = [ function, " should have ", diff --git a/slither/detectors/functions/suicidal.py b/slither/detectors/functions/suicidal.py index f0af978ec7..7c7d87f8a6 100644 --- a/slither/detectors/functions/suicidal.py +++ b/slither/detectors/functions/suicidal.py @@ -59,7 +59,7 @@ def detect_suicidal_func(func: FunctionContract) -> bool: if func.visibility not in ["public", "external"]: return False - calls = [c.name for c in func.all_internal_calls()] + calls = [ir.function.name for ir in func.all_internal_calls()] if not ("suicide(address)" in calls or "selfdestruct(address)" in calls): return False diff --git a/slither/detectors/reentrancy/reentrancy.py b/slither/detectors/reentrancy/reentrancy.py index 8dd9aecc05..2982801cb7 100644 --- a/slither/detectors/reentrancy/reentrancy.py +++ b/slither/detectors/reentrancy/reentrancy.py @@ -145,15 +145,16 @@ def analyze_node(self, node: Node, detector: "Reentrancy") -> bool: ) slithir_operations = [] # Add the state variables written in internal calls - for internal_call in node.internal_calls: + for ir in node.internal_calls: # Filter to Function, as internal_call can be a solidity call - if isinstance(internal_call, Function): - for internal_node in internal_call.all_nodes(): + function = ir.function + if isinstance(function, Function): + for internal_node in function.all_nodes(): for read in internal_node.state_variables_read: state_vars_read[read].add(internal_node) for write in internal_node.state_variables_written: state_vars_written[write].add(internal_node) - slithir_operations += internal_call.all_slithir_operations() + slithir_operations += function.all_slithir_operations() contains_call = False diff --git a/slither/detectors/statements/assert_state_change.py b/slither/detectors/statements/assert_state_change.py index 769d730b82..8a04f11e0d 100644 --- a/slither/detectors/statements/assert_state_change.py +++ b/slither/detectors/statements/assert_state_change.py @@ -32,7 +32,7 @@ def detect_assert_state_change( for function in contract.functions_declared + list(contract.modifiers_declared): for node in function.nodes: # Detect assert() calls - if any(c.name == "assert(bool)" for c in node.internal_calls) and ( + if any(ir.function.name == "assert(bool)" for ir in node.internal_calls) and ( # Detect direct changes to state node.state_variables_written or diff --git a/slither/detectors/statements/divide_before_multiply.py b/slither/detectors/statements/divide_before_multiply.py index e33477135d..6734fb239d 100644 --- a/slither/detectors/statements/divide_before_multiply.py +++ b/slither/detectors/statements/divide_before_multiply.py @@ -56,7 +56,7 @@ def is_assert(node: Node) -> bool: # Old Solidity code where using an internal 'assert(bool)' function # While we dont check that this function is correct, we assume it is # To avoid too many FP - if "assert(bool)" in [c.full_name for c in node.internal_calls]: + if "assert(bool)" in [ir.function.full_name for ir in node.internal_calls]: return True return False diff --git a/slither/detectors/statements/unprotected_upgradeable.py b/slither/detectors/statements/unprotected_upgradeable.py index d25aff187d..681057749e 100644 --- a/slither/detectors/statements/unprotected_upgradeable.py +++ b/slither/detectors/statements/unprotected_upgradeable.py @@ -35,8 +35,8 @@ def _has_initializing_protection(functions: List[Function]) -> bool: for m in f.modifiers: if m.name == "initializer": return True - for ifc in f.all_internal_calls(): - if ifc.name == "_disableInitializers": + for ir in f.all_internal_calls(): + if ir.function.name == "_disableInitializers": return True # to avoid future FPs in different modifier + function naming implementations, we can also implement a broader check for state var "_initialized" being written to in the constructor diff --git a/slither/printers/call/call_graph.py b/slither/printers/call/call_graph.py index 38225e6d7a..668606760b 100644 --- a/slither/printers/call/call_graph.py +++ b/slither/printers/call/call_graph.py @@ -10,6 +10,7 @@ from slither.core.declarations import Contract, FunctionContract from slither.core.declarations.function import Function +from slither.slithir.operations import HighLevelCall, InternalCall from slither.core.declarations.solidity_variables import SolidityFunction from slither.core.variables.variable import Variable from slither.printers.abstract_printer import AbstractPrinter @@ -49,26 +50,26 @@ def _node(node: str, label: Optional[str] = None) -> str: def _process_internal_call( contract: Contract, function: Function, - internal_call: Union[Function, SolidityFunction], + internal_call: InternalCall, contract_calls: Dict[Contract, Set[str]], solidity_functions: Set[str], solidity_calls: Set[str], ) -> None: - if isinstance(internal_call, (Function)): + if isinstance(internal_call.function, (Function)): contract_calls[contract].add( _edge( _function_node(contract, function), - _function_node(contract, internal_call), + _function_node(contract, internal_call.function), ) ) - elif isinstance(internal_call, (SolidityFunction)): + elif isinstance(internal_call.function, (SolidityFunction)): solidity_functions.add( - _node(_solidity_function_node(internal_call)), + _node(_solidity_function_node(internal_call.function)), ) solidity_calls.add( _edge( _function_node(contract, function), - _solidity_function_node(internal_call), + _solidity_function_node(internal_call.function), ) ) @@ -112,29 +113,29 @@ def _render_solidity_calls(solidity_functions: Set[str], solidity_calls: Set[str def _process_external_call( contract: Contract, function: Function, - external_call: Tuple[Contract, Union[Function, Variable]], + external_call: Tuple[Contract, HighLevelCall], contract_functions: Dict[Contract, Set[str]], external_calls: Set[str], all_contracts: Set[Contract], ) -> None: - external_contract, external_function = external_call + external_contract, ir = external_call if not external_contract in all_contracts: return # add variable as node to respective contract - if isinstance(external_function, (Variable)): + if isinstance(ir.function, (Variable)): contract_functions[external_contract].add( _node( - _function_node(external_contract, external_function), - external_function.name, + _function_node(external_contract, ir.function), + ir.function.name, ) ) external_calls.add( _edge( _function_node(contract, function), - _function_node(external_contract, external_function), + _function_node(external_contract, ir.function), ) ) diff --git a/slither/printers/functions/authorization.py b/slither/printers/functions/authorization.py index 32efeaabeb..288392a468 100644 --- a/slither/printers/functions/authorization.py +++ b/slither/printers/functions/authorization.py @@ -19,7 +19,11 @@ class PrinterWrittenVariablesAndAuthorization(AbstractPrinter): @staticmethod def get_msg_sender_checks(function: Function) -> List[str]: all_functions = ( - [f for f in function.all_internal_calls() if isinstance(f, Function)] + [ + ir.function + for ir in function.all_internal_calls() + if isinstance(ir.function, Function) + ] + [function] + [m for m in function.modifiers if isinstance(m, Function)] ) diff --git a/slither/printers/guidance/echidna.py b/slither/printers/guidance/echidna.py index 0c47fa0f98..ae5fc8be5f 100644 --- a/slither/printers/guidance/echidna.py +++ b/slither/printers/guidance/echidna.py @@ -140,8 +140,8 @@ def _extract_assert(contracts: List[Contract]) -> Dict[str, Dict[str, List[Dict] for contract in contracts: functions_using_assert = [] # Dict[str, List[Dict]] = defaultdict(list) for f in contract.functions_entry_points: - for v in f.all_solidity_calls(): - if v == SolidityFunction("assert(bool)"): + for ir in f.all_solidity_calls(): + if ir.function == SolidityFunction("assert(bool)"): functions_using_assert.append(_get_name(f)) break # Revert https://github.com/crytic/slither/pull/2105 until format is supported by echidna. diff --git a/slither/printers/summary/modifier_calls.py b/slither/printers/summary/modifier_calls.py index cd6c4062e3..225376a3cf 100644 --- a/slither/printers/summary/modifier_calls.py +++ b/slither/printers/summary/modifier_calls.py @@ -29,12 +29,12 @@ def output(self, _filename): table = MyPrettyTable(["Function", "Modifiers"]) for function in contract.functions: modifiers = function.modifiers - for call in function.all_internal_calls(): - if isinstance(call, Function): - modifiers += call.modifiers - for (_, call) in function.all_library_calls(): - if isinstance(call, Function): - modifiers += call.modifiers + for ir in function.all_internal_calls(): + if isinstance(ir.function, Function): + modifiers += ir.function.modifiers + for ir in function.all_library_calls(): + if isinstance(ir.function, Function): + modifiers += ir.function.modifiers table.add_row([function.name, sorted([m.name for m in set(modifiers)])]) txt += "\n" + str(table) self.info(txt) diff --git a/slither/printers/summary/when_not_paused.py b/slither/printers/summary/when_not_paused.py index aaeeeacec2..fc96268ef3 100644 --- a/slither/printers/summary/when_not_paused.py +++ b/slither/printers/summary/when_not_paused.py @@ -11,8 +11,8 @@ def _use_modifier(function: Function, modifier_name: str = "whenNotPaused") -> bool: - for internal_call in function.all_internal_calls(): - if isinstance(internal_call, SolidityFunction): + for ir in function.all_internal_calls(): + if isinstance(ir, function, SolidityFunction): continue if any(modifier.name == modifier_name for modifier in function.modifiers): return True diff --git a/slither/tools/flattening/flattening.py b/slither/tools/flattening/flattening.py index 9cb2abc3f1..182333d3fc 100644 --- a/slither/tools/flattening/flattening.py +++ b/slither/tools/flattening/flattening.py @@ -294,10 +294,9 @@ def _export_list_used_contracts( # pylint: disable=too-many-branches self._export_list_used_contracts(inherited, exported, list_contract, list_top_level) # Find all the external contracts called - externals = contract.all_library_calls + contract.all_high_level_calls - # externals is a list of (contract, function) + # High level calls already includes library calls # We also filter call to itself to avoid infilite loop - externals = list({e[0] for e in externals if e[0] != contract}) + externals = list({e[0] for e in contract.all_high_level_calls if e[0] != contract}) for inherited in externals: self._export_list_used_contracts(inherited, exported, list_contract, list_top_level) diff --git a/slither/tools/possible_paths/possible_paths.py b/slither/tools/possible_paths/possible_paths.py index 6e836e76ab..15218a8720 100644 --- a/slither/tools/possible_paths/possible_paths.py +++ b/slither/tools/possible_paths/possible_paths.py @@ -123,10 +123,14 @@ def __find_target_paths( # Find all function calls in this function (except for low level) called_functions_list = [ - f for (_, f) in function.high_level_calls if isinstance(f, Function) + ir.function + for _, ir in function.high_level_calls + if isinstance(ir.function, Function) + ] + called_functions_list += [ir.function for ir in function.library_calls] + called_functions_list += [ + ir.function for ir in function.internal_calls if isinstance(ir.function, Function) ] - called_functions_list += [f for (_, f) in function.library_calls] - called_functions_list += [f for f in function.internal_calls if isinstance(f, Function)] called_functions = set(called_functions_list) # If any of our target functions are reachable from this function, it's a result. diff --git a/slither/utils/upgradeability.py b/slither/utils/upgradeability.py index 22461dbcf6..59979bca64 100644 --- a/slither/utils/upgradeability.py +++ b/slither/utils/upgradeability.py @@ -123,7 +123,9 @@ def compare( ): continue modified_calls = [ - func for func in new_modified_functions if func in function.internal_calls + func + for func in new_modified_functions + if func in [ir.function for ir in function.internal_calls] ] tainted_vars = [ var @@ -179,7 +181,8 @@ def tainted_external_contracts(funcs: List[Function]) -> List[TaintedExternalCon tainted_list: list[TaintedExternalContract] = [] for func in funcs: - for contract, target in func.all_high_level_calls(): + for contract, ir in func.all_high_level_calls(): + target = ir.function if contract.is_library: # Not interested in library calls continue @@ -254,7 +257,11 @@ def tainted_inheriting_contracts( new_taint = TaintedExternalContract(c) for f in c.functions_declared: # Search for functions that call an inherited tainted function or access an inherited tainted variable - internal_calls = [c for c in f.all_internal_calls() if isinstance(c, Function)] + internal_calls = [ + ir.function + for ir in f.all_internal_calls() + if isinstance(ir.function, Function) + ] if any( call.canonical_name == t.canonical_name for t in tainted.tainted_functions From a60bd63af058013ef51bd9f33855e6c43b25aec1 Mon Sep 17 00:00:00 2001 From: Simone Date: Fri, 13 Sep 2024 14:41:41 +0200 Subject: [PATCH 19/71] Fix test --- tests/unit/slithir/vyper/test_ir_generation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/slithir/vyper/test_ir_generation.py b/tests/unit/slithir/vyper/test_ir_generation.py index 73c9b5e70b..efcf5ce549 100644 --- a/tests/unit/slithir/vyper/test_ir_generation.py +++ b/tests/unit/slithir/vyper/test_ir_generation.py @@ -35,9 +35,9 @@ def bar(): interface = next(iter(x for x in sl.contracts if x.is_interface)) contract = next(iter(x for x in sl.contracts if not x.is_interface)) func = contract.get_function_from_signature("bar()") - (contract, function) = func.high_level_calls[0] + (contract, ir) = func.high_level_calls[0] assert contract == interface - assert function.signature_str == "foo() returns(int128,uint256)" + assert ir.function.signature_str == "foo() returns(int128,uint256)" def test_phi_entry_point_internal_call(slither_from_vyper_source): From 7b408b26e756ce4a9ee0963240d78ca7462e3508 Mon Sep 17 00:00:00 2001 From: Simone Date: Fri, 13 Sep 2024 15:01:29 +0200 Subject: [PATCH 20/71] Lint --- slither/printers/guidance/echidna.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/slither/printers/guidance/echidna.py b/slither/printers/guidance/echidna.py index ae5fc8be5f..2eb0b76410 100644 --- a/slither/printers/guidance/echidna.py +++ b/slither/printers/guidance/echidna.py @@ -162,9 +162,7 @@ def json_serializable(cls): my_super = super def as_dict(self): - yield { - name: value for name, value in zip(self._fields, iter(my_super(cls, self).__iter__())) - } + yield {name: value for name, value in zip(self._fields, iter(my_super(cls, self).iter()))} cls.__iter__ = as_dict return cls From cee74517e5d58197fb671468e0590bfcccb71f45 Mon Sep 17 00:00:00 2001 From: Simone Date: Fri, 13 Sep 2024 15:05:24 +0200 Subject: [PATCH 21/71] Revert lint --- slither/printers/guidance/echidna.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/slither/printers/guidance/echidna.py b/slither/printers/guidance/echidna.py index 2eb0b76410..35a6091935 100644 --- a/slither/printers/guidance/echidna.py +++ b/slither/printers/guidance/echidna.py @@ -156,13 +156,15 @@ def _extract_assert(contracts: List[Contract]) -> Dict[str, Dict[str, List[Dict] # Create a named tuple that is serialization in json def json_serializable(cls): - # pylint: disable=unnecessary-comprehension + # pylint: disable=unnecessary-comprehension,unnecessary-dunder-call # TODO: the next line is a quick workaround to prevent pylint from crashing # It can be removed once https://github.com/PyCQA/pylint/pull/3810 is merged my_super = super def as_dict(self): - yield {name: value for name, value in zip(self._fields, iter(my_super(cls, self).iter()))} + yield { + name: value for name, value in zip(self._fields, iter(my_super(cls, self).__iter__())) + } cls.__iter__ = as_dict return cls From 13b25e8756a4bd42a84dde1a66f892cd7012a508 Mon Sep 17 00:00:00 2001 From: Simone Date: Fri, 13 Sep 2024 16:47:36 +0200 Subject: [PATCH 22/71] Fix solidity calls analysis --- slither/core/declarations/function.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/slither/core/declarations/function.py b/slither/core/declarations/function.py index 0b44a9e255..b91e58f24c 100644 --- a/slither/core/declarations/function.py +++ b/slither/core/declarations/function.py @@ -1661,7 +1661,9 @@ def _analyze_calls(self) -> None: internal_calls = [item for sublist in internal_calls for item in sublist] self._internal_calls = list(set(internal_calls)) - self._solidity_calls = [c for c in internal_calls if isinstance(c, SolidityFunction)] + self._solidity_calls = [ + ir for ir in internal_calls if isinstance(ir.function, SolidityFunction) + ] low_level_calls = [x.low_level_calls for x in self.nodes] low_level_calls = [x for x in low_level_calls if x] From 439010aefae7570c69049e093b631e945dcaaf39 Mon Sep 17 00:00:00 2001 From: Simone Date: Fri, 13 Sep 2024 18:44:27 +0200 Subject: [PATCH 23/71] Use new *Calls API in detectors --- .../detectors/assembly/incorrect_return.py | 43 +++++----- .../assembly/return_instead_of_leave.py | 14 ++-- .../compiler_bugs/array_by_reference.py | 41 ++++------ .../erc/erc20/arbitrary_send_erc20.py | 79 +++++++++---------- .../detectors/functions/external_function.py | 13 ++- slither/detectors/operations/encode_packed.py | 37 ++++----- .../detectors/operations/low_level_calls.py | 7 +- .../statements/assert_state_change.py | 10 +-- .../statements/controlled_delegatecall.py | 16 ++-- slither/detectors/statements/return_bomb.py | 55 ++++++------- .../statements/unprotected_upgradeable.py | 23 +++--- .../variables/var_read_using_this.py | 17 ++-- 12 files changed, 163 insertions(+), 192 deletions(-) diff --git a/slither/detectors/assembly/incorrect_return.py b/slither/detectors/assembly/incorrect_return.py index 48ecab1789..9052979ace 100644 --- a/slither/detectors/assembly/incorrect_return.py +++ b/slither/detectors/assembly/incorrect_return.py @@ -21,10 +21,8 @@ def _assembly_node(function: Function) -> Optional[SolidityCall]: """ - for ir in function.all_slithir_operations(): - if isinstance(ir, SolidityCall) and ir.function == SolidityFunction( - "return(uint256,uint256)" - ): + for ir in function.all_solidity_calls(): + if ir.function == SolidityFunction("return(uint256,uint256)"): return ir return None @@ -71,24 +69,23 @@ def _detect(self) -> List[Output]: for c in self.contracts: for f in c.functions_and_modifiers_declared: - for node in f.nodes: - if node.sons: - for ir in node.internal_calls: - function_called = ir.function - if isinstance(function_called, Function): - found = _assembly_node(function_called) - if found: - - info: DETECTOR_INFO = [ - f, - " calls ", - function_called, - " which halt the execution ", - found.node, - "\n", - ] - json = self.generate_result(info) - - results.append(json) + for ir in f.internal_calls: + if ir.node.sons: + function_called = ir.function + if isinstance(function_called, Function): + found = _assembly_node(function_called) + if found: + + info: DETECTOR_INFO = [ + f, + " calls ", + function_called, + " which halt the execution ", + found.node, + "\n", + ] + json = self.generate_result(info) + + results.append(json) return results diff --git a/slither/detectors/assembly/return_instead_of_leave.py b/slither/detectors/assembly/return_instead_of_leave.py index a1ad9c87e9..6037059744 100644 --- a/slither/detectors/assembly/return_instead_of_leave.py +++ b/slither/detectors/assembly/return_instead_of_leave.py @@ -6,7 +6,6 @@ DetectorClassification, DETECTOR_INFO, ) -from slither.slithir.operations import SolidityCall from slither.utils.output import Output @@ -42,15 +41,12 @@ class ReturnInsteadOfLeave(AbstractDetector): def _check_function(self, f: Function) -> List[Output]: results: List[Output] = [] - for node in f.nodes: - for ir in node.irs: - if isinstance(ir, SolidityCall) and ir.function == SolidityFunction( - "return(uint256,uint256)" - ): - info: DETECTOR_INFO = [f, " contains an incorrect call to return: ", node, "\n"] - json = self.generate_result(info) + for ir in f.solidity_calls: + if ir.function == SolidityFunction("return(uint256,uint256)"): + info: DETECTOR_INFO = [f, " contains an incorrect call to return: ", ir.node, "\n"] + json = self.generate_result(info) - results.append(json) + results.append(json) return results def _detect(self) -> List[Output]: diff --git a/slither/detectors/compiler_bugs/array_by_reference.py b/slither/detectors/compiler_bugs/array_by_reference.py index 47e2af5819..e4dde43608 100644 --- a/slither/detectors/compiler_bugs/array_by_reference.py +++ b/slither/detectors/compiler_bugs/array_by_reference.py @@ -13,8 +13,6 @@ from slither.core.solidity_types.array_type import ArrayType from slither.core.variables.state_variable import StateVariable from slither.core.variables.local_variable import LocalVariable -from slither.slithir.operations.high_level_call import HighLevelCall -from slither.slithir.operations.internal_call import InternalCall from slither.core.cfg.node import Node from slither.core.declarations.contract import Contract from slither.core.declarations.function_contract import FunctionContract @@ -117,37 +115,26 @@ def detect_calls_passing_ref_to_function( # pylint: disable=too-many-nested-blocks for contract in contracts: for function in contract.functions_and_modifiers_declared: - for node in function.nodes: + for ir in [ir for _, ir in function.high_level_calls] + function.internal_calls: - # If this node has no expression, skip it. - if not node.expression: + # Verify this references a function in our array modifying functions collection. + if ir.function not in array_modifying_funcs: continue - for ir in node.irs: - # Verify this is a high level call. - if not isinstance(ir, (HighLevelCall, InternalCall)): + # Verify one of these parameters is an array in storage. + for (param, arg) in zip(ir.function.parameters, ir.arguments): + # Verify this argument is a variable that is an array type. + if not isinstance(arg, (StateVariable, LocalVariable)): continue - - # Verify this references a function in our array modifying functions collection. - if ir.function not in array_modifying_funcs: + if not isinstance(arg.type, ArrayType): continue - # Verify one of these parameters is an array in storage. - for (param, arg) in zip(ir.function.parameters, ir.arguments): - # Verify this argument is a variable that is an array type. - if not isinstance(arg, (StateVariable, LocalVariable)): - continue - if not isinstance(arg.type, ArrayType): - continue - - # If it is a state variable OR a local variable referencing storage, we add it to the list. - if ( - isinstance(arg, StateVariable) - or (isinstance(arg, LocalVariable) and arg.location == "storage") - ) and ( - isinstance(param.type, ArrayType) and param.location != "storage" - ): - results.append((node, arg, ir.function)) + # If it is a state variable OR a local variable referencing storage, we add it to the list. + if ( + isinstance(arg, StateVariable) + or (isinstance(arg, LocalVariable) and arg.location == "storage") + ) and (isinstance(param.type, ArrayType) and param.location != "storage"): + results.append((ir.node, arg, ir.function)) return results def _detect(self) -> List[Output]: diff --git a/slither/detectors/erc/erc20/arbitrary_send_erc20.py b/slither/detectors/erc/erc20/arbitrary_send_erc20.py index 046b53ccbf..4dc1f8db50 100644 --- a/slither/detectors/erc/erc20/arbitrary_send_erc20.py +++ b/slither/detectors/erc/erc20/arbitrary_send_erc20.py @@ -3,7 +3,7 @@ from slither.analyses.data_dependency.data_dependency import is_dependent from slither.core.cfg.node import Node from slither.core.compilation_unit import SlitherCompilationUnit -from slither.core.declarations import Contract, Function, SolidityVariableComposed +from slither.core.declarations import Contract, Function, SolidityVariableComposed, FunctionContract from slither.core.declarations.solidity_variables import SolidityVariable from slither.slithir.operations import HighLevelCall, LibraryCall @@ -44,51 +44,50 @@ def _detect_arbitrary_from(self, contract: Contract) -> None: "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)" in all_high_level_calls ): - ArbitrarySendErc20._arbitrary_from(f.nodes, self._permit_results) + ArbitrarySendErc20._arbitrary_from(f, self._permit_results) else: - ArbitrarySendErc20._arbitrary_from(f.nodes, self._no_permit_results) + ArbitrarySendErc20._arbitrary_from(f, self._no_permit_results) @staticmethod - def _arbitrary_from(nodes: List[Node], results: List[Node]) -> None: + def _arbitrary_from(function: FunctionContract, results: List[Node]) -> None: """Finds instances of (safe)transferFrom that do not use msg.sender or address(this) as from parameter.""" - for node in nodes: - for ir in node.irs: - if ( - isinstance(ir, HighLevelCall) - and isinstance(ir.function, Function) - and ir.function.solidity_signature == "transferFrom(address,address,uint256)" - and not ( - is_dependent( - ir.arguments[0], - SolidityVariableComposed("msg.sender"), - node, - ) - or is_dependent( - ir.arguments[0], - SolidityVariable("this"), - node, - ) + for _, ir in function.high_level_calls: + if ( + isinstance(ir, LibraryCall) + and ir.function.solidity_signature + == "safeTransferFrom(address,address,address,uint256)" + and not ( + is_dependent( + ir.arguments[1], + SolidityVariableComposed("msg.sender"), + ir.node, ) - ): - results.append(ir.node) - elif ( - isinstance(ir, LibraryCall) - and ir.function.solidity_signature - == "safeTransferFrom(address,address,address,uint256)" - and not ( - is_dependent( - ir.arguments[1], - SolidityVariableComposed("msg.sender"), - node, - ) - or is_dependent( - ir.arguments[1], - SolidityVariable("this"), - node, - ) + or is_dependent( + ir.arguments[1], + SolidityVariable("this"), + ir.node, ) - ): - results.append(ir.node) + ) + ): + results.append(ir.node) + elif ( + isinstance(ir, HighLevelCall) + and isinstance(ir.function, Function) + and ir.function.solidity_signature == "transferFrom(address,address,uint256)" + and not ( + is_dependent( + ir.arguments[0], + SolidityVariableComposed("msg.sender"), + ir.node, + ) + or is_dependent( + ir.arguments[0], + SolidityVariable("this"), + ir.node, + ) + ) + ): + results.append(ir.node) def detect(self) -> None: """Detect transfers that use arbitrary `from` parameter.""" diff --git a/slither/detectors/functions/external_function.py b/slither/detectors/functions/external_function.py index 5858c2baf2..c59fe44920 100644 --- a/slither/detectors/functions/external_function.py +++ b/slither/detectors/functions/external_function.py @@ -13,8 +13,7 @@ make_solc_versions, ) from slither.formatters.functions.external_function import custom_format -from slither.slithir.operations import InternalCall, InternalDynamicCall -from slither.slithir.operations import SolidityCall +from slither.slithir.operations import InternalDynamicCall from slither.utils.output import Output @@ -55,11 +54,11 @@ def detect_functions_called(contract: Contract) -> List[Function]: for func in contract.all_functions_called: if not isinstance(func, Function): continue - # Loop through all nodes in the function, add all calls to a list. - for node in func.nodes: - for ir in node.irs: - if isinstance(ir, (InternalCall, SolidityCall)): - result.append(ir.function) + + # Loop through all internal and solidity calls in the function, add them to a list. + for ir in func.internal_calls + func.solidity_calls: + result.append(ir.function) + return result @staticmethod diff --git a/slither/detectors/operations/encode_packed.py b/slither/detectors/operations/encode_packed.py index ea7b094df2..b661ddcd72 100644 --- a/slither/detectors/operations/encode_packed.py +++ b/slither/detectors/operations/encode_packed.py @@ -3,14 +3,14 @@ """ from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification -from slither.core.declarations.solidity_variables import SolidityFunction -from slither.slithir.operations import SolidityCall +from slither.core.declarations import Contract, SolidityFunction +from slither.core.variables import Variable from slither.analyses.data_dependency.data_dependency import is_tainted from slither.core.solidity_types import ElementaryType from slither.core.solidity_types import ArrayType -def _is_dynamic_type(arg): +def _is_dynamic_type(arg: Variable): """ Args: arg (function argument) @@ -25,7 +25,7 @@ def _is_dynamic_type(arg): return False -def _detect_abi_encodePacked_collision(contract): +def _detect_abi_encodePacked_collision(contract: Contract): """ Args: contract (Contract) @@ -35,22 +35,19 @@ def _detect_abi_encodePacked_collision(contract): ret = [] # pylint: disable=too-many-nested-blocks for f in contract.functions_and_modifiers_declared: - for n in f.nodes: - for ir in n.irs: - if isinstance(ir, SolidityCall) and ir.function == SolidityFunction( - "abi.encodePacked()" - ): - dynamic_type_count = 0 - for arg in ir.arguments: - if is_tainted(arg, contract) and _is_dynamic_type(arg): - dynamic_type_count += 1 - elif dynamic_type_count > 1: - ret.append((f, n)) - dynamic_type_count = 0 - else: - dynamic_type_count = 0 - if dynamic_type_count > 1: - ret.append((f, n)) + for ir in f.solidity_calls: + if ir.function == SolidityFunction("abi.encodePacked()"): + dynamic_type_count = 0 + for arg in ir.arguments: + if is_tainted(arg, contract) and _is_dynamic_type(arg): + dynamic_type_count += 1 + elif dynamic_type_count > 1: + ret.append((f, ir.node)) + dynamic_type_count = 0 + else: + dynamic_type_count = 0 + if dynamic_type_count > 1: + ret.append((f, ir.node)) return ret diff --git a/slither/detectors/operations/low_level_calls.py b/slither/detectors/operations/low_level_calls.py index 463c748757..4925fc4661 100644 --- a/slither/detectors/operations/low_level_calls.py +++ b/slither/detectors/operations/low_level_calls.py @@ -44,10 +44,9 @@ def detect_low_level_calls( ) -> List[Tuple[FunctionContract, List[Node]]]: ret = [] for f in [f for f in contract.functions if contract == f.contract_declarer]: - nodes = f.nodes - assembly_nodes = [n for n in nodes if self._contains_low_level_calls(n)] - if assembly_nodes: - ret.append((f, assembly_nodes)) + low_level_nodes = [ir.node for ir in f.low_level_calls] + if low_level_nodes: + ret.append((f, low_level_nodes)) return ret def _detect(self) -> List[Output]: diff --git a/slither/detectors/statements/assert_state_change.py b/slither/detectors/statements/assert_state_change.py index 8a04f11e0d..d70495365f 100644 --- a/slither/detectors/statements/assert_state_change.py +++ b/slither/detectors/statements/assert_state_change.py @@ -30,22 +30,22 @@ def detect_assert_state_change( # Loop for each function and modifier. for function in contract.functions_declared + list(contract.modifiers_declared): - for node in function.nodes: + for ir_call in function.internal_calls: # Detect assert() calls - if any(ir.function.name == "assert(bool)" for ir in node.internal_calls) and ( + if ir_call.function.name == "assert(bool)" and ( # Detect direct changes to state - node.state_variables_written + ir_call.node.state_variables_written or # Detect changes to state via function calls any( ir - for ir in node.irs + for ir in ir_call.node.irs if isinstance(ir, InternalCall) and ir.function and ir.function.state_variables_written ) ): - results.append((function, node)) + results.append((function, ir_call.node)) # Return the resulting set of nodes return results diff --git a/slither/detectors/statements/controlled_delegatecall.py b/slither/detectors/statements/controlled_delegatecall.py index 32e59d6eb7..bf78b3bf98 100644 --- a/slither/detectors/statements/controlled_delegatecall.py +++ b/slither/detectors/statements/controlled_delegatecall.py @@ -8,20 +8,18 @@ DetectorClassification, DETECTOR_INFO, ) -from slither.slithir.operations import LowLevelCall from slither.utils.output import Output def controlled_delegatecall(function: FunctionContract) -> List[Node]: ret = [] - for node in function.nodes: - for ir in node.irs: - if isinstance(ir, LowLevelCall) and ir.function_name in [ - "delegatecall", - "callcode", - ]: - if is_tainted(ir.destination, function.contract): - ret.append(node) + for ir in function.low_level_calls: + if ir.function_name in [ + "delegatecall", + "callcode", + ]: + if is_tainted(ir.destination, function.contract): + ret.append(ir.node) return ret diff --git a/slither/detectors/statements/return_bomb.py b/slither/detectors/statements/return_bomb.py index 8b6cd07a29..6d7052cf40 100644 --- a/slither/detectors/statements/return_bomb.py +++ b/slither/detectors/statements/return_bomb.py @@ -9,7 +9,7 @@ DetectorClassification, DETECTOR_INFO, ) -from slither.slithir.operations import LowLevelCall, HighLevelCall +from slither.slithir.operations import HighLevelCall from slither.analyses.data_dependency.data_dependency import is_tainted from slither.utils.output import Output @@ -71,34 +71,31 @@ def is_dynamic_type(ty: Type) -> bool: def get_nodes_for_function(self, function: Function, contract: Contract) -> List[Node]: nodes = [] - for node in function.nodes: - for ir in node.irs: - if isinstance(ir, (HighLevelCall, LowLevelCall)): - if not is_tainted(ir.destination, contract): # type:ignore - # Only interested if the target address is controlled/tainted - continue - - if isinstance(ir, HighLevelCall) and isinstance(ir.function, Function): - # in normal highlevel calls return bombs are _possible_ - # if the return type is dynamic and the caller tries to copy and decode large data - has_dyn = False - if ir.function.return_type: - has_dyn = any( - self.is_dynamic_type(ty) for ty in ir.function.return_type - ) - - if not has_dyn: - continue - - # If a gas budget was specified then the - # user may not know about the return bomb - if ir.call_gas is None: - # if a gas budget was NOT specified then the caller - # may already suspect the call may spend all gas? - continue - - nodes.append(node) - # TODO: check that there is some state change after the call + + for ir in [ir for _, ir in function.high_level_calls] + function.low_level_calls: + if not is_tainted(ir.destination, contract): # type:ignore + # Only interested if the target address is controlled/tainted + continue + + if isinstance(ir, HighLevelCall) and isinstance(ir.function, Function): + # in normal highlevel calls return bombs are _possible_ + # if the return type is dynamic and the caller tries to copy and decode large data + has_dyn = False + if ir.function.return_type: + has_dyn = any(self.is_dynamic_type(ty) for ty in ir.function.return_type) + + if not has_dyn: + continue + + # If a gas budget was specified then the + # user may not know about the return bomb + if ir.call_gas is None: + # if a gas budget was NOT specified then the caller + # may already suspect the call may spend all gas? + continue + + nodes.append(ir.node) + # TODO: check that there is some state change after the call return nodes diff --git a/slither/detectors/statements/unprotected_upgradeable.py b/slither/detectors/statements/unprotected_upgradeable.py index 681057749e..aeb785da36 100644 --- a/slither/detectors/statements/unprotected_upgradeable.py +++ b/slither/detectors/statements/unprotected_upgradeable.py @@ -7,23 +7,28 @@ DetectorClassification, DETECTOR_INFO, ) -from slither.slithir.operations import LowLevelCall, SolidityCall from slither.utils.output import Output def _can_be_destroyed(contract: Contract) -> List[Function]: targets = [] for f in contract.functions_entry_points: - for ir in f.all_slithir_operations(): - if ( - isinstance(ir, LowLevelCall) and ir.function_name in ["delegatecall", "codecall"] - ) or ( - isinstance(ir, SolidityCall) - and ir.function - in [SolidityFunction("suicide(address)"), SolidityFunction("selfdestruct(address)")] - ): + found = False + for ir in f.all_low_level_calls(): + if ir.function_name in ["delegatecall", "codecall"]: targets.append(f) + found = True break + + if not found: + for ir in f.all_solidity_calls(): + if ir.function in [ + SolidityFunction("suicide(address)"), + SolidityFunction("selfdestruct(address)"), + ]: + targets.append(f) + break + return targets diff --git a/slither/detectors/variables/var_read_using_this.py b/slither/detectors/variables/var_read_using_this.py index 537eecf8a3..1e4787e363 100644 --- a/slither/detectors/variables/var_read_using_this.py +++ b/slither/detectors/variables/var_read_using_this.py @@ -7,7 +7,6 @@ DetectorClassification, DETECTOR_INFO, ) -from slither.slithir.operations.high_level_call import HighLevelCall from slither.utils.output import Output @@ -54,13 +53,11 @@ def _detect(self) -> List[Output]: @staticmethod def _detect_var_read_using_this(func: Function) -> List[Node]: results: List[Node] = [] - for node in func.nodes: - for ir in node.irs: - if isinstance(ir, HighLevelCall): - if ( - ir.destination == SolidityVariable("this") - and ir.is_static_call() - and ir.function.visibility == "public" - ): - results.append(node) + for _, ir in func.high_level_calls: + if ( + ir.destination == SolidityVariable("this") + and ir.is_static_call() + and ir.function.visibility == "public" + ): + results.append(ir.node) return sorted(results, key=lambda x: x.node_id) From 2c5162966aef7d1daaae52cd57e21ac16a0d44c2 Mon Sep 17 00:00:00 2001 From: Simone Date: Fri, 13 Sep 2024 18:51:25 +0200 Subject: [PATCH 24/71] Lint --- slither/detectors/functions/external_function.py | 1 + 1 file changed, 1 insertion(+) diff --git a/slither/detectors/functions/external_function.py b/slither/detectors/functions/external_function.py index c59fe44920..d9cc2bc361 100644 --- a/slither/detectors/functions/external_function.py +++ b/slither/detectors/functions/external_function.py @@ -100,6 +100,7 @@ def get_base_most_function(function: FunctionContract) -> FunctionContract: # Somehow we couldn't resolve it, which shouldn't happen, as the provided function should be found if we could # not find some any more basic. + # pylint: disable=broad-exception-raised raise Exception("Could not resolve the base-most function for the provided function.") @staticmethod From bf5d6d64459297a2f1104fa559b6095f271acba7 Mon Sep 17 00:00:00 2001 From: Simone Date: Wed, 18 Sep 2024 15:18:25 +0200 Subject: [PATCH 25/71] Add assert information for echidna --- slither/printers/guidance/echidna.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/slither/printers/guidance/echidna.py b/slither/printers/guidance/echidna.py index 0c47fa0f98..9b97d0b737 100644 --- a/slither/printers/guidance/echidna.py +++ b/slither/printers/guidance/echidna.py @@ -138,17 +138,13 @@ def _extract_assert(contracts: List[Contract]) -> Dict[str, Dict[str, List[Dict] """ ret: Dict[str, Dict[str, List[Dict]]] = {} for contract in contracts: - functions_using_assert = [] # Dict[str, List[Dict]] = defaultdict(list) + functions_using_assert: Dict[str, List[Dict]] = defaultdict(list) for f in contract.functions_entry_points: - for v in f.all_solidity_calls(): - if v == SolidityFunction("assert(bool)"): - functions_using_assert.append(_get_name(f)) + for node in f.all_node(): + if SolidityFunction("assert(bool)") in node.solidity_calls and node.source_mapping: + func_name = _get_name(f) + functions_using_assert[func_name].append(node.source_mapping.to_json()) break - # Revert https://github.com/crytic/slither/pull/2105 until format is supported by echidna. - # for node in f.all_nodes(): - # if SolidityFunction("assert(bool)") in node.solidity_calls and node.source_mapping: - # func_name = _get_name(f) - # functions_using_assert[func_name].append(node.source_mapping.to_json()) if functions_using_assert: ret[contract.name] = functions_using_assert return ret From 4de824bac7af0236a3bf82e09bf6e9b9bb3d4e7b Mon Sep 17 00:00:00 2001 From: Simone Date: Wed, 18 Sep 2024 15:22:47 +0200 Subject: [PATCH 26/71] Fix function name --- slither/printers/guidance/echidna.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slither/printers/guidance/echidna.py b/slither/printers/guidance/echidna.py index 9b97d0b737..4b2526b1be 100644 --- a/slither/printers/guidance/echidna.py +++ b/slither/printers/guidance/echidna.py @@ -140,7 +140,7 @@ def _extract_assert(contracts: List[Contract]) -> Dict[str, Dict[str, List[Dict] for contract in contracts: functions_using_assert: Dict[str, List[Dict]] = defaultdict(list) for f in contract.functions_entry_points: - for node in f.all_node(): + for node in f.all_nodes(): if SolidityFunction("assert(bool)") in node.solidity_calls and node.source_mapping: func_name = _get_name(f) functions_using_assert[func_name].append(node.source_mapping.to_json()) From fdf1db3382e4d3b52d7d7e3122a0a84058d9a92c Mon Sep 17 00:00:00 2001 From: Simone Date: Wed, 18 Sep 2024 15:27:31 +0200 Subject: [PATCH 27/71] Lint --- slither/printers/guidance/echidna.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slither/printers/guidance/echidna.py b/slither/printers/guidance/echidna.py index 4b2526b1be..c3fb5561ff 100644 --- a/slither/printers/guidance/echidna.py +++ b/slither/printers/guidance/echidna.py @@ -152,7 +152,7 @@ def _extract_assert(contracts: List[Contract]) -> Dict[str, Dict[str, List[Dict] # Create a named tuple that is serialization in json def json_serializable(cls): - # pylint: disable=unnecessary-comprehension + # pylint: disable=unnecessary-comprehension,unnecessary-dunder-call # TODO: the next line is a quick workaround to prevent pylint from crashing # It can be removed once https://github.com/PyCQA/pylint/pull/3810 is merged my_super = super From 584d64c73fff7e3d7f08d952725ed4645bcc8897 Mon Sep 17 00:00:00 2001 From: Hamdi Allam Date: Wed, 25 Sep 2024 14:43:01 -0700 Subject: [PATCH 28/71] add event as value member --- slither/slithir/operations/member.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/slither/slithir/operations/member.py b/slither/slithir/operations/member.py index 0942813cfc..193f6f177e 100644 --- a/slither/slithir/operations/member.py +++ b/slither/slithir/operations/member.py @@ -1,5 +1,5 @@ from typing import List, Union -from slither.core.declarations import Contract, Function +from slither.core.declarations import Contract, Function, Event from slither.core.declarations.custom_error import CustomError from slither.core.declarations.enum import Enum from slither.core.declarations.solidity_import_placeholder import SolidityImportPlaceHolder @@ -33,14 +33,14 @@ def __init__( # Can be an ElementaryType because of bytes.concat, string.concat assert is_valid_rvalue(variable_left) or isinstance( variable_left, - (Contract, Enum, Function, CustomError, SolidityImportPlaceHolder, ElementaryType), + (Contract, Enum, Function, Event, CustomError, SolidityImportPlaceHolder, ElementaryType), ) assert isinstance(variable_right, Constant) assert isinstance(result, ReferenceVariable) super().__init__() self._variable_left: Union[ - RVALUE, Contract, Enum, Function, CustomError, SolidityImportPlaceHolder, ElementaryType + RVALUE, Contract, Enum, Function, Event, CustomError, SolidityImportPlaceHolder, ElementaryType ] = variable_left self._variable_right = variable_right self._lvalue = result From 0cfaa681a2255da6834a492468d725c8103c0fa1 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Thu, 26 Sep 2024 17:58:45 -0500 Subject: [PATCH 29/71] fix: mapping to type value lookup with top-level constant --- slither/solc_parsing/solidity_types/type_parsing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slither/solc_parsing/solidity_types/type_parsing.py b/slither/solc_parsing/solidity_types/type_parsing.py index 6ca015127e..cb0b1125db 100644 --- a/slither/solc_parsing/solidity_types/type_parsing.py +++ b/slither/solc_parsing/solidity_types/type_parsing.py @@ -293,7 +293,7 @@ def parse_type( scope = custom_error.contract.file_scope sl = caller_context.compilation_unit - next_context = caller_context.slither_parser + next_context = caller_context structures_direct_access = list(scope.structures.values()) all_structuress = [c.structures for c in scope.contracts.values()] all_structures = [item for sublist in all_structuress for item in sublist] From 2075e6f1e9f7f5ba14364db256b2e6a3517629ec Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Thu, 26 Sep 2024 18:02:35 -0500 Subject: [PATCH 30/71] fmt --- slither/slithir/operations/member.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/slither/slithir/operations/member.py b/slither/slithir/operations/member.py index 193f6f177e..55979572c5 100644 --- a/slither/slithir/operations/member.py +++ b/slither/slithir/operations/member.py @@ -33,14 +33,29 @@ def __init__( # Can be an ElementaryType because of bytes.concat, string.concat assert is_valid_rvalue(variable_left) or isinstance( variable_left, - (Contract, Enum, Function, Event, CustomError, SolidityImportPlaceHolder, ElementaryType), + ( + Contract, + Enum, + Function, + Event, + CustomError, + SolidityImportPlaceHolder, + ElementaryType, + ), ) assert isinstance(variable_right, Constant) assert isinstance(result, ReferenceVariable) super().__init__() self._variable_left: Union[ - RVALUE, Contract, Enum, Function, Event, CustomError, SolidityImportPlaceHolder, ElementaryType + RVALUE, + Contract, + Enum, + Function, + Event, + CustomError, + SolidityImportPlaceHolder, + ElementaryType, ] = variable_left self._variable_right = variable_right self._lvalue = result From ad9e8dda1af8fcadc2e7ff48b651c57f71da2e54 Mon Sep 17 00:00:00 2001 From: Josselin Feist Date: Wed, 2 Oct 2024 10:47:57 +0200 Subject: [PATCH 31/71] Update test_printers.py --- tests/e2e/printers/test_printers.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/e2e/printers/test_printers.py b/tests/e2e/printers/test_printers.py index 0c00a657f8..3ca536b0ac 100644 --- a/tests/e2e/printers/test_printers.py +++ b/tests/e2e/printers/test_printers.py @@ -41,6 +41,17 @@ def test_inheritance_printer(solc_binary_path) -> None: assert counter["B -> A"] == 2 assert counter["C -> A"] == 1 + # Let also test the include/exclude interface behavior + # Check that the interface is not included + assert "MyInterfaceX" not in content + + slither.include_interfaces = True + output = printer.output("test_printer.dot") + content = output.elements[0]["name"]["content"] + assert "MyInterfaceX" in content + + # Remove test generated files + Path("test_printer.dot").unlink(missing_ok=True) @pytest.mark.skipif( @@ -58,17 +69,6 @@ def test_printer_cheatcode(): output.data["description"] == "CounterTest (test/Counter.t.sol)\n\tsetUp\n\t\tdeal - (test/Counter.t.sol#21 (9 - 32)\n\t\tvm.deal(alice,1000000000000000000)\n\n\t\tdeal - (test/Counter.t.sol#22 (9 - 30)\n\t\tvm.deal(bob,2000000000000000000)\n\n\ttestIncrement\n\t\tprank - (test/Counter.t.sol#28 (9 - 24)\n\t\tvm.prank(alice)\n\n\t\tassertEq - (test/Counter.t.sol#30 (9 - 38)\n\t\tassertEq(counter.number(),1)\n\n\t\tprank - (test/Counter.t.sol#32 (9 - 22)\n\t\tvm.prank(bob)\n\n\t\tassertEq - (test/Counter.t.sol#34 (9 - 38)\n\t\tassertEq(counter.number(),2)\n\n" ) - # Let also test the include/exclude interface behavior - # Check that the interface is not included - assert "MyInterfaceX" not in content - - slither.include_interfaces = True - output = printer.output("test_printer.dot") - content = output.elements[0]["name"]["content"] - assert "MyInterfaceX" in content - - # Remove test generated files - Path("test_printer.dot").unlink(missing_ok=True) def test_slithir_printer(solc_binary_path) -> None: From d1bf27a277b02d88e6801674d13f330987c6cd13 Mon Sep 17 00:00:00 2001 From: Feist Josselin Date: Wed, 2 Oct 2024 12:15:10 +0200 Subject: [PATCH 32/71] Update CI --- .github/workflows/black_auto.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/black_auto.yml b/.github/workflows/black_auto.yml index 4c0dca0c63..ad1ad2ea4b 100644 --- a/.github/workflows/black_auto.yml +++ b/.github/workflows/black_auto.yml @@ -30,12 +30,14 @@ jobs: with: python-version: 3.8 - - name: Install Python dependencies - run: pip install .[dev] + - name: Run black + uses: psf/black@stable + with: + options: "" + summary: false + version: "~= 22.3.0" - - name: Run linters - uses: wearerequired/lint-action@v2 + - name: Annotate diff changes using reviewdog + uses: reviewdog/action-suggester@v1 with: - auto_fix: true - black: true - black_auto_fix: true + tool_name: blackfmt From 7e3f9c56df70c7a3265a9b4f72072448166b0821 Mon Sep 17 00:00:00 2001 From: Josselin Feist Date: Wed, 2 Oct 2024 12:21:04 +0200 Subject: [PATCH 33/71] Update slither/core/cfg/node.py Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- slither/core/cfg/node.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/slither/core/cfg/node.py b/slither/core/cfg/node.py index 1d976af450..fc178db4a7 100644 --- a/slither/core/cfg/node.py +++ b/slither/core/cfg/node.py @@ -518,7 +518,8 @@ def contains_require_or_assert(self) -> bool: bool: True if the node has a require or assert call """ return any( - ir.function.name in ["require(bool)", "require(bool,string)", "require(bool,error)", "assert(bool)"] + ir.function.name + in ["require(bool)", "require(bool,string)", "require(bool,error)", "assert(bool)"] for ir in self.internal_calls ) From 9113f49bbd5e5e8ede82d8df0f8a32dd17bb91eb Mon Sep 17 00:00:00 2001 From: Feist Josselin Date: Wed, 2 Oct 2024 14:34:45 +0200 Subject: [PATCH 34/71] Reduce the number of CI tests on PR Everything is still run on a daily basis with cron, but this PR remove unecessary/problematic CI checks on every PR --- .github/workflows/ci.yml | 11 +++--- .github/workflows/ci_pr.yml | 72 +++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/ci_pr.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d2ba83d0d..24ce1154fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,12 +5,11 @@ defaults: run: shell: bash +# We run all the CI tests only on cron job +# This is to reduce the CI time, and prevent issues +# Like with the etherscan API +# All the test from ci_pr are run here + additional ones on: - push: - branches: - - master - - dev - pull_request: schedule: # run CI every day even if no PRs/merges occur - cron: '0 12 * * *' @@ -31,9 +30,7 @@ jobs: "dapp", "data_dependency", "path_filtering", - # "embark", "erc", - # "etherlime", "etherscan", "find_paths", "flat", diff --git a/.github/workflows/ci_pr.yml b/.github/workflows/ci_pr.yml new file mode 100644 index 0000000000..95d088d2a7 --- /dev/null +++ b/.github/workflows/ci_pr.yml @@ -0,0 +1,72 @@ +--- +name: CI + +defaults: + run: + shell: bash + +on: + push: + branches: + - master + - dev + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + tests: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: ["ubuntu-latest", "windows-2022"] + python: ${{ (github.event_name == 'pull_request' && fromJSON('["3.8", "3.12"]')) || fromJSON('["3.8", "3.9", "3.10", "3.11", "3.12"]') }} + type: ["data_dependency", + "path_filtering", + "erc", + "find_paths", + "flat", + "interface", + "printers", + "slither_config", + "upgradability"] + exclude: + # Requires nix + - os: windows-2022 + type: dapp + # Requires nvm + - os: windows-2022 + type: truffle + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: | + pip install ".[test]" + solc-select use 0.4.25 --always-install + solc-select use 0.8.0 --always-install + solc-select use 0.5.1 --always-install + + - name: Set up nix + if: matrix.type == 'dapp' + uses: cachix/install-nix-action@V27 + + - name: Set up cachix + if: matrix.type == 'dapp' + uses: cachix/cachix-action@v15 + with: + name: dapp + + - name: Run Tests + env: + PYTHONUTF8: 1 + TEST_TYPE: ${{ matrix.type }} + GITHUB_ETHERSCAN: ${{ secrets.GITHUB_ETHERSCAN }} + run: | + bash "scripts/ci_test_${TEST_TYPE}.sh" From dfab5ae696a26c2efce9de6cc569d49663af8001 Mon Sep 17 00:00:00 2001 From: Feist Josselin Date: Wed, 2 Oct 2024 14:36:39 +0200 Subject: [PATCH 35/71] Remove windows tests from PR --- .github/workflows/ci_pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci_pr.yml b/.github/workflows/ci_pr.yml index 95d088d2a7..a1a8aadc1b 100644 --- a/.github/workflows/ci_pr.yml +++ b/.github/workflows/ci_pr.yml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - os: ["ubuntu-latest", "windows-2022"] + os: ["ubuntu-latest"] # Do not run on windows to save time. In practice not many users are on windows, and this is still run on the cron job python: ${{ (github.event_name == 'pull_request' && fromJSON('["3.8", "3.12"]')) || fromJSON('["3.8", "3.9", "3.10", "3.11", "3.12"]') }} type: ["data_dependency", "path_filtering", From 3a6888694d40267e869176d0e97da634b7949add Mon Sep 17 00:00:00 2001 From: Feist Josselin Date: Wed, 2 Oct 2024 14:42:03 +0200 Subject: [PATCH 36/71] Fix CI lint --- .github/workflows/ci_pr.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/ci_pr.yml b/.github/workflows/ci_pr.yml index a1a8aadc1b..7ad7d34c81 100644 --- a/.github/workflows/ci_pr.yml +++ b/.github/workflows/ci_pr.yml @@ -33,13 +33,6 @@ jobs: "printers", "slither_config", "upgradability"] - exclude: - # Requires nix - - os: windows-2022 - type: dapp - # Requires nvm - - os: windows-2022 - type: truffle steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} From 9122cef24e7c11c7a2363074566d02c3e5085d2b Mon Sep 17 00:00:00 2001 From: Feist Josselin Date: Wed, 2 Oct 2024 15:38:47 +0200 Subject: [PATCH 37/71] Improvements --- .github/workflows/ci.yml | 28 ++++------------ .github/workflows/ci_pr.yml | 65 ------------------------------------- 2 files changed, 7 insertions(+), 86 deletions(-) delete mode 100644 .github/workflows/ci_pr.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24ce1154fb..2ee7ee5fb7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,11 +5,12 @@ defaults: run: shell: bash -# We run all the CI tests only on cron job -# This is to reduce the CI time, and prevent issues -# Like with the etherscan API -# All the test from ci_pr are run here + additional ones on: + push: + branches: + - master + - dev + pull_request: schedule: # run CI every day even if no PRs/merges occur - cron: '0 12 * * *' @@ -24,24 +25,9 @@ jobs: strategy: fail-fast: false matrix: - os: ["ubuntu-latest", "windows-2022"] + os: ${{ (github.event_name == 'pull_request' && fromJSON('["ubuntu-latest"]')) || fromJSON('["ubuntu-latest", "windows-2022"]') }} python: ${{ (github.event_name == 'pull_request' && fromJSON('["3.8", "3.12"]')) || fromJSON('["3.8", "3.9", "3.10", "3.11", "3.12"]') }} - type: ["cli", - "dapp", - "data_dependency", - "path_filtering", - "erc", - "etherscan", - "find_paths", - "flat", - "interface", - "kspec", - "printers", - # "prop" - "simil", - "slither_config", - "truffle", - "upgradability"] + type: ${{ (github.event_name == 'pull_request' && fromJSON('["data_dependency", "path_filtering","erc","find_paths","flat","interface", "printers","slither_config","upgradability"]')) || fromJSON('["data_dependency", "path_filtering","erc","find_paths","flat","interface", "printers","slither_config","upgradability", "cli", "dapp", "etherscan", "kspec", "simil", "truffle"]') }} exclude: # Requires nix - os: windows-2022 diff --git a/.github/workflows/ci_pr.yml b/.github/workflows/ci_pr.yml deleted file mode 100644 index 7ad7d34c81..0000000000 --- a/.github/workflows/ci_pr.yml +++ /dev/null @@ -1,65 +0,0 @@ ---- -name: CI - -defaults: - run: - shell: bash - -on: - push: - branches: - - master - - dev - pull_request: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - tests: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: ["ubuntu-latest"] # Do not run on windows to save time. In practice not many users are on windows, and this is still run on the cron job - python: ${{ (github.event_name == 'pull_request' && fromJSON('["3.8", "3.12"]')) || fromJSON('["3.8", "3.9", "3.10", "3.11", "3.12"]') }} - type: ["data_dependency", - "path_filtering", - "erc", - "find_paths", - "flat", - "interface", - "printers", - "slither_config", - "upgradability"] - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python }} - - name: Install dependencies - run: | - pip install ".[test]" - solc-select use 0.4.25 --always-install - solc-select use 0.8.0 --always-install - solc-select use 0.5.1 --always-install - - - name: Set up nix - if: matrix.type == 'dapp' - uses: cachix/install-nix-action@V27 - - - name: Set up cachix - if: matrix.type == 'dapp' - uses: cachix/cachix-action@v15 - with: - name: dapp - - - name: Run Tests - env: - PYTHONUTF8: 1 - TEST_TYPE: ${{ matrix.type }} - GITHUB_ETHERSCAN: ${{ secrets.GITHUB_ETHERSCAN }} - run: | - bash "scripts/ci_test_${TEST_TYPE}.sh" From afac8c4bf036583a66f40ee2845df3334be4fd77 Mon Sep 17 00:00:00 2001 From: Simone Date: Fri, 4 Oct 2024 18:12:00 +0200 Subject: [PATCH 38/71] Improve values extraction --- slither/printers/guidance/echidna.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/slither/printers/guidance/echidna.py b/slither/printers/guidance/echidna.py index 35a6091935..b87fc76e64 100644 --- a/slither/printers/guidance/echidna.py +++ b/slither/printers/guidance/echidna.py @@ -226,6 +226,9 @@ def _extract_constants_from_irs( # pylint: disable=too-many-branches,too-many-n # Do not report struct_name in a.struct_name if isinstance(ir, Member): continue + if isinstance(var_read, Variable) and var_read.is_constant: + value = ConstantFolding(var_read.expression, var_read.type).result() + all_cst_used.append(ConstantValue(str(value), str(var_read.type))) if isinstance(var_read, Constant): all_cst_used.append(ConstantValue(str(var_read.value), str(var_read.type))) if isinstance(var_read, StateVariable): From d96b87e140a102da86703ff16882323abeb15850 Mon Sep 17 00:00:00 2001 From: Simone Date: Fri, 4 Oct 2024 19:52:08 +0200 Subject: [PATCH 39/71] Add Optimism deprecation detector --- slither/detectors/all_detectors.py | 1 + .../functions/optimism_deprecation.py | 91 ++++++++++++++++++ ...ion_0_8_20_optimism_deprecation_sol__0.txt | 4 + .../0.8.20/optimism_deprecation.sol | 27 ++++++ .../optimism_deprecation.sol-0.8.20.zip | Bin 0 -> 3060 bytes tests/e2e/detectors/test_detectors.py | 5 + 6 files changed, 128 insertions(+) create mode 100644 slither/detectors/functions/optimism_deprecation.py create mode 100644 tests/e2e/detectors/snapshots/detectors__detector_OptimismDeprecation_0_8_20_optimism_deprecation_sol__0.txt create mode 100644 tests/e2e/detectors/test_data/optimism-deprecation/0.8.20/optimism_deprecation.sol create mode 100644 tests/e2e/detectors/test_data/optimism-deprecation/0.8.20/optimism_deprecation.sol-0.8.20.zip diff --git a/slither/detectors/all_detectors.py b/slither/detectors/all_detectors.py index 44a168c2b6..0222608a19 100644 --- a/slither/detectors/all_detectors.py +++ b/slither/detectors/all_detectors.py @@ -97,5 +97,6 @@ from .statements.tautological_compare import TautologicalCompare from .statements.return_bomb import ReturnBomb from .functions.out_of_order_retryable import OutOfOrderRetryable +from .functions.optimism_deprecation import OptimismDeprecation # from .statements.unused_import import UnusedImport diff --git a/slither/detectors/functions/optimism_deprecation.py b/slither/detectors/functions/optimism_deprecation.py new file mode 100644 index 0000000000..426ec79490 --- /dev/null +++ b/slither/detectors/functions/optimism_deprecation.py @@ -0,0 +1,91 @@ +from typing import List + +from slither.detectors.abstract_detector import ( + AbstractDetector, + DetectorClassification, + DETECTOR_INFO, +) +from slither.core.cfg.node import Node +from slither.core.expressions import TypeConversion, Literal +from slither.utils.output import Output + + +class OptimismDeprecation(AbstractDetector): + + ARGUMENT = "optimism-deprecation" + HELP = "Detect when deprecated Optimism predeploy or function is used." + IMPACT = DetectorClassification.LOW + CONFIDENCE = DetectorClassification.HIGH + + WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#optimism-deprecation" + + WIKI_TITLE = "Optimism deprecated predeploy or function" + WIKI_DESCRIPTION = "Detect when deprecated Optimism predeploy or function is used." + + # region wiki_exploit_scenario + WIKI_EXPLOIT_SCENARIO = """ +```solidity +interface GasPriceOracle { + function scalar() external view returns (uint256); +} + +contract Test { + GasPriceOracle constant OPT_GAS = GasPriceOracle(0x420000000000000000000000000000000000000F); + + function a() public { + OPT_GAS.scalar(); + } +} +``` +The call to the `scalar` function of the Optimism GasPriceOracle predeploy always revert. +""" + # endregion wiki_exploit_scenario + + WIKI_RECOMMENDATION = "D." + + def _detect(self) -> List[Output]: + results = [] + + deprecated_predeploys = [ + "0x4200000000000000000000000000000000000000", # LegacyMessagePasser + "0x4200000000000000000000000000000000000001", # L1MessageSender + "0x4200000000000000000000000000000000000002", # DeployerWhitelist + "0x4200000000000000000000000000000000000013", # L1BlockNumber + ] + + for contract in self.compilation_unit.contracts_derived: + use_deprecated: List[Node] = [] + + for _, ir in contract.all_high_level_calls: + # To avoid FPs we assume predeploy contracts are always assigned to a constant and typecasted to an interface + # and we check the target address of a high level call. + if isinstance(ir.destination.expression, TypeConversion) and isinstance( + ir.destination.expression.expression, Literal + ): + if ir.destination.expression.expression.value in deprecated_predeploys: + use_deprecated.append(ir.node) + + if ( + ir.destination.expression.expression.value + == "0x420000000000000000000000000000000000000F" + and ir.function_name in ("overhead", "scalar", "getL1GasUsed") + ): + use_deprecated.append(ir.node) + + # Sort so output is deterministic + use_deprecated.sort(key=lambda x: (x.node_id, x.function.full_name)) + + if len(use_deprecated) > 0: + info: DETECTOR_INFO = [ + "A deprecated Optimism predeploy or function is used in the ", + contract.name, + " contract.\n", + ] + + for node in use_deprecated: + info.extend(["\t - ", node, "\n"]) + + res = self.generate_result(info) + results.append(res) + + return results diff --git a/tests/e2e/detectors/snapshots/detectors__detector_OptimismDeprecation_0_8_20_optimism_deprecation_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_OptimismDeprecation_0_8_20_optimism_deprecation_sol__0.txt new file mode 100644 index 0000000000..f6f4dccba0 --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_OptimismDeprecation_0_8_20_optimism_deprecation_sol__0.txt @@ -0,0 +1,4 @@ +A deprecated Optimism predeploy or function is used in the Test contract. + - OPT_GAS.scalar() (tests/e2e/detectors/test_data/optimism-deprecation/0.8.20/optimism_deprecation.sol#15) + - L1_BLOCK_NUMBER.q() (tests/e2e/detectors/test_data/optimism-deprecation/0.8.20/optimism_deprecation.sol#19) + diff --git a/tests/e2e/detectors/test_data/optimism-deprecation/0.8.20/optimism_deprecation.sol b/tests/e2e/detectors/test_data/optimism-deprecation/0.8.20/optimism_deprecation.sol new file mode 100644 index 0000000000..7ad55f3ddd --- /dev/null +++ b/tests/e2e/detectors/test_data/optimism-deprecation/0.8.20/optimism_deprecation.sol @@ -0,0 +1,27 @@ +interface GasPriceOracle { + function scalar() external view returns (uint256); + function baseFee() external view returns (uint256); +} + +interface L1BlockNumber { + function q() external view returns (uint256); +} + +contract Test { + GasPriceOracle constant OPT_GAS = GasPriceOracle(0x420000000000000000000000000000000000000F); + L1BlockNumber constant L1_BLOCK_NUMBER = L1BlockNumber(0x4200000000000000000000000000000000000013); + + function bad() public { + OPT_GAS.scalar(); + } + + function bad2() public { + L1_BLOCK_NUMBER.q(); + } + + function good() public { + OPT_GAS.baseFee(); + } + + +} diff --git a/tests/e2e/detectors/test_data/optimism-deprecation/0.8.20/optimism_deprecation.sol-0.8.20.zip b/tests/e2e/detectors/test_data/optimism-deprecation/0.8.20/optimism_deprecation.sol-0.8.20.zip new file mode 100644 index 0000000000000000000000000000000000000000..de18d4a0dde56342b0a92296cedfe5b567d7d705 GIT binary patch literal 3060 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 Date: Fri, 4 Oct 2024 19:55:24 +0200 Subject: [PATCH 40/71] Add Chainlink feed registry detector --- slither/detectors/all_detectors.py | 1 + .../functions/chainlink_feed_registry.py | 103 ++++++++++++++++++ ..._0_8_20_chainlink_feed_registry_sol__0.txt | 3 + .../0.8.20/chainlink_feed_registry.sol | 37 +++++++ .../chainlink_feed_registry.sol-0.8.20.zip | Bin 0 -> 5853 bytes tests/e2e/detectors/test_detectors.py | 5 + 6 files changed, 149 insertions(+) create mode 100644 slither/detectors/functions/chainlink_feed_registry.py create mode 100644 tests/e2e/detectors/snapshots/detectors__detector_ChainlinkFeedRegistry_0_8_20_chainlink_feed_registry_sol__0.txt create mode 100644 tests/e2e/detectors/test_data/chainlink-feed-registry/0.8.20/chainlink_feed_registry.sol create mode 100644 tests/e2e/detectors/test_data/chainlink-feed-registry/0.8.20/chainlink_feed_registry.sol-0.8.20.zip diff --git a/slither/detectors/all_detectors.py b/slither/detectors/all_detectors.py index 44a168c2b6..0b93b9d753 100644 --- a/slither/detectors/all_detectors.py +++ b/slither/detectors/all_detectors.py @@ -97,5 +97,6 @@ from .statements.tautological_compare import TautologicalCompare from .statements.return_bomb import ReturnBomb from .functions.out_of_order_retryable import OutOfOrderRetryable +from .functions.chainlink_feed_registry import ChainlinkFeedRegistry # from .statements.unused_import import UnusedImport diff --git a/slither/detectors/functions/chainlink_feed_registry.py b/slither/detectors/functions/chainlink_feed_registry.py new file mode 100644 index 0000000000..951dfb5846 --- /dev/null +++ b/slither/detectors/functions/chainlink_feed_registry.py @@ -0,0 +1,103 @@ +from typing import List + +from slither.detectors.abstract_detector import ( + AbstractDetector, + DetectorClassification, + DETECTOR_INFO, +) +from slither.utils.output import Output + + +class ChainlinkFeedRegistry(AbstractDetector): + + ARGUMENT = "chainlink-feed-registry" + HELP = "Detect when chainlink feed registry is used" + IMPACT = DetectorClassification.LOW + CONFIDENCE = DetectorClassification.HIGH + + WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#chainlink-feed-registry" + + WIKI_TITLE = "Chainlink Feed Registry usage" + WIKI_DESCRIPTION = "Detect when Chainlink Feed Registry is used. At the moment is only available on Ethereum Mainnet." + + # region wiki_exploit_scenario + WIKI_EXPLOIT_SCENARIO = """ +```solidity +import "chainlink/contracts/src/v0.8/interfaces/FeedRegistryInteface.sol" + +contract A { + FeedRegistryInterface public immutable registry; + + constructor(address _registry) { + registry = _registry; + } + + function getPrice(address base, address quote) public return(uint256) { + (, int256 price,,,) = registry.latestRoundData(base, quote); + // Do price validation + return uint256(price); + } +} +``` +If the contract is deployed on a different chain than Ethereum Mainnet the `getPrice` function will revert. +""" + # endregion wiki_exploit_scenario + + WIKI_RECOMMENDATION = "Do not use Chainlink Feed Registry outside of Ethereum Mainnet." + + def _detect(self) -> List[Output]: + # https://github.com/smartcontractkit/chainlink/blob/8ca41fc8f722accfccccb4b1778db2df8fef5437/contracts/src/v0.8/interfaces/FeedRegistryInterface.sol + registry_functions = [ + "decimals", + "description", + "versiom", + "latestRoundData", + "getRoundData", + "latestAnswer", + "latestTimestamp", + "latestRound", + "getAnswer", + "getTimestamp", + "getFeed", + "getPhaseFeed", + "isFeedEnabled", + "getPhase", + "getRoundFeed", + "getPhaseRange", + "getPreviousRoundId", + "getNextRoundId", + "proposeFeed", + "confirmFeed", + "getProposedFeed", + "proposedGetRoundData", + "proposedLatestRoundData", + "getCurrentPhaseId", + ] + results = [] + + for contract in self.compilation_unit.contracts_derived: + nodes = [] + for target, ir in contract.all_high_level_calls: + if ( + target.name == "FeedRegistryInterface" + and ir.function_name in registry_functions + ): + nodes.append(ir.node) + + # Sort so output is deterministic + nodes.sort(key=lambda x: (x.node_id, x.function.full_name)) + + if len(nodes) > 0: + info: DETECTOR_INFO = [ + "The Chainlink Feed Registry is used in the ", + contract.name, + " contract. It's only available on Ethereum Mainnet, consider to not use it if the contract needs to be deployed on other chains.\n", + ] + + for node in nodes: + info.extend(["\t - ", node, "\n"]) + + res = self.generate_result(info) + results.append(res) + + return results diff --git a/tests/e2e/detectors/snapshots/detectors__detector_ChainlinkFeedRegistry_0_8_20_chainlink_feed_registry_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_ChainlinkFeedRegistry_0_8_20_chainlink_feed_registry_sol__0.txt new file mode 100644 index 0000000000..6b7653ed03 --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_ChainlinkFeedRegistry_0_8_20_chainlink_feed_registry_sol__0.txt @@ -0,0 +1,3 @@ +The Chainlink Feed Registry is used in the A contract. It's only available on Ethereum Mainnet, consider to not use it if the contract needs to be deployed on other chains. + - (None,price,None,None,None) = registry.latestRoundData(base,quote) (tests/e2e/detectors/test_data/chainlink-feed-registry/0.8.20/chainlink_feed_registry.sol#25) + diff --git a/tests/e2e/detectors/test_data/chainlink-feed-registry/0.8.20/chainlink_feed_registry.sol b/tests/e2e/detectors/test_data/chainlink-feed-registry/0.8.20/chainlink_feed_registry.sol new file mode 100644 index 0000000000..cf5d1ad4d4 --- /dev/null +++ b/tests/e2e/detectors/test_data/chainlink-feed-registry/0.8.20/chainlink_feed_registry.sol @@ -0,0 +1,37 @@ +interface FeedRegistryInterface { + function latestRoundData( + address base, + address quote + ) external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); +} + +interface MyInterface { + function latestRoundData( + address base, + address quote + ) external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); +} + +contract A { + FeedRegistryInterface public immutable registry; + MyInterface public immutable my_interface; + + constructor(FeedRegistryInterface _registry, MyInterface _my_interface) { + registry = _registry; + my_interface = _my_interface; + } + + function getPriceBad(address base, address quote) public returns (uint256) { + (, int256 price,,,) = registry.latestRoundData(base, quote); + // Do price validation + return uint256(price); + } + + function getPriceGood(address base, address quote) public returns (uint256) { + (, int256 price,,,) = my_interface.latestRoundData(base, quote); + // Do price validation + return uint256(price); + } + + +} \ No newline at end of file diff --git a/tests/e2e/detectors/test_data/chainlink-feed-registry/0.8.20/chainlink_feed_registry.sol-0.8.20.zip b/tests/e2e/detectors/test_data/chainlink-feed-registry/0.8.20/chainlink_feed_registry.sol-0.8.20.zip new file mode 100644 index 0000000000000000000000000000000000000000..262ede23f1aed4f717506adaae6be3b1b7dabd49 GIT binary patch literal 5853 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 Date: Fri, 4 Oct 2024 19:59:55 +0200 Subject: [PATCH 41/71] Update slither/detectors/functions/optimism_deprecation.py Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- slither/detectors/functions/optimism_deprecation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/slither/detectors/functions/optimism_deprecation.py b/slither/detectors/functions/optimism_deprecation.py index 426ec79490..8d81851a01 100644 --- a/slither/detectors/functions/optimism_deprecation.py +++ b/slither/detectors/functions/optimism_deprecation.py @@ -71,7 +71,6 @@ def _detect(self) -> List[Output]: and ir.function_name in ("overhead", "scalar", "getL1GasUsed") ): use_deprecated.append(ir.node) - # Sort so output is deterministic use_deprecated.sort(key=lambda x: (x.node_id, x.function.full_name)) From 6afd2810121d4d2f2906c9141b2e1ccaca2c898d Mon Sep 17 00:00:00 2001 From: Simone <79767264+smonicas@users.noreply.github.com> Date: Fri, 4 Oct 2024 20:00:00 +0200 Subject: [PATCH 42/71] Update slither/detectors/functions/optimism_deprecation.py Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- slither/detectors/functions/optimism_deprecation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/slither/detectors/functions/optimism_deprecation.py b/slither/detectors/functions/optimism_deprecation.py index 8d81851a01..329b78adfa 100644 --- a/slither/detectors/functions/optimism_deprecation.py +++ b/slither/detectors/functions/optimism_deprecation.py @@ -73,7 +73,6 @@ def _detect(self) -> List[Output]: use_deprecated.append(ir.node) # Sort so output is deterministic use_deprecated.sort(key=lambda x: (x.node_id, x.function.full_name)) - if len(use_deprecated) > 0: info: DETECTOR_INFO = [ "A deprecated Optimism predeploy or function is used in the ", From 909ab5193e2cc9eb64baa87613e2b7610e94d190 Mon Sep 17 00:00:00 2001 From: Simone <79767264+smonicas@users.noreply.github.com> Date: Fri, 4 Oct 2024 20:00:28 +0200 Subject: [PATCH 43/71] Update slither/detectors/functions/chainlink_feed_registry.py Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- slither/detectors/functions/chainlink_feed_registry.py | 1 - 1 file changed, 1 deletion(-) diff --git a/slither/detectors/functions/chainlink_feed_registry.py b/slither/detectors/functions/chainlink_feed_registry.py index 951dfb5846..82ab17424e 100644 --- a/slither/detectors/functions/chainlink_feed_registry.py +++ b/slither/detectors/functions/chainlink_feed_registry.py @@ -83,7 +83,6 @@ def _detect(self) -> List[Output]: and ir.function_name in registry_functions ): nodes.append(ir.node) - # Sort so output is deterministic nodes.sort(key=lambda x: (x.node_id, x.function.full_name)) From 96c6c94dd6c5946cb4031a88c39aecc86e1504c3 Mon Sep 17 00:00:00 2001 From: Simone Date: Fri, 4 Oct 2024 20:06:52 +0200 Subject: [PATCH 44/71] Only consider if call target is a variable --- slither/detectors/functions/optimism_deprecation.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/slither/detectors/functions/optimism_deprecation.py b/slither/detectors/functions/optimism_deprecation.py index 329b78adfa..08d736a2bd 100644 --- a/slither/detectors/functions/optimism_deprecation.py +++ b/slither/detectors/functions/optimism_deprecation.py @@ -6,6 +6,7 @@ DETECTOR_INFO, ) from slither.core.cfg.node import Node +from slither.core.variables.variable import Variable from slither.core.expressions import TypeConversion, Literal from slither.utils.output import Output @@ -59,8 +60,10 @@ def _detect(self) -> List[Output]: for _, ir in contract.all_high_level_calls: # To avoid FPs we assume predeploy contracts are always assigned to a constant and typecasted to an interface # and we check the target address of a high level call. - if isinstance(ir.destination.expression, TypeConversion) and isinstance( - ir.destination.expression.expression, Literal + if ( + isinstance(ir.destination, Variable) + and isinstance(ir.destination.expression, TypeConversion) + and isinstance(ir.destination.expression.expression, Literal) ): if ir.destination.expression.expression.value in deprecated_predeploys: use_deprecated.append(ir.node) From 6b503a9970f50bc60b98977d61498e8dba69f05a Mon Sep 17 00:00:00 2001 From: Josselin Feist Date: Fri, 4 Oct 2024 20:12:49 +0200 Subject: [PATCH 45/71] Update optimism_deprecation.py --- slither/detectors/functions/optimism_deprecation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slither/detectors/functions/optimism_deprecation.py b/slither/detectors/functions/optimism_deprecation.py index 08d736a2bd..752e8bb2d9 100644 --- a/slither/detectors/functions/optimism_deprecation.py +++ b/slither/detectors/functions/optimism_deprecation.py @@ -42,7 +42,7 @@ class OptimismDeprecation(AbstractDetector): """ # endregion wiki_exploit_scenario - WIKI_RECOMMENDATION = "D." + WIKI_RECOMMENDATION = "Do not use the deprecated components." def _detect(self) -> List[Output]: results = [] From 48d7d9beef3d2dcc54aff6a2ea7ab7656d05b2a0 Mon Sep 17 00:00:00 2001 From: Simone Date: Mon, 7 Oct 2024 17:44:13 +0200 Subject: [PATCH 46/71] Improve support for type .max/.min and Enums --- .../visitors/expression/constants_folding.py | 122 +++++++++++++++++- 1 file changed, 116 insertions(+), 6 deletions(-) diff --git a/slither/visitors/expression/constants_folding.py b/slither/visitors/expression/constants_folding.py index b1fa570c6a..c8cfeb7165 100644 --- a/slither/visitors/expression/constants_folding.py +++ b/slither/visitors/expression/constants_folding.py @@ -13,7 +13,9 @@ TupleExpression, TypeConversion, CallExpression, + MemberAccess, ) +from slither.core.expressions.elementary_type_name_expression import ElementaryTypeNameExpression from slither.core.variables import Variable from slither.utils.integer_conversion import convert_string_to_fraction, convert_string_to_int from slither.visitors.expression.expression import ExpressionVisitor @@ -27,7 +29,13 @@ class NotConstant(Exception): KEY = "ConstantFolding" CONSTANT_TYPES_OPERATIONS = Union[ - Literal, BinaryOperation, UnaryOperation, Identifier, TupleExpression, TypeConversion + Literal, + BinaryOperation, + UnaryOperation, + Identifier, + TupleExpression, + TypeConversion, + MemberAccess, ] @@ -69,6 +77,7 @@ def result(self) -> "Literal": # pylint: disable=import-outside-toplevel def _post_identifier(self, expression: Identifier) -> None: from slither.core.declarations.solidity_variables import SolidityFunction + from slither.core.declarations.enum import Enum if isinstance(expression.value, Variable): if expression.value.is_constant: @@ -77,7 +86,14 @@ def _post_identifier(self, expression: Identifier) -> None: # Everything outside of literal if isinstance( expr, - (BinaryOperation, UnaryOperation, Identifier, TupleExpression, TypeConversion), + ( + BinaryOperation, + UnaryOperation, + Identifier, + TupleExpression, + TypeConversion, + MemberAccess, + ), ): cf = ConstantFolding(expr, self._type) expr = cf.result() @@ -88,7 +104,10 @@ def _post_identifier(self, expression: Identifier) -> None: elif isinstance(expression.value, SolidityFunction): set_val(expression, expression.value) else: - raise NotConstant + # We don't want to raise an error for a direct access to an Enum as they can be converted to a constant value + # We can't handle it here because we don't have the field accessed so we do it in _post_member_access + if not isinstance(expression.value, Enum): + raise NotConstant # pylint: disable=too-many-branches,too-many-statements def _post_binary_operation(self, expression: BinaryOperation) -> None: @@ -96,12 +115,28 @@ def _post_binary_operation(self, expression: BinaryOperation) -> None: expression_right = expression.expression_right if not isinstance( expression_left, - (Literal, BinaryOperation, UnaryOperation, Identifier, TupleExpression, TypeConversion), + ( + Literal, + BinaryOperation, + UnaryOperation, + Identifier, + TupleExpression, + TypeConversion, + MemberAccess, + ), ): raise NotConstant if not isinstance( expression_right, - (Literal, BinaryOperation, UnaryOperation, Identifier, TupleExpression, TypeConversion), + ( + Literal, + BinaryOperation, + UnaryOperation, + Identifier, + TupleExpression, + TypeConversion, + MemberAccess, + ), ): raise NotConstant left = get_val(expression_left) @@ -205,6 +240,22 @@ def _post_assignement_operation(self, expression: expressions.AssignmentOperatio raise NotConstant def _post_call_expression(self, expression: expressions.CallExpression) -> None: + from slither.core.declarations.solidity_variables import SolidityFunction + from slither.core.declarations.enum import Enum + + # pylint: disable=too-many-boolean-expressions + if ( + isinstance(expression.called, Identifier) + and expression.called.value == SolidityFunction("type()") + and len(expression.arguments) == 1 + and ( + isinstance(expression.arguments[0], ElementaryTypeNameExpression) + or isinstance(expression.arguments[0], Identifier) + and isinstance(expression.arguments[0].value, Enum) + ) + ): + # Returning early to support type(ElemType).max/min or type(MyEnum).max/min + return called = get_val(expression.called) args = [get_val(arg) for arg in expression.arguments] if called.name == "keccak256(bytes)": @@ -220,12 +271,70 @@ def _post_conditional_expression(self, expression: expressions.ConditionalExpres def _post_elementary_type_name_expression( self, expression: expressions.ElementaryTypeNameExpression ) -> None: - raise NotConstant + # We don't have to raise an exception to support type(uint112).max or similar + pass def _post_index_access(self, expression: expressions.IndexAccess) -> None: raise NotConstant def _post_member_access(self, expression: expressions.MemberAccess) -> None: + from slither.core.declarations import ( + SolidityFunction, + Contract, + EnumContract, + EnumTopLevel, + Enum, + ) + from slither.core.solidity_types import UserDefinedType + + # pylint: disable=too-many-nested-blocks + if isinstance(expression.expression, CallExpression) and expression.member_name in [ + "min", + "max", + ]: + if isinstance(expression.expression.called, Identifier): + if expression.expression.called.value == SolidityFunction("type()"): + assert len(expression.expression.arguments) == 1 + type_expression_found = expression.expression.arguments[0] + type_found: Union[ElementaryType, UserDefinedType] + if isinstance(type_expression_found, ElementaryTypeNameExpression): + type_expression_found_type = type_expression_found.type + assert isinstance(type_expression_found_type, ElementaryType) + type_found = type_expression_found_type + value = ( + type_found.max if expression.member_name == "max" else type_found.min + ) + set_val(expression, value) + return + # type(enum).max/min + # Case when enum is in another contract e.g. type(C.E).max + if isinstance(type_expression_found, MemberAccess): + contract = type_expression_found.expression.value + assert isinstance(contract, Contract) + for enum in contract.enums: + if enum.name == type_expression_found.member_name: + type_found_in_expression = enum + type_found = UserDefinedType(enum) + break + else: + assert isinstance(type_expression_found, Identifier) + type_found_in_expression = type_expression_found.value + assert isinstance(type_found_in_expression, (EnumContract, EnumTopLevel)) + type_found = UserDefinedType(type_found_in_expression) + value = ( + type_found_in_expression.max + if expression.member_name == "max" + else type_found_in_expression.min + ) + set_val(expression, value) + return + elif isinstance(expression.expression, Identifier) and isinstance( + expression.expression.value, Enum + ): + # Handle direct access to enum field + set_val(expression, expression.expression.value.values.index(expression.member_name)) + return + raise NotConstant def _post_new_array(self, expression: expressions.NewArray) -> None: @@ -272,6 +381,7 @@ def _post_type_conversion(self, expression: expressions.TypeConversion) -> None: TupleExpression, TypeConversion, CallExpression, + MemberAccess, ), ): raise NotConstant From f93b65fef3bc7e6cebccd444215d9579e0bc22bd Mon Sep 17 00:00:00 2001 From: Simone Date: Mon, 7 Oct 2024 23:09:01 +0200 Subject: [PATCH 47/71] Fix for user defined type and variables defined in another contract --- slither/printers/guidance/echidna.py | 12 ++++- .../visitors/expression/constants_folding.py | 48 +++++++++++++++++-- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/slither/printers/guidance/echidna.py b/slither/printers/guidance/echidna.py index b87fc76e64..057e782697 100644 --- a/slither/printers/guidance/echidna.py +++ b/slither/printers/guidance/echidna.py @@ -227,8 +227,16 @@ def _extract_constants_from_irs( # pylint: disable=too-many-branches,too-many-n if isinstance(ir, Member): continue if isinstance(var_read, Variable) and var_read.is_constant: - value = ConstantFolding(var_read.expression, var_read.type).result() - all_cst_used.append(ConstantValue(str(value), str(var_read.type))) + # In case of type conversion we use the destination type + if isinstance(ir, TypeConversion): + if isinstance(ir.type, TypeAlias): + value_type = ir.type.type + else: + value_type = ir.type + else: + value_type = var_read.type + value = ConstantFolding(var_read.expression, value_type).result() + all_cst_used.append(ConstantValue(str(value), str(value_type))) if isinstance(var_read, Constant): all_cst_used.append(ConstantValue(str(var_read.value), str(var_read.type))) if isinstance(var_read, StateVariable): diff --git a/slither/visitors/expression/constants_folding.py b/slither/visitors/expression/constants_folding.py index c8cfeb7165..63828bdd84 100644 --- a/slither/visitors/expression/constants_folding.py +++ b/slither/visitors/expression/constants_folding.py @@ -78,6 +78,8 @@ def result(self) -> "Literal": def _post_identifier(self, expression: Identifier) -> None: from slither.core.declarations.solidity_variables import SolidityFunction from slither.core.declarations.enum import Enum + from slither.core.solidity_types.type_alias import TypeAlias + from slither.core.declarations.contract import Contract if isinstance(expression.value, Variable): if expression.value.is_constant: @@ -104,9 +106,11 @@ def _post_identifier(self, expression: Identifier) -> None: elif isinstance(expression.value, SolidityFunction): set_val(expression, expression.value) else: - # We don't want to raise an error for a direct access to an Enum as they can be converted to a constant value + # Enum: We don't want to raise an error for a direct access to an Enum as they can be converted to a constant value # We can't handle it here because we don't have the field accessed so we do it in _post_member_access - if not isinstance(expression.value, Enum): + # TypeAlias: Support when a .wrap() is done with a constant + # Contract: Support when a constatn is use from a different contract + if not isinstance(expression.value, (Enum, TypeAlias, Contract)): raise NotConstant # pylint: disable=too-many-branches,too-many-statements @@ -242,6 +246,7 @@ def _post_assignement_operation(self, expression: expressions.AssignmentOperatio def _post_call_expression(self, expression: expressions.CallExpression) -> None: from slither.core.declarations.solidity_variables import SolidityFunction from slither.core.declarations.enum import Enum + from slither.core.solidity_types import TypeAlias # pylint: disable=too-many-boolean-expressions if ( @@ -256,6 +261,17 @@ def _post_call_expression(self, expression: expressions.CallExpression) -> None: ): # Returning early to support type(ElemType).max/min or type(MyEnum).max/min return + if ( + isinstance(expression.called.expression, Identifier) + and isinstance(expression.called.expression.value, TypeAlias) + and isinstance(expression.called, MemberAccess) + and expression.called.member_name == "wrap" + and len(expression.arguments) == 1 + ): + # Handle constants in .wrap of user defined type + set_val(expression, get_val(expression.arguments[0])) + return + called = get_val(expression.called) args = [get_val(arg) for arg in expression.arguments] if called.name == "keccak256(bytes)": @@ -277,6 +293,7 @@ def _post_elementary_type_name_expression( def _post_index_access(self, expression: expressions.IndexAccess) -> None: raise NotConstant + # pylint: disable=too-many-locals def _post_member_access(self, expression: expressions.MemberAccess) -> None: from slither.core.declarations import ( SolidityFunction, @@ -285,7 +302,7 @@ def _post_member_access(self, expression: expressions.MemberAccess) -> None: EnumTopLevel, Enum, ) - from slither.core.solidity_types import UserDefinedType + from slither.core.solidity_types import UserDefinedType, TypeAlias # pylint: disable=too-many-nested-blocks if isinstance(expression.expression, CallExpression) and expression.member_name in [ @@ -334,6 +351,31 @@ def _post_member_access(self, expression: expressions.MemberAccess) -> None: # Handle direct access to enum field set_val(expression, expression.expression.value.values.index(expression.member_name)) return + elif isinstance(expression.expression, Identifier) and isinstance( + expression.expression.value, TypeAlias + ): + # User defined type .wrap call handled in _post_call_expression + return + elif ( + isinstance(expression.expression.value, Contract) + and expression.member_name in expression.expression.value.variables_as_dict + and expression.expression.value.variables_as_dict[expression.member_name].is_constant + ): + # Handles when a constant is accessed on another contract + variables = expression.expression.value.variables_as_dict + if isinstance(variables[expression.member_name].expression, MemberAccess): + self._post_member_access(variables[expression.member_name].expression) + set_val(expression, get_val(variables[expression.member_name].expression)) + return + + # If the variable is a Literal we convert its value to int + value = ( + convert_string_to_int(variables[expression.member_name].expression.converted_value) + if isinstance(variables[expression.member_name].expression, Literal) + else variables[expression.member_name].expression + ) + set_val(expression, value) + return raise NotConstant From 607b22a506fb875c6710fd337d61cc2d1f39e0c2 Mon Sep 17 00:00:00 2001 From: Simone Date: Mon, 7 Oct 2024 23:40:56 +0200 Subject: [PATCH 48/71] Handle UnaryOperation from another contract --- .../visitors/expression/constants_folding.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/slither/visitors/expression/constants_folding.py b/slither/visitors/expression/constants_folding.py index 63828bdd84..ddadb77a14 100644 --- a/slither/visitors/expression/constants_folding.py +++ b/slither/visitors/expression/constants_folding.py @@ -369,11 +369,19 @@ def _post_member_access(self, expression: expressions.MemberAccess) -> None: return # If the variable is a Literal we convert its value to int - value = ( - convert_string_to_int(variables[expression.member_name].expression.converted_value) - if isinstance(variables[expression.member_name].expression, Literal) - else variables[expression.member_name].expression - ) + if isinstance(variables[expression.member_name].expression, Literal): + value = convert_string_to_int( + variables[expression.member_name].expression.converted_value + ) + # If the variable is a UnaryOperation we need convert its value to int + # and replacing possible spaces + elif isinstance(variables[expression.member_name].expression, UnaryOperation): + value = convert_string_to_int( + str(variables[expression.member_name].expression).replace(" ", "") + ) + else: + value = variables[expression.member_name].expression + set_val(expression, value) return From a5e970a2d4a00ccfc5b0d9eb4bb24072fc916c0e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 22:39:03 +0000 Subject: [PATCH 49/71] Bump cachix/install-nix-action from V27 to 30 Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from V27 to 30. This release includes the previously tagged commit. - [Release notes](https://github.com/cachix/install-nix-action/releases) - [Commits](https://github.com/cachix/install-nix-action/compare/V27...v30) --- updated-dependencies: - dependency-name: cachix/install-nix-action dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ee7ee5fb7..0bac900ab2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,7 @@ jobs: - name: Set up nix if: matrix.type == 'dapp' - uses: cachix/install-nix-action@V27 + uses: cachix/install-nix-action@v30 - name: Set up cachix if: matrix.type == 'dapp' From 25a467241231f015a65fc10a9f88794973c96471 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 22:39:05 +0000 Subject: [PATCH 50/71] Bump pypa/gh-action-pypi-publish from 1.9.0 to 1.10.3 Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.9.0 to 1.10.3. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.9.0...v1.10.3) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0a0f04f2bc..2e9f9d966b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -44,7 +44,7 @@ jobs: path: dist/ - name: publish - uses: pypa/gh-action-pypi-publish@v1.9.0 + uses: pypa/gh-action-pypi-publish@v1.10.3 - name: sign uses: sigstore/gh-action-sigstore-python@v3.0.0 From bae46ba9cec65a87e8bb686850f7463e72448ca1 Mon Sep 17 00:00:00 2001 From: Simone Date: Tue, 8 Oct 2024 13:56:18 +0200 Subject: [PATCH 51/71] Add Pyth deprecated functions detector --- slither/detectors/all_detectors.py | 1 + .../functions/pyth_deprecated_functions.py | 73 ++++++++++++++++++ ..._8_20_pyth_deprecated_functions_sol__0.txt | 3 + .../0.8.20/pyth_deprecated_functions.sol | 35 +++++++++ .../pyth_deprecated_functions.sol-0.8.20.zip | Bin 0 -> 4367 bytes tests/e2e/detectors/test_detectors.py | 5 ++ 6 files changed, 117 insertions(+) create mode 100644 slither/detectors/functions/pyth_deprecated_functions.py create mode 100644 tests/e2e/detectors/snapshots/detectors__detector_PythDeprecatedFunctions_0_8_20_pyth_deprecated_functions_sol__0.txt create mode 100644 tests/e2e/detectors/test_data/pyth-deprecated-functions/0.8.20/pyth_deprecated_functions.sol create mode 100644 tests/e2e/detectors/test_data/pyth-deprecated-functions/0.8.20/pyth_deprecated_functions.sol-0.8.20.zip diff --git a/slither/detectors/all_detectors.py b/slither/detectors/all_detectors.py index 44a168c2b6..6f1f944334 100644 --- a/slither/detectors/all_detectors.py +++ b/slither/detectors/all_detectors.py @@ -97,5 +97,6 @@ from .statements.tautological_compare import TautologicalCompare from .statements.return_bomb import ReturnBomb from .functions.out_of_order_retryable import OutOfOrderRetryable +from .functions.pyth_deprecated_functions import PythDeprecatedFunctions # from .statements.unused_import import UnusedImport diff --git a/slither/detectors/functions/pyth_deprecated_functions.py b/slither/detectors/functions/pyth_deprecated_functions.py new file mode 100644 index 0000000000..87cff9181b --- /dev/null +++ b/slither/detectors/functions/pyth_deprecated_functions.py @@ -0,0 +1,73 @@ +from typing import List + +from slither.detectors.abstract_detector import ( + AbstractDetector, + DetectorClassification, + DETECTOR_INFO, +) +from slither.utils.output import Output + + +class PythDeprecatedFunctions(AbstractDetector): + """ + Documentation: This detector finds deprecated Pyth function calls + """ + + ARGUMENT = "pyth-deprecated-functions" + HELP = "Detect Pyth deprecated functions" + IMPACT = DetectorClassification.MEDIUM + CONFIDENCE = DetectorClassification.HIGH + + WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#pyth-deprecated-functions" + WIKI_TITLE = "Pyth deprecated functions" + WIKI_DESCRIPTION = "Detect when a Pyth deprecated function is used" + WIKI_RECOMMENDATION = ( + "Do not use deprecated Pyth functions. Visit https://api-reference.pyth.network/." + ) + + WIKI_EXPLOIT_SCENARIO = """ +```solidity +import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; +import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol"; + +contract C { + + IPyth pyth; + + constructor(IPyth _pyth) { + pyth = _pyth; + } + + function A(bytes32 priceId) public { + PythStructs.Price memory price = pyth.getPrice(priceId); + ... + } +} +``` +The function `A` uses the deprecated `getPrice` Pyth function. +""" + + def _detect(self): + DEPRECATED_PYTH_FUNCTIONS = [ + "getValidTimePeriod", + "getEmaPrice", + "getPrice", + ] + results: List[Output] = [] + + for contract in self.compilation_unit.contracts_derived: + for target_contract, ir in contract.all_high_level_calls: + if ( + target_contract.name == "IPyth" + and ir.function_name in DEPRECATED_PYTH_FUNCTIONS + ): + info: DETECTOR_INFO = [ + "The following Pyth deprecated function is used\n\t- ", + ir.node, + "\n", + ] + + res = self.generate_result(info) + results.append(res) + + return results diff --git a/tests/e2e/detectors/snapshots/detectors__detector_PythDeprecatedFunctions_0_8_20_pyth_deprecated_functions_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_PythDeprecatedFunctions_0_8_20_pyth_deprecated_functions_sol__0.txt new file mode 100644 index 0000000000..4cc23d2138 --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_PythDeprecatedFunctions_0_8_20_pyth_deprecated_functions_sol__0.txt @@ -0,0 +1,3 @@ +The following Pyth deprecated function is used + - price = pyth.getPrice(priceId) (tests/e2e/detectors/test_data/pyth-deprecated-functions/0.8.20/pyth_deprecated_functions.sol#23) + diff --git a/tests/e2e/detectors/test_data/pyth-deprecated-functions/0.8.20/pyth_deprecated_functions.sol b/tests/e2e/detectors/test_data/pyth-deprecated-functions/0.8.20/pyth_deprecated_functions.sol new file mode 100644 index 0000000000..dc8130db59 --- /dev/null +++ b/tests/e2e/detectors/test_data/pyth-deprecated-functions/0.8.20/pyth_deprecated_functions.sol @@ -0,0 +1,35 @@ + +// Fake Pyth interface +interface IPyth { + function getPrice(bytes32 id) external returns (uint256 price); + function notDeprecated(bytes32 id) external returns (uint256 price); +} + +interface INotPyth { + function getPrice(bytes32 id) external returns (uint256 price); +} + +contract C { + + IPyth pyth; + INotPyth notPyth; + + constructor(IPyth _pyth, INotPyth _notPyth) { + pyth = _pyth; + notPyth = _notPyth; + } + + function Deprecated(bytes32 priceId) public { + uint256 price = pyth.getPrice(priceId); + } + + function notDeprecated(bytes32 priceId) public { + uint256 price = pyth.notDeprecated(priceId); + } + + function notPythCall(bytes32 priceId) public { + uint256 price = notPyth.getPrice(priceId); + } + + +} diff --git a/tests/e2e/detectors/test_data/pyth-deprecated-functions/0.8.20/pyth_deprecated_functions.sol-0.8.20.zip b/tests/e2e/detectors/test_data/pyth-deprecated-functions/0.8.20/pyth_deprecated_functions.sol-0.8.20.zip new file mode 100644 index 0000000000000000000000000000000000000000..258a28c938effe4b95c819b04d6e7629b1eb21d7 GIT binary patch literal 4367 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 literal 0 HcmV?d00001 diff --git a/tests/e2e/detectors/test_detectors.py b/tests/e2e/detectors/test_detectors.py index 2c6a5f55a3..e30dc962d7 100644 --- a/tests/e2e/detectors/test_detectors.py +++ b/tests/e2e/detectors/test_detectors.py @@ -1714,6 +1714,11 @@ def id_test(test_item: Test): "out_of_order_retryable.sol", "0.8.20", ), + Test( + all_detectors.PythDeprecatedFunctions, + "pyth_deprecated_functions.sol", + "0.8.20", + ), # Test( # all_detectors.UnusedImport, # "ConstantContractLevelUsedInContractTest.sol", From f25344c6a29a5dfd501f00a474fb4b9c520fc11a Mon Sep 17 00:00:00 2001 From: Simone Date: Tue, 8 Oct 2024 18:32:11 +0200 Subject: [PATCH 52/71] Add Pyth unchecked confidence detector --- slither/detectors/all_detectors.py | 2 + .../detectors/statements/pyth_unchecked.py | 79 +++++++ .../statements/pyth_unchecked_confidence.py | 50 +++++ ..._8_20_pyth_unchecked_confidence_sol__0.txt | 3 + .../0.8.20/pyth_unchecked_confidence.sol | 193 ++++++++++++++++++ .../pyth_unchecked_confidence.sol-0.8.20.zip | Bin 0 -> 10811 bytes tests/e2e/detectors/test_detectors.py | 10 + 7 files changed, 337 insertions(+) create mode 100644 slither/detectors/statements/pyth_unchecked.py create mode 100644 slither/detectors/statements/pyth_unchecked_confidence.py create mode 100644 tests/e2e/detectors/snapshots/detectors__detector_PythUncheckedConfidence_0_8_20_pyth_unchecked_confidence_sol__0.txt create mode 100644 tests/e2e/detectors/test_data/pyth-unchecked-confidence/0.8.20/pyth_unchecked_confidence.sol create mode 100644 tests/e2e/detectors/test_data/pyth-unchecked-confidence/0.8.20/pyth_unchecked_confidence.sol-0.8.20.zip diff --git a/slither/detectors/all_detectors.py b/slither/detectors/all_detectors.py index 44a168c2b6..75e838e53a 100644 --- a/slither/detectors/all_detectors.py +++ b/slither/detectors/all_detectors.py @@ -97,5 +97,7 @@ from .statements.tautological_compare import TautologicalCompare from .statements.return_bomb import ReturnBomb from .functions.out_of_order_retryable import OutOfOrderRetryable +from .statements.pyth_unchecked_confidence import PythUncheckedConfidence +from .statements.pyth_unchecked_publishtime import PythUncheckedPublishTime # from .statements.unused_import import UnusedImport diff --git a/slither/detectors/statements/pyth_unchecked.py b/slither/detectors/statements/pyth_unchecked.py new file mode 100644 index 0000000000..959aee6a55 --- /dev/null +++ b/slither/detectors/statements/pyth_unchecked.py @@ -0,0 +1,79 @@ +from typing import List + +from slither.detectors.abstract_detector import ( + AbstractDetector, + DETECTOR_INFO, +) +from slither.utils.output import Output +from slither.slithir.operations import Member, Binary, Assignment + + +class PythUnchecked(AbstractDetector): + """ + Documentation: This detector finds deprecated Pyth function calls + """ + + # To be overriden in the derived class + PYTH_FUNCTIONS = [] + PYTH_FIELD = "" + + # pylint: disable=too-many-nested-blocks + def _detect(self) -> List[Output]: + results: List[Output] = [] + + for contract in self.compilation_unit.contracts_derived: + for target_contract, ir in contract.all_high_level_calls: + if target_contract.name == "IPyth" and ir.function_name in self.PYTH_FUNCTIONS: + # We know for sure the second IR in the node is an Assignment operation of the TMP variable. Example: + # Expression: price = pyth.getEmaPriceNoOlderThan(id,age) + # IRs: + # TMP_0(PythStructs.Price) = HIGH_LEVEL_CALL, dest:pyth(IPyth), function:getEmaPriceNoOlderThan, arguments:['id', 'age'] + # price(PythStructs.Price) := TMP_0(PythStructs.Price) + assert isinstance(ir.node.irs[1], Assignment) + return_variable = ir.node.irs[1].lvalue + checked = False + + possible_unchecked_variable_ir = None + nodes = ir.node.sons + visited = set() + while nodes: + if checked: + break + next_node = nodes[0] + nodes = nodes[1:] + + for node_ir in next_node.all_slithir_operations(): + # We are accessing the unchecked_var field of the returned Price struct + if ( + isinstance(node_ir, Member) + and node_ir.variable_left == return_variable + and node_ir.variable_right.name == self.PYTH_FIELD + ): + possible_unchecked_variable_ir = node_ir.lvalue + # We assume that if unchecked_var happens to be inside a binary operation is checked + if ( + isinstance(node_ir, Binary) + and possible_unchecked_variable_ir is not None + and possible_unchecked_variable_ir in node_ir.read + ): + checked = True + break + + if next_node not in visited: + visited.add(next_node) + for son in next_node.sons: + if son not in visited: + nodes.append(son) + + if not checked: + info: DETECTOR_INFO = [ + f"Pyth price {self.PYTH_FIELD} field is not checked in ", + ir.node.function, + "\n\t- ", + ir.node, + "\n", + ] + res = self.generate_result(info) + results.append(res) + + return results diff --git a/slither/detectors/statements/pyth_unchecked_confidence.py b/slither/detectors/statements/pyth_unchecked_confidence.py new file mode 100644 index 0000000000..2e99851a85 --- /dev/null +++ b/slither/detectors/statements/pyth_unchecked_confidence.py @@ -0,0 +1,50 @@ +from slither.detectors.abstract_detector import DetectorClassification +from slither.detectors.statements.pyth_unchecked import PythUnchecked + + +class PythUncheckedConfidence(PythUnchecked): + """ + Documentation: This detector finds when the confidence level of a Pyth price is not checked + """ + + ARGUMENT = "pyth-unchecked-confidence" + HELP = "Detect when the confidence level of a Pyth price is not checked" + IMPACT = DetectorClassification.MEDIUM + CONFIDENCE = DetectorClassification.HIGH + + WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#pyth-unchecked-confidence" + WIKI_TITLE = "Pyth unchecked confidence level" + WIKI_DESCRIPTION = "Detect when the confidence level of a Pyth price is not checked" + WIKI_RECOMMENDATION = "Check the confidence level of a Pyth price. Visit https://docs.pyth.network/price-feeds/best-practices#confidence-intervals for more information." + + WIKI_EXPLOIT_SCENARIO = """ +```solidity +import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; +import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol"; + +contract C { + IPyth pyth; + + constructor(IPyth _pyth) { + pyth = _pyth; + } + + function bad(bytes32 id, uint256 age) public { + PythStructs.Price memory price = pyth.getEmaPriceNoOlderThan(id, age); + // Use price + } +} +``` +The function `A` uses the price without checking its confidence level. +""" + + PYTH_FUNCTIONS = [ + "getEmaPrice", + "getEmaPriceNoOlderThan", + "getEmaPriceUnsafe", + "getPrice", + "getPriceNoOlderThan", + "getPriceUnsafe", + ] + + PYTH_FIELD = "conf" diff --git a/tests/e2e/detectors/snapshots/detectors__detector_PythUncheckedConfidence_0_8_20_pyth_unchecked_confidence_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_PythUncheckedConfidence_0_8_20_pyth_unchecked_confidence_sol__0.txt new file mode 100644 index 0000000000..ae0dc2ae2f --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_PythUncheckedConfidence_0_8_20_pyth_unchecked_confidence_sol__0.txt @@ -0,0 +1,3 @@ +Pyth price conf field is not checked in C.bad(bytes32,uint256) (tests/e2e/detectors/test_data/pyth-unchecked-confidence/0.8.20/pyth_unchecked_confidence.sol#171-175) + - price = pyth.getEmaPriceNoOlderThan(id,age) (tests/e2e/detectors/test_data/pyth-unchecked-confidence/0.8.20/pyth_unchecked_confidence.sol#172) + diff --git a/tests/e2e/detectors/test_data/pyth-unchecked-confidence/0.8.20/pyth_unchecked_confidence.sol b/tests/e2e/detectors/test_data/pyth-unchecked-confidence/0.8.20/pyth_unchecked_confidence.sol new file mode 100644 index 0000000000..58880c382d --- /dev/null +++ b/tests/e2e/detectors/test_data/pyth-unchecked-confidence/0.8.20/pyth_unchecked_confidence.sol @@ -0,0 +1,193 @@ +contract PythStructs { + // A price with a degree of uncertainty, represented as a price +- a confidence interval. + // + // The confidence interval roughly corresponds to the standard error of a normal distribution. + // Both the price and confidence are stored in a fixed-point numeric representation, + // `x * (10^expo)`, where `expo` is the exponent. + // + // Please refer to the documentation at https://docs.pyth.network/consumers/best-practices for how + // to how this price safely. + struct Price { + // Price + int64 price; + // Confidence interval around the price + uint64 conf; + // Price exponent + int32 expo; + // Unix timestamp describing when the price was published + uint publishTime; + } + + // PriceFeed represents a current aggregate price from pyth publisher feeds. + struct PriceFeed { + // The price ID. + bytes32 id; + // Latest available price + Price price; + // Latest available exponentially-weighted moving average price + Price emaPrice; + } +} + +interface IPyth { + /// @notice Returns the period (in seconds) that a price feed is considered valid since its publish time + function getValidTimePeriod() external view returns (uint validTimePeriod); + + /// @notice Returns the price and confidence interval. + /// @dev Reverts if the price has not been updated within the last `getValidTimePeriod()` seconds. + /// @param id The Pyth Price Feed ID of which to fetch the price and confidence interval. + /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. + function getPrice( + bytes32 id + ) external view returns (PythStructs.Price memory price); + + /// @notice Returns the exponentially-weighted moving average price and confidence interval. + /// @dev Reverts if the EMA price is not available. + /// @param id The Pyth Price Feed ID of which to fetch the EMA price and confidence interval. + /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. + function getEmaPrice( + bytes32 id + ) external view returns (PythStructs.Price memory price); + + /// @notice Returns the price of a price feed without any sanity checks. + /// @dev This function returns the most recent price update in this contract without any recency checks. + /// This function is unsafe as the returned price update may be arbitrarily far in the past. + /// + /// Users of this function should check the `publishTime` in the price to ensure that the returned price is + /// sufficiently recent for their application. If you are considering using this function, it may be + /// safer / easier to use either `getPrice` or `getPriceNoOlderThan`. + /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. + function getPriceUnsafe( + bytes32 id + ) external view returns (PythStructs.Price memory price); + + /// @notice Returns the price that is no older than `age` seconds of the current time. + /// @dev This function is a sanity-checked version of `getPriceUnsafe` which is useful in + /// applications that require a sufficiently-recent price. Reverts if the price wasn't updated sufficiently + /// recently. + /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. + function getPriceNoOlderThan( + bytes32 id, + uint age + ) external view returns (PythStructs.Price memory price); + + /// @notice Returns the exponentially-weighted moving average price of a price feed without any sanity checks. + /// @dev This function returns the same price as `getEmaPrice` in the case where the price is available. + /// However, if the price is not recent this function returns the latest available price. + /// + /// The returned price can be from arbitrarily far in the past; this function makes no guarantees that + /// the returned price is recent or useful for any particular application. + /// + /// Users of this function should check the `publishTime` in the price to ensure that the returned price is + /// sufficiently recent for their application. If you are considering using this function, it may be + /// safer / easier to use either `getEmaPrice` or `getEmaPriceNoOlderThan`. + /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. + function getEmaPriceUnsafe( + bytes32 id + ) external view returns (PythStructs.Price memory price); + + /// @notice Returns the exponentially-weighted moving average price that is no older than `age` seconds + /// of the current time. + /// @dev This function is a sanity-checked version of `getEmaPriceUnsafe` which is useful in + /// applications that require a sufficiently-recent price. Reverts if the price wasn't updated sufficiently + /// recently. + /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. + function getEmaPriceNoOlderThan( + bytes32 id, + uint age + ) external view returns (PythStructs.Price memory price); + + /// @notice Update price feeds with given update messages. + /// This method requires the caller to pay a fee in wei; the required fee can be computed by calling + /// `getUpdateFee` with the length of the `updateData` array. + /// Prices will be updated if they are more recent than the current stored prices. + /// The call will succeed even if the update is not the most recent. + /// @dev Reverts if the transferred fee is not sufficient or the updateData is invalid. + /// @param updateData Array of price update data. + function updatePriceFeeds(bytes[] calldata updateData) external payable; + + /// @notice Wrapper around updatePriceFeeds that rejects fast if a price update is not necessary. A price update is + /// necessary if the current on-chain publishTime is older than the given publishTime. It relies solely on the + /// given `publishTimes` for the price feeds and does not read the actual price update publish time within `updateData`. + /// + /// This method requires the caller to pay a fee in wei; the required fee can be computed by calling + /// `getUpdateFee` with the length of the `updateData` array. + /// + /// `priceIds` and `publishTimes` are two arrays with the same size that correspond to senders known publishTime + /// of each priceId when calling this method. If all of price feeds within `priceIds` have updated and have + /// a newer or equal publish time than the given publish time, it will reject the transaction to save gas. + /// Otherwise, it calls updatePriceFeeds method to update the prices. + /// + /// @dev Reverts if update is not needed or the transferred fee is not sufficient or the updateData is invalid. + /// @param updateData Array of price update data. + /// @param priceIds Array of price ids. + /// @param publishTimes Array of publishTimes. `publishTimes[i]` corresponds to known `publishTime` of `priceIds[i]` + function updatePriceFeedsIfNecessary( + bytes[] calldata updateData, + bytes32[] calldata priceIds, + uint64[] calldata publishTimes + ) external payable; + + /// @notice Returns the required fee to update an array of price updates. + /// @param updateData Array of price update data. + /// @return feeAmount The required fee in Wei. + function getUpdateFee( + bytes[] calldata updateData + ) external view returns (uint feeAmount); + + /// @notice Parse `updateData` and return price feeds of the given `priceIds` if they are all published + /// within `minPublishTime` and `maxPublishTime`. + /// + /// You can use this method if you want to use a Pyth price at a fixed time and not the most recent price; + /// otherwise, please consider using `updatePriceFeeds`. This method does not store the price updates on-chain. + /// + /// This method requires the caller to pay a fee in wei; the required fee can be computed by calling + /// `getUpdateFee` with the length of the `updateData` array. + /// + /// + /// @dev Reverts if the transferred fee is not sufficient or the updateData is invalid or there is + /// no update for any of the given `priceIds` within the given time range. + /// @param updateData Array of price update data. + /// @param priceIds Array of price ids. + /// @param minPublishTime minimum acceptable publishTime for the given `priceIds`. + /// @param maxPublishTime maximum acceptable publishTime for the given `priceIds`. + /// @return priceFeeds Array of the price feeds corresponding to the given `priceIds` (with the same order). + function parsePriceFeedUpdates( + bytes[] calldata updateData, + bytes32[] calldata priceIds, + uint64 minPublishTime, + uint64 maxPublishTime + ) external payable returns (PythStructs.PriceFeed[] memory priceFeeds); +} + + +contract C { + IPyth pyth; + + constructor(IPyth _pyth) { + pyth = _pyth; + } + + function bad(bytes32 id, uint256 age) public { + PythStructs.Price memory price = pyth.getEmaPriceNoOlderThan(id, age); + require(price.publishTime > block.timestamp - 120); + // Use price + } + + function good(bytes32 id, uint256 age) public { + PythStructs.Price memory price = pyth.getEmaPriceNoOlderThan(id, age); + require(price.conf < 10000); + require(price.publishTime > block.timestamp - 120); + // Use price + } + + function good2(bytes32 id, uint256 age) public { + PythStructs.Price memory price = pyth.getEmaPriceNoOlderThan(id, age); + require(price.publishTime > block.timestamp - 120); + if (price.conf >= 10000) { + revert(); + } + // Use price + } + +} \ No newline at end of file diff --git a/tests/e2e/detectors/test_data/pyth-unchecked-confidence/0.8.20/pyth_unchecked_confidence.sol-0.8.20.zip b/tests/e2e/detectors/test_data/pyth-unchecked-confidence/0.8.20/pyth_unchecked_confidence.sol-0.8.20.zip new file mode 100644 index 0000000000000000000000000000000000000000..6e5fa1b9f1b470d9ad9ca7632115fd7f49e67ff9 GIT binary patch literal 10811 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 WvXD^!J%awHhX2{o{}53SkpBS-=ILPo literal 0 HcmV?d00001 diff --git a/tests/e2e/detectors/test_detectors.py b/tests/e2e/detectors/test_detectors.py index 2c6a5f55a3..5efd26153a 100644 --- a/tests/e2e/detectors/test_detectors.py +++ b/tests/e2e/detectors/test_detectors.py @@ -1714,6 +1714,16 @@ def id_test(test_item: Test): "out_of_order_retryable.sol", "0.8.20", ), + Test( + all_detectors.PythUncheckedConfidence, + "pyth_unchecked_confidence.sol", + "0.8.20", + ), + Test( + all_detectors.PythUncheckedPublishTime, + "pyth_unchecked_publishtime.sol", + "0.8.20", + ), # Test( # all_detectors.UnusedImport, # "ConstantContractLevelUsedInContractTest.sol", From 7e54d41faae06a5048b782845ddd97297cd67fa2 Mon Sep 17 00:00:00 2001 From: Simone Date: Tue, 8 Oct 2024 18:32:29 +0200 Subject: [PATCH 53/71] Add Pyth unchecked publishTime detector --- .../statements/pyth_unchecked_publishtime.py | 52 +++++ ...8_20_pyth_unchecked_publishtime_sol__0.txt | 3 + .../0.8.20/pyth_unchecked_publishtime.sol | 193 ++++++++++++++++++ .../pyth_unchecked_publishtime.sol-0.8.20.zip | Bin 0 -> 10531 bytes 4 files changed, 248 insertions(+) create mode 100644 slither/detectors/statements/pyth_unchecked_publishtime.py create mode 100644 tests/e2e/detectors/snapshots/detectors__detector_PythUncheckedPublishTime_0_8_20_pyth_unchecked_publishtime_sol__0.txt create mode 100644 tests/e2e/detectors/test_data/pyth-unchecked-publishtime/0.8.20/pyth_unchecked_publishtime.sol create mode 100644 tests/e2e/detectors/test_data/pyth-unchecked-publishtime/0.8.20/pyth_unchecked_publishtime.sol-0.8.20.zip diff --git a/slither/detectors/statements/pyth_unchecked_publishtime.py b/slither/detectors/statements/pyth_unchecked_publishtime.py new file mode 100644 index 0000000000..e3e2010d67 --- /dev/null +++ b/slither/detectors/statements/pyth_unchecked_publishtime.py @@ -0,0 +1,52 @@ +from slither.detectors.abstract_detector import DetectorClassification +from slither.detectors.statements.pyth_unchecked import PythUnchecked + + +class PythUncheckedPublishTime(PythUnchecked): + """ + Documentation: This detector finds when the publishTime of a Pyth price is not checked + """ + + ARGUMENT = "pyth-unchecked-publishtime" + HELP = "Detect when the publishTime of a Pyth price is not checked" + IMPACT = DetectorClassification.MEDIUM + CONFIDENCE = DetectorClassification.HIGH + + WIKI = ( + "https://github.com/crytic/slither/wiki/Detector-Documentation#pyth-unchecked-publishtime" + ) + WIKI_TITLE = "Pyth unchecked publishTime" + WIKI_DESCRIPTION = "Detect when the publishTime of a Pyth price is not checked" + WIKI_RECOMMENDATION = "Check the publishTime of a Pyth price." + + WIKI_EXPLOIT_SCENARIO = """ +```solidity +import "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; +import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol"; + +contract C { + IPyth pyth; + + constructor(IPyth _pyth) { + pyth = _pyth; + } + + function bad(bytes32 id) public { + PythStructs.Price memory price = pyth.getEmaPriceUnsafe(id); + // Use price + } +} +``` +The function `A` uses the price without checking its `publishTime` coming from the `getEmaPriceUnsafe` function. +""" + + PYTH_FUNCTIONS = [ + "getEmaPrice", + # "getEmaPriceNoOlderThan", + "getEmaPriceUnsafe", + "getPrice", + # "getPriceNoOlderThan", + "getPriceUnsafe", + ] + + PYTH_FIELD = "publishTime" diff --git a/tests/e2e/detectors/snapshots/detectors__detector_PythUncheckedPublishTime_0_8_20_pyth_unchecked_publishtime_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_PythUncheckedPublishTime_0_8_20_pyth_unchecked_publishtime_sol__0.txt new file mode 100644 index 0000000000..cb331c8d53 --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_PythUncheckedPublishTime_0_8_20_pyth_unchecked_publishtime_sol__0.txt @@ -0,0 +1,3 @@ +Pyth price publishTime field is not checked in C.bad(bytes32) (tests/e2e/detectors/test_data/pyth-unchecked-publishtime/0.8.20/pyth_unchecked_publishtime.sol#171-175) + - price = pyth.getEmaPriceUnsafe(id) (tests/e2e/detectors/test_data/pyth-unchecked-publishtime/0.8.20/pyth_unchecked_publishtime.sol#172) + diff --git a/tests/e2e/detectors/test_data/pyth-unchecked-publishtime/0.8.20/pyth_unchecked_publishtime.sol b/tests/e2e/detectors/test_data/pyth-unchecked-publishtime/0.8.20/pyth_unchecked_publishtime.sol new file mode 100644 index 0000000000..74ab10fe31 --- /dev/null +++ b/tests/e2e/detectors/test_data/pyth-unchecked-publishtime/0.8.20/pyth_unchecked_publishtime.sol @@ -0,0 +1,193 @@ +contract PythStructs { + // A price with a degree of uncertainty, represented as a price +- a confidence interval. + // + // The confidence interval roughly corresponds to the standard error of a normal distribution. + // Both the price and confidence are stored in a fixed-point numeric representation, + // `x * (10^expo)`, where `expo` is the exponent. + // + // Please refer to the documentation at https://docs.pyth.network/consumers/best-practices for how + // to how this price safely. + struct Price { + // Price + int64 price; + // Confidence interval around the price + uint64 conf; + // Price exponent + int32 expo; + // Unix timestamp describing when the price was published + uint publishTime; + } + + // PriceFeed represents a current aggregate price from pyth publisher feeds. + struct PriceFeed { + // The price ID. + bytes32 id; + // Latest available price + Price price; + // Latest available exponentially-weighted moving average price + Price emaPrice; + } +} + +interface IPyth { + /// @notice Returns the period (in seconds) that a price feed is considered valid since its publish time + function getValidTimePeriod() external view returns (uint validTimePeriod); + + /// @notice Returns the price and confidence interval. + /// @dev Reverts if the price has not been updated within the last `getValidTimePeriod()` seconds. + /// @param id The Pyth Price Feed ID of which to fetch the price and confidence interval. + /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. + function getPrice( + bytes32 id + ) external view returns (PythStructs.Price memory price); + + /// @notice Returns the exponentially-weighted moving average price and confidence interval. + /// @dev Reverts if the EMA price is not available. + /// @param id The Pyth Price Feed ID of which to fetch the EMA price and confidence interval. + /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. + function getEmaPrice( + bytes32 id + ) external view returns (PythStructs.Price memory price); + + /// @notice Returns the price of a price feed without any sanity checks. + /// @dev This function returns the most recent price update in this contract without any recency checks. + /// This function is unsafe as the returned price update may be arbitrarily far in the past. + /// + /// Users of this function should check the `publishTime` in the price to ensure that the returned price is + /// sufficiently recent for their application. If you are considering using this function, it may be + /// safer / easier to use either `getPrice` or `getPriceNoOlderThan`. + /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. + function getPriceUnsafe( + bytes32 id + ) external view returns (PythStructs.Price memory price); + + /// @notice Returns the price that is no older than `age` seconds of the current time. + /// @dev This function is a sanity-checked version of `getPriceUnsafe` which is useful in + /// applications that require a sufficiently-recent price. Reverts if the price wasn't updated sufficiently + /// recently. + /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. + function getPriceNoOlderThan( + bytes32 id, + uint age + ) external view returns (PythStructs.Price memory price); + + /// @notice Returns the exponentially-weighted moving average price of a price feed without any sanity checks. + /// @dev This function returns the same price as `getEmaPrice` in the case where the price is available. + /// However, if the price is not recent this function returns the latest available price. + /// + /// The returned price can be from arbitrarily far in the past; this function makes no guarantees that + /// the returned price is recent or useful for any particular application. + /// + /// Users of this function should check the `publishTime` in the price to ensure that the returned price is + /// sufficiently recent for their application. If you are considering using this function, it may be + /// safer / easier to use either `getEmaPrice` or `getEmaPriceNoOlderThan`. + /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. + function getEmaPriceUnsafe( + bytes32 id + ) external view returns (PythStructs.Price memory price); + + /// @notice Returns the exponentially-weighted moving average price that is no older than `age` seconds + /// of the current time. + /// @dev This function is a sanity-checked version of `getEmaPriceUnsafe` which is useful in + /// applications that require a sufficiently-recent price. Reverts if the price wasn't updated sufficiently + /// recently. + /// @return price - please read the documentation of PythStructs.Price to understand how to use this safely. + function getEmaPriceNoOlderThan( + bytes32 id, + uint age + ) external view returns (PythStructs.Price memory price); + + /// @notice Update price feeds with given update messages. + /// This method requires the caller to pay a fee in wei; the required fee can be computed by calling + /// `getUpdateFee` with the length of the `updateData` array. + /// Prices will be updated if they are more recent than the current stored prices. + /// The call will succeed even if the update is not the most recent. + /// @dev Reverts if the transferred fee is not sufficient or the updateData is invalid. + /// @param updateData Array of price update data. + function updatePriceFeeds(bytes[] calldata updateData) external payable; + + /// @notice Wrapper around updatePriceFeeds that rejects fast if a price update is not necessary. A price update is + /// necessary if the current on-chain publishTime is older than the given publishTime. It relies solely on the + /// given `publishTimes` for the price feeds and does not read the actual price update publish time within `updateData`. + /// + /// This method requires the caller to pay a fee in wei; the required fee can be computed by calling + /// `getUpdateFee` with the length of the `updateData` array. + /// + /// `priceIds` and `publishTimes` are two arrays with the same size that correspond to senders known publishTime + /// of each priceId when calling this method. If all of price feeds within `priceIds` have updated and have + /// a newer or equal publish time than the given publish time, it will reject the transaction to save gas. + /// Otherwise, it calls updatePriceFeeds method to update the prices. + /// + /// @dev Reverts if update is not needed or the transferred fee is not sufficient or the updateData is invalid. + /// @param updateData Array of price update data. + /// @param priceIds Array of price ids. + /// @param publishTimes Array of publishTimes. `publishTimes[i]` corresponds to known `publishTime` of `priceIds[i]` + function updatePriceFeedsIfNecessary( + bytes[] calldata updateData, + bytes32[] calldata priceIds, + uint64[] calldata publishTimes + ) external payable; + + /// @notice Returns the required fee to update an array of price updates. + /// @param updateData Array of price update data. + /// @return feeAmount The required fee in Wei. + function getUpdateFee( + bytes[] calldata updateData + ) external view returns (uint feeAmount); + + /// @notice Parse `updateData` and return price feeds of the given `priceIds` if they are all published + /// within `minPublishTime` and `maxPublishTime`. + /// + /// You can use this method if you want to use a Pyth price at a fixed time and not the most recent price; + /// otherwise, please consider using `updatePriceFeeds`. This method does not store the price updates on-chain. + /// + /// This method requires the caller to pay a fee in wei; the required fee can be computed by calling + /// `getUpdateFee` with the length of the `updateData` array. + /// + /// + /// @dev Reverts if the transferred fee is not sufficient or the updateData is invalid or there is + /// no update for any of the given `priceIds` within the given time range. + /// @param updateData Array of price update data. + /// @param priceIds Array of price ids. + /// @param minPublishTime minimum acceptable publishTime for the given `priceIds`. + /// @param maxPublishTime maximum acceptable publishTime for the given `priceIds`. + /// @return priceFeeds Array of the price feeds corresponding to the given `priceIds` (with the same order). + function parsePriceFeedUpdates( + bytes[] calldata updateData, + bytes32[] calldata priceIds, + uint64 minPublishTime, + uint64 maxPublishTime + ) external payable returns (PythStructs.PriceFeed[] memory priceFeeds); +} + + +contract C { + IPyth pyth; + + constructor(IPyth _pyth) { + pyth = _pyth; + } + + function bad(bytes32 id) public { + PythStructs.Price memory price = pyth.getEmaPriceUnsafe(id); + require(price.conf < 10000); + // Use price + } + + function good(bytes32 id) public { + PythStructs.Price memory price = pyth.getEmaPriceUnsafe(id); + require(price.publishTime > block.timestamp - 120); + require(price.conf < 10000); + // Use price + } + + function good2(bytes32 id) public { + PythStructs.Price memory price = pyth.getEmaPriceUnsafe(id); + require(price.conf < 10000); + if (price.publishTime <= block.timestamp - 120) { + revert(); + } + // Use price + } + +} \ No newline at end of file diff --git a/tests/e2e/detectors/test_data/pyth-unchecked-publishtime/0.8.20/pyth_unchecked_publishtime.sol-0.8.20.zip b/tests/e2e/detectors/test_data/pyth-unchecked-publishtime/0.8.20/pyth_unchecked_publishtime.sol-0.8.20.zip new file mode 100644 index 0000000000000000000000000000000000000000..178b65b38891d69b198dce4828330e4d726101e8 GIT binary patch literal 10531 zcmb7~LwGIm>cL=o7U7TG#5F=bJZ^u0RnLukmc)6iXHdRLPm?S|T3>Q@%&~*&ogPv| zyJ8rGPge&t%5%NNtE&4UF35~!TEsw)N3ysv-z%LAWUKDBJBocnNY9T`XiU)+@x}FA z`7D$$3TUi(E(nH*aAUFBlQw2?w(uJn9!MBdim$_cf^a5z(x2{hi1rytPA2>EHIXvd zfj(DpuO=aSK2&z`;X_XXVzH@9qD_Ma$yh7zapN*RY0~Ke^XL_##L)LcjJ&RMF{v#1 zoG#P&FC8~0TllxASxhGT_rurO+(JMOwQ?3BfnCf9&VyqH4UtfQBKc_b|0A4 z5RgdTrrvNm{Xv+#wBvNip_lhZsn_pW|M2|Gcng6*jH&!)R(DmB0})_}<)HRFs30ce z_UzbazLZ`afL)YR#foVtOUFHHLx#qwljp*qoH)+`HON+q47O5Pq`$pDy7!`0$DgI{ zY(9hnz|9#;`94@B1=LC}&4l4vyKFTnFPeoG^{C!5Z&o2%^6}a3UV3$(Q&+I;k`{M& zEFJcHCFKp|jNp#StE$YCweVb4gR3Y_9&St1(WY}+STj;V$r&Zb@c|q;p6|9$*a{Y` zzpA97YZOg>()hT_rtIv|4PN_MW|<0%1KD`gCbI~(UWf6Q<{cEMfJMA7^mfEWS_SAw zJGqI*BMo;I8~`w;#fX>yste8zzd^%u;usEGJtGGOF?H&+Uy-m(sPr(kb3R{E=cl(4 z3&|6oO@0jeK9ldkAZK6Y)5Y4b$#pX5CS+O$1VYb0eA*iWu$~FyiL>@{61gbHEZX)u zg_PxkcSCqd4`~zwbm63F&gxFs^%is8iP}5!I=&;8A^T>+Ce-7}Iu5ou_^8S)=6gm; zzQU{l1FFcL^5BtJTHjpJlPb)7&t@nOD6hp*9FT#)`z38ruxM{I;uwIK0|@=tu^{Sg zO3A@glpRf=Y>Yqhcq6S2!?`aMgakg*Wtp+c=bQQZ#KH8B%|>3sNuiZRA~Z(HUl~EV z54M#6j?B~kisU((_ss2yORfcYFi~P=W>HLU$%*CyoG!>>WO>pSDa$lCxj~r=@REJUU{#>NR9o8ji|kf?%{kib|J8`0GA~@7ji# zyVOR-(%dRtC)J_P(_3^;5wG;ENKaP=(?fw)54Di8 z?o5-0c)DmnNExeUzX9%tvrCyry$mJ^!%{WTB1ND9Qbx|pCeURlu&uW2cp;D0!U7a| z9F`ROu|?@Zimr*CUsYslIhG1GVb0tiNS8>_>LOa|fV_@TRwIqxv_x0B#!RuFtC#Pz z+cBGcHFNF`_ewcLZY&h(mzZ186B{*z#^&BoS+WVv>XiC+E`r{$4Pu5^bDN^+rj~z) zw}o6sOz7xN@x~2;J9}NQ+rnxiVymzDvoh7+Ake18m8Us#lIlpP zCc zKMDr{OjKT|8ZJ;k^t8>2&U1ekOzV-ZsS-&nt_Beq^s`>Sl5!l_4I|S_Oa6xIm*@Fw zQp>bOM)pfcb_f&xYUscoUTOgVUB3soK9m%J!HL;yV+lFh4sMk7?4~G$Lrc}nR;zII zc8i1LFB%Pi!*^CB1%DCEw*}GKVXH|o@1ypjW--rY{N~Na z5bI}LQ?>GuV;ERAv9o7xFll z2Pb8P7lA-%Hw-a_7moS6c`X90($snak#BdkY;vNsB4Y$>vU`v=nAxr>DPx@C`9L&o zlOONqy&jl&*OQr@T_)e$Yt(W3r|9jU0$14JoCu1uWYlO+ujBe+<$(wO8UOR$)2e^} zM`?m&s>`S41`NHq8S!qSOq6io@@Ck0f2vHzPOFA_JlbTu0sx1Mt@-S!%9bDVCh~<6 z`c2DU7iK{CMIMZBJKUiNu&}7r#b~Ly7NK?%Im=e=gP;6*XVJD!TK;@Na^~Gcg`i1N zSA0Q!cFBw}7tp0@ib80&DzgG-Nc1j}2m7lfJJyEZ$0G(JC#KR(M8^qu;ANWd8$sP* z?7+!2LfRH13v|T2TDATab-j|zjN3q)!Arf`stespD|KHI!;MxNaHh4hp%(zwt($pn z8s0Cl;#iI0c8$1ywagZgwB?w9T5nSnp5je~U`W<-$8cdAwqE{6^h#CFB=npqKUdL> zRbf7M!Zb{fpxxNx^iQ9Sgu8r;rVwV5cP5=9+1k@{p1gumR29S_&-*;N&F70OF z8PA0oMBI=k1^Leu&Q6?C8}`fnO(0Se?v&7}mmF3%f^D%jl3e0WNL6WljWHVcttOc=Vk|x$pCmh;&chtwlZWnO3u{uq5|;xd1SnK2l6_{ z(3Jkjl9ub#F_eL9A?HAEbPA{_b`jj)aV8m}MCkgT^`hhb!#_4FeoJMG^gTq=n}+Mj zB&?i~C{=m~2KiltcyA_v8`U2kmq(RO{oTelpXa7rHGE=*bs~S(HDe|qgc6@pj+uph zD-?7snrYnAVDY_UD1NzLiEBZrWWA;@fxO3{xB$S%#^OXl&(@Y+BT+nANYtWmuGd2z zUVBV1=Z+eLO=WK}REfv~y6_p^xUvtvxeB;|%+RC*XIQruMElNf#Ybpbp9iErK(`zD zR0_TINy@`HTaUDN^$CuB4%YgieDYKckCocmy}NNOSUMfLmXU3QxQ34)($r3->o43x zm%8kJ4Qu2_sOcumix#B)xppzVpI z4rX!?2DBxy3dosFpgI<=D^FzMCq+cG|1J_y3M9Z|Xw0^t)zmTAHTB|PBtX|>rM*MW zbE%=Wl(Gw)obf=3DE)&@FsjwyB!QsDFs0RgvR|N@19YXJ#_2+VeS%OQ(p6aRpK&YJ zh);(;3SH-D^OW{2(P7mb(t4)E`5b=|P#aUDWkhzaRtI;!n4Rwt^&aP#V~C3Op6RPD zlUi3{rdXX{@&qz<^~)Yu*m9+!!P7EuYYrTf{DyK)SOrWM)(#eb5o;$pyLZ#=MgH0} zGNcSG7sgraiybgq!&Jj86ni36xgZhyDPWRXqJCqh0%4X!tir9{!}Xc_kc?8(cFG5q zluN3D1=!(l7mAG{XeZ3{vNSafvj6SBmYJgA03-2H3fHvrkF}1~TD*y?KW8En-n~?N z-nnP^K)S#UY6FlJ|DsW>6vQ1NbN!8})Th@uA)Th+887B6I<7IUWH8B;;d*g)2f3m@ zGCgY;t6`9#7DXScc+z=-S;cF3Y{72NW?SdB4dYdetj@q7m%ZqN9*33I5&M;=1IKmz zfPcz|G%Y8mFd?*iq?f#;*#n(q!m%i$>HAjbM=hQ$3mMNk*j)@@*5K2Ko9QMI_7@an zp2|XSTz4l*ma$%H8XyE<?N(J`0${*>ouo#!zUiYIV!b z(w*A{EPAgoFwhz(nhal?SuP4Pl-Ar^8B>(Cq`#3!#Z?S%x=yI0Vl<5iRh2=P>7O)W zkEi7pCfveDpf;$5n;q%o;HGFO{rqT7=q{Qx^p5tCq93Sm(alZS(P_bm4w9bL1_C{K z7AZ&=rW$?~UclXL=3P+=EoGT!hM+P63-{)hb85JorlluBE$%P_-sT(J?On3BvUOWA zkFB02N6f~f*}8Hd&l=p_EPR(-rfy)T>U~;T2}|m(bO@oN8xc5bk+YnB5Myy~k4^J| ziV0liBc`#BHZKr0SJLgsDdxU%oyhN10WgpAZ6kcrGI9$x(lKBn#ds}Le#Y5QWP2gJ zAwXm#jP)O;2vCsg-tlTi8Qn$f}Cu% z9U;J{UJqw7Cn%F!yI~A|iofPuF{L^W$Ullx=l=`Xk5e?rF4)OYT_EAu)mTHvM-fI2 zO&JdDmSS=pbD-~C|I5scp)y#LPqr&Pu_=S)u*~tsa5b{VD7;FS4C6DkVKhd^z{Ul` z9Udl4QBjnnBepf&z8&y8uIdK=y<3sWWS*FelEp!zR=UZ72WS{ZB;poHyxJ~Z?VM33INS6!C_n|t1RvZM~@j&bm0Stw+ z$qFGtyND8jY?COW4+|4)PeFKHkWu5v6+( z6`Ou5vRz=7Oa?8#Q1ClkT7PM(KPZ5~OrTL)kb)PV>ms0`jjuXgtpKCfl6pLSX@}`a zWD!MqGm}@Dv>F+vAbLsVuDue%h;yFS{%D*5GU~(2syP;8;bs9tYbE|aMvi*9Ipb_d% zFZH^(qP1{KE95=q2RP7C`~cfS_02c-vOjjAX16|uM_PvFU81Ht^@KaVPQ01Yaus+Y zm)(u|M|Hy|98p0oA~R9=87 zeMM@e0ZL9D7=P9^xe61AL1>299CzDX!N(Jh#Nl)q4GRnf3no|Fq6{7B)`u2(#WKf^ z63Z?Fk|;tA4Z^s2BUXAk)%HPJjyh%MC|I5yn$%JJrM*5{8-FL`5gqj9yydg3>3gS- z2ww*SYN?O`GGWm)M1xQG;VY8Y<*0lJt>z)bG;8SvAGSvvcXLm!=;qU1~OYXtYkB#bY0Y4fl zTMSr$O0e{1PDJVIQN)CtGhuIG0So9w$2wlRyD`>9eX1itcIa3r7q+QVB(>2^U%{N<-U8QWom%3V4sg$iq*0>_+ zu4>Zd8c?o!j@}r>`6pbpyxQ{FP1{`bG(F=-WdEm9PN}q{-T~MyPJ^LCA}`3t+D6yZD^T0SXG}sO`ELT!XUHK?7fT>L?@%X(BMVbat+#k z_=P(^G^?KV&dhB;ej8Xqvrcs2z2NcGG32^2xA_Z$55cyVWX^N(bS`?@0xY zLX0}<5!Cf?Surt8CZ_m3yF<9C*nD`Yw^d`WvY2_8HgGLHVPuIlQ1A9XRvq#qiFy^^aqIte)E zz-ZnD-p~$d>noRBYQ%!h+_^I5N>>#*jZWM6JCZ%eH*Z#U5VZ?jJwMRX7%jY`8^lzU zI=1@5ruN>n2X>nDrR6bg0_|LPGu;1LCfp^pf#Pfx};JuPbp7&FkR2Ds0^iUgJMTfTA7*b?Eg zLprX8ijapFeu%1MLFcV~F>wqKTm6|D{qHvtajhit;M_T!bvV*`upx`~PY54V*>r2A zZVwSA%j8)}hYpyk)FsMXqoTL)(sK7rKu5^ueCbEZ>NWpPzm1JLt>c0L zB~w{qFK*rrR4IHOSee{@^>bI4ZY?U*07^0~w5@k%c}SllNduOH?e`3e{nMRHZHO4o zbo*RMS2P%&ozry9tS0k!R%#5~(f3$NhMxFjVd(XIhdVqYpyh@c;3n5LdsS20Ss^^T zjvaT{FV16GQM7s}o>{UmU|+S6Lip*raQP*t45#gVc2M**hoO zhm2Y0v?ck=reHmZ$CAFNMn}5XT$KPFJzhQOGhx4Lfl2p=t360>vNLz7YN}}6Z^DS_ zprI$@1eT=r0R2kTP65XAhQzrb`ur-UK8qtyz)1zi>alLH162G z679v|0apzaBPj}N<)p0`N#isW3ppGaSHgY%B)wi(8XE7i@Aj684v){fa>F{aTXe!b z;O(02mq;gH_)l<9q$o6GLaLBbyfteJQ9smsFR(VSQlzL=I~;ymgxua(d;<1pGOy=P z#7D|@9{RWp=Uv`IxkpF>R<+_@YffQeBiHCd7WOG107n#CT>YSVeO$Zj_;EIC2wWUTFopm`r-JlK!1Bun_Y#W7*>sx#L(s# z7&CzHOV4y4OIMS}s$IT1NJZ`rE&TP}w_t43X4nsAS7Aki#=((*IFZwb!i^EiuY936 z$DjJA)zZ7=ij#e|InS-{S?lSfBiJ*O>~Sr~$1NQLbilk*Kk2|0po+|n3gvQIBS#m3 zqhhS*;!nP7V-i#(Lp|oXB;9o*ymR8BG-$m2n$q~MS)iv9c2Y7BhE zN-9BNI75tsy_s|M1A#Tv8hmx|wAJ{nGDAL%CHDv?BZUu_y z%6NS}J?!xVN%`pz;VFe;Bkp|8QuUq@h1V5DPg&^A>6??9lSpz9Il3v&=ByXnxnk;H z%oft!1g@r|<&K!B*jWqJYwuD27dt(^pY&>cg8xBeUF;775afS6fOwDA*^X(hm4+m%4I=i??w zyYiwkH)n;sqGHxi&S>K84LFDUe>tZeyJ95P`3v!rcC>NMA{ z?2*X2sy7J{XL$@43}b0pYREdnS{*U5|L;4+mcm;f%iW8xQrBbR-#>mLSrU)hBz!j>Z5QR zauPkT?IlMkG%xU>=1-GQ%6tL?;=JOotL^lm4qJZ(nLIELDOKK)Us`AL6h;Oz-aQz6 zxDK4u&ahpMe(B-Ud_IKkRwjxg)?bWAX-sp+edV(@c*001PKACRB2@^^9(y7awEq!r zGqK-lzxtd>6Obm=sZ!L7?b!6;|JgXN#GW{Gnt}o@Y3Rmd*v z`u>^DJwGALq|31AaeoYLO9Q$Ms2EM^!Nk%r~4nxpT~uI9egTP$aOU2 z7ZhF+^-FF0^~_lxjAr5$^^UpGws4+Fm4qrt8=}P+jSx%tZwag-MP!Wgi5!8%O23n5n!&|Fy_>s<_~VS+vrTT5&H5|Fc)_i*U;MA~f5c zfE*LT@rPy2-5A-uDO&aOn~e{9Q)~8Y|5R{to`Sx(^k_+@tE&qL>uF9Z>%ZP{_RMqbcfjo4X0IN9XEou{KJ;X^L*!4wb5nXPuYMR`BMPt#D^zafS>x&jtMSs zi9$i5kJ^j?rLNvWYKrwz$@kJ~HoAG(1G>q3!IR3gKj;JEl1t|XX6Xf+-+2x3M{bTj z#fLZozPW!QV6bN<1UmrZ_gi5LhD%&?%9o~KPu>etBsFIdqx=kdwo@AFAl0}VLI5y8 z72WF;G+Y8))g2o_wNoTllUNVMDtT8(;N~ljE28Dpr>)U3Pfhj^sI`=z+!U$B@t1(i zkv2)d;c|$gG;{4`dL#~5Gv_%Zl6>VgG6kLV)3K8dm~by>4(A**pt!Av`**AM{Su{_ zl3_8P=?_a5WdhNOa?+_b7XSCuBs!r|1nw{PwN;@I4CUv5Fc`x72QgP-@|AIWHhHTX z>WK0zZlCQMb%^)Yt!wT@Ng|o4InO8bX-+tVaCz-O=-cS(nxwJ`9S)~hhCu`lTH$jV zJ4J1s-7cQ!q=YC1J8T%b1q#a`^W_}G;+6lIayhB{vU4P#IxO!<%_}5Q-Qful%uvO!bhCTowm91dEy<*kc&4r;Z4onyjTW_fPSRK)Pbc;F%J7Cp~?D z-A8?Dv!!|M0{$4KGIT_^L=TfxBD2W_B2e&m@!2V4@`{g=R($t3>XEpZ0;Ue1P`Jd$ z6qmPtJO~2Sks(h#+2!yfUp3&Spwq&64C)Ox4q)q;DExSOwx=JC-E>M-fE$BuXfD;v1TV{495rHxzUd>kUmZ9 zy_u~J`a`zAmv32saze_~fSJAP!alp5r$jt)18RnY{(x2WYC2JpaA&`SE8$f|0mh8$tLXE zPn|R``UbC>9rAIEzoO8D^M&q9w4F!HorbX!rIO)^#CMHKwS}2Rnh zrfxjl1Lg@f(~ki)Kp;yt)E`n}JQJvLcJ6D&iCl28SAjuLU-*?M;=Mn@OI4qxHh^bD z9klijjK@80CecnY!TQQ7a?@%Rag}n4inX*YgsU~?P@-Dv+iMGV0O?V|(IW@=DJpW@ zHjb;!BOx#~ivp(GMh9;&6TLI900NzBaP>73&978n!;V5O5%CeOzdztwue=BnUs5im5W;0V^ zHHMRL6fl_;eO+;vRpqFcA%{5Euov7md;?ks15Hd#J({)@FwQL3*z$J2gU8Rs&brO6 zggG8}45pqFCUr{JmCuv7k-bJdjSq7t>2+|i&u+B@^{E-b3s-PoJl${C{pl6OZYJYh z2iywlcUBDPUZJfI-}g?}!Ue0eT%aEiPMZUe62rgDx?K*E=WuSx5%6UnZWTDKCwJ27 z3Et;ZUX2JM)S76vf3cl<$uzckrB-zy51pqGHiCr;i+aHz@IS;R03GtQg7g&2jp6vt zfN#0id=Yjk!DqqwEg8gX)ZUp`WfBtoK7R<$&yV|ZS`8+*X!_#Nb`1)@0vYXv7)ZWG zyw_Bc$08lGCXM);=O)pgpD7N^?Y0{Qjp<|g3}4C!wLdto;PEdj(t+D$wIZ^7`VK+-w15}I5~9xTj~*KL7M(cJ0gmI4oZ2{H+WK@S zeXuNGb=-%V9ZIMW8(fC|RS}}kLZYlI{fEzQxO_BIpNU3J4VG7_QFEc z2$XbPx!Ox`zXz-Ft}O{sX<>(n`()du*uW!l0GF2^p*y{J1j3n+q;y|*#kV}rK>d9~ zvthp>HV(#(qRhu3r8zZQyFtZN+%ed+E{iKxo{+1$=6=j#=c72QhLE?yghKjK{+E*7DPWROC z5@W3u>Pp9mfw@M!iaNtv!_tLeCdSrewPJxcV&&3>7C zK^xfMAR>+?=p?x4TKx(&O96mwIAClYD@M9CC@I$S0?`Fr=D{kNKg59wdw>~AK)kzJ zUAYTzZ~ayJ#o)j?aV}g>NLEEJlKx3Hf-rB(Y%8@7DX+?ArzbTi)i~ti zRt$NYiphwmoFMk9lqqSO_*t-lJ2xpt7F0$*nb6|>rNqyDjx$pHX>gUb+|IGI%sFC= zvVJKJX|@|rn#c+k;!N+6M$fe=l&jsZ*xMSCbjb?INjmvUG`h?2a2lX#LYodRILJDT zc4v=NqsH9wEvYnw@@byqQopWS7zFxgXs3cH`#!$RFEFfaZfVG_<oxg6u%SMspNw z3y~QYr5A52iX@cVN_=6yC1oTMSl})DTg~>7&Tx2Z!Ug?cNdNh1`CRFiWZ!okFKZ{9 z7Zk=uv!*m9-G2R#AlIvZW=>e^L&Tp_Ps}gsGBD2C4iJ#}+yMUy?qWT1$D`G!lQuy<-u%pMWE*tt$vP z<5mj!9>_3#kw&r2Ns1c~PaKJ5rP0?-6+EonYls(dy_mY`fQyQ(F!2(RyHXe>Y6DJ)qR^ZOzKIQkY^TB8-U^61? z595r=W7+k-k~J!m`OPtLhH#b?~8AHeM`o%6aCvDsISC) z6Xx0m`@RAO6TaghD>Oe#RqE-GVm7IR-D&iNwb!);1@d}rQ7T5&<7FTBoi^lib4S2+$q(a_>OjZ&w2k>@LOg<>L3OwAy8Fr}1LO()IH*d}GXAbUVA zZS}1F7kP`#Kr^B?U?A(b1|4dLYkfx?$vu@-!Ux$3AE;3^DLU{Scp zgIJN5jFc&0ECaYVBuvORTDPr8LQvs7(F$(iWTRHjVAdm6GS^`7*1{ zNf_|@dzZgPt0)5uA^`fo>x%ywi~oQ8f&T~pcX?4!1{~sllEDA1;eV|8PlW^m`hUHm BQ^^1T literal 0 HcmV?d00001 From 24a9d9d36f67d219bceaf7120db14e2c6ed5817e Mon Sep 17 00:00:00 2001 From: Simone Date: Wed, 9 Oct 2024 14:59:51 +0200 Subject: [PATCH 54/71] Add Gelato VRF unprotected request detector --- slither/detectors/all_detectors.py | 1 + .../gelato_unprotected_randomness.py | 78 ++++++++++++++++++ ...0_gelato_unprotected_randomness_sol__0.txt | 6 ++ .../0.8.20/gelato_unprotected_randomness.sol | 61 ++++++++++++++ ...lato_unprotected_randomness.sol-0.8.20.zip | Bin 0 -> 6629 bytes 5 files changed, 146 insertions(+) create mode 100644 slither/detectors/functions/gelato_unprotected_randomness.py create mode 100644 tests/e2e/detectors/snapshots/detectors__detector_GelatoUnprotectedRandomness_0_8_20_gelato_unprotected_randomness_sol__0.txt create mode 100644 tests/e2e/detectors/test_data/gelato-unprotected-randomness/0.8.20/gelato_unprotected_randomness.sol create mode 100644 tests/e2e/detectors/test_data/gelato-unprotected-randomness/0.8.20/gelato_unprotected_randomness.sol-0.8.20.zip diff --git a/slither/detectors/all_detectors.py b/slither/detectors/all_detectors.py index 44a168c2b6..c317b6c0d0 100644 --- a/slither/detectors/all_detectors.py +++ b/slither/detectors/all_detectors.py @@ -97,5 +97,6 @@ from .statements.tautological_compare import TautologicalCompare from .statements.return_bomb import ReturnBomb from .functions.out_of_order_retryable import OutOfOrderRetryable +from .functions.gelato_unprotected_randomness import GelatoUnprotectedRandomness # from .statements.unused_import import UnusedImport diff --git a/slither/detectors/functions/gelato_unprotected_randomness.py b/slither/detectors/functions/gelato_unprotected_randomness.py new file mode 100644 index 0000000000..bdc3a6fb09 --- /dev/null +++ b/slither/detectors/functions/gelato_unprotected_randomness.py @@ -0,0 +1,78 @@ +from typing import List + +from slither.slithir.operations.internal_call import InternalCall +from slither.detectors.abstract_detector import ( + AbstractDetector, + DetectorClassification, + DETECTOR_INFO, +) +from slither.utils.output import Output + + +class GelatoUnprotectedRandomness(AbstractDetector): + """ + Unprotected Gelato VRF requests + """ + + ARGUMENT = "gelato-unprotected-randomness" + HELP = "Call to _requestRandomness within an unprotected function" + IMPACT = DetectorClassification.MEDIUM + CONFIDENCE = DetectorClassification.MEDIUM + + WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#gelato-unprotected-randomness" + + WIKI_TITLE = "Gelato unprotected randomness" + WIKI_DESCRIPTION = "Detect calls to `_requestRandomness` within an unprotected function." + + # region wiki_exploit_scenario + WIKI_EXPLOIT_SCENARIO = """ +```solidity +contract C is GelatoVRFConsumerBase { + function _fulfillRandomness( + uint256 randomness, + uint256, + bytes memory extraData + ) internal override { + // Do something with the random number + } + + function bad() public { + _requestRandomness(abi.encode(msg.sender)); + } +} +``` +The function `bad` is uprotected and requests randomness.""" + # endregion wiki_exploit_scenario + + WIKI_RECOMMENDATION = ( + "Function that request randomness should be allowed only to authorized users." + ) + + def _detect(self) -> List[Output]: + results = [] + + for contract in self.compilation_unit.contracts_derived: + if "GelatoVRFConsumerBase" in [c.name for c in contract.inheritance]: + for function in contract.functions_entry_points: + if not function.is_protected() and ( + nodes_request := [ + ir.node + for ir in function.all_internal_calls() + if isinstance(ir, InternalCall) + and ir.function_name == "_requestRandomness" + ] + ): + # Sort so output is deterministic + nodes_request.sort(key=lambda x: (x.node_id, x.function.full_name)) + + for node in nodes_request: + info: DETECTOR_INFO = [ + function, + " is unprotected and request randomness from Gelato VRF\n\t- ", + node, + "\n", + ] + res = self.generate_result(info) + results.append(res) + + return results diff --git a/tests/e2e/detectors/snapshots/detectors__detector_GelatoUnprotectedRandomness_0_8_20_gelato_unprotected_randomness_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_GelatoUnprotectedRandomness_0_8_20_gelato_unprotected_randomness_sol__0.txt new file mode 100644 index 0000000000..aee2ea4ddd --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_GelatoUnprotectedRandomness_0_8_20_gelato_unprotected_randomness_sol__0.txt @@ -0,0 +1,6 @@ +C.bad() (tests/e2e/detectors/test_data/gelato-unprotected-randomness/0.8.20/gelato_unprotected_randomness.sol#42-44) is unprotected and request randomness from Gelato VRF + - id = _requestRandomness(abi.encode(msg.sender)) (tests/e2e/detectors/test_data/gelato-unprotected-randomness/0.8.20/gelato_unprotected_randomness.sol#43) + +C.good2() (tests/e2e/detectors/test_data/gelato-unprotected-randomness/0.8.20/gelato_unprotected_randomness.sol#51-54) is unprotected and request randomness from Gelato VRF + - id = _requestRandomness(abi.encode(msg.sender)) (tests/e2e/detectors/test_data/gelato-unprotected-randomness/0.8.20/gelato_unprotected_randomness.sol#53) + diff --git a/tests/e2e/detectors/test_data/gelato-unprotected-randomness/0.8.20/gelato_unprotected_randomness.sol b/tests/e2e/detectors/test_data/gelato-unprotected-randomness/0.8.20/gelato_unprotected_randomness.sol new file mode 100644 index 0000000000..1a173f7d41 --- /dev/null +++ b/tests/e2e/detectors/test_data/gelato-unprotected-randomness/0.8.20/gelato_unprotected_randomness.sol @@ -0,0 +1,61 @@ +// Mock GelatoVRFConsumerBase for what we need +abstract contract GelatoVRFConsumerBase { + bool[] public requestPending; + mapping(uint256 => bytes32) public requestedHash; + + function _fulfillRandomness( + uint256 randomness, + uint256 requestId, + bytes memory extraData + ) internal virtual; + + function _requestRandomness( + bytes memory extraData + ) internal returns (uint256 requestId) { + requestId = uint256(requestPending.length); + requestPending.push(); + requestPending[requestId] = true; + + bytes memory data = abi.encode(requestId, extraData); + uint256 round = 111; + + bytes memory dataWithRound = abi.encode(round, data); + bytes32 requestHash = keccak256(dataWithRound); + + requestedHash[requestId] = requestHash; + } + +} + +contract C is GelatoVRFConsumerBase { + address owner; + mapping(address => bool) authorized; + + function _fulfillRandomness( + uint256 randomness, + uint256, + bytes memory extraData + ) internal override { + // Do something with the random number + } + + function bad() public { + uint id = _requestRandomness(abi.encode(msg.sender)); + } + + function good() public { + require(msg.sender == owner); + uint id = _requestRandomness(abi.encode(msg.sender)); + } + + function good2() public { + require(authorized[msg.sender]); + uint id = _requestRandomness(abi.encode(msg.sender)); + } + + function good3() public { + if (msg.sender != owner) { revert(); } + uint id = _requestRandomness(abi.encode(msg.sender)); + } + +} \ No newline at end of file diff --git a/tests/e2e/detectors/test_data/gelato-unprotected-randomness/0.8.20/gelato_unprotected_randomness.sol-0.8.20.zip b/tests/e2e/detectors/test_data/gelato-unprotected-randomness/0.8.20/gelato_unprotected_randomness.sol-0.8.20.zip new file mode 100644 index 0000000000000000000000000000000000000000..013d3ef28d1b71bd8543ddf8923c81735f85437f GIT binary patch literal 6629 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 By$S#T literal 0 HcmV?d00001 From 138b62664d1295f617db47e8d1dbba70c7256a9d Mon Sep 17 00:00:00 2001 From: Simone Date: Wed, 9 Oct 2024 16:53:49 +0200 Subject: [PATCH 55/71] Add test --- tests/e2e/detectors/test_detectors.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/e2e/detectors/test_detectors.py b/tests/e2e/detectors/test_detectors.py index 2c6a5f55a3..a1ec1cf64c 100644 --- a/tests/e2e/detectors/test_detectors.py +++ b/tests/e2e/detectors/test_detectors.py @@ -1714,6 +1714,11 @@ def id_test(test_item: Test): "out_of_order_retryable.sol", "0.8.20", ), + Test( + all_detectors.GelatoUnprotectedRandomness, + "gelato_unprotected_randomness.sol", + "0.8.20", + ), # Test( # all_detectors.UnusedImport, # "ConstantContractLevelUsedInContractTest.sol", From 2d46a3deaac05eff66996d32a96d163377152439 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Wed, 9 Oct 2024 11:17:43 -0500 Subject: [PATCH 56/71] Update CODEOWNERS --- CODEOWNERS | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index c92f0d79df..496da0c304 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,5 +1,4 @@ -* @montyly @0xalpharush @smonicas -/slither/tools/read_storage/ @0xalpharush +* @montyly @smonicas /slither/tools/doctor/ @elopez /slither/slithir/ @montyly /slither/analyses/ @montyly From 4a9cdcef2c65246c023c88e14d469912b86b9451 Mon Sep 17 00:00:00 2001 From: Simone Date: Wed, 9 Oct 2024 20:16:51 +0200 Subject: [PATCH 57/71] Add Chronicle unchecked price detector --- slither/detectors/all_detectors.py | 1 + .../statements/chronicle_unchecked_price.py | 147 ++++++++++++++++++ ..._8_20_chronicle_unchecked_price_sol__0.txt | 18 +++ .../0.8.20/chronicle_unchecked_price.sol | 119 ++++++++++++++ .../chronicle_unchecked_price.sol-0.8.20.zip | Bin 0 -> 8287 bytes tests/e2e/detectors/test_detectors.py | 5 + 6 files changed, 290 insertions(+) create mode 100644 slither/detectors/statements/chronicle_unchecked_price.py create mode 100644 tests/e2e/detectors/snapshots/detectors__detector_ChronicleUncheckedPrice_0_8_20_chronicle_unchecked_price_sol__0.txt create mode 100644 tests/e2e/detectors/test_data/chronicle-unchecked-price/0.8.20/chronicle_unchecked_price.sol create mode 100644 tests/e2e/detectors/test_data/chronicle-unchecked-price/0.8.20/chronicle_unchecked_price.sol-0.8.20.zip diff --git a/slither/detectors/all_detectors.py b/slither/detectors/all_detectors.py index 44a168c2b6..d69b208209 100644 --- a/slither/detectors/all_detectors.py +++ b/slither/detectors/all_detectors.py @@ -97,5 +97,6 @@ from .statements.tautological_compare import TautologicalCompare from .statements.return_bomb import ReturnBomb from .functions.out_of_order_retryable import OutOfOrderRetryable +from .statements.chronicle_unchecked_price import ChronicleUncheckedPrice # from .statements.unused_import import UnusedImport diff --git a/slither/detectors/statements/chronicle_unchecked_price.py b/slither/detectors/statements/chronicle_unchecked_price.py new file mode 100644 index 0000000000..47ad2ddc57 --- /dev/null +++ b/slither/detectors/statements/chronicle_unchecked_price.py @@ -0,0 +1,147 @@ +from typing import List + +from slither.detectors.abstract_detector import ( + AbstractDetector, + DetectorClassification, + DETECTOR_INFO, +) +from slither.utils.output import Output +from slither.slithir.operations import Binary, Assignment, Unpack, SolidityCall +from slither.core.variables import Variable +from slither.core.declarations.solidity_variables import SolidityFunction +from slither.core.cfg.node import Node + + +class ChronicleUncheckedPrice(AbstractDetector): + """ + Documentation: This detector finds calls to Chronicle oracle where the returned price is not checked + https://docs.chroniclelabs.org/Resources/FAQ/Oracles#how-do-i-check-if-an-oracle-becomes-inactive-gets-deprecated + """ + + ARGUMENT = "chronicle-unchecked-price" + HELP = "Detect when Chronicle price is not checked." + IMPACT = DetectorClassification.MEDIUM + CONFIDENCE = DetectorClassification.MEDIUM + + WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#chronicle-unchecked-price" + + WIKI_TITLE = "Chronicle unchecked price" + WIKI_DESCRIPTION = "Chronicle oracle is used and the price returned is not checked to be valid. For more information https://docs.chroniclelabs.org/Resources/FAQ/Oracles#how-do-i-check-if-an-oracle-becomes-inactive-gets-deprecated." + + # region wiki_exploit_scenario + WIKI_EXPLOIT_SCENARIO = """ +```solidity +contract C { + IChronicle chronicle; + + constructor(address a) { + chronicle = IChronicle(a); + } + + function bad() public { + uint256 price = chronicle.read(); + } +``` +The `bad` function gets the price from Chronicle by calling the read function however it does not check if the price is valid.""" + # endregion wiki_exploit_scenario + + WIKI_RECOMMENDATION = "Validate that the price returned by the oracle is valid." + + def _var_is_checked(self, nodes: List[Node], var_to_check: Variable) -> bool: + visited = set() + checked = False + + while nodes: + if checked: + break + next_node = nodes[0] + nodes = nodes[1:] + + for node_ir in next_node.all_slithir_operations(): + if isinstance(node_ir, Binary) and var_to_check in node_ir.read: + checked = True + break + # This case is for tryRead and tryReadWithAge + # if the isValid boolean is checked inside a require(isValid) + if ( + isinstance(node_ir, SolidityCall) + and node_ir.function + in ( + SolidityFunction("require(bool)"), + SolidityFunction("require(bool,string)"), + SolidityFunction("require(bool,error)"), + ) + and var_to_check in node_ir.read + ): + checked = True + break + + if next_node not in visited: + visited.add(next_node) + for son in next_node.sons: + if son not in visited: + nodes.append(son) + return checked + + # pylint: disable=too-many-nested-blocks,too-many-branches + def _detect(self) -> List[Output]: + results: List[Output] = [] + + for contract in self.compilation_unit.contracts_derived: + for target_contract, ir in sorted( + contract.all_high_level_calls, + key=lambda x: (x[1].node.node_id, x[1].node.function.full_name), + ): + if target_contract.name in ("IScribe", "IChronicle") and ir.function_name in ( + "read", + "tryRead", + "readWithAge", + "tryReadWithAge", + "latestAnswer", + "latestRoundData", + ): + found = False + if ir.function_name in ("read", "latestAnswer"): + # We need to iterate the IRs as we are not always sure that the following IR is the assignment + # for example in case of type conversion it isn't + for node_ir in ir.node.irs: + if isinstance(node_ir, Assignment): + possible_unchecked_variable_ir = node_ir.lvalue + found = True + break + elif ir.function_name in ("readWithAge", "tryRead", "tryReadWithAge"): + # We are interested in the first item of the tuple + # readWithAge : value + # tryRead/tryReadWithAge : isValid + for node_ir in ir.node.irs: + if isinstance(node_ir, Unpack) and node_ir.index == 0: + possible_unchecked_variable_ir = node_ir.lvalue + found = True + break + elif ir.function_name == "latestRoundData": + found = False + for node_ir in ir.node.irs: + if isinstance(node_ir, Unpack) and node_ir.index == 1: + possible_unchecked_variable_ir = node_ir.lvalue + found = True + break + + # If we did not find the variable assignment we know it's not checked + checked = ( + self._var_is_checked(ir.node.sons, possible_unchecked_variable_ir) + if found + else False + ) + + if not checked: + info: DETECTOR_INFO = [ + "Chronicle price is not checked to be valid in ", + ir.node.function, + "\n\t- ", + ir.node, + "\n", + ] + res = self.generate_result(info) + results.append(res) + + return results diff --git a/tests/e2e/detectors/snapshots/detectors__detector_ChronicleUncheckedPrice_0_8_20_chronicle_unchecked_price_sol__0.txt b/tests/e2e/detectors/snapshots/detectors__detector_ChronicleUncheckedPrice_0_8_20_chronicle_unchecked_price_sol__0.txt new file mode 100644 index 0000000000..6ddbfa4e58 --- /dev/null +++ b/tests/e2e/detectors/snapshots/detectors__detector_ChronicleUncheckedPrice_0_8_20_chronicle_unchecked_price_sol__0.txt @@ -0,0 +1,18 @@ +Chronicle price is not checked to be valid in C.bad2() (tests/e2e/detectors/test_data/chronicle-unchecked-price/0.8.20/chronicle_unchecked_price.sol#74-76) + - (price,None) = chronicle.readWithAge() (tests/e2e/detectors/test_data/chronicle-unchecked-price/0.8.20/chronicle_unchecked_price.sol#75) + +Chronicle price is not checked to be valid in C.bad() (tests/e2e/detectors/test_data/chronicle-unchecked-price/0.8.20/chronicle_unchecked_price.sol#65-67) + - price = chronicle.read() (tests/e2e/detectors/test_data/chronicle-unchecked-price/0.8.20/chronicle_unchecked_price.sol#66) + +Chronicle price is not checked to be valid in C.bad5() (tests/e2e/detectors/test_data/chronicle-unchecked-price/0.8.20/chronicle_unchecked_price.sol#101-103) + - price = scribe.latestAnswer() (tests/e2e/detectors/test_data/chronicle-unchecked-price/0.8.20/chronicle_unchecked_price.sol#102) + +Chronicle price is not checked to be valid in C.bad4() (tests/e2e/detectors/test_data/chronicle-unchecked-price/0.8.20/chronicle_unchecked_price.sol#92-94) + - (isValid,price,None) = chronicle.tryReadWithAge() (tests/e2e/detectors/test_data/chronicle-unchecked-price/0.8.20/chronicle_unchecked_price.sol#93) + +Chronicle price is not checked to be valid in C.bad3() (tests/e2e/detectors/test_data/chronicle-unchecked-price/0.8.20/chronicle_unchecked_price.sol#83-85) + - (isValid,price) = chronicle.tryRead() (tests/e2e/detectors/test_data/chronicle-unchecked-price/0.8.20/chronicle_unchecked_price.sol#84) + +Chronicle price is not checked to be valid in C.bad6() (tests/e2e/detectors/test_data/chronicle-unchecked-price/0.8.20/chronicle_unchecked_price.sol#110-112) + - (None,price,None,None,None) = scribe.latestRoundData() (tests/e2e/detectors/test_data/chronicle-unchecked-price/0.8.20/chronicle_unchecked_price.sol#111) + diff --git a/tests/e2e/detectors/test_data/chronicle-unchecked-price/0.8.20/chronicle_unchecked_price.sol b/tests/e2e/detectors/test_data/chronicle-unchecked-price/0.8.20/chronicle_unchecked_price.sol new file mode 100644 index 0000000000..e12560fa7d --- /dev/null +++ b/tests/e2e/detectors/test_data/chronicle-unchecked-price/0.8.20/chronicle_unchecked_price.sol @@ -0,0 +1,119 @@ +interface IChronicle { + /// @notice Returns the oracle's current value. + /// @dev Reverts if no value set. + /// @return value The oracle's current value. + function read() external view returns (uint value); + + /// @notice Returns the oracle's current value and its age. + /// @dev Reverts if no value set. + /// @return value The oracle's current value. + /// @return age The value's age. + function readWithAge() external view returns (uint value, uint age); + + /// @notice Returns the oracle's current value. + /// @return isValid True if value exists, false otherwise. + /// @return value The oracle's current value if it exists, zero otherwise. + function tryRead() external view returns (bool isValid, uint value); + + /// @notice Returns the oracle's current value and its age. + /// @return isValid True if value exists, false otherwise. + /// @return value The oracle's current value if it exists, zero otherwise. + /// @return age The value's age if value exists, zero otherwise. + function tryReadWithAge() + external + view + returns (bool isValid, uint value, uint age); +} + +interface IScribe is IChronicle { + /// @notice Returns the oracle's latest value. + /// @dev Provides partial compatibility with Chainlink's + /// IAggregatorV3Interface. + /// @return roundId 1. + /// @return answer The oracle's latest value. + /// @return startedAt 0. + /// @return updatedAt The timestamp of oracle's latest update. + /// @return answeredInRound 1. + function latestRoundData() + external + view + returns ( + uint80 roundId, + int answer, + uint startedAt, + uint updatedAt, + uint80 answeredInRound + ); + + /// @notice Returns the oracle's latest value. + /// @dev Provides partial compatibility with Chainlink's + /// IAggregatorV3Interface. + /// @custom:deprecated See https://docs.chain.link/data-feeds/api-reference/#latestanswer. + /// @return answer The oracle's latest value. + function latestAnswer() external view returns (int); +} + +contract C { + IScribe scribe; + IChronicle chronicle; + + constructor(address a) { + scribe = IScribe(a); + chronicle = IChronicle(a); + } + + function bad() public { + uint256 price = chronicle.read(); + } + + function good() public { + uint256 price = chronicle.read(); + require(price != 0); + } + + function bad2() public { + (uint256 price,) = chronicle.readWithAge(); + } + + function good2() public { + (uint256 price,) = chronicle.readWithAge(); + require(price != 0); + } + + function bad3() public { + (bool isValid, uint256 price) = chronicle.tryRead(); + } + + function good3() public { + (bool isValid, uint256 price) = chronicle.tryRead(); + require(isValid); + } + + function bad4() public { + (bool isValid, uint256 price,) = chronicle.tryReadWithAge(); + } + + function good4() public { + (bool isValid, uint256 price,) = chronicle.tryReadWithAge(); + require(isValid); + } + + function bad5() public { + int256 price = scribe.latestAnswer(); + } + + function good5() public { + int256 price = scribe.latestAnswer(); + require(price != 0); + } + + function bad6() public { + (, int256 price,,,) = scribe.latestRoundData(); + } + + function good6() public { + (, int256 price,,,) = scribe.latestRoundData(); + require(price != 0); + } + +} \ No newline at end of file diff --git a/tests/e2e/detectors/test_data/chronicle-unchecked-price/0.8.20/chronicle_unchecked_price.sol-0.8.20.zip b/tests/e2e/detectors/test_data/chronicle-unchecked-price/0.8.20/chronicle_unchecked_price.sol-0.8.20.zip new file mode 100644 index 0000000000000000000000000000000000000000..746efabf66030b2e0990d1243c6cd27d8dd38c32 GIT binary patch literal 8287 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 literal 0 HcmV?d00001 diff --git a/tests/e2e/detectors/test_detectors.py b/tests/e2e/detectors/test_detectors.py index 2c6a5f55a3..768ccb3f29 100644 --- a/tests/e2e/detectors/test_detectors.py +++ b/tests/e2e/detectors/test_detectors.py @@ -1714,6 +1714,11 @@ def id_test(test_item: Test): "out_of_order_retryable.sol", "0.8.20", ), + Test( + all_detectors.ChronicleUncheckedPrice, + "chronicle_unchecked_price.sol", + "0.8.20", + ), # Test( # all_detectors.UnusedImport, # "ConstantContractLevelUsedInContractTest.sol", From 9c9818c24df73c20560b29dcbbfa7a311d051f8e Mon Sep 17 00:00:00 2001 From: Simone <79767264+smonicas@users.noreply.github.com> Date: Wed, 9 Oct 2024 20:19:13 +0200 Subject: [PATCH 58/71] Update tests/e2e/detectors/test_detectors.py Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- tests/e2e/detectors/test_detectors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/detectors/test_detectors.py b/tests/e2e/detectors/test_detectors.py index 768ccb3f29..b42af234f4 100644 --- a/tests/e2e/detectors/test_detectors.py +++ b/tests/e2e/detectors/test_detectors.py @@ -1718,7 +1718,7 @@ def id_test(test_item: Test): all_detectors.ChronicleUncheckedPrice, "chronicle_unchecked_price.sol", "0.8.20", - ), + ), # Test( # all_detectors.UnusedImport, # "ConstantContractLevelUsedInContractTest.sol", From 1f3285568080479548d4e2d7e3b396292159818b Mon Sep 17 00:00:00 2001 From: Simone Date: Thu, 10 Oct 2024 10:45:18 +0200 Subject: [PATCH 59/71] Add StateVariable location --- slither/core/variables/state_variable.py | 14 ++++++++++++++ slither/solc_parsing/variables/state_variable.py | 15 +++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/slither/core/variables/state_variable.py b/slither/core/variables/state_variable.py index f2a2d6ee3c..404cf74ba3 100644 --- a/slither/core/variables/state_variable.py +++ b/slither/core/variables/state_variable.py @@ -12,6 +12,7 @@ class StateVariable(ContractLevel, Variable): def __init__(self) -> None: super().__init__() self._node_initialization: Optional["Node"] = None + self._location: Optional[str] = None def is_declared_by(self, contract: "Contract") -> bool: """ @@ -21,6 +22,19 @@ def is_declared_by(self, contract: "Contract") -> bool: """ return self.contract == contract + def set_location(self, loc: str) -> None: + self._location = loc + + @property + def location(self) -> Optional[str]: + """ + Variable Location + Can be default or transient + Returns: + (str) + """ + return self._location + # endregion ################################################################################### ################################################################################### diff --git a/slither/solc_parsing/variables/state_variable.py b/slither/solc_parsing/variables/state_variable.py index a9c0ff730b..227a84c61b 100644 --- a/slither/solc_parsing/variables/state_variable.py +++ b/slither/solc_parsing/variables/state_variable.py @@ -13,3 +13,18 @@ def underlying_variable(self) -> StateVariable: # Todo: Not sure how to overcome this with mypy assert isinstance(self._variable, StateVariable) return self._variable + + def _analyze_variable_attributes(self, attributes: Dict) -> None: + """ + Variable Location + Can be default or transient + """ + if "storageLocation" in attributes: + self.underlying_variable.set_location(attributes["storageLocation"]) + else: + # We don't have to support legacy ast + # as transient location was added in 0.8.28 + # and we know it must be default + self.underlying_variable.set_location("default") + + super()._analyze_variable_attributes(attributes) From 9ce83021d7beb3b5f31c0e989759c12ca0d8d7ab Mon Sep 17 00:00:00 2001 From: Josselin Feist Date: Thu, 10 Oct 2024 11:48:21 +0200 Subject: [PATCH 60/71] Update gelato_unprotected_randomness.sol --- .../0.8.20/gelato_unprotected_randomness.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/e2e/detectors/test_data/gelato-unprotected-randomness/0.8.20/gelato_unprotected_randomness.sol b/tests/e2e/detectors/test_data/gelato-unprotected-randomness/0.8.20/gelato_unprotected_randomness.sol index 1a173f7d41..108859e9e3 100644 --- a/tests/e2e/detectors/test_data/gelato-unprotected-randomness/0.8.20/gelato_unprotected_randomness.sol +++ b/tests/e2e/detectors/test_data/gelato-unprotected-randomness/0.8.20/gelato_unprotected_randomness.sol @@ -48,6 +48,7 @@ contract C is GelatoVRFConsumerBase { uint id = _requestRandomness(abi.encode(msg.sender)); } + // This is currently a FP due to the limitation of function.is_protected function good2() public { require(authorized[msg.sender]); uint id = _requestRandomness(abi.encode(msg.sender)); @@ -58,4 +59,4 @@ contract C is GelatoVRFConsumerBase { uint id = _requestRandomness(abi.encode(msg.sender)); } -} \ No newline at end of file +} From ecc20101dee3ac88fa6d5ae5d54539e4b02524f6 Mon Sep 17 00:00:00 2001 From: Josselin Feist Date: Thu, 10 Oct 2024 11:53:43 +0200 Subject: [PATCH 61/71] Update tests/e2e/detectors/test_detectors.py Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- tests/e2e/detectors/test_detectors.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/e2e/detectors/test_detectors.py b/tests/e2e/detectors/test_detectors.py index 2fa9dbbf20..d2f191a4d9 100644 --- a/tests/e2e/detectors/test_detectors.py +++ b/tests/e2e/detectors/test_detectors.py @@ -1719,7 +1719,8 @@ def id_test(test_item: Test): "gelato_unprotected_randomness.sol", "0.8.20", ), - Test( all_detectors.ChronicleUncheckedPrice, + Test( + all_detectors.ChronicleUncheckedPrice, "chronicle_unchecked_price.sol", "0.8.20", ), From ef03cb7060b67ae5c14251873ae89ada30b759fc Mon Sep 17 00:00:00 2001 From: Simone Date: Thu, 10 Oct 2024 18:32:26 +0200 Subject: [PATCH 62/71] Improve transient storage support --- slither/core/compilation_unit.py | 60 ++++++++---- slither/core/declarations/contract.py | 46 ++++----- slither/core/variables/state_variable.py | 16 ++++ slither/core/variables/variable.py | 7 -- .../variables/unchanged_state_variables.py | 2 +- slither/printers/summary/variable_order.py | 13 ++- slither/solc_parsing/declarations/contract.py | 4 +- .../checks/variable_initialization.py | 2 +- .../upgradeability/checks/variables_order.py | 93 ++++++++++++------- slither/utils/upgradeability.py | 8 +- .../vyper_parsing/declarations/contract.py | 2 +- 11 files changed, 152 insertions(+), 101 deletions(-) diff --git a/slither/core/compilation_unit.py b/slither/core/compilation_unit.py index df652dab0c..f4bd07e556 100644 --- a/slither/core/compilation_unit.py +++ b/slither/core/compilation_unit.py @@ -73,7 +73,8 @@ def __init__(self, core: "SlitherCore", crytic_compilation_unit: CompilationUnit # Memoize self._all_state_variables: Optional[Set[StateVariable]] = None - self._storage_layouts: Dict[str, Dict[str, Tuple[int, int]]] = {} + self._persistent_storage_layouts: Dict[str, Dict[str, Tuple[int, int]]] = {} + self._transient_storage_layouts: Dict[str, Dict[str, Tuple[int, int]]] = {} self._contract_with_missing_inheritance: Set[Contract] = set() @@ -297,33 +298,52 @@ def get_scope(self, filename_str: str) -> FileScope: def compute_storage_layout(self) -> None: assert self.is_solidity + for contract in self.contracts_derived: - self._storage_layouts[contract.name] = {} - - slot = 0 - offset = 0 - for var in contract.stored_state_variables_ordered: - assert var.type - size, new_slot = var.type.storage_size - - if new_slot: - if offset > 0: - slot += 1 - offset = 0 - elif size + offset > 32: + self._compute_storage_layout(contract.name, contract.storage_variables_ordered, False) + self._compute_storage_layout(contract.name, contract.transient_variables_ordered, True) + + def _compute_storage_layout( + self, contract_name: str, state_variables_ordered: List[StateVariable], is_transient: bool + ): + if is_transient: + self._transient_storage_layouts[contract_name] = {} + else: + self._persistent_storage_layouts[contract_name] = {} + + slot = 0 + offset = 0 + for var in state_variables_ordered: + assert var.type + size, new_slot = var.type.storage_size + + if new_slot: + if offset > 0: slot += 1 offset = 0 + elif size + offset > 32: + slot += 1 + offset = 0 - self._storage_layouts[contract.name][var.canonical_name] = ( + if is_transient: + self._transient_storage_layouts[contract_name][var.canonical_name] = ( slot, offset, ) - if new_slot: - slot += math.ceil(size / 32) - else: - offset += size + else: + self._persistent_storage_layouts[contract_name][var.canonical_name] = ( + slot, + offset, + ) + + if new_slot: + slot += math.ceil(size / 32) + else: + offset += size def storage_layout_of(self, contract: Contract, var: StateVariable) -> Tuple[int, int]: - return self._storage_layouts[contract.name][var.canonical_name] + if var.is_stored: + return self._persistent_storage_layouts[contract.name][var.canonical_name] + return self._transient_storage_layouts[contract.name][var.canonical_name] # endregion diff --git a/slither/core/declarations/contract.py b/slither/core/declarations/contract.py index a2f24fc6cd..8dccc007f9 100644 --- a/slither/core/declarations/contract.py +++ b/slither/core/declarations/contract.py @@ -440,55 +440,43 @@ def variables_as_dict(self) -> Dict[str, "StateVariable"]: def state_variables(self) -> List["StateVariable"]: """ Returns all the accessible variables (do not include private variable from inherited contract). - Use state_variables_ordered for all the variables following the storage order + Use stored_state_variables_ordered for all the storage variables following the storage order + Use transient_state_variables_ordered for all the transient variables following the storage order list(StateVariable): List of the state variables. """ return list(self._variables.values()) @property - def stored_state_variables(self) -> List["StateVariable"]: + def state_variables_entry_points(self) -> List["StateVariable"]: """ - Returns state variables with storage locations, excluding private variables from inherited contracts. - Use stored_state_variables_ordered to access variables with storage locations in their declaration order. - - This implementation filters out state variables if they are constant or immutable. It will be - updated to accommodate any new non-storage keywords that might replace 'constant' and 'immutable' in the future. - - Returns: - List[StateVariable]: A list of state variables with storage locations. + list(StateVariable): List of the state variables that are public. """ - return [variable for variable in self.state_variables if variable.is_stored] + return [var for var in self._variables.values() if var.visibility == "public"] @property - def stored_state_variables_ordered(self) -> List["StateVariable"]: + def state_variables_ordered(self) -> List["StateVariable"]: """ - list(StateVariable): List of the state variables with storage locations by order of declaration. - - This implementation filters out state variables if they are constant or immutable. It will be - updated to accommodate any new non-storage keywords that might replace 'constant' and 'immutable' in the future. - - Returns: - List[StateVariable]: A list of state variables with storage locations ordered by declaration. + list(StateVariable): List of the state variables by order of declaration. """ - return [variable for variable in self.state_variables_ordered if variable.is_stored] + return self._variables_ordered + + def add_state_variables_ordered(self, new_vars: List["StateVariable"]) -> None: + self._variables_ordered += new_vars @property - def state_variables_entry_points(self) -> List["StateVariable"]: + def storage_variables_ordered(self) -> List["StateVariable"]: """ - list(StateVariable): List of the state variables that are public. + list(StateVariable): List of the state variables in storage location by order of declaration. """ - return [var for var in self._variables.values() if var.visibility == "public"] + return [v for v in self._variables_ordered if v.is_stored] @property - def state_variables_ordered(self) -> List["StateVariable"]: + def transient_variables_ordered(self) -> List["StateVariable"]: """ - list(StateVariable): List of the state variables by order of declaration. + list(StateVariable): List of the state variables in transient location by order of declaration. """ - return list(self._variables_ordered) - - def add_variables_ordered(self, new_vars: List["StateVariable"]) -> None: - self._variables_ordered += new_vars + return [v for v in self._variables_ordered if v.is_transient] @property def state_variables_inherited(self) -> List["StateVariable"]: diff --git a/slither/core/variables/state_variable.py b/slither/core/variables/state_variable.py index 404cf74ba3..d3e3e60182 100644 --- a/slither/core/variables/state_variable.py +++ b/slither/core/variables/state_variable.py @@ -35,6 +35,22 @@ def location(self) -> Optional[str]: """ return self._location + @property + def is_stored(self) -> bool: + """ + Checks if the state variable is stored, based on it not being constant or immutable or transient. + """ + return ( + not self._is_constant and not self._is_immutable and not self._location == "transient" + ) + + @property + def is_transient(self) -> bool: + """ + Checks if the state variable is transient. A transient variable can not be constant or immutable. + """ + return self._location == "transient" + # endregion ################################################################################### ################################################################################### diff --git a/slither/core/variables/variable.py b/slither/core/variables/variable.py index 63d1a7a838..f9ef190246 100644 --- a/slither/core/variables/variable.py +++ b/slither/core/variables/variable.py @@ -93,13 +93,6 @@ def is_constant(self) -> bool: def is_constant(self, is_cst: bool) -> None: self._is_constant = is_cst - @property - def is_stored(self) -> bool: - """ - Checks if a variable is stored, based on it not being constant or immutable. Future updates may adjust for new non-storage keywords. - """ - return not self._is_constant and not self._is_immutable - @property def is_reentrant(self) -> bool: return self._is_reentrant diff --git a/slither/detectors/variables/unchanged_state_variables.py b/slither/detectors/variables/unchanged_state_variables.py index 5771d96303..64c4c350f6 100644 --- a/slither/detectors/variables/unchanged_state_variables.py +++ b/slither/detectors/variables/unchanged_state_variables.py @@ -92,7 +92,7 @@ def detect(self) -> None: variables = [] functions = [] - variables.append(c.stored_state_variables) + variables.append(c.storage_variables_ordered) functions.append(c.all_functions_called) valid_candidates: Set[StateVariable] = { diff --git a/slither/printers/summary/variable_order.py b/slither/printers/summary/variable_order.py index 0d8ce2612c..fb19e39857 100644 --- a/slither/printers/summary/variable_order.py +++ b/slither/printers/summary/variable_order.py @@ -27,10 +27,17 @@ def output(self, _filename: str) -> Output: for contract in self.slither.contracts_derived: txt += f"\n{contract.name}:\n" - table = MyPrettyTable(["Name", "Type", "Slot", "Offset"]) - for variable in contract.stored_state_variables_ordered: + table = MyPrettyTable(["Name", "Type", "Slot", "Offset", "State"]) + for variable in contract.storage_variables_ordered: slot, offset = contract.compilation_unit.storage_layout_of(contract, variable) - table.add_row([variable.canonical_name, str(variable.type), slot, offset]) + table.add_row( + [variable.canonical_name, str(variable.type), slot, offset, "Storage"] + ) + for variable in contract.transient_variables_ordered: + slot, offset = contract.compilation_unit.storage_layout_of(contract, variable) + table.add_row( + [variable.canonical_name, str(variable.type), slot, offset, "Transient"] + ) all_tables.append((contract.name, table)) txt += str(table) + "\n" diff --git a/slither/solc_parsing/declarations/contract.py b/slither/solc_parsing/declarations/contract.py index 1ccdc57602..06fc03b7a0 100644 --- a/slither/solc_parsing/declarations/contract.py +++ b/slither/solc_parsing/declarations/contract.py @@ -350,7 +350,7 @@ def parse_state_variables(self) -> None: if v.visibility != "private" } ) - self._contract.add_variables_ordered( + self._contract.add_state_variables_ordered( [ var for var in father.state_variables_ordered @@ -370,7 +370,7 @@ def parse_state_variables(self) -> None: if var_parser.reference_id is not None: self._contract.state_variables_by_ref_id[var_parser.reference_id] = var self._contract.variables_as_dict[var.name] = var - self._contract.add_variables_ordered([var]) + self._contract.add_state_variables_ordered([var]) def _parse_modifier(self, modifier_data: Dict) -> None: modif = Modifier(self._contract.compilation_unit) diff --git a/slither/tools/upgradeability/checks/variable_initialization.py b/slither/tools/upgradeability/checks/variable_initialization.py index b86036c87f..047c652dc4 100644 --- a/slither/tools/upgradeability/checks/variable_initialization.py +++ b/slither/tools/upgradeability/checks/variable_initialization.py @@ -43,7 +43,7 @@ class VariableWithInit(AbstractCheck): def _check(self) -> List[Output]: results = [] - for s in self.contract.stored_state_variables_ordered: + for s in self.contract.storage_variables_ordered: if s.initialized: info: CHECK_INFO = [s, " is a state variable with an initial value.\n"] json = self.generate_result(info) diff --git a/slither/tools/upgradeability/checks/variables_order.py b/slither/tools/upgradeability/checks/variables_order.py index 8d525a6dd3..8f5017d74c 100644 --- a/slither/tools/upgradeability/checks/variables_order.py +++ b/slither/tools/upgradeability/checks/variables_order.py @@ -115,29 +115,43 @@ def _contract2(self) -> Contract: def _check(self) -> List[Output]: contract1 = self._contract1() contract2 = self._contract2() - order1 = contract1.stored_state_variables_ordered - order2 = contract2.stored_state_variables_ordered results: List[Output] = [] - for idx, _ in enumerate(order1): - if len(order2) <= idx: - # Handle by MissingVariable - return results - - variable1 = order1[idx] - variable2 = order2[idx] - if (variable1.name != variable2.name) or (variable1.type != variable2.type): - info: CHECK_INFO = [ - "Different variables between ", - contract1, - " and ", - contract2, - "\n", - ] - info += ["\t ", variable1, "\n"] - info += ["\t ", variable2, "\n"] - json = self.generate_result(info) - results.append(json) + + def _check_internal( + contract1: Contract, contract2: Contract, results: List[Output], is_transient: bool + ): + if is_transient: + order1 = contract1.transient_variables_ordered + order2 = contract2.transient_variables_ordered + else: + order1 = contract1.storage_variables_ordered + order2 = contract2.storage_variables_ordered + + for idx, _ in enumerate(order1): + if len(order2) <= idx: + # Handle by MissingVariable + return + + variable1 = order1[idx] + variable2 = order2[idx] + if (variable1.name != variable2.name) or (variable1.type != variable2.type): + info: CHECK_INFO = [ + "Different variables between ", + contract1, + " and ", + contract2, + "\n", + ] + info += ["\t ", variable1, "\n"] + info += ["\t ", variable2, "\n"] + json = self.generate_result(info) + results.append(json) + + # Checking state variables with storage location + _check_internal(contract1, contract2, results, False) + # Checking state variables with transient location + _check_internal(contract1, contract2, results, True) return results @@ -236,22 +250,35 @@ def _contract2(self) -> Contract: def _check(self) -> List[Output]: contract1 = self._contract1() contract2 = self._contract2() - order1 = contract1.stored_state_variables_ordered - order2 = contract2.stored_state_variables_ordered - results = [] + results: List[Output] = [] - if len(order2) <= len(order1): - return [] + def _check_internal( + contract1: Contract, contract2: Contract, results: List[Output], is_transient: bool + ): + if is_transient: + order1 = contract1.transient_variables_ordered + order2 = contract2.transient_variables_ordered + else: + order1 = contract1.storage_variables_ordered + order2 = contract2.storage_variables_ordered - idx = len(order1) + if len(order2) <= len(order1): + return - while idx < len(order2): - variable2 = order2[idx] - info: CHECK_INFO = ["Extra variables in ", contract2, ": ", variable2, "\n"] - json = self.generate_result(info) - results.append(json) - idx = idx + 1 + idx = len(order1) + + while idx < len(order2): + variable2 = order2[idx] + info: CHECK_INFO = ["Extra variables in ", contract2, ": ", variable2, "\n"] + json = self.generate_result(info) + results.append(json) + idx = idx + 1 + + # Checking state variables with storage location + _check_internal(contract1, contract2, results, False) + # Checking state variables with transient location + _check_internal(contract1, contract2, results, True) return results diff --git a/slither/utils/upgradeability.py b/slither/utils/upgradeability.py index 59979bca64..bedf08d4b2 100644 --- a/slither/utils/upgradeability.py +++ b/slither/utils/upgradeability.py @@ -80,8 +80,8 @@ def compare( tainted-contracts: list[TaintedExternalContract] """ - order_vars1 = v1.stored_state_variables_ordered - order_vars2 = v2.stored_state_variables_ordered + order_vars1 = v1.storage_variables_ordered + v1.transient_variables_ordered + order_vars2 = v2.storage_variables_ordered + v2.transient_variables_ordered func_sigs1 = [function.solidity_signature for function in v1.functions] func_sigs2 = [function.solidity_signature for function in v2.functions] @@ -306,8 +306,8 @@ def get_missing_vars(v1: Contract, v2: Contract) -> List[StateVariable]: List of StateVariables from v1 missing in v2 """ results = [] - order_vars1 = v1.stored_state_variables_ordered - order_vars2 = v2.stored_state_variables_ordered + order_vars1 = v1.storage_variables_ordered + v1.transient_variables_ordered + order_vars2 = v2.storage_variables_ordered + v2.transient_variables_ordered if len(order_vars2) < len(order_vars1): for variable in order_vars1: if variable.name not in [v.name for v in order_vars2]: diff --git a/slither/vyper_parsing/declarations/contract.py b/slither/vyper_parsing/declarations/contract.py index 64fab1c549..ddf5161509 100644 --- a/slither/vyper_parsing/declarations/contract.py +++ b/slither/vyper_parsing/declarations/contract.py @@ -470,7 +470,7 @@ def parse_state_variables(self) -> None: assert var.name self._contract.variables_as_dict[var.name] = var - self._contract.add_variables_ordered([var]) + self._contract.add_state_variables_ordered([var]) # Interfaces can refer to constants self._contract.file_scope.variables[var.name] = var From 4b91acd5f5f7d458b3718ecbcf73ad72426e6452 Mon Sep 17 00:00:00 2001 From: Simone Date: Fri, 11 Oct 2024 16:05:00 +0200 Subject: [PATCH 63/71] Fix IR conversion when an Event selector is accessed --- slither/slithir/convert.py | 16 +++++++++++++--- slither/utils/function.py | 13 +++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/slither/slithir/convert.py b/slither/slithir/convert.py index 7d8aa543bf..d2380f4668 100644 --- a/slither/slithir/convert.py +++ b/slither/slithir/convert.py @@ -81,7 +81,7 @@ from slither.slithir.tmp_operations.tmp_new_structure import TmpNewStructure from slither.slithir.variables import Constant, ReferenceVariable, TemporaryVariable from slither.slithir.variables import TupleVariable -from slither.utils.function import get_function_id +from slither.utils.function import get_function_id, get_event_id from slither.utils.type import export_nested_types_from_variable from slither.utils.using_for import USING_FOR from slither.visitors.slithir.expression_to_slithir import ExpressionToSlithIR @@ -793,8 +793,18 @@ def propagate_types(ir: Operation, node: "Node"): # pylint: disable=too-many-lo assignment.set_node(ir.node) assignment.lvalue.set_type(ElementaryType("bytes4")) return assignment - if ir.variable_right == "selector" and isinstance( - ir.variable_left.type, (Function) + if ir.variable_right == "selector" and isinstance(ir.variable_left, (Event)): + assignment = Assignment( + ir.lvalue, + Constant(str(get_event_id(ir.variable_left.full_name))), + ElementaryType("bytes32"), + ) + assignment.set_expression(ir.expression) + assignment.set_node(ir.node) + assignment.lvalue.set_type(ElementaryType("bytes32")) + return assignment + if ir.variable_right == "selector" and ( + isinstance(ir.variable_left.type, (Function)) ): assignment = Assignment( ir.lvalue, diff --git a/slither/utils/function.py b/slither/utils/function.py index 34e6f221bb..64c098bfdd 100644 --- a/slither/utils/function.py +++ b/slither/utils/function.py @@ -12,3 +12,16 @@ def get_function_id(sig: str) -> int: digest = keccak.new(digest_bits=256) digest.update(sig.encode("utf-8")) return int("0x" + digest.hexdigest()[:8], 16) + + +def get_event_id(sig: str) -> int: + """' + Return the event id of the given signature + Args: + sig (str) + Return: + (int) + """ + digest = keccak.new(digest_bits=256) + digest.update(sig.encode("utf-8")) + return int("0x" + digest.hexdigest(), 16) From 1941e9160241c843c225cd886b461a6c206eb9e8 Mon Sep 17 00:00:00 2001 From: Feist Josselin Date: Thu, 17 Oct 2024 19:31:28 +0200 Subject: [PATCH 64/71] Improve selector parsing Add tests --- slither/core/solidity_types/function_type.py | 2 +- slither/core/variables/variable.py | 7 +-- slither/slithir/convert.py | 37 +++++++-------- tests/unit/slithir/test_data/selector.sol | 47 ++++++++++++++++++++ tests/unit/slithir/test_selector.py | 32 +++++++++++++ 5 files changed, 99 insertions(+), 26 deletions(-) create mode 100644 tests/unit/slithir/test_data/selector.sol create mode 100644 tests/unit/slithir/test_selector.py diff --git a/slither/core/solidity_types/function_type.py b/slither/core/solidity_types/function_type.py index 2d644148e0..8a328e361a 100644 --- a/slither/core/solidity_types/function_type.py +++ b/slither/core/solidity_types/function_type.py @@ -36,7 +36,7 @@ def storage_size(self) -> Tuple[int, bool]: def is_dynamic(self) -> bool: return False - def __str__(self): + def __str__(self) -> str: # Use x.type # x.name may be empty params = ",".join([str(x.type) for x in self._params]) diff --git a/slither/core/variables/variable.py b/slither/core/variables/variable.py index f9ef190246..4af350d31e 100644 --- a/slither/core/variables/variable.py +++ b/slither/core/variables/variable.py @@ -9,6 +9,7 @@ if TYPE_CHECKING: from slither.core.expressions.expression import Expression + from slither.core.declarations import Function # pylint: disable=too-many-instance-attributes class Variable(SourceMapping): @@ -16,7 +17,7 @@ def __init__(self) -> None: super().__init__() self._name: Optional[str] = None self._initial_expression: Optional["Expression"] = None - self._type: Optional[Type] = None + self._type: Optional[Union[List, Type, "Function", str]] = None self._initialized: Optional[bool] = None self._visibility: Optional[str] = None self._is_constant = False @@ -77,7 +78,7 @@ def name(self, name: str) -> None: self._name = name @property - def type(self) -> Optional[Type]: + def type(self) -> Optional[Union[List, Type, "Function", str]]: return self._type @type.setter @@ -120,7 +121,7 @@ def visibility(self) -> Optional[str]: def visibility(self, v: str) -> None: self._visibility = v - def set_type(self, t: Optional[Union[List, Type, str]]) -> None: + def set_type(self, t: Optional[Union[List, Type, "Function", str]]) -> None: if isinstance(t, str): self._type = ElementaryType(t) return diff --git a/slither/slithir/convert.py b/slither/slithir/convert.py index d2380f4668..a689cff4cd 100644 --- a/slither/slithir/convert.py +++ b/slither/slithir/convert.py @@ -279,20 +279,6 @@ def is_temporary(ins: Operation) -> bool: ) -def _make_function_type(func: Function) -> FunctionType: - parameters = [] - returns = [] - for parameter in func.parameters: - v = FunctionTypeVariable() - v.name = parameter.name - parameters.append(v) - for return_var in func.returns: - v = FunctionTypeVariable() - v.name = return_var.name - returns.append(v) - return FunctionType(parameters, returns) - - # endregion ################################################################################### ################################################################################### @@ -794,9 +780,13 @@ def propagate_types(ir: Operation, node: "Node"): # pylint: disable=too-many-lo assignment.lvalue.set_type(ElementaryType("bytes4")) return assignment if ir.variable_right == "selector" and isinstance(ir.variable_left, (Event)): + # the event selector returns a bytes32, which is different from the error/function selector + # which returns a bytes4 assignment = Assignment( ir.lvalue, - Constant(str(get_event_id(ir.variable_left.full_name))), + Constant( + str(get_event_id(ir.variable_left.full_name)), ElementaryType("bytes32") + ), ElementaryType("bytes32"), ) assignment.set_expression(ir.expression) @@ -808,7 +798,10 @@ def propagate_types(ir: Operation, node: "Node"): # pylint: disable=too-many-lo ): assignment = Assignment( ir.lvalue, - Constant(str(get_function_id(ir.variable_left.type.full_name))), + Constant( + str(get_function_id(ir.variable_left.type.full_name)), + ElementaryType("bytes4"), + ), ElementaryType("bytes4"), ) assignment.set_expression(ir.expression) @@ -836,10 +829,10 @@ def propagate_types(ir: Operation, node: "Node"): # pylint: disable=too-many-lo targeted_function = next( (x for x in ir_func.contract.functions if x.name == str(ir.variable_right)) ) - t = _make_function_type(targeted_function) - ir.lvalue.set_type(t) + # t = _make_function_type(targeted_function) + ir.lvalue.set_type(targeted_function) elif isinstance(left, (Variable, SolidityVariable)): - t = ir.variable_left.type + t = left.type elif isinstance(left, (Contract, Enum, Structure)): t = UserDefinedType(left) # can be None due to temporary operation @@ -856,10 +849,10 @@ def propagate_types(ir: Operation, node: "Node"): # pylint: disable=too-many-lo ir.lvalue.set_type(elems[elem].type) else: assert isinstance(type_t, Contract) - # Allow type propagtion as a Function + # Allow type propagation as a Function # Only for reference variables # This allows to track the selector keyword - # We dont need to check for function collision, as solc prevents the use of selector + # We don't need to check for function collision, as solc prevents the use of selector # if there are multiple functions with the same name f = next( (f for f in type_t.functions if f.name == ir.variable_right), @@ -868,7 +861,7 @@ def propagate_types(ir: Operation, node: "Node"): # pylint: disable=too-many-lo if f: ir.lvalue.set_type(f) else: - # Allow propgation for variable access through contract's name + # Allow propagation for variable access through contract's name # like Base_contract.my_variable v = next( ( diff --git a/tests/unit/slithir/test_data/selector.sol b/tests/unit/slithir/test_data/selector.sol new file mode 100644 index 0000000000..60ec33cd66 --- /dev/null +++ b/tests/unit/slithir/test_data/selector.sol @@ -0,0 +1,47 @@ +interface I{ + function testFunction(uint a) external ; +} + +contract A{ + function testFunction() public{} +} + +contract Test{ + event TestEvent(); + struct St{ + uint a; + } + error TestError(); + + function testFunction(uint a) public {} + + + function testFunctionStructure(St memory s) public {} + + function returnEvent() public returns (bytes32){ + return TestEvent.selector; + } + + function returnError() public returns (bytes4){ + return TestError.selector; + } + + + function returnFunctionFromContract() public returns (bytes4){ + return I.testFunction.selector; + } + + + function returnFunction() public returns (bytes4){ + return this.testFunction.selector; + } + + function returnFunctionWithStructure() public returns (bytes4){ + return this.testFunctionStructure.selector; + } + + function returnFunctionThroughLocaLVar() public returns(bytes4){ + A a; + return a.testFunction.selector; + } +} \ No newline at end of file diff --git a/tests/unit/slithir/test_selector.py b/tests/unit/slithir/test_selector.py new file mode 100644 index 0000000000..34643b58d8 --- /dev/null +++ b/tests/unit/slithir/test_selector.py @@ -0,0 +1,32 @@ +from pathlib import Path +from slither import Slither +from slither.slithir.operations import Assignment +from slither.slithir.variables import Constant + +TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" + + +func_to_results = { + "returnEvent()": "16700440330922901039223184000601971290390760458944929668086539975128325467771", + "returnError()": "224292994", + "returnFunctionFromContract()": "890000139", + "returnFunction()": "890000139", + "returnFunctionWithStructure()": "1430834845", + "returnFunctionThroughLocaLVar()": "3781905051", +} + + +def test_enum_max_min(solc_binary_path) -> None: + solc_path = solc_binary_path("0.8.19") + slither = Slither(Path(TEST_DATA_DIR, "selector.sol").as_posix(), solc=solc_path) + + contract = slither.get_contract_from_name("Test")[0] + + for func_name, value in func_to_results.items(): + f = contract.get_function_from_signature(func_name) + assignment = f.slithir_operations[0] + assert ( + isinstance(assignment, Assignment) + and isinstance(assignment.rvalue, Constant) + and assignment.rvalue.value == value + ) From 1ae2e9eb368e3238b21d5d3e28596381808825d7 Mon Sep 17 00:00:00 2001 From: Feist Josselin Date: Thu, 17 Oct 2024 20:39:09 +0200 Subject: [PATCH 65/71] Remove unused import --- slither/slithir/convert.py | 1 - 1 file changed, 1 deletion(-) diff --git a/slither/slithir/convert.py b/slither/slithir/convert.py index a689cff4cd..1e67064aa2 100644 --- a/slither/slithir/convert.py +++ b/slither/slithir/convert.py @@ -36,7 +36,6 @@ ) from slither.core.solidity_types.type import Type from slither.core.solidity_types.type_alias import TypeAliasTopLevel, TypeAlias -from slither.core.variables.function_type_variable import FunctionTypeVariable from slither.core.variables.state_variable import StateVariable from slither.core.variables.variable import Variable from slither.slithir.exceptions import SlithIRError From 5488d50b759cf574c8a8ea31e7e992890c0e4f2b Mon Sep 17 00:00:00 2001 From: Feist Josselin Date: Thu, 17 Oct 2024 20:53:56 +0200 Subject: [PATCH 66/71] Remove commented code --- slither/slithir/convert.py | 1 - 1 file changed, 1 deletion(-) diff --git a/slither/slithir/convert.py b/slither/slithir/convert.py index 1e67064aa2..cfeb0fe398 100644 --- a/slither/slithir/convert.py +++ b/slither/slithir/convert.py @@ -828,7 +828,6 @@ def propagate_types(ir: Operation, node: "Node"): # pylint: disable=too-many-lo targeted_function = next( (x for x in ir_func.contract.functions if x.name == str(ir.variable_right)) ) - # t = _make_function_type(targeted_function) ir.lvalue.set_type(targeted_function) elif isinstance(left, (Variable, SolidityVariable)): t = left.type From f6b250944569a7e2eb6380f7593f89bc84bddc56 Mon Sep 17 00:00:00 2001 From: Feist Josselin Date: Thu, 17 Oct 2024 22:41:28 +0200 Subject: [PATCH 67/71] Add try/catch, add tests, refactor guidance/echidna to reduce pylint exceptions --- slither/printers/guidance/echidna.py | 110 +++++++++++------- tests/unit/slithir/test_constantfolding.py | 22 ++++ .../slithir/test_data/constantfolding.sol | 19 +++ 3 files changed, 106 insertions(+), 45 deletions(-) create mode 100644 tests/unit/slithir/test_constantfolding.py create mode 100644 tests/unit/slithir/test_data/constantfolding.sol diff --git a/slither/printers/guidance/echidna.py b/slither/printers/guidance/echidna.py index 057e782697..7e76cec0db 100644 --- a/slither/printers/guidance/echidna.py +++ b/slither/printers/guidance/echidna.py @@ -13,6 +13,7 @@ from slither.core.expressions import NewContract from slither.core.slither_core import SlitherCore from slither.core.solidity_types import TypeAlias +from slither.core.source_mapping.source_mapping import SourceMapping from slither.core.variables.state_variable import StateVariable from slither.core.variables.variable import Variable from slither.printers.abstract_printer import AbstractPrinter @@ -179,7 +180,66 @@ class ConstantValue(NamedTuple): # pylint: disable=inherit-non-class,too-few-pu type: str -def _extract_constants_from_irs( # pylint: disable=too-many-branches,too-many-nested-blocks +def _extract_constant_from_read( + ir: Operation, + r: SourceMapping, + all_cst_used: List[ConstantValue], + all_cst_used_in_binary: Dict[str, List[ConstantValue]], + context_explored: Set[Node], +) -> None: + var_read = r.points_to_origin if isinstance(r, ReferenceVariable) else r + # Do not report struct_name in a.struct_name + if isinstance(ir, Member): + return + if isinstance(var_read, Variable) and var_read.is_constant: + # In case of type conversion we use the destination type + if isinstance(ir, TypeConversion): + if isinstance(ir.type, TypeAlias): + value_type = ir.type.type + else: + value_type = ir.type + else: + value_type = var_read.type + try: + value = ConstantFolding(var_read.expression, value_type).result() + all_cst_used.append(ConstantValue(str(value), str(value_type))) + except NotConstant: + pass + if isinstance(var_read, Constant): + all_cst_used.append(ConstantValue(str(var_read.value), str(var_read.type))) + if isinstance(var_read, StateVariable): + if var_read.node_initialization: + if var_read.node_initialization.irs: + if var_read.node_initialization in context_explored: + return + context_explored.add(var_read.node_initialization) + _extract_constants_from_irs( + var_read.node_initialization.irs, + all_cst_used, + all_cst_used_in_binary, + context_explored, + ) + + +def _extract_constant_from_binary( + ir: Binary, + all_cst_used: List[ConstantValue], + all_cst_used_in_binary: Dict[str, List[ConstantValue]], +): + for r in ir.read: + if isinstance(r, Constant): + all_cst_used_in_binary[str(ir.type)].append(ConstantValue(str(r.value), str(r.type))) + if isinstance(ir.variable_left, Constant) or isinstance(ir.variable_right, Constant): + if ir.lvalue: + try: + type_ = ir.lvalue.type + cst = ConstantFolding(ir.expression, type_).result() + all_cst_used.append(ConstantValue(str(cst.value), str(type_))) + except NotConstant: + pass + + +def _extract_constants_from_irs( irs: List[Operation], all_cst_used: List[ConstantValue], all_cst_used_in_binary: Dict[str, List[ConstantValue]], @@ -187,21 +247,7 @@ def _extract_constants_from_irs( # pylint: disable=too-many-branches,too-many-n ) -> None: for ir in irs: if isinstance(ir, Binary): - for r in ir.read: - if isinstance(r, Constant): - all_cst_used_in_binary[str(ir.type)].append( - ConstantValue(str(r.value), str(r.type)) - ) - if isinstance(ir.variable_left, Constant) or isinstance( - ir.variable_right, Constant - ): - if ir.lvalue: - try: - type_ = ir.lvalue.type - cst = ConstantFolding(ir.expression, type_).result() - all_cst_used.append(ConstantValue(str(cst.value), str(type_))) - except NotConstant: - pass + _extract_constant_from_binary(ir, all_cst_used, all_cst_used_in_binary) if isinstance(ir, TypeConversion): if isinstance(ir.variable, Constant): if isinstance(ir.type, TypeAlias): @@ -222,35 +268,9 @@ def _extract_constants_from_irs( # pylint: disable=too-many-branches,too-many-n except ValueError: # index could fail; should never happen in working solidity code pass for r in ir.read: - var_read = r.points_to_origin if isinstance(r, ReferenceVariable) else r - # Do not report struct_name in a.struct_name - if isinstance(ir, Member): - continue - if isinstance(var_read, Variable) and var_read.is_constant: - # In case of type conversion we use the destination type - if isinstance(ir, TypeConversion): - if isinstance(ir.type, TypeAlias): - value_type = ir.type.type - else: - value_type = ir.type - else: - value_type = var_read.type - value = ConstantFolding(var_read.expression, value_type).result() - all_cst_used.append(ConstantValue(str(value), str(value_type))) - if isinstance(var_read, Constant): - all_cst_used.append(ConstantValue(str(var_read.value), str(var_read.type))) - if isinstance(var_read, StateVariable): - if var_read.node_initialization: - if var_read.node_initialization.irs: - if var_read.node_initialization in context_explored: - continue - context_explored.add(var_read.node_initialization) - _extract_constants_from_irs( - var_read.node_initialization.irs, - all_cst_used, - all_cst_used_in_binary, - context_explored, - ) + _extract_constant_from_read( + ir, r, all_cst_used, all_cst_used_in_binary, context_explored + ) def _extract_constants( diff --git a/tests/unit/slithir/test_constantfolding.py b/tests/unit/slithir/test_constantfolding.py new file mode 100644 index 0000000000..fcf00035bd --- /dev/null +++ b/tests/unit/slithir/test_constantfolding.py @@ -0,0 +1,22 @@ +from pathlib import Path + +from slither import Slither +from slither.printers.guidance.echidna import _extract_constants, ConstantValue + +TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" + + +def test_enum_max_min(solc_binary_path) -> None: + solc_path = solc_binary_path("0.8.19") + slither = Slither(Path(TEST_DATA_DIR, "constantfolding.sol").as_posix(), solc=solc_path) + + contracts = slither.get_contract_from_name("A") + + constants = _extract_constants(contracts)[0]["A"]["use()"] + + assert set(constants) == { + ConstantValue(value="2", type="uint256"), + ConstantValue(value="10", type="uint256"), + ConstantValue(value="100", type="uint256"), + ConstantValue(value="4294967295", type="uint32"), + } diff --git a/tests/unit/slithir/test_data/constantfolding.sol b/tests/unit/slithir/test_data/constantfolding.sol new file mode 100644 index 0000000000..aef4a24270 --- /dev/null +++ b/tests/unit/slithir/test_data/constantfolding.sol @@ -0,0 +1,19 @@ +type MyType is uint256; + +contract A{ + + enum E{ + a,b,c + } + + + uint a = 10; + E b = type(E).max; + uint c = type(uint32).max; + MyType d = MyType.wrap(100); + + function use() public returns(uint){ + E e = b; + return a +c + MyType.unwrap(d); + } +} \ No newline at end of file From 1bbc13b3086100f20180017851680fb0cfdda313 Mon Sep 17 00:00:00 2001 From: Simone Date: Thu, 24 Oct 2024 13:11:47 +0200 Subject: [PATCH 68/71] Use the new API --- slither/printers/guidance/echidna.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/slither/printers/guidance/echidna.py b/slither/printers/guidance/echidna.py index c3cb16a56c..07ca3427f2 100644 --- a/slither/printers/guidance/echidna.py +++ b/slither/printers/guidance/echidna.py @@ -141,10 +141,10 @@ def _extract_assert(contracts: List[Contract]) -> Dict[str, Dict[str, List[Dict] for contract in contracts: functions_using_assert: Dict[str, List[Dict]] = defaultdict(list) for f in contract.functions_entry_points: - for node in f.all_nodes(): - if SolidityFunction("assert(bool)") in node.solidity_calls and node.source_mapping: + for ir in f.all_solidity_calls(): + if ir.function == SolidityFunction("assert(bool)") and ir.node.source_mapping: func_name = _get_name(f) - functions_using_assert[func_name].append(node.source_mapping.to_json()) + functions_using_assert[func_name].append(ir.node.source_mapping.to_json()) break if functions_using_assert: ret[contract.name] = functions_using_assert From 537ee273c28096c1147a3ed31109bab0dcdc7c1e Mon Sep 17 00:00:00 2001 From: Simone Date: Thu, 24 Oct 2024 13:24:55 +0200 Subject: [PATCH 69/71] Extract multiple assert in a single function --- slither/printers/guidance/echidna.py | 1 - 1 file changed, 1 deletion(-) diff --git a/slither/printers/guidance/echidna.py b/slither/printers/guidance/echidna.py index 07ca3427f2..48e40a90f3 100644 --- a/slither/printers/guidance/echidna.py +++ b/slither/printers/guidance/echidna.py @@ -145,7 +145,6 @@ def _extract_assert(contracts: List[Contract]) -> Dict[str, Dict[str, List[Dict] if ir.function == SolidityFunction("assert(bool)") and ir.node.source_mapping: func_name = _get_name(f) functions_using_assert[func_name].append(ir.node.source_mapping.to_json()) - break if functions_using_assert: ret[contract.name] = functions_using_assert return ret From 2c401e6ca30bc154e6941c5871fa3b6886b344c8 Mon Sep 17 00:00:00 2001 From: Simone Date: Tue, 3 Dec 2024 18:05:29 +0100 Subject: [PATCH 70/71] Fix reorder arguments when a function is overridden with diff param names --- slither/slithir/convert.py | 42 +++++++++++++------ tests/unit/slithir/test_argument_reorder.py | 32 ++++++++++++++ .../test_overridden_function.sol | 8 ++++ 3 files changed, 70 insertions(+), 12 deletions(-) create mode 100644 tests/unit/slithir/test_data/argument_reorder/test_overridden_function.sol diff --git a/slither/slithir/convert.py b/slither/slithir/convert.py index e4798cced5..c220952261 100644 --- a/slither/slithir/convert.py +++ b/slither/slithir/convert.py @@ -380,7 +380,7 @@ def get_declared_param_names( InternalDynamicCall, EventCall, ] -) -> Optional[List[str]]: +) -> Optional[List[List[str]]]: """ Given a call operation, return the list of parameter names, in the order listed in the function declaration. @@ -388,26 +388,31 @@ def get_declared_param_names( ins - The call instruction #### Possible Returns - List[str] - - A list of the parameters in declaration order + List[List[str]] - + A list of list of parameters in declaration order. Note only if it's a Function there can be multiple list None - Workaround: Unable to obtain list of parameters in declaration order """ if isinstance(ins, NewStructure): - return [x.name for x in ins.structure.elems_ordered if not isinstance(x.type, MappingType)] + return [ + [x.name for x in ins.structure.elems_ordered if not isinstance(x.type, MappingType)] + ] if isinstance(ins, (InternalCall, LibraryCall, HighLevelCall)): if isinstance(ins.function, Function): - return [p.name for p in ins.function.parameters] + res = [[p.name for p in ins.function.parameters]] + for f in ins.function.overrides: + res.append([p.name for p in f.parameters]) + return res return None if isinstance(ins, InternalDynamicCall): - return [p.name for p in ins.function_type.params] + return [[p.name for p in ins.function_type.params]] assert isinstance(ins, (EventCall, NewContract)) return None def reorder_arguments( - args: List[Variable], call_names: List[str], decl_names: List[str] + args: List[Variable], call_names: List[str], decl_names: List[List[str]] ) -> List[Variable]: """ Reorder named struct constructor arguments so that they match struct declaration ordering rather @@ -419,17 +424,30 @@ def reorder_arguments( names - Parameter names in call order decl_names - - Parameter names in declaration order + List of list of parameter names in declaration order #### Returns Reordered arguments to constructor call, now in declaration order """ assert len(args) == len(call_names) - assert len(call_names) == len(decl_names) + for names in decl_names: + assert len(call_names) == len(names) args_ret = [] - for n in decl_names: - ind = call_names.index(n) - args_ret.append(args[ind]) + index_seen = [] + + for names in decl_names: + if len(index_seen) == len(args): + break + + for n in names: + try: + ind = call_names.index(n) + if ind in index_seen: + continue + except ValueError: + continue + index_seen.append(ind) + args_ret.append(args[ind]) return args_ret diff --git a/tests/unit/slithir/test_argument_reorder.py b/tests/unit/slithir/test_argument_reorder.py index 12f5bd7f28..0743bf6949 100644 --- a/tests/unit/slithir/test_argument_reorder.py +++ b/tests/unit/slithir/test_argument_reorder.py @@ -63,3 +63,35 @@ def test_internal_call_reorder(solc_binary_path) -> None: isinstance(internal_calls[0].arguments[2], Constant) and internal_calls[0].arguments[2].value == 5 ) + +def test_overridden_function_reorder(solc_binary_path) -> None: + solc_path = solc_binary_path("0.8.15") + slither = Slither( + Path(ARG_REORDER_TEST_ROOT, "test_overridden_function.sol").as_posix(), solc=solc_path + ) + + operations = slither.contracts[0].functions[1].slithir_operations + internal_calls = [x for x in operations if isinstance(x, InternalCall)] + assert len(internal_calls) == 1 + + assert ( + isinstance(internal_calls[0].arguments[0], Constant) + and internal_calls[0].arguments[0].value == 34 + ) + assert ( + isinstance(internal_calls[0].arguments[1], Constant) + and internal_calls[0].arguments[1].value == 23 + ) + + operations = slither.contracts[1].functions[1].slithir_operations + internal_calls = [x for x in operations if isinstance(x, InternalCall)] + assert len(internal_calls) == 1 + + assert ( + isinstance(internal_calls[0].arguments[0], Constant) + and internal_calls[0].arguments[0].value == 34 + ) + assert ( + isinstance(internal_calls[0].arguments[1], Constant) + and internal_calls[0].arguments[1].value == 23 + ) diff --git a/tests/unit/slithir/test_data/argument_reorder/test_overridden_function.sol b/tests/unit/slithir/test_data/argument_reorder/test_overridden_function.sol new file mode 100644 index 0000000000..2be5e45a15 --- /dev/null +++ b/tests/unit/slithir/test_data/argument_reorder/test_overridden_function.sol @@ -0,0 +1,8 @@ +contract A { + function a(uint256 q, uint256 e) internal virtual {} + function b() public { a({e: 23, q: 34}); } +} + +contract B is A { + function a(uint256 w, uint256 q) internal override {} +} \ No newline at end of file From c97abac28dbcf1f7974524dbec30c7c318bc906e Mon Sep 17 00:00:00 2001 From: Simone <79767264+smonicas@users.noreply.github.com> Date: Tue, 3 Dec 2024 18:13:50 +0100 Subject: [PATCH 71/71] Update tests/unit/slithir/test_argument_reorder.py Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- tests/unit/slithir/test_argument_reorder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/slithir/test_argument_reorder.py b/tests/unit/slithir/test_argument_reorder.py index 0743bf6949..5d1142694f 100644 --- a/tests/unit/slithir/test_argument_reorder.py +++ b/tests/unit/slithir/test_argument_reorder.py @@ -64,6 +64,7 @@ def test_internal_call_reorder(solc_binary_path) -> None: and internal_calls[0].arguments[2].value == 5 ) + def test_overridden_function_reorder(solc_binary_path) -> None: solc_path = solc_binary_path("0.8.15") slither = Slither(