Skip to content

Commit

Permalink
feat: optimistic timelocks (#230)
Browse files Browse the repository at this point in the history
* refactor: move existing timelocks to timelocks folder; update README and tests

* tmp: add copy of CompTimelockCompatibleExecutionStrategy

* feat: add OptimisticCompTimelockCompatibleExecutionStrategy

* chore: rename Optimstic to Optimistic

* tmp: add copy of OptimisticTimelockExecutionStrategy.sol

* feat: add OptimisticTimelockExecutionStrategy

* feat: add tests
  • Loading branch information
pscott authored Jun 30, 2023
1 parent c3b5e3d commit 7437cfb
Show file tree
Hide file tree
Showing 9 changed files with 1,579 additions and 9 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ src
│ ├─ WhitelistVotingStrategy.sol — "Strategy that gives predetermined voting power for members in a whitelist, otherwise zero"
│ └─ VanillaVotingStrategy.sol — "Vanilla Strategy"
├─ execution-strategies
│ ├─ timelocks
│ | ├─ CompTimelockCompatibleExecutionStrategy.sol - "Strategy that provides compatibility with existing Comp Timelock contracts"
│ | ├─ OptimisticCompTimelockCompatibleExecutionStrategy.sol - "Optimistic strategy that provides compatibility with existing Comp Timelock contracts"
│ | ├─ OptimisticTimelockExecutionStrategy.sol - "Optimistic strategy that can be used to execute proposal transactions according to a timelock delay"
│ | └─ TimelockExecutionStrategy.sol - "Strategy that can be used to execute proposal transactions according to a timelock delay"
│ ├─ AvatarExecutionStrategy.sol - "Strategy that allows proposal transactions to be executed from an Avatar contract"
│ ├─ TimelockExecutionStrategy.sol - "Strategy that can be used to execute proposal transactions according to a timelock delay"
│ ├─ CompTimelockCompatibleExecutionStrategy.sol - "Strategy that provides compatibility with existing Comp Timelock contracts"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

pragma solidity ^0.8.18;

import { ICompTimelock } from "../interfaces/ICompTimelock.sol";
import { SimpleQuorumExecutionStrategy } from "./SimpleQuorumExecutionStrategy.sol";
import { SpaceManager } from "../utils/SpaceManager.sol";
import { MetaTransaction, Proposal, ProposalStatus, TRUE, FALSE } from "../types.sol";
import { ICompTimelock } from "../../interfaces/ICompTimelock.sol";
import { SimpleQuorumExecutionStrategy } from "../SimpleQuorumExecutionStrategy.sol";
import { SpaceManager } from "../../utils/SpaceManager.sol";
import { MetaTransaction, Proposal, ProposalStatus, TRUE, FALSE } from "../../types.sol";
import { Enum } from "@gnosis.pm/safe-contracts/contracts/common/Enum.sol";

/// @title Comp Timelock Execution Strategy
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.18;

import { ICompTimelock } from "../../interfaces/ICompTimelock.sol";
import { OptimisticQuorumExecutionStrategy } from "../OptimisticQuorumExecutionStrategy.sol";
import { SpaceManager } from "../../utils/SpaceManager.sol";
import { MetaTransaction, Proposal, ProposalStatus, TRUE, FALSE } from "../../types.sol";
import { Enum } from "@gnosis.pm/safe-contracts/contracts/common/Enum.sol";

/// @title Optimistic Comp Timelock Execution Strategy
/// @notice An optimstic execution strategy that provides compatibility with existing Comp Timelock contracts.
contract OptimisticCompTimelockCompatibleExecutionStrategy is OptimisticQuorumExecutionStrategy {
/// @notice Thrown if timelock delay is in the future.
error TimelockDelayNotMet();
/// @notice Thrown if the proposal execution payload hash is not queued.
error ProposalNotQueued();
/// @notice Thrown if the proposal execution payload hash is already queued.
error DuplicateExecutionPayloadHash();
/// @notice Thrown if the same MetaTransaction appears twice in the same payload (salt is not taken into account).
error DuplicateMetaTransaction();
/// @notice Thrown if veto caller is not the veto guardian.
error OnlyVetoGuardian();
/// @notice Thrown if the transaction is invalid.
error InvalidTransaction();

event OptimisticCompTimelockCompatibleExecutionStrategySetUp(
address owner,
address vetoGuardian,
address[] spaces,
uint256 quorum,
address timelock
);
event TransactionQueued(MetaTransaction transaction, uint256 executionTime);
event TransactionExecuted(MetaTransaction transaction);
event TransactionVetoed(MetaTransaction transaction);
event VetoGuardianSet(address vetoGuardian, address newVetoGuardian);
event ProposalVetoed(bytes32 executionPayloadHash);
event ProposalQueued(bytes32 executionPayloadHash);
event ProposalExecuted(bytes32 executionPayloadHash);

/// @notice The time at which a proposal can be executed. Indexed by the hash of the proposal execution payload.
mapping(bytes32 => uint256) public proposalExecutionTime;

/// @notice Mapping of queued transaction hashes.
mapping(bytes32 => uint256) public txHashes;

/// @notice Veto guardian is given permission to veto any queued proposal.
address public vetoGuardian;

/// @notice The timelock contract.
ICompTimelock public timelock;

/// @notice Constructor
/// @param _owner Address of the owner of this contract.
/// @param _vetoGuardian Address of the veto guardian.
/// @param _spaces Array of whitelisted space contracts.
/// @param _quorum The quorum required to reject a proposal.
constructor(address _owner, address _vetoGuardian, address[] memory _spaces, uint256 _quorum, address _timelock) {
setUp(abi.encode(_owner, _vetoGuardian, _spaces, _quorum, _timelock));
}

function setUp(bytes memory initializeParams) public initializer {
(address _owner, address _vetoGuardian, address[] memory _spaces, uint256 _quorum, address _timelock) = abi
.decode(initializeParams, (address, address, address[], uint256, address));
__Ownable_init();
transferOwnership(_owner);
vetoGuardian = _vetoGuardian;
__SpaceManager_init(_spaces);
__OptimisticQuorumExecutionStrategy_init(_quorum);
timelock = ICompTimelock(_timelock);
emit OptimisticCompTimelockCompatibleExecutionStrategySetUp(_owner, _vetoGuardian, _spaces, _quorum, _timelock);
}

/// @notice Accepts admin role of the timelock contract. Must be called before using the timelock.
function acceptAdmin() external {
timelock.acceptAdmin();
}

/// @notice The delay in seconds between a proposal being queued and the execution of the proposal.
function timelockDelay() public view returns (uint256) {
return timelock.delay();
}

/// @notice Executes a proposal by queueing its transactions in the timelock. Can only be called by approved spaces.
/// @param proposal The proposal.
/// @param votesFor The number of votes for the proposal.
/// @param votesAgainst The number of votes against the proposal.
/// @param votesAbstain The number of abstaining votes for the proposal.
/// @param payload The encoded payload of the proposal to execute.
function execute(
Proposal memory proposal,
uint256 votesFor,
uint256 votesAgainst,
uint256 votesAbstain,
bytes memory payload
) external override onlySpace {
ProposalStatus proposalStatus = getProposalStatus(proposal, votesFor, votesAgainst, votesAbstain);
if ((proposalStatus != ProposalStatus.Accepted) && (proposalStatus != ProposalStatus.VotingPeriodAccepted)) {
revert InvalidProposalStatus(proposalStatus);
}

if (proposalExecutionTime[proposal.executionPayloadHash] != 0) revert DuplicateExecutionPayloadHash();

uint256 executionTime = block.timestamp + timelockDelay();
proposalExecutionTime[proposal.executionPayloadHash] = executionTime;

MetaTransaction[] memory transactions = abi.decode(payload, (MetaTransaction[]));

for (uint256 i = 0; i < transactions.length; i++) {
// Comp Timelock does not support delegate calls.
if (transactions[i].operation == Enum.Operation.DelegateCall) {
revert InvalidTransaction();
}

// Check there are not duplicates.
// We must do this because the Compound Timelock will silently "merge" two duplicate transactions.
// This could be problematic for off-chain indexers and UI tools.
bytes32 txHash = keccak256(
abi.encode(transactions[i].to, transactions[i].value, "", transactions[i].data, executionTime)
);
// We use `!= FALSE` rather than `== TRUE` for gas optimisations.
if (txHashes[txHash] != FALSE) revert DuplicateMetaTransaction();

// Store the transaction hash.
txHashes[txHash] = TRUE;

timelock.queueTransaction(
transactions[i].to,
transactions[i].value,
"",
transactions[i].data,
executionTime
);
emit TransactionQueued(transactions[i], executionTime);
}
emit ProposalQueued(proposal.executionPayloadHash);
}

/// @notice Executes a queued proposal.
/// @param payload The encoded payload of the proposal to execute.
/// @dev Due to possible reentrancy, one cannot rely on the invariant that proposal payloads are executed atomically.
/// As follows: If Proposal A is composed of MetaTransaction a1 and a2, and proposal B of MetaTransaction b1.
/// If A.a1 executes code that triggers a proposal execution, then the execution order overall can potentially
/// become [A.a1, B.b1, A.a2].
function executeQueuedProposal(bytes memory payload) external {
bytes32 executionPayloadHash = keccak256(payload);

uint256 executionTime = proposalExecutionTime[executionPayloadHash];

if (executionTime == 0) revert ProposalNotQueued();
if (proposalExecutionTime[executionPayloadHash] > block.timestamp) revert TimelockDelayNotMet();

// Reset the execution time to 0 to prevent reentrancy.
proposalExecutionTime[executionPayloadHash] = 0;

MetaTransaction[] memory transactions = abi.decode(payload, (MetaTransaction[]));
for (uint256 i = 0; i < transactions.length; i++) {
// Clear out the transactions from the mapping.
bytes32 txHash = keccak256(
abi.encode(transactions[i].to, transactions[i].value, "", transactions[i].data, executionTime)
);
txHashes[txHash] = FALSE;

timelock.executeTransaction(
transactions[i].to,
transactions[i].value,
"",
transactions[i].data,
executionTime
);
emit TransactionExecuted(transactions[i]);
}
emit ProposalExecuted(executionPayloadHash);
}

/// @notice Vetoes a queued proposal.
/// @param payload The encoded payload of the proposal to veto.
function veto(bytes memory payload) external {
bytes32 payloadHash = keccak256(payload);
if (msg.sender != vetoGuardian) revert OnlyVetoGuardian();

uint256 executionTime = proposalExecutionTime[payloadHash];
if (executionTime == 0) revert ProposalNotQueued();

MetaTransaction[] memory transactions = abi.decode(payload, (MetaTransaction[]));
for (uint256 i = 0; i < transactions.length; i++) {
timelock.cancelTransaction(
transactions[i].to,
transactions[i].value,
"",
transactions[i].data,
executionTime
);
emit TransactionVetoed(transactions[i]);
}
proposalExecutionTime[payloadHash] = 0;
emit ProposalVetoed(payloadHash);
}

/// @notice Sets the veto guardian.
/// @param newVetoGuardian The new veto guardian.
function setVetoGuardian(address newVetoGuardian) external onlyOwner {
emit VetoGuardianSet(vetoGuardian, newVetoGuardian);
vetoGuardian = newVetoGuardian;
}

/// @notice Returns the strategy type string.
function getStrategyType() external pure override returns (string memory) {
return "CompTimelockCompatibleOptimisticQuorum";
}
}
Loading

0 comments on commit 7437cfb

Please sign in to comment.