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

Enable timestamp-based governor with choice of timer type #3080

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
24 changes: 19 additions & 5 deletions contracts/governance/Governor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ import "./IGovernor.sol";
*/
abstract contract Governor is Context, ERC165, EIP712, IGovernor {
using SafeCast for uint256;
using Timers for Timers.BlockNumber;
using Timers for Timers.Generic;

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

struct ProposalCore {
Timers.BlockNumber voteStart;
Timers.BlockNumber voteEnd;
Timers.Generic voteStart;
Timers.Generic voteEnd;
bool executed;
bool canceled;
}
Expand Down Expand Up @@ -84,6 +84,20 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor {
return "1";
}

/**
* @dev See {IGovernor-timerType}.
*/
function timerType() public view override returns (string memory) {
return _timerType() == Timers.Type.BLOCKNUMBER ? "BlockNumber" : "Timestamp";
}

/**
* @dev Override this function to modify Governor Timer type.
*/
function _timerType() internal view virtual returns (Timers.Type) {
return Timers.Type.BLOCKNUMBER;
}

/**
* @dev See {IGovernor-hashProposal}.
*
Expand Down Expand Up @@ -212,8 +226,8 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor {
uint64 snapshot = block.number.toUint64() + votingDelay().toUint64();
uint64 deadline = snapshot + votingPeriod().toUint64();

proposal.voteStart.setDeadline(snapshot);
proposal.voteEnd.setDeadline(deadline);
proposal.voteStart.setDeadline(snapshot, _timerType());
proposal.voteEnd.setDeadline(deadline, _timerType());

emit ProposalCreated(
proposalId,
Expand Down
6 changes: 6 additions & 0 deletions contracts/governance/IGovernor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ abstract contract IGovernor is IERC165 {
*/
function version() public view virtual returns (string memory);

/**
* @notice module:core
* @dev The type of timer being used by this implementation (BlockNumber or Timestamp). Default: "BlockNumber"
*/
function timerType() public view virtual returns (string memory);

/**
* @notice module:voting
* @dev A description of the possible `support` values for {castVote} and the way these votes are counted, meant to
Expand Down
39 changes: 39 additions & 0 deletions contracts/mocks/TimersGenericImpl.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../utils/Timers.sol";

contract TimersGenericImpl {
using Timers for Timers.Generic;

Timers.Generic private _timer;

function getDeadline() public view returns (uint64) {
return _timer.getDeadline();
}

function setDeadline(uint64 generic, Timers.Type timerType) public {
_timer.setDeadline(generic, timerType);
}

function reset() public {
_timer.reset();
}

function isUnset() public view returns (bool) {
return _timer.isUnset();
}

function isStarted() public view returns (bool) {
return _timer.isStarted();
}

function isPending() public view returns (bool) {
return _timer.isPending();
}

function isExpired() public view returns (bool) {
return _timer.isExpired();
}
}
51 changes: 51 additions & 0 deletions contracts/utils/Timers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,55 @@ library Timers {
function isExpired(BlockNumber memory timer) internal view returns (bool) {
return isStarted(timer) && timer._deadline <= block.number;
}

enum Type {
TIMESTAMP,
BLOCKNUMBER
}

struct Generic {
uint64 _deadline;
Type _type;
}

function getDeadline(Generic memory timer) internal pure returns (uint64) {
return timer._deadline;
}

function setDeadline(
Generic storage timer,
uint64 deadline,
Type typeSelector
) internal {
timer._deadline = deadline;
timer._type = typeSelector;
}

function reset(Generic storage timer) internal {
timer._deadline = 0;
}

function isUnset(Generic memory timer) internal pure returns (bool) {
return timer._deadline == 0;
}

function isStarted(Generic memory timer) internal pure returns (bool) {
return timer._deadline > 0;
}

function isPending(Generic memory timer) internal view returns (bool) {
if (timer._type == Type.BLOCKNUMBER) {
return timer._deadline > block.number;
} else {
return timer._deadline > block.timestamp;
}
}

function isExpired(Generic memory timer) internal view returns (bool) {
if (timer._type == Type.BLOCKNUMBER) {
return isStarted(timer) && timer._deadline <= block.number;
} else {
return isStarted(timer) && timer._deadline <= block.timestamp;
}
}
}
7 changes: 7 additions & 0 deletions test/governance/Governor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -929,6 +929,13 @@ contract('Governor', function (accounts) {
runGovernorWorkflow();
});

describe('timers', function () {
it('timerType', async function () {
const timerType = await this.mock.timerType();
expect(timerType).to.be.equal('BlockNumber');
});
});

describe('update protected', function () {
it('setVotingDelay', async function () {
await expectRevert(this.mock.setVotingDelay('0'), 'Governor: onlyGovernance');
Expand Down
55 changes: 55 additions & 0 deletions test/utils/TimersGeneric.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const { BN, time } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');

const TimersGenericImpl = artifacts.require('TimersGenericImpl');

contract('TimersGeneric', function (accounts) {
beforeEach(async function () {
this.instance = await TimersGenericImpl.new();
this.now = await web3.eth.getBlock('latest').then(({ timestamp }) => timestamp);
});

it('unset', async function () {
expect(await this.instance.getDeadline()).to.be.bignumber.equal('0');
expect(await this.instance.isUnset()).to.be.equal(true);
expect(await this.instance.isStarted()).to.be.equal(false);
expect(await this.instance.isPending()).to.be.equal(false);
expect(await this.instance.isExpired()).to.be.equal(false);
});

it('pending', async function () {
await this.instance.setDeadline(this.now + 100, 0);
expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(this.now + 100));
expect(await this.instance.isUnset()).to.be.equal(false);
expect(await this.instance.isStarted()).to.be.equal(true);
expect(await this.instance.isPending()).to.be.equal(true);
expect(await this.instance.isExpired()).to.be.equal(false);
});

it('expired', async function () {
await this.instance.setDeadline(this.now - 100, 1);
expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(this.now - 100));
expect(await this.instance.isUnset()).to.be.equal(false);
expect(await this.instance.isStarted()).to.be.equal(true);
expect(await this.instance.isPending()).to.be.equal(true);
expect(await this.instance.isExpired()).to.be.equal(false);
});

it('reset', async function () {
await this.instance.reset();
expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(0));
expect(await this.instance.isUnset()).to.be.equal(true);
expect(await this.instance.isStarted()).to.be.equal(false);
expect(await this.instance.isPending()).to.be.equal(false);
expect(await this.instance.isExpired()).to.be.equal(false);
});

it('fast forward', async function () {
await this.instance.setDeadline(this.now + 100, 0);
expect(await this.instance.isPending()).to.be.equal(true);
expect(await this.instance.isExpired()).to.be.equal(false);
await time.increaseTo(this.now + 100);
expect(await this.instance.isPending()).to.be.equal(false);
expect(await this.instance.isExpired()).to.be.equal(true);
});
});
1 change: 1 addition & 0 deletions test/utils/introspection/SupportsInterface.behavior.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const INTERFACES = {
Governor: [
'name()',
'version()',
'timerType()',
'COUNTING_MODE()',
'hashProposal(address[],uint256[],bytes[],bytes32)',
'state(uint256)',
Expand Down