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

Improve security of the onlyGovernance modifier #3147

Merged
merged 22 commits into from Feb 18, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
51 changes: 51 additions & 0 deletions contracts/governance/Governor.sol
Expand Up @@ -9,6 +9,7 @@ import "../utils/introspection/ERC165.sol";
import "../utils/math/SafeCast.sol";
import "../utils/Address.sol";
import "../utils/Context.sol";
import "../utils/Counters.sol";
import "../utils/Timers.sol";
import "./IGovernor.sol";

Expand All @@ -26,6 +27,7 @@ import "./IGovernor.sol";
abstract contract Governor is Context, ERC165, EIP712, IGovernor {
using SafeCast for uint256;
using Timers for Timers.BlockNumber;
using Counters for Counters.Counter;

bytes32 public constant BALLOT_TYPEHASH = keccak256("Ballot(uint256 proposalId,uint8 support)");

Expand All @@ -40,12 +42,21 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor {

mapping(uint256 => ProposalCore) private _proposals;

// This mapping keeps track of the governor operating on itself. Calls to functions protected by the
// {onlyGovernance} modifier needs to be whitelisted in this mapping. Whitelisting is set in {_beforeExecute},
// and reset in {_afterExecute}. This ensures that the execution of {onlyGovernance} protected calls can only
// be achieved through successful proposals.
mapping(bytes => Counters.Counter) private _governanceCall;
frangio marked this conversation as resolved.
Show resolved Hide resolved

/**
* @dev Restrict access to governor executing address. Some module might override the _executor function to make
* sure this modifier is consistant with the execution model.
*/
modifier onlyGovernance() {
require(_msgSender() == _executor(), "Governor: onlyGovernance");
if (_executor() != address(this)) {
_governanceCall[msg.data].decrement();
}
_;
}

Expand Down Expand Up @@ -250,7 +261,9 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor {

emit ProposalExecuted(proposalId);

_beforeExecute(proposalId, targets, values, calldatas, descriptionHash);
_execute(proposalId, targets, values, calldatas, descriptionHash);
_afterExecute(proposalId, targets, values, calldatas, descriptionHash);

return proposalId;
}
Expand All @@ -272,6 +285,44 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor {
}
}

/**
* @dev Hook before execution is trigerred.
*/
function _beforeExecute(
uint256, /* proposalId */
address[] memory targets,
uint256[] memory /* values */,
bytes[] memory calldatas,
bytes32 /*descriptionHash*/
) internal virtual {
if (_executor() != address(this)) {
for (uint256 i = 0; i < targets.length; ++i) {
if (targets[i] == address(this)) {
_governanceCall[calldatas[i]].increment();
}
}
}
}

/**
* @dev Hook after execution is trigerred.
*/
function _afterExecute(
uint256, /* proposalId */
address[] memory targets,
uint256[] memory /* values */,
bytes[] memory calldatas,
bytes32 /*descriptionHash*/
) internal virtual {
if (_executor() != address(this)) {
for (uint256 i = 0; i < targets.length; ++i) {
if (targets[i] == address(this)) {
_governanceCall[calldatas[i]].reset();
}
}
}
}

/**
* @dev Internal cancel mechanism: locks up the proposal timer, preventing it from being re-submitted. Marks it as
* canceled to allow distinguishing it from executed proposals.
Expand Down
30 changes: 28 additions & 2 deletions test/governance/extensions/GovernorTimelockControl.test.js
@@ -1,4 +1,4 @@
const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
const { constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const Enums = require('../../helpers/enums');

Expand Down Expand Up @@ -31,7 +31,7 @@ contract('GovernorTimelockControl', function (accounts) {
this.timelock = await Timelock.new(3600, [], []);
this.mock = await Governor.new(name, this.token.address, 4, 16, this.timelock.address, 0);
this.receiver = await CallReceiver.new();
// normal setup: governor is proposer, everyone is executor, timelock is its own admin
// normal setup: governor and admin are proposers, everyone is executor, timelock is its own admin
await this.timelock.grantRole(await this.timelock.PROPOSER_ROLE(), this.mock.address);
await this.timelock.grantRole(await this.timelock.PROPOSER_ROLE(), admin);
await this.timelock.grantRole(await this.timelock.EXECUTOR_ROLE(), constants.ZERO_ADDRESS);
Expand Down Expand Up @@ -338,6 +338,32 @@ contract('GovernorTimelockControl', function (accounts) {
);
});

it('protected against other proposers', async function () {
await this.timelock.schedule(
this.mock.address,
web3.utils.toWei('0'),
this.mock.contract.methods.relay(...this.call).encodeABI(),
constants.ZERO_BYTES32,
constants.ZERO_BYTES32,
3600,
{ from: admin},
);

await time.increase(3600);

await expectRevert(
this.timelock.execute(
this.mock.address,
web3.utils.toWei('0'),
this.mock.contract.methods.relay(...this.call).encodeABI(),
constants.ZERO_BYTES32,
constants.ZERO_BYTES32,
{ from: admin},
),
'TimelockController: underlying transaction reverted',
);
});

describe('using workflow', function () {
beforeEach(async function () {
this.settings = {
Expand Down