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

feat: optimistic timelocks #230

Merged
merged 8 commits into from
Jun 30, 2023
Merged
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
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