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 13 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
57 changes: 54 additions & 3 deletions contracts/governance/Governor.sol
Expand Up @@ -7,6 +7,7 @@ import "../utils/cryptography/ECDSA.sol";
import "../utils/cryptography/draft-EIP712.sol";
import "../utils/introspection/ERC165.sol";
import "../utils/math/SafeCast.sol";
import "../utils/structs/DoubleEndedQueue.sol";
import "../utils/Address.sol";
import "../utils/Context.sol";
import "../utils/Timers.sol";
Expand All @@ -24,6 +25,7 @@ import "./IGovernor.sol";
* _Available since v4.3._
*/
abstract contract Governor is Context, ERC165, EIP712, IGovernor {
using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque;
using SafeCast for uint256;
using Timers for Timers.BlockNumber;

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

mapping(uint256 => ProposalCore) private _proposals;

// This queue keeps track of the governor operating on itself. Calls to functions protected by the
// {onlyGovernance} modifier needs to be whitelisted in this queue. Whitelisting is set in {_beforeExecute},
// consummed by the {onlyGovernance} modifier and eventually reset in {_afterExecute}. This ensures that the
// execution of {onlyGovernance} protected calls can only be achieved through successful proposals.
DoubleEndedQueue.Bytes32Deque private _governanceCall;

/**
* @dev Restrict access of functions to the governance executor, which may be the Governor itself or a timelock
* contract, as specified by {_executor}. This generally means that function with this modifier must be voted on and
* executed through the governance protocol.
* @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)) {
// loop until poping the expected operation - throw if deque is empty (operation not authorized)
while (_governanceCall.popFront() != keccak256(msg.data)) {}
frangio marked this conversation as resolved.
Show resolved Hide resolved
}
_;
}

Expand Down Expand Up @@ -251,7 +262,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 @@ -273,6 +286,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.pushBack(keccak256(calldatas[i]));
}
}
}
}

/**
* @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.clear();
}
}
Amxx marked this conversation as resolved.
Show resolved Hide resolved
}
}

/**
* @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