diff --git a/contracts/governance/GovernorV3sol b/contracts/governance/GovernorV3sol new file mode 100644 index 00000000..bbde5d5a --- /dev/null +++ b/contracts/governance/GovernorV3sol @@ -0,0 +1,215 @@ +pragma solidity ^0.8.17; + +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {TimeLockTx, IGovernorV3} from "../interfaces/IGovernorV3.sol"; +import {ITimeLock} from "../interfaces/ITimeLock.sol"; + +enum TxAction { + Execute, + Cancel +} + +uint256 constant BATCH_SIZE_BITS = 16; + +contract GovernorV3 is IGovernorV3 { + using EnumerableSet for EnumerableSet.AddressSet; + + address public immutable override timeLock; + + EnumerableSet.AddressSet internal queueAdmins; + + address public override vetoAdmin; + + /// Batches + uint240 public override batchNum; + + mapping(bytes32 => uint256) public override batchedTransactions; + mapping(uint240 => uint256) public override batchedTransactionsCount; + + modifier queueAdminOnly() { + if (!queueAdmins.contains(msg.sender)) revert CallerNotQueueAdminException(); + _; + } + + modifier timeLockOnly() { + if (msg.sender != timeLock) revert CallerNotTimelockException(); + + _; + } + + modifier vetoAdminOnly() { + if (msg.sender != vetoAdmin) revert CallerNotVetoAdminException(); + + _; + } + + constructor(address _timeLock, address _queueAdmin, address _vetoAdmin) { + timeLock = _timeLock; + _addQueueAdmin(_queueAdmin); + _updateVetoAdmin(_vetoAdmin); + } + + // QUEUE + + function queueTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 eta) + external + override + queueAdminOnly + returns (bytes32) + { + return ITimeLock(timeLock).queueTransaction(target, value, signature, data, eta); + } + + function sealBatch(TimeLockTx[] calldata txs) external override queueAdminOnly { + uint256 len = txs.length; + if (len >= 2 ** BATCH_SIZE_BITS) revert IncorrectBatchLengthException(); + + ++batchNum; + uint240 _batchNum = batchNum; + + uint256 batchShifted = uint256(_batchNum) << BATCH_SIZE_BITS; + + unchecked { + for (uint256 i = 0; i < len; ++i) { + TimeLockTx calldata ttx = txs[i]; + bytes32 txHash = getTxHash(ttx); + + if (batchedTransactions[txHash] != 0) { + revert TxHashCollisionException(ttx.target, ttx.value, ttx.signature, ttx.data, ttx.eta); + } + + batchedTransactions[txHash] = batchShifted + i; + } + } + + batchedTransactionsCount[_batchNum] = len; + + emit SealBatch(batchNum, len); + } + + // EXECUTE + + function executeTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 eta) + external + payable + override + returns (bytes memory) + { + return _operation(target, value, signature, data, eta, TxAction.Execute); + } + + function executeBatch(TimeLockTx[] calldata txs) external payable override { + uint240 _batchNum = _batch(txs, TxAction.Execute); + emit ExecuteBatch(_batchNum); + } + + // CANCELLATION + function cancelTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 eta) + external + override + vetoAdminOnly + { + _operation(target, value, signature, data, eta, TxAction.Cancel); + } + + function cancelBatch(TimeLockTx[] calldata txs) external override vetoAdminOnly { + uint240 _batchNum = _batch(txs, TxAction.Cancel); + emit CancelBatch(_batchNum); + } + + // INTERNAL + + function _operation( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta, + TxAction action + ) internal returns (bytes memory result) { + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + if (batchedTransactions[txHash] != 0) revert TxCouldNotBeExecutedOutsideTheBatchException(); + + if (action == TxAction.Execute) { + result = ITimeLock(timeLock).executeTransaction{value: value}(target, value, signature, data, eta); + } else { + ITimeLock(timeLock).cancelTransaction(target, value, signature, data, eta); + } + } + + function _batch(TimeLockTx[] calldata txs, TxAction action) internal override returns (uint240) { + uint256 len = txs.length; + if (len == 0) revert IncorrectBatchLengthException(); + + uint256 batchShifted = batchedTransactions[getTxHash(txs[0])]; + uint240 _batchNum = uint240(batchShifted >> BATCH_SIZE_BITS); + if (_batchNum == 0) revert BatchNotFoundException(); + + if (batchedTransactionsCount[_batchNum] != len) revert IncorrectBatchLengthException(); + + unchecked { + for (uint256 i = 0; i < len; ++i) { + TimeLockTx calldata ttx = txs[i]; + bytes32 txHash = getTxHash(ttx); + + if (batchedTransactions[txHash] != batchShifted + i) { + revert TxIncorrectOrder(ttx.target, ttx.value, ttx.signature, ttx.data, ttx.eta); + } + + if (action == TxAction.Execute) { + ITimeLock(timeLock).executeTransaction{value: ttx.value}( + ttx.target, ttx.value, ttx.signature, ttx.data, ttx.eta + ); + } else { + ITimeLock(timeLock).cancelTransaction(ttx.target, ttx.value, ttx.signature, ttx.data, ttx.eta); + } + + delete batchedTransactions[txHash]; + } + } + + delete batchedTransactionsCount[_batchNum]; + + return _batchNum; + } + + // GETTER + + function getTxHash(TimeLockTx calldata ttx) public pure returns (bytes32) { + return keccak256(abi.encode(ttx.target, ttx.value, ttx.signature, ttx.data, ttx.eta)); + } + + /// Setting admins + function addQueueAdmin(address _admin) external override timeLockOnly { + _addQueueAdmin(_admin); + } + + function _addQueueAdmin(address _admin) internal { + if (!queueAdmins.contains(_admin)) { + queueAdmins.add(_admin); + emit AddQueueAdmin(_admin); + } + } + + function removeQueueAdmin(address _admin) external override timeLockOnly { + if (queueAdmins.contains(_admin)) { + if (queueAdmins.length() == 1) revert CantRemoveLastQueueAdminException(); + + queueAdmins.remove(_admin); + emit RemoveQueueAdmin(_admin); + } + } + + function updateVetoAdmin(address _admin) external override timeLockOnly { + _updateVetoAdmin(_admin); + } + + function _updateVetoAdmin(address _vetoAdmin) internal { + vetoAdmin = _vetoAdmin; + emit UpdateVetoAdmin(vetoAdmin); + } + + function claimTimeLockOwnership() external queueAdminOnly { + ITimeLock(timeLock).acceptAdmin(); + } +} diff --git a/contracts/interfaces/IGovernorV3.sol b/contracts/interfaces/IGovernorV3.sol new file mode 100644 index 00000000..529891eb --- /dev/null +++ b/contracts/interfaces/IGovernorV3.sol @@ -0,0 +1,80 @@ +pragma solidity ^0.8.17; + +struct TimeLockTx { + address target; + uint256 value; + string signature; + bytes data; + uint256 eta; +} + +interface IGovernorV3Events { + event SealBatch(uint256 indexed batchNum, uint256 length); + + event ExecuteBatch(uint256 indexed batchNum); + + event CancelBatch(uint256 indexed batchNum); + + event AddQueueAdmin(address indexed admin); + + event RemoveQueueAdmin(address indexed admin); + + event UpdateVetoAdmin(address indexed vetoAdmin); +} + +interface IGovernorV3 is IGovernorV3Events { + error CallerNotQueueAdminException(); + + error CallerNotTimelockException(); + + error CallerNotVetoAdminException(); + + error CantRemoveLastQueueAdminException(); + + error TxHashCollisionException(address target, uint256 value, string signature, bytes data, uint256 eta); + + error TxIncorrectOrder(address target, uint256 value, string signature, bytes data, uint256 eta); + + error TxCouldNotBeExecutedOutsideTheBatchException(); + + error IncorrectBatchLengthException(); + + error BatchNotFoundException(); + + function queueTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 eta) + external + returns (bytes32); + + function executeTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 eta) + external + payable + returns (bytes memory); + + function sealBatch(TimeLockTx[] calldata txs) external; + + function executeBatch(TimeLockTx[] calldata txs) external payable; + + function cancelBatch(TimeLockTx[] calldata txs) external; + + /// GETTERS + + function getTxHash(TimeLockTx calldata ttx) external pure returns (bytes32); + + function timeLock() external view returns (address); + + function vetoAdmin() external view returns (address); + + function batchNum() external view returns (uint240); + + function batchedTransactions(bytes32) external view returns (uint256); + + function batchedTransactionsCount(uint240) external view returns (uint256); + + // CONFIGURE + + function addQueueAdmin(address _admin) external; + + function removeQueueAdmin(address _admin) external; + + function updateVetoAdmin(address _admin) external; +} diff --git a/contracts/interfaces/ITimeLock.sol b/contracts/interfaces/ITimeLock.sol new file mode 100644 index 00000000..87762790 --- /dev/null +++ b/contracts/interfaces/ITimeLock.sol @@ -0,0 +1,17 @@ +pragma solidity ^0.8.17; + +interface ITimeLock { + function queueTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 eta) + external + returns (bytes32); + + function executeTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 eta) + external + payable + returns (bytes memory); + + function cancelTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 eta) + external; + + function acceptAdmin() external; +} diff --git a/contracts/test/helpers/IntegrationTestHelper.sol b/contracts/test/helpers/IntegrationTestHelper.sol index 159f0559..e2e6cdcf 100644 --- a/contracts/test/helpers/IntegrationTestHelper.sol +++ b/contracts/test/helpers/IntegrationTestHelper.sol @@ -229,21 +229,24 @@ contract IntegrationTestHelper is TestHelper, BalanceHelper, ConfigManager { modifier attachAllV3CMTest() { _attachCore(); - address creditManagerAddr; - bool skipTest = false; - - address[] memory cms = cr.getCreditManagers(); - uint256 len = cms.length; - unchecked { - for (uint256 i = 0; i < len; i++) { - address poolAddr = cr.poolByIndex(i); - if (!_attachPool(poolAddr)) { - console.log("Skipped"); - skipTest = true; - break; - } else {} - } - } + // address creditManagerAddr; + // bool skipTest = false; + + // address[] memory cms = cr.getCreditManagers(); + // uint256 len = cms.length; + + // address[] memory pools = cr.getPools(); + // unchecked { + // for (uint256 i = 0; i < len; i++) { + // address poolAddr = cr.poolByIndex(i); + // if (!_attachPool(poolAddr)) { + // console.log("Skipped"); + // skipTest = true; + // break; + // } else {} + // } + // } + _; } function _setupCore() internal {