solidity-audit
Version:
Solidity Audit Code
1,302 lines (1,116 loc) • 45.2 kB
JavaScript
;
/**
* @author github.com/tintinweb
* @license MIT
*
*
* */
const parser = require("@solidity-parser/parser");
const parserHelpers = require("./parserHelpers");
const { scores, tshirtSizes } = require("./constants");
const { capitalFirst } = require("./helper");
const fs = require("fs");
const crypto = require("crypto");
const sloc = require("sloc");
const axios = require('axios');
const surya = require("surya");
const { SolidityDoppelganger } = require("solidity-doppelganger");
// we initialize the "immutable" variable in the global scope and only instantiate if there is an argument
let doppelGanger;
//read report.json
const fetcher = (url) => fetch(url).then((res) => res.json());
class SolidityMetricsContainer {
constructor(name, args) {
this.name = name;
this.id = 0;
this.basePath = args.basePath || "";
this.basePathRegex = new RegExp(this.basePath, "g");
this.inputFileGlobExclusions = args.inputFileGlobExclusions || "";
this.inputFileGlob = args.inputFileGlob || "";
this.inputFileGlobLimit = args.inputFileGlobLimit;
this.excludeFileGlobLimit = args.inputFileGlobLimit;
this.debug = args.debug;
this.repoInfo = args.repoInfo;
doppelGanger =
args.disableDoppelGanger === true
? undefined
: new SolidityDoppelganger(); //load this only once
this.seenFiles = [];
this.seenDuplicates = [];
this.seenHashes = [];
this.metrics = [];
this.errors = [];
this.truffleProjectLocations = [];
this.excludedFiles = [];
}
changeName(paramName) {
this.name = paramName;
}
addRequestId(id) {
this.id = id;
}
addResultJson(json) {
this.resultJson = json;
}
addTruffleProjectLocation(truffleJsPath) {
this.truffleProjectLocations = Array.from(
new Set([truffleJsPath, ...this.truffleProjectLocations])
);
}
addExcludedFile(exfile) {
this.excludedFiles = Array.from(new Set([exfile, ...this.excludedFiles]));
}
analyze(inputFileGlobs) {
return this.analyzeFile(inputFileGlobs);
}
analyzeFile(filepath) {
let content = fs.readFileSync(filepath).toString("utf-8");
let hash = crypto.createHash("sha1").update(content).digest("base64");
try {
var metrics = new SolidityFileMetrics(filepath, content);
this.seenFiles.push(filepath);
if (this.seenHashes.indexOf(hash) >= 0) {
//DUP
this.seenDuplicates.push(filepath);
} else {
//NEW
this.seenHashes.push(hash);
}
this.metrics.push(metrics);
} catch (e) {
console.error(e);
this.errors.push(filepath);
if (e instanceof parser.ParserError) {
console.log(e.errors);
}
return;
}
}
getDotGraphs() {
let ret = {
"#surya-inheritance": "",
"#surya-callgraph": "",
};
try {
ret["#surya-inheritance"] = surya.inheritance(this.seenFiles, {
draggable: false,
}); //key must match the div-id in the markdown template!
} catch (e) {
console.error(e);
}
try {
ret["#surya-callgraph"] = surya.graph(this.seenFiles);
} catch (e) {
console.error(e);
}
return ret;
}
totals() {
let total = {
totals: new Metric(),
avg: new Metric(),
num: {
sourceUnits: this.seenFiles.length,
metrics: this.metrics.length,
duplicates: this.seenDuplicates.length,
errors: this.errors.length,
},
};
total.totals = total.totals.sumCreateNewMetric(...this.metrics);
total.totals.sloc.commentToSourceRatio =
total.totals.sloc.comment / total.totals.sloc.source;
total.avg = total.avg.sumAvgCreateNewMetric(...this.metrics);
total.totals.nsloc.commentToSourceRatio =
total.totals.nsloc.comment / total.totals.nsloc.source;
return total;
}
/**
* @note: div id must match the SolidityMetricsContainer.getDotGraphs() object key which is also the div-id!
*/
async generateReportMarkdown() {
let that = this;
let totals = this.totals();
let response = await axios({
method: "GET",
url:
"http://192.168.11.143:8888/request/get",
params: {
id: this.id
}
});
let requestData = response.data.data;
this.name = requestData.company_name;
const date = new Date();
//ugly hacks ahead! :/
let reports = require(this.resultJson);
let detectors = reports.results?.detectors ?? [];
async function mergeDoppelganger(doppelgangers) {
let resolved = (await Promise.all(doppelgangers)).filter(
(r) => Object.keys(r.results).length
);
if (resolved.length == 0) {
return;
}
let first = resolved.shift();
for (let r of resolved) {
for (let x of Object.values(r.results)) {
first.addResult(x.target, x.matches);
}
}
return first;
//otherwise merge all results
}
function formatDoppelgangerSection(astHashCompareResults) {
if (
!astHashCompareResults ||
Object.keys(astHashCompareResults.results).length == 0
) {
return "";
}
let lines = [];
for (let result of Object.values(astHashCompareResults.results)) {
//line |file|contract|doppelganger|
//Deduplicate paths
let exact = [];
let fuzzy = [];
for (let m of result.matches) {
if (m.options.mode.includes("EXACT")) {
exact.push(m.path);
} else {
fuzzy.push(m.path);
}
}
let matchText = "";
if (exact.length) {
matchText =
"(exact) " + exact.map((path, i) => `[${i}](${path})`).join(", "); //exact
} else if (fuzzy.length) {
matchText =
"(fuzzy) " +
fuzzy
.filter((f) => !exact.includes(f))
.map((path, i) => `[${i}](${path})`)
.join(", "); //fuzzy-exact
}
lines.push(
`| ${
result.target.path
? result.target.path.replace(that.basePath, "")
: ""
} | ${result.target.name} | ${matchText} |`
);
}
return lines.join("\n");
}
let doppelganger = await mergeDoppelganger(
totals.totals.other.doppelganger
);
let pathToDoppelganger = {};
if (doppelganger) {
for (let r of Object.values(doppelganger.results)) {
let rpath = r.target.path.replace(this.basePath, "") || "";
if (typeof pathToDoppelganger[rpath] === "undefined") {
pathToDoppelganger[rpath] = [];
}
pathToDoppelganger[rpath].push(r);
}
}
let suryamdreport;
try {
suryamdreport = surya
.mdreport(this.seenFiles)
.replace("### ", "#####")
.replace("## Sūrya's Description Report", "")
.replace(this.basePathRegex, ""); /* remove surya title, fix layout */
} catch (error) {
suryamdreport = `\`\`\`
${error}
\`\`\``;
console.error(error);
}
// We are now defining the doppelganger related sections to be included in the Markdown only if doppelGanger was instantiated
const doppelgangerToC =
doppelGanger !== undefined
? `
- [Doppelganger Contracts](#t-out-of-scope-doppelganger-contracts)`
: "";
const doppelgangerSection =
doppelGanger !== undefined
? `##### <span id=t-out-of-scope-doppelganger-contracts>Doppelganger Contracts</span>
Doppelganger Contracts: **\`${
doppelganger && doppelganger.results
? Object.keys(doppelganger.results).length
: 0
}\`**
<div id="doppelganger-contracts" style="display:block">
| File | Contract | Doppelganger |
| ------ | -------- | ------------ |
${formatDoppelgangerSection(doppelganger)}`
: "";
let home_page = `
<img width="100%" height="100%" alt="Background Header" src="./public/images/bgr-header.png" id="bgr-header")
<br/>
<img alt="Logo" src="./public/images/Logo.svg" height="48px" id="ic-logo")
<br/>
<p id="p-security">Security Assessment</p>
<h2 id="h2-deFi">${this.name} - Audit</h2>
---------------------
<p id="verified-date">TechRight Verified on ${date.getMonth()}/${date.getDay()}/${date.getFullYear()}</p>
<img alt="QRCode" src="./public/images/ic-qr.png" height="98px" width="98px" id="ic-qrCode")
<br/>
`;
let footer2 = `
<img width="100%" height="100%" alt="Background Header" src="./public/images/bgr-header.png" id="bgr-header")
<br/>
<img alt="Logo" src="./public/images/Logo.svg" height="48px" id="ic-logo")
<br/>
<p id="p-security">Security Assessment</p>
<h2 id="h2-deFi">DeFi Exchange - Audit</h2>
---------------------
<p id="verified-date">TechRight Verified on Mar 18th, 2023</p>
<img alt="QRCode" src="./public/images/ic-qr.png" height="98px" width="98px" id="ic-qrCode")
<br/>
`;
let footer_page = `
<img width="100%" height="100%" alt="Background Header" src="./public/images/bgr-footer.png")
<br/>
`;
let mdreport_head = `
## Table of contents
- [Disclaimer](#t-disclaimer)
- [Description](#t-description)
- [Vulnerability & Risk Level](#t-vulnerability)
- [Auditing Strategy and Techniques](#t-auditing-strategy)
- [Tested Contract Files](#t-tested-contract)
- [Scope](#t-scope)
- [Source Units in Scope](#t-source-Units-in-Scope)
- [Out of Scope](#t-out-of-scope)
- [Excluded Source Units](#t-out-of-scope-excluded-source-units)
- [Duplicate Source Units](#t-out-of-scope-duplicate-source-units)${doppelgangerToC}
- [Report Overview](#t-report)
- [Risk Summary](#t-risk)
- [Source Lines](#t-source-lines)
- [Inline Documentation](#t-inline-documentation)
- [Components](#t-components)
- [Exposed Functions](#t-exposed-functions)
- [StateVariables](#t-statevariables)
- [Capabilities](#t-capabilities)
- [Dependencies](#t-package-imports)
- [Totals](#t-totals)
- [Detectors Issue](#t-detectors)
<br/>
<br/>
## <span id=t-disclaimer>Disclaimer</span>
<p class="content-disclaimer">TechRight.io Reports do not constitute an endorsement or disapproval of any specific project or team, and they should not be taken as an indication of the economic value of any product or asset created by a team. Additionally, TechRight.io does not perform testing or auditing of integration with external contracts or services like Unicrypt, Uniswap, PancakeSwap, and others.
</p>
<p class="content-disclaimer">TechRight.io Audits do not offer any assurance or pledge about the complete absence of bugs in the evaluated technology, and they do not give any hint about the owners of the technology. These audits should not be relied upon to make any investment or participation decisions in any specific project, nor should they be used as any form of investment advice.
</p>
<p class="content-disclaimer">TechRight.io Reports involve a comprehensive auditing process to support our clients in enhancing their code quality while reducing the risk associated with blockchain technology and cryptographic assets. Please note that every company and individual is responsible for conducting their own due diligence and maintaining continuous security. Please note that TechRight does not guarantee the security or functionality of the technology we confirm to evaluate.
</p>
<br/>
<br/>
<br/>
<br/>
## <span id=t-description>Description</span>
<p class="p-description">Network</p>
<p class="p-description">Binance Smart Chain (BEP20)</p>
<br/>
<br/>
## <span id=t-vulnerability>Vulnerability & Risk Level</span>
<p>Risk represents the probability that a certain source-threat will exploit vulnerability, and the impact of that event on the organization or system. Risk Level is computed based on CVSS version 3.0
</p>
| Level | Value | Vulnerability | Risk<br/>(Required Action) |
| ------------- | ----------- | ------------ | ---------- |
| Critical | 9 - 10 | A vulnerability that can disrupt the contract functioning in a number of scenarios, or creates a risk that the contract may be broken. | Immediate action to reduce risk level. |
| High | 7 - 8.9 | A vulnerability that affects the desired outcome when using a contract, or provides the opportunity to use a contract in an unintended way. | Implementation of corrective actions as soon as possible. |
| Medium | 4 - 6.9 | A vulnerability that could affect the desired outcome of executing the contract in a specific scenario. | Implementation of corrective actions in a certain period. |
| Low | 2 - 3.9 | A vulnerability that does not have a significant impact on possible scenarios for the use of the contract and is probably subjective. | Implementation of certain corrective actions or accepting the risk. |
| Informational | 0 - 1.9 | A vulnerability that has informational character but is not affecting any of the code.|An observation that does not determine a level of risk |
<br/>
<br/>
## <span id=t-auditing-strategy>Auditing Strategy and Techniques Applied</span>
<p>During the evaluation process, the repository was thoroughly examined to identify any security-related concerns, assess code quality, and ensure adherence to specifications and best practices. Our team of expert pentesters and smart contract developers reviewed the code line-by-line and documented any issues identified.
</p>
<h2>Methodology</h2>
<p>The auditing process follows a step-by-step routine:
</p>
1. Code review that includes:<br/>
i. Review of the specifications, sources and instructions provided to TechRight to ensure a thorough understanding of the size, scope, and functionality of the smart contract's.
ii. Manual review of code, which involves carefully reading the source code line-by-line to identify potential vulnerabilities.
iii. Comparison to specification, which is the process of confirming whether the code performs as described in the specifications, sources, and instructions provided.
2. Testing and automated analysis that includes the following:<br/>
i. Test coverage analysis, which involves assessing the degree to which test cases cover the code and how much of the code is executed while running those test cases.
ii. Symbolic execution, which refers to the analysis of a program to identify the inputs that trigger each component of the program to execute.
3. Best practices review, which involves evaluating smart contracts to enhance efficiency, effectiveness, clarity, maintainability, security, and control in accordance with industry and academic practices, recommendations, and research.
4. Specific, itemized, actionable recommendations that enable you to take necessary measures to secure your smart contracts.
<br/>
<br/>
## <span id=t-tested-contract>Tested Contract Files</span>
<p>This audit covered the following files listed below with a SHA-1 Hash.
</p>
<p class="italic">A file with a different Hash has been modified, intentionally or otherwise, after the security review. A different Hash could be (but not necessarily) an indication of a changed condition or potential vulnerability that was not within the scope of this review
</p>
<br/>
<br/>
## <span id=t-scope>Scope</span>
This section lists files that are in scope for the metrics report.
- **Project:** \`${this.name}\`
- **Included Files:** ${
`\n` +
this.inputFileGlob
.replace("{", "")
.replace("}", "")
.split(",")
.map((g) => ` - \`${g}\``)
.join("\n")
}
- **Excluded Paths:** ${
`\n` +
this.inputFileGlobExclusions
.replace("{", "")
.replace("}", "")
.split(",")
.map((g) => ` - \`${g}\``)
.join("\n")
}
- **File Limit:** \`${this.inputFileGlobLimit}\`
- **Exclude File list Limit:** \`${this.excludeFileGlobLimit}\`
- **Workspace Repository:** \`${this.repoInfo.remote || "unknown"}\` (\`${
this.repoInfo.branch
}\`@\`${this.repoInfo.commit}\`)
### <span id=t-source-Units-in-Scope>Source Units in Scope</span>
Source Units Analyzed: **\`${this.seenFiles.length}\`**<br>
Source Units in Scope: **\`${this.metrics.length}\`** (**${Math.round(
(this.metrics.length / this.seenFiles.length) * 100
)}%**)
| Type | File | Logic Contracts | Interfaces | Lines | nLines | nSLOC | Comment Lines | Complex. Score | Capabilities |
| ---- | ------ | --------------- | ---------- | ----- | ------ | ----- | ------------- | -------------- | ------------ |
${this.metrics
.map(
(m) =>
`| ${m.metrics.num.contracts ? "📝" : ""}${
m.metrics.num.libraries ? "📚" : ""
}${m.metrics.num.interfaces ? "🔍" : ""}${
m.metrics.num.abstract ? "🎨" : ""
} | ${m.filename.replace(this.basePath, "")} | ${
m.metrics.num.contracts +
m.metrics.num.libraries +
m.metrics.num.abstract || "****"
} | ${m.metrics.num.interfaces || "****"} | ${
m.metrics.sloc.total || "****"
} | ${m.metrics.nsloc.total || "****"} | ${
m.metrics.nsloc.source || "****"
} | ${m.metrics.sloc.comment || "****"} | ${
m.metrics.complexity.perceivedNaiveScore || "****"
} | **${
m.metrics.capabilities.assembly
? "<abbr title='Uses Assembly'>🖥</abbr>"
: ""
}${
m.metrics.capabilities.experimental.length
? "<abbr title='Experimental Features'>🧪</abbr>"
: ""
}${
m.metrics.capabilities.canReceiveFunds
? "<abbr title='Payable Functions'>💰</abbr>"
: ""
}${
m.metrics.capabilities.destroyable
? "<abbr title='Destroyable Contract'>💣</abbr>"
: ""
}${
m.metrics.capabilities.explicitValueTransfer
? "<abbr title='Initiates ETH Value Transfer'>📤</abbr>"
: ""
}${
m.metrics.capabilities.lowLevelCall
? "<abbr title='Performs Low-Level Calls'>⚡</abbr>"
: ""
}${
m.metrics.capabilities.delegateCall
? "<abbr title='DelegateCall'>👥</abbr>"
: ""
}${
m.metrics.capabilities.hashFuncs
? "<abbr title='Uses Hash-Functions'>🧮</abbr>"
: ""
}${
m.metrics.capabilities.ecrecover
? "<abbr title='Handles Signatures: ecrecover'>🔖</abbr>"
: ""
}${
m.metrics.capabilities.deploysContract
? "<abbr title='create/create2'>🌀</abbr>"
: ""
}${
doppelGanger !== undefined &&
pathToDoppelganger &&
pathToDoppelganger[m.filename.replace(this.basePath, "")]
? "<abbr title='doppelganger(" +
pathToDoppelganger[m.filename.replace(this.basePath, "")]
.map((r) => r.target.name)
.join(", ") +
")'>🔆</abbr>"
: ""
}${
m.metrics.capabilities.tryCatchBlocks
? "<abbr title='TryCatch Blocks'>♻️</abbr>"
: ""
}${
m.metrics.capabilities.uncheckedBlocks
? "<abbr title='Unchecked Blocks'>Σ</abbr>"
: ""
}** |`
)
.join("\n")}
| ${totals.totals.num.contracts ? "📝" : ""}${
totals.totals.num.libraries ? "📚" : ""
}${totals.totals.num.interfaces ? "🔍" : ""}${
totals.totals.num.abstract ? "🎨" : ""
} | **Totals** | **${
totals.totals.num.contracts +
totals.totals.num.libraries +
totals.totals.num.abstract || ""
}** | **${totals.totals.num.interfaces || ""}** | **${
totals.totals.sloc.total
}** | **${totals.totals.nsloc.total}** | **${
totals.totals.nsloc.source
}** | **${totals.totals.sloc.comment}** | **${
totals.totals.complexity.perceivedNaiveScore
}** | **${
totals.totals.capabilities.assembly
? "<abbr title='Uses Assembly'>🖥</abbr>"
: ""
}${
totals.totals.capabilities.experimental.length
? "<abbr title='Experimental Features'>🧪</abbr>"
: ""
}${
totals.totals.capabilities.canReceiveFunds
? "<abbr title='Payable Functions'>💰</abbr>"
: ""
}${
totals.totals.capabilities.destroyable
? "<abbr title='Destroyable Contract'>💣</abbr>"
: ""
}${
totals.totals.capabilities.explicitValueTransfer
? "<abbr title='Initiates ETH Value Transfer'>📤</abbr>"
: ""
}${
totals.totals.capabilities.lowLevelCall
? "<abbr title='Performs Low-Level Calls'>⚡</abbr>"
: ""
}${
totals.totals.capabilities.delegateCall
? "<abbr title='DelegateCall'>👥</abbr>"
: ""
}${
totals.totals.capabilities.hashFuncs
? "<abbr title='Uses Hash-Functions'>🧮</abbr>"
: ""
}${
totals.totals.capabilities.ecrecover
? "<abbr title='Handles Signatures: ecrecover'>🔖</abbr>"
: ""
}${
totals.totals.capabilities.deploysContract
? "<abbr title='create/create2'>🌀</abbr>"
: ""
}${
doppelGanger !== undefined &&
pathToDoppelganger &&
Object.keys(pathToDoppelganger).length
? "<abbr title='doppelganger'>🔆</abbr>"
: ""
}${
totals.totals.capabilities.tryCatchBlocks
? "<abbr title='TryCatch Blocks'>♻️</abbr>"
: ""
}${
totals.totals.capabilities.uncheckedBlocks
? "<abbr title='Unchecked Blocks'>Σ</abbr>"
: ""
}** |
<sub>
Legend:
<div id="table-legend" style="display:block">
<ul>
<li> <b>Lines</b>: total lines of the source unit </li>
<li> <b>nLines</b>: normalized lines of the source unit (e.g. normalizes functions spanning multiple lines) </li>
<li> <b>nSLOC</b>: normalized source lines of code (only source-code lines; no comments, no blank lines) </li>
<li> <b>Comment Lines</b>: lines containing single or block comments </li>
<li> <b>Complexity Score</b>: a custom complexity score derived from code statements that are known to introduce code complexity (branches, loops, calls, external interfaces, ...) </li>
</ul>
</div>
</sub>
#### <span id=t-out-of-scope>Out of Scope</span>
##### <span id=t-out-of-scope-excluded-source-units>Excluded Source Units</span>
Source Units Excluded: **\`${this.excludedFiles.length}\`**
<div id="excluded-files" style="display:block">
| File |
| ------ |
${
this.excludedFiles.length
? this.excludedFiles
.map((f) => `|${f.replace(this.basePath, "")}|`)
.join("\n")
: "| None |"
}
</div>
##### <span id=t-out-of-scope-duplicate-source-units>Duplicate Source Units</span>
Duplicate Source Units Excluded: **\`${this.seenDuplicates.length}\`**
<div id="duplicate-files" style="display:block">
| File |
| ------ |
${
this.seenDuplicates.length
? this.seenDuplicates
.map((f) => `|${f.replace(this.basePath, "")}|`)
.join("\n")
: "| None |"
}
</div>
${doppelgangerSection}
</div>
## <span id=t-report>Report</span>
### Overview
The analysis finished with **\`${this.errors.length}\`** errors and **\`${
this.seenDuplicates.length
}\`** duplicate files.
${this.errors.length ? "**Errors:**\n\n" + this.errors.join("\n* ") : ""}
${
this.truffleProjectLocations.length
? "**Truffle Project Locations Observed:**\n* " +
this.truffleProjectLocations
.map((f) => "./" + f.replace(this.basePath, ""))
.join("\n* ")
: ""
}
#### <span id=t-risk>Risk</span>
<div class="wrapper" style="max-width: 512px; margin: auto">
<canvas id="chart-risk-summary"></canvas>
</div>
#### <span id=t-source-lines>Source Lines (sloc vs. nsloc)</span>
<div class="wrapper" style="max-width: 512px; margin: auto">
<canvas id="chart-nsloc-total"></canvas>
</div>
#### <span id=t-inline-documentation>Inline Documentation</span>
- **Comment-to-Source Ratio:** On average there are\`${
Math.round(
(totals.totals.sloc.source / totals.totals.sloc.comment) * 100
) / 100
}\` code lines per comment (lower=better).
- **ToDo's:** \`${totals.totals.sloc.todo}\`
#### <span id=t-components>Components</span>
| 📝Contracts | 📚Libraries | 🔍Interfaces | 🎨Abstract |
| ------------- | ----------- | ------------ | ---------- |
| ${totals.totals.num.contracts} | ${totals.totals.num.libraries} | ${
totals.totals.num.interfaces
} | ${totals.totals.num.abstract} |
#### <span id=t-exposed-functions>Exposed Functions</span>
This section lists functions that are explicitly declared public or payable. Please note that getter methods for public stateVars are not included.
| 🌐Public | 💰Payable |
| ---------- | --------- |
| ${totals.totals.num.functionsPublic} | ${
totals.totals.num.functionsPayable
} |
| External | Internal | Private | Pure | View |
| ---------- | -------- | ------- | ---- | ---- |
| ${totals.totals.ast["FunctionDefinition:External"] || 0} | ${
totals.totals.ast["FunctionDefinition:Internal"] || 0
} | ${totals.totals.ast["FunctionDefinition:Private"] || 0} | ${
totals.totals.ast["FunctionDefinition:Pure"] || 0
} | ${totals.totals.ast["FunctionDefinition:View"] || 0} |
#### <span id=t-statevariables>StateVariables</span>
| Total | 🌐Public |
| ---------- | --------- |
| ${totals.totals.num.stateVars} | ${totals.totals.num.stateVarsPublic} |
#### <span id=t-capabilities>Capabilities</span>
| Solidity Versions observed | 🧪 Experimental Features | 💰 Can Receive Funds | 🖥 Uses Assembly | 💣 Has Destroyable Contracts |
| -------------------------- | ------------------------ | -------------------- | ---------------- | ---------------------------- |
| ${totals.totals.capabilities.solidityVersions
.map((v) => `\`${v}\``)
.join("<br/>")} | ${totals.totals.capabilities.experimental
.map((v) => `\`${v}\``)
.join("<br/>")} | ${
totals.totals.capabilities.canReceiveFunds ? "`yes`" : "****"
} | ${
totals.totals.capabilities.assembly
? `\`yes\` <br/>(${totals.totals.num.assemblyBlocks} asm blocks)`
: "****"
} | ${totals.totals.capabilities.destroyable ? "`yes`" : "****"} |
| 📤 Transfers ETH | ⚡ Low-Level Calls | 👥 DelegateCall | 🧮 Uses Hash Functions | 🔖 ECRecover | 🌀 New/Create/Create2 |
| ---------------- | ----------------- | --------------- | ---------------------- | ------------ | --------------------- |
| ${totals.totals.capabilities.explicitValueTransfer ? "`yes`" : "****"} | ${
totals.totals.capabilities.lowLevelCall ? "`yes`" : "****"
} | ${totals.totals.capabilities.delegateCall ? "`yes`" : "****"} | ${
totals.totals.capabilities.hashFuncs ? "`yes`" : "****"
} | ${totals.totals.capabilities.ecrecover ? "`yes`" : "****"} | ${
totals.totals.capabilities.deploysContract ? "`yes`<br>" : "****"
}${Object.keys(totals.totals.ast)
.filter((k) =>
k.match(
/(NewContract:|AssemblyCall:Name:create|AssemblyCall:Name:create2)/g
)
)
.map((k) => `→ \`${k}\``)
.join("<br/>")} |
| ♻️ TryCatch | Σ Unchecked |
| ---------- | ----------- |
| ${totals.totals.capabilities.tryCatchBlocks ? "`yes`" : "****"} | ${
totals.totals.capabilities.uncheckedBlocks ? "`yes`" : "****"
} |
#### <span id=t-package-imports>Dependencies / External Imports</span>
| Dependency / Import Path | Count |
| ------------------------ | ------ |
${Object.keys(totals.totals.ast)
.filter((k) => k.startsWith("ImportDirective:Path:"))
.sort()
.map(
(ki) =>
`| ${ki.replace("ImportDirective:Path:", "")} | ${
totals.totals.ast[ki]
} |`
)
.join("\n")}
#### <span id=t-totals>Totals</span>
##### Summary
<div class="wrapper" style="max-width: 90%; margin: auto">
<canvas id="chart-num-bar"></canvas>
</div>
##### AST Node Statistics
###### Function Calls
<div class="wrapper" style="max-width: 90%; margin: auto">
<canvas id="chart-num-bar-ast-funccalls"></canvas>
</div>
###### Assembly Calls
<div class="wrapper" style="max-width: 90%; margin: auto">
<canvas id="chart-num-bar-ast-asmcalls"></canvas>
</div>
###### AST Total
<div class="wrapper" style="max-width: 90%; margin: auto">
<canvas id="chart-num-bar-ast"></canvas>
</div>
##### Inheritance Graph
<div id="surya-inherit" style="display:block">
<div class="wrapper" style="max-width: 512px; margin: auto">
<div id="surya-inheritance" style="text-align: center;"></div>
</div>
</div>
##### CallGraph
<div id="surya-call" style="display:block">
<div class="wrapper" style="max-width: 512px; margin: auto">
<div id="surya-callgraph" style="text-align: center;"></div>
</div>
</div>
###### Contract Summary
<div id="surya-mdreport" style="display:block">
${suryamdreport}
</div>
____
<sub>
Thinking about smart contract security? We can provide training, ongoing advice, and smart contract auditing. [Contact us](https://diligence.consensys.net/contact/).
</sub>
<br/>
<br/>
## <span id=t-detectors>Detectors Issue</span>
| Description | Check | Impact | Confidence |
| ------------- | ----------- | ------------ | ---------- |
${detectors
?.map(
(item) =>
`| ${item?.description?.replaceAll("\n", "<br/>")} | ${
item?.check ?? ""
} | ${item?.impact ?? ""} | ${item?.confidence ?? ""} |`
)
.join("\n")};
<br/>
<br/>
<br/>
`;
let debug_dump_totals = `
\`\`\`json
${JSON.stringify(totals, null, 2)}
\`\`\`
`;
let debug_dump_units = `
#### Source Units
\`\`\`json
${JSON.stringify(this.metrics, null, 2)}
\`\`\`
`;
if (this.debug) {
return mdreport_head + debug_dump_totals + debug_dump_units;
}
return home_page + mdreport_head + footer_page;
}
}
class Metric {
constructor() {
this.ast = {};
this.sloc = {};
this.nsloc = {};
this.complexity = {
cyclomatic: undefined,
perceivedNaiveScore: 0,
};
this.summary = {
perceivedComplexity: undefined,
size: undefined,
numLogicContracts: undefined,
numFiles: undefined,
inheritance: undefined,
callgraph: undefined,
cyclomatic: undefined,
interfaceRisk: undefined,
inlineDocumentation: undefined,
compilerFeatures: undefined,
compilerVersion: undefined,
};
this.num = {
astStatements: 0,
contractDefinitions: 0,
contracts: 0,
libraries: 0,
interfaces: 0,
abstract: 0,
imports: 0,
functionsPublic: 0,
functionsPayable: 0,
assemblyBlocks: 0,
stateVars: 0,
stateVarsPublic: 0,
};
this.capabilities = {
solidityVersions: [],
assembly: false,
experimental: [],
canReceiveFunds: false,
destroyable: false,
explicitValueTransfer: false,
lowLevelCall: false,
hashFuncs: false,
ecrecover: false,
deploysContract: false,
uncheckedBlocks: false,
tryCatchBlocks: false,
};
this.other = {
doppelganger: [],
};
}
update() {
// calculate naiveScore (perceived complexity)
this.complexity.perceivedNaiveScore = 0;
Object.keys(this.ast).map(function (value, index) {
this.complexity.perceivedNaiveScore +=
this.ast[value] * (scores[value] || 0);
}, this);
this.num.contractDefinitions = this.ast["ContractDefinition"] || 0;
this.num.contracts = this.ast["ContractDefinition:Contract"] || 0;
this.num.libraries = this.ast["ContractDefinition:Library"] || 0;
this.num.interfaces = this.ast["ContractDefinition:Interface"] || 0;
this.num.abstract = this.ast["ContractDefinition:Abstract"] || 0;
this.num.imports = this.ast["ImportDirective"] || 0;
this.num.functionsPublic =
(this.ast["FunctionDefinition:Public"] || 0) +
(this.ast["FunctionDefinition:External"] || 0);
this.num.functionsPayable = this.ast["FunctionDefinition:Payable"] || 0;
this.num.assemblyBlocks = this.ast["InlineAssemblyStatement"] || 0;
this.num.stateVars = this.ast["StateVariableDeclaration"] || 0;
this.num.stateVarsPublic = this.ast["StateVariableDeclaration:Public"] || 0;
// generate human readable ratings
this.summary.size = tshirtSizes.nsloc(this.nsloc.source);
this.summary.perceivedComplexity = tshirtSizes.perceivedComplexity(
this.complexity.perceivedNaiveScore
);
this.summary.numLogicContracts = tshirtSizes.files(
this.num.contracts + this.num.libraries + this.num.abstract
);
this.summary.interfaceRisk = tshirtSizes.files(
this.num.functionsPublic + this.num.functionsPayable
);
this.summary.inlineDocumentation = tshirtSizes.commentRatio(
this.nsloc.commentToSourceRatio
);
this.summary.compilerFeatures = tshirtSizes.experimentalFeatures(
this.capabilities.experimental
);
this.summary.compilerVersion = tshirtSizes.compilerVersion(
this.capabilities.solidityVersions
);
if (this.ast["SourceUnit"] > 1)
this.summary.numFiles = tshirtSizes.files(this.ast["SourceUnit"]);
//postprocess the ast
this.capabilities.assembly = Object.keys(this.ast).some(function (k) {
return ~k.toLowerCase().indexOf("assembly");
});
this.capabilities.canReceiveFunds =
!!this.ast["FunctionDefinition:Payable"];
this.capabilities.destroyable = !!(
this.ast["FunctionCall:Name:selfdestruct"] ||
this.ast["FunctionCall:Name:suicide"] ||
this.ast["AssemblyCall:Name:selfdestruct"] ||
this.ast["AssemblyCall:Name:suicide"]
);
this.capabilities.explicitValueTransfer =
this.ast["FunctionCall:Name:transfer"] ||
this.ast["FunctionCall:Name:send"] ||
Object.keys(this.ast)
.filter((k) => k.startsWith("FunctionCall:Name:"))
.some((k) => k.endsWith(".value")); //any value call
this.capabilities.lowLevelCall = Object.keys(this.ast).some((k) =>
k.match(
/(Function|Assembly)Call:Name:(delegatecall|callcode|staticcall|call)[\.$]/g
)
);
this.capabilities.delegateCall =
Object.keys(this.ast).some((k) =>
k.startsWith("FunctionCall:Name:delegatecall")
) || !!this.ast["AssemblyCall:Name:delegatecall"];
this.capabilities.hashFuncs = Object.keys(this.ast).some((k) =>
k.match(
/(Function|Assembly)Call:Name:(keccak256|sha3|sha256|ripemed160)/g
)
); //ignore addmod|mulmod
this.capabilities.ecrecover = !!this.ast["FunctionCall:Name:ecrecover"];
this.capabilities.deploysContract =
Object.keys(this.ast).some((k) => k.startsWith("NewContract:")) ||
!!(
this.ast["AssemblyCall:Name:create"] ||
this.ast["AssemblyCall:Name:create2"]
);
this.capabilities.tryCatchBlocks = !!this.ast["TryStatement"];
this.capabilities.uncheckedBlocks = !!this.ast["UncheckedStatement"];
}
sumCreateNewMetric(...solidityFileMetrics) {
let result = new Metric();
solidityFileMetrics.forEach((a) => {
//arguments
Object.keys(result).forEach((attrib) => {
// metric attribs -> object
Object.keys(a.metrics[attrib]).map(function (key, index) {
// argument.keys
if (typeof a.metrics[attrib][key] === "number")
// ADD
result[attrib][key] =
(result[attrib][key] || 0) + a.metrics[attrib][key];
else if (typeof a.metrics[attrib][key] === "boolean")
// OR
result[attrib][key] = result[attrib][key] || a.metrics[attrib][key];
else if (Array.isArray(a.metrics[attrib][key]))
// concat arrays -> maybe switch to sets
result[attrib][key] = Array.from(
new Set([...result[attrib][key], ...a.metrics[attrib][key]])
);
});
});
});
result.update();
return result;
}
sumAvgCreateNewMetric(...solidityFileMetrics) {
let result = this.sumCreateNewMetric(...solidityFileMetrics);
Object.keys(result).forEach((attrib) => {
// metric attribs -> object
Object.keys(result[attrib]).map(function (key, index) {
// argument.keys
if (typeof result[attrib][key] === "number")
// ADD
result[attrib][key] /= solidityFileMetrics.length;
else delete result[attrib][key]; //not used
});
});
result.update();
return result;
}
}
class SolidityFileMetrics {
constructor(filepath, content) {
this.filename = filepath;
this.metrics = new Metric();
// analyze
this.analyze(content);
// get sloc
this.metrics.sloc = sloc(content, "js");
this.metrics.sloc.commentToSourceRatio =
this.metrics.sloc.comment / this.metrics.sloc.source;
// get normalized sloc (function heads normalized)
const normalized = content.replace(
/function\s*\S+\s*\([^{]*/g,
"function ",
content
);
this.metrics.nsloc = sloc(normalized, "js");
this.metrics.nsloc.commentToSourceRatio =
this.metrics.nsloc.comment / this.metrics.nsloc.source;
this.metrics.update();
}
analyze(content) {
let that = this;
let ast = this.parse(content);
let countAllHandler = {
PragmaDirective(node) {
let pragmaString = node.name + ":" + node.value.replace(" ", "_");
that.metrics.ast["Pragma:" + pragmaString] =
++that.metrics.ast["Pragma:" + pragmaString] || 1;
if (node.name.toLowerCase().indexOf("experimental") >= 0) {
that.metrics.capabilities.experimental.push(node.value);
} else if (node.name.toLowerCase().indexOf("solidity") >= 0) {
that.metrics.capabilities.solidityVersions.push(node.value);
}
},
ImportDirective(node) {
if (node.path && !node.path.startsWith(".")) {
// track all package dependencies, assuming they give information about the systems capabilities (e.g. erc20, oracles, ...)
that.metrics.ast["ImportDirective:Path:" + node.path] =
++that.metrics.ast["ImportDirective:Path:" + node.path] || 1;
}
},
ContractDefinition(node) {
that.metrics.ast["ContractDefinition:" + capitalFirst(node.kind)] =
++that.metrics.ast["ContractDefinition:" + capitalFirst(node.kind)] ||
1;
that.metrics.ast["ContractDefinition:BaseContracts"] =
that.metrics.ast["ContractDefinition:BaseContracts"] +
node.baseContracts.length || node.baseContracts.length;
if (doppelGanger !== undefined) {
try {
that.metrics.other.doppelganger.push(
doppelGanger.compareContractAst(node, that.filename)
);
} catch (e) {
console.error(e);
}
}
},
FunctionDefinition(node) {
let stateMutability = node.stateMutability || "internal"; //set default
that.metrics.ast[
"FunctionDefinition:" + capitalFirst(stateMutability)
] =
++that.metrics.ast[
"FunctionDefinition:" + capitalFirst(stateMutability)
] || 1;
that.metrics.ast[
"FunctionDefinition:" + capitalFirst(node.visibility)
] =
++that.metrics.ast[
"FunctionDefinition:" + capitalFirst(node.visibility)
] || 1;
},
StateVariableDeclaration(node) {
//NOP - this already counts the VariableDeclaration subelements.
},
VariableDeclaration(node) {
let typeName = "VariableDeclaration";
if (node.isStateVar) {
typeName = "StateVariableDeclaration";
that.metrics.ast[typeName + ":" + capitalFirst(node.visibility)] =
++that.metrics.ast[
typeName + ":" + capitalFirst(node.visibility)
] || 1;
}
if (node.storageLocation) {
that.metrics.ast[
typeName + ":" + capitalFirst(node.storageLocation)
] =
++that.metrics.ast[
typeName + ":" + capitalFirst(node.storageLocation)
] || 1;
}
if (node.isDeclaredConst)
that.metrics.ast[typeName + ":Const"] =
++that.metrics.ast[typeName + ":Const"] || 1;
if (node.isIndexed)
that.metrics.ast[typeName + ":Indexed"] =
++that.metrics.ast[typeName + ":Indexed"] || 1;
},
UserDefinedTypeName(node) {
that.metrics.ast["UserDefinedTypeName:" + capitalFirst(node.namePath)] =
++that.metrics.ast[
"UserDefinedTypeName:" + capitalFirst(node.namePath)
] || 1;
},
FunctionCall(node) {
let callName;
let funcCallType;
if (parserHelpers.isRegularFunctionCall(node)) {
funcCallType = "Regular";
callName = node.expression.name;
} else if (parserHelpers.isMemberAccess(node)) {
funcCallType = parserHelpers.isMemberAccessOfAddress(node)
? "Address"
: parserHelpers.isAContractTypecast(node)
? "ContractTypecast"
: "MemberAccess";
callName = node.expression.memberName;
if (
callName == "value" &&
parserHelpers.isMemberAccess(node.expression)
) {
// address.call.value(val)(data)
callName = node.expression.expression.memberName + "." + callName;
}
} else if (
node.expression &&
node.expression.type == "Identifier" &&
parserHelpers.BUILTINS.includes(node.expression.name)
) {
funcCallType = "BuiltIn";
callName = node.expression.name;
} else if (node.expression && node.expression.type == "NewExpression") {
if (node.expression.typeName.type == "UserDefinedTypeName") {
//count contract creation calls
that.metrics.ast[
"NewContract:" + node.expression.typeName.namePath
] =
++that.metrics.ast[
"NewContract:" + node.expression.typeName.namePath
] || 1;
}
} else {
// else TypeNameConversion (e.g. casts.)
}
if (funcCallType) {
that.metrics.ast["FunctionCall:Type:" + funcCallType] =
++that.metrics.ast["FunctionCall:Type:" + funcCallType] || 1;
that.metrics.ast["FunctionCall:Name:" + callName] =
++that.metrics.ast["FunctionCall:Name:" + callName] || 1;
}
},
AssemblyCall(node) {
if (
node.functionName &&
parserHelpers.BUILTINS_ASM.includes(node.functionName)
) {
that.metrics.ast["AssemblyCall:Name:" + node.functionName] =
++that.metrics.ast["AssemblyCall:Name:" + node.functionName] || 1;
}
},
};
let countAll = new Proxy(countAllHandler, {
get(target, name) {
//note: this is getting called twice , once for the vistor[node.name] check and then for visitor[node.name](node) call.
if (name.endsWith(":exit")) return; //skip func-exits
//we have to fix all handler native values afterwards (/2) for the 2nd get call.
that.metrics.ast[name] = ++that.metrics.ast[name] || 1;
that.metrics.num.astStatements += 1;
return target[name];
},
});
parser.visit(ast, countAll);
// IMPORTANT: fix values caused by the proxy being entered twice by diligence parser for defined handler functions (once to check if handler is available, and for handler call)
Object.keys(countAllHandler).forEach((k) =>
this.metrics.ast[k] ? (this.metrics.ast[k] /= 2) : undefined
);
}
parse(content) {
var ast = parser.parse(content, { loc: false, tolerant: true });
return ast;
}
}
module.exports = {
SolidityMetricsContainer: SolidityMetricsContainer,
};