Commit 5e61196a authored by chriseth's avatar chriseth Committed by GitHub

Merge pull request #484 from soad003/master

Static analysis, this on local calls.
parents eca5243d ff3db096
module.exports = [
require('./txOrigin'),
require('./gasCosts')
require('./gasCosts'),
require('./thisLocal')
]
'use strict'
var utils = require('../../utils')
var nodeTypes = {
IDENTIFIER: 'Identifier',
MEMBERACCESS: 'MemberAccess',
FUNCTIONCALL: 'FunctionCall'
}
var basicTypes = {
UINT: 'uint256',
BOOL: 'bool',
ADDRESS: 'address',
BYTES32: 'bytes32'
}
var basicRegex = {
CONTRACTTYPE: '^contract ',
FUNCTIONTYPE: '^function \\('
}
var basicFunctionTypes = {
SEND: buildFunctionSignature([basicTypes.UINT], [basicTypes.BOOL], false),
CALL: buildFunctionSignature([], [basicTypes.BOOL], true),
DELEGATECALL: buildFunctionSignature([], [basicTypes.BOOL], false)
}
var lowLevelCallTypes = {
CALL: { ident: 'call', type: basicFunctionTypes.CALL },
CALLCODE: { ident: 'callcode', type: basicFunctionTypes.CALL },
DELEGATECALL: { ident: 'delegatecall', type: basicFunctionTypes.DELEGATECALL },
SEND: { ident: 'send', type: basicFunctionTypes.SEND }
}
var specialVariables = {
BLOCKTIMESTAMP: { obj: 'block', member: 'timestamp', type: basicTypes.UINT },
BLOCKHASH: {
obj: 'block',
member: 'blockhash',
type: buildFunctionSignature([basicTypes.UINT], [basicTypes.BYTES32], false)
}
}
// usage of now special variable
function isNowAccess (node) {
return nodeType(node, nodeTypes.IDENTIFIER) &&
expressionType(node, basicTypes.UINT) &&
name(node, 'now')
}
// usage of block timestamp
function isBlockTimestampAccess (node) {
return isSpecialVariableAccess(node, specialVariables.BLOCKTIMESTAMP)
}
function isSpecialVariableAccess (node, varType) {
return isMemberAccess(node, varType.type, varType.obj, varType.obj, varType.member)
}
function isThisLocalCall (node) {
return isMemberAccess(node, basicRegex.FUNCTIONTYPE, 'this', basicRegex.CONTRACTTYPE, undefined)
}
function isLowLevelCall (node) {
return isLLCall(node) ||
isLLCallcode(node) ||
isLLDelegatecall(node) ||
isLLSend(node)
}
function isLLSend (node) {
return isMemberAccess(node,
utils.escapeRegExp(lowLevelCallTypes.SEND.type),
undefined, basicTypes.ADDRESS, lowLevelCallTypes.SEND.ident)
}
function isLLCall (node) {
return isMemberAccess(node,
utils.escapeRegExp(lowLevelCallTypes.CALL.type),
undefined, basicTypes.ADDRESS, lowLevelCallTypes.CALL.ident)
}
function isLLCallcode (node) {
return isMemberAccess(node,
utils.escapeRegExp(lowLevelCallTypes.CALLCODE.type),
undefined, basicTypes.ADDRESS, lowLevelCallTypes.CALLCODE.ident)
}
function isLLDelegatecall (node) {
return isMemberAccess(node,
utils.escapeRegExp(lowLevelCallTypes.DELEGATECALL.type),
undefined, basicTypes.ADDRESS, lowLevelCallTypes.DELEGATECALL.ident)
}
function isMemberAccess (node, retType, accessor, accessorType, memberName) {
return nodeType(node, nodeTypes.MEMBERACCESS) &&
expressionType(node, retType) &&
name(node, memberName) &&
nrOfChildren(node, 1) &&
name(node.children[0], accessor) &&
expressionType(node.children[0], accessorType)
}
function nrOfChildren (node, nr) {
return (node && (nr === undefined || nr === null)) || (node && node.children && node.children.length === nr)
}
function expressionType (node, typeRegex) {
return (node && !typeRegex) || (node && node.attributes && new RegExp(typeRegex).test(node.attributes.type))
}
function nodeType (node, typeRegex) {
return (node && !typeRegex) || (node && new RegExp(typeRegex).test(node.name))
}
function name (node, nameRegex) {
var regex = new RegExp(nameRegex)
return (node && !nameRegex) || (node && node.attributes && (regex.test(node.attributes.value) || regex.test(node.attributes.member_name)))
}
/**
* Builds an function signature as used in the AST of the solc-json AST
* @param {Array} paramTypes
* list of parameter type names
* @param {Array} returnTypes
* list of return type names
* @return {Boolean} isPayable
* CAUTION: only needed in low level call signature or message-calls (other contracts, this.)
*/
function buildFunctionSignature (paramTypes, returnTypes, isPayable) {
return 'function (' + utils.concatWithSeperator(paramTypes, ',') + ')' + ((isPayable) ? ' payable' : '') + ((returnTypes.length) ? ' returns (' + utils.concatWithSeperator(returnTypes, ',') + ')' : '')
}
module.exports = {
isNowAccess: isNowAccess,
isBlockTimestampAccess: isBlockTimestampAccess,
isThisLocalCall: isThisLocalCall,
isLowLevelCall: isLowLevelCall,
isLowLevelCallInst: isLLCall,
isLowLevelCallcodeInst: isLLCallcode,
isLowLevelDelegatecallInst: isLLDelegatecall,
isLowLevelSendInst: isLLSend,
nodeTypes: nodeTypes,
basicTypes: basicTypes,
basicFunctionTypes: basicFunctionTypes,
lowLevelCallTypes: lowLevelCallTypes,
specialVariables: specialVariables,
helpers: {
nrOfChildren: nrOfChildren,
expressionType: expressionType,
nodeType: nodeType,
name: name,
buildFunctionSignature: buildFunctionSignature
}
}
var name = 'this on local'
var desc = 'Invocation of local functions via this'
var categories = require('./categories')
var common = require('./staticAnalysisCommon')
function thisLocal () {
this.warningNodes = []
}
thisLocal.prototype.visit = function (node) {
if (common.isThisLocalCall(node)) this.warningNodes.push(node)
}
thisLocal.prototype.report = function (compilationResults) {
this.warningNowNodes = []
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,
Module: thisLocal
}
......@@ -77,7 +77,7 @@ staticAnalysisView.prototype.run = function () {
location = self.appAPI.offsetToLineColumn(location, file)
location = self.lastCompilationResult.sourceList[file] + ':' + (location.start.line + 1) + ':' + (location.start.column + 1) + ':'
}
self.appAPI.renderWarning(location + ' ' + item.warning, warningContainer, {type: 'warning', useSpan: true, isHTML: true})
self.appAPI.renderWarning(location + ' ' + item.warning + ((item.more) ? '<br><a href="' + item.more + '" target="blank">more</a>' : ''), warningContainer, {type: 'warning', useSpan: true, isHTML: true})
})
})
if (warningContainer.html() === '') {
......
......@@ -14,7 +14,17 @@ function groupBy (arr, key) {
}, {})
}
function concatWithSeperator (list, seperator) {
return list.reduce((sum, item) => sum + item + seperator, '').slice(0, -seperator.length)
}
function escapeRegExp (str) {
return str.replace(/[-[\]/{}()+?.\\^$|]/g, '\\$&')
}
module.exports = {
errortype: errortype,
groupBy: groupBy
groupBy: groupBy,
concatWithSeperator: concatWithSeperator,
escapeRegExp: escapeRegExp
}
......@@ -4,3 +4,4 @@ require('./compiler-test')
require('./gist-handler-test')
require('./query-params-test')
require('./util-test')
require('./staticanalysis/staticAnalysisCommon-test')
var test = require('tape')
var common = require('../../babelify-src/app/staticanalysis/modules/staticAnalysisCommon')
var utils = require('../../babelify-src/app/utils')
test('staticAnalysisCommon.helpers.buildFunctionSignature', function (t) {
t.plan(7)
t.equal(common.helpers.buildFunctionSignature([common.basicTypes.UINT, common.basicTypes.ADDRESS], [common.basicTypes.BOOL], false),
'function (uint256,address) returns (bool)',
'two params and return value without payable')
t.equal(common.helpers.buildFunctionSignature([common.basicTypes.UINT, common.basicTypes.BYTES32, common.basicTypes.BYTES32], [], true),
'function (uint256,bytes32,bytes32) payable',
'three params and no return with payable')
t.equal(common.helpers.buildFunctionSignature([common.basicTypes.BOOL], [common.basicTypes.BYTES32, common.basicTypes.ADDRESS], true),
'function (bool) payable returns (bytes32,address)',
'one param and two return values with payable')
t.equal(common.lowLevelCallTypes.CALL.type,
'function () payable returns (bool)',
'check fixed call type')
t.equal(common.lowLevelCallTypes.CALLCODE.type,
'function () payable returns (bool)',
'check fixed callcode type')
t.equal(common.lowLevelCallTypes.SEND.type,
'function (uint256) returns (bool)',
'check fixed send type')
t.equal(common.lowLevelCallTypes.DELEGATECALL.type,
'function () returns (bool)',
'check fixed call type')
})
test('staticAnalysisCommon.helpers.name', function (t) {
t.plan(9)
var node = { attributes: { value: 'now' } }
var node2 = { attributes: { member_name: 'call' } }
t.ok(common.helpers.name(node, 'now'), 'should work for values')
t.ok(common.helpers.name(node2, 'call'), 'should work for member_name')
t.ok(common.helpers.name(node2, '.all'), 'regex should work')
lowlevelAccessersCommon(t, common.helpers.name, node)
})
test('staticAnalysisCommon.helpers.nodeType', function (t) {
t.plan(9)
var node = { name: 'Identifier', attributes: { name: 'now' } }
var node2 = { name: 'FunctionCall', attributes: { member_name: 'call' } }
t.ok(common.helpers.nodeType(node, common.nodeTypes.IDENTIFIER), 'should work for ident')
t.ok(common.helpers.nodeType(node2, common.nodeTypes.FUNCTIONCALL), 'should work for funcall')
t.ok(common.helpers.nodeType(node2, '^F'), 'regex should work for funcall')
lowlevelAccessersCommon(t, common.helpers.nodeType, node)
})
test('staticAnalysisCommon.helpers.expressionType', function (t) {
t.plan(9)
var node = { name: 'Identifier', attributes: { value: 'now', type: 'uint256' } }
var node2 = { name: 'FunctionCall', attributes: { member_name: 'call', type: 'function () payable returns (bool)' } }
t.ok(common.helpers.expressionType(node, common.basicTypes.UINT), 'should work for ident')
t.ok(common.helpers.expressionType(node2, utils.escapeRegExp(common.basicFunctionTypes.CALL)), 'should work for funcall')
t.ok(common.helpers.expressionType(node2, '^function \\('), 'regex should work')
lowlevelAccessersCommon(t, common.helpers.expressionType, node)
})
test('staticAnalysisCommon.helpers.nrOfChildren', function (t) {
t.plan(10)
var node = { name: 'Identifier', children: ['a', 'b'], attributes: { value: 'now', type: 'uint256' } }
var node2 = { name: 'FunctionCall', children: [], attributes: { member_name: 'call', type: 'function () payable returns (bool)' } }
var node3 = { name: 'FunctionCall', attributes: { member_name: 'call', type: 'function () payable returns (bool)' } }
t.ok(common.helpers.nrOfChildren(node, 2), 'should work for 2 children')
t.notOk(common.helpers.nrOfChildren(node, '1+2'), 'regex should not work')
t.ok(common.helpers.nrOfChildren(node2, 0), 'should work for 0 children')
t.notOk(common.helpers.nrOfChildren(node3, 0), 'should not work without children arr')
lowlevelAccessersCommon(t, common.helpers.nrOfChildren, node)
})
function lowlevelAccessersCommon (t, f, someNode) {
t.ok(f(someNode), 'always ok if type is undefinded')
t.ok(f(someNode, undefined), 'always ok if name is undefinded 2')
t.notOk(f(null, undefined), 'false on no node')
t.notOk(f(null, 'call'), 'false on no node')
t.notOk(f(undefined, null), 'false on no node')
t.notOk(f(), 'false on no params')
}
test('staticAnalysisCommon.helpers.isLowLevelCall', function (t) {
t.plan(4)
var sendAst = { name: 'MemberAccess', children: [{attributes: { value: 'd', type: 'address' }}], attributes: { value: 'send', type: 'function (uint256) returns (bool)' } }
var callAst = { name: 'MemberAccess', children: [{attributes: { value: 'f', type: 'address' }}], attributes: { member_name: 'call', type: 'function () payable returns (bool)' } }
var callcodeAst = { name: 'MemberAccess', children: [{attributes: { value: 'f', type: 'address' }}], attributes: { member_name: 'callcode', type: 'function () payable returns (bool)' } }
var delegatecallAst = { name: 'MemberAccess', children: [{attributes: { value: 'g', type: 'address' }}], attributes: { member_name: 'delegatecall', type: 'function () returns (bool)' } }
t.ok(common.isLowLevelSendInst(sendAst) && common.isLowLevelCall(sendAst), 'send is llc should work')
t.ok(common.isLowLevelCallInst(callAst) && common.isLowLevelCall(callAst), 'call is llc should work')
t.ok(common.isLowLevelCallcodeInst(callcodeAst) && common.isLowLevelCall(callcodeAst), 'callcode is llc should work')
t.ok(common.isLowLevelDelegatecallInst(delegatecallAst) && common.isLowLevelCall(delegatecallAst), 'delegatecall is llc should work')
})
test('staticAnalysisCommon.helpers.isThisLocalCall', function (t) {
t.plan(3)
var node = { name: 'MemberAccess', children: [{attributes: { value: 'this', type: 'contract test' }}], attributes: { value: 'b', type: 'function (bytes32,address) returns (bool)' } }
t.ok(common.isThisLocalCall(node), 'is this.local_method() used should 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.helpers.isBlockTimestampAccess', function (t) {
t.plan(3)
var node = { name: 'MemberAccess', children: [{attributes: { value: 'block', type: 'block' }}], attributes: { value: 'timestamp', type: 'uint256' } }
t.notOk(common.isThisLocalCall(node), 'is this.local_method() used should not work')
t.ok(common.isBlockTimestampAccess(node), 'is block.timestamp used should work')
t.notOk(common.isNowAccess(node), 'is now used should not work')
})
test('staticAnalysisCommon.helpers.isNowAccess', function (t) {
t.plan(3)
var node = { name: 'Identifier', attributes: { value: 'now', type: 'uint256' } }
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.ok(common.isNowAccess(node), 'is now used should work')
})
......@@ -24,3 +24,19 @@ test('util.groupBy on valid input', function (t) {
t.deepEqual(result, expectedResult)
})
test('util.concatWithSeperator valid output', function (t) {
t.plan(4)
t.notEqual(utils.concatWithSeperator(['a', 'b', 'c'], ','), 'a, b, c', 'Concat with comma should not produce spaces')
t.equal(utils.concatWithSeperator(['a', 'b', 'c'], ','), 'a,b,c', 'Concat with comma should not produce spaces')
t.equal(utils.concatWithSeperator(['a', 'b', 'c'], ', '), 'a, b, c', 'Concat with comma space should not produce trailing comma')
t.equal(utils.concatWithSeperator(['a', 'b', 'c'], '+'), 'a+b+c', 'Concat with plus')
})
test('util.escapeRegExp', function (t) {
t.plan(3)
var original = 'function (uint256) returns (bool)'
t.equal(utils.escapeRegExp('abcd'), 'abcd', 'String with no regex')
t.equal(utils.escapeRegExp(original), 'function \\(uint256\\) returns \\(bool\\)', 'function string with regex')
t.ok(new RegExp(utils.escapeRegExp(original)).test(original), 'should still test for original string')
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment