From 8bacf03ffe5d7fbe9c17c186064f61f75dfc36b7 Mon Sep 17 00:00:00 2001 From: Mathias Baumann Date: Wed, 18 Nov 2020 18:24:33 +0100 Subject: [PATCH 01/10] MultiUseYulFunctionCollector::requestedFunctions(): Refactor the loop to use destructuring --- libsolidity/codegen/MultiUseYulFunctionCollector.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libsolidity/codegen/MultiUseYulFunctionCollector.cpp b/libsolidity/codegen/MultiUseYulFunctionCollector.cpp index 541171818..bfc44e927 100644 --- a/libsolidity/codegen/MultiUseYulFunctionCollector.cpp +++ b/libsolidity/codegen/MultiUseYulFunctionCollector.cpp @@ -32,11 +32,11 @@ using namespace solidity::frontend; string MultiUseYulFunctionCollector::requestedFunctions() { string result; - for (auto const& f: m_requestedFunctions) + for (auto const& [name, code]: m_requestedFunctions) { - solAssert(f.second != "< Date: Sat, 6 Feb 2021 15:36:43 +0100 Subject: [PATCH 02/10] FunctionType: Add assertions against missing type annotations - This should make it easier to realize that one of the analysis phases has not been executed. --- libsolidity/ast/Types.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libsolidity/ast/Types.cpp b/libsolidity/ast/Types.cpp index 182b2b72b..4cafc201c 100644 --- a/libsolidity/ast/Types.cpp +++ b/libsolidity/ast/Types.cpp @@ -2632,11 +2632,13 @@ FunctionType::FunctionType(FunctionDefinition const& _function, Kind _kind): for (ASTPointer const& var: _function.parameters()) { + solAssert(var->annotation().type, "Parameter type is not yet available in the AST."); m_parameterNames.push_back(var->name()); m_parameterTypes.push_back(var->annotation().type); } for (ASTPointer const& var: _function.returnParameters()) { + solAssert(var->annotation().type, "Return parameter type is not yet available in the AST."); m_returnParameterNames.push_back(var->name()); m_returnParameterTypes.push_back(var->annotation().type); } From e27afe93a9c26d3ec7103ecaff4dbd37f2e84065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Fri, 29 Jan 2021 20:15:01 +0100 Subject: [PATCH 03/10] CompilerStack: Fix style on local variable name --- libsolidity/interface/CompilerStack.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libsolidity/interface/CompilerStack.cpp b/libsolidity/interface/CompilerStack.cpp index 1995c7750..0ca9bf39f 100644 --- a/libsolidity/interface/CompilerStack.cpp +++ b/libsolidity/interface/CompilerStack.cpp @@ -341,9 +341,9 @@ bool CompilerStack::analyze() if (source->ast && !syntaxChecker.checkSyntax(*source->ast)) noErrors = false; - DocStringTagParser DocStringTagParser(m_errorReporter); + DocStringTagParser docStringTagParser(m_errorReporter); for (Source const* source: m_sourceOrder) - if (source->ast && !DocStringTagParser.parseDocStrings(*source->ast)) + if (source->ast && !docStringTagParser.parseDocStrings(*source->ast)) noErrors = false; m_globalContext = make_shared(); From 529495c530f12ac6a5c9676d4ab489ffa86d3f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Fri, 29 Jan 2021 20:15:26 +0100 Subject: [PATCH 04/10] CompilerStack: Typo in a comment --- libsolidity/interface/CompilerStack.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libsolidity/interface/CompilerStack.cpp b/libsolidity/interface/CompilerStack.cpp index 0ca9bf39f..b1539e10a 100644 --- a/libsolidity/interface/CompilerStack.cpp +++ b/libsolidity/interface/CompilerStack.cpp @@ -387,7 +387,7 @@ bool CompilerStack::analyze() if (source->ast && !docStringAnalyser.analyseDocStrings(*source->ast)) noErrors = false; - // New we run full type checks that go down to the expression level. This + // Now we run full type checks that go down to the expression level. This // cannot be done earlier, because we need cross-contract types and information // about whether a contract is abstract for the `new` expression. // This populates the `type` annotation for all expressions. From 64d14b4cc401aee45f05192bb2b69a79712a2d3e Mon Sep 17 00:00:00 2001 From: Mathias Baumann Date: Wed, 18 Nov 2020 18:24:33 +0100 Subject: [PATCH 05/10] IRNames::function(): Return correct name if the function is a constructor --- libsolidity/codegen/ir/Common.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libsolidity/codegen/ir/Common.cpp b/libsolidity/codegen/ir/Common.cpp index 321becc51..7e858eb78 100644 --- a/libsolidity/codegen/ir/Common.cpp +++ b/libsolidity/codegen/ir/Common.cpp @@ -35,6 +35,9 @@ YulArity YulArity::fromType(FunctionType const& _functionType) string IRNames::function(FunctionDefinition const& _function) { + if (_function.isConstructor()) + return implicitConstructor(*_function.annotation().contract); + return "fun_" + _function.name() + "_" + to_string(_function.id()); } From 4c1f8d69f969cddcb95622ebfb62b6f14eca1008 Mon Sep 17 00:00:00 2001 From: Mathias Baumann Date: Wed, 18 Nov 2020 18:24:33 +0100 Subject: [PATCH 06/10] Implement a Function Call Graph --- libsolidity/CMakeLists.txt | 2 + libsolidity/analysis/FunctionCallGraph.cpp | 280 +++++++++++++++++++++ libsolidity/analysis/FunctionCallGraph.h | 122 +++++++++ 3 files changed, 404 insertions(+) create mode 100644 libsolidity/analysis/FunctionCallGraph.cpp create mode 100644 libsolidity/analysis/FunctionCallGraph.h diff --git a/libsolidity/CMakeLists.txt b/libsolidity/CMakeLists.txt index 6ca24bc8a..981617a84 100644 --- a/libsolidity/CMakeLists.txt +++ b/libsolidity/CMakeLists.txt @@ -18,6 +18,8 @@ set(sources analysis/DocStringAnalyser.h analysis/DocStringTagParser.cpp analysis/DocStringTagParser.h + analysis/FunctionCallGraph.cpp + analysis/FunctionCallGraph.h analysis/ImmutableValidator.cpp analysis/ImmutableValidator.h analysis/GlobalContext.cpp diff --git a/libsolidity/analysis/FunctionCallGraph.cpp b/libsolidity/analysis/FunctionCallGraph.cpp new file mode 100644 index 000000000..7808e4134 --- /dev/null +++ b/libsolidity/analysis/FunctionCallGraph.cpp @@ -0,0 +1,280 @@ +/* + 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 + +#include + +#include +#include + +using namespace std; +using namespace ranges; +using namespace solidity::frontend; + +bool FunctionCallGraphBuilder::CompareByID::operator()(Node const& _lhs, Node const& _rhs) const +{ + if (_lhs.index() != _rhs.index()) + return _lhs.index() < _rhs.index(); + + if (holds_alternative(_lhs)) + return get(_lhs) < get(_rhs); + return get(_lhs)->id() < get(_rhs)->id(); +} + +bool FunctionCallGraphBuilder::CompareByID::operator()(Node const& _lhs, int64_t _rhs) const +{ + solAssert(!holds_alternative(_lhs), ""); + + return get(_lhs)->id() < _rhs; +} + +bool FunctionCallGraphBuilder::CompareByID::operator()(int64_t _lhs, Node const& _rhs) const +{ + solAssert(!holds_alternative(_rhs), ""); + + return _lhs < get(_rhs)->id(); +} + +FunctionCallGraphBuilder::ContractCallGraph FunctionCallGraphBuilder::buildCreationGraph(ContractDefinition const& _contract) +{ + FunctionCallGraphBuilder builder(_contract); + solAssert(builder.m_currentNode == Node(SpecialNode::Entry), ""); + + // Create graph for constructor, state vars, etc + for (ContractDefinition const* base: _contract.annotation().linearizedBaseContracts | views::reverse) + { + // The constructor and functions called in state variable initial assignments should have + // an edge from Entry + builder.m_currentNode = SpecialNode::Entry; + for (auto const* stateVar: base->stateVariables()) + stateVar->accept(builder); + + if (base->constructor()) + { + builder.functionReferenced(*base->constructor()); + + // Constructors and functions called in state variable initializers have an edge either from + // the previous class in linearized order or from Entry if there's no class before. + builder.m_currentNode = base->constructor(); + } + + // Functions called from the inheritance specifier should have an edge from the constructor + // for consistency with functions called from constructor modifiers. + for (auto const& inheritanceSpecifier: base->baseContracts()) + inheritanceSpecifier->accept(builder); + } + + builder.m_currentNode = SpecialNode::Entry; + builder.processQueue(); + + return move(builder.m_graph); +} + +FunctionCallGraphBuilder::ContractCallGraph FunctionCallGraphBuilder::buildDeployedGraph( + ContractDefinition const& _contract, + FunctionCallGraphBuilder::ContractCallGraph const& _creationGraph +) +{ + solAssert(&_creationGraph.contract == &_contract, ""); + + FunctionCallGraphBuilder builder(_contract); + solAssert(builder.m_currentNode == Node(SpecialNode::Entry), ""); + + auto getSecondElement = [](auto const& _tuple){ return get<1>(_tuple); }; + + // Create graph for all publicly reachable functions + for (FunctionTypePointer functionType: _contract.interfaceFunctionList() | views::transform(getSecondElement)) + { + auto const* function = dynamic_cast(&functionType->declaration()); + auto const* variable = dynamic_cast(&functionType->declaration()); + + if (function) + builder.functionReferenced(*function); + else + // If it's not a function, it must be a getter of a public variable; we ignore those + solAssert(variable, ""); + } + + if (_contract.fallbackFunction()) + builder.functionReferenced(*_contract.fallbackFunction()); + + if (_contract.receiveFunction()) + builder.functionReferenced(*_contract.receiveFunction()); + + // All functions present in internal dispatch at creation time could potentially be pointers + // assigned to state variables and as such may be reachable after deployment as well. + builder.m_currentNode = SpecialNode::InternalDispatch; + for (Node const& dispatchTarget: valueOrDefault(_creationGraph.edges, SpecialNode::InternalDispatch, {})) + { + solAssert(!holds_alternative(dispatchTarget), ""); + solAssert(get(dispatchTarget) != nullptr, ""); + + // Visit the callable to add not only it but also everything it calls too + builder.functionReferenced(*get(dispatchTarget), false); + } + + builder.m_currentNode = SpecialNode::Entry; + builder.processQueue(); + + return move(builder.m_graph); +} + +bool FunctionCallGraphBuilder::visit(FunctionCall const& _functionCall) +{ + if (*_functionCall.annotation().kind != FunctionCallKind::FunctionCall) + return true; + + auto const* functionType = dynamic_cast(_functionCall.expression().annotation().type); + solAssert(functionType, ""); + + if (functionType->kind() == FunctionType::Kind::Internal && !_functionCall.expression().annotation().calledDirectly) + // If it's not a direct call, we don't really know which function will be called (it may even + // change at runtime). All we can do is to add an edge to the dispatch which in turn has + // edges to all functions could possibly be called. + add(m_currentNode, SpecialNode::InternalDispatch); + + return true; +} + +bool FunctionCallGraphBuilder::visit(EmitStatement const& _emitStatement) +{ + auto const* functionType = dynamic_cast(_emitStatement.eventCall().expression().annotation().type); + solAssert(functionType, ""); + + m_graph.emittedEvents.insert(&dynamic_cast(functionType->declaration())); + + return true; +} + +bool FunctionCallGraphBuilder::visit(Identifier const& _identifier) +{ + if (auto const* callable = dynamic_cast(_identifier.annotation().referencedDeclaration)) + { + solAssert(*_identifier.annotation().requiredLookup == VirtualLookup::Virtual, ""); + + auto funType = dynamic_cast(_identifier.annotation().type); + + // For events kind() == Event, so we have an extra check here + if (funType && funType->kind() == FunctionType::Kind::Internal) + functionReferenced(callable->resolveVirtual(m_graph.contract), _identifier.annotation().calledDirectly); + } + + return true; +} + +bool FunctionCallGraphBuilder::visit(MemberAccess const& _memberAccess) +{ + auto functionType = dynamic_cast(_memberAccess.annotation().type); + auto functionDef = dynamic_cast(_memberAccess.annotation().referencedDeclaration); + if (!functionType || !functionDef || functionType->kind() != FunctionType::Kind::Internal) + return true; + + // Super functions + if (*_memberAccess.annotation().requiredLookup == VirtualLookup::Super) + { + if (auto const* typeType = dynamic_cast(_memberAccess.expression().annotation().type)) + if (auto const contractType = dynamic_cast(typeType->actualType())) + { + solAssert(contractType->isSuper(), ""); + functionDef = &functionDef->resolveVirtual( + m_graph.contract, + contractType->contractDefinition().superContract(m_graph.contract) + ); + } + } + else + solAssert(*_memberAccess.annotation().requiredLookup == VirtualLookup::Static, ""); + + functionReferenced(*functionDef, _memberAccess.annotation().calledDirectly); + + return true; +} + +bool FunctionCallGraphBuilder::visit(ModifierInvocation const& _modifierInvocation) +{ + if (auto const* modifier = dynamic_cast(_modifierInvocation.name().annotation().referencedDeclaration)) + { + VirtualLookup const& requiredLookup = *_modifierInvocation.name().annotation().requiredLookup; + + if (requiredLookup == VirtualLookup::Virtual) + functionReferenced(modifier->resolveVirtual(m_graph.contract)); + else + { + solAssert(requiredLookup == VirtualLookup::Static, ""); + functionReferenced(*modifier); + } + } + + return true; +} + +bool FunctionCallGraphBuilder::visit(NewExpression const& _newExpression) +{ + if (ContractType const* contractType = dynamic_cast(_newExpression.typeName().annotation().type)) + m_graph.createdContracts.emplace(&contractType->contractDefinition()); + + return true; +} + +void FunctionCallGraphBuilder::enqueueCallable(CallableDeclaration const& _callable) +{ + if (!m_graph.edges.count(&_callable)) + { + m_visitQueue.push_back(&_callable); + + // Insert the callable to the graph (with no edges coming out of it) to mark it as visited. + m_graph.edges.insert({Node(&_callable), {}}); + } +} + +void FunctionCallGraphBuilder::processQueue() +{ + solAssert(m_currentNode == Node(SpecialNode::Entry), "Visit queue is already being processed."); + + while (!m_visitQueue.empty()) + { + m_currentNode = m_visitQueue.front(); + solAssert(holds_alternative(m_currentNode), ""); + + m_visitQueue.pop_front(); + get(m_currentNode)->accept(*this); + } + + m_currentNode = SpecialNode::Entry; +} + +void FunctionCallGraphBuilder::add(Node _caller, Node _callee) +{ + m_graph.edges[_caller].insert(_callee); +} + +void FunctionCallGraphBuilder::functionReferenced(CallableDeclaration const& _callable, bool _calledDirectly) +{ + if (_calledDirectly) + { + solAssert( + holds_alternative(m_currentNode) || m_graph.edges.count(m_currentNode) > 0, + "Adding an edge from a node that has not been visited yet." + ); + + add(m_currentNode, &_callable); + } + else + add(SpecialNode::InternalDispatch, &_callable); + + enqueueCallable(_callable); +} diff --git a/libsolidity/analysis/FunctionCallGraph.h b/libsolidity/analysis/FunctionCallGraph.h new file mode 100644 index 000000000..59f239b0e --- /dev/null +++ b/libsolidity/analysis/FunctionCallGraph.h @@ -0,0 +1,122 @@ +/* + 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 + +#pragma once + +#include +#include + +#include +#include +#include + +namespace solidity::frontend +{ + +/** + * Creates a Function call graph for a contract at the granularity of Solidity + * functions and modifiers. The graph can represent the situation either at contract creation + * or after deployment. The graph does not preserve temporal relations between calls - edges + * coming out of the same node show which calls were performed but not in what order. + * + * Includes the following special nodes: + * - Entry: represents a call from the outside of the contract. + * After deployment this is the node that connects to all the functions exposed through the + * external interface. At contract creation it connects to the constructors and variable + * initializers, which are not explicitly called from within another function. + * - InternalDispatch: Represents the internal dispatch function, which calls internal functions + * determined at runtime by values of variables and expressions. Functions that are not called + * right away get an edge from this node. + * + * Nodes are a variant of either the enum SpecialNode or a CallableDeclaration which currently + * can be a function or a modifier. There are no nodes representing event calls. Instead all + * emitted events and created contracts are gathered in separate sets included in the graph just + * for that purpose. + * + * Auto-generated getter functions for public state variables are ignored, but function calls + * inside initial assignments are included in the creation graph. + * + * Only calls reachable from an Entry node are included in the graph. The map representing edges + * is also guaranteed to contain keys representing all the reachable functions and modifiers, even + * if they have no outgoing edges. + */ +class FunctionCallGraphBuilder: private ASTConstVisitor +{ +public: + enum class SpecialNode + { + InternalDispatch, + Entry, + }; + + using Node = std::variant; + + struct CompareByID + { + using is_transparent = void; + bool operator()(Node const& _lhs, Node const& _rhs) const; + bool operator()(Node const& _lhs, int64_t _rhs) const; + bool operator()(int64_t _lhs, Node const& _rhs) const; + }; + + struct ContractCallGraph + { + /// Contract for which this is the graph + ContractDefinition const& contract; + + /// Graph edges. Edges are directed and lead from the caller to the callee. + /// The map contains a key for every possible caller, even if does not actually perform + /// any calls. + std::map, CompareByID> edges; + + /// Contracts that may get created with `new` by functions present in the graph. + std::set createdContracts; + + /// Events that may get emitted by functions present in the graph. + std::set emittedEvents; + }; + + static ContractCallGraph buildCreationGraph(ContractDefinition const& _contract); + static ContractCallGraph buildDeployedGraph( + ContractDefinition const& _contract, + FunctionCallGraphBuilder::ContractCallGraph const& _creationGraph + ); + +private: + FunctionCallGraphBuilder(ContractDefinition const& _contract): + m_graph{_contract, {}, {}, {}} {} + + bool visit(FunctionCall const& _functionCall) override; + bool visit(EmitStatement const& _emitStatement) override; + bool visit(Identifier const& _identifier) override; + bool visit(MemberAccess const& _memberAccess) override; + bool visit(ModifierInvocation const& _modifierInvocation) override; + bool visit(NewExpression const& _newExpression) override; + + void enqueueCallable(CallableDeclaration const& _callable); + void processQueue(); + + void add(Node _caller, Node _callee); + void functionReferenced(CallableDeclaration const& _callable, bool _calledDirectly = true); + + Node m_currentNode = SpecialNode::Entry; + ContractCallGraph m_graph; + std::deque m_visitQueue; +}; + +} From 74ef7dd790758ea221e7d0c7400771602665d0d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Thu, 28 Jan 2021 19:20:00 +0100 Subject: [PATCH 07/10] FunctionCallGraph: operator<< for printing nodes in an unambiguous way --- libsolidity/analysis/FunctionCallGraph.cpp | 63 ++++++++++++++++++++++ libsolidity/analysis/FunctionCallGraph.h | 3 ++ 2 files changed, 66 insertions(+) diff --git a/libsolidity/analysis/FunctionCallGraph.cpp b/libsolidity/analysis/FunctionCallGraph.cpp index 7808e4134..aa182cd3a 100644 --- a/libsolidity/analysis/FunctionCallGraph.cpp +++ b/libsolidity/analysis/FunctionCallGraph.cpp @@ -18,12 +18,16 @@ #include +#include + +#include #include #include using namespace std; using namespace ranges; using namespace solidity::frontend; +using namespace solidity::util; bool FunctionCallGraphBuilder::CompareByID::operator()(Node const& _lhs, Node const& _rhs) const { @@ -278,3 +282,62 @@ void FunctionCallGraphBuilder::functionReferenced(CallableDeclaration const& _ca enqueueCallable(_callable); } + +ostream& solidity::frontend::operator<<(ostream& _out, FunctionCallGraphBuilder::Node const& _node) +{ + using SpecialNode = FunctionCallGraphBuilder::SpecialNode; + + if (holds_alternative(_node)) + switch (get(_node)) + { + case SpecialNode::InternalDispatch: + _out << "InternalDispatch"; + break; + case SpecialNode::Entry: + _out << "Entry"; + break; + default: solAssert(false, "Invalid SpecialNode type"); + } + else + { + solAssert(holds_alternative(_node), ""); + + auto const* callableDeclaration = get(_node); + solAssert(callableDeclaration, ""); + + auto const* function = dynamic_cast(callableDeclaration); + auto const* event = dynamic_cast(callableDeclaration); + auto const* modifier = dynamic_cast(callableDeclaration); + + auto typeToString = [](auto const& _var) -> string { return _var->type()->toString(true); }; + vector parameters = callableDeclaration->parameters() | views::transform(typeToString) | to>(); + + string scopeName; + if (!function || !function->isFree()) + { + solAssert(callableDeclaration->annotation().scope, ""); + auto const* parentContract = dynamic_cast(callableDeclaration->annotation().scope); + solAssert(parentContract, ""); + scopeName = parentContract->name(); + } + + if (function && function->isFree()) + _out << "function " << function->name() << "(" << joinHumanReadable(parameters, ",") << ")"; + else if (function && function->isConstructor()) + _out << "constructor of " << scopeName; + else if (function && function->isFallback()) + _out << "fallback of " << scopeName; + else if (function && function->isReceive()) + _out << "receive of " << scopeName; + else if (function) + _out << "function " << scopeName << "." << function->name() << "(" << joinHumanReadable(parameters, ",") << ")"; + else if (event) + _out << "event " << scopeName << "." << event->name() << "(" << joinHumanReadable(parameters, ",") << ")"; + else if (modifier) + _out << "modifier " << scopeName << "." << modifier->name(); + else + solAssert(false, "Unexpected AST node type in function call graph"); + } + + return _out; +} diff --git a/libsolidity/analysis/FunctionCallGraph.h b/libsolidity/analysis/FunctionCallGraph.h index 59f239b0e..53d1dceb2 100644 --- a/libsolidity/analysis/FunctionCallGraph.h +++ b/libsolidity/analysis/FunctionCallGraph.h @@ -22,6 +22,7 @@ #include #include +#include #include #include @@ -119,4 +120,6 @@ private: std::deque m_visitQueue; }; +std::ostream& operator<<(std::ostream& _out, FunctionCallGraphBuilder::Node const& _node); + } From 4c283f00c1f2d3ae91aa216522a834a7e5566400 Mon Sep 17 00:00:00 2001 From: Mathias Baumann Date: Wed, 18 Nov 2020 18:24:33 +0100 Subject: [PATCH 08/10] Add IRGenerator::verifyCallGraphs() --- libsolidity/codegen/ir/IRGenerator.cpp | 82 ++++++++++++++++++++++++-- libsolidity/codegen/ir/IRGenerator.h | 12 +++- 2 files changed, 89 insertions(+), 5 deletions(-) diff --git a/libsolidity/codegen/ir/IRGenerator.cpp b/libsolidity/codegen/ir/IRGenerator.cpp index bec31200c..92406aa7b 100644 --- a/libsolidity/codegen/ir/IRGenerator.cpp +++ b/libsolidity/codegen/ir/IRGenerator.cpp @@ -36,18 +36,61 @@ #include #include #include +#include #include +#include + #include #include +#include using namespace std; +using namespace ranges; using namespace solidity; using namespace solidity::util; using namespace solidity::frontend; +namespace +{ + +void verifyCallGraph( + set const& _expectedCallables, + set _generatedFunctions +) +{ + for (auto const& expectedCallable: _expectedCallables) + if (auto const* expectedFunction = dynamic_cast(expectedCallable)) + { + solAssert( + _generatedFunctions.count(expectedFunction) == 1 || expectedFunction->isConstructor(), + "No code generated for function " + expectedFunction->name() + "even though it is not a constructor." + ); + _generatedFunctions.erase(expectedFunction); + } + + solAssert( + _generatedFunctions.size() == 0, + "Of the generated functions " + toString(_generatedFunctions.size()) + " are not in the call graph." + ); +} + +set collectReachableCallables( + FunctionCallGraphBuilder::ContractCallGraph const& _graph +) +{ + set reachableCallables; + for (FunctionCallGraphBuilder::Node const& reachableNode: _graph.edges | views::keys) + if (holds_alternative(reachableNode)) + reachableCallables.emplace(get(reachableNode)); + + return reachableCallables; +} + +} + pair IRGenerator::run( ContractDefinition const& _contract, map const& _otherYulSources @@ -76,11 +119,31 @@ pair IRGenerator::run( return {warning + ir, warning + asmStack.print()}; } +void IRGenerator::verifyCallGraphs( + FunctionCallGraphBuilder::ContractCallGraph const& _creationGraph, + FunctionCallGraphBuilder::ContractCallGraph const& _deployedGraph +) +{ + // m_creationFunctionList and m_deployedFunctionList are not used for any other purpose so + // we can just destroy them without bothering to make a copy. + + verifyCallGraph(collectReachableCallables(_creationGraph), move(m_creationFunctionList)); + m_creationFunctionList = {}; + + verifyCallGraph(collectReachableCallables(_deployedGraph), move(m_deployedFunctionList)); + m_deployedFunctionList = {}; +} + string IRGenerator::generate( ContractDefinition const& _contract, map const& _otherYulSources ) { + // Remember to call verifyCallGraphs() (which clears the list of generated functions) if you + // want to reuse the generator. + solAssert(m_creationFunctionList.empty(), ""); + solAssert(m_deployedFunctionList.empty(), ""); + auto subObjectSources = [&_otherYulSources](std::set const& subObjects) -> string { std::string subObjectsSources; @@ -142,8 +205,9 @@ string IRGenerator::generate( t("deploy", deployCode(_contract)); generateImplicitConstructors(_contract); - generateQueuedFunctions(); + m_creationFunctionList = generateQueuedFunctions(); InternalDispatchMap internalDispatchMap = generateInternalDispatchFunctions(); + t("functions", m_context.functionCollector().requestedFunctions()); t("subObjects", subObjectSources(m_context.subObjectsCreated())); @@ -162,7 +226,7 @@ string IRGenerator::generate( t("DeployedObject", IRNames::deployedObject(_contract)); t("library_address", IRNames::libraryAddressImmutable()); t("dispatch", dispatchRoutine(_contract)); - generateQueuedFunctions(); + m_deployedFunctionList = generateQueuedFunctions(); generateInternalDispatchFunctions(); t("deployedFunctions", m_context.functionCollector().requestedFunctions()); t("deployedSubObjects", subObjectSources(m_context.subObjectsCreated())); @@ -170,6 +234,7 @@ string IRGenerator::generate( // This has to be called only after all other code generation for the deployed object is complete. bool deployedInvolvesAssembly = m_context.inlineAssemblySeen(); t("memoryInitDeployed", memoryInit(!deployedInvolvesAssembly)); + return t.render(); } @@ -180,11 +245,20 @@ string IRGenerator::generate(Block const& _block) return generator.code(); } -void IRGenerator::generateQueuedFunctions() +set IRGenerator::generateQueuedFunctions() { + set functions; + while (!m_context.functionGenerationQueueEmpty()) + { + FunctionDefinition const& functionDefinition = *m_context.dequeueFunctionForCodeGeneration(); + + functions.emplace(&functionDefinition); // NOTE: generateFunction() may modify function generation queue - generateFunction(*m_context.dequeueFunctionForCodeGeneration()); + generateFunction(functionDefinition); + } + + return functions; } InternalDispatchMap IRGenerator::generateInternalDispatchFunctions() diff --git a/libsolidity/codegen/ir/IRGenerator.h b/libsolidity/codegen/ir/IRGenerator.h index 7b7c58bd4..f467b5d3f 100644 --- a/libsolidity/codegen/ir/IRGenerator.h +++ b/libsolidity/codegen/ir/IRGenerator.h @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -56,6 +57,11 @@ public: std::map const& _otherYulSources ); + void verifyCallGraphs( + FunctionCallGraphBuilder::ContractCallGraph const& _creationGraph, + FunctionCallGraphBuilder::ContractCallGraph const& _deployedGraph + ); + private: std::string generate( ContractDefinition const& _contract, @@ -65,7 +71,8 @@ private: /// Generates code for all the functions from the function generation queue. /// The resulting code is stored in the function collector in IRGenerationContext. - void generateQueuedFunctions(); + /// @returns A set of ast nodes of the generated functions. + std::set generateQueuedFunctions(); /// Generates all the internal dispatch functions necessary to handle any function that could /// possibly be called via a pointer. /// @return The content of the dispatch for reuse in runtime code. Reuse is necessary because @@ -115,6 +122,9 @@ private: langutil::EVMVersion const m_evmVersion; OptimiserSettings const m_optimiserSettings; + std::set m_creationFunctionList; + std::set m_deployedFunctionList; + IRGenerationContext m_context; YulUtilFunctions m_utils; }; From 53d70dec57f88bb407e864dcf0fee4e069a5240e Mon Sep 17 00:00:00 2001 From: Mathias Baumann Date: Wed, 18 Nov 2020 18:24:33 +0100 Subject: [PATCH 09/10] CompilerStack: Build call graphs in the analysis phase --- libsolidity/interface/CompilerStack.cpp | 33 +++++++++++++++++++++++++ libsolidity/interface/CompilerStack.h | 9 +++++++ 2 files changed, 42 insertions(+) diff --git a/libsolidity/interface/CompilerStack.cpp b/libsolidity/interface/CompilerStack.cpp index b1539e10a..426eaceba 100644 --- a/libsolidity/interface/CompilerStack.cpp +++ b/libsolidity/interface/CompilerStack.cpp @@ -399,6 +399,19 @@ bool CompilerStack::analyze() if (source->ast && !typeChecker.checkTypeRequirements(*source->ast)) noErrors = false; + if (noErrors) + { + for (Source const* source: m_sourceOrder) + if (source->ast) + for (ASTPointer const& node: source->ast->nodes()) + if (auto const* contractDefinition = dynamic_cast(node.get())) + { + Contract& contractState = m_contracts.at(contractDefinition->fullyQualifiedName()); + contractState.creationCallGraph.emplace(FunctionCallGraphBuilder::buildCreationGraph(*contractDefinition)); + contractState.deployedCallGraph.emplace(FunctionCallGraphBuilder::buildDeployedGraph(*contractDefinition, *contractState.creationCallGraph)); + } + } + if (noErrors) { // Checks that can only be done when all types of all AST nodes are known. @@ -937,6 +950,24 @@ string const& CompilerStack::metadata(Contract const& _contract) const return _contract.metadata.init([&]{ return createMetadata(_contract); }); } +FunctionCallGraphBuilder::ContractCallGraph const& CompilerStack::creationCallGraph(string const& _contractName) const +{ + if (m_stackState < AnalysisPerformed) + BOOST_THROW_EXCEPTION(CompilerError() << errinfo_comment("Analysis was not successful.")); + + solAssert(contract(_contractName).creationCallGraph.has_value(), ""); + return contract(_contractName).creationCallGraph.value(); +} + +FunctionCallGraphBuilder::ContractCallGraph const& CompilerStack::deployedCallGraph(string const& _contractName) const +{ + if (m_stackState < AnalysisPerformed) + BOOST_THROW_EXCEPTION(CompilerError() << errinfo_comment("Analysis was not successful.")); + + solAssert(contract(_contractName).deployedCallGraph.has_value(), ""); + return contract(_contractName).deployedCallGraph.value(); +} + Scanner const& CompilerStack::scanner(string const& _sourceName) const { if (m_stackState < SourcesSet) @@ -1275,6 +1306,8 @@ void CompilerStack::generateIR(ContractDefinition const& _contract) IRGenerator generator(m_evmVersion, m_revertStrings, m_optimiserSettings); tie(compiledContract.yulIR, compiledContract.yulIROptimized) = generator.run(_contract, otherYulSources); + + generator.verifyCallGraphs(compiledContract.creationCallGraph.value(), compiledContract.deployedCallGraph.value()); } void CompilerStack::generateEVMFromIR(ContractDefinition const& _contract) diff --git a/libsolidity/interface/CompilerStack.h b/libsolidity/interface/CompilerStack.h index db087a663..e8ab7c8c9 100644 --- a/libsolidity/interface/CompilerStack.h +++ b/libsolidity/interface/CompilerStack.h @@ -24,6 +24,7 @@ #pragma once +#include #include #include #include @@ -342,6 +343,12 @@ public: /// @returns a JSON representing the estimated gas usage for contract creation, internal and external functions Json::Value gasEstimates(std::string const& _contractName) const; + /// @returns a graph with edges representing calls between functions that may happen during contract construction. + FunctionCallGraphBuilder::ContractCallGraph const& creationCallGraph(std::string const& _contractName) const; + + /// @returns a graph with edges representing calls between functions that may happen in a deployed contract. + FunctionCallGraphBuilder::ContractCallGraph const& deployedCallGraph(std::string const& _contractName) const; + /// Changes the format of the metadata appended at the end of the bytecode. /// This is mostly a workaround to avoid bytecode and gas differences between compiler builds /// caused by differences in metadata. Should only be used for testing. @@ -383,6 +390,8 @@ private: util::LazyInit runtimeGeneratedSources; mutable std::optional sourceMapping; mutable std::optional runtimeSourceMapping; + std::optional creationCallGraph; + std::optional deployedCallGraph; }; /// Loads the missing sources from @a _ast (named @a _path) using the callback From 051995a373272b4ac53fa24d9ff86bbecc83d994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Thu, 28 Jan 2021 21:11:30 +0100 Subject: [PATCH 10/10] Test suite for FunctionCallGraph --- test/CMakeLists.txt | 1 + .../analysis/FunctionCallGraph.cpp | 2005 +++++++++++++++++ 2 files changed, 2006 insertions(+) create mode 100644 test/libsolidity/analysis/FunctionCallGraph.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index efdc3e7bb..2e9fbbc4e 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..506509ea9 --- /dev/null +++ b/test/libsolidity/analysis/FunctionCallGraph.cpp @@ -0,0 +1,2005 @@ +/* + 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 + +using namespace std; +using namespace ranges; +using namespace solidity::langutil; +using namespace solidity::frontend; + +using EdgeMap = map< + FunctionCallGraphBuilder::Node, + set, + FunctionCallGraphBuilder::CompareByID +>; +using EdgeNames = set>; +using CallGraphMap = map; + +namespace +{ + +unique_ptr parseAndAnalyzeContracts(string _sourceCode) +{ + ReadCallback::Callback fileReader = [](string const&, string const&) + { + soltestAssert(false, "For simplicity this test suite supports only files without imports."); + return ReadCallback::Result{true, ""}; + }; + + auto compilerStack = make_unique(fileReader); + compilerStack->setSources({{"", _sourceCode}}); + + // 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 errors. + bool success = compilerStack->parseAndAnalyze(); + soltestAssert(success, ""); + + soltestAssert( + ranges::all_of( + compilerStack->ast("").nodes(), + [](auto const& _node){ return !dynamic_cast(_node.get()); } + ), + "For simplicity this test suite supports only files without imports." + ); + + return compilerStack; +} + +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; +} + +tuple collectGraphs(CompilerStack const& _compilerStack) +{ + soltestAssert(!_compilerStack.hasError(), ""); + + tuple graphs; + + for (string const& fullyQualifiedContractName: _compilerStack.contractNames()) + { + soltestAssert(get<0>(graphs).count(fullyQualifiedContractName) == 0 && get<1>(graphs).count(fullyQualifiedContractName) == 0, ""); + + // This relies on two assumptions: (1) CompilerStack received an empty string as a path for + // the contract and (2) contracts used in test cases have no imports. + soltestAssert(fullyQualifiedContractName.size() > 0 && fullyQualifiedContractName[0] == ':', ""); + string contractName = fullyQualifiedContractName.substr(1); + + get<0>(graphs).emplace(contractName, &_compilerStack.creationCallGraph(fullyQualifiedContractName)); + get<1>(graphs).emplace(contractName, &_compilerStack.deployedCallGraph(fullyQualifiedContractName)); + } + + return graphs; +} + +void checkCallGraphExpectations( + CallGraphMap const& _callGraphs, + map const& _expectedEdges, + map> const& _expectedCreatedContractSets = {}, + map> const& _expectedEmittedEventSets = {} +) +{ + auto getContractName = [](ContractDefinition const* _contract){ return _contract->name(); }; + auto eventToString = [](EventDefinition const* _event){ return toString(FunctionCallGraphBuilder::Node(_event)); }; + 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( + (_expectedEdges | views::keys | to()) == (_callGraphs | views::keys | to()) && + (ranges::views::set_difference(_expectedCreatedContractSets | views::keys, _expectedEdges | views::keys)).empty(), + "Contracts listed in expectations do not match contracts actually found in the source file or in other expectations." + ); + for (string const& contractName: _expectedEdges | views::keys) + { + soltestAssert( + (ranges::views::set_difference(valueOrDefault(_expectedCreatedContractSets, contractName, {}), _expectedEdges | views::keys)).empty(), + "Inconsistent expectations: contract expected to be created but not to be present in the source file." + ); + } + + map edges; + map> createdContractSets; + map> emittedEventSets; + for (string const& contractName: _expectedEdges | views::keys) + { + soltestAssert(_callGraphs.at(contractName) != nullptr, ""); + FunctionCallGraphBuilder::ContractCallGraph const& callGraph = *_callGraphs.at(contractName); + + edges[contractName] = edgeNames(callGraph.edges); + if (!callGraph.createdContracts.empty()) + createdContractSets[contractName] = callGraph.createdContracts | views::transform(getContractName) | to>(); + if (!callGraph.emittedEvents.empty()) + emittedEventSets[contractName] = callGraph.emittedEvents | views::transform(eventToString) | to>(); + } + + BOOST_CHECK_EQUAL(edges, _expectedEdges); + BOOST_CHECK_EQUAL(createdContractSets, _expectedCreatedContractSets); + BOOST_CHECK_EQUAL(emittedEventSets, _expectedEmittedEventSets); +} + +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) +{ + _out << "{" << (_set | views::join(", ") | to()) << "}"; + return _out; +} + +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) +{ + // 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 + +namespace boost::test_tools::tt_detail +{ + +// Boost won't find find the << operator unless we put it in the std namespace which is illegal. +// The recommended solution is to overload print_log_value<> struct and make it use our operator. + +template<> +struct print_log_value +{ + void operator()(std::ostream& _output, EdgeNames const& _edgeNames) { ::operator<<(_output, _edgeNames); } +}; + +template<> +struct print_log_value> +{ + void operator()(std::ostream& _output, set const& _set) { ::operator<<(_output, _set); } +}; + +template<> +struct print_log_value> +{ + void operator()(std::ostream& _output, map const& _edgeSets) { ::operator<<(_output, _edgeSets); } +}; + +template<> +struct print_log_value>> +{ + void operator()(std::ostream& _output, map> const& _map) { ::operator<<(_output, _map); } +}; + +} // namespace boost::test_tools::tt_detail + +namespace solidity::frontend::test +{ + +BOOST_AUTO_TEST_SUITE(FunctionCallGraphTest) + +BOOST_AUTO_TEST_CASE(only_definitions) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(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); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"C", {}}, + {"L", {}}, + }; + + map expectedDeployedEdges = { + {"C", { + {"Entry", "function C.ext()"}, + {"Entry", "function C.pub()"}, + }}, + {"L", { + {"Entry", "function L.ext()"}, + {"Entry", "function L.pub()"}, + }}, + }; + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges); +} + +BOOST_AUTO_TEST_CASE(ordinary_calls) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(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); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"C", {}}, + {"L", {}}, + }; + + map expectedDeployedEdges = { + {"C", { + {"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", { + {"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()"}, + }}, + }; + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges); +} + +BOOST_AUTO_TEST_CASE(call_chains_through_externals) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(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); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"C", {}}, + {"L", {}}, + }; + + map expectedDeployedEdges = { + {"C", { + {"Entry", "function C.ext()"}, + {"Entry", "function C.ext2()"}, + {"Entry", "function C.pub()"}, + {"Entry", "function C.pub2()"}, + {"Entry", "function C.pub3()"}, + }}, + {"L", { + {"Entry", "function L.ext()"}, + {"Entry", "function L.pub()"}, + }}, + }; + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges); +} + +BOOST_AUTO_TEST_CASE(calls_from_constructors) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(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); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"C", { + {"Entry", "constructor of C"}, + {"constructor of C", "function C.inr()"}, + {"constructor of C", "function free()"}, + }}, + {"D", { + {"Entry", "function D.inr()"}, + {"Entry", "function free()"}, + }}, + {"L", {}}, + }; + + map expectedDeployedEdges = { + {"C", { + {"Entry", "function C.ext()"}, + }}, + {"D", { + {"Entry", "function D.ext()"}, + }}, + {"L", { + {"Entry", "function L.ext()"}, + }}, + }; + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges); +} + +BOOST_AUTO_TEST_CASE(calls_to_constructors) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(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); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"C", { + {"Entry", "constructor of C"}, + }}, + {"D", {}}, + {"L", {}}, + }; + + map expectedDeployedEdges = { + {"C", { + {"Entry", "function C.ext()"}, + {"function C.ext()", "function C.inr()"}, + {"function C.inr()", "function free()"}, + }}, + {"D", {}}, + {"L", { + {"Entry", "function L.ext()"}, + {"function L.ext()", "function L.inr()"}, + {"function L.inr()", "function free()"}, + }}, + }; + + map> expectedCreatedContractsAtCreation = { + {"C", {"D"}}, + }; + map> expectedCreatedContractsAfterDeployment = { + {"C", {"D"}}, + {"L", {"C", "D"}}, + }; + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges, expectedCreatedContractsAtCreation); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges, expectedCreatedContractsAfterDeployment); +} + +BOOST_AUTO_TEST_CASE(inherited_constructors) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(R"( + function free() {} + + library L { + function ext() external { inr(); } + function inr() internal { free(); } + } + + contract C { + constructor() { inrC(); free(); } + + function extC() external returns (uint) { inrC(); } + function inrC() internal returns (uint) { free(); } + } + + contract D { + constructor() { L.ext(); } + } + + contract E is C { + uint e2 = this.extE(); + uint i2 = inrE(); + + function extE() external returns (uint) { inrE(); } + function inrE() internal returns (uint) { free(); } + } + + contract F is C, D(), E { + uint e3 = this.extF(); + uint i3 = inrF(); + + constructor() E() C() {} + + function extF() external returns (uint) { inrF(); } + function inrF() internal returns (uint) { free(); } + } + + contract G is E() { + function extG() external returns (uint) { new F(); } + } + )"s); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"C", { + {"Entry", "constructor of C"}, + {"constructor of C", "function C.inrC()"}, + {"constructor of C", "function free()"}, + {"function C.inrC()", "function free()"}, + }}, + {"D", { + {"Entry", "constructor of D"}, + }}, + {"E", { + {"Entry", "constructor of C"}, + {"Entry", "function E.inrE()"}, + {"constructor of C", "function C.inrC()"}, + {"constructor of C", "function free()"}, + {"function C.inrC()", "function free()"}, + {"function E.inrE()", "function free()"}, + }}, + {"F", { + {"Entry", "constructor of C"}, + {"Entry", "constructor of D"}, + {"Entry", "constructor of F"}, + {"Entry", "function E.inrE()"}, + {"Entry", "function F.inrF()"}, + {"constructor of C", "function C.inrC()"}, + {"constructor of C", "function free()"}, + {"function C.inrC()", "function free()"}, + {"function E.inrE()", "function free()"}, + {"function F.inrF()", "function free()"}, + }}, + {"G", { + {"Entry", "constructor of C"}, + {"Entry", "function E.inrE()"}, + {"constructor of C", "function C.inrC()"}, + {"constructor of C", "function free()"}, + {"function C.inrC()", "function free()"}, + {"function E.inrE()", "function free()"}, + }}, + {"L", {}}, + }; + + map expectedDeployedEdges = { + {"C", { + {"Entry", "function C.extC()"}, + {"function C.extC()", "function C.inrC()"}, + {"function C.inrC()", "function free()"}, + }}, + {"D", {}}, + {"E", { + {"Entry", "function C.extC()"}, + {"Entry", "function E.extE()"}, + {"function C.extC()", "function C.inrC()"}, + {"function E.extE()", "function E.inrE()"}, + {"function C.inrC()", "function free()"}, + {"function E.inrE()", "function free()"}, + }}, + {"F", { + {"Entry", "function C.extC()"}, + {"Entry", "function E.extE()"}, + {"Entry", "function F.extF()"}, + {"function C.extC()", "function C.inrC()"}, + {"function E.extE()", "function E.inrE()"}, + {"function F.extF()", "function F.inrF()"}, + {"function C.inrC()", "function free()"}, + {"function E.inrE()", "function free()"}, + {"function F.inrF()", "function free()"}, + }}, + {"G", { + {"Entry", "function C.extC()"}, + {"Entry", "function E.extE()"}, + {"Entry", "function G.extG()"}, + {"function C.extC()", "function C.inrC()"}, + {"function E.extE()", "function E.inrE()"}, + {"function C.inrC()", "function free()"}, + {"function E.inrE()", "function free()"}, + }}, + {"L", { + {"Entry", "function L.ext()"}, + {"function L.ext()", "function L.inr()"}, + {"function L.inr()", "function free()"}, + }}, + }; + + map> expectedCreatedContractsAtCreation = {}; + map> expectedCreatedContractsAfterDeployment = { + {"G", {"F"}}, + }; + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges, expectedCreatedContractsAtCreation); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges, expectedCreatedContractsAfterDeployment); +} + +BOOST_AUTO_TEST_CASE(inheritance_specifiers) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(R"( + function fD() returns (uint) {} + function fE() returns (uint) {} + function fFD() returns (uint) {} + function fFE() returns (uint) {} + function fG() returns (uint) {} + + function fVarC() returns (uint) {} + function fVarD() returns (uint) {} + function fVarE() returns (uint) {} + function fVarF() returns (uint) {} + + contract C { + uint c = fVarC(); + + constructor (uint) {} + } + + contract D is C(fD()) { + uint d = fVarD(); + + constructor (uint) {} + } + + abstract contract E is C { + uint e = fVarE(); + + constructor (uint) {} + } + + contract F is D(fFD()), E { + uint f = fVarF(); + + constructor (uint) E(fFE()) {} + } + + contract G is D(fG()), E(fG()) {} + )"s); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"C", { + {"Entry", "constructor of C"}, + {"Entry", "function fVarC()"}, + }}, + {"D", { + {"Entry", "constructor of C"}, + {"Entry", "constructor of D"}, + {"Entry", "function fVarC()"}, + {"Entry", "function fVarD()"}, + {"constructor of D", "function fD()"}, + }}, + {"E", { + {"Entry", "constructor of C"}, + {"Entry", "constructor of E"}, + {"Entry", "function fVarC()"}, + {"Entry", "function fVarE()"}, + }}, + {"F", { + {"Entry", "constructor of C"}, + {"Entry", "constructor of D"}, + {"Entry", "constructor of E"}, + {"Entry", "constructor of F"}, + {"Entry", "function fVarC()"}, + {"Entry", "function fVarD()"}, + {"Entry", "function fVarE()"}, + {"Entry", "function fVarF()"}, + {"constructor of D", "function fD()"}, + {"constructor of F", "function fFD()"}, + {"constructor of F", "function fFE()"}, + }}, + {"G", { + {"Entry", "constructor of C"}, + {"Entry", "constructor of D"}, + {"Entry", "constructor of E"}, + // G, unlike F, has no constructor so fG() gets an edge from Entry. + {"Entry", "function fG()"}, + {"Entry", "function fVarC()"}, + {"Entry", "function fVarD()"}, + {"Entry", "function fVarE()"}, + {"constructor of D", "function fD()"}, + }}, + }; + + map expectedDeployedEdges = { + {"C", {}}, + {"D", {}}, + {"E", {}}, + {"F", {}}, + {"G", {}}, + }; + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges); +} + +BOOST_AUTO_TEST_CASE(inherited_functions_virtual_and_super) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(R"( + contract C { + function f() internal {} + function g() internal virtual {} + function h() internal virtual {} + + function ext() external virtual {} + } + + contract D { + function h() internal virtual {} + + function ext() external virtual {} + } + + contract E is C, D { + function g() internal override {} + function h() internal override(C, D) {} + function i() internal {} + + function ext() external override(C, D) {} + + 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); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"C", {}}, + {"D", {}}, + {"E", {}}, + }; + + map expectedDeployedEdges = { + {"C", { + {"Entry", "function C.ext()"}, + }}, + {"D", { + {"Entry", "function D.ext()"}, + }}, + {"E", { + {"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()"}, + {"Entry", "function E.ext()"}, + {"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()"}, + }}, + }; + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges); +} + +BOOST_AUTO_TEST_CASE(overloaded_functions) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(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); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"C", {}}, + {"D", {}}, + }; + + map expectedDeployedEdges = { + {"C", { + {"Entry", "function C.f(bool)"}, + }}, + {"D", { + {"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)"}, + }}, + }; + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges); +} + +BOOST_AUTO_TEST_CASE(modifiers) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(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); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"C", {}}, + {"D", {}}, + {"L", {}}, + {"E", { + {"Entry", "constructor of E"}, + {"constructor of E", "modifier E.m1"}, + {"constructor of E", "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"}, + {"modifier L.m", "function L.g()"}, + {"modifier D.m2", "function E.q()"}, + {"modifier E.m3", "function D.p()"}, + }}, + }; + + map expectedDeployedEdges = { + {"C", {}}, + {"D", {}}, + {"L", {}}, + {"E", { + {"Entry", "function E.ext()"}, + {"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> expectedCreatedContractsAtCreation = {{"E", {"C"}}}; + map> expectedCreatedContractsAfterDeployment = {{"E", {"C"}}}; + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges, expectedCreatedContractsAtCreation); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges, expectedCreatedContractsAfterDeployment); +} + +BOOST_AUTO_TEST_CASE(events) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(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(f()); inr(); } + function inr() m internal { emit EvD1(1); emit C.EvC(f()); L.inr(); free(); EvD2; } + + function f() internal returns (uint) {} + } + )"s); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"C", {}}, + {"D", {}}, + {"L", {}}, + }; + + map expectedDeployedEdges = { + {"C", {}}, + {"D", { + {"Entry", "function D.ext()"}, + {"function D.ext()", "function D.inr()"}, + {"function D.ext()", "modifier C.m"}, + {"function D.ext()", "function D.f()"}, + {"function D.inr()", "function L.inr()"}, + {"function D.inr()", "function free()"}, + {"function D.inr()", "modifier C.m"}, + {"function D.inr()", "function D.f()"}, + }}, + {"L", { + {"Entry", "function L.ext()"}, + }}, + }; + + map> expectedCreationEvents = {}; + map> expectedDeployedEvents = { + {"D", { + "event D.EvD1(uint256)", + "event C.EvC(uint256)", + "event L.Ev(bytes4,string)", + "event L.Ev()", + }}, + {"L", { + "event L.Ev()", + }}, + }; + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges, {}, expectedCreationEvents); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges, {}, expectedDeployedEvents); +} + +BOOST_AUTO_TEST_CASE(cycles) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(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); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"L", {}}, + {"C", {}}, + {"D", {}}, + }; + + map expectedDeployedEdges = { + {"L", {}}, + {"C", {}}, + {"D", { + {"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()"}, + }}, + }; + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges); +} + +BOOST_AUTO_TEST_CASE(interfaces_and_abstract_contracts) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(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); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"I", {}}, + {"J", {}}, + {"C", {}}, + {"D", {}}, + }; + + map expectedDeployedEdges = { + {"I", { + {"Entry", "function I.ext1()"}, + {"Entry", "function I.ext2()"}, + }}, + {"J", { + {"Entry", "function I.ext1()"}, + {"Entry", "function J.ext2()"}, + {"Entry", "function J.ext3()"}, + }}, + {"C", { + {"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", { + {"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()", "function D.inr1()"}, + {"function D.ext1()", "function C.inr2()"}, + }}, + }; + + map> expectedCreationEvents = {}; + map> expectedDeployedEvents = { + {"D", { + "event I.Ev(uint256)", + }}, + }; + + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges, {}, expectedCreationEvents); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges, {}, expectedDeployedEvents); +} + +BOOST_AUTO_TEST_CASE(indirect_calls) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(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); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"L", {}}, + {"C", {}}, + {"D", { + {"InternalDispatch", "function L.inr2()"}, + {"InternalDispatch", "function C.inr1()"}, + {"InternalDispatch", "function C.inr2()"}, + {"InternalDispatch", "function free1()"}, + {"InternalDispatch", "function free2()"}, + {"InternalDispatch", "function L.inr1()"}, + {"InternalDispatch", "function L.inr2()"}, + {"Entry", "constructor of D"}, + {"constructor of D", "function C.access()"}, + {"constructor of D", "function C.expression()"}, + {"function C.expression()", "InternalDispatch"}, + }}, + }; + + map expectedDeployedEdges = { + {"L", { + {"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", "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", { + {"InternalDispatch", "function L.inr2()"}, + {"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"}, + }}, + }; + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges); +} + +BOOST_AUTO_TEST_CASE(calls_via_pointers) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(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); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"L", {}}, + {"C", {}}, + {"D", { + {"InternalDispatch", "function C.inr1()"}, + {"InternalDispatch", "function C.inr2()"}, + {"InternalDispatch", "function L.inr1()"}, + {"InternalDispatch", "function L.inr2()"}, + {"InternalDispatch", "function free1()"}, + {"InternalDispatch", "function free2()"}, + {"Entry", "constructor of D"}, + {"constructor of D", "function C.testLocalVars()"}, + {"constructor of D", "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.callStatePtrs()", "InternalDispatch"}, + {"function L.callPtrs(function () external,function (),function (),function ())", "InternalDispatch"}, + }}, + }; + + map expectedDeployedEdges = { + {"L", {}}, + {"C", { + {"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", { + {"InternalDispatch", "function C.inr1()"}, + {"InternalDispatch", "function C.inr2()"}, + {"InternalDispatch", "function L.inr1()"}, + {"InternalDispatch", "function L.inr2()"}, + {"InternalDispatch", "function free1()"}, + {"InternalDispatch", "function free2()"}, + {"Entry", "function C.ext1()"}, + {"Entry", "function C.ext2()"}, + {"Entry", "function C.ext3()"}, + {"Entry", "function C.testLocalVars()"}, + {"Entry", "function D.testStateVars()"}, + {"Entry", "function D.testImmutablePtr()"}, + {"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()", "InternalDispatch"}, + {"function L.callPtrs(function () external,function (),function (),function ())", "InternalDispatch"}, + }}, + }; + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges); +} + +BOOST_AUTO_TEST_CASE(pointer_to_overridden_function) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(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); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"C", {}}, + {"D", {}}, + }; + + map expectedDeployedEdges = { + {"C", {}}, + {"D", { + {"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"}, + }}, + }; + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges); +} + +BOOST_AUTO_TEST_CASE(pointer_to_nonexistent_function) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(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); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"I", {}}, + {"C", {}}, + {"D", {}}, + }; + + map expectedDeployedEdges = { + {"I", { + {"Entry", "function I.f()"}, + }}, + {"C", { + {"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", "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"}, + }}, + }; + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges); +} + +BOOST_AUTO_TEST_CASE(function_self_reference) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(R"( + contract C { + function f() public returns (bool ret) { + return f == f; + } + } + )"s); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"C", {}}, + }; + + map expectedDeployedEdges = { + {"C", { + {"Entry", "function C.f()"}, + {"InternalDispatch", "function C.f()"}, + }}, + }; + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges); +} + +BOOST_AUTO_TEST_CASE(pointer_cycle) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(R"( + contract C { + function () ptr = f; + + function f() internal { ptr(); } + + function test() public { + ptr(); + } + } + )"s); + + map expectedCreationEdges = { + {"C", { + {"InternalDispatch", "function C.f()"}, + {"function C.f()", "InternalDispatch"}, + }}, + }; + + map expectedDeployedEdges = { + {"C", { + {"InternalDispatch", "function C.f()"}, + {"Entry", "function C.test()"}, + {"function C.test()", "InternalDispatch"}, + {"function C.f()", "InternalDispatch"}, + }}, + }; + tuple graphs = collectGraphs(*compilerStack); + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges); +} + +BOOST_AUTO_TEST_CASE(using_for) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(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); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"L", {}}, + {"C", {}}, + }; + + map expectedDeployedEdges = { + {"L", { + {"Entry", "function L.ext(struct S)"}, + }}, + {"C", { + {"Entry", "function C.test()"}, + {"function C.test()", "function L.inr(struct S)"}, + }}, + }; + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges); +} + +BOOST_AUTO_TEST_CASE(getters) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(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); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"C", {}}, + }; + + map expectedDeployedEdges = { + {"C", {{"Entry", "function C.test()"}}}, + }; + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges); +} + +BOOST_AUTO_TEST_CASE(fallback_and_receive) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(R"( + contract C { + function ext() external {} + function inr() internal {} + + fallback() external { + this.ext(); + inr(); + } + + receive() external payable { + this.ext(); + inr(); + } + } + + 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); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"C", {}}, + {"D", {}}, + {"E", {}}, + }; + + map expectedDeployedEdges = { + {"C", { + {"Entry", "function C.ext()"}, + {"Entry", "receive of C"}, + {"Entry", "fallback of C"}, + {"fallback of C", "function C.inr()"}, + {"receive of C", "function C.inr()"}, + }}, + {"D", { + {"Entry", "function D.test()"}, + {"Entry", "fallback of D"}, + }}, + {"E", { + {"Entry", "function C.ext()"}, + {"Entry", "fallback of C"}, + {"Entry", "receive of C"}, + {"fallback of C", "function C.inr()"}, + {"receive of C", "function C.inr()"}, + }}, + }; + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges); +} + +BOOST_AUTO_TEST_CASE(virtual_fallback_and_receive) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(R"( + contract C { + fallback() external virtual {} + receive() external payable virtual {} + } + + contract D is C {} + + contract E is D { + fallback() external virtual override {} + receive() external payable virtual override {} + } + + contract F is E { + fallback() external override {} + receive() external payable override {} + } + )"s); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"C", {}}, + {"D", {}}, + {"E", {}}, + {"F", {}}, + }; + + map expectedDeployedEdges = { + {"C", { + {"Entry", "receive of C"}, + {"Entry", "fallback of C"}, + }}, + {"D", { + {"Entry", "receive of C"}, + {"Entry", "fallback of C"}, + }}, + {"E", { + {"Entry", "receive of E"}, + {"Entry", "fallback of E"}, + }}, + {"F", { + {"Entry", "receive of F"}, + {"Entry", "fallback of F"}, + }}, + }; + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges); +} + +BOOST_AUTO_TEST_CASE(builtins) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(R"( + interface I {} + + contract C { + function accessBuiltin() public payable { + abi.decode; + abi.encode; + abi.encodePacked; + abi.encodeWithSelector; + abi.encodeWithSignature; + block.chainid; + block.coinbase; + block.difficulty; + block.gaslimit; + block.number; + block.timestamp; + gasleft; + msg.data; + msg.sender; + msg.value; + tx.gasprice; + tx.origin; + blockhash; + keccak256; + sha256; + ripemd160; + ecrecover; + addmod; + mulmod; + this; + super; + selfdestruct; + address(0).balance; + address(0).code; + address(0).codehash; + payable(0).send; + payable(0).transfer; + address(0).call; + address(0).delegatecall; + address(0).staticcall; + type(C).name; + type(I).interfaceId; + type(uint).min; + type(uint).max; + assert; + } + + function callBuiltin() public payable { + bytes memory data; + + abi.decode(data, (uint)); + abi.encode(0); + abi.encodePacked(data); + abi.encodeWithSelector(0x12345678); + abi.encodeWithSignature("abc"); + gasleft(); + blockhash(0); + keccak256(data); + sha256(data); + ripemd160(data); + ecrecover(0x0, 0, 0, 0); + addmod(1, 2, 3); + mulmod(1, 2, 3); + selfdestruct(payable(0)); + payable(0).send(0); + payable(0).transfer(0); + address(0).call(data); + address(0).delegatecall(data); + address(0).staticcall(data); + assert(true); + require(true); + revert(); + } + } + )"s); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"I", {}}, + {"C", {}}, + }; + + map expectedDeployedEdges = { + {"I", {}}, + {"C", { + {"Entry", "function C.accessBuiltin()"}, + {"Entry", "function C.callBuiltin()"}, + }}, + }; + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges); +} + +BOOST_AUTO_TEST_CASE(conversions_and_struct_array_constructors) +{ + unique_ptr compilerStack = parseAndAnalyzeContracts(R"( + interface I {} + + enum E {A, B, C} + + struct S { + uint a; + E b; + } + + contract C is I { + uint[] u; + + function convert() public payable { + uint(0); + int(0); + bool(true); + bytes16(0); + payable(address(0)); + E(0); + C(address(C(address(0)))); + I(C(address(0))); + + bytes memory b; + string(b); + bytes(b); + } + + function create() public payable { + S(1, E.A); + + uint[3] memory u3; + uint[3](u3); + uint[](new uint[](3)); + } + + function pushPop() public payable { + u.push(); + u.push(1); + u.pop(); + } + } + )"s); + tuple graphs = collectGraphs(*compilerStack); + + map expectedCreationEdges = { + {"I", {}}, + {"C", {}}, + }; + + map expectedDeployedEdges = { + {"I", {}}, + {"C", { + {"Entry", "function C.convert()"}, + {"Entry", "function C.create()"}, + {"Entry", "function C.pushPop()"}, + }}, + }; + + checkCallGraphExpectations(get<0>(graphs), expectedCreationEdges); + checkCallGraphExpectations(get<1>(graphs), expectedDeployedEdges); +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace solidity::frontend::test