diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index efdc3e7bb..885fbc3cb 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -97,6 +97,7 @@ set(libsolidity_sources libsolidity/SyntaxTest.cpp libsolidity/SyntaxTest.h libsolidity/ViewPureChecker.cpp + libsolidity/analysis/FunctionCallGraph.cpp ) detect_stray_source_files("${libsolidity_sources}" "libsolidity/") diff --git a/test/libsolidity/analysis/FunctionCallGraph.cpp b/test/libsolidity/analysis/FunctionCallGraph.cpp new file mode 100644 index 000000000..1cfc304b4 --- /dev/null +++ b/test/libsolidity/analysis/FunctionCallGraph.cpp @@ -0,0 +1,1450 @@ +/* + 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