Static Analysis: library support

pull/1/head
Michael Fröwis 8 years ago committed by chriseth
parent f2068ea236
commit dc7dc11954
  1. 6
      src/app/staticanalysis/modules/checksEffectsInteraction.js
  2. 8
      src/app/staticanalysis/modules/constantFunctions.js
  3. 34
      src/app/staticanalysis/modules/staticAnalysisCommon.js
  4. 96
      test/staticanalysis/staticAnalysisCommon-test.js
  5. 31
      test/staticanalysis/staticAnalysisIntegration-test.js
  6. 54
      test/staticanalysis/test-contracts/library.sol

@ -16,11 +16,10 @@ function checksEffectsInteraction () {
checksEffectsInteraction.prototype.report = function (compilationResults) {
var warnings = []
var hasModifiers = this.contracts.some((item) => item.modifiers.length > 0)
var cg = callGraph.buildGlobalFuncCallGraph(this.contracts)
if (this.contracts.some((item) => item.modifiers.length > 0)) warnings.push({ warning: `Modifiers are currently not supported by the '${name}' static analysis.` })
this.contracts.forEach((contract) => {
contract.functions.forEach((func) => {
func.changesState = checkIfChangesState(common.getFullQuallyfiedFuncDefinitionIdent(contract.node, func.node, func.parameters),
@ -30,8 +29,9 @@ checksEffectsInteraction.prototype.report = function (compilationResults) {
contract.functions.forEach((func) => {
if (isPotentialVulnerableFunction(func, getContext(cg, contract, func))) {
var funcName = common.getFullQuallyfiedFuncDefinitionIdent(contract.node, func.node, func.parameters)
var comments = (hasModifiers) ? '<br/><i>Note:</i>Modifiers are currently not considered by the this static analysis.' : ''
warnings.push({
warning: `Potential Violation of Checks-Effects-Interaction pattern in <i>${funcName}</i>: Could potentially lead to re-entrancy vulnerability.`,
warning: `Potential Violation of Checks-Effects-Interaction pattern in <i>${funcName}</i>: Could potentially lead to re-entrancy vulnerability.${comments}`,
location: func.src,
more: 'http://solidity.readthedocs.io/en/develop/security-considerations.html#re-entrancy'
})

@ -16,11 +16,10 @@ function constantFunctions () {
constantFunctions.prototype.report = function (compilationResults) {
var warnings = []
var hasModifiers = this.contracts.some((item) => item.modifiers.length > 0)
var cg = callGraph.buildGlobalFuncCallGraph(this.contracts)
if (this.contracts.some((item) => item.modifiers.length > 0)) warnings.push({ warning: `Modifiers are currently not supported by the '${name}' static analysis.` })
this.contracts.forEach((contract) => {
if (!common.isFullyImplementedContract(contract.node)) return
@ -32,15 +31,16 @@ constantFunctions.prototype.report = function (compilationResults) {
contract.functions.forEach((func) => {
if (common.isConstantFunction(func.node) !== func.potentiallyshouldBeConst) {
var funcName = common.getFullQuallyfiedFuncDefinitionIdent(contract.node, func.node, func.parameters)
var comments = (hasModifiers) ? '<br/><i>Note:</i>Modifiers are currently not considered by the this static analysis.' : ''
if (func.potentiallyshouldBeConst) {
warnings.push({
warning: `<i>${funcName}</i>: Potentially should be constant but is not.`,
warning: `<i>${funcName}</i>: 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: `<i>${funcName}</i>: Is constant but potentially should not be.`,
warning: `<i>${funcName}</i>: Is constant but potentially should not be.${comments}`,
location: func.src,
more: 'http://solidity.readthedocs.io/en/develop/contracts.html#constant-functions'
})

@ -32,7 +32,8 @@ var basicRegex = {
EXTERNALFUNCTIONTYPE: '^function \\(.*\\).* external',
CONSTANTFUNCTIONTYPE: '^function \\(.*\\).* constant',
REFTYPE: '( storage )|(mapping\\()|(\\[\\])',
FUNCTIONSIGNATURE: '^function \\(([^\\(]*)\\)'
FUNCTIONSIGNATURE: '^function \\(([^\\(]*)\\)',
LIBRARYTYPE: '^type\\(library (.*)\\)'
}
var basicFunctionTypes = {
@ -77,8 +78,8 @@ function getType (node) {
// #################### Complex Getters
function getFunctionCallType (func) {
if (!(isExternalDirectCall(func) || isThisLocalCall(func) || isSuperLocalCall(func) || isLocalCall(func))) throw new Error('staticAnalysisCommon.js: not function call Node')
if (isExternalDirectCall(func) || isThisLocalCall(func) || isSuperLocalCall(func)) return func.attributes.type
if (!(isExternalDirectCall(func) || isThisLocalCall(func) || isSuperLocalCall(func) || isLocalCall(func) || isLibraryCall(func))) throw new Error('staticAnalysisCommon.js: not function call Node')
if (isExternalDirectCall(func) || isThisLocalCall(func) || isSuperLocalCall(func) || isLibraryCall(func)) return func.attributes.type
return findFirstSubNodeLTR(func, exactMatch(nodeTypes.IDENTIFIER)).attributes.type
}
@ -151,11 +152,22 @@ function getFunctionCallTypeParameterType (func) {
return new RegExp(basicRegex.FUNCTIONSIGNATURE).exec(getFunctionCallType(func))[1]
}
function getLibraryCallContractName (funcCall) {
if (!isLibraryCall(funcCall)) throw new Error('staticAnalysisCommon.js: not an this library call Node')
return new RegExp(basicRegex.LIBRARYTYPE).exec(funcCall.children[0].attributes.type)[1]
}
function getLibraryCallMemberName (funcCall) {
if (!isLibraryCall(funcCall)) throw new Error('staticAnalysisCommon.js: not an library call Node')
return funcCall.attributes.member_name
}
function getFullQualifiedFunctionCallIdent (contract, func) {
if (isLocalCall(func)) return getContractName(contract) + '.' + getLocalCallName(func) + '(' + getFunctionCallTypeParameterType(func) + ')'
else if (isThisLocalCall(func)) return getThisLocalCallContractName(func) + '.' + getThisLocalCallName(func) + '(' + getFunctionCallTypeParameterType(func) + ')'
else if (isSuperLocalCall(func)) return getContractName(contract) + '.' + getSuperLocalCallName(func) + '(' + getFunctionCallTypeParameterType(func) + ')'
else if (isExternalDirectCall(func)) return getExternalDirectCallContractName(func) + '.' + getExternalDirectCallMemberName(func) + '(' + getFunctionCallTypeParameterType(func) + ')'
else if (isLibraryCall(func)) return getLibraryCallContractName(func) + '.' + getLibraryCallMemberName(func) + '(' + getFunctionCallTypeParameterType(func) + ')'
else throw new Error('staticAnalysisCommon.js: Can not get function name form non function call node')
}
@ -200,7 +212,7 @@ function isInlineAssembly (node) {
// #################### Complex Node Identification
function isLocalCallGraphRelevantNode (node) {
return ((isLocalCall(node) || isSuperLocalCall(node)) && !isBuiltinFunctionCall(node))
return ((isLocalCall(node) || isSuperLocalCall(node) || isLibraryCall(node)) && !isBuiltinFunctionCall(node))
}
function isBuiltinFunctionCall (node) {
@ -220,7 +232,7 @@ function isEffect (node) {
}
function isWriteOnStateVariable (effectNode, stateVariables) {
return isEffect(effectNode) && !isInlineAssembly(effectNode) && isStateVariable(getEffectedVariableName(effectNode), stateVariables)
return isInlineAssembly(effectNode) || (isEffect(effectNode) && isStateVariable(getEffectedVariableName(effectNode), stateVariables))
}
function isStateVariable (name, stateVariables) {
@ -243,10 +255,18 @@ function isFullyImplementedContract (node) {
return nodeType(node, exactMatch(nodeTypes.CONTRACTDEFINITION)) && node.attributes.fullyImplemented === true
}
function isLibrary (node) {
return nodeType(node, exactMatch(nodeTypes.CONTRACTDEFINITION)) && node.attributes.isLibrary === true
}
function isCallToNonConstLocalFunction (node) {
return isLocalCall(node) && !expressionType(node.children[0], basicRegex.CONSTANTFUNCTIONTYPE)
}
function isLibraryCall (node) {
return isMemberAccess(node, basicRegex.FUNCTIONTYPE, undefined, basicRegex.LIBRARYTYPE, undefined)
}
function isExternalDirectCall (node) {
return isMemberAccess(node, basicRegex.EXTERNALFUNCTIONTYPE, undefined, basicRegex.CONTRACTTYPE, undefined) && !isThisLocalCall(node) && !isSuperLocalCall(node)
}
@ -403,6 +423,8 @@ module.exports = {
getExternalDirectCallMemberName: getExternalDirectCallMemberName,
getFunctionDefinitionName: getFunctionDefinitionName,
getFunctionCallTypeParameterType: getFunctionCallTypeParameterType,
getLibraryCallContractName: getLibraryCallContractName,
getLibraryCallMemberName: getLibraryCallMemberName,
getFullQualifiedFunctionCallIdent: getFullQualifiedFunctionCallIdent,
getFullQuallyfiedFuncDefinitionIdent: getFullQuallyfiedFuncDefinitionIdent,
getStateVariableDeclarationsFormContractNode: getStateVariableDeclarationsFormContractNode,
@ -416,6 +438,7 @@ module.exports = {
isBlockBlockHashAccess: isBlockBlockHashAccess,
isThisLocalCall: isThisLocalCall,
isSuperLocalCall: isSuperLocalCall,
isLibraryCall: isLibraryCall,
isLocalCallGraphRelevantNode: isLocalCallGraphRelevantNode,
isLocalCall: isLocalCall,
isWriteOnStateVariable: isWriteOnStateVariable,
@ -427,6 +450,7 @@ module.exports = {
isLowLevelSendInst: isLLSend,
isExternalDirectCall: isExternalDirectCall,
isFullyImplementedContract: isFullyImplementedContract,
isLibrary: isLibrary,
isCallToNonConstLocalFunction: isCallToNonConstLocalFunction,
isPlusPlusUnaryOperation: isPlusPlusUnaryOperation,
isMinusMinusUnaryOperation: isMinusMinusUnaryOperation,

@ -147,7 +147,7 @@ test('staticAnalysisCommon.getType', function (t) {
// #################### Complex Getter Test
test('staticAnalysisCommon.getFunctionCallType', function (t) {
t.plan(4)
t.plan(5)
var localCall = {
'attributes': {
'type': 'tuple()',
@ -198,9 +198,26 @@ test('staticAnalysisCommon.getFunctionCallType', function (t) {
name: 'MemberAccess',
src: '405:6:0'
}
t.ok(common.getFunctionCallType(thisLocalCall) === 'function (bytes32,address) returns (bool)', 'this local call returns correct type')
t.ok(common.getFunctionCallType(externalDirect) === 'function () payable external returns (uint256)', 'external direct call returns correct type')
t.ok(common.getFunctionCallType(localCall) === 'function (struct Ballot.Voter storage pointer)', 'local call returns correct type')
var libCall = {
'attributes': {
'member_name': 'insert',
'type': 'function (struct Set.Data storage pointer,uint256) returns (bool)'
},
'children': [
{
'attributes': {
'type': 'type(library Set)',
'value': 'Set'
},
'name': 'Identifier'
}
],
'name': 'MemberAccess'
}
t.equal(common.getFunctionCallType(libCall), 'function (struct Set.Data storage pointer,uint256) returns (bool)', 'this lib call returns correct type')
t.equal(common.getFunctionCallType(thisLocalCall), 'function (bytes32,address) returns (bool)', 'this local call returns correct type')
t.equal(common.getFunctionCallType(externalDirect), 'function () payable external returns (uint256)', 'external direct call returns correct type')
t.equal(common.getFunctionCallType(localCall), 'function (struct Ballot.Voter storage pointer)', 'local call returns correct type')
t.throws(() => common.getFunctionCallType({ name: 'MemberAccess' }), undefined, 'throws on wrong type')
})
@ -910,6 +927,50 @@ test('staticAnalysisCommon.getFunctionCallTypeParameterType', function (t) {
t.throws(() => common.getFunctionCallTypeParameterType({ name: 'MemberAccess' }), undefined, 'throws on wrong type')
})
test('staticAnalysisCommon.getLibraryCallContractName', function (t) {
t.plan(2)
var node = {
'attributes': {
'member_name': 'insert',
'type': 'function (struct Set.Data storage pointer,uint256) returns (bool)'
},
'children': [
{
'attributes': {
'type': 'type(library Set)',
'value': 'Set'
},
'name': 'Identifier'
}
],
'name': 'MemberAccess'
}
t.equal(common.getLibraryCallContractName(node), 'Set', 'should return correct contract name')
t.throws(() => common.getLibraryCallContractName({ name: 'Identifier' }), undefined, 'should throw on wrong node')
})
test('staticAnalysisCommon.getLibraryCallMemberName', function (t) {
t.plan(2)
var node = {
'attributes': {
'member_name': 'insert',
'type': 'function (struct Set.Data storage pointer,uint256) returns (bool)'
},
'children': [
{
'attributes': {
'type': 'type(library Set)',
'value': 'Set'
},
'name': 'Identifier'
}
],
'name': 'MemberAccess'
}
t.equal(common.getLibraryCallMemberName(node), 'insert', 'should return correct member name')
t.throws(() => common.getLibraryCallMemberName({ name: 'Identifier' }), undefined, 'should throw on wrong node')
})
test('staticAnalysisCommon.getFullQualifiedFunctionCallIdent', function (t) {
t.plan(4)
var contract = { name: 'ContractDefinition', attributes: { name: 'baz' } }
@ -1538,7 +1599,7 @@ test('staticAnalysisCommon.isWriteOnStateVariable', function (t) {
],
'name': 'VariableDeclaration'
}
t.notOk(common.isWriteOnStateVariable(inlineAssembly, [node1, node2, node3]), 'inline Assembly is not write on state')
t.ok(common.isWriteOnStateVariable(inlineAssembly, [node1, node2, node3]), 'inline Assembly is write on state')
t.notOk(common.isWriteOnStateVariable(assignment, [node1, node2, node3]), 'assignment on non state is not write on state')
node3.attributes.name = 'c'
t.ok(common.isWriteOnStateVariable(assignment, [node1, node2, node3]), 'assignment on state is not write on state')
@ -1777,6 +1838,31 @@ test('staticAnalysisCommon.isSuperLocalCall', function (t) {
t.notOk(common.isNowAccess(node), 'is now used should not work')
})
test('staticAnalysisCommon.isLibraryCall', function (t) {
t.plan(5)
var node = {
'attributes': {
'member_name': 'insert',
'type': 'function (struct Set.Data storage pointer,uint256) returns (bool)'
},
'children': [
{
'attributes': {
'type': 'type(library Set)',
'value': 'Set'
},
'name': 'Identifier'
}
],
'name': 'MemberAccess'
}
t.ok(common.isLibraryCall(node), 'is lib call should not work')
t.notOk(common.isSuperLocalCall(node), 'is super.local_method() used should not work')
t.notOk(common.isThisLocalCall(node), 'is this.local_method() used should not work')
t.notOk(common.isBlockTimestampAccess(node), 'is block.timestamp used should not work')
t.notOk(common.isNowAccess(node), 'is now used should not work')
})
test('staticAnalysisCommon.isLocalCall', function (t) {
t.plan(5)
var node1 = {

@ -21,7 +21,8 @@ var testFiles = [
'notReentrant.sol',
'structReentrant.sol',
'thisLocal.sol',
'globals.sol'
'globals.sol',
'library.sol'
]
var testFileAsts = {}
@ -49,7 +50,8 @@ test('Integration test thisLocal.js', function (t) {
'notReentrant.sol': 0,
'structReentrant.sol': 0,
'thisLocal.sol': 1,
'globals.sol': 0
'globals.sol': 0,
'library.sol': 0
}
runModuleOnFiles(module, t, (file, report) => {
@ -64,18 +66,19 @@ test('Integration test checksEffectsInteraction.js', function (t) {
var lengthCheck = {
'KingOfTheEtherThrone.sol': 1,
'assembly.sol': 0,
'assembly.sol': 1,
'ballot.sol': 0,
'ballot_reentrant.sol': 1,
'ballot_withoutWarnings.sol': 0,
'cross_contract.sol': 0,
'inheritance.sol': 1,
'modifier1.sol': 1,
'modifier2.sol': 1,
'modifier1.sol': 0,
'modifier2.sol': 0,
'notReentrant.sol': 0,
'structReentrant.sol': 1,
'thisLocal.sol': 0,
'globals.sol': 1
'globals.sol': 1,
'library.sol': 1
}
runModuleOnFiles(module, t, (file, report) => {
@ -96,12 +99,13 @@ test('Integration test constantFunctions.js', function (t) {
'ballot_withoutWarnings.sol': 0,
'cross_contract.sol': 1,
'inheritance.sol': 0,
'modifier1.sol': 2,
'modifier2.sol': 1,
'modifier1.sol': 1,
'modifier2.sol': 0,
'notReentrant.sol': 0,
'structReentrant.sol': 0,
'thisLocal.sol': 1,
'globals.sol': 0
'globals.sol': 0,
'library.sol': 1
}
runModuleOnFiles(module, t, (file, report) => {
@ -127,7 +131,8 @@ test('Integration test inlineAssembly.js', function (t) {
'notReentrant.sol': 0,
'structReentrant.sol': 0,
'thisLocal.sol': 0,
'globals.sol': 0
'globals.sol': 0,
'library.sol': 0
}
runModuleOnFiles(module, t, (file, report) => {
@ -153,7 +158,8 @@ test('Integration test txOrigin.js', function (t) {
'notReentrant.sol': 0,
'structReentrant.sol': 0,
'thisLocal.sol': 0,
'globals.sol': 1
'globals.sol': 1,
'library.sol': 0
}
runModuleOnFiles(module, t, (file, report) => {
@ -179,7 +185,8 @@ test('Integration test gasCosts.js', function (t) {
'notReentrant.sol': 1,
'structReentrant.sol': 1,
'thisLocal.sol': 2,
'globals.sol': 1
'globals.sol': 1,
'library.sol': 1
}
runModuleOnFiles(module, t, (file, report) => {

@ -0,0 +1,54 @@
pragma solidity ^0.4.0;
library Set {
// We define a new struct datatype that will be used to
// hold its data in the calling contract.
struct Data { mapping(uint => bool) flags; }
// Note that the first parameter is of type "storage
// reference" and thus only its storage address and not
// its contents is passed as part of the call. This is a
// special feature of library functions. It is idiomatic
// to call the first parameter 'self', if the function can
// be seen as a method of that object.
function insert(Data storage self, uint value)
returns (bool)
{
if (self.flags[value])
return false; // already there
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
returns (bool)
{
if (!self.flags[value])
return false; // not there
self.flags[value] = false;
return true;
}
function contains(Data storage self, uint value)
returns (bool)
{
return self.flags[value];
}
}
contract C {
Set.Data knownValues;
function register(uint value) {
// The library functions can be called without a
// specific instance of the library, since the
// "instance" will be the current contract.
address a;
a.send(10 wei);
if (!Set.insert(knownValues, value))
throw;
}
// In this contract, we can also directly access knownValues.flags, if we want.
}
Loading…
Cancel
Save