Skip to content

Commit

Permalink
Implement reentrancy error validation rule #7
Browse files Browse the repository at this point in the history
  • Loading branch information
idrabenia committed Oct 17, 2017
1 parent d543da9 commit d44af05
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 26 deletions.
1 change: 0 additions & 1 deletion lib/comment-directive-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ class CommentDirectiveParser {
isRuleEnabled(line, ruleId) {
return this.ruleStore.isRuleEnabled(line, ruleId);
}

}


Expand Down
11 changes: 11 additions & 0 deletions lib/common/tree-traversing.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,15 @@ TreeTraversing.hasMethodCalls = function (ctx, methodNames) {
};


TreeTraversing.findPropertyInParents = function (ctx, property) {
let curCtx = ctx;

while (curCtx !== null && !curCtx[property]) {
curCtx = curCtx.parentCtx;
}

return curCtx && curCtx[property];
};


module.exports = TreeTraversing;
101 changes: 89 additions & 12 deletions lib/rules/security/reentrancy.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const BaseChecker = require('./../base-checker');
const _ = require('lodash');
const { hasMethodCalls } = require('./../../common/tree-traversing');
const TreeTraversing = require('./../../common/tree-traversing');
const { typeOf, hasMethodCalls, findPropertyInParents } = TreeTraversing;


class ReentrancyChecker extends BaseChecker {
Expand All @@ -9,8 +10,17 @@ class ReentrancyChecker extends BaseChecker {
super(reporter);
}

enterContractDefinition(ctx) {
ctx.stateDeclarationScope = new StateDeclarationScope();
const scope = ctx.stateDeclarationScope;

new ContractDefinition(ctx)
.stateDefinitions()
.forEach(i => scope.trackStateDeclaration(i));
}

enterFunctionDefinition(ctx) {
ctx.effects = new Effects();
ctx.effects = new Effects(StateDeclarationScope.of(ctx));
}

enterExpression(ctx) {
Expand All @@ -21,7 +31,7 @@ class ReentrancyChecker extends BaseChecker {
_checkAssignment(ctx) {
const effects = Effects.of(ctx);

if (isAssignOperator(ctx) && effects && !effects.isAllowedAssign()) {
if (isAssignOperator(ctx) && effects && !effects.isAllowedAssign(ctx)) {
this._warn(ctx);
}
}
Expand All @@ -38,29 +48,96 @@ class ReentrancyChecker extends BaseChecker {
}


class Effects {
class StateDeclarationScope {

static of (ctx) {
let curCtx = ctx;
return findPropertyInParents(ctx, 'stateDeclarationScope');
}

while (curCtx !== null && !curCtx.effects) {
curCtx = curCtx.parentCtx;
}
constructor () {
this.states = [];
}

return curCtx.effects;
trackStateDeclaration (stateDefinition) {
const stateName = stateDefinition.stateName();
this.states.push(stateName);
}
}

constructor () {

class ContractDefinition {

constructor (ctx) {
this.ctx = ctx;
}

stateDefinitions() {
return this.ctx
.children
.map(i => new ContractPart(i))
.filter(i => i.isStateDefinition())
.map(i => i.getStateDefinition());
}
}


class ContractPart {

constructor (ctx) {
this.ctx = ctx;
}

isStateDefinition() {
return typeOf(this._firstChild()) === 'stateVariableDeclaration';
}

getStateDefinition () {
return new StateDefinition(this._firstChild());
}

_firstChild () {
return _.first(this.ctx.children);
}
}

class StateDefinition {

constructor(ctx) {
this.ctx = ctx;
}

stateName() {
return _(this.ctx.children)
.find(i => typeOf(i) === 'identifier')
.getText();
}
}


class Effects {

static of (ctx) {
return findPropertyInParents(ctx, 'effects');
}

constructor (statesScope) {
this.states = statesScope && statesScope.states;
this.hasTransfer = false;
}

isAllowedAssign() {
return !this.hasTransfer;
isAllowedAssign(ctx) {
const assignee = ctx.children[0].getText();

return !(this.hasTransfer && this._isContainsStateName(assignee));
}

trackTransfer() {
this.hasTransfer = true;
}

_isContainsStateName(expressionText) {
return this.states.some(i => expressionText.includes(i));
}
}


Expand Down
48 changes: 35 additions & 13 deletions test/security-rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,15 +185,23 @@ describe('Linter - SecurityRules', function() {
describe('Reentrancy', function () {

const REENTRANCY_ERROR = [
funcWith(`
uint amount = shares[msg.sender];
bool a = msg.sender.send(amount);
if (a) { shares[msg.sender] = 0; }
contractWith(`
mapping(address => uint) private shares;
function b() external {
uint amount = shares[msg.sender];
bool a = msg.sender.send(amount);
if (a) { shares[msg.sender] = 0; }
}
`),
funcWith(`
uint amount = shares[msg.sender];
msg.sender.transfer(amount);
shares[msg.sender] = 0;
contractWith(`
mapping(address => uint) private shares;
function b() external {
uint amount = shares[msg.sender];
msg.sender.transfer(amount);
shares[msg.sender] = 0;
}
`)
];

Expand All @@ -207,15 +215,29 @@ describe('Linter - SecurityRules', function() {
);

const NO_REENTRANCY_ERRORS = [
funcWith(`
uint amount = shares[msg.sender];
user.test(amount);
shares[msg.sender] = 0;
contractWith(`
mapping(address => uint) private shares;
function b() external {
uint amount = shares[msg.sender];
shares[msg.sender] = 0;
msg.sender.transfer(amount);
}
`),
contractWith(`
mapping(address => uint) private shares;
function b() external {
uint amount = shares[msg.sender];
user.test(amount);
shares[msg.sender] = 0;
}
`),
funcWith(`
uint[] shares;
uint amount = shares[msg.sender];
shares[msg.sender] = 0;
msg.sender.transfer(amount);
shares[msg.sender] = 0;
`)
];

Expand Down

0 comments on commit d44af05

Please sign in to comment.