parent
4d34dc8c01
commit
738b9afe4b
@ -1,90 +0,0 @@ |
|||||||
const name = 'Check effects: ' |
|
||||||
const desc = 'Avoid potential reentrancy bugs' |
|
||||||
const categories = require('./categories') |
|
||||||
const common = require('./staticAnalysisCommon') |
|
||||||
const fcallGraph = require('./functionCallGraph') |
|
||||||
const AbstractAst = require('./abstractAstView') |
|
||||||
const algo = require('./algorithmCategories') |
|
||||||
|
|
||||||
function checksEffectsInteraction () { |
|
||||||
this.abstractAst = new AbstractAst() |
|
||||||
this.visit = this.abstractAst.build_visit( |
|
||||||
(node) => common.isInteraction(node) || common.isEffect(node) || common.isLocalCallGraphRelevantNode(node) |
|
||||||
) |
|
||||||
|
|
||||||
this.report = this.abstractAst.build_report(report) |
|
||||||
} |
|
||||||
|
|
||||||
checksEffectsInteraction.prototype.visit = function () { throw new Error('checksEffectsInteraction.js no visit function set upon construction') } |
|
||||||
|
|
||||||
checksEffectsInteraction.prototype.report = function () { throw new Error('checksEffectsInteraction.js no report function set upon construction') } |
|
||||||
|
|
||||||
function report (contracts, multipleContractsWithSameName) { |
|
||||||
const warnings = [] |
|
||||||
const hasModifiers = contracts.some((item) => item.modifiers.length > 0) |
|
||||||
|
|
||||||
const callGraph = fcallGraph.buildGlobalFuncCallGraph(contracts) |
|
||||||
|
|
||||||
contracts.forEach((contract) => { |
|
||||||
contract.functions.forEach((func) => { |
|
||||||
func.changesState = checkIfChangesState(common.getFullQuallyfiedFuncDefinitionIdent(contract.node, func.node, func.parameters), |
|
||||||
getContext(callGraph, contract, func)) |
|
||||||
}) |
|
||||||
|
|
||||||
contract.functions.forEach((func) => { |
|
||||||
if (isPotentialVulnerableFunction(func, getContext(callGraph, contract, func))) { |
|
||||||
const funcName = common.getFullQuallyfiedFuncDefinitionIdent(contract.node, func.node, func.parameters) |
|
||||||
let comments = (hasModifiers) ? 'Note: Modifiers are currently not considered by this static analysis.' : '' |
|
||||||
comments += (multipleContractsWithSameName) ? 'Note: Import aliases are currently not supported by this static analysis.' : '' |
|
||||||
warnings.push({ |
|
||||||
warning: `Potential Violation of Checks-Effects-Interaction pattern in ${funcName}: Could potentially lead to re-entrancy vulnerability. ${comments}`, |
|
||||||
location: func.src, |
|
||||||
more: 'http://solidity.readthedocs.io/en/develop/security-considerations.html#re-entrancy' |
|
||||||
}) |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
return warnings |
|
||||||
} |
|
||||||
|
|
||||||
function getContext (callGraph, currentContract, func) { |
|
||||||
return { callGraph: callGraph, currentContract: currentContract, stateVariables: getStateVariables(currentContract, func) } |
|
||||||
} |
|
||||||
|
|
||||||
function getStateVariables (contract, func) { |
|
||||||
return contract.stateVariables.concat(func.localVariables.filter(common.isStorageVariableDeclaration)) |
|
||||||
} |
|
||||||
|
|
||||||
function isPotentialVulnerableFunction (func, context) { |
|
||||||
let isPotentialVulnerable = false |
|
||||||
let interaction = false |
|
||||||
func.relevantNodes.forEach((node) => { |
|
||||||
if (common.isInteraction(node)) { |
|
||||||
interaction = true |
|
||||||
} else if (interaction && (common.isWriteOnStateVariable(node, context.stateVariables) || isLocalCallWithStateChange(node, context))) { |
|
||||||
isPotentialVulnerable = true |
|
||||||
} |
|
||||||
}) |
|
||||||
return isPotentialVulnerable |
|
||||||
} |
|
||||||
|
|
||||||
function isLocalCallWithStateChange (node, context) { |
|
||||||
if (common.isLocalCallGraphRelevantNode(node)) { |
|
||||||
const func = fcallGraph.resolveCallGraphSymbol(context.callGraph, common.getFullQualifiedFunctionCallIdent(context.currentContract.node, node)) |
|
||||||
return !func || (func && func.node.changesState) |
|
||||||
} |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
function checkIfChangesState (startFuncName, context) { |
|
||||||
return fcallGraph.analyseCallGraph(context.callGraph, startFuncName, context, (node, context) => common.isWriteOnStateVariable(node, context.stateVariables)) |
|
||||||
} |
|
||||||
|
|
||||||
module.exports = { |
|
||||||
name: name, |
|
||||||
description: desc, |
|
||||||
category: categories.SECURITY, |
|
||||||
algorithm: algo.HEURISTIC, |
|
||||||
Module: checksEffectsInteraction |
|
||||||
} |
|
@ -0,0 +1,89 @@ |
|||||||
|
import { default as category } from './categories' |
||||||
|
import { isInteraction, isEffect, isLocalCallGraphRelevantNode, getFullQuallyfiedFuncDefinitionIdent, |
||||||
|
isWriteOnStateVariable, isStorageVariableDeclaration, getFullQualifiedFunctionCallIdent } from './staticAnalysisCommon' |
||||||
|
import { default as algorithm } from './algorithmCategories' |
||||||
|
import { buildGlobalFuncCallGraph, resolveCallGraphSymbol, analyseCallGraph } from './functionCallGraph' |
||||||
|
import AbstractAst from './abstractAstView' |
||||||
|
|
||||||
|
export default class checksEffectsInteraction { |
||||||
|
|
||||||
|
name = 'Check effects: ' |
||||||
|
desc = 'Avoid potential reentrancy bugs' |
||||||
|
categories = category.SECURITY |
||||||
|
algorithm = algorithm.HEURISTIC |
||||||
|
Module = this |
||||||
|
|
||||||
|
abstractAst = new AbstractAst() |
||||||
|
|
||||||
|
visit = this.abstractAst.build_visit((node) => isInteraction(node) || isEffect(node) || isLocalCallGraphRelevantNode(node)) |
||||||
|
|
||||||
|
report = this.abstractAst.build_report(this._report) |
||||||
|
|
||||||
|
private _report (contracts, multipleContractsWithSameName) { |
||||||
|
const warnings: any = [] |
||||||
|
const hasModifiers = contracts.some((item) => item.modifiers.length > 0) |
||||||
|
const callGraph = buildGlobalFuncCallGraph(contracts) |
||||||
|
contracts.forEach((contract) => { |
||||||
|
contract.functions.forEach((func) => { |
||||||
|
func.changesState = this.checkIfChangesState( |
||||||
|
getFullQuallyfiedFuncDefinitionIdent( |
||||||
|
contract.node,
|
||||||
|
func.node,
|
||||||
|
func.parameters |
||||||
|
), |
||||||
|
this.getContext( |
||||||
|
callGraph,
|
||||||
|
contract,
|
||||||
|
func) |
||||||
|
) |
||||||
|
}) |
||||||
|
contract.functions.forEach((func) => { |
||||||
|
if (this.isPotentialVulnerableFunction(func, this.getContext(callGraph, contract, func))) { |
||||||
|
const funcName = getFullQuallyfiedFuncDefinitionIdent(contract.node, func.node, func.parameters) |
||||||
|
let comments = (hasModifiers) ? 'Note: Modifiers are currently not considered by this static analysis.' : '' |
||||||
|
comments += (multipleContractsWithSameName) ? 'Note: Import aliases are currently not supported by this static analysis.' : '' |
||||||
|
warnings.push({ |
||||||
|
warning: `Potential Violation of Checks-Effects-Interaction pattern in ${funcName}: Could potentially lead to re-entrancy vulnerability. ${comments}`, |
||||||
|
location: func.src, |
||||||
|
more: 'http://solidity.readthedocs.io/en/develop/security-considerations.html#re-entrancy' |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
return warnings |
||||||
|
} |
||||||
|
|
||||||
|
private getContext (callGraph, currentContract, func) { |
||||||
|
return { callGraph: callGraph, currentContract: currentContract, stateVariables: this.getStateVariables(currentContract, func) } |
||||||
|
} |
||||||
|
|
||||||
|
private getStateVariables (contract, func) { |
||||||
|
return contract.stateVariables.concat(func.localVariables.filter(isStorageVariableDeclaration)) |
||||||
|
} |
||||||
|
|
||||||
|
private isPotentialVulnerableFunction (func, context) { |
||||||
|
let isPotentialVulnerable = false |
||||||
|
let interaction = false |
||||||
|
func.relevantNodes.forEach((node) => { |
||||||
|
if (isInteraction(node)) { |
||||||
|
interaction = true |
||||||
|
} else if (interaction && (isWriteOnStateVariable(node, context.stateVariables) || this.isLocalCallWithStateChange(node, context))) { |
||||||
|
isPotentialVulnerable = true |
||||||
|
} |
||||||
|
}) |
||||||
|
return isPotentialVulnerable |
||||||
|
} |
||||||
|
|
||||||
|
private isLocalCallWithStateChange (node, context) { |
||||||
|
if (isLocalCallGraphRelevantNode(node)) { |
||||||
|
const func = resolveCallGraphSymbol(context.callGraph, getFullQualifiedFunctionCallIdent(context.currentContract.node, node)) |
||||||
|
return !func || (func && func.node.changesState) |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
private checkIfChangesState (startFuncName, context) { |
||||||
|
return analyseCallGraph(context.callGraph, startFuncName, context, (node, context) => isWriteOnStateVariable(node, context.stateVariables)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
@ -1,110 +0,0 @@ |
|||||||
const name = 'Constant functions: ' |
|
||||||
const desc = 'Check for potentially constant functions' |
|
||||||
const categories = require('./categories') |
|
||||||
const common = require('./staticAnalysisCommon') |
|
||||||
const fcallGraph = require('./functionCallGraph') |
|
||||||
const AbstractAst = require('./abstractAstView') |
|
||||||
const algo = require('./algorithmCategories') |
|
||||||
|
|
||||||
function constantFunctions () { |
|
||||||
this.abstractAst = new AbstractAst() |
|
||||||
|
|
||||||
this.visit = this.abstractAst.build_visit( |
|
||||||
(node) => common.isLowLevelCall(node) || |
|
||||||
common.isTransfer(node) || |
|
||||||
common.isExternalDirectCall(node) || |
|
||||||
common.isEffect(node) || |
|
||||||
common.isLocalCallGraphRelevantNode(node) || |
|
||||||
common.isInlineAssembly(node) || |
|
||||||
common.isNewExpression(node) || |
|
||||||
common.isSelfdestructCall(node) || |
|
||||||
common.isDeleteUnaryOperation(node) |
|
||||||
) |
|
||||||
|
|
||||||
this.report = this.abstractAst.build_report(report) |
|
||||||
} |
|
||||||
|
|
||||||
constantFunctions.prototype.visit = function () { throw new Error('constantFunctions.js no visit function set upon construction') } |
|
||||||
|
|
||||||
constantFunctions.prototype.report = function () { throw new Error('constantFunctions.js no report function set upon construction') } |
|
||||||
|
|
||||||
function report (contracts, multipleContractsWithSameName) { |
|
||||||
const warnings = [] |
|
||||||
const hasModifiers = contracts.some((item) => item.modifiers.length > 0) |
|
||||||
|
|
||||||
const callGraph = fcallGraph.buildGlobalFuncCallGraph(contracts) |
|
||||||
|
|
||||||
contracts.forEach((contract) => { |
|
||||||
contract.functions.forEach((func) => { |
|
||||||
if (common.isPayableFunction(func.node) || common.isConstructor(func.node)) { |
|
||||||
func.potentiallyshouldBeConst = false |
|
||||||
} else { |
|
||||||
func.potentiallyshouldBeConst = checkIfShouldBeConstant(common.getFullQuallyfiedFuncDefinitionIdent(contract.node, func.node, func.parameters), |
|
||||||
getContext(callGraph, contract, func)) |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
contract.functions.filter((func) => common.hasFunctionBody(func.node)).forEach((func) => { |
|
||||||
if (common.isConstantFunction(func.node) !== func.potentiallyshouldBeConst) { |
|
||||||
const funcName = common.getFullQuallyfiedFuncDefinitionIdent(contract.node, func.node, func.parameters) |
|
||||||
let comments = (hasModifiers) ? 'Note: Modifiers are currently not considered by this static analysis.' : '' |
|
||||||
comments += (multipleContractsWithSameName) ? 'Note: Import aliases are currently not supported by this static analysis.' : '' |
|
||||||
if (func.potentiallyshouldBeConst) { |
|
||||||
warnings.push({ |
|
||||||
warning: `${funcName} : Potentially should be constant but is not. ${comments}`, |
|
||||||
location: func.src, |
|
||||||
more: 'http://solidity.readthedocs.io/en/develop/contracts.html#constant-functions' |
|
||||||
}) |
|
||||||
} else { |
|
||||||
warnings.push({ |
|
||||||
warning: `${funcName} : Is constant but potentially should not be. ${comments}`, |
|
||||||
location: func.src, |
|
||||||
more: 'http://solidity.readthedocs.io/en/develop/contracts.html#constant-functions' |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
return warnings |
|
||||||
} |
|
||||||
|
|
||||||
function getContext (callGraph, currentContract, func) { |
|
||||||
return { callGraph: callGraph, currentContract: currentContract, stateVariables: getStateVariables(currentContract, func) } |
|
||||||
} |
|
||||||
|
|
||||||
function getStateVariables (contract, func) { |
|
||||||
return contract.stateVariables.concat(func.localVariables.filter(common.isStorageVariableDeclaration)) |
|
||||||
} |
|
||||||
|
|
||||||
function checkIfShouldBeConstant (startFuncName, context) { |
|
||||||
return !fcallGraph.analyseCallGraph(context.callGraph, startFuncName, context, isConstBreaker) |
|
||||||
} |
|
||||||
|
|
||||||
function isConstBreaker (node, context) { |
|
||||||
return common.isWriteOnStateVariable(node, context.stateVariables) || |
|
||||||
common.isLowLevelCall(node) || |
|
||||||
common.isTransfer(node) || |
|
||||||
isCallOnNonConstExternalInterfaceFunction(node, context) || |
|
||||||
common.isCallToNonConstLocalFunction(node) || |
|
||||||
common.isInlineAssembly(node) || |
|
||||||
common.isNewExpression(node) || |
|
||||||
common.isSelfdestructCall(node) || |
|
||||||
common.isDeleteUnaryOperation(node) |
|
||||||
} |
|
||||||
|
|
||||||
function isCallOnNonConstExternalInterfaceFunction (node, context) { |
|
||||||
if (common.isExternalDirectCall(node)) { |
|
||||||
const func = fcallGraph.resolveCallGraphSymbol(context.callGraph, common.getFullQualifiedFunctionCallIdent(context.currentContract, node)) |
|
||||||
return !func || (func && !common.isConstantFunction(func.node.node)) |
|
||||||
} |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
module.exports = { |
|
||||||
name: name, |
|
||||||
description: desc, |
|
||||||
category: categories.MISC, |
|
||||||
algorithm: algo.HEURISTIC, |
|
||||||
Module: constantFunctions |
|
||||||
} |
|
@ -0,0 +1,114 @@ |
|||||||
|
import { default as category } from './categories' |
||||||
|
import { isLowLevelCall, isTransfer, isExternalDirectCall, isEffect, isLocalCallGraphRelevantNode, |
||||||
|
isInlineAssembly, isNewExpression, isSelfdestructCall, isDeleteUnaryOperation, isPayableFunction, |
||||||
|
isConstructor, getFullQuallyfiedFuncDefinitionIdent, hasFunctionBody, isConstantFunction, isWriteOnStateVariable, |
||||||
|
isStorageVariableDeclaration, isCallToNonConstLocalFunction, getFullQualifiedFunctionCallIdent} from './staticAnalysisCommon' |
||||||
|
import { default as algorithm } from './algorithmCategories' |
||||||
|
import { buildGlobalFuncCallGraph, resolveCallGraphSymbol, analyseCallGraph } from './functionCallGraph' |
||||||
|
import AbstractAst from './abstractAstView' |
||||||
|
|
||||||
|
export default class constantFunctions { |
||||||
|
name = 'Constant functions: ' |
||||||
|
desc = 'Check for potentially constant functions' |
||||||
|
categories = category.MISC |
||||||
|
algorithm = algorithm.HEURISTIC |
||||||
|
Module = this |
||||||
|
|
||||||
|
abstractAst = new AbstractAst() |
||||||
|
|
||||||
|
visit = this.abstractAst.build_visit( |
||||||
|
(node) => isLowLevelCall(node) || |
||||||
|
isTransfer(node) || |
||||||
|
isExternalDirectCall(node) || |
||||||
|
isEffect(node) || |
||||||
|
isLocalCallGraphRelevantNode(node) || |
||||||
|
isInlineAssembly(node) || |
||||||
|
isNewExpression(node) || |
||||||
|
isSelfdestructCall(node) || |
||||||
|
isDeleteUnaryOperation(node) |
||||||
|
) |
||||||
|
|
||||||
|
report = this.abstractAst.build_report(this._report) |
||||||
|
|
||||||
|
private _report (contracts, multipleContractsWithSameName) { |
||||||
|
const warnings: any = [] |
||||||
|
const hasModifiers = contracts.some((item) => item.modifiers.length > 0) |
||||||
|
|
||||||
|
const callGraph = buildGlobalFuncCallGraph(contracts) |
||||||
|
|
||||||
|
contracts.forEach((contract) => { |
||||||
|
contract.functions.forEach((func) => { |
||||||
|
if (isPayableFunction(func.node) || isConstructor(func.node)) { |
||||||
|
func.potentiallyshouldBeConst = false |
||||||
|
} else { |
||||||
|
func.potentiallyshouldBeConst = this.checkIfShouldBeConstant( |
||||||
|
getFullQuallyfiedFuncDefinitionIdent( |
||||||
|
contract.node,
|
||||||
|
func.node,
|
||||||
|
func.parameters |
||||||
|
), |
||||||
|
this.getContext( |
||||||
|
callGraph,
|
||||||
|
contract,
|
||||||
|
func |
||||||
|
) |
||||||
|
) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
contract.functions.filter((func) => hasFunctionBody(func.node)).forEach((func) => { |
||||||
|
if (isConstantFunction(func.node) !== func.potentiallyshouldBeConst) { |
||||||
|
const funcName = getFullQuallyfiedFuncDefinitionIdent(contract.node, func.node, func.parameters) |
||||||
|
let comments = (hasModifiers) ? 'Note: Modifiers are currently not considered by this static analysis.' : '' |
||||||
|
comments += (multipleContractsWithSameName) ? 'Note: Import aliases are currently not supported by this static analysis.' : '' |
||||||
|
if (func.potentiallyshouldBeConst) { |
||||||
|
warnings.push({ |
||||||
|
warning: `${funcName} : Potentially should be constant but is not. ${comments}`, |
||||||
|
location: func.src, |
||||||
|
more: 'http://solidity.readthedocs.io/en/develop/contracts.html#constant-functions' |
||||||
|
}) |
||||||
|
} else { |
||||||
|
warnings.push({ |
||||||
|
warning: `${funcName} : Is constant but potentially should not be. ${comments}`, |
||||||
|
location: func.src, |
||||||
|
more: 'http://solidity.readthedocs.io/en/develop/contracts.html#constant-functions' |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
return warnings |
||||||
|
} |
||||||
|
|
||||||
|
private getContext (callGraph, currentContract, func) { |
||||||
|
return { callGraph: callGraph, currentContract: currentContract, stateVariables: this.getStateVariables(currentContract, func) } |
||||||
|
} |
||||||
|
|
||||||
|
private getStateVariables (contract, func) { |
||||||
|
return contract.stateVariables.concat(func.localVariables.filter(isStorageVariableDeclaration)) |
||||||
|
} |
||||||
|
|
||||||
|
private checkIfShouldBeConstant (startFuncName, context) { |
||||||
|
return !analyseCallGraph(context.callGraph, startFuncName, context, this.isConstBreaker) |
||||||
|
} |
||||||
|
|
||||||
|
private isConstBreaker (node, context) { |
||||||
|
return isWriteOnStateVariable(node, context.stateVariables) || |
||||||
|
isLowLevelCall(node) || |
||||||
|
isTransfer(node) || |
||||||
|
this.isCallOnNonConstExternalInterfaceFunction(node, context) || |
||||||
|
isCallToNonConstLocalFunction(node) || |
||||||
|
isInlineAssembly(node) || |
||||||
|
isNewExpression(node) || |
||||||
|
isSelfdestructCall(node) || |
||||||
|
isDeleteUnaryOperation(node) |
||||||
|
} |
||||||
|
|
||||||
|
private isCallOnNonConstExternalInterfaceFunction (node, context) { |
||||||
|
if (isExternalDirectCall(node)) { |
||||||
|
const func = resolveCallGraphSymbol(context.callGraph, getFullQualifiedFunctionCallIdent(context.currentContract, node)) |
||||||
|
return !func || (func && !isConstantFunction(func.node.node)) |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
@ -1,63 +0,0 @@ |
|||||||
const name = 'ERC20: ' |
|
||||||
const desc = 'Decimal should be uint8' |
|
||||||
const categories = require('./categories') |
|
||||||
const common = require('./staticAnalysisCommon') |
|
||||||
const AbstractAst = require('./abstractAstView') |
|
||||||
const algo = require('./algorithmCategories') |
|
||||||
|
|
||||||
function erc20Decimals () { |
|
||||||
this.abstractAst = new AbstractAst() |
|
||||||
this.visit = this.abstractAst.build_visit( |
|
||||||
(node) => false |
|
||||||
) |
|
||||||
this.report = this.abstractAst.build_report(report) |
|
||||||
} |
|
||||||
|
|
||||||
erc20Decimals.prototype.visit = function () { throw new Error('erc20Decimals.js no visit function set upon construction') } |
|
||||||
|
|
||||||
erc20Decimals.prototype.report = function () { throw new Error('erc20Decimals.js no report function set upon construction') } |
|
||||||
|
|
||||||
function report (contracts, multipleContractsWithSameName) { |
|
||||||
const warnings = [] |
|
||||||
|
|
||||||
contracts.forEach((contract) => { |
|
||||||
const contractAbiSignatures = contract.functions.map((f) => common.helpers.buildAbiSignature(common.getFunctionDefinitionName(f.node), f.parameters)) |
|
||||||
|
|
||||||
if (isERC20(contractAbiSignatures)) { |
|
||||||
const decimalsVar = contract.stateVariables.filter((stateVar) => common.getDeclaredVariableName(stateVar) === 'decimals' && (common.getDeclaredVariableType(stateVar) !== 'uint8' || stateVar.attributes.visibility !== 'public')) |
|
||||||
const decimalsFun = contract.functions.filter((f) => common.getFunctionDefinitionName(f.node) === 'decimals' && |
|
||||||
( |
|
||||||
(f.returns.length === 0 || f.returns.length > 1) || |
|
||||||
(f.returns.length === 1 && (f.returns[0].type !== 'uint8' || f.node.attributes.visibility !== 'public')) |
|
||||||
) |
|
||||||
) |
|
||||||
|
|
||||||
if (decimalsVar.length > 0 || decimalsFun.length > 0) { |
|
||||||
warnings.push({ |
|
||||||
warning: 'ERC20 Contracts decimals function should have uint8 as return type', |
|
||||||
location: null, |
|
||||||
more: ' https://eips.ethereum.org/EIPS/eip-20' |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
return warnings |
|
||||||
} |
|
||||||
|
|
||||||
function isERC20 (funSignatures) { |
|
||||||
return funSignatures.includes('totalSupply()') && |
|
||||||
funSignatures.includes('balanceOf(address)') && |
|
||||||
funSignatures.includes('transfer(address,uint256)') && |
|
||||||
funSignatures.includes('transferFrom(address,address,uint256)') && |
|
||||||
funSignatures.includes('approve(address,uint256)') && |
|
||||||
funSignatures.includes('allowance(address,address)') |
|
||||||
} |
|
||||||
|
|
||||||
module.exports = { |
|
||||||
name: name, |
|
||||||
description: desc, |
|
||||||
category: categories.ERC, |
|
||||||
algorithm: algo.EXACT, |
|
||||||
Module: erc20Decimals |
|
||||||
} |
|
@ -0,0 +1,54 @@ |
|||||||
|
import { default as category } from './categories' |
||||||
|
import { getFunctionDefinitionName, helpers, getDeclaredVariableName, getDeclaredVariableType } from './staticAnalysisCommon' |
||||||
|
import { default as algorithm } from './algorithmCategories' |
||||||
|
import AbstractAst from './abstractAstView' |
||||||
|
|
||||||
|
export default class erc20Decimals { |
||||||
|
name = 'ERC20: ' |
||||||
|
desc = 'Decimal should be uint8' |
||||||
|
categories = category.ERC |
||||||
|
algorithm = algorithm.EXACT |
||||||
|
Module = this |
||||||
|
|
||||||
|
abstractAst = new AbstractAst() |
||||||
|
visit = this.abstractAst.build_visit((node) => false) |
||||||
|
report = this.abstractAst.build_report(this._report) |
||||||
|
|
||||||
|
private _report (contracts, multipleContractsWithSameName) { |
||||||
|
const warnings: any = [] |
||||||
|
|
||||||
|
contracts.forEach((contract) => { |
||||||
|
const contractAbiSignatures = contract.functions.map((f) => helpers.buildAbiSignature(getFunctionDefinitionName(f.node), f.parameters)) |
||||||
|
|
||||||
|
if (this.isERC20(contractAbiSignatures)) { |
||||||
|
const decimalsVar = contract.stateVariables.filter((stateVar) => getDeclaredVariableName(stateVar) === 'decimals' && (getDeclaredVariableType(stateVar) !== 'uint8' || stateVar.attributes.visibility !== 'public')) |
||||||
|
const decimalsFun = contract.functions.filter((f) => getFunctionDefinitionName(f.node) === 'decimals' && |
||||||
|
( |
||||||
|
(f.returns.length === 0 || f.returns.length > 1) || |
||||||
|
(f.returns.length === 1 && (f.returns[0].type !== 'uint8' || f.node.attributes.visibility !== 'public')) |
||||||
|
) |
||||||
|
) |
||||||
|
|
||||||
|
if (decimalsVar.length > 0 || decimalsFun.length > 0) { |
||||||
|
warnings.push({ |
||||||
|
warning: 'ERC20 Contracts decimals function should have uint8 as return type', |
||||||
|
location: null, |
||||||
|
more: ' https://eips.ethereum.org/EIPS/eip-20' |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
return warnings |
||||||
|
} |
||||||
|
|
||||||
|
private isERC20 (funSignatures) { |
||||||
|
return funSignatures.includes('totalSupply()') && |
||||||
|
funSignatures.includes('balanceOf(address)') && |
||||||
|
funSignatures.includes('transfer(address,uint256)') && |
||||||
|
funSignatures.includes('transferFrom(address,address,uint256)') && |
||||||
|
funSignatures.includes('approve(address,uint256)') && |
||||||
|
funSignatures.includes('allowance(address,address)') |
||||||
|
} |
||||||
|
} |
||||||
|
|
@ -1,67 +0,0 @@ |
|||||||
const name = 'Gas costs: ' |
|
||||||
const desc = 'Warn if the gas requirements of functions are too high.' |
|
||||||
const categories = require('./categories') |
|
||||||
const algo = require('./algorithmCategories') |
|
||||||
|
|
||||||
function gasCosts () { |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* call the given @arg cb (function) for all the contracts. Uses last compilation result |
|
||||||
* stop visiting when cb return true |
|
||||||
* @param {Function} cb - callback |
|
||||||
*/ |
|
||||||
// @TODO has been copied from remix-ide repo ! should fix that soon !
|
|
||||||
function visitContracts (contracts, cb) { |
|
||||||
for (let file in contracts) { |
|
||||||
for (let name in contracts[file]) { |
|
||||||
if (cb({ name: name, object: contracts[file][name], file: file })) return |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
gasCosts.prototype.report = function (compilationResults) { |
|
||||||
const report = [] |
|
||||||
visitContracts(compilationResults.contracts, (contract) => { |
|
||||||
if ( |
|
||||||
!contract.object.evm.gasEstimates || |
|
||||||
!contract.object.evm.gasEstimates.external |
|
||||||
) { |
|
||||||
return |
|
||||||
} |
|
||||||
const fallback = contract.object.evm.gasEstimates.external[''] |
|
||||||
if (fallback !== undefined) { |
|
||||||
if (fallback === null || fallback >= 2100 || fallback === 'infinite') { |
|
||||||
report.push({ |
|
||||||
warning: `Fallback function of contract ${contract.name} requires too much gas (${fallback}).
|
|
||||||
If the fallback function requires more than 2300 gas, the contract cannot receive Ether.` |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
for (var functionName in contract.object.evm.gasEstimates.external) { |
|
||||||
if (functionName === '') { |
|
||||||
continue |
|
||||||
} |
|
||||||
const gas = contract.object.evm.gasEstimates.external[functionName] |
|
||||||
const gasString = gas === null ? 'unknown or not constant' : 'high: ' + gas |
|
||||||
if (gas === null || gas >= 3000000 || gas === 'infinite') { |
|
||||||
report.push({ |
|
||||||
warning: `Gas requirement of function ${contract.name}.${functionName} ${gasString}.
|
|
||||||
If the gas requirement of a function is higher than the block gas limit, it cannot be executed. |
|
||||||
Please avoid loops in your functions or actions that modify large areas of storage |
|
||||||
(this includes clearing or copying arrays in storage)` |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
}) |
|
||||||
return report |
|
||||||
} |
|
||||||
|
|
||||||
module.exports = { |
|
||||||
name: name, |
|
||||||
description: desc, |
|
||||||
category: categories.GAS, |
|
||||||
algorithm: algo.EXACT, |
|
||||||
Module: gasCosts |
|
||||||
} |
|
@ -0,0 +1,63 @@ |
|||||||
|
import { default as category } from './categories' |
||||||
|
import { default as algorithm } from './algorithmCategories' |
||||||
|
|
||||||
|
export default class gasCosts { |
||||||
|
name = 'Gas costs: ' |
||||||
|
desc = 'Warn if the gas requirements of functions are too high.' |
||||||
|
categories = category.GAS |
||||||
|
algorithm = algorithm.EXACT |
||||||
|
Module = this |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* call the given @arg cb (function) for all the contracts. Uses last compilation result |
||||||
|
* stop visiting when cb return true |
||||||
|
* @param {Function} cb - callback |
||||||
|
*/ |
||||||
|
// @TODO has been copied from remix-ide repo ! should fix that soon !
|
||||||
|
visitContracts (contracts, cb) { |
||||||
|
for (let file in contracts) { |
||||||
|
for (let name in contracts[file]) { |
||||||
|
if (cb({ name: name, object: contracts[file][name], file: file })) return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
report (compilationResults) { |
||||||
|
const report: any[] = [] |
||||||
|
this.visitContracts(compilationResults.contracts, (contract) => { |
||||||
|
if ( |
||||||
|
!contract.object.evm.gasEstimates || |
||||||
|
!contract.object.evm.gasEstimates.external |
||||||
|
) { |
||||||
|
return |
||||||
|
} |
||||||
|
const fallback = contract.object.evm.gasEstimates.external[''] |
||||||
|
if (fallback !== undefined) { |
||||||
|
if (fallback === null || fallback >= 2100 || fallback === 'infinite') { |
||||||
|
report.push({ |
||||||
|
warning: `Fallback function of contract ${contract.name} requires too much gas (${fallback}).
|
||||||
|
If the fallback function requires more than 2300 gas, the contract cannot receive Ether.` |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for (var functionName in contract.object.evm.gasEstimates.external) { |
||||||
|
if (functionName === '') { |
||||||
|
continue |
||||||
|
} |
||||||
|
const gas = contract.object.evm.gasEstimates.external[functionName] |
||||||
|
const gasString = gas === null ? 'unknown or not constant' : 'high: ' + gas |
||||||
|
if (gas === null || gas >= 3000000 || gas === 'infinite') { |
||||||
|
report.push({ |
||||||
|
warning: `Gas requirement of function ${contract.name}.${functionName} ${gasString}.
|
||||||
|
If the gas requirement of a function is higher than the block gas limit, it cannot be executed. |
||||||
|
Please avoid loops in your functions or actions that modify large areas of storage |
||||||
|
(this includes clearing or copying arrays in storage)` |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
return report |
||||||
|
} |
||||||
|
} |
@ -1,21 +0,0 @@ |
|||||||
module.exports = [ |
|
||||||
require('./txOrigin'), |
|
||||||
require('./gasCosts'), |
|
||||||
require('./thisLocal'), |
|
||||||
require('./checksEffectsInteraction'), |
|
||||||
require('./constantFunctions'), |
|
||||||
require('./similarVariableNames.js'), |
|
||||||
require('./inlineAssembly'), |
|
||||||
require('./blockTimestamp'), |
|
||||||
require('./lowLevelCalls'), |
|
||||||
require('./blockBlockhash'), |
|
||||||
require('./noReturn'), |
|
||||||
require('./selfdestruct'), |
|
||||||
require('./guardConditions'), |
|
||||||
require('./deleteDynamicArrays'), |
|
||||||
require('./assignAndCompare'), |
|
||||||
require('./erc20Decimals'), |
|
||||||
require('./stringBytesLength'), |
|
||||||
require('./deleteFromDynamicArray'), |
|
||||||
require('./forLoopIteratesOverDynamicArray') |
|
||||||
] |
|
@ -0,0 +1,41 @@ |
|||||||
|
import txOrigin from './txOrigin' |
||||||
|
import gasCosts from './gasCosts' |
||||||
|
import thisLocal from './thisLocal' |
||||||
|
import checksEffectsInteraction from './checksEffectsInteraction' |
||||||
|
import constantFunctions from './constantFunctions' |
||||||
|
import similarVariableNames from './similarVariableNames' |
||||||
|
import inlineAssembly from './inlineAssembly' |
||||||
|
import blockTimestamp from './blockTimestamp' |
||||||
|
import lowLevelCalls from './lowLevelCalls' |
||||||
|
import blockBlockhash from './blockBlockhash' |
||||||
|
import noReturn from './noReturn' |
||||||
|
import selfdestruct from './selfdestruct' |
||||||
|
import guardConditions from './guardConditions' |
||||||
|
import deleteDynamicArrays from './deleteDynamicArrays' |
||||||
|
import assignAndCompare from './assignAndCompare' |
||||||
|
import erc20Decimals from './erc20Decimals' |
||||||
|
import stringBytesLength from './stringBytesLength' |
||||||
|
import deleteFromDynamicArray from './deleteFromDynamicArray' |
||||||
|
import forLoopIteratesOverDynamicArray from './forLoopIteratesOverDynamicArray' |
||||||
|
|
||||||
|
export default [ |
||||||
|
new txOrigin(), |
||||||
|
new gasCosts(), |
||||||
|
new thisLocal(), |
||||||
|
new checksEffectsInteraction(), |
||||||
|
new constantFunctions(), |
||||||
|
new similarVariableNames(), |
||||||
|
new inlineAssembly(), |
||||||
|
new blockTimestamp(), |
||||||
|
new lowLevelCalls(), |
||||||
|
new blockBlockhash(), |
||||||
|
new noReturn(), |
||||||
|
new selfdestruct(), |
||||||
|
new guardConditions(), |
||||||
|
new deleteDynamicArrays(), |
||||||
|
new assignAndCompare(), |
||||||
|
new erc20Decimals(), |
||||||
|
new stringBytesLength(), |
||||||
|
new deleteFromDynamicArray(), |
||||||
|
new forLoopIteratesOverDynamicArray() |
||||||
|
] |
@ -1,75 +0,0 @@ |
|||||||
const name = 'no return: ' |
|
||||||
const desc = 'Function with return type is not returning' |
|
||||||
const categories = require('./categories') |
|
||||||
const common = require('./staticAnalysisCommon') |
|
||||||
const AbstractAst = require('./abstractAstView') |
|
||||||
const algo = require('./algorithmCategories') |
|
||||||
|
|
||||||
function noReturn () { |
|
||||||
this.abstractAst = new AbstractAst() |
|
||||||
|
|
||||||
this.visit = this.abstractAst.build_visit( |
|
||||||
(node) => common.isReturn(node) || common.isAssignment(node) |
|
||||||
) |
|
||||||
|
|
||||||
this.report = this.abstractAst.build_report(report) |
|
||||||
} |
|
||||||
|
|
||||||
noReturn.prototype.visit = function () { throw new Error('noReturn.js no visit function set upon construction') } |
|
||||||
|
|
||||||
noReturn.prototype.report = function () { throw new Error('noReturn.js no report function set upon construction') } |
|
||||||
|
|
||||||
function report (contracts, multipleContractsWithSameName) { |
|
||||||
const warnings = [] |
|
||||||
|
|
||||||
contracts.forEach((contract) => { |
|
||||||
contract.functions.filter((func) => common.hasFunctionBody(func.node)).forEach((func) => { |
|
||||||
const funcName = common.getFullQuallyfiedFuncDefinitionIdent(contract.node, func.node, func.parameters) |
|
||||||
if (hasNamedAndUnnamedReturns(func)) { |
|
||||||
warnings.push({ |
|
||||||
warning: `${funcName}: Mixing of named and unnamed return parameters is not advised.`, |
|
||||||
location: func.src |
|
||||||
}) |
|
||||||
} else if (shouldReturn(func) && !(hasReturnStatement(func) || (hasNamedReturns(func) && hasAssignToAllNamedReturns(func)))) { |
|
||||||
warnings.push({ |
|
||||||
warning: `${funcName}: Defines a return type but never explicitly returns a value.`, |
|
||||||
location: func.src |
|
||||||
}) |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
return warnings |
|
||||||
} |
|
||||||
|
|
||||||
function shouldReturn (func) { |
|
||||||
return func.returns.length > 0 |
|
||||||
} |
|
||||||
|
|
||||||
function hasReturnStatement (func) { |
|
||||||
return func.relevantNodes.filter(common.isReturn).length > 0 |
|
||||||
} |
|
||||||
|
|
||||||
function hasAssignToAllNamedReturns (func) { |
|
||||||
const namedReturns = func.returns.filter((n) => n.name.length > 0).map((n) => n.name) |
|
||||||
const assignedVars = func.relevantNodes.filter(common.isAssignment).map(common.getEffectedVariableName) |
|
||||||
const diff = namedReturns.filter(e => !assignedVars.includes(e)) |
|
||||||
return diff.length === 0 |
|
||||||
} |
|
||||||
|
|
||||||
function hasNamedReturns (func) { |
|
||||||
return func.returns.filter((n) => n.name.length > 0).length > 0 |
|
||||||
} |
|
||||||
|
|
||||||
function hasNamedAndUnnamedReturns (func) { |
|
||||||
return func.returns.filter((n) => n.name.length === 0).length > 0 && |
|
||||||
hasNamedReturns(func) |
|
||||||
} |
|
||||||
|
|
||||||
module.exports = { |
|
||||||
name: name, |
|
||||||
description: desc, |
|
||||||
category: categories.MISC, |
|
||||||
algorithm: algo.EXACT, |
|
||||||
Module: noReturn |
|
||||||
} |
|
@ -0,0 +1,66 @@ |
|||||||
|
import { default as category } from './categories' |
||||||
|
import { isReturn, isAssignment, hasFunctionBody, getFullQuallyfiedFuncDefinitionIdent, getEffectedVariableName } from './staticAnalysisCommon' |
||||||
|
import { default as algorithm } from './algorithmCategories' |
||||||
|
import AbstractAst from './abstractAstView' |
||||||
|
|
||||||
|
export default class noReturn { |
||||||
|
name = 'no return: ' |
||||||
|
desc = 'Function with return type is not returning' |
||||||
|
categories = category.MISC |
||||||
|
algorithm = algorithm.EXACT |
||||||
|
Module = this |
||||||
|
|
||||||
|
abstractAst = new AbstractAst() |
||||||
|
|
||||||
|
visit = this.abstractAst.build_visit( |
||||||
|
(node) => isReturn(node) || isAssignment(node) |
||||||
|
) |
||||||
|
|
||||||
|
report = this.abstractAst.build_report(this._report) |
||||||
|
private _report (contracts, multipleContractsWithSameName) { |
||||||
|
const warnings: any[] = [] |
||||||
|
|
||||||
|
contracts.forEach((contract) => { |
||||||
|
contract.functions.filter((func) => hasFunctionBody(func.node)).forEach((func) => { |
||||||
|
const funcName = getFullQuallyfiedFuncDefinitionIdent(contract.node, func.node, func.parameters) |
||||||
|
if (this.hasNamedAndUnnamedReturns(func)) { |
||||||
|
warnings.push({ |
||||||
|
warning: `${funcName}: Mixing of named and unnamed return parameters is not advised.`, |
||||||
|
location: func.src |
||||||
|
}) |
||||||
|
} else if (this.shouldReturn(func) && !(this.hasReturnStatement(func) || (this.hasNamedReturns(func) && this.hasAssignToAllNamedReturns(func)))) { |
||||||
|
warnings.push({ |
||||||
|
warning: `${funcName}: Defines a return type but never explicitly returns a value.`, |
||||||
|
location: func.src |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
return warnings |
||||||
|
} |
||||||
|
|
||||||
|
private shouldReturn (func) { |
||||||
|
return func.returns.length > 0 |
||||||
|
} |
||||||
|
|
||||||
|
private hasReturnStatement (func) { |
||||||
|
return func.relevantNodes.filter(isReturn).length > 0 |
||||||
|
} |
||||||
|
|
||||||
|
private hasAssignToAllNamedReturns (func) { |
||||||
|
const namedReturns = func.returns.filter((n) => n.name.length > 0).map((n) => n.name) |
||||||
|
const assignedVars = func.relevantNodes.filter(isAssignment).map(getEffectedVariableName) |
||||||
|
const diff = namedReturns.filter(e => !assignedVars.includes(e)) |
||||||
|
return diff.length === 0 |
||||||
|
} |
||||||
|
|
||||||
|
private hasNamedReturns (func) { |
||||||
|
return func.returns.filter((n) => n.name.length > 0).length > 0 |
||||||
|
} |
||||||
|
|
||||||
|
private hasNamedAndUnnamedReturns (func) { |
||||||
|
return func.returns.filter((n) => n.name.length === 0).length > 0 && |
||||||
|
this.hasNamedReturns(func) |
||||||
|
} |
||||||
|
} |
@ -1,59 +0,0 @@ |
|||||||
const name = 'Selfdestruct: ' |
|
||||||
const desc = 'Be aware of caller contracts.' |
|
||||||
const categories = require('./categories') |
|
||||||
const common = require('./staticAnalysisCommon') |
|
||||||
const AbstractAst = require('./abstractAstView') |
|
||||||
const algo = require('./algorithmCategories') |
|
||||||
|
|
||||||
function selfdestruct () { |
|
||||||
this.abstractAst = new AbstractAst() |
|
||||||
|
|
||||||
this.visit = this.abstractAst.build_visit( |
|
||||||
(node) => common.isStatement(node) || |
|
||||||
common.isSelfdestructCall(node) |
|
||||||
) |
|
||||||
|
|
||||||
this.report = this.abstractAst.build_report(report) |
|
||||||
} |
|
||||||
|
|
||||||
selfdestruct.prototype.visit = function () { throw new Error('selfdestruct.js no visit function set upon construction') } |
|
||||||
|
|
||||||
selfdestruct.prototype.report = function () { throw new Error('selfdestruct.js no report function set upon construction') } |
|
||||||
|
|
||||||
function report (contracts, multipleContractsWithSameName) { |
|
||||||
const warnings = [] |
|
||||||
|
|
||||||
contracts.forEach((contract) => { |
|
||||||
contract.functions.forEach((func) => { |
|
||||||
let hasSelf = false |
|
||||||
func.relevantNodes.forEach((node) => { |
|
||||||
if (common.isSelfdestructCall(node)) { |
|
||||||
warnings.push({ |
|
||||||
warning: 'Use of selfdestruct: can block calling contracts unexpectedly. Be especially careful if this contract is planned to be used by other contracts (i.e. library contracts, interactions). Selfdestruction of the callee contract can leave callers in an inoperable state.', |
|
||||||
location: node.src, |
|
||||||
more: 'https://paritytech.io/blog/security-alert.html' |
|
||||||
}) |
|
||||||
hasSelf = true |
|
||||||
} |
|
||||||
if (common.isStatement(node) && hasSelf) { |
|
||||||
warnings.push({ |
|
||||||
warning: 'Use of selfdestruct: No code after selfdestruct is executed. Selfdestruct is a terminal.', |
|
||||||
location: node.src, |
|
||||||
more: 'http://solidity.readthedocs.io/en/develop/introduction-to-smart-contracts.html#self-destruct' |
|
||||||
}) |
|
||||||
hasSelf = false |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
return warnings |
|
||||||
} |
|
||||||
|
|
||||||
module.exports = { |
|
||||||
name: name, |
|
||||||
description: desc, |
|
||||||
category: categories.SECURITY, |
|
||||||
algorithm: algo.HEURISTIC, |
|
||||||
Module: selfdestruct |
|
||||||
} |
|
@ -0,0 +1,50 @@ |
|||||||
|
import { default as category } from './categories' |
||||||
|
import { isStatement, isSelfdestructCall } from './staticAnalysisCommon' |
||||||
|
import { default as algorithm } from './algorithmCategories' |
||||||
|
import AbstractAst from './abstractAstView' |
||||||
|
|
||||||
|
export default class selfdestruct { |
||||||
|
name = 'Selfdestruct: ' |
||||||
|
desc = 'Be aware of caller contracts.' |
||||||
|
categories = category.SECURITY |
||||||
|
algorithm = algorithm.HEURISTIC |
||||||
|
Module = this |
||||||
|
|
||||||
|
abstractAst = new AbstractAst() |
||||||
|
|
||||||
|
visit = this.abstractAst.build_visit( |
||||||
|
(node) => isStatement(node) || |
||||||
|
isSelfdestructCall(node) |
||||||
|
) |
||||||
|
|
||||||
|
report = this.abstractAst.build_report(this._report) |
||||||
|
private _report (contracts, multipleContractsWithSameName) { |
||||||
|
const warnings: any[] = [] |
||||||
|
|
||||||
|
contracts.forEach((contract) => { |
||||||
|
contract.functions.forEach((func) => { |
||||||
|
let hasSelf = false |
||||||
|
func.relevantNodes.forEach((node) => { |
||||||
|
if (isSelfdestructCall(node)) { |
||||||
|
warnings.push({ |
||||||
|
warning: 'Use of selfdestruct: can block calling contracts unexpectedly. Be especially careful if this contract is planned to be used by other contracts (i.e. library contracts, interactions). Selfdestruction of the callee contract can leave callers in an inoperable state.', |
||||||
|
location: node.src, |
||||||
|
more: 'https://paritytech.io/blog/security-alert.html' |
||||||
|
}) |
||||||
|
hasSelf = true |
||||||
|
} |
||||||
|
if (isStatement(node) && hasSelf) { |
||||||
|
warnings.push({ |
||||||
|
warning: 'Use of selfdestruct: No code after selfdestruct is executed. Selfdestruct is a terminal.', |
||||||
|
location: node.src, |
||||||
|
more: 'http://solidity.readthedocs.io/en/develop/introduction-to-smart-contracts.html#self-destruct' |
||||||
|
}) |
||||||
|
hasSelf = false |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
return warnings |
||||||
|
} |
||||||
|
} |
@ -1,87 +0,0 @@ |
|||||||
const name = 'Similar variable names: ' |
|
||||||
const desc = 'Check if variable names are too similar' |
|
||||||
const categories = require('./categories') |
|
||||||
const common = require('./staticAnalysisCommon') |
|
||||||
const AbstractAst = require('./abstractAstView') |
|
||||||
const levenshtein = require('fast-levenshtein') |
|
||||||
const remixLib = require('remix-lib') |
|
||||||
const util = remixLib.util |
|
||||||
const algo = require('./algorithmCategories') |
|
||||||
|
|
||||||
function similarVariableNames () { |
|
||||||
this.abstractAst = new AbstractAst() |
|
||||||
|
|
||||||
this.visit = this.abstractAst.build_visit( |
|
||||||
(node) => false |
|
||||||
) |
|
||||||
|
|
||||||
this.report = this.abstractAst.build_report(report) |
|
||||||
} |
|
||||||
|
|
||||||
similarVariableNames.prototype.visit = function () { throw new Error('similarVariableNames.js no visit function set upon construction') } |
|
||||||
|
|
||||||
similarVariableNames.prototype.report = function () { throw new Error('similarVariableNames.js no report function set upon construction') } |
|
||||||
|
|
||||||
function report (contracts, multipleContractsWithSameName) { |
|
||||||
const warnings = [] |
|
||||||
const hasModifiers = contracts.some((item) => item.modifiers.length > 0) |
|
||||||
|
|
||||||
contracts.forEach((contract) => { |
|
||||||
contract.functions.forEach((func) => { |
|
||||||
const funcName = common.getFullQuallyfiedFuncDefinitionIdent(contract.node, func.node, func.parameters) |
|
||||||
let hasModifiersComments = '' |
|
||||||
if (hasModifiers) { |
|
||||||
hasModifiersComments = 'Note: Modifiers are currently not considered by this static analysis.' |
|
||||||
} |
|
||||||
let multipleContractsWithSameNameComments = '' |
|
||||||
if (multipleContractsWithSameName) { |
|
||||||
multipleContractsWithSameNameComments = 'Note: Import aliases are currently not supported by this static analysis.' |
|
||||||
} |
|
||||||
|
|
||||||
const vars = getFunctionVariables(contract, func).map(common.getDeclaredVariableName) |
|
||||||
|
|
||||||
findSimilarVarNames(vars).map((sim) => { |
|
||||||
warnings.push({ |
|
||||||
warning: `${funcName} : Variables have very similar names ${sim.var1} and ${sim.var2}. ${hasModifiersComments} ${multipleContractsWithSameNameComments}`, |
|
||||||
location: func.src |
|
||||||
}) |
|
||||||
}) |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
return warnings |
|
||||||
} |
|
||||||
|
|
||||||
function findSimilarVarNames (vars) { |
|
||||||
const similar = [] |
|
||||||
const comb = {} |
|
||||||
vars.map((varName1) => vars.map((varName2) => { |
|
||||||
if (varName1.length > 1 && varName2.length > 1 && varName2 !== varName1 && !isCommonPrefixedVersion(varName1, varName2) && !isCommonNrSuffixVersion(varName1, varName2) && !(comb[varName1 + ';' + varName2] || comb[varName2 + ';' + varName1])) { |
|
||||||
comb[varName1 + ';' + varName2] = true |
|
||||||
const distance = levenshtein.get(varName1, varName2) |
|
||||||
if (distance <= 2) similar.push({ var1: varName1, var2: varName2, distance: distance }) |
|
||||||
} |
|
||||||
})) |
|
||||||
return similar |
|
||||||
} |
|
||||||
|
|
||||||
function isCommonPrefixedVersion (varName1, varName2) { |
|
||||||
return (varName1.startsWith('_') && varName1.slice(1) === varName2) || (varName2.startsWith('_') && varName2.slice(1) === varName1) |
|
||||||
} |
|
||||||
|
|
||||||
function isCommonNrSuffixVersion (varName1, varName2) { |
|
||||||
const ref = '^' + util.escapeRegExp(varName1.slice(0, -1)) + '[0-9]*$' |
|
||||||
return varName2.match(ref) != null |
|
||||||
} |
|
||||||
|
|
||||||
function getFunctionVariables (contract, func) { |
|
||||||
return contract.stateVariables.concat(func.localVariables) |
|
||||||
} |
|
||||||
|
|
||||||
module.exports = { |
|
||||||
name: name, |
|
||||||
description: desc, |
|
||||||
category: categories.MISC, |
|
||||||
algorithm: algo.EXACT, |
|
||||||
Module: similarVariableNames |
|
||||||
} |
|
@ -0,0 +1,77 @@ |
|||||||
|
import { default as category } from './categories' |
||||||
|
import { getDeclaredVariableName, getFullQuallyfiedFuncDefinitionIdent } from './staticAnalysisCommon' |
||||||
|
import { default as algorithm } from './algorithmCategories' |
||||||
|
import AbstractAst from './abstractAstView' |
||||||
|
import { get } from 'fast-levenshtein' |
||||||
|
import { util } from 'remix-lib' |
||||||
|
|
||||||
|
export default class similarVariableNames { |
||||||
|
name = 'Similar variable names: ' |
||||||
|
desc = 'Check if variable names are too similar' |
||||||
|
abstractAst = new AbstractAst() |
||||||
|
categories = category.MISC |
||||||
|
algorithm = algorithm.EXACT |
||||||
|
Module = this |
||||||
|
|
||||||
|
visit = this.abstractAst.build_visit( |
||||||
|
(node) => false |
||||||
|
) |
||||||
|
|
||||||
|
report = this.abstractAst.build_report(this._report) |
||||||
|
|
||||||
|
private _report (contracts, multipleContractsWithSameName) { |
||||||
|
const warnings: any[] = [] |
||||||
|
const hasModifiers = contracts.some((item) => item.modifiers.length > 0) |
||||||
|
|
||||||
|
contracts.forEach((contract) => { |
||||||
|
contract.functions.forEach((func) => { |
||||||
|
const funcName = getFullQuallyfiedFuncDefinitionIdent(contract.node, func.node, func.parameters) |
||||||
|
let hasModifiersComments = '' |
||||||
|
if (hasModifiers) { |
||||||
|
hasModifiersComments = 'Note: Modifiers are currently not considered by this static analysis.' |
||||||
|
} |
||||||
|
let multipleContractsWithSameNameComments = '' |
||||||
|
if (multipleContractsWithSameName) { |
||||||
|
multipleContractsWithSameNameComments = 'Note: Import aliases are currently not supported by this static analysis.' |
||||||
|
} |
||||||
|
|
||||||
|
const vars = this.getFunctionVariables(contract, func).map(getDeclaredVariableName) |
||||||
|
|
||||||
|
this.findSimilarVarNames(vars).map((sim) => { |
||||||
|
warnings.push({ |
||||||
|
warning: `${funcName} : Variables have very similar names ${sim.var1} and ${sim.var2}. ${hasModifiersComments} ${multipleContractsWithSameNameComments}`, |
||||||
|
location: func.src |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
return warnings |
||||||
|
} |
||||||
|
|
||||||
|
private findSimilarVarNames (vars) { |
||||||
|
const similar: any[] = [] |
||||||
|
const comb = {} |
||||||
|
vars.map((varName1) => vars.map((varName2) => { |
||||||
|
if (varName1.length > 1 && varName2.length > 1 && varName2 !== varName1 && !this.isCommonPrefixedVersion(varName1, varName2) && !this.isCommonNrSuffixVersion(varName1, varName2) && !(comb[varName1 + ';' + varName2] || comb[varName2 + ';' + varName1])) { |
||||||
|
comb[varName1 + ';' + varName2] = true |
||||||
|
const distance = get(varName1, varName2) |
||||||
|
if (distance <= 2) similar.push({ var1: varName1, var2: varName2, distance: distance }) |
||||||
|
} |
||||||
|
})) |
||||||
|
return similar |
||||||
|
} |
||||||
|
|
||||||
|
private isCommonPrefixedVersion (varName1, varName2) { |
||||||
|
return (varName1.startsWith('_') && varName1.slice(1) === varName2) || (varName2.startsWith('_') && varName2.slice(1) === varName1) |
||||||
|
} |
||||||
|
|
||||||
|
private isCommonNrSuffixVersion (varName1, varName2) { |
||||||
|
const ref = '^' + util.escapeRegExp(varName1.slice(0, -1)) + '[0-9]*$' |
||||||
|
return varName2.match(ref) != null |
||||||
|
} |
||||||
|
|
||||||
|
private getFunctionVariables (contract, func) { |
||||||
|
return contract.stateVariables.concat(func.localVariables) |
||||||
|
} |
||||||
|
} |
@ -1,33 +0,0 @@ |
|||||||
const name = 'String Length: ' |
|
||||||
const desc = 'Bytes length != String length' |
|
||||||
const categories = require('./categories') |
|
||||||
const common = require('./staticAnalysisCommon') |
|
||||||
|
|
||||||
function stringBytesLength () { |
|
||||||
this.stringToBytesConversions = [] |
|
||||||
this.bytesLengthChecks = [] |
|
||||||
} |
|
||||||
|
|
||||||
stringBytesLength.prototype.visit = function (node) { |
|
||||||
if (common.isStringToBytesConversion(node)) this.stringToBytesConversions.push(node) |
|
||||||
else if (common.isBytesLengthCheck(node)) this.bytesLengthChecks.push(node) |
|
||||||
} |
|
||||||
|
|
||||||
stringBytesLength.prototype.report = function (compilationResults) { |
|
||||||
if (this.stringToBytesConversions.length > 0 && this.bytesLengthChecks.length > 0) { |
|
||||||
return [{ |
|
||||||
warning: 'Bytes and string length are not the same since strings are assumed to be UTF-8 encoded (according to the ABI defintion) therefore one character is not nessesarily encoded in one byte of data.', |
|
||||||
location: this.bytesLengthChecks[0].src, |
|
||||||
more: 'https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI#argument-encoding' |
|
||||||
}] |
|
||||||
} else { |
|
||||||
return [] |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
module.exports = { |
|
||||||
name: name, |
|
||||||
description: desc, |
|
||||||
category: categories.MISC, |
|
||||||
Module: stringBytesLength |
|
||||||
} |
|
@ -0,0 +1,30 @@ |
|||||||
|
import { default as category } from './categories' |
||||||
|
import { isStringToBytesConversion, isBytesLengthCheck } from './staticAnalysisCommon' |
||||||
|
|
||||||
|
export default class stringBytesLength { |
||||||
|
name = 'String Length: ' |
||||||
|
desc = 'Bytes length != String length' |
||||||
|
categories = category.MISC |
||||||
|
Module = this |
||||||
|
|
||||||
|
stringToBytesConversions: any[] = [] |
||||||
|
bytesLengthChecks: any[] = [] |
||||||
|
|
||||||
|
|
||||||
|
visit (node) { |
||||||
|
if (isStringToBytesConversion(node)) this.stringToBytesConversions.push(node) |
||||||
|
else if (isBytesLengthCheck(node)) this.bytesLengthChecks.push(node) |
||||||
|
} |
||||||
|
|
||||||
|
report (compilationResults) { |
||||||
|
if (this.stringToBytesConversions.length > 0 && this.bytesLengthChecks.length > 0) { |
||||||
|
return [{ |
||||||
|
warning: 'Bytes and string length are not the same since strings are assumed to be UTF-8 encoded (according to the ABI defintion) therefore one character is not nessesarily encoded in one byte of data.', |
||||||
|
location: this.bytesLengthChecks[0].src, |
||||||
|
more: 'https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI#argument-encoding' |
||||||
|
}] |
||||||
|
} else { |
||||||
|
return [] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -1,31 +0,0 @@ |
|||||||
const name = 'This on local calls: ' |
|
||||||
const desc = 'Invocation of local functions via this' |
|
||||||
const categories = require('./categories') |
|
||||||
const common = require('./staticAnalysisCommon') |
|
||||||
const algo = require('./algorithmCategories') |
|
||||||
|
|
||||||
function thisLocal () { |
|
||||||
this.warningNodes = [] |
|
||||||
} |
|
||||||
|
|
||||||
thisLocal.prototype.visit = function (node) { |
|
||||||
if (common.isThisLocalCall(node)) this.warningNodes.push(node) |
|
||||||
} |
|
||||||
|
|
||||||
thisLocal.prototype.report = function (compilationResults) { |
|
||||||
return this.warningNodes.map(function (item, i) { |
|
||||||
return { |
|
||||||
warning: 'Use of "this" for local functions: Never use this to call functions in the same contract, it only consumes more gas than normal local calls.', |
|
||||||
location: item.src, |
|
||||||
more: 'http://solidity.readthedocs.io/en/develop/control-structures.html#external-function-calls' |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
module.exports = { |
|
||||||
name: name, |
|
||||||
description: desc, |
|
||||||
category: categories.GAS, |
|
||||||
algorithm: algo.EXACT, |
|
||||||
Module: thisLocal |
|
||||||
} |
|
@ -0,0 +1,26 @@ |
|||||||
|
import { default as category } from './categories' |
||||||
|
import { isThisLocalCall } from './staticAnalysisCommon' |
||||||
|
import { default as algorithm } from './algorithmCategories' |
||||||
|
|
||||||
|
export default class thisLocal { |
||||||
|
warningNodes: any[] = [] |
||||||
|
name = 'This on local calls: ' |
||||||
|
desc = 'Invocation of local functions via this' |
||||||
|
categories = category.GAS |
||||||
|
algorithm = algorithm.EXACT |
||||||
|
Module = this |
||||||
|
|
||||||
|
visit (node) { |
||||||
|
if (isThisLocalCall(node)) this.warningNodes.push(node) |
||||||
|
} |
||||||
|
|
||||||
|
report (compilationResults) { |
||||||
|
return this.warningNodes.map(function (item, i) { |
||||||
|
return { |
||||||
|
warning: 'Use of "this" for local functions: Never use this to call functions in the same contract, it only consumes more gas than normal local calls.', |
||||||
|
location: item.src, |
||||||
|
more: 'http://solidity.readthedocs.io/en/develop/control-structures.html#external-function-calls' |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
@ -1,37 +0,0 @@ |
|||||||
const name = 'Transaction origin: ' |
|
||||||
const desc = 'Warn if tx.origin is used' |
|
||||||
const categories = require('./categories') |
|
||||||
const algo = require('./algorithmCategories') |
|
||||||
|
|
||||||
function txOrigin () { |
|
||||||
this.txOriginNodes = [] |
|
||||||
} |
|
||||||
|
|
||||||
txOrigin.prototype.visit = function (node) { |
|
||||||
if (node.name === 'MemberAccess' && |
|
||||||
node.attributes.member_name === 'origin' && |
|
||||||
(node.attributes.type === 'address' || node.attributes.type === 'address payable') && |
|
||||||
node.children && node.children.length && |
|
||||||
node.children[0].attributes.type === 'tx' && |
|
||||||
node.children[0].attributes.value === 'tx') { |
|
||||||
this.txOriginNodes.push(node) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
txOrigin.prototype.report = function () { |
|
||||||
return this.txOriginNodes.map((item, i) => { |
|
||||||
return { |
|
||||||
warning: `Use of tx.origin: "tx.origin" is useful only in very exceptional cases.
|
|
||||||
If you use it for authentication, you usually want to replace it by "msg.sender", because otherwise any contract you call can act on your behalf.`,
|
|
||||||
location: item.src |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
module.exports = { |
|
||||||
name: name, |
|
||||||
description: desc, |
|
||||||
category: categories.SECURITY, |
|
||||||
algorithm: algo.EXACT, |
|
||||||
Module: txOrigin |
|
||||||
} |
|
@ -0,0 +1,32 @@ |
|||||||
|
import { default as category } from './categories' |
||||||
|
import { default as algorithm } from './algorithmCategories' |
||||||
|
|
||||||
|
export default class txOrigin { |
||||||
|
txOriginNodes: any[] = [] |
||||||
|
name = 'Transaction origin: ' |
||||||
|
desc = 'Warn if tx.origin is used' |
||||||
|
categories = category.SECURITY |
||||||
|
algorithm = algorithm.EXACT |
||||||
|
Module = this |
||||||
|
|
||||||
|
visit (node) { |
||||||
|
if (node.name === 'MemberAccess' && |
||||||
|
node.attributes.member_name === 'origin' && |
||||||
|
(node.attributes.type === 'address' || node.attributes.type === 'address payable') && |
||||||
|
node.children && node.children.length && |
||||||
|
node.children[0].attributes.type === 'tx' && |
||||||
|
node.children[0].attributes.value === 'tx') { |
||||||
|
this.txOriginNodes.push(node) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
report () { |
||||||
|
return this.txOriginNodes.map((item, i) => { |
||||||
|
return { |
||||||
|
warning: `Use of tx.origin: "tx.origin" is useful only in very exceptional cases.
|
||||||
|
If you use it for authentication, you usually want to replace it by "msg.sender", because otherwise any contract you call can act on your behalf.`,
|
||||||
|
location: item.src |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue