Openzeppelin Governance Module Queuing Revert Issue

I am using OpenZeppelin's Governance module along with a timelock contract to try and create a DAO with governance built in to the smart contracts following this guide https://docs.openzeppelin.com/contracts/4.x/governance

I am trying to test a proposal being voted upon, passing, queuing, and executing however once the proposal passes and I try to queue the proposal to be executed I get a generic revert error. I know the proposal succeeds as I can check the state of it after it is voted upon so I do not think that is the issue.

I believe the issue comes in from the interaction between my custom Governor contract and custom Timelock contract. When the queue function is called from the Governor contract which is inherited from the GovernorTimelockControl module from OpenZeppelin it calls the scheduleBatch function within the TimelockController module which then calls the _schedule function which tries to set the private _timestamp state mapping for the key of proposal ID and throws a revert error. However, when making the _schedule function public and calling it directly off my custom Timelock contract, it succeeds.

I'd like for the Governor to queue proposals so that the governor is the only address/entity that can queue and execute those proposals through the Timelock.

Any help or thoughts would be greatly appreciated!

:1234: Code to reproduce

Tests I am running:

import { expect, use } from 'chai'
import { describe, it, before } from 'mocha'
import { utils, BigNumber } from 'ethers'
import { MockProvider, solidity } from 'ethereum-waffle'
import { deploySpreadlyProtocol, ISpreadlyProtocol } from './deployment'

use(solidity)
// const assert = require('assert')

describe('propose & execute outcome reporter function', async () => {
  let SPREADLY_PROTOCOL: ISpreadlyProtocol
  const provider = new MockProvider()
  const [wallet, otherWallet] = provider.getWallets()
  // wallet is maker, otherwallet is taker
  // before hook is code that executes before every test (it() function)
  before(async () => {
    SPREADLY_PROTOCOL = await deploySpreadlyProtocol()
  })

  it('get call data for chaning the platform fee', async () => {
    let call_data = SPREADLY_PROTOCOL.outcomeReporterWhitelist.interface.encodeFunctionData(
      'addAddressToWhitelist',
      [otherWallet.address]
    );

    // delegate before the proposal
    await SPREADLY_PROTOCOL.spreadly.delegate(wallet.address)

    await SPREADLY_PROTOCOL.spreadlyGovernor["propose(address[],uint256[],bytes[],string)"](
      [SPREADLY_PROTOCOL.outcomeReporterWhitelist.address],
      [0],
      [call_data],
      "Add Address to Outcome Reporter Whitelist"
    );

    const descriptionHash = utils.id("Add Address to Outcome Reporter Whitelist");

    let proposal_id = await SPREADLY_PROTOCOL.spreadlyGovernor.hashProposal(
      [SPREADLY_PROTOCOL.outcomeReporterWhitelist.address],
      [0],
      [call_data],
      descriptionHash
    );

    await SPREADLY_PROTOCOL.spreadlyGovernor['castVote(uint256,uint8)'](
      proposal_id,
      BigNumber.from('1'),
      {
        gasPrice: provider.getGasPrice(),
        gasLimit: 100000,
        nonce: 19
      }
    );  

    // await provider.send('evm_increaseTime', [86400752 * 24 * 60 * 60])
    // await provider.send('evm_mine', [])
    // const timestamp_2 = (await provider.getBlock('latest'));
    // console.log('timestamp_2', timestamp_2);
    for (let x of Array(11).keys()) {
      console.log(Mining block # ${x})
      await SPREADLY_PROTOCOL.spreadly.delegate(wallet.address)
    }

    let state = await SPREADLY_PROTOCOL.spreadlyGovernor.state(proposal_id)
    console.log('state', state)
    
    // let timelock_timestamp_id_hash = await SPREADLY_PROTOCOL.spreadlyGovernor.getInternalTimelockProposalHash(
    //   [SPREADLY_PROTOCOL.outcomeReporterWhitelist.address],
    //   [0],
    //   [call_data],
    //   descriptionHash,
    // );
    // console.log('timelock_timestamp_id_hash ', timelock_timestamp_id_hash);

    // timelock controller calls it and it works FINE
    // let try_outside = await SPREADLY_PROTOCOL.spreadlyTimelock._schedule(
    //   timelock_timestamp_id_hash,
    //   10,
    //   {
    //     gasPrice: provider.getGasPrice(),
    //     gasLimit: 100000,
    //     nonce: 31
    //   }
    // );

    // mine blocks to allow proposal to become active
    for (let x of Array(50).keys()) {
      console.log(Mining block # ${x})
      await SPREADLY_PROTOCOL.spreadly.delegate(wallet.address)
    }

    // reverts & does not change the _timestamp mapping in the timelock contract
    await SPREADLY_PROTOCOL.spreadlyGovernor['queue(uint256)'](
      proposal_id,
      {
        gasPrice: provider.getGasPrice(),
        gasLimit: 100000,
        nonce: 81
      }
    );

    await SPREADLY_PROTOCOL.spreadlyGovernor['execute(address[],uint256[],bytes[],bytes32)'](
      [SPREADLY_PROTOCOL.outcomeReporterWhitelist.address],
      [0],
      [call_data],
      descriptionHash,
      {
        gasPrice: provider.getGasPrice(),
        gasLimit: 100000,
        nonce: 82
      }
    );

    // let snapshot = await SPREADLY_PROTOCOL.spreadlyGovernor.proposalSnapshot(
    //   proposal_id,
    //   {
    //     gasPrice: provider.getGasPrice(),
    //     gasLimit: 100000,
    //     nonce: 16
    //   }
    // );

    // console.log(snapshot.toNumber())



  })
})

This is the queue function that calls the Timelock contract in the GovernorCompatibilityBravo module from OpenZeppelin:

/**
     * @dev Function to queue a proposal to the timelock.
     */
    function queue(
        address[] memory targets,
        uint256[] memory values,
        bytes[] memory calldatas,
        bytes32 descriptionHash
    ) public virtual override returns (uint256) {
        uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);

        require(state(proposalId) == ProposalState.Succeeded, "Governor: proposal not successful");

        uint256 delay = _timelock.getMinDelay();
        _timelockIds[proposalId] = _timelock.hashOperationBatch(targets, values, calldatas, 0, descriptionHash);
        _timelock.scheduleBatch(targets, values, calldatas, 0, descriptionHash, delay);

        emit ProposalQueued(proposalId, block.timestamp + delay);

        return proposalId;
    }

This is the function in the TimelockController contract that causes the revert:

/
     * @dev Schedule an operation containing a batch of transactions.
     *
     * Emits one {CallScheduled} event per transaction in the batch.
     *
     * Requirements:
     *
     * - the caller must have the 'proposer' role.
     */
    function scheduleBatch(
        address[] calldata targets,
        uint256[] calldata values,
        bytes[] calldata datas,
        bytes32 predecessor,
        bytes32 salt,
        uint256 delay
    ) public onlyRole(PROPOSER_ROLE) {
        require(targets.length == values.length, "TimelockController: length mismatch");
        require(targets.length == datas.length, "TimelockController: length mismatch");

        bytes32 id = hashOperationBatch(targets, values, datas, predecessor, salt);
        _schedule(id, delay);
        for (uint256 i = 0; i < targets.length; ++i) {
            emit CallScheduled(id, i, targets[i], values[i], datas[i], predecessor, delay);
        }
    }

    /
     * @dev Schedule an operation that is to becomes valid after a given delay.
     */
    function _schedule(bytes32 id, uint256 delay) private {
        require(!isOperation(id), "TimelockController: operation already scheduled");
        require(delay >= getMinDelay(), "TimelockController: insufficient delay");
        _timestamps[id] = block.timestamp + delay;
    }

This is my custom Timelock contract:

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/governance/TimelockController.sol";

contract SpreadlyTimelock is TimelockController {
    constructor(
        uint256 _minDelay, 
        address[] memory _proposers, 
        address[] memory _executors
    ) TimelockController(_minDelay, _proposers, _executors) {}

    /*
        Need to build out custom queue function so that the timelock controller
        Calls the same stuff that the queue function 
    */
}

This is my custom Governor contract (Copied from OpenZeppelin):

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/governance/Governor.sol";
import "@openzeppelin/contracts/governance/utils/IVotes.sol";
import "@openzeppelin/contracts/governance/compatibility/GovernorCompatibilityBravo.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";

contract SpreadlyGovernor is Governor, GovernorCompatibilityBravo, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl {
    constructor(IVotes _token, TimelockController _timelock)
        Governor("Spreadly Governor")
        GovernorVotes(_token)
        GovernorVotesQuorumFraction(4)
        GovernorTimelockControl(_timelock)
    {}

    function votingDelay() public pure override returns (uint256) {
        return 0;//6575; // 1 day
    }

    function votingPeriod() public pure override returns (uint256) {
        return 10; //46027; // 1 week
    }

    function proposalThreshold() public pure override returns (uint256) {
        return 0;
    }

    // create functions to delegate & vote all in one....

    // The functions below are overrides required by Solidity.

    function quorum(uint256 blockNumber)
        public
        view
        override(IGovernor, GovernorVotesQuorumFraction)
        returns (uint256)
    {
        return super.quorum(blockNumber);
    }

    function getVotes(address account, uint256 blockNumber)
        public
        view
        override(IGovernor, GovernorVotes)
        returns (uint256)
    {
        return super.getVotes(account, blockNumber);
    }

    function state(uint256 proposalId)
        public
        view
        override(Governor, IGovernor, GovernorTimelockControl)
        returns (ProposalState)
    {
        return super.state(proposalId);
    }

    function propose(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description)
        public
        override(Governor, GovernorCompatibilityBravo, IGovernor)
        returns (uint256)
    {
        return super.propose(targets, values, calldatas, description);
    }

    function _execute(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)
        internal
        override(Governor, GovernorTimelockControl)
    {
        super._execute(proposalId, targets, values, calldatas, descriptionHash);
    }

    function _cancel(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)
        internal
        override(Governor, GovernorTimelockControl)
        returns (uint256)
    {
        return super._cancel(targets, values, calldatas, descriptionHash);
    }

    function _executor()
        internal
        view
        override(Governor, GovernorTimelockControl)
        returns (address)
    {
        return super._executor();
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(Governor, IERC165, GovernorTimelockControl)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }

    function queue(uint256 proposalId)
        public
        override(GovernorCompatibilityBravo) 
    {
        super.queue(proposalId);
    }
}

:computer: Environment

Waffle
Solidity version 8.0.0

1 Like

Having a similar issue, can't get the _timestamp mapping in the TimelockController to set from a function originating outside of the timelock contract itself.

Figured out what the issue was! Apparently there's a bug with how waffle calls ganache in the background. Using the hardhat package manager made the queueing and executing work for me.

Here's a link to their site: https://hardhat.org/getting-started/

2 Likes

Hello @neonfox300

Can you confirm that your Governor contract has the PROPOSER_ROLE on the Timelock?

Yes I can confirm that the Governor contract is given that role. I ended up switching from Waffle to hardhat and it seemed to fix my problem and I can now queue and execute successful proposals.