Building tools to automatically detect issues in Solidity code. Compatible with all existing EVMs
Table of Content
I want to show you how source code analyzers works with a simple example. Let’s build an analyzer that will check if Solidity source files contains a floating pragma declaration or not. There are some steps we need to follow, such as finding or building a good Solidity grammar file, parse the input content, build a parse tree, process the tree, and finally, find issues. All the process is explained below.
Solidity Language Grammar definition
I will be using Solidity provided ANTLR grammar file, but you can use any other grammar file, like this one.
NOTE: it is required to do following replacements in autogenerated grammar go code:
replace: type=typeName with varType=typeName
replace: String with StringLiteral
Adding test data
To include some test data to evaluate the detector, we need to define some Solidity code examples. The easiest way is to download some opensource solidity project from Github. In this case, I choose to use code snippets from
https://solidity-by-example.org/first-app/. Our test example will be:
// SPDX-License-Identifier: MIT
pragma solidity^0.8.13;contractCounter{uintpubliccount;// Function to get the current count
functionget()publicviewreturns(uint){returncount;}// Function to increment count by 1
functioninc()public{count+=1;}// Function to decrement count by 1
functiondec()public{// This function will fail if count = 0
count-=1;}}
We copy the content to a local first-app.sol file and store the content in our project ./testdata dir.
Building our Test before the implementation.
This is something known as TDD or Test Driven Development, in where one of the foundations is to build your code based on test collection data. In this scenarios, some test are required to be designed first, and then, the code is developed so they are all passed successfully.
In my case, I write the following test and basic empty function CheckVersion that will hold all the complexity.
If we run the test with go test, it should fail since we don’t have any valid code yet.
1
2
3
4
5
6
7
8
9
--- FAIL: TestDetector (0.00s) --- FAIL: TestDetector/first-app-example (0.00s) detector_test.go:19:
Error Trace: /home/r00t/go/src/github.com/zerjioang/solidity-version-check/detector_test.go:19
Error: Should be true Test: TestDetector/first-app-example
FAIL
exit status 1FAIL github.com/zerjioang/solidity-version-check 0.010s
Building our result data model
After the execution of the algorithm, the function CheckVersion should return some information about detection process. That information will be handled by struct VersionStatus defined as
Now that we have already defined the function input and output parameters as
1
funcCheckVersion(code[]byte)(VersionStatus,error)
is time to build the body. According to ANTLR documentation and some visited blogs out there like
GopherAcademy, the basic steps to include are:
Read input file content
Build the lexer
Build the token stream for lexer data
Build a parser for token stream data
Build a event listener for the parser
Walk the parser tree
Previous steps, in code, are:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
funcCheckVersion(code[]byte)(VersionStatus,error){varvVersionStatus// Setup the input
is:=antlr.NewInputStream(string(code))// Create the Lexer
lexer:=solidity.NewSolidityLexer(is)stream:=antlr.NewCommonTokenStream(lexer,antlr.TokenDefaultChannel)// Create the Parser
p:=solidity.NewSolidityParser(stream)// Finally parse the expression
varlistenersolidity.CustomSolidityListenerantlr.ParseTreeWalkerDefault.Walk(&listener,p.SourceUnit())returnv,nil}
So, at this point the function CheckVersion has support to read and parse the input data according to Solidity provided grammar and walk the parse tree. But still, this is not enough to pass the test
1
2
3
4
5
6
detector_test.go:19:
Error Trace: /home/r00t/go/src/github.com/zerjioang/solidity-version-check/detector_test.go:19
Error: Should be true Test: TestDetector/first-app-example
--- FAIL: TestDetector (0.01s)--- FAIL: TestDetector/first-app-example (0.01s)
This is where all our logic needs to be implemented. We need to find the right spot in the listener to implement this feature so that the algorithm is able to implement a detection mechanism and trigger some alarms. This steps requires to review and understand the grammar file. After some digging, we found that best point for our detection is this rule: the pragmaDirective.
For this purpose, we implement a custom event logic in the pragmaDirective rule.
1
2
3
4
5
6
// EnterPragmaDirective is called when production pragmaDirective is entered.
func(s*CustomSolidityListener)EnterPragmaDirective(ctx*PragmaDirectiveContext){// 1 read the content of the pragma
// 2 check if its unlocked
// 3 trigger an alert
}
Depending on the information we need to read, we need to call one method or another. For example:
ctx.GetText(): returns pragma solidity ^0.8.13;
ctx.PragmaToken(0): returns the first child of type PragmaToken
So with these tips in mind, you can now build your own simple if-else conditional to trigger an alert when ^solidity is found in the pragma declaration.
Unlocked compiler version alert reporting
After implementing the alert detection for unlocked pragmas, we can now report to the user. I choose to report via stdout as follows, but you can choose whatever method you want, for example: encoding result as JSON and exposing it to an API, sending an automated email notification, telegram message, etc.
1
2
3
4
5
6
Unlocked Compiler Version Detected
----------------------------------
Affected line (L2) : pragma solidity ^0.8.13;Suggested fix : pragma solidity 0.8.13;Confidence : Very High
Impact : Informational
As you see, I also added a fix suggestion for the detected alert, which can help newcomers to solve the issue rapidly. Finally, we need to run the test again to see if it pass.
1
2
3
4
5
6
7
8
9
Unlocked Compiler Version Detected
----------------------------------
Affected line (L2) : pragma solidity ^0.8.13;Suggested fix : pragma solidity 0.8.13;Confidence : Very High
Impact : Informational
PASS
ok github.com/zerjioang/solidity-version-check 0.026s
And as always, the process needs to be fast. In this case, only 0.026 seconds were required for whole process.
Conclusion
I introduced you an easy workflow to start detecting issues in any programming language by just inspecing the source code structure by means of a parse tree evaluation. Obviously, this educational example has low complexity but in the same way, more complex detectors or source code analyzers can be built to trigger alarms on more complex bugs.
Thanks for checking this out and I hope you found the info useful! If you have any questions, don't hesitate to write me a comment below. And remember that if you like to see more content on, just let me know it and share this post with your colleges, co-workers, FFF, etc.