Skip to content
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

Revokable contract with Revoker Role #2037

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions contracts/access/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ NOTE: This page is incomplete. We're working to improve it for the next release.

{{PauserRole}}

{{RevokerRole}}

{{SignerRole}}

{{WhitelistAdminRole}}
Expand Down
44 changes: 44 additions & 0 deletions contracts/access/roles/RevokerRole.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
pragma solidity ^0.5.0;

import "../../GSN/Context.sol";
import "../Roles.sol";

contract RevokerRole is Context {
using Roles for Roles.Role;

event RevokerAdded(address indexed account);
event RevokerRemoved(address indexed account);

Roles.Role private _revokers;

constructor () internal {
_addRevoker(_msgSender());
}

modifier onlyRevoker() {
require(isRevoker(_msgSender()), "RevokerRole: caller does not have the Revoker role");
_;
}

function isRevoker(address account) public view returns (bool) {
return _revokers.has(account);
}

function addRevoker(address account) public onlyRevoker {
_addRevoker(account);
}

function renounceRevoker() public {
_removeRevoker(_msgSender());
}

function _addRevoker(address account) internal {
_revokers.add(account);
emit RevokerAdded(account);
}

function _removeRevoker(address account) internal {
_revokers.remove(account);
emit RevokerRemoved(account);
}
}
4 changes: 2 additions & 2 deletions contracts/lifecycle/Pausable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import "../GSN/Context.sol";
import "../access/roles/PauserRole.sol";

/**
* @dev Contract module which allows children to implement an emergency stop
* mechanism that can be triggered by an authorized account.
* @dev Contract module which allows children to implement an emergency pause
* mechanism that can be triggered and undone by an authorized account.
*
* This module is used through inheritance. It will make available the
* modifiers `whenNotPaused` and `whenPaused`, which can be applied to
Expand Down
4 changes: 4 additions & 0 deletions contracts/lifecycle/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
== Pausable

{{Pausable}}

== Revokable

{{Revokable}}
54 changes: 54 additions & 0 deletions contracts/lifecycle/Revokable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
pragma solidity ^0.5.0;

import "../GSN/Context.sol";
import "../access/roles/RevokerRole.sol";

/**
* @dev Contract module which allows children to implement an emergency stop
* mechanism that can be triggered by an authorized account, to permanently
* disable certain contract functions.
*
* This module is used through inheritance. It will make available the
* modifier `whenNotRevoked`, which can be applied to
* the functions of your contract. Note that they will not be revokable by
* simply including this module, only once the modifier are put in place.
*/
contract Revokable is Context, RevokerRole {
/**
* @dev Emitted when the revoke is triggered by a revoker (`account`).
*/
event Revoked(address account);

bool private _revoked;

/**
* @dev Initializes the contract in not revoked state. Assigns the Revoker role
* to the deployer.
*/
constructor () internal {
_revoked = false;
}

/**
* @dev Returns true if the contract is revoked, and false otherwise.
*/
function revoked() public view returns (bool) {
return _revoked;
}

/**
* @dev Modifier to make a function callable only when the contract is not revoked.
*/
modifier whenNotRevoked() {
require(!_revoked, "Revokable: revoked");
_;
}

/**
* @dev Called by a revoker to revoke, triggers revoked state.
*/
function revoke() public onlyRevoker whenNotRevoked {
_revoked = true;
emit Revoked(_msgSender());
}
}
23 changes: 23 additions & 0 deletions contracts/mocks/RevokableMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
pragma solidity ^0.5.0;

import "../lifecycle/Revokable.sol";
import "./RevokerRoleMock.sol";

// mock class using Revokable
contract RevokableMock is Revokable, RevokerRoleMock {
bool public drasticMeasureTaken;
uint256 public count;

constructor () public {
drasticMeasureTaken = false;
count = 0;
}

function normalProcess() external whenNotRevoked {
count++;
}

function drasticMeasure() external whenPaused {
drasticMeasureTaken = true;
}
}
18 changes: 18 additions & 0 deletions contracts/mocks/RevokerRoleMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
pragma solidity ^0.5.0;

import "../access/roles/RevokerRole.sol";

contract RevokerRoleMock is RevokerRole {
function removeRevoker(address account) public {
_removeRevoker(account);
}

function onlyRevokerMock() public view onlyRevoker {
// solhint-disable-previous-line no-empty-blocks
}

// Causes a compilation error if super._removeRevoker is not internal
function _removeRevoker(address account) internal {
super._removeRevoker(account);
}
}
15 changes: 15 additions & 0 deletions test/access/roles/RevokerRole.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const { accounts, contract } = require('@openzeppelin/test-environment');

const { shouldBehaveLikePublicRole } = require('../../behaviors/access/roles/PublicRole.behavior');
const RevokerRoleMock = contract.fromArtifact('RevokerRoleMock');

describe('RevokerRole', function () {
const [ revoker, otherRevoker, ...otherAccounts ] = accounts;

beforeEach(async function () {
this.contract = await RevokerRoleMock.new({ from: revoker });
await this.contract.addRevoker(otherRevoker, { from: revoker });
});

shouldBehaveLikePublicRole(revoker, otherRevoker, otherAccounts, 'revoker');
});
74 changes: 74 additions & 0 deletions test/lifecycle/Revokable.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const { accounts, contract } = require('@openzeppelin/test-environment');

const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
const { shouldBehaveLikePublicRole } = require('../behaviors/access/roles/PublicRole.behavior');

const { expect } = require('chai');

const RevokableMock = contract.fromArtifact('RevokableMock');

describe('Revokable', function () {
const [ revoker, otherRevoker, other, ...otherAccounts ] = accounts;

beforeEach(async function () {
this.revokable = await RevokableMock.new({ from: revoker });
});

describe('revoker role', function () {
beforeEach(async function () {
this.contract = this.revokable;
await this.contract.addRevoker(otherRevoker, { from: revoker });
});

shouldBehaveLikePublicRole(revoker, otherRevoker, otherAccounts, 'revoker');
});

context('when not revoked', function () {
beforeEach(async function () {
expect(await this.revokable.revoked()).to.equal(false);
});

it('can perform normal process in non-revoked', async function () {
expect(await this.revokable.count()).to.be.bignumber.equal('0');

await this.revokable.normalProcess({ from: other });
expect(await this.revokable.count()).to.be.bignumber.equal('1');
});

describe('revoking', function () {
it('is revokable by the revoker', async function () {
await this.revokable.revoke({ from: revoker });
expect(await this.revokable.revoked()).to.equal(true);
});

it('reverts when revoking from non-revoker', async function () {
await expectRevert(this.revokable.revoke({ from: other }),
'RevokerRole: caller does not have the Revoker role'
);
});

context('when revoked', function () {
beforeEach(async function () {
({ logs: this.logs } = await this.revokable.revoke({ from: revoker }));
});

it('emits a Revoked event', function () {
expectEvent.inLogs(this.logs, 'Revoked', { account: revoker });
});

it('cannot perform normal process while revoked', async function () {
await expectRevert(this.revokable.normalProcess({ from: other }), 'Revokable: revoked');
});

it('can take a drastic measure while revoked', async function () {
await this.revokable.drasticMeasure({ from: other });
expect(await this.revokable.drasticMeasureTaken()).to.equal(true);
});

it('reverts when re-revoking', async function () {
await expectRevert(this.revokable.revoke({ from: revoker }), 'Revokable: revoked');
});
});
});
});
});