Skip to content

CFG and Standards package improvements - Introducing Providers #166

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/gosec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ jobs:
SOLC_SWITCH_GITHUB_TOKEN: ${{ secrets.SOLC_SWITCH_GITHUB_TOKEN }}
FULL_NODE_TEST_URL: ${{ secrets.FULL_NODE_TEST_URL }}
ARCHIVE_NODE_TEST_URL: ${{ secrets.ARCHIVE_NODE_TEST_URL }}
ETHERSCAN_API_KEYS: ${{ secrets.ETHERSCAN_API_KEYS }}
steps:
- name: Checkout Source
uses: actions/checkout@v3
Expand All @@ -29,4 +30,4 @@ jobs:
- name: Run Gosec Security Scanner
uses: securego/gosec@master
with:
args: --exclude=G302,G304,G306 ./...
args: --exclude=G302,G304,G306,G107 ./...
1 change: 1 addition & 0 deletions .github/workflows/goveralls.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ jobs:
SOLC_SWITCH_GITHUB_TOKEN: ${{ secrets.SOLC_SWITCH_GITHUB_TOKEN }}
FULL_NODE_TEST_URL: ${{ secrets.FULL_NODE_TEST_URL }}
ARCHIVE_NODE_TEST_URL: ${{ secrets.ARCHIVE_NODE_TEST_URL }}
ETHERSCAN_API_KEYS: ${{ secrets.ETHERSCAN_API_KEYS }}
strategy:
fail-fast: false
matrix:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ jobs:
SOLC_SWITCH_GITHUB_TOKEN: ${{ secrets.SOLC_SWITCH_GITHUB_TOKEN }}
FULL_NODE_TEST_URL: ${{ secrets.FULL_NODE_TEST_URL }}
ARCHIVE_NODE_TEST_URL: ${{ secrets.ARCHIVE_NODE_TEST_URL }}
ETHERSCAN_API_KEYS: ${{ secrets.ETHERSCAN_API_KEYS }}
steps:
- name: Checkout Source
uses: actions/checkout@v3
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,19 @@ This project is ideal for those diving into data analysis, construction of robus

## Solidity Version Support

**Currently Solidity versions equal or higher to 0.6.0 are supported.**
**Currently, Solidity versions equal or higher to 0.6.0 are supported.**

Older versions may or may not work due to changes in syntax that is not currently supported by the grammar file. In the future, we have plans to support all versions of Solidity.

## Disclaimer

Please be aware that this project is still under active development. While it is approaching a state suitable for production use, there may still be undiscovered issues or limitations. Over the next few months, extensive testing will be conducted to evaluate its performance and stability. Additional tests and documentation will also be added during this phase.
Additionally, most of the interfaces will stay as is, however, there could be architectural changes that may break your build in the future. I'll try to change as less as possible and notify everyone about the change in release notes.

Once I am confident that the project is fully ready for production, this disclaimer will be removed. Until then, please use the software with caution and report any potential issues or feedback to help improve its quality.



## Documentation

The SolGo basic documentation is hosted on GitHub, ensuring it's always up-to-date with the latest changes and features. You can access the full documentation [here](https://github.com/unpackdev/solgo/wiki).
Expand Down
173 changes: 49 additions & 124 deletions cfg/builder.go
Original file line number Diff line number Diff line change
@@ -1,161 +1,86 @@
package cfg

import (
"bytes"
"context"
"errors"
"fmt"

"github.com/goccy/go-graphviz"
"github.com/goccy/go-graphviz/cgraph"
"github.com/unpackdev/solgo/ir"
)

// Builder is responsible for constructing the control flow graph.
// Builder is responsible for constructing the control flow graph (CFG) of Solidity contracts.
// It utilizes the Intermediate Representation (IR) provided by solgo and Graphviz for graph operations.
type Builder struct {
ctx context.Context // Context for the builder operations
builder *ir.Builder // IR builder from solgo
viz *graphviz.Graphviz // Graphviz instance for graph operations
ctx context.Context // Context for the builder operations.
builder *ir.Builder // IR builder from solgo, used for generating the IR of the contracts.
viz *graphviz.Graphviz // Graphviz instance for visualizing the CFG.
graph *Graph // Internal representation of the CFG.
}

// NewBuilder initializes a new CFG builder.
func NewBuilder(ctx context.Context, builder *ir.Builder) *Builder {
// NewBuilder initializes a new CFG builder with the given context and IR builder.
// Returns an error if the provided IR builder is nil or if it does not have a root contract set.
func NewBuilder(ctx context.Context, builder *ir.Builder) (*Builder, error) {
if builder == nil || builder.GetRoot() == nil {
return nil, errors.New("builder is not set")
}

return &Builder{
ctx: ctx,
builder: builder,
viz: graphviz.New(),
}
graph: NewGraph(),
}, nil
}

// Close releases any resources used by the Graphviz instance.
func (b *Builder) Close() error {
return b.viz.Close()
// GetGraph returns the internal Graph instance of the CFG.
func (b *Builder) GetGraph() *Graph {
return b.graph
}

// GetGraphviz returns the underlying Graphviz instance.
func (b *Builder) GetGraphviz() *graphviz.Graphviz {
return b.viz
}

// Build constructs the control flow graph for the given IR.
func (b *Builder) Build() (*cgraph.Graph, error) {
if b.viz == nil {
return nil, errors.New("graphviz instance is not set")
}

// Build processes the Solidity contracts using the IR builder to construct the CFG.
// It identifies the entry contract and explores all dependencies and inherited contracts.
// Returns an error if the root node or entry contract is not set in the IR builder.
func (b *Builder) Build() error {
root := b.builder.GetRoot()
if root == nil {
return nil, errors.New("root node is not set")
}

graph, err := b.viz.Graph()
if err != nil {
return nil, err
return errors.New("root node is not set in IR builder")
}

if _, err := b.traverseIR(root, graph); err != nil {
return nil, err
entryContract := root.GetEntryContract()
if entryContract == nil {
return errors.New("no entry contract found")
}

return graph, nil
}

// traverseIR recursively traverses the IR to build nodes and edges for the graph.
func (b *Builder) traverseIR(root *ir.RootSourceUnit, graph *cgraph.Graph) (*cgraph.Node, error) {
rootNode, err := graph.CreateNode("You")
if err != nil {
return nil, err
if b.graph == nil {
b.graph = NewGraph()
}

nodeMap := make(map[string]*cgraph.Node)
nodeMap["You"] = rootNode

if len(root.Contracts) == 0 {
return nil, nil
}

for _, contract := range root.Contracts {
// Create a subgraph for the contract with the "cluster" prefix
clusterName := fmt.Sprintf("cluster_%s", contract.GetName())
contractSubGraph := graph.SubGraph(clusterName, 1)
contractSubGraph.SetLabel(contract.GetName())

// Create a node for the contract within the subgraph
contractNode, err := contractSubGraph.CreateNode(contract.GetName())
if err != nil {
return nil, err
}

// Link the rootNode to the contractNode
if _, err := graph.CreateEdge("", rootNode, contractNode); err != nil {
return nil, err
}

// Traverse functions within the contract (assuming there's a method to get functions)
for _, function := range contract.GetFunctions() {
funcNode, err := contractSubGraph.CreateNode(function.GetName())
if err != nil {
return nil, err
}
nodeMap[function.GetName()] = funcNode

// Link the contractNode to the funcNode
if _, err := graph.CreateEdge("", contractNode, funcNode); err != nil {
return nil, err
}

refFns := b.builder.LookupReferencedFunctionsByNode(function.GetAST())
for _, refFn := range refFns {
refFnNode, exists := nodeMap[refFn.GetName()]
if !exists {
refFnNode, err = graph.CreateNode(refFn.GetName())
if err != nil {
return nil, err
}
nodeMap[refFn.GetName()] = refFnNode
var dfs func(contract *ir.Contract, isEntryContract bool)
dfs = func(contract *ir.Contract, isEntryContract bool) {
if !b.graph.NodeExists(contract.GetName()) {
b.graph.AddNode(contract.GetName(), contract, isEntryContract)
allRelatedContracts := make([]*ir.Contract, 0)
for _, importStmt := range contract.GetImports() {
importedContract := root.GetContractById(importStmt.GetContractId())
if importedContract != nil {
b.graph.AddDependency(contract.GetName(), importStmt)
allRelatedContracts = append(allRelatedContracts, importedContract)
}

// Create an edge from the current function node to the referenced function node
if _, err := graph.CreateEdge("", funcNode, refFnNode); err != nil {
return nil, err
}
for _, baseContract := range contract.GetBaseContracts() {
baseContractId := baseContract.GetBaseName().GetReferencedDeclaration()
baseContractObj := root.GetContractBySourceUnitId(baseContractId)
if baseContractObj != nil {
b.graph.AddInheritance(contract.GetName(), baseContract)
allRelatedContracts = append(allRelatedContracts, baseContractObj)
}
}
for _, relatedContract := range allRelatedContracts {
dfs(relatedContract, false)
}
}
}

return rootNode, nil
}

// GenerateDOT produces the DOT representation of the given graph.
func (b *Builder) GenerateDOT(graph *cgraph.Graph) (string, error) {
if b.viz == nil {
return "", errors.New("graphviz instance is not set")
}

if graph == nil {
return "", errors.New("graph is not set")
}

var buf bytes.Buffer
if err := b.viz.Render(graph, "dot", &buf); err != nil {
return "", err
}

return buf.String(), nil
}

// SaveAs renders the graph to a file in the specified format.
func (b *Builder) SaveAs(graph *cgraph.Graph, format graphviz.Format, file string) error {
if b.viz == nil {
return errors.New("graphviz instance is not set")
}

if graph == nil {
return errors.New("graph is not set")
}

if err := b.viz.RenderFilename(graph, format, file); err != nil {
return err
}
dfs(entryContract, true)
return nil
}
27 changes: 27 additions & 0 deletions cfg/builder_json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package cfg

import (
"encoding/json"
"fmt"
)

// ToJSON converts a specified contract or the entire graph to a JSON representation.
// This method is part of the Builder type which presumably builds or manages the CFG.
//
// If a contractName is provided, the method attempts to find and convert only the specified
// contract node within the graph to JSON. If the specified contract is not found, it returns
// an error.
//
// If no contractName is provided (i.e., an empty string), the method converts the entire graph
// to JSON, representing all nodes within the graph.
func (b *Builder) ToJSON(contractName string) ([]byte, error) {
if contractName == "" {
return json.Marshal(b.GetGraph().Nodes)
}

node, exists := b.GetGraph().Nodes[contractName]
if !exists {
return []byte{}, fmt.Errorf("contract %s not found in the graph", contractName)
}
return json.Marshal(node)
}
46 changes: 46 additions & 0 deletions cfg/builder_mermaid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package cfg

import (
"fmt"
"strings"
)

// ToMermaid generates a string representation of the control flow graph (CFG) in Mermaid syntax.
// Mermaid is a tool that generates diagrams and flowcharts from text in a similar manner as markdown.
//
// The function iterates through each node in the graph and constructs the Mermaid syntax accordingly.
// It represents each contract in the graph as a node in the Mermaid graph. Entry contracts are
// distinguished with a special notation. The function also visualizes dependencies and inheritance
// relationships between contracts using arrows in the Mermaid syntax.
//
// If the graph is nil or contains no nodes, the function returns a default string indicating that
// no contracts are found.
func (b *Builder) ToMermaid() string {
if b.graph == nil || len(b.graph.Nodes) == 0 {
return "graph LR\n No_Contracts[No contracts found]"
}

var mermaidGraph strings.Builder
mermaidGraph.WriteString("graph LR\n")
for _, node := range b.graph.Nodes {
nodeName := node.Name

if node.EntryContract {
mermaidGraph.WriteString(fmt.Sprintf(" %s[(%s - Entry)]\n", nodeName, nodeName))
} else {
mermaidGraph.WriteString(fmt.Sprintf(" %s[%s]\n", nodeName, nodeName))
}

for _, imp := range node.Imports {
importedName := imp.GetAbsolutePath()
mermaidGraph.WriteString(fmt.Sprintf(" %s --> %s\n", nodeName, importedName))
}

for _, inherit := range node.Inherits {
inheritedName := inherit.BaseName.Name
mermaidGraph.WriteString(fmt.Sprintf(" %s -->|inherits| %s\n", nodeName, inheritedName))
}
}

return mermaidGraph.String()
}
58 changes: 58 additions & 0 deletions cfg/builder_printer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package cfg

import (
"fmt"
"strings"
)

// Print displays information about a specific contract or the entry contract in the graph.
// If the contractName is provided, it prints details of the specified contract.
// If the contractName is empty, it finds and prints details of the entry contract in the graph.
//
// The function prints the contract's name, its status as an entry contract, and recursively
// prints information about its imports and inherited contracts, if any.
func (b *Builder) Print(contractName string) {
if contractName == "" {
for _, node := range b.graph.Nodes {
if node.EntryContract {
b.printNode(node.Name, 0)
return
}
}
} else {
b.printNode(contractName, 0)
}
}

// printNode is a helper function used by Print to recursively print details of a contract and its dependencies.
// It prints the contract's name, its status as an entry contract, and details of its imports and inherited contracts.
// The depth parameter is used for indentation purposes to represent the level of recursion and relationship in the graph.
func (b *Builder) printNode(name string, depth int) {
var output strings.Builder

node, exists := b.graph.Nodes[name]
if !exists {
output.WriteString(fmt.Sprintf("%sContract %s not found in the graph.\n", strings.Repeat(" ", depth), name))
fmt.Print(output.String())
return
}

indent := strings.Repeat(" ", depth)
output.WriteString(fmt.Sprintf("%sNode %s:\n", indent, node.Name))
output.WriteString(fmt.Sprintf("%s Entry Contract: %v\n", indent, node.EntryContract))

if len(node.Imports) == 0 && len(node.Inherits) == 0 {
output.WriteString(fmt.Sprintf("%s No imports or base contracts\n", indent))
} else {
for _, imp := range node.Imports {
output.WriteString(fmt.Sprintf("%s Imports: %s (ID: %d, File: %s)\n", indent, imp.GetAbsolutePath(), imp.GetId(), imp.GetFile()))
}
for _, inherit := range node.Inherits {
inheritedContractName := inherit.BaseName.Name
output.WriteString(fmt.Sprintf("%s Inherits: %s\n", indent, inheritedContractName))
b.printNode(inheritedContractName, depth+1)
}
}

fmt.Print(output.String())
}
Loading