Skip to content

Commit

Permalink
Add files via upload
Browse files Browse the repository at this point in the history
  • Loading branch information
Dharma-09 authored Mar 17, 2024
1 parent d349b2e commit 15db388
Show file tree
Hide file tree
Showing 5 changed files with 482 additions and 0 deletions.
196 changes: 196 additions & 0 deletions analyze.ts
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;


150 changes: 150 additions & 0 deletions compile.ts
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;
17 changes: 17 additions & 0 deletions index.ts
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);
Loading

0 comments on commit 15db388

Please sign in to comment.