diff --git a/src/orbit-chain-governance/factories/GovernanceChainGovFactory.sol b/src/orbit-chain-governance/factories/GovernanceChainGovFactory.sol new file mode 100644 index 00000000..e430f4e3 --- /dev/null +++ b/src/orbit-chain-governance/factories/GovernanceChainGovFactory.sol @@ -0,0 +1,133 @@ +// SPDX-LicensĀ©e-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "../../L2ArbitrumGovernor.sol"; +import "../../ArbitrumTimelock.sol"; +import "../../UpgradeExecutor.sol"; +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts-upgradeable/governance/utils/IVotesUpgradeable.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; + +/// @notice Parameters for deployment +struct DeployParams { + address _governanceToken; // Address of IVotesUpgradeable token deployed on the governance chain + address _govChainUpExec; // Address of governance UpgradeExecutor deployed on the governance chain + address _govChainProxyAdmin; // Address of ProxyAdmin deployed on the governance chain + uint256 _proposalThreshold; // Number of votes required to submit a proposal + uint256 _votingPeriod; // Time period in blocks during which voting for a proposal occurs + uint256 _votingDelay; // Delay in blocks after a proposal is submitted before voting begins + uint256 _minTimelockDelay; // Delay in seconds after proposal passes before it can be executed + uint64 _minPeriodAfterQuorum; // Minimum voting time in blocks after a quorum is reached + uint256 _coreQuorumThreshold; // Required quorum for proposal to pass; has 10k denominator +} +/// @title Factory that deploys governance chain contracts for cross chain governance +/// @notice Requires an UpgradeExecutor, a ProxyAdmin, and a governance token +/// that implements IVotesUpgradeable to already be deployed on the governance chain. +/// To be executed prior to ParentChainGovFactory on the parent chain. + +contract GovernanceChainGovFactory is Ownable { + bool private done = false; + + event Deployed(ArbitrumTimelock coreTimelock, L2ArbitrumGovernor coreGoverner); + + error AlreadyExecuted(); + error NotAContract(address _address); + error NotAGovernanceToken(address _address); + + address immutable govLogic; + address immutable timelockLogic; + + constructor() { + govLogic = address(new L2ArbitrumGovernor()); + timelockLogic = address(new ArbitrumTimelock()); + } + + function deployStep1(DeployParams memory params) + public + virtual + onlyOwner + returns (ArbitrumTimelock, L2ArbitrumGovernor) + { + if (done) { + revert AlreadyExecuted(); + } + done = true; + // sanity checks: + // provided upgrade executor is a contract + if (!Address.isContract(params._govChainUpExec)) { + revert NotAContract(params._govChainUpExec); + } + // provided proxy admin is a contract + if (!Address.isContract(params._govChainProxyAdmin)) { + revert NotAContract(params._govChainProxyAdmin); + } + // provided governance token has an expected IVotesUpgradeable method + try IVotesUpgradeable(params._governanceToken).getPastTotalSupply(0) {} + catch (bytes memory) { + revert NotAGovernanceToken(params._governanceToken); + } + // end of sanity checks + + // deploy and init the timelock + ArbitrumTimelock coreTimelock = + deployTimelock(ProxyAdmin(params._govChainProxyAdmin), timelockLogic); + coreTimelock.initialize(params._minTimelockDelay, new address[](0), new address[](0)); + + // deploy and init the core governor + L2ArbitrumGovernor coreGov = + deployGovernor(ProxyAdmin(params._govChainProxyAdmin), govLogic); + coreGov.initialize({ + _token: IVotesUpgradeable(params._governanceToken), + _timelock: coreTimelock, + _owner: params._govChainUpExec, + _votingDelay: params._votingDelay, + _votingPeriod: params._votingPeriod, + _quorumNumerator: params._coreQuorumThreshold, + _proposalThreshold: params._proposalThreshold, + _minPeriodAfterQuorum: params._minPeriodAfterQuorum + }); + + // governor can submit proposals to timelock propose + coreTimelock.grantRole(coreTimelock.PROPOSER_ROLE(), address(coreGov)); + // upgrade executor can cancel + coreTimelock.grantRole(coreTimelock.CANCELLER_ROLE(), params._govChainUpExec); + + // anyone is allowed to execute on the timelock + coreTimelock.grantRole(coreTimelock.EXECUTOR_ROLE(), address(0)); + + // after initialisation, give admin roles to the upgrade executor + // and revoke admin roles from the timelock and from this deployer + coreTimelock.grantRole(coreTimelock.TIMELOCK_ADMIN_ROLE(), params._govChainUpExec); + coreTimelock.revokeRole(coreTimelock.TIMELOCK_ADMIN_ROLE(), address(coreTimelock)); + coreTimelock.revokeRole(coreTimelock.TIMELOCK_ADMIN_ROLE(), address(this)); + + emit Deployed(coreTimelock, coreGov); + return (coreTimelock, coreGov); + } + + function deployGovernor(ProxyAdmin _proxyAdmin, address _govChainGovernorLogic) + internal + returns (L2ArbitrumGovernor) + { + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + _govChainGovernorLogic, + address(_proxyAdmin), + bytes("") + ); + return L2ArbitrumGovernor(payable(address(proxy))); + } + + function deployTimelock(ProxyAdmin _proxyAdmin, address _govChainTimelockLogic) + internal + returns (ArbitrumTimelock) + { + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + _govChainTimelockLogic, + address(_proxyAdmin), + bytes("") + ); + return ArbitrumTimelock(payable(address(proxy))); + } +} diff --git a/src/orbit-chain-governance/factories/ParentChainGovFactory.sol b/src/orbit-chain-governance/factories/ParentChainGovFactory.sol new file mode 100644 index 00000000..cc21a49c --- /dev/null +++ b/src/orbit-chain-governance/factories/ParentChainGovFactory.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "../../L1ArbitrumTimelock.sol"; +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import "@arbitrum/nitro-contracts/src/bridge/IInbox.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; + +/// @title Factory that deploys governance chain contracts for cross chain governance +/// @notice Requires core nitro contracts (i.e., inbox), and Upgrade Executor, and a Proxy Admin to +/// be deployed on the parent chain, and a governance timelock be deployed on the governance chain +/// via GovernanceChainGovFactory. +contract ParentChainGovFactory is Ownable { + bool private done = false; + + event Deployed(L1ArbitrumTimelock timelock, address inbox); + + error AlreadyExecuted(); + error NotAContract(address _address); + + /// @param _parentChainUpExec address of UpgradeExecutor on parent chain + /// @param _parentChainProxyAdmin address of ProxyAdmin on parent chain + /// @param _inbox address of governance chain's inbox on parent chain + /// @param _governanceChainCoreTimelock address of core timelock on governance chain + /// @param _minTimelockDelay time in seconds after governance-initiated child-to-parent message is executed in the outbox before it can be executed in the timelock + function deployStep2( + address _parentChainUpExec, + address _parentChainProxyAdmin, + address _inbox, + address _governanceChainCoreTimelock, + uint256 _minTimelockDelay + ) external onlyOwner returns (L1ArbitrumTimelock timelock) { + if (done) { + revert AlreadyExecuted(); + } + done = true; + // sanity checks + if (!Address.isContract(_parentChainUpExec)) { + revert NotAContract(_parentChainUpExec); + } + if (!Address.isContract(_parentChainProxyAdmin)) { + revert NotAContract(_parentChainProxyAdmin); + } + if (!Address.isContract(_inbox)) { + revert NotAContract(_inbox); + } + // end sanity checks + + // deploy and init the timelock + timelock = deployTimelock(ProxyAdmin(_parentChainProxyAdmin)); + timelock.initialize( + _minTimelockDelay, new address[](0), _inbox, _governanceChainCoreTimelock + ); + // anyone can execute + timelock.grantRole(timelock.EXECUTOR_ROLE(), address(0)); + + // revoke admin rights and give them to the upgrade executor + timelock.grantRole(timelock.TIMELOCK_ADMIN_ROLE(), address(_parentChainUpExec)); + timelock.revokeRole(timelock.TIMELOCK_ADMIN_ROLE(), address(timelock)); + timelock.revokeRole(timelock.TIMELOCK_ADMIN_ROLE(), address(this)); + + // grant canceller role to upgrade executor; this can be used e.g. by an admin with executor affordance granted to the upgrade executor + timelock.grantRole(timelock.CANCELLER_ROLE(), address(_parentChainUpExec)); + + emit Deployed(timelock, _inbox); + } + + function deployTimelock(ProxyAdmin _proxyAdmin) + internal + returns (L1ArbitrumTimelock timelock) + { + address logic = address(new L1ArbitrumTimelock()); + TransparentUpgradeableProxy proxy = + new TransparentUpgradeableProxy(logic, address(_proxyAdmin), bytes("")); + timelock = L1ArbitrumTimelock(payable(address(proxy))); + } +} diff --git a/src/orbit-chain-governance/lib/WrappedNativeGovToken.sol b/src/orbit-chain-governance/lib/WrappedNativeGovToken.sol new file mode 100644 index 00000000..899c0834 --- /dev/null +++ b/src/orbit-chain-governance/lib/WrappedNativeGovToken.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity 0.8.16; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/draft-ERC20PermitUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20VotesUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +interface IWETH9 { + function deposit() external payable; + + function withdraw(uint256 _amount) external; +} + +/// @notice Contract that be used as an Orbit chain's governance token. +/// Functions as an ERC20 wrapper for the chain's native asset and and implements the IVotesUpgradeable interface. +/// Also includes parent chain token address for Gateway compatibility. +contract WrappedNativeGovToken is + IWETH9, + Initializable, + ERC20Upgradeable, + ERC20PermitUpgradeable, + ERC20VotesUpgradeable +{ + constructor() { + _disableInitializers(); + } + + /// @notice The address of the parent chain counterpart of this token + address public l1Address; + + /// @param _name Token name + /// @param _symbol Token symbol + /// @param _parentChainTokenAddress Address of the parent chain counterpart of this token + /// @param _initialSupply Initial token supply + /// @param _initialSupplyRecipient Recipient of initial token supply + function initialize( + string memory _name, + string memory _symbol, + address _parentChainTokenAddress, + uint256 _initialSupply, + address _initialSupplyRecipient + ) external initializer { + l1Address = _parentChainTokenAddress; + _mint(_initialSupplyRecipient, _initialSupply); + __ERC20_init(_name, _symbol); + __ERC20Permit_init(_name); + __ERC20Votes_init(); + } + + function deposit() external payable override { + depositTo(msg.sender); + } + + function withdraw(uint256 amount) external override { + withdrawTo(msg.sender, amount); + } + + function depositTo(address account) public payable { + _mint(account, msg.value); + } + + function withdrawTo(address account, uint256 amount) public { + _burn(msg.sender, amount); + (bool success,) = account.call{value: amount}(""); + require(success, "FAIL_TRANSFER"); + } + + receive() external payable { + depositTo(msg.sender); + } + + function _afterTokenTransfer(address from, address to, uint256 amount) + internal + override(ERC20Upgradeable, ERC20VotesUpgradeable) + { + super._afterTokenTransfer(from, to, amount); + } + + function _mint(address to, uint256 amount) + internal + override(ERC20Upgradeable, ERC20VotesUpgradeable) + { + super._mint(to, amount); + } + + function _burn(address account, uint256 amount) + internal + override(ERC20Upgradeable, ERC20VotesUpgradeable) + { + super._burn(account, amount); + } +} diff --git a/src/orbit-chain-governance/orbit-gov.md b/src/orbit-chain-governance/orbit-gov.md new file mode 100644 index 00000000..b12633df --- /dev/null +++ b/src/orbit-chain-governance/orbit-gov.md @@ -0,0 +1,23 @@ +## Orbit Chain Governance Deployment + +The Orbit Chain Governance Deployment factories deploy a cross chain governance system; proposal creation and voting takes place on an Orbit chain referred to as the "governance chain," which serves to govern contracts both on the governance chain and its parent chain via cross chain messages. + +This deployment is comparable to the one carried by the `L2GovernanceFactory` / `L1GovernanceFactory` contracts, except the Orbit deployment excludes certain contracts prupose-specific to the Arbitrum DAO (e.g., the DAO treasury). For info on the DAO's governance deployment, see [Arbitrum DAO Governance](../../docs/overview.md). + + +### Steps +0. Prior to Orbit Governance deployment, the following contracts should already be deployed: + - Governance chain: + - Governance ERC20 Token that implements the Open Zeppelin `IVotesUpgradeable` interface. + - `UpgradeExecutor` + - `ProxyAdmin` + - Parent chain: + - `UpgradeExecutor` + - `ProxyAdmin` +1. Deploy `GovernanceChainGovFactory`` and execute `deployStep1` +2. Deploy `ParentChainGovFactory` and execute `deployStep2`; requires address of timelock on deployed in `GovernanceChainGovFactory` +3. Grant `EXECUTOR_ROLE` affordance on the parent chain `UpgradeExecutor` to the governance timelock on the parent chain. Grant `EXECUTOR_ROLE` affordance on the governance chain `UpgradeExecutor` to the [address-alias](https://docs.arbitrum.io/arbos/l1-to-l2-messaging#address-aliasing) of the governance timelock on the parent chain. + +Optional: +1. Additional execution affordances can be given by granting the `EXECUTOR_ROLE` to additional contracts on the `UpradeExecutor`s; e.g., a multisig that can bypass voting and delays to make emergency upgrades (a la the Arbitrum DAO Security Council). +1. Other `EXECUTOR_ROLE` affordances that were granted the UpgradeExecutor prior to deployment to facilitate the deployment process can now be removed.