New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add ReentrancyGuard #503
Merged
Merged
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
7542b4e
feat: add ReentrancyGuard
sohkai cf07f35
test: add storage position test for ReentrancyGuard
sohkai 884cbeb
test: add unstructured storage test for ReentrancyGuard
sohkai 6d7be74
ReentrancyGuard: add tests to ensure mutex always ends false
sohkai File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/* | ||
* SPDX-License-Identitifer: MIT | ||
*/ | ||
|
||
pragma solidity ^0.4.24; | ||
|
||
import "../common/UnstructuredStorage.sol"; | ||
|
||
|
||
contract ReentrancyGuard { | ||
using UnstructuredStorage for bytes32; | ||
|
||
/* Hardcoded constants to save gas | ||
bytes32 internal constant REENTRANCY_MUTEX_POSITION = keccak256("aragonOS.reentrancyGuard.mutex"); | ||
*/ | ||
bytes32 private constant REENTRANCY_MUTEX_POSITION = 0xe855346402235fdd185c890e68d2c4ecad599b88587635ee285bce2fda58dacb; | ||
|
||
string private constant ERROR_REENTRANT = "REENTRANCY_REENTRANT_CALL"; | ||
|
||
modifier nonReentrant() { | ||
// Ensure mutex is unlocked | ||
require(!REENTRANCY_MUTEX_POSITION.getStorageBool(), ERROR_REENTRANT); | ||
|
||
// Lock mutex before function call | ||
REENTRANCY_MUTEX_POSITION.setStorageBool(true); | ||
|
||
// Perform function call | ||
_; | ||
|
||
// Unlock mutex after function call | ||
REENTRANCY_MUTEX_POSITION.setStorageBool(false); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
pragma solidity 0.4.24; | ||
|
||
import "../../common/ReentrancyGuard.sol"; | ||
import "../../common/UnstructuredStorage.sol"; | ||
|
||
|
||
contract ReentrantActor { | ||
bool reenterNonReentrant; | ||
|
||
constructor(bool _reenterNonReentrant) public { | ||
reenterNonReentrant = _reenterNonReentrant; | ||
} | ||
|
||
function reenter(ReentrancyGuardMock _mock) public { | ||
// Set the reentrancy target to 0 so we don't infinite loop | ||
ReentrantActor reentrancyTarget = ReentrantActor(0); | ||
|
||
if (reenterNonReentrant) { | ||
_mock.nonReentrantCall(reentrancyTarget); | ||
} else { | ||
_mock.reentrantCall(reentrancyTarget); | ||
} | ||
} | ||
} | ||
|
||
|
||
contract ReentrancyGuardMock is ReentrancyGuard { | ||
using UnstructuredStorage for bytes32; | ||
|
||
uint256 public callCounter; | ||
|
||
function nonReentrantCall(ReentrantActor _target) public nonReentrant { | ||
callCounter++; | ||
if (_target != address(0)) { | ||
_target.reenter(this); | ||
} | ||
} | ||
|
||
function reentrantCall(ReentrantActor _target) public { | ||
callCounter++; | ||
if (_target != address(0)) { | ||
_target.reenter(this); | ||
} | ||
} | ||
|
||
function setReentrancyMutex(bool _mutex) public { | ||
getReentrancyMutexPosition().setStorageBool(_mutex); | ||
} | ||
|
||
function getReentrancyMutexPosition() public pure returns (bytes32) { | ||
return keccak256("aragonOS.reentrancyGuard.mutex"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
const { assertRevert } = require('./helpers/assertThrow') | ||
|
||
const ZERO_ADDR = '0x0000000000000000000000000000000000000000' | ||
|
||
contract('ReentrancyGuard', accounts => { | ||
let reentrancyMock | ||
|
||
beforeEach(async () => { | ||
reentrancyMock = await artifacts.require('ReentrancyGuardMock').new() | ||
}) | ||
|
||
it('starts with false mutex', async () => { | ||
assert.equal( | ||
await web3.eth.getStorageAt(reentrancyMock.address, (await reentrancyMock.getReentrancyMutexPosition())), | ||
false, | ||
're-entrancy guard should start false' | ||
) | ||
}) | ||
|
||
it('starts with no calls', async () => { | ||
assert.equal((await reentrancyMock.callCounter()).toString(), 0, 'should start with no calls') | ||
}) | ||
|
||
it('can call re-entrant function normally', async () => { | ||
await reentrancyMock.reentrantCall(ZERO_ADDR) | ||
assert.equal((await reentrancyMock.callCounter()).toString(), 1, 'should have registered one call') | ||
}) | ||
|
||
it('can call non-re-entrant function normally', async () => { | ||
await reentrancyMock.nonReentrantCall(ZERO_ADDR) | ||
assert.equal((await reentrancyMock.callCounter()).toString(), 1, 'should have registered one call') | ||
}) | ||
|
||
context('> Enabled re-entrancy guard', () => { | ||
beforeEach(async () => { | ||
// Manually set re-entrancy guard | ||
await reentrancyMock.setReentrancyMutex(true) | ||
}) | ||
|
||
it('can call re-entrant function if re-entrancy guard is enabled', async () => { | ||
await reentrancyMock.reentrantCall(ZERO_ADDR) | ||
assert.equal((await reentrancyMock.callCounter()).toString(), 1, 'should have called') | ||
}) | ||
|
||
it('can not call non-re-entrant function if re-entrancy guard is enabled', async () => { | ||
await assertRevert(async () => { | ||
await reentrancyMock.nonReentrantCall(ZERO_ADDR) | ||
}) | ||
assert.equal((await reentrancyMock.callCounter()).toString(), 0, 'should not have called') | ||
}) | ||
}) | ||
|
||
context('> Re-entering through contract', async () => { | ||
let reentrantActor | ||
|
||
context('> Re-enters re-entrant call', async () => { | ||
before(async () => { | ||
reentrantActor = await artifacts.require('ReentrantActor').new(false) | ||
}) | ||
|
||
it('allows re-entering re-entrant call', async () => { | ||
await reentrancyMock.reentrantCall(reentrantActor.address) | ||
assert.equal((await reentrancyMock.callCounter()).toString(), 2, 'should have called twice') | ||
}) | ||
|
||
it('allows entering non-re-entrant call from re-entrant call', async () => { | ||
await reentrancyMock.nonReentrantCall(reentrantActor.address) | ||
assert.equal((await reentrancyMock.callCounter()).toString(), 2, 'should have called twice') | ||
}) | ||
}) | ||
|
||
context('> Re-enters non-reentrant call', async () => { | ||
before(async () => { | ||
reentrantActor = await artifacts.require('ReentrantActor').new(true) | ||
}) | ||
|
||
it('disallows re-entering non-re-entrant call', async () => { | ||
await assertRevert(async () => { | ||
await reentrancyMock.nonReentrantCall(reentrantActor.address) | ||
}) | ||
assert.equal((await reentrancyMock.callCounter()).toString(), 0, 'should not have completed any calls') | ||
}) | ||
|
||
it('allows entering non-entrant call from re-entrant call', async () => { | ||
await reentrancyMock.reentrantCall(reentrantActor.address) | ||
assert.equal((await reentrancyMock.callCounter()).toString(), 2, 'should have called twice') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to ensure consistency I'd check that the mutex variable stays in |
||
}) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not being used for testing. Maybe we could use it in
ReentrancyGuardMock
so we implicitly test that it is correct.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, forgot to add this to the keccak constants test. Add now in cf07f35.