Skip to content

Commit af7ec04

Browse files
Amxxfrangio
andauthored
Improve security of the onlyGovernance modifier (OpenZeppelin#3147)
* add a protection mechanism to prevent relaying transaction that are not part of an execute operation * more accurate relay authorization * force reset the relay authorizations after executions * refactor of the onlyGovernor modifier * only whitelist when executor is not governor itself * fix lint * add private function for call permission management * use deque * fix lint * remove unecessary dependency * remove unecessary dependency * comment rephrasing * Update contracts/governance/Governor.sol Co-authored-by: Francisco Giordano <frangio.1@gmail.com> * cache keccak256(_msgData()) * use Context * lint * conditionnal clear * add test to cover queue.clear() * lint * write more extended docs for onlyGovernance * add changelog entry Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
1 parent eae2384 commit af7ec04

File tree

4 files changed

+120
-6
lines changed

4 files changed

+120
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* `EnumerableMap`: add new `AddressToUintMap` map type. ([#3150](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3150))
77
* `ERC1155`: Add a `_afterTokenTransfer` hook for improved extensibility. ([#3166](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3166))
88
* `DoubleEndedQueue`: a new data structure that supports efficient push and pop to both front and back, useful for FIFO and LIFO queues. ([#3153](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3153))
9+
* `Governor`: improved security of `onlyGovernance` modifier when using an external executor contract (e.g. a timelock) that can operate without necessarily going through the governance protocol. ([#3147](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3147))
910

1011
## 4.5.0 (2022-02-09)
1112

contracts/governance/Governor.sol

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import "../utils/cryptography/ECDSA.sol";
77
import "../utils/cryptography/draft-EIP712.sol";
88
import "../utils/introspection/ERC165.sol";
99
import "../utils/math/SafeCast.sol";
10+
import "../utils/structs/DoubleEndedQueue.sol";
1011
import "../utils/Address.sol";
1112
import "../utils/Context.sol";
1213
import "../utils/Timers.sol";
@@ -24,6 +25,7 @@ import "./IGovernor.sol";
2425
* _Available since v4.3._
2526
*/
2627
abstract contract Governor is Context, ERC165, EIP712, IGovernor {
28+
using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque;
2729
using SafeCast for uint256;
2830
using Timers for Timers.BlockNumber;
2931

@@ -40,13 +42,29 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor {
4042

4143
mapping(uint256 => ProposalCore) private _proposals;
4244

45+
// This queue keeps track of the governor operating on itself. Calls to functions protected by the
46+
// {onlyGovernance} modifier needs to be whitelisted in this queue. Whitelisting is set in {_beforeExecute},
47+
// consummed by the {onlyGovernance} modifier and eventually reset in {_afterExecute}. This ensures that the
48+
// execution of {onlyGovernance} protected calls can only be achieved through successful proposals.
49+
DoubleEndedQueue.Bytes32Deque private _governanceCall;
50+
4351
/**
44-
* @dev Restrict access of functions to the governance executor, which may be the Governor itself or a timelock
45-
* contract, as specified by {_executor}. This generally means that function with this modifier must be voted on and
46-
* executed through the governance protocol.
52+
* @dev Restricts a function so it can only be executed through governance proposals. For example, governance
53+
* parameter setters in {GovernorSettings} are protected using this modifier.
54+
*
55+
* The governance executing address may be different from the Governor's own address, for example it could be a
56+
* timelock. This can be customized by modules by overriding {_executor}. The executor is only able to invoke these
57+
* functions during the execution of the governor's {execute} function, and not under any other circumstances. Thus,
58+
* for example, additional timelock proposers are not able to change governance parameters without going through the
59+
* governance protocol (since v4.6).
4760
*/
4861
modifier onlyGovernance() {
4962
require(_msgSender() == _executor(), "Governor: onlyGovernance");
63+
if (_executor() != address(this)) {
64+
bytes32 msgDataHash = keccak256(_msgData());
65+
// loop until poping the expected operation - throw if deque is empty (operation not authorized)
66+
while (_governanceCall.popFront() != msgDataHash) {}
67+
}
5068
_;
5169
}
5270

@@ -197,7 +215,7 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor {
197215
string memory description
198216
) public virtual override returns (uint256) {
199217
require(
200-
getVotes(msg.sender, block.number - 1) >= proposalThreshold(),
218+
getVotes(_msgSender(), block.number - 1) >= proposalThreshold(),
201219
"GovernorCompatibilityBravo: proposer votes below proposal threshold"
202220
);
203221

@@ -251,7 +269,9 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor {
251269

252270
emit ProposalExecuted(proposalId);
253271

272+
_beforeExecute(proposalId, targets, values, calldatas, descriptionHash);
254273
_execute(proposalId, targets, values, calldatas, descriptionHash);
274+
_afterExecute(proposalId, targets, values, calldatas, descriptionHash);
255275

256276
return proposalId;
257277
}
@@ -273,6 +293,42 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor {
273293
}
274294
}
275295

296+
/**
297+
* @dev Hook before execution is trigerred.
298+
*/
299+
function _beforeExecute(
300+
uint256, /* proposalId */
301+
address[] memory targets,
302+
uint256[] memory, /* values */
303+
bytes[] memory calldatas,
304+
bytes32 /*descriptionHash*/
305+
) internal virtual {
306+
if (_executor() != address(this)) {
307+
for (uint256 i = 0; i < targets.length; ++i) {
308+
if (targets[i] == address(this)) {
309+
_governanceCall.pushBack(keccak256(calldatas[i]));
310+
}
311+
}
312+
}
313+
}
314+
315+
/**
316+
* @dev Hook after execution is trigerred.
317+
*/
318+
function _afterExecute(
319+
uint256, /* proposalId */
320+
address[] memory, /* targets */
321+
uint256[] memory, /* values */
322+
bytes[] memory, /* calldatas */
323+
bytes32 /*descriptionHash*/
324+
) internal virtual {
325+
if (_executor() != address(this)) {
326+
if (!_governanceCall.empty()) {
327+
_governanceCall.clear();
328+
}
329+
}
330+
}
331+
276332
/**
277333
* @dev Internal cancel mechanism: locks up the proposal timer, preventing it from being re-submitted. Marks it as
278334
* canceled to allow distinguishing it from executed proposals.

contracts/mocks/GovernorTimelockControlMock.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,6 @@ contract GovernorTimelockControlMock is
105105
function _executor() internal view virtual override(Governor, GovernorTimelockControl) returns (address) {
106106
return super._executor();
107107
}
108+
109+
function nonGovernanceFunction() external {}
108110
}

test/governance/extensions/GovernorTimelockControl.test.js

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
1+
const { constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
22
const { expect } = require('chai');
33
const Enums = require('../../helpers/enums');
44

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

341+
it('protected against other proposers', async function () {
342+
await this.timelock.schedule(
343+
this.mock.address,
344+
web3.utils.toWei('0'),
345+
this.mock.contract.methods.relay(...this.call).encodeABI(),
346+
constants.ZERO_BYTES32,
347+
constants.ZERO_BYTES32,
348+
3600,
349+
{ from: admin },
350+
);
351+
352+
await time.increase(3600);
353+
354+
await expectRevert(
355+
this.timelock.execute(
356+
this.mock.address,
357+
web3.utils.toWei('0'),
358+
this.mock.contract.methods.relay(...this.call).encodeABI(),
359+
constants.ZERO_BYTES32,
360+
constants.ZERO_BYTES32,
361+
{ from: admin },
362+
),
363+
'TimelockController: underlying transaction reverted',
364+
);
365+
});
366+
341367
describe('using workflow', function () {
342368
beforeEach(async function () {
343369
this.settings = {
@@ -461,4 +487,33 @@ contract('GovernorTimelockControl', function (accounts) {
461487
runGovernorWorkflow();
462488
});
463489
});
490+
491+
describe('clear queue of pending governor calls', function () {
492+
beforeEach(async function () {
493+
this.settings = {
494+
proposal: [
495+
[ this.mock.address ],
496+
[ web3.utils.toWei('0') ],
497+
[ this.mock.contract.methods.nonGovernanceFunction().encodeABI() ],
498+
'<proposal description>',
499+
],
500+
voters: [
501+
{ voter: voter, support: Enums.VoteType.For },
502+
],
503+
steps: {
504+
queue: { delay: 3600 },
505+
},
506+
};
507+
});
508+
509+
afterEach(async function () {
510+
expectEvent(
511+
this.receipts.execute,
512+
'ProposalExecuted',
513+
{ proposalId: this.id },
514+
);
515+
});
516+
517+
runGovernorWorkflow();
518+
});
464519
});

0 commit comments

Comments
 (0)