-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
482 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
import { InputType, Instance, Issue } from './types'; | ||
import { lineFromIndex } from './utils'; | ||
import { } from 'readline'; | ||
|
||
const issueTypesTitles = { | ||
H: 'High Issues', | ||
M: 'Medium Issues', | ||
L: 'Low Issues', | ||
NC: 'Non Critical Issues', | ||
GAS: 'Gas Optimizations', | ||
}; | ||
|
||
/*** | ||
* @notice Runs the given issues on files and generate the report markdown string | ||
* @param githubLink optional url to generate links | ||
*/ | ||
|
||
const analyze = (files: InputType, issues: Issue[], githubLink?: string): string => { | ||
let result = ''; | ||
let analyze: { issue: Issue; instances: Instance[] }[] = []; | ||
for (const issue of issues) { | ||
let instances: Instance[] = []; | ||
// If issue is a regex | ||
if (issue.regexOrAST === 'Regex') { | ||
for (const file of files) { | ||
const matches: any = [...file.content.matchAll(issue.regex)]; | ||
if (!!issue.regexPreCondition) { | ||
const preConditionMatches: any = [...file.content.matchAll(issue.regexPreCondition)]; | ||
if (preConditionMatches.length == 0) continue; | ||
} | ||
for (const res of matches) { | ||
// Filter lines that are comments | ||
const line = [...res.input?.slice(0, res.index).matchAll(/\n/g)!].length; | ||
const comments = [...res.input?.split('\n')[line].matchAll(/([ \t]*\/\/|[ \t]*\/\*|[ \t]*\*)/g)]; | ||
if (comments.length === 0 || comments?.[0]?.index !== 0) { | ||
let line = lineFromIndex(res.input, res.index); | ||
let endLine = undefined; | ||
if (!!issue.startLineModifier) line += issue.startLineModifier; | ||
if (!!issue.endLineModifier) endLine = line + issue.endLineModifier; | ||
instances.push({ fileName: file.name, line, endLine, fileContent: res.input! }); | ||
} | ||
} | ||
} | ||
} else { | ||
instances = issue.detector(files); | ||
} | ||
if (instances.length > 0) { | ||
analyze.push({ issue, instances }); | ||
} | ||
} | ||
|
||
/** Summary */ | ||
|
||
let totalGasSavingsAcrossAllInstances = 0; | ||
let c = 0; | ||
let summaryTable = ''; | ||
let totalIssues = 0; | ||
let totalInstances = 0; | ||
|
||
if (analyze.length > 0) { | ||
summaryTable += `\n## ${issueTypesTitles[analyze[0].issue.type]}\n\n`; | ||
|
||
// Determine whether to include the "Total Gas Savings" column | ||
const includeGasSavingsColumn = analyze.some(({ issue }) => issue.type === 'GAS' && typeof issue.gasCost === 'number'); | ||
|
||
if (includeGasSavingsColumn) { | ||
summaryTable += '\n| |Issue|Instances|Total Gas Savings|\n|-|:-|:-:|:-:|\n'; | ||
} else { | ||
summaryTable += '\n| |Issue|Instances|\n|-|:-|:-:|\n'; | ||
} | ||
|
||
for (const { issue, instances } of analyze) { | ||
c++; | ||
let totalGasSavings = ''; | ||
|
||
if (issue.type === 'GAS' && typeof issue.gasCost === 'number') { | ||
const baseGas = issue.gasCost; | ||
const numInstances = instances.length; | ||
totalGasSavings = calculateTotalGas(issue.gasCost, instances.length).toString(); | ||
totalGasSavingsAcrossAllInstances += parseFloat(totalGasSavings); | ||
} | ||
|
||
if (includeGasSavingsColumn) { | ||
summaryTable += `| [${issue.type}-${c}](#${issue.type}-${c}) | ${issue.title} | ${instances.length} | ${issue.type === 'GAS' ? totalGasSavings : "-"} |\n`; | ||
} else { | ||
summaryTable += `| [${issue.type}-${c}](#${issue.type}-${c}) | ${issue.title} | ${instances.length} | - |\n`; | ||
} | ||
|
||
// Update total issues and instances | ||
totalIssues++; | ||
totalInstances += instances.length; | ||
} | ||
|
||
// Display the total gas savings only when issue type is 'GAS' | ||
if (totalGasSavingsAcrossAllInstances > 0) { | ||
summaryTable += `\n\n### Total Gas Savings for GAS Issues: ${totalGasSavingsAcrossAllInstances} gas\n\n`; | ||
} | ||
|
||
// Add the total line | ||
summaryTable += `\n\nTotal: ${totalInstances} instances over ${totalIssues} issues\n`; | ||
result += summaryTable; | ||
} | ||
|
||
/** Issue breakdown */ | ||
c = 0; | ||
for (const { issue, instances } of analyze) { | ||
c++; | ||
result += `\n ### <a name="${issue.type}-${c}"></a>[${issue.type}-${c}] ${issue.title}\n`; | ||
if (!!issue.description) { | ||
result += `${issue.description}\n`; | ||
} | ||
if (!!issue.impact) { | ||
result += '\n#### Impact:\n'; | ||
result += `${issue.impact}\n`; | ||
} | ||
|
||
// Check the number of instances | ||
const useToggleList = instances.length > 7; | ||
|
||
//write all finding into toggle list | ||
if (useToggleList) { | ||
result += `\n<details>\n`; | ||
result += `<summary>There are ${instances.length} instances of this issue:</summary>\n\n`; | ||
result += `---\n\n`; | ||
} else { | ||
result += `\n\nInstances of this issue:\n\n`; | ||
} | ||
|
||
|
||
let previousFileName = ''; | ||
let line = -1; | ||
let generatedLink = false; | ||
const instanceLinks = []; | ||
for (const o of instances.sort((a, b) => { | ||
if (a.fileName < b.fileName) return -1; | ||
if (a.fileName > b.fileName) return 1; | ||
return !!a.line && !!b.line && a.line < b.line ? -1 : 1; | ||
})) { | ||
if (o.fileName !== previousFileName) { | ||
if (previousFileName !== '') { | ||
result += `\n${'```'}\n`; | ||
// if (!!githubLink && line > 0) { | ||
// result += `[[${o.line}]](${generateGitHubLink(githubLink, o.fileName, o.line)})\n`; | ||
// generatedLink = false; | ||
// } | ||
result += `\n`; | ||
} | ||
result += `${'```'}solidity\nFile: ${o.fileName}\n`; | ||
previousFileName = o.fileName; | ||
|
||
} | ||
|
||
line = o.line || -1; | ||
// Insert code snippet | ||
const lineSplit = o.fileContent?.split('\n'); | ||
const offset = o.line.toString().length; | ||
result += `\n${o.line}: ${lineSplit[o.line - 1]}\n`; | ||
if (!!o.endLine) { | ||
let currentLine = o.line + 1; | ||
while (currentLine <= o.endLine) { | ||
result += `${' '.repeat(offset)} ${lineSplit[currentLine - 1]}\n`; | ||
currentLine++; | ||
} | ||
} | ||
if (!generatedLink && !!githubLink && line > 0) { | ||
|
||
const instanceLink = `[[${line}]](${generateGitHubLink(githubLink, previousFileName, line)})\n`; | ||
instanceLinks.push(instanceLink); // Store the instance link in the array | ||
} | ||
} | ||
result += `\n${'```'}\n`; | ||
for (const instanceLink of instanceLinks) { | ||
result += instanceLink + ','; | ||
} | ||
if (useToggleList) { | ||
result += `\n</details>\n\n`; | ||
} | ||
} | ||
|
||
//Github Link | ||
function generateGitHubLink(githubLink: string, previousFileName: string, line: number) { | ||
// Assuming baseUrl ends with a '/' | ||
return `${githubLink}${previousFileName}#L${line}`; | ||
} | ||
// total gas | ||
function calculateTotalGas(baseGas: number, numInstances: number) { | ||
return baseGas * numInstances; | ||
} | ||
return result; | ||
}; | ||
|
||
|
||
|
||
export default analyze; | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
import * as fs from 'fs'; | ||
import * as path from 'path'; | ||
import * as semver from 'semver'; | ||
import type { SourceUnit } from 'solidity-ast'; | ||
|
||
const versions = Object.keys(require('../package.json').dependencies) | ||
.filter(s => s.startsWith('solc-')) | ||
.map(s => s.replace('solc-', '')) | ||
.sort(semver.compare) | ||
.reverse(); | ||
|
||
type ToCompile = { [file: string]: { content: string } }; | ||
type Sources = { file: string; index: number; content: string; version: string; compiled: boolean; ast?: SourceUnit }[]; | ||
|
||
/*** | ||
* @notice Compiles `toCompile` with solc | ||
* @param toCompile source files with content already loaded | ||
*/ | ||
const compile = async (version: string, toCompile: ToCompile, basePath: string) => { | ||
const solc = require(`solc-${version}`); | ||
|
||
// version() returns something like '0.8.13+commit.abaa5c0e.Emscripten.clang' | ||
const [trueVersion] = solc.version().split('+'); | ||
|
||
let output; | ||
if (trueVersion !== version) { | ||
output = { | ||
errors: [{ formattedMessage: `Package solc-${version} is actually solc@${trueVersion}` }], | ||
}; | ||
} else { | ||
output = JSON.parse( | ||
solc.compile( | ||
JSON.stringify({ | ||
sources: toCompile, | ||
language: 'Solidity', | ||
settings: { | ||
outputSelection: { '*': { '': ['ast'] } }, | ||
}, | ||
}), | ||
{ import: findImports(basePath) }, | ||
), | ||
); | ||
} | ||
|
||
return output; | ||
}; | ||
|
||
/*** | ||
* @notice Reads and load an import file | ||
*/ | ||
const findImports = (basePath: string) => { | ||
const res = (relativePath: string) => { | ||
const depth = 5; | ||
let prefix = ''; | ||
for (let i = 0; i < depth; i++) { | ||
/** 1 - import are stored in `node_modules` */ | ||
try { | ||
const absolutePath = path.resolve(basePath, prefix, 'node_modules/', relativePath); | ||
const source = fs.readFileSync(absolutePath, 'utf8'); | ||
return { contents: source }; | ||
} catch {} | ||
|
||
/** 2 - import are stored in `lib` | ||
* In this case you need to check eventual remappings | ||
*/ | ||
try { | ||
const remappings = fs.readFileSync(path.resolve(basePath, prefix, 'remappings.txt'), 'utf8'); | ||
for (const line of remappings.split('\n')) { | ||
if (!!line.split('=')[0] && !!line.split('=')[1]) { | ||
relativePath = relativePath.replace(line.split('=')[0], line.split('=')[1]); | ||
} | ||
} | ||
|
||
const absolutePath = path.resolve(basePath, relativePath); | ||
const source = fs.readFileSync(absolutePath, 'utf8'); | ||
return { contents: source }; | ||
} catch {} | ||
|
||
/** 3 - import are stored relatively */ | ||
try { | ||
const absolutePath = path.resolve(basePath, prefix, relativePath); | ||
const source = fs.readFileSync(absolutePath, 'utf8'); | ||
return { contents: source }; | ||
} catch {} | ||
|
||
prefix += '../'; | ||
} | ||
|
||
console.error( | ||
`${relativePath} import not found\n\nMake sure you can compile the contracts in the original repository.\n`, | ||
); | ||
}; | ||
return res; | ||
}; | ||
|
||
const compileAndBuildAST = async (basePath: string, fileNames: string[]): Promise<SourceUnit[]> => { | ||
let sources: Sources = []; | ||
|
||
/** Read scope and fill file list */ | ||
let i = 0; | ||
for (const file of fileNames) { | ||
const content = await fs.readFileSync(path.join(basePath, file), { encoding: 'utf8', flag: 'r' }); | ||
if (!!content) { | ||
if (!content.match(/pragma solidity (.*);/)) { | ||
console.log(`Cannot find pragma in ${path.join(basePath, file)}`); | ||
} else { | ||
sources.push({ | ||
file: path.join(basePath, file), | ||
index: i++, // Used to know when a file is compiled | ||
content, | ||
version: content.match(/pragma solidity (.*);/)![1], | ||
compiled: false, | ||
}); | ||
} | ||
} | ||
} | ||
|
||
const promises: Promise<void>[] = []; | ||
for (const version of versions) { | ||
const filteredSources = sources.filter(f => semver.satisfies(version, f.version) && !f.compiled); | ||
// Mark the filteredSources as being sent to compilation | ||
for (const f of filteredSources) { | ||
sources[f.index].compiled = true; | ||
} | ||
|
||
if (filteredSources.length > 0) { | ||
promises.push( | ||
compile( | ||
version, | ||
filteredSources.reduce((res: ToCompile, curr) => { | ||
res[curr.file] = { content: curr.content }; | ||
return res; | ||
}, {}), | ||
basePath, | ||
).then(output => { | ||
for (const f of filteredSources) { | ||
if (!output.sources[f.file]?.ast) { | ||
console.log(`Cannot compile AST for ${f.file}`); | ||
} | ||
sources[f.index].ast = output.sources[f.file]?.ast; | ||
} | ||
}), | ||
); | ||
} | ||
} | ||
await Promise.all(promises); | ||
return sources.map(f => f.ast!); | ||
}; | ||
|
||
export default compileAndBuildAST; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import main from './main'; | ||
|
||
/* | ||
Arjun | ||
*/ | ||
|
||
// ================================= PARAMETERS ================================ | ||
|
||
const basePath = | ||
process.argv.length > 2 ? (process.argv[2].endsWith('/') ? process.argv[2] : process.argv[2] + '/') : 'contracts/'; | ||
const scopeFile = process.argv.length > 3 && process.argv[3].endsWith('txt') ? process.argv[3] : 'scope.txt'; | ||
const githubLink = process.argv.length > 4 && process.argv[4] ? process.argv[4] :null; | ||
const out = 'report.md' | ||
|
||
// ============================== GENERATE REPORT ============================== | ||
|
||
main(basePath, scopeFile, githubLink, out); |
Oops, something went wrong.