/* This file is part of solidity. solidity is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. solidity is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with solidity. If not, see . */ // SPDX-License-Identifier: GPL-3.0 /// Unit tests for libsolidity/analysis/FunctionCallGraph.h #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace ranges; using namespace solidity::langutil; using namespace solidity::frontend; using ContractMap = map; using EdgeMap = map< FunctionCallGraphBuilder::Node, set, FunctionCallGraphBuilder::CompareByID >; using EdgeNames = set>; namespace { struct ParsingResult { ASTPointer ast; shared_ptr globalContext; ContractMap contractMap; }; ParsingResult parseAndAnalyzeContracts(string const& _path, string _sourceCode) { EVMVersion evmVersion; ErrorList errorList; ErrorReporter errorReporter(errorList); Parser parser(errorReporter, evmVersion, /* _errorRecovery = */ false); auto scanner = make_shared(CharStream(std::move(_sourceCode), _path)); ASTPointer ast = parser.parse(scanner); if (!ast) { soltestAssert(!Error::containsOnlyWarnings(errorReporter.errors()), "Parser returned null but did not report error."); return {}; } soltestAssert( ranges::all_of(ast->nodes(), [](auto const& _node){ return !dynamic_cast(_node.get()); }), "For simplicity this test suite supports only files without imports." ); // Do only just enough analysis to satisfy FunctionCallGraph's requirements. Scoper::assignScopes(*ast); auto globalContext = make_shared(); NameAndTypeResolver resolver(*globalContext, evmVersion, errorReporter); SyntaxChecker syntaxChecker(errorReporter, /* _useYulOptimizer = */ false); DocStringTagParser docStringTagParser(errorReporter); DeclarationTypeChecker declarationTypeChecker(errorReporter, evmVersion); ContractLevelChecker contractLevelChecker(errorReporter); DocStringAnalyser docStringAnalyser(errorReporter); TypeChecker typeChecker(evmVersion, errorReporter); PostTypeChecker postTypeChecker(errorReporter); soltestAssert(syntaxChecker.checkSyntax(*ast), "Syntax check failed."); soltestAssert(docStringTagParser.parseDocStrings(*ast), "Docstring tag parser failed."); soltestAssert(resolver.registerDeclarations(*ast), "Declaration registration failed."); soltestAssert(resolver.performImports(*ast, {}), "Import resolution failed."); resolver.warnHomonymDeclarations(); soltestAssert(resolver.resolveNamesAndTypes(*ast), "Type and name resolution failed."); soltestAssert(declarationTypeChecker.check(*ast), "Declaration type check failed."); soltestAssert(contractLevelChecker.check(*ast), "Contract-level checks failed."); soltestAssert(docStringAnalyser.analyseDocStrings(*ast), "Docstring analysis failed."); soltestAssert(typeChecker.checkTypeRequirements(*ast), "Type check failed."); soltestAssert(postTypeChecker.check(*ast), "Post type check failed."); soltestAssert(postTypeChecker.finalize(), "Post type check failed to finalize."); ContractMap contractMap; for (ASTPointer const& node: ast->nodes()) { if (auto const* contract = dynamic_cast(node.get())) { soltestAssert(contractMap.count(contract->name()) == 0, "Contract names in the source are not unique."); contractMap[contract->name()] = contract; } } // NOTE: The code in test cases is expected to be correct so we can keep error handling simple // here and just assert that there are no compilation errors. solAssert(Error::containsOnlyWarnings(errorReporter.errors()), ""); return {ast, globalContext, std::move(contractMap)}; } EdgeNames edgeNames(EdgeMap const& _edgeMap) { EdgeNames names; for (auto const& [edgeStart, allEnds]: _edgeMap) for (auto const& edgeEnd: allEnds) names.emplace(toString(edgeStart), toString(edgeEnd)); return names; } void buildGraphsAndCheckExpectations( ContractMap const& _parsedContracts, map const& _expectedEdges, map> const& _expectedCreatedContractSets ) { using GraphPtr = unique_ptr; auto getName = [](auto const* _contract){ return _contract->name(); }; auto notEmpty = [](set const& _set){ return !_set.empty(); }; soltestAssert( (_expectedCreatedContractSets | views::values | views::remove_if(notEmpty)).empty(), "Contracts that are not expected to create other contracts should not be included in _expectedCreatedContractSets." ); soltestAssert( (_parsedContracts | views::keys | to()) == (_expectedEdges | views::keys | to()) && (ranges::views::set_difference(_expectedCreatedContractSets | views::keys, _parsedContracts | views::keys)).empty(), "Contracts listed in expectations do not match contracts actually found in the source file." ); for (string const& contractName: _expectedEdges | views::keys) soltestAssert( (ranges::views::set_difference(valueOrDefault(_expectedCreatedContractSets, contractName, {}), _parsedContracts | views::keys)).empty(), "Inconsistent expectations: contract expected to be created but not to be present in the source file." ); map edges; map> createdContractSets; for (string const& contractName: _expectedEdges | views::keys) { GraphPtr callGraph = FunctionCallGraphBuilder::create(*_parsedContracts.at(contractName)); edges[contractName] = edgeNames(callGraph->edges); if (!callGraph->createdContracts.empty()) createdContractSets[contractName] = callGraph->createdContracts | views::transform(getName) | to>(); BOOST_TEST(&callGraph->contract == _parsedContracts.at(contractName)); } BOOST_CHECK_EQUAL(edges, _expectedEdges); BOOST_CHECK_EQUAL(createdContractSets, _expectedCreatedContractSets); } } // namespace namespace std { // TMP: Try to move these operators from std to boost::test_tools::tt_detail where they belong ostream& operator<<(ostream& _out, EdgeNames const& _edgeNames); ostream& operator<<(ostream& _out, EdgeNames const& _edgeNames) { for (auto const& edge: _edgeNames | to() | actions::sort(std::less())) _out << " " << get<0>(edge) << " -> " << get<1>(edge) << endl; return _out; } ostream& operator<<(ostream& _out, set const& _set); ostream& operator<<(ostream& _out, set const& _set) { _out << "{" << (_set | views::join(", ") | to()) << "}"; return _out; } ostream& operator<<(ostream& _out, map const& _edgeSets); ostream& operator<<(ostream& _out, map const& _edgeSets) { // Extra newline for error report readability. Otherwise the first line does not start at the first column. _out << endl; for (auto const &[contractName, edges]: _edgeSets) { _out << contractName << ":" << endl; _out << edges; } return _out; } ostream& operator<<(ostream& _out, map> const& _map); ostream& operator<<(ostream& _out, map> const& _map) { // Extra newline for error report readability. Otherwise the first line does not start at the first column. _out << endl; for (auto const &[key, value]: _map) _out << key << ": " << value << endl; return _out; } } // namespace std namespace solidity::frontend::test { BOOST_AUTO_TEST_SUITE(FunctionCallGraphTest) BOOST_AUTO_TEST_CASE(only_definitions) { ParsingResult parsingResult = parseAndAnalyzeContracts(boost::unit_test::framework::current_test_case().p_name, R"( function free() {} library L { function ext() external {} function pub() public {} function inr() internal {} function prv() private {} } contract C { function ext() external {} function pub() public {} function inr() internal {} function prv() private {} } )"s); map expectedEdges = { {"C", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function C.ext()"}, {"Entry", "function C.pub()"}, }}, {"L", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function L.ext()"}, {"Entry", "function L.pub()"}, }}, }; buildGraphsAndCheckExpectations(parsingResult.contractMap, expectedEdges, {}); } BOOST_AUTO_TEST_CASE(ordinary_calls) { ParsingResult parsingResult = parseAndAnalyzeContracts(boost::unit_test::framework::current_test_case().p_name, R"( function free() {} library L { function ext() external { pub(); inr(); } function pub() public { inr(); } function inr() internal { prv(); } function prv() private { free(); free(); } } contract C { function ext() external { pub(); } function pub() public { inr(); prv(); free(); } function inr() internal { prv(); L.inr(); } function prv() private { free(); free(); } } )"s); map expectedEdges = { {"C", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function C.ext()"}, {"Entry", "function C.pub()"}, {"function C.ext()", "function C.pub()"}, {"function C.pub()", "function C.inr()"}, {"function C.pub()", "function C.prv()"}, {"function C.pub()", "function free()"}, {"function C.inr()", "function C.prv()"}, {"function C.inr()", "function L.inr()"}, {"function C.prv()", "function free()"}, {"function L.inr()", "function L.prv()"}, {"function L.prv()", "function free()"}, }}, {"L", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function L.ext()"}, {"Entry", "function L.pub()"}, {"function L.ext()", "function L.pub()"}, {"function L.ext()", "function L.inr()"}, {"function L.pub()", "function L.inr()"}, {"function L.inr()", "function L.prv()"}, {"function L.prv()", "function free()"}, }}, }; buildGraphsAndCheckExpectations(parsingResult.contractMap, expectedEdges, {}); } BOOST_AUTO_TEST_CASE(call_chains_through_externals) { ParsingResult parsingResult = parseAndAnalyzeContracts(boost::unit_test::framework::current_test_case().p_name, R"( library L { function ext() external { C(address(0x0)).ext(); } function pub() public {} function inr() internal {} function prv() private {} } contract C { function ext() external {} function pub() public {} function inr() internal {} function prv() private {} function ext2() external { this.ext(); this.pub(); L.ext(); L.pub(); } function pub2() public { this.ext(); this.pub(); L.ext(); L.pub(); } function pub3() public { C(address(0x0)).ext(); } } )"s); map expectedEdges = { {"C", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function C.ext()"}, {"Entry", "function C.ext2()"}, {"Entry", "function C.pub()"}, {"Entry", "function C.pub2()"}, {"Entry", "function C.pub3()"}, }}, {"L", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function L.ext()"}, {"Entry", "function L.pub()"}, }}, }; buildGraphsAndCheckExpectations(parsingResult.contractMap, expectedEdges, {}); } BOOST_AUTO_TEST_CASE(calls_from_constructors) { ParsingResult parsingResult = parseAndAnalyzeContracts(boost::unit_test::framework::current_test_case().p_name, R"( function free() returns (uint) {} library L { function ext() external {} } contract C { constructor() { this.ext(); inr(); L.ext(); free(); } function ext() external {} function inr() internal {} } contract D { uint a = this.ext(); uint b = inr(); uint c = free(); function ext() external returns (uint) {} function inr() internal returns (uint) {} } )"s); map expectedEdges = { {"C", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function C.ext()"}, {"EntryCreation", "constructor of C"}, {"EntryCreation", "function C.inr()"}, {"EntryCreation", "function free()"}, }}, {"D", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function D.ext()"}, {"EntryCreation", "function D.inr()"}, {"EntryCreation", "function free()"}, }}, {"L", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function L.ext()"}, }}, }; buildGraphsAndCheckExpectations(parsingResult.contractMap, expectedEdges, {}); } BOOST_AUTO_TEST_CASE(calls_to_constructors) { ParsingResult parsingResult = parseAndAnalyzeContracts(boost::unit_test::framework::current_test_case().p_name, R"( function free() { new D(); } library L { function ext() external { new C(); new D(); inr(); } function inr() internal { new C(); new D(); free(); } } contract C { constructor() { new D(); } function ext() external { new D(); inr(); } function inr() internal { new D(); free(); } } contract D {} )"s); map expectedEdges = { {"C", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function C.ext()"}, {"EntryCreation", "constructor of C"}, {"function C.ext()", "function C.inr()"}, {"function C.inr()", "function free()"}, }}, {"D", { {"InternalDispatch", "InternalCreationDispatch"}, }}, {"L", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function L.ext()"}, {"function L.ext()", "function L.inr()"}, {"function L.inr()", "function free()"}, }}, }; map> expectedCreatedContracts = { {"C", {"D"}}, {"L", {"C", "D"}}, }; buildGraphsAndCheckExpectations(parsingResult.contractMap, expectedEdges, expectedCreatedContracts); } BOOST_AUTO_TEST_CASE(inherited_constructors) { ParsingResult parsingResult = parseAndAnalyzeContracts(boost::unit_test::framework::current_test_case().p_name, R"( function free() {} library L { function ext() external { inr(); } function inr() internal { free(); } } contract C { constructor() { inr(); free(); } function ext() external { inr(); } function inr() internal { free(); } } contract D { constructor() { L.ext(); } } contract E is C {} contract F is C, D(), E { constructor() E() C() {} } contract G is E() { function ext2() external { new F(); } } )"s); map expectedEdges = { {"C", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function C.ext()"}, {"EntryCreation", "constructor of C"}, {"EntryCreation", "function C.inr()"}, {"EntryCreation", "function free()"}, {"function C.ext()", "function C.inr()"}, {"function C.inr()", "function free()"}, }}, {"D", { {"InternalDispatch", "InternalCreationDispatch"}, {"EntryCreation", "constructor of D"}, }}, {"E", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function C.ext()"}, {"EntryCreation", "constructor of C"}, {"EntryCreation", "function C.inr()"}, {"EntryCreation", "function free()"}, {"function C.ext()", "function C.inr()"}, {"function C.inr()", "function free()"}, }}, {"F", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function C.ext()"}, {"EntryCreation", "constructor of C"}, {"EntryCreation", "function C.inr()"}, {"EntryCreation", "function free()"}, {"constructor of C", "constructor of D"}, {"constructor of D", "constructor of F"}, {"function C.ext()", "function C.inr()"}, {"function C.inr()", "function free()"}, }}, {"G", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function C.ext()"}, {"Entry", "function G.ext2()"}, {"EntryCreation", "constructor of C"}, {"EntryCreation", "function C.inr()"}, {"EntryCreation", "function free()"}, {"EntryCreation", "function C.inr()"}, {"EntryCreation", "function free()"}, {"function C.ext()", "function C.inr()"}, {"function C.inr()", "function free()"}, }}, {"L", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function L.ext()"}, {"function L.ext()", "function L.inr()"}, {"function L.inr()", "function free()"}, }}, }; map> expectedCreatedContracts = { {"G", {"F"}}, }; buildGraphsAndCheckExpectations(parsingResult.contractMap, expectedEdges, expectedCreatedContracts); } BOOST_AUTO_TEST_CASE(inherited_functions_virtual_and_super) { ParsingResult parsingResult = parseAndAnalyzeContracts(boost::unit_test::framework::current_test_case().p_name, R"( contract C { function f() internal {} function g() internal virtual {} function h() internal virtual {} } contract D { function h() internal virtual {} } contract E is C, D { function g() internal override {} function h() internal override(C, D) {} function i() internal {} function callF() external { f(); } function callG() external { g(); } function callH() external { h(); } function callI() external { i(); } function callCF() external { C.f(); } function callCG() external { C.g(); } function callCH() external { C.h(); } function callDH() external { D.h(); } function callEI() external { E.i(); } function callSuperF() external { super.f(); } function callSuperG() external { super.g(); } function callSuperH() external { super.h(); } } )"s); map expectedEdges = { {"C", {{"InternalDispatch", "InternalCreationDispatch"}}}, {"D", {{"InternalDispatch", "InternalCreationDispatch"}}}, {"E", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function E.callF()"}, {"Entry", "function E.callG()"}, {"Entry", "function E.callH()"}, {"Entry", "function E.callI()"}, {"Entry", "function E.callCF()"}, {"Entry", "function E.callCG()"}, {"Entry", "function E.callCH()"}, {"Entry", "function E.callDH()"}, {"Entry", "function E.callEI()"}, {"Entry", "function E.callSuperF()"}, {"Entry", "function E.callSuperG()"}, {"Entry", "function E.callSuperH()"}, {"function E.callF()", "function C.f()"}, {"function E.callG()", "function E.g()"}, {"function E.callH()", "function E.h()"}, {"function E.callI()", "function E.i()"}, {"function E.callCF()", "function C.f()"}, {"function E.callCG()", "function C.g()"}, {"function E.callCH()", "function C.h()"}, {"function E.callDH()", "function D.h()"}, {"function E.callEI()", "function E.i()"}, {"function E.callSuperF()", "function C.f()"}, {"function E.callSuperG()", "function C.g()"}, {"function E.callSuperH()", "function D.h()"}, }}, }; buildGraphsAndCheckExpectations(parsingResult.contractMap, expectedEdges, {}); } BOOST_AUTO_TEST_CASE(overloaded_functions) { ParsingResult parsingResult = parseAndAnalyzeContracts(boost::unit_test::framework::current_test_case().p_name, R"( enum E {E1, E2, E3} function free() {} function free(uint) {} function free(bytes memory) {} function free(E) {} contract C { function f(E) internal {} function f(bool) external {} } contract D is C { function ext1() external { free(); free(123); free("123"); } function ext2() external { f(); f(123); f("123"); } function ext3() external { free(E.E2); f(E.E2); } function ext4() external { this.f(false); } function f() internal {} function f(uint) internal {} function f(bytes memory) internal {} } )"s); map expectedEdges = { {"C", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function C.f(bool)"}, }}, {"D", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function C.f(bool)"}, {"Entry", "function D.ext1()"}, {"Entry", "function D.ext2()"}, {"Entry", "function D.ext3()"}, {"Entry", "function D.ext4()"}, {"function D.ext1()", "function free()"}, {"function D.ext1()", "function free(uint256)"}, {"function D.ext1()", "function free(bytes)"}, {"function D.ext2()", "function D.f()"}, {"function D.ext2()", "function D.f(uint256)"}, {"function D.ext2()", "function D.f(bytes)"}, {"function D.ext3()", "function free(enum E)"}, {"function D.ext3()", "function C.f(enum E)"}, }}, }; buildGraphsAndCheckExpectations(parsingResult.contractMap, expectedEdges, {}); } BOOST_AUTO_TEST_CASE(modifiers) { ParsingResult parsingResult = parseAndAnalyzeContracts(boost::unit_test::framework::current_test_case().p_name, R"( library L { modifier m() { g(); _; } function f() m internal {} function g() internal {} } contract C { modifier m1() virtual { _; } function q() m1 internal virtual { L.f(); } } contract D is C { modifier m2() { q(); _; new C(); } function p() m2 internal { C.q(); } function q() m2 internal override virtual {} } contract E is D { modifier m1() override { _; } modifier m3() { p(); _; } constructor() D() m1 E.m3 {} function ext() external m1 E.m3 { inr(); } function inr() internal m1 E.m3 { L.f(); } function q() internal override {} } )"s); map expectedEdges = { {"C", {{"InternalDispatch", "InternalCreationDispatch"}}}, {"D", {{"InternalDispatch", "InternalCreationDispatch"}}}, {"L", {{"InternalDispatch", "InternalCreationDispatch"}}}, {"E", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function E.ext()"}, {"EntryCreation", "constructor of E"}, {"EntryCreation", "modifier E.m1"}, {"EntryCreation", "modifier E.m3"}, {"function C.q()", "modifier E.m1"}, {"function C.q()", "function L.f()"}, {"function D.p()", "modifier D.m2"}, {"function D.p()", "function C.q()"}, {"function L.f()", "modifier L.m"}, {"function E.ext()", "function E.inr()"}, {"function E.ext()", "modifier E.m1"}, {"function E.ext()", "modifier E.m3"}, {"function E.inr()", "modifier E.m1"}, {"function E.inr()", "modifier E.m3"}, {"function E.inr()", "function L.f()"}, {"modifier L.m", "function L.g()"}, {"modifier D.m2", "function E.q()"}, {"modifier E.m3", "function D.p()"}, }}, }; map> expectedCreatedContracts = { {"E", {"C"}}, }; buildGraphsAndCheckExpectations(parsingResult.contractMap, expectedEdges, expectedCreatedContracts); } BOOST_AUTO_TEST_CASE(events) { ParsingResult parsingResult = parseAndAnalyzeContracts(boost::unit_test::framework::current_test_case().p_name, R"( function free() { emit L.Ev(); } library L { event Ev(); event Ev(bytes4, string indexed); function ext() external { emit Ev(); } function inr() internal { emit Ev(0x12345678, "a"); emit L.Ev(); } } contract C { event EvC(uint) anonymous; modifier m() { emit EvC(1); _; } } contract D is C { event EvD1(uint); event EvD2(uint); function ext() m external { emit D.EvD1(1); emit EvC(1); inr(); } function inr() m internal { emit EvD1(1); emit C.EvC(1); L.inr(); free(); EvD2; } } )"s); map expectedEdges = { {"C", {{"InternalDispatch", "InternalCreationDispatch"}}}, {"D", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function D.ext()"}, {"function D.ext()", "event D.EvD1(uint256)"}, {"function D.ext()", "event C.EvC(uint256)"}, {"function D.ext()", "function D.inr()"}, {"function D.ext()", "modifier C.m"}, {"function D.inr()", "event D.EvD1(uint256)"}, {"function D.inr()", "event C.EvC(uint256)"}, {"function D.inr()", "function L.inr()"}, {"function D.inr()", "function free()"}, {"function D.inr()", "modifier C.m"}, {"function L.inr()", "event L.Ev(bytes4,string)"}, {"function L.inr()", "event L.Ev()"}, {"function free()", "event L.Ev()"}, {"modifier C.m", "event C.EvC(uint256)"}, }}, {"L", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function L.ext()"}, {"function L.ext()", "event L.Ev()"}, }}, }; buildGraphsAndCheckExpectations(parsingResult.contractMap, expectedEdges, {}); } BOOST_AUTO_TEST_CASE(cycles) { ParsingResult parsingResult = parseAndAnalyzeContracts(boost::unit_test::framework::current_test_case().p_name, R"( function free1() { free1(); } function free2() { free3(); } function free3() { free2(); } library L { function inr1() internal { inr1(); } function inr2() internal { inr3(); } function inr3() internal { inr2(); } } contract C { function virt() internal virtual { virt(); } } contract D is C { function init() external { this.ext1(); inr1(); inr2(); L.inr1(); L.inr2(); free1(); free2(); virt(); } function ext1() external { this.ext1(); } function ext2() external { this.ext3(); } function ext3() external { this.ext2(); } function inr1() internal { inr1(); } function inr2() internal { inr3(); } function inr3() internal { inr2(); } function inr3(uint) internal {} function virt() internal override { C.virt(); } } )"s); map expectedEdges = { {"L", {{"InternalDispatch", "InternalCreationDispatch"}}}, {"C", {{"InternalDispatch", "InternalCreationDispatch"}}}, {"D", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function D.init()"}, {"Entry", "function D.ext1()"}, {"Entry", "function D.ext2()"}, {"Entry", "function D.ext3()"}, {"function D.init()", "function D.inr1()"}, {"function D.init()", "function D.inr2()"}, {"function D.init()", "function D.virt()"}, {"function D.init()", "function L.inr1()"}, {"function D.init()", "function L.inr2()"}, {"function D.init()", "function free1()"}, {"function D.init()", "function free2()"}, {"function D.inr1()", "function D.inr1()"}, {"function D.inr2()", "function D.inr3()"}, {"function D.inr3()", "function D.inr2()"}, {"function D.virt()", "function C.virt()"}, {"function C.virt()", "function D.virt()"}, {"function L.inr1()", "function L.inr1()"}, {"function L.inr2()", "function L.inr3()"}, {"function L.inr3()", "function L.inr2()"}, {"function free1()", "function free1()"}, {"function free2()", "function free3()"}, {"function free3()", "function free2()"}, }}, }; buildGraphsAndCheckExpectations(parsingResult.contractMap, expectedEdges, {}); } BOOST_AUTO_TEST_CASE(interfaces_and_abstract_contracts) { ParsingResult parsingResult = parseAndAnalyzeContracts(boost::unit_test::framework::current_test_case().p_name, R"( interface I { event Ev(uint); modifier m() virtual; function ext1() external; function ext2() external; } interface J is I { function ext2() external override; function ext3() external; } abstract contract C is J { function ext3() external override virtual; function ext4() external { inr2();} function inr1() internal virtual; function inr2() m internal { inr1(); this.ext1(); this.ext2(); this.ext3(); } } contract D is C { function ext1() public override { emit I.Ev(1); inr1(); inr2(); } function ext2() external override { I(this).ext1(); } function ext3() external override {} function inr1() internal override {} modifier m() override { _; } } )"s); map expectedEdges = { {"I", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function I.ext1()"}, {"Entry", "function I.ext2()"}, }}, {"J", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function I.ext1()"}, {"Entry", "function J.ext2()"}, {"Entry", "function J.ext3()"}, }}, {"C", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function I.ext1()"}, {"Entry", "function J.ext2()"}, {"Entry", "function C.ext3()"}, {"Entry", "function C.ext4()"}, {"function C.ext4()", "function C.inr2()"}, {"function C.inr2()", "function C.inr1()"}, {"function C.inr2()", "modifier I.m"}, }}, {"D", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function D.ext1()"}, {"Entry", "function D.ext2()"}, {"Entry", "function D.ext3()"}, {"Entry", "function C.ext4()"}, {"function C.ext4()", "function C.inr2()"}, {"function C.inr2()", "function D.inr1()"}, {"function C.inr2()", "modifier D.m"}, {"function D.ext1()", "event I.Ev(uint256)"}, {"function D.ext1()", "function D.inr1()"}, {"function D.ext1()", "function C.inr2()"}, }}, }; buildGraphsAndCheckExpectations(parsingResult.contractMap, expectedEdges, {}); } BOOST_AUTO_TEST_CASE(indirect_calls) { ParsingResult parsingResult = parseAndAnalyzeContracts(boost::unit_test::framework::current_test_case().p_name, R"( function free1() {} function free2() {} function free3() {} library L { function ext() external {} function inr1() internal {} function inr2() internal {} function inr3() internal {} function access() public { free1; inr1; L.ext; } function expression() public { (free2)(); (inr2)(); } } contract C { function ext1() external {} function ext2() external {} function ext3() external {} function inr1() internal {} function inr2() internal {} function inr3() internal {} function access() public { this.ext1; inr1; free1; L.inr1; L.ext; } function expression() public { (this.ext2)(); (inr2)(); (free2)(); (L.inr2)(); (L.ext)(); } } contract D is C { constructor() { access(); expression(); } } )"s); map expectedEdges = { {"L", { {"InternalDispatch", "InternalCreationDispatch"}, {"InternalDispatch", "function L.inr1()"}, {"InternalDispatch", "function L.inr2()"}, {"InternalDispatch", "function free1()"}, {"InternalDispatch", "function free2()"}, {"Entry", "function L.ext()"}, {"Entry", "function L.access()"}, {"Entry", "function L.expression()"}, {"function L.expression()", "InternalDispatch"}, }}, {"C", { {"InternalDispatch", "InternalCreationDispatch"}, {"InternalDispatch", "function C.inr1()"}, {"InternalDispatch", "function C.inr2()"}, {"InternalDispatch", "function free1()"}, {"InternalDispatch", "function free2()"}, {"InternalDispatch", "function L.inr1()"}, {"InternalDispatch", "function L.inr2()"}, {"Entry", "function C.ext1()"}, {"Entry", "function C.ext2()"}, {"Entry", "function C.ext3()"}, {"Entry", "function C.access()"}, {"Entry", "function C.expression()"}, {"function C.expression()", "InternalDispatch"}, }}, {"D", { {"InternalCreationDispatch", "function L.inr2()"}, {"InternalCreationDispatch", "function C.inr1()"}, {"InternalCreationDispatch", "function C.inr2()"}, {"InternalCreationDispatch", "function free1()"}, {"InternalCreationDispatch", "function free2()"}, {"InternalCreationDispatch", "function L.inr1()"}, {"InternalCreationDispatch", "function L.inr2()"}, {"InternalDispatch", "InternalCreationDispatch"}, // NOTE: These three edges from InternalDispatch are only here because the graph builder // visits C.access() twice (once from constructor and then again as a part of the contract // interface) and each time the current dispatch is different. Functions that do not // perform any calls are currently never detected as already visited. {"InternalDispatch", "function C.inr1()"}, {"InternalDispatch", "function free1()"}, {"InternalDispatch", "function L.inr1()"}, {"Entry", "function C.ext1()"}, {"Entry", "function C.ext2()"}, {"Entry", "function C.ext3()"}, {"Entry", "function C.access()"}, {"Entry", "function C.expression()"}, {"EntryCreation", "constructor of D"}, {"EntryCreation", "function C.access()"}, {"EntryCreation", "function C.expression()"}, {"function C.expression()", "InternalCreationDispatch"}, }}, }; buildGraphsAndCheckExpectations(parsingResult.contractMap, expectedEdges, {}); } BOOST_AUTO_TEST_CASE(calls_via_pointers) { ParsingResult parsingResult = parseAndAnalyzeContracts(boost::unit_test::framework::current_test_case().p_name, R"( function free1() {} function free2() {} function free3() {} library L { function inr1() internal {} function inr2() internal {} function inr3() internal {} function callPtrs( function () external e, function () internal i, function () internal f, function () internal l ) internal { e(); i(); f(); l(); } } contract C { function ext1() external {} function ext2() external {} function ext3() external {} function inr1() internal {} function inr2() internal {} function inr3() internal {} function getPtrs2() internal returns ( function () external, function () internal, function () internal, function () internal ) { return (this.ext2, inr2, free2, L.inr2); } function testLocalVars() public { (function () external e, function () i, function () f, function () l) = getPtrs2(); L.callPtrs(e, i, f, l); } } contract D is C { function () external m_e = this.ext1; function () internal m_i = inr1; function () internal m_f = free1; function () internal m_l = L.inr1; function () internal immutable m_imm = inr1; function callStatePtrs() internal { m_e(); m_i(); m_f(); m_l(); } function updateStatePtrs( function () external e, function () internal i, function () internal f, function () internal l ) internal { m_e = e; m_i = i; m_f = f; m_l = l; } function testStateVars() public { (function () external e, function () i, function () f, function () l) = getPtrs2(); updateStatePtrs(e, i, f, l); callStatePtrs(); } function testImmutablePtr() public { m_imm(); } constructor() { testStateVars(); testLocalVars(); } } )"s); map expectedEdges = { {"L", {{"InternalDispatch", "InternalCreationDispatch"}}}, {"C", { {"InternalDispatch", "InternalCreationDispatch"}, {"InternalDispatch", "function C.inr2()"}, {"InternalDispatch", "function L.inr2()"}, {"InternalDispatch", "function free2()"}, {"Entry", "function C.ext1()"}, {"Entry", "function C.ext2()"}, {"Entry", "function C.ext3()"}, {"Entry", "function C.testLocalVars()"}, {"function C.testLocalVars()", "function C.getPtrs2()"}, {"function C.testLocalVars()", "function L.callPtrs(function () external,function (),function (),function ())"}, {"function L.callPtrs(function () external,function (),function (),function ())", "InternalDispatch"}, }}, {"D", { {"InternalCreationDispatch", "function C.inr1()"}, {"InternalCreationDispatch", "function C.inr2()"}, {"InternalCreationDispatch", "function L.inr1()"}, {"InternalCreationDispatch", "function L.inr2()"}, {"InternalCreationDispatch", "function free1()"}, {"InternalCreationDispatch", "function free2()"}, {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function C.ext1()"}, {"Entry", "function C.ext2()"}, {"Entry", "function C.ext3()"}, {"Entry", "function C.testLocalVars()"}, {"Entry", "function D.testStateVars()"}, {"Entry", "function D.testImmutablePtr()"}, {"EntryCreation", "constructor of D"}, {"EntryCreation", "function C.testLocalVars()"}, {"EntryCreation", "function D.testStateVars()"}, {"function C.testLocalVars()", "function C.getPtrs2()"}, {"function C.testLocalVars()", "function L.callPtrs(function () external,function (),function (),function ())"}, {"function D.testStateVars()", "function C.getPtrs2()"}, {"function D.testStateVars()", "function D.updateStatePtrs(function () external,function (),function (),function ())"}, {"function D.testStateVars()", "function D.callStatePtrs()"}, {"function D.testImmutablePtr()", "InternalDispatch"}, {"function D.callStatePtrs()", "InternalCreationDispatch"}, {"function L.callPtrs(function () external,function (),function (),function ())", "InternalCreationDispatch"}, }}, }; buildGraphsAndCheckExpectations(parsingResult.contractMap, expectedEdges, {}); } BOOST_AUTO_TEST_CASE(pointer_to_overridden_function) { ParsingResult parsingResult = parseAndAnalyzeContracts(boost::unit_test::framework::current_test_case().p_name, R"( contract C { function f() internal virtual {} } contract D is C { function f() internal override {} function getF() internal returns (function ()) { return C.f; } function getSuperF() internal returns (function ()) { return super.f; } function test1() public { getF()(); } function test2() public { getSuperF()(); } } )"s); map expectedEdges = { {"C", {{"InternalDispatch", "InternalCreationDispatch"}}}, {"D", { {"InternalDispatch", "InternalCreationDispatch"}, {"InternalDispatch", "function C.f()"}, {"Entry", "function D.test1()"}, {"Entry", "function D.test2()"}, {"function D.test1()", "function D.getF()"}, {"function D.test1()", "InternalDispatch"}, {"function D.test2()", "function D.getSuperF()"}, {"function D.test2()", "InternalDispatch"}, }}, }; buildGraphsAndCheckExpectations(parsingResult.contractMap, expectedEdges, {}); } BOOST_AUTO_TEST_CASE(pointer_to_nonexistent_function) { ParsingResult parsingResult = parseAndAnalyzeContracts(boost::unit_test::framework::current_test_case().p_name, R"( interface I { function f() external; } abstract contract C is I { function g() internal virtual; function getF() internal returns (function () external) { return this.f; } function getG() internal returns (function () internal) { return g; } function testInterface() public { getF()(); getG()(); } function testBadPtr() public { function () ptr; ptr(); } } contract D is C { function f() public override {} function g() internal override {} } )"s); map expectedEdges = { {"I", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function I.f()"}, }}, {"C", { {"InternalDispatch", "InternalCreationDispatch"}, {"InternalDispatch", "function C.g()"}, {"Entry", "function C.testInterface()"}, {"Entry", "function C.testBadPtr()"}, {"Entry", "function I.f()"}, {"function C.testInterface()", "function C.getF()"}, {"function C.testInterface()", "function C.getG()"}, {"function C.testInterface()", "InternalDispatch"}, {"function C.testBadPtr()", "InternalDispatch"}, }}, {"D", { {"InternalDispatch", "InternalCreationDispatch"}, {"InternalDispatch", "function D.g()"}, {"Entry", "function C.testInterface()"}, {"Entry", "function C.testBadPtr()"}, {"Entry", "function D.f()"}, {"function C.testInterface()", "function C.getF()"}, {"function C.testInterface()", "function C.getG()"}, {"function C.testInterface()", "InternalDispatch"}, {"function C.testBadPtr()", "InternalDispatch"}, }}, }; buildGraphsAndCheckExpectations(parsingResult.contractMap, expectedEdges, {}); } BOOST_AUTO_TEST_CASE(pointer_cycle) { ParsingResult parsingResult = parseAndAnalyzeContracts(boost::unit_test::framework::current_test_case().p_name, R"( contract C { function () ptr = f; function f() internal { ptr(); } function test() public { ptr(); } } )"s); map expectedEdges = { {"C", { {"InternalCreationDispatch", "function C.f()"}, {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function C.test()"}, {"function C.test()", "InternalDispatch"}, {"function C.f()", "InternalCreationDispatch"}, }}, }; buildGraphsAndCheckExpectations(parsingResult.contractMap, expectedEdges, {}); } BOOST_AUTO_TEST_CASE(using_for) { ParsingResult parsingResult = parseAndAnalyzeContracts(boost::unit_test::framework::current_test_case().p_name, R"( struct S { uint x; } library L { function ext(S memory _s) external {} function inr(S memory _s) internal {} } contract C { using L for S; function test() public { S memory s = S(42); s.ext(); s.inr(); } } )"s); map expectedEdges = { {"L", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function L.ext(struct S)"}, }}, {"C", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function C.test()"}, {"function C.test()", "function L.inr(struct S)"}, }}, }; buildGraphsAndCheckExpectations(parsingResult.contractMap, expectedEdges, {}); } BOOST_AUTO_TEST_CASE(getters) { ParsingResult parsingResult = parseAndAnalyzeContracts(boost::unit_test::framework::current_test_case().p_name, R"( contract C { uint public variable; uint[][] public array; mapping(bytes => bytes) public map; function test() public { this.variable(); this.array(1, 2); this.map("value"); } } )"s); map expectedEdges = { {"C", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "function C.test()"}, }}, }; buildGraphsAndCheckExpectations(parsingResult.contractMap, expectedEdges, {}); } BOOST_AUTO_TEST_CASE(fallback_and_receive) { ParsingResult parsingResult = parseAndAnalyzeContracts(boost::unit_test::framework::current_test_case().p_name, R"( contract C { fallback() external {} receive() external payable {} } contract D { fallback(bytes calldata) external returns (bytes memory){} function test() public { (bool success, bytes memory result) = address(this).call("abc"); } } contract E is C {} )"s); map expectedEdges = { {"C", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "receive of C"}, {"Entry", "fallback of C"}, }}, {"D", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "fallback of D"}, {"Entry", "function D.test()"}, }}, {"E", { {"InternalDispatch", "InternalCreationDispatch"}, {"Entry", "receive of C"}, {"Entry", "fallback of C"}, }}, }; buildGraphsAndCheckExpectations(parsingResult.contractMap, expectedEdges, {}); } BOOST_AUTO_TEST_SUITE_END() } // namespace solidity::frontend::test