diff --git a/brownie/abi/timelock.json b/brownie/abi/timelock.json new file mode 100644 index 0000000000..c540c69502 --- /dev/null +++ b/brownie/abi/timelock.json @@ -0,0 +1 @@ +[{"inputs": [{"internalType": "address[]","name": "proposers","type": "address[]"},{"internalType": "address[]","name": "executors","type": "address[]"}],"stateMutability": "nonpayable","type": "constructor","name": "constructor"},{"anonymous": false,"inputs": [{"indexed": true,"internalType": "bytes32","name": "id","type": "bytes32"},{"indexed": true,"internalType": "uint256","name": "index","type": "uint256"},{"indexed": false,"internalType": "address","name": "target","type": "address"},{"indexed": false,"internalType": "uint256","name": "value","type": "uint256"},{"indexed": false,"internalType": "bytes","name": "data","type": "bytes"}],"name": "CallExecuted","type": "event"},{"anonymous": false,"inputs": [{"indexed": true,"internalType": "bytes32","name": "id","type": "bytes32"},{"indexed": true,"internalType": "uint256","name": "index","type": "uint256"},{"indexed": false,"internalType": "address","name": "target","type": "address"},{"indexed": false,"internalType": "uint256","name": "value","type": "uint256"},{"indexed": false,"internalType": "bytes","name": "data","type": "bytes"},{"indexed": false,"internalType": "bytes32","name": "predecessor","type": "bytes32"},{"indexed": false,"internalType": "uint256","name": "delay","type": "uint256"}],"name": "CallScheduled","type": "event"},{"anonymous": false,"inputs": [{"indexed": true,"internalType": "bytes32","name": "id","type": "bytes32"}],"name": "Cancelled","type": "event"},{"anonymous": false,"inputs": [{"indexed": false,"internalType": "uint256","name": "oldDuration","type": "uint256"},{"indexed": false,"internalType": "uint256","name": "newDuration","type": "uint256"}],"name": "MinDelayChange","type": "event"},{"anonymous": false,"inputs": [{"indexed": true,"internalType": "bytes32","name": "role","type": "bytes32"},{"indexed": true,"internalType": "bytes32","name": "previousAdminRole","type": "bytes32"},{"indexed": true,"internalType": "bytes32","name": "newAdminRole","type": "bytes32"}],"name": "RoleAdminChanged","type": "event"},{"anonymous": false,"inputs": [{"indexed": true,"internalType": "bytes32","name": "role","type": "bytes32"},{"indexed": true,"internalType": "address","name": "account","type": "address"},{"indexed": true,"internalType": "address","name": "sender","type": "address"}],"name": "RoleGranted","type": "event"},{"anonymous": false,"inputs": [{"indexed": true,"internalType": "bytes32","name": "role","type": "bytes32"},{"indexed": true,"internalType": "address","name": "account","type": "address"},{"indexed": true,"internalType": "address","name": "sender","type": "address"}],"name": "RoleRevoked","type": "event"},{"inputs": [],"name": "CANCELLER_ROLE","outputs": [{"internalType": "bytes32","name": "","type": "bytes32"}],"stateMutability": "view","type": "function"},{"inputs": [],"name": "DEFAULT_ADMIN_ROLE","outputs": [{"internalType": "bytes32","name": "","type": "bytes32"}],"stateMutability": "view","type": "function"},{"inputs": [],"name": "EXECUTOR_ROLE","outputs": [{"internalType": "bytes32","name": "","type": "bytes32"}],"stateMutability": "view","type": "function"},{"inputs": [],"name": "PROPOSER_ROLE","outputs": [{"internalType": "bytes32","name": "","type": "bytes32"}],"stateMutability": "view","type": "function"},{"inputs": [],"name": "TIMELOCK_ADMIN_ROLE","outputs": [{"internalType": "bytes32","name": "","type": "bytes32"}],"stateMutability": "view","type": "function"},{"inputs": [{"internalType": "bytes32","name": "id","type": "bytes32"}],"name": "cancel","outputs": [],"stateMutability": "nonpayable","type": "function"},{"inputs": [{"internalType": "address","name": "target","type": "address"},{"internalType": "uint256","name": "value","type": "uint256"},{"internalType": "bytes","name": "data","type": "bytes"},{"internalType": "bytes32","name": "predecessor","type": "bytes32"},{"internalType": "bytes32","name": "salt","type": "bytes32"}],"name": "execute","outputs": [],"stateMutability": "payable","type": "function"},{"inputs": [{"internalType": "address[]","name": "targets","type": "address[]"},{"internalType": "uint256[]","name": "values","type": "uint256[]"},{"internalType": "bytes[]","name": "payloads","type": "bytes[]"},{"internalType": "bytes32","name": "predecessor","type": "bytes32"},{"internalType": "bytes32","name": "salt","type": "bytes32"}],"name": "executeBatch","outputs": [],"stateMutability": "payable","type": "function"},{"inputs": [],"name": "getMinDelay","outputs": [{"internalType": "uint256","name": "duration","type": "uint256"}],"stateMutability": "view","type": "function"},{"inputs": [{"internalType": "bytes32","name": "role","type": "bytes32"}],"name": "getRoleAdmin","outputs": [{"internalType": "bytes32","name": "","type": "bytes32"}],"stateMutability": "view","type": "function"},{"inputs": [{"internalType": "bytes32","name": "id","type": "bytes32"}],"name": "getTimestamp","outputs": [{"internalType": "uint256","name": "timestamp","type": "uint256"}],"stateMutability": "view","type": "function"},{"inputs": [{"internalType": "bytes32","name": "role","type": "bytes32"},{"internalType": "address","name": "account","type": "address"}],"name": "grantRole","outputs": [],"stateMutability": "nonpayable","type": "function"},{"inputs": [{"internalType": "bytes32","name": "role","type": "bytes32"},{"internalType": "address","name": "account","type": "address"}],"name": "hasRole","outputs": [{"internalType": "bool","name": "","type": "bool"}],"stateMutability": "view","type": "function"},{"inputs": [{"internalType": "address","name": "target","type": "address"},{"internalType": "uint256","name": "value","type": "uint256"},{"internalType": "bytes","name": "data","type": "bytes"},{"internalType": "bytes32","name": "predecessor","type": "bytes32"},{"internalType": "bytes32","name": "salt","type": "bytes32"}],"name": "hashOperation","outputs": [{"internalType": "bytes32","name": "hash","type": "bytes32"}],"stateMutability": "pure","type": "function"},{"inputs": [{"internalType": "address[]","name": "targets","type": "address[]"},{"internalType": "uint256[]","name": "values","type": "uint256[]"},{"internalType": "bytes[]","name": "payloads","type": "bytes[]"},{"internalType": "bytes32","name": "predecessor","type": "bytes32"},{"internalType": "bytes32","name": "salt","type": "bytes32"}],"name": "hashOperationBatch","outputs": [{"internalType": "bytes32","name": "hash","type": "bytes32"}],"stateMutability": "pure","type": "function"},{"inputs": [{"internalType": "bytes32","name": "id","type": "bytes32"}],"name": "isOperation","outputs": [{"internalType": "bool","name": "pending","type": "bool"}],"stateMutability": "view","type": "function"},{"inputs": [{"internalType": "bytes32","name": "id","type": "bytes32"}],"name": "isOperationDone","outputs": [{"internalType": "bool","name": "done","type": "bool"}],"stateMutability": "view","type": "function"},{"inputs": [{"internalType": "bytes32","name": "id","type": "bytes32"}],"name": "isOperationPending","outputs": [{"internalType": "bool","name": "pending","type": "bool"}],"stateMutability": "view","type": "function"},{"inputs": [{"internalType": "bytes32","name": "id","type": "bytes32"}],"name": "isOperationReady","outputs": [{"internalType": "bool","name": "ready","type": "bool"}],"stateMutability": "view","type": "function"},{"inputs": [{"internalType": "address","name": "","type": "address"},{"internalType": "address","name": "","type": "address"},{"internalType": "uint256[]","name": "","type": "uint256[]"},{"internalType": "uint256[]","name": "","type": "uint256[]"},{"internalType": "bytes","name": "","type": "bytes"}],"name": "onERC1155BatchReceived","outputs": [{"internalType": "bytes4","name": "","type": "bytes4"}],"stateMutability": "nonpayable","type": "function"},{"inputs": [{"internalType": "address","name": "","type": "address"},{"internalType": "address","name": "","type": "address"},{"internalType": "uint256","name": "","type": "uint256"},{"internalType": "uint256","name": "","type": "uint256"},{"internalType": "bytes","name": "","type": "bytes"}],"name": "onERC1155Received","outputs": [{"internalType": "bytes4","name": "","type": "bytes4"}],"stateMutability": "nonpayable","type": "function"},{"inputs": [{"internalType": "address","name": "","type": "address"},{"internalType": "address","name": "","type": "address"},{"internalType": "uint256","name": "","type": "uint256"},{"internalType": "bytes","name": "","type": "bytes"}],"name": "onERC721Received","outputs": [{"internalType": "bytes4","name": "","type": "bytes4"}],"stateMutability": "nonpayable","type": "function"},{"inputs": [{"internalType": "bytes32","name": "role","type": "bytes32"},{"internalType": "address","name": "account","type": "address"}],"name": "renounceRole","outputs": [],"stateMutability": "nonpayable","type": "function"},{"inputs": [{"internalType": "bytes32","name": "role","type": "bytes32"},{"internalType": "address","name": "account","type": "address"}],"name": "revokeRole","outputs": [],"stateMutability": "nonpayable","type": "function"},{"inputs": [{"internalType": "address","name": "target","type": "address"},{"internalType": "uint256","name": "value","type": "uint256"},{"internalType": "bytes","name": "data","type": "bytes"},{"internalType": "bytes32","name": "predecessor","type": "bytes32"},{"internalType": "bytes32","name": "salt","type": "bytes32"},{"internalType": "uint256","name": "delay","type": "uint256"}],"name": "schedule","outputs": [],"stateMutability": "nonpayable","type": "function"},{"inputs": [{"internalType": "address[]","name": "targets","type": "address[]"},{"internalType": "uint256[]","name": "values","type": "uint256[]"},{"internalType": "bytes[]","name": "payloads","type": "bytes[]"},{"internalType": "bytes32","name": "predecessor","type": "bytes32"},{"internalType": "bytes32","name": "salt","type": "bytes32"},{"internalType": "uint256","name": "delay","type": "uint256"}],"name": "scheduleBatch","outputs": [],"stateMutability": "nonpayable","type": "function"},{"inputs": [{"internalType": "bytes4","name": "interfaceId","type": "bytes4"}],"name": "supportsInterface","outputs": [{"internalType": "bool","name": "","type": "bool"}],"stateMutability": "view","type": "function"},{"inputs": [{"internalType": "uint256","name": "newDelay","type": "uint256"}],"name": "updateDelay","outputs": [],"stateMutability": "nonpayable","type": "function"},{"stateMutability": "payable","type": "receive"}] \ No newline at end of file diff --git a/brownie/scripts/test_multisig_as_governor.py b/brownie/scripts/test_multisig_as_governor.py new file mode 100644 index 0000000000..c271a4ae0c --- /dev/null +++ b/brownie/scripts/test_multisig_as_governor.py @@ -0,0 +1,122 @@ +from world import * +from brownie import chain + +def main(): + with TemporaryFork(): + calldata = vault_oeth_admin.setRedeemFeeBps.encode_input(0) + + print("Creating a test proposal on Timelock...") + # Check if multisig can create proposals on Timelock + tx = timelock_contract.schedule( + OETH_VAULT, # Target + 0, # Value + calldata, + "", + "", + 24 * 60 * 60, # delay + {'from': GOV_MULTISIG} + ) + tx.info() + + print("Waiting until it's ready for execution...") + # Fast forward to execute + chain.sleep(24 * 60 * 60 + 10) + + print("Executing...") + # Test execution + tx = timelock_contract.execute( + OETH_VAULT, # Target + 0, # Value + calldata, + "", + "", + {'from': GOV_MULTISIG} + ) + tx.info() + + if vault_oeth_admin.redeemFeeBps() != 0: + raise Exception("Action not updated") + + print("All Good!") + + print("-------------------------------------------") + with TemporaryFork(): + calldata = vault_oeth_admin.setRedeemFeeBps.encode_input(1) + + print("\n\nCreating another test proposal on Timelock...") + tx = timelock_contract.schedule( + OETH_VAULT, # Target + 0, # Value + calldata, + "", + "", + 24 * 60 * 60, # delay + {'from': GOV_MULTISIG} + ) + tx.info() + + action_hash = timelock_contract.hashOperation( + OETH_VAULT, # Target + 0, # Value + calldata, + "", + "" + ) + + print("Cancelling tx...") + # Make sure the multisig can cancel txs as well + tx = timelock_contract.cancel(action_hash, {'from': GOV_MULTISIG}) + tx.info() + + if timelock_contract.isOperation(action_hash): + raise Exception("Failed to cancel op") + + print("All Good!") + + print("-------------------------------------------") + with TemporaryFork(): + print("\n\nMaking sure that it's possible to grant role...") + calldata = timelock_contract.grantRole.encode_input( + # keccak256("TIMELOCK_ADMIN_ROLE") + "0xb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc1", + "0x0000000000000000000000000000000000000011" + ) + + print("\n\nCreating another test proposal on Timelock...") + tx = timelock_contract.schedule( + TIMELOCK, # Target + 0, # Value + calldata, + "", + "", + 24 * 60 * 60, # delay + {'from': GOV_MULTISIG} + ) + tx.info() + + print("Waiting until it's ready for execution...") + # Fast forward to execute + chain.sleep(24 * 60 * 60 + 10) + + print("Executing...") + # Test execution + tx = timelock_contract.execute( + TIMELOCK, # Target + 0, # Value + calldata, + "", + "", + {'from': GOV_MULTISIG} + ) + tx.info() + + if not timelock_contract.hasRole( + "0xb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc1", + "0x0000000000000000000000000000000000000011" + ): + raise Exception("Failed to grant roles") + + print("All Good!") + + + diff --git a/brownie/world.py b/brownie/world.py index 799f88f6d3..921b8bfccf 100644 --- a/brownie/world.py +++ b/brownie/world.py @@ -84,6 +84,7 @@ def load_contract(name, address): gova = brownie.accounts.at(GOVERNOR, force=True) governor = load_contract('governor', GOVERNOR) governor_five = load_contract('governor_five', GOVERNOR_FIVE) +timelock_contract = load_contract('timelock', TIMELOCK) rewards_source = load_contract('rewards_source', REWARDS_SOURCE) diff --git a/contracts/contracts/interfaces/ITimelockController.sol b/contracts/contracts/interfaces/ITimelockController.sol new file mode 100644 index 0000000000..b7a204dec8 --- /dev/null +++ b/contracts/contracts/interfaces/ITimelockController.sol @@ -0,0 +1,9 @@ +pragma solidity ^0.8.0; + +interface ITimelockController { + function grantRole(bytes32 role, address account) external; + + function revokeRole(bytes32 role, address account) external; + + function renounceRole(bytes32 role, address account) external; +} diff --git a/contracts/deploy/mainnet/094_add_multisig_to_timelock.js b/contracts/deploy/mainnet/094_add_multisig_to_timelock.js new file mode 100644 index 0000000000..536760af29 --- /dev/null +++ b/contracts/deploy/mainnet/094_add_multisig_to_timelock.js @@ -0,0 +1,50 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "094_add_multisig_to_timelock", + forceDeploy: false, + // forceSkip: true, + // onlyOnFork: true, // this is only executed in forked environment + reduceQueueTime: true, // just to solve the issue of later active proposals failing + proposalId: + "76565349657922140684589379459208028078547529916278436872532296348233972897503", + }, + async ({ ethers }) => { + const PROPOSER_ROLE = + "0xb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc1"; // keccak256("PROPOSER_ROLE"); + const EXECUTOR_ROLE = + "0xd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e63"; // keccak256("EXECUTOR_ROLE"); + const CANCELLER_ROLE = + "0xfd643c72710c63c0180259aba6b2d05451e3591a24e58b62239378085726f783"; // keccak256("CANCELLER_ROLE"); + const cTimelock = await ethers.getContractAt( + "ITimelockController", + addresses.mainnet.Timelock + ); + + return { + name: "Add Guardian Multisig to Timelock\n\ + \n\ + Part of the OGN/OGV merger. This proposal adds the Origin's 5 of 8 Multisig to the Timelock to provide a backup governance during the transition between governance systems during the token merger. This permission will be revoked once the merger is complete. \ + ", + actions: [ + { + contract: cTimelock, + signature: "grantRole(bytes32,address)", + args: [PROPOSER_ROLE, addresses.mainnet.Guardian], + }, + { + contract: cTimelock, + signature: "grantRole(bytes32,address)", + args: [EXECUTOR_ROLE, addresses.mainnet.Guardian], + }, + { + contract: cTimelock, + signature: "grantRole(bytes32,address)", + args: [CANCELLER_ROLE, addresses.mainnet.Guardian], + }, + ], + }; + } +); diff --git a/contracts/deployments/mainnet/.migrations.json b/contracts/deployments/mainnet/.migrations.json index e4b4b6e7c0..e8e182ea7a 100644 --- a/contracts/deployments/mainnet/.migrations.json +++ b/contracts/deployments/mainnet/.migrations.json @@ -83,5 +83,6 @@ "090_disable_compound": 1711469659, "091_simplified_oeth_vault": 1714138519, "092_woeth_ccip_zapper": 1714111493, - "093_disable_frxeth_strategies": 1714495720 + "093_disable_frxeth_strategies": 1714495720, + "094_add_multisig_to_timelock": 1715176032 }