diff --git a/contracts/contracts/governance/L2Governance.sol b/contracts/contracts/governance/L2Governance.sol new file mode 100644 index 0000000000..8386568595 --- /dev/null +++ b/contracts/contracts/governance/L2Governance.sol @@ -0,0 +1,532 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { CCIPReceiver } from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol"; +import { Client } from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol"; +import { IARM } from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IARM.sol"; + +import { MAINNET_SELECTOR } from "../utils/CCIPChainSelectors.sol"; +import { Governable } from "./Governable.sol"; +import { Initializable } from "../utils/Initializable.sol"; +import { ITimelockController } from "../interfaces/ITimelockController.sol"; +import { ICCIPRouter } from "../interfaces/ICCIPRouter.sol"; + +bytes2 constant QUEUE_PROPOSAL_COMMAND = hex"0001"; +bytes2 constant CANCEL_PROPOSAL_COMMAND = hex"0002"; + +contract L2Governance is Governable, Initializable, CCIPReceiver { + /*************************************** + Events + ****************************************/ + /** + * @dev Emitted when timelock address is changed + */ + event TimelockChanged( + address indexed oldTimelock, + address indexed newTimelock + ); + + /** + * @dev Emitted when timelock address is changed + */ + event MainnetExectutorChanged( + address indexed oldExecutor, + address indexed newExecutor + ); + + /** + * @dev Emitted when a proposal is created. + */ + event ProposalCreated( + uint256 indexed proposalId, + address proposer, + address[] targets, + uint256[] values, + string[] signatures, + bytes[] calldatas, + string description + ); + + /** + * @dev Emitted when a proposal is queued on the Timelock. + */ + event ProposalQueued(uint256 indexed proposalId); + + /** + * @dev Emitted when a proposal is canceled. + */ + event ProposalCanceled(uint256 indexed proposalId); + + /** + * @dev Emitted when a proposal has been executed. + */ + event ProposalExecuted(uint256 indexed proposalId); + + /*************************************** + Errors + ****************************************/ + error NotMainnetExecutor(); + error NotL2Executor(); + error InvalidSourceChainSelector(); + error DuplicateProposal(uint256 proposalId); + error InvalidProposal(); + error EmptyProposal(); + error InvalidProposalLength(); + error InvalidGovernanceCommand(bytes2 command); + error InvalidProposalState(); + error EmptyAddress(); + error TokenTransfersNotAccepted(); + error CCIPRouterIsCursed(); + + /*************************************** + Storage + ****************************************/ + // Returns the current state of a proposal + enum ProposalState { + Pending, // Proposal Created + Queued, // Queued by Mainnet Governance + Ready, // Ready to be Executed + Executed + } + + /** + * @dev Mainnet Governance Exectuor + */ + address public mainnetExecutor; + + /** + * @dev L2 Timelock Controller + */ + address private timelock; + + struct ProposalDetails { + bool exists; + address proposer; + address[] targets; + uint256[] values; + string[] signatures; + bytes[] calldatas; + bytes32 descriptionHash; + } + + /** + * @dev Stores the details of the proposal by ID + */ + mapping(uint256 => ProposalDetails) public proposalDetails; + + /*************************************** + Modifiers + ****************************************/ + /** + * @dev Ensures that the requests are from Mainnet + * and from Governance Executor + */ + modifier onlyMainnetGovernance(uint64 chainSelector, address sender) { + if (chainSelector != MAINNET_SELECTOR) { + // Ensure it's from mainnet + revert InvalidSourceChainSelector(); + } + + if (sender != mainnetExecutor) { + // Ensure it's from Mainnet Governance + revert NotMainnetExecutor(); + } + + _; + } + + /** + * @dev Ensures that the calls are from L2 Timelock + * and from Governance Executor + */ + modifier onlyL2Timelock() { + if (msg.sender != timelock) { + revert NotL2Executor(); + } + + _; + } + + /** + * @dev Reverts if CCIP's Risk Management contract (ARM) is cursed + */ + modifier onlyIfNotCursed() { + IARM arm = IARM(ICCIPRouter(this.getRouter()).getArmProxy()); + + if (arm.isCursed()) { + revert CCIPRouterIsCursed(); + } + + _; + } + + /*************************************** + Constructor + ****************************************/ + constructor(address l2Router_) CCIPReceiver(l2Router_) {} + + function initialize(address timelock_, address mainnetExecutor_) + external + initializer + { + if (timelock_ == address(0)) { + revert EmptyAddress(); + } + if (mainnetExecutor_ == address(0)) { + revert EmptyAddress(); + } + timelock = timelock_; + mainnetExecutor = mainnetExecutor_; + } + + /*************************************** + CCIPReceiver + ****************************************/ + /** + * @inheritdoc CCIPReceiver + */ + function _ccipReceive(Client.Any2EVMMessage memory message) + internal + override + onlyMainnetGovernance( + message.sourceChainSelector, + abi.decode(message.sender, (address)) + ) + onlyIfNotCursed + { + if (message.destTokenAmounts.length > 0) { + revert TokenTransfersNotAccepted(); + } + + // Decode the command & message + (bytes2 cmd, bytes memory cmdData) = abi.decode( + message.data, + (bytes2, bytes) + ); + + if (cmd == QUEUE_PROPOSAL_COMMAND) { + // Queue actions + uint256 proposalId = abi.decode(cmdData, (uint256)); + _queue(proposalId); + } else if (cmd == CANCEL_PROPOSAL_COMMAND) { + // Cancel proposal + uint256 proposalId = abi.decode(cmdData, (uint256)); + _cancel(proposalId); + } else { + revert InvalidGovernanceCommand(cmd); + } + } + + /*************************************** + Governance + ****************************************/ + /** + * @dev L2 Executor is always same as Timelock + */ + function executor() external view returns (address) { + return timelock; + } + + /** + * @dev Returns the state of the proposal + * @param proposalId The proposal ID + * @return ProposalState + */ + function state(uint256 proposalId) external view returns (ProposalState) { + ProposalDetails memory details = proposalDetails[proposalId]; + if (!details.exists) { + revert InvalidProposal(); + } + + bytes32 timelockHash = _getTimelockHash(proposalId); + ITimelockController controller = ITimelockController(timelock); + + if (controller.isOperationDone(timelockHash)) { + return ProposalState.Executed; + } else if (controller.isOperationReady(timelockHash)) { + return ProposalState.Ready; + } else if (controller.isOperationPending(timelockHash)) { + return ProposalState.Queued; + } + + return ProposalState.Pending; + } + + /** + * @dev Returns a unique ID for the given proposal args + * @return Propsal ID + */ + function hashProposal( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) public pure virtual returns (uint256) { + return + uint256( + keccak256( + abi.encode(targets, values, calldatas, descriptionHash) + ) + ); + } + + /** + * @dev Returns the actions of a proposal + */ + function getActions(uint256 proposalId) + external + view + virtual + returns ( + address[] memory, + uint256[] memory, + string[] memory, + bytes[] memory + ) + { + ProposalDetails memory details = proposalDetails[proposalId]; + + if (!details.exists) { + revert InvalidProposal(); + } + + return ( + details.targets, + details.values, + details.signatures, + details.calldatas + ); + } + + /** + * @dev Encodes calldatas with optional function signature. + */ + function _encodeCalldata( + string[] memory signatures, + bytes[] memory calldatas + ) private pure returns (bytes[] memory) { + bytes[] memory fullcalldatas = new bytes[](calldatas.length); + + uint256 len = signatures.length; + for (uint256 i = 0; i < len; ++i) { + fullcalldatas[i] = bytes(signatures[i]).length == 0 + ? calldatas[i] + : bytes.concat( + bytes4(keccak256(bytes(signatures[i]))), + calldatas[i] + ); + } + + return fullcalldatas; + } + + /** + * @dev Store proposal metadata for later lookup + */ + function _storeProposal( + address proposer, + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas, + string memory description + ) private returns (uint256) { + if ( + targets.length != values.length || + targets.length != calldatas.length || + targets.length != signatures.length + ) { + revert InvalidProposalLength(); + } + + if (targets.length == 0) { + revert EmptyProposal(); + } + + bytes32 descriptionHash = keccak256(bytes(description)); + uint256 proposalId = hashProposal( + targets, + values, + _encodeCalldata(signatures, calldatas), + descriptionHash + ); + + ProposalDetails storage details = proposalDetails[proposalId]; + + if (details.exists) { + revert DuplicateProposal(proposalId); + } + + details.exists = true; + details.proposer = proposer; + details.targets = targets; + details.values = values; + details.signatures = signatures; + details.calldatas = calldatas; + details.descriptionHash = descriptionHash; + + emit ProposalCreated( + proposalId, + proposer, + targets, + values, + new string[](targets.length), + calldatas, + description + ); + + return proposalId; + } + + /** + * @dev Creates a proposal with the given args. + * Can be called by anyone + */ + function propose( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas, + string memory description + ) external virtual returns (uint256) { + return + _storeProposal( + msg.sender, + targets, + values, + signatures, + calldatas, + description + ); + } + + /** + * @dev Queues a proposal on the Timelock + * Private and only to be used by Mainnet Governance through CCIP Router + */ + function _queue(uint256 proposalId) internal { + if (this.state(proposalId) != ProposalState.Pending) { + revert InvalidProposalState(); + } + + ITimelockController controller = ITimelockController(timelock); + ProposalDetails memory details = proposalDetails[proposalId]; + + controller.scheduleBatch( + details.targets, + details.values, + _encodeCalldata(details.signatures, details.calldatas), + 0, + details.descriptionHash, + controller.getMinDelay() + ); + + emit ProposalQueued(proposalId); + } + + /** + * @dev Cancels a pending proposal on the Timelock + * Private and only to be used by Mainnet Governance through CCIP Router + */ + function _cancel(uint256 proposalId) internal { + if (this.state(proposalId) == ProposalState.Executed) { + revert InvalidProposalState(); + } + + ITimelockController controller = ITimelockController(timelock); + + bytes32 timelockHash = _getTimelockHash(proposalId); + + proposalDetails[proposalId].exists = false; + controller.cancel(timelockHash); + + emit ProposalCanceled(proposalId); + } + + /** + * @dev Returns the timelock hash for a proposal + */ + function _getTimelockHash(uint256 proposalId) + internal + view + returns (bytes32 timelockHash) + { + ITimelockController controller = ITimelockController(timelock); + + ProposalDetails memory details = proposalDetails[proposalId]; + + if (!details.exists) { + revert InvalidProposal(); + } + + timelockHash = controller.hashOperationBatch( + details.targets, + details.values, + _encodeCalldata(details.signatures, details.calldatas), + 0, + details.descriptionHash + ); + } + + function getTimelockHash(uint256 proposalId) + external + view + returns (bytes32) + { + return _getTimelockHash(proposalId); + } + + /** + * @dev Executes an already queued proposal on the Timelock. + * Can be called by anyone. Reverts if CCIP bridge + * status is cursed. + * @param proposalId Proposal ID + */ + function execute(uint256 proposalId) external payable onlyIfNotCursed { + if (this.state(proposalId) != ProposalState.Ready) { + revert InvalidProposalState(); + } + + ITimelockController controller = ITimelockController(timelock); + ProposalDetails memory details = proposalDetails[proposalId]; + + controller.executeBatch{ value: msg.value }( + details.targets, + details.values, + _encodeCalldata(details.signatures, details.calldatas), + 0, + details.descriptionHash + ); + + emit ProposalExecuted(proposalId); + } + + /*************************************** + Configuration + ****************************************/ + /** + * @dev Changes the address of the Timelock. + * Has to go through Timelock + * @param timelock_ New timelock address + */ + function setTimelock(address timelock_) external onlyL2Timelock { + if (timelock_ == address(0)) { + revert EmptyAddress(); + } + emit TimelockChanged(timelock, timelock_); + timelock = timelock_; + } + + /** + * @dev Changes the address of the Mainnet Executor. + * Has to go through Timelock + * @param executor_ New Mainnet Executor address + */ + function setMainnetExecutor(address executor_) external onlyL2Timelock { + if (executor_ == address(0)) { + revert EmptyAddress(); + } + emit MainnetExectutorChanged(mainnetExecutor, executor_); + mainnetExecutor = executor_; + } +} diff --git a/contracts/contracts/governance/L2Governor.sol b/contracts/contracts/governance/L2Governor.sol new file mode 100644 index 0000000000..93bf36725e --- /dev/null +++ b/contracts/contracts/governance/L2Governor.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { TimelockController } from "@openzeppelin/contracts/governance/TimelockController.sol"; + +contract L2Governor is TimelockController { + constructor( + uint256 minDelay, + address[] memory proposers, + address[] memory executors + ) TimelockController(minDelay, proposers, executors) {} +} diff --git a/contracts/contracts/governance/MainnetGovernanceExecutor.sol b/contracts/contracts/governance/MainnetGovernanceExecutor.sol new file mode 100644 index 0000000000..7c7abfe8f0 --- /dev/null +++ b/contracts/contracts/governance/MainnetGovernanceExecutor.sol @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Governable } from "./Governable.sol"; +import { QUEUE_PROPOSAL_COMMAND, CANCEL_PROPOSAL_COMMAND } from "./L2Governance.sol"; +import { Initializable } from "../utils/Initializable.sol"; + +import { ARBITRUM_ONE_SELECTOR } from "../utils/CCIPChainSelectors.sol"; + +import { Client } from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol"; +import { IRouterClient } from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol"; + +contract MainnetGovernanceExecutor is Governable, Initializable { + /*************************************** + Events + ****************************************/ + /** + * @dev Emitted whenever a command is forwarded to CCIP Router + */ + event CommandSentToCCIPRouter( + uint64 indexed chainSelector, + bytes32 messageId, + bytes2 commandSelector, + uint256 indexed proposalId + ); + /** + * @dev Emitted when a Chain Config is added + */ + event ChainConfigAdded( + uint64 indexed chainSelector, + address indexed l2Governance + ); + /** + * @dev Emitted when a Chain Config is removed + */ + event ChainConfigRemoved(uint64 indexed chainSelector); + + /*************************************** + Errors + ****************************************/ + error UnsupportedChain(uint64 chainSelector); + error InsufficientBalanceForFees(uint256 feesRequired); + error DuplicateChainConfig(uint64 chainSelector); + error InvalidInitializationArgLength(); + error InvalidGovernanceAddress(); + + /*************************************** + Storage + ****************************************/ + address public immutable ccipRouter; + + struct ChainConfig { + bool isSupported; + address l2Governance; + } + /** + * @dev All supported chains + */ + mapping(uint64 => ChainConfig) public chainConfig; + + constructor(address _ccipRouter) { + ccipRouter = _ccipRouter; + } + + function initialize( + uint64[] calldata chainSelectors, + address[] calldata l2Governances + ) external initializer { + uint256 len = chainSelectors.length; + if (len != l2Governances.length) { + revert InvalidInitializationArgLength(); + } + + for (uint256 i = 0; i < len; ++i) { + _addChainConfig(chainSelectors[i], l2Governances[i]); + } + } + + /*************************************** + CCIP + ****************************************/ + /** + * @dev Send a command to queue/cancel a L2 Proposal through CCIP Router + * @param commandSelector Command to send + * @param chainSelector Destination chain + * @param proposalId L2 Proposal ID + * @param maxGasLimit Max Gas Limit to use + */ + function _sendCommandToL2( + bytes2 commandSelector, + uint64 chainSelector, + uint256 proposalId, + uint256 maxGasLimit + ) internal { + // Build the message + Client.EVM2AnyMessage memory message = _buildCCIPMessage( + commandSelector, + chainSelector, + proposalId, + maxGasLimit + ); + + IRouterClient router = IRouterClient(ccipRouter); + + // Compute fees + uint256 fees = router.getFee(chainSelector, message); + + // Ensure the contract has enough balance to pay the fees + if (fees > address(this).balance) { + revert InsufficientBalanceForFees(fees); + } + + // Forward to CCIP Router + // slither-disable-next-line arbitrary-send-eth + bytes32 messageId = router.ccipSend{ value: fees }( + chainSelector, + message + ); + + emit CommandSentToCCIPRouter( + chainSelector, + messageId, + commandSelector, + proposalId + ); + } + + /** + * @dev Returns the CCIP fees that the contract needs to execute the command + * @param commandSelector Command to send + * @param chainSelector Destination chain + * @param proposalId L2 Proposal ID + * @param maxGasLimit Max Gas Limit to use + */ + function getCCIPFees( + bytes2 commandSelector, + uint64 chainSelector, + uint256 proposalId, + uint256 maxGasLimit + ) external view returns (uint256) { + // Build the message + Client.EVM2AnyMessage memory message = _buildCCIPMessage( + commandSelector, + chainSelector, + proposalId, + maxGasLimit + ); + + return IRouterClient(ccipRouter).getFee(chainSelector, message); + } + + /** + * @dev Builds the CCIP message for the given command + * @param commandSelector Command to send + * @param chainSelector Destination chain + * @param proposalId L2 Proposal ID + * @param maxGasLimit Max Gas Limit to use + */ + function _buildCCIPMessage( + bytes2 commandSelector, + uint64 chainSelector, + uint256 proposalId, + uint256 maxGasLimit + ) internal view returns (Client.EVM2AnyMessage memory message) { + ChainConfig memory config = chainConfig[chainSelector]; + + // Ensure it's a supported chain + if (!config.isSupported) { + revert UnsupportedChain(chainSelector); + } + + // Build the command data + bytes memory data = abi.encode( + // Command Selector + commandSelector, + // Encoded Command Data + abi.encode(proposalId) + ); + + bytes memory extraArgs = hex""; + + // Set gas limit if needed + if (maxGasLimit > 0) { + extraArgs = Client._argsToBytes( + // Set gas limit + Client.EVMExtraArgsV1({ gasLimit: maxGasLimit }) + ); + } + + // Build the message + message = Client.EVM2AnyMessage({ + receiver: abi.encode(config.l2Governance), + data: data, + tokenAmounts: new Client.EVMTokenAmount[](0), + extraArgs: extraArgs, + feeToken: address(0) + }); + } + + /** + * @dev Send a command to queue a L2 Proposal through CCIP Router. + * Has to come through Governance + * @param chainSelector Destination chain + * @param proposalId L2 Proposal ID + * @param maxGasLimit Max Gas Limit to use + */ + function queueL2Proposal( + uint64 chainSelector, + uint256 proposalId, + uint256 maxGasLimit + ) external payable onlyGovernor { + _sendCommandToL2( + QUEUE_PROPOSAL_COMMAND, + chainSelector, + proposalId, + maxGasLimit + ); + } + + /** + * @dev Send a command to cancel a L2 Proposal through CCIP Router. + * Has to come through Governance + * @param chainSelector Destination chain + * @param proposalId L2 Proposal ID + * @param maxGasLimit Max Gas Limit to use + */ + function cancelL2Proposal( + uint64 chainSelector, + uint256 proposalId, + uint256 maxGasLimit + ) external payable onlyGovernor { + _sendCommandToL2( + CANCEL_PROPOSAL_COMMAND, + chainSelector, + proposalId, + maxGasLimit + ); + } + + /*************************************** + Configuration + ****************************************/ + /** + * @dev Add a L2 Chain to forward commands to. + * Has to go through Governance + * @param chainSelector New timelock address + * @param l2Governance New timelock address + */ + function addChainConfig(uint64 chainSelector, address l2Governance) + external + onlyGovernor + { + _addChainConfig(chainSelector, l2Governance); + } + + function _addChainConfig(uint64 chainSelector, address l2Governance) + internal + { + if (chainConfig[chainSelector].isSupported) { + revert DuplicateChainConfig(chainSelector); + } + + if (l2Governance == address(0)) { + revert InvalidGovernanceAddress(); + } + + chainConfig[chainSelector] = ChainConfig({ + isSupported: true, + l2Governance: l2Governance + }); + + emit ChainConfigAdded(chainSelector, l2Governance); + } + + /** + * @dev Remove a supported L2 chain. + * Has to go through Governance + * @param chainSelector New timelock address + */ + function removeChainConfig(uint64 chainSelector) external onlyGovernor { + if (!chainConfig[chainSelector].isSupported) { + revert UnsupportedChain(chainSelector); + } + + chainConfig[chainSelector] = ChainConfig({ + isSupported: false, + l2Governance: address(0) + }); + + emit ChainConfigRemoved(chainSelector); + } + + // Accept ETH + receive() external payable {} +} diff --git a/contracts/contracts/interfaces/ICCIPRouter.sol b/contracts/contracts/interfaces/ICCIPRouter.sol new file mode 100644 index 0000000000..576a345693 --- /dev/null +++ b/contracts/contracts/interfaces/ICCIPRouter.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface ICCIPRouter { + function getArmProxy() external view returns (address); +} diff --git a/contracts/contracts/interfaces/ITimelockController.sol b/contracts/contracts/interfaces/ITimelockController.sol new file mode 100644 index 0000000000..4d524a06d7 --- /dev/null +++ b/contracts/contracts/interfaces/ITimelockController.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface ITimelockController { + function isOperation(bytes32 id) external view returns (bool); + + function isOperationPending(bytes32 id) external view returns (bool); + + function isOperationReady(bytes32 id) external view returns (bool); + + function isOperationDone(bytes32 id) external view returns (bool); + + function getMinDelay() external view returns (uint256 duration); + + function hashOperationBatch( + address[] calldata targets, + uint256[] calldata values, + bytes[] calldata datas, + bytes32 predecessor, + bytes32 salt + ) external pure returns (bytes32 hash); + + function scheduleBatch( + address[] calldata targets, + uint256[] calldata values, + bytes[] calldata datas, + bytes32 predecessor, + bytes32 salt, + uint256 delay + ) external; + + function executeBatch( + address[] calldata targets, + uint256[] calldata values, + bytes[] calldata datas, + bytes32 predecessor, + bytes32 salt + ) external payable; + + function cancel(bytes32 id) external; +} diff --git a/contracts/contracts/mocks/MockCCIPRouter.sol b/contracts/contracts/mocks/MockCCIPRouter.sol new file mode 100644 index 0000000000..17e18ce8e0 --- /dev/null +++ b/contracts/contracts/mocks/MockCCIPRouter.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Client } from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol"; + +import { CCIPReceiver } from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol"; + +contract MockCCIPRouter { + uint256 public feeToReturn; + bool public isCursed_; + bool public forwardRequests; + + uint64 public lastChainSelector; + Client.EVM2AnyMessage public lastMessage; + + constructor() {} + + function ccipSend( + uint64 destinationChainSelector, + Client.EVM2AnyMessage calldata message + ) external payable returns (bytes32) { + if (forwardRequests) { + address receiver = abi.decode(message.receiver, (address)); + Client.Any2EVMMessage memory messageToForward = Client + .Any2EVMMessage({ + sender: message.receiver, + data: message.data, + destTokenAmounts: message.tokenAmounts, + sourceChainSelector: destinationChainSelector, + messageId: bytes32(hex"deadfeed") + }); + + CCIPReceiver(receiver).ccipReceive(messageToForward); + } else { + lastChainSelector = destinationChainSelector; + lastMessage = message; + } + + return bytes32(hex"deadfeed"); + } + + function mockSend( + address receiver, + uint64 destinationChainSelector, + address sender, + bytes calldata data, + Client.EVMTokenAmount[] calldata tokenAmounts + ) external payable returns (bytes32) { + Client.Any2EVMMessage memory messageToForward = Client.Any2EVMMessage({ + sender: abi.encode(sender), + data: data, + destTokenAmounts: tokenAmounts, + sourceChainSelector: destinationChainSelector, + messageId: bytes32(hex"deadfeed") + }); + + CCIPReceiver(receiver).ccipReceive(messageToForward); + + return bytes32(hex"deadfeed"); + } + + function getFee(uint64, Client.EVM2AnyMessage calldata) + external + view + returns (uint256) + { + return feeToReturn; + } + + function setFee(uint256 fee) external { + feeToReturn = fee; + } + + function getArmProxy() external view returns (address) { + return address(this); + } + + function isCursed() external view returns (bool) { + return isCursed_; + } + + function setIsCursed(bool value) external { + isCursed_ = value; + } + + function setForwardRequests(bool value) external { + forwardRequests = value; + } +} diff --git a/contracts/contracts/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol index b564900de9..9408b9855a 100644 --- a/contracts/contracts/proxies/Proxies.sol +++ b/contracts/contracts/proxies/Proxies.sol @@ -209,3 +209,19 @@ contract OETHBuybackProxy is InitializeGovernedUpgradeabilityProxy { contract BridgedWOETHProxy is InitializeGovernedUpgradeabilityProxy { } + +/** + * @notice L2GovernanceProxy delegates calls to L2Governance implementation + */ +contract L2GovernanceProxy is InitializeGovernedUpgradeabilityProxy { + +} + +/** + * @notice MainnetGovernanceExecutorProxy delegates calls to MainnetGovernanceExecutor implementation + */ +contract MainnetGovernanceExecutorProxy is + InitializeGovernedUpgradeabilityProxy +{ + +} diff --git a/contracts/contracts/utils/CCIPChainSelectors.sol b/contracts/contracts/utils/CCIPChainSelectors.sol new file mode 100644 index 0000000000..f0cad1bde5 --- /dev/null +++ b/contracts/contracts/utils/CCIPChainSelectors.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Ref: https://docs.chain.link/ccip/supported-networks/v1_2_0/mainnet#arbitrum-mainnet +uint64 constant MAINNET_SELECTOR = 5009297550715157269; +uint64 constant ARBITRUM_ONE_SELECTOR = 4949039107694359620; diff --git a/contracts/deploy/000_mock.js b/contracts/deploy/000_mock.js index 824a69f8a5..e8ed43556c 100644 --- a/contracts/deploy/000_mock.js +++ b/contracts/deploy/000_mock.js @@ -409,6 +409,10 @@ const deployMocks = async ({ getNamedAccounts, deployments }) => { from: deployerAddr, }); + await deploy("MockCCIPRouter", { + from: deployerAddr, + }); + console.log("000_mock deploy done."); return true; diff --git a/contracts/deploy/001_core.js b/contracts/deploy/001_core.js index 73fb309bae..a613916ffe 100644 --- a/contracts/deploy/001_core.js +++ b/contracts/deploy/001_core.js @@ -12,6 +12,7 @@ const { deployWithConfirmation, withConfirmation } = require("../utils/deploy"); const { metapoolLPCRVPid, lusdMetapoolLPCRVPid, + CCIPChainSelectors, } = require("../utils/constants"); const log = require("../utils/logger")("deploy:001_core"); @@ -1207,6 +1208,100 @@ const deployOUSDSwapper = async () => { await vault.connect(sGovernor).setOracleSlippage(assetAddresses.USDT, 50); }; +const deployL2Contracts = async () => { + if (hre.network.name != "hardhat") { + // Only for unit tests + return; + } + + const { deployerAddr, governorAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + + const mockRouter = await ethers.getContract("MockCCIPRouter"); + + // Deploy proxies + await deployWithConfirmation("MainnetGovernanceExecutorProxy"); + await deployWithConfirmation("L2GovernanceProxy"); + + const executorProxy = await ethers.getContract( + "MainnetGovernanceExecutorProxy" + ); + const l2GovernanceProxy = await ethers.getContract("L2GovernanceProxy"); + + // Deploy Implemenations + await deployWithConfirmation("MainnetGovernanceExecutor", [ + mockRouter.address, + ]); + const executorImpl = await ethers.getContract("MainnetGovernanceExecutor"); + await deployWithConfirmation("L2Governance", [mockRouter.address]); + const l2GovernanceImpl = await ethers.getContract("L2Governance"); + + // Deploy L2Governor + await deployWithConfirmation("L2Governor", [ + 60 * 5, // 5 min Timelock + [l2GovernanceProxy.address], + [l2GovernanceProxy.address], + ]); + const l2Governor = await ethers.getContract("L2Governor"); + + // Init L2GovernanceProxy + const l2GovernanceInitData = l2GovernanceImpl.interface.encodeFunctionData( + "initialize(address,address)", + [l2Governor.address, executorProxy.address] + ); + + await l2GovernanceProxy.connect(sDeployer)[ + // eslint-disable-next-line no-unexpected-multiline + "initialize(address,address,bytes)" + ](l2GovernanceImpl.address, l2Governor.address, l2GovernanceInitData); + log("Initialized L2GovernanceProxy"); + + // Init MainnetGovernanceExecutorProxy + const executorInitData = executorImpl.interface.encodeFunctionData( + "initialize(uint64[],address[])", + [[CCIPChainSelectors.ArbitrumOne], [l2GovernanceProxy.address]] + ); + + await executorProxy.connect(sDeployer)[ + // eslint-disable-next-line no-unexpected-multiline + "initialize(address,address,bytes)" + ](executorImpl.address, governorAddr, executorInitData); + log("Initialized MainnetGovernanceExecutorProxy"); + + // Deploy bridged WOETH + await deployWithConfirmation("BridgedWOETHProxy"); + const woethProxy = await ethers.getContract("BridgedWOETHProxy"); + + await deployWithConfirmation("BridgedWOETH"); + const woethImpl = await ethers.getContract("BridgedWOETH"); + + const woethInitData = woethImpl.interface.encodeFunctionData( + "initialize()", + [] + ); + await woethProxy.connect(sDeployer)[ + // eslint-disable-next-line no-unexpected-multiline + "initialize(address,address,bytes)" + ](woethImpl.address, l2Governor.address, woethInitData); + log("Initialized BridgedWOETHProxy"); + + // Grant roles to Governor + const woeth = await ethers.getContractAt("BridgedWOETH", woethProxy.address); + await woeth + .connect(sDeployer) + .grantRole( + "0x0000000000000000000000000000000000000000000000000000000000000000", + l2Governor.address + ); + + await woeth + .connect(sDeployer) + .revokeRole( + "0x0000000000000000000000000000000000000000000000000000000000000000", + deployerAddr + ); +}; + const main = async () => { console.log("Running 001_core deployment..."); await deployOracles(); @@ -1232,6 +1327,10 @@ const main = async () => { await deployWOusd(); await deployOETHSwapper(); await deployOUSDSwapper(); + + // Deploy only L2 contracts + await deployL2Contracts(); + console.log("001_core deploy done."); return true; }; diff --git a/contracts/deploy/085_deploy_l2_governance_proxies.js b/contracts/deploy/085_deploy_l2_governance_proxies.js new file mode 100644 index 0000000000..54c6da758b --- /dev/null +++ b/contracts/deploy/085_deploy_l2_governance_proxies.js @@ -0,0 +1,36 @@ +const { isArbitrumOneOrFork, isMainnetOrFork } = require("../test/helpers"); +const { deployWithConfirmation } = require("../utils/deploy"); +const { impersonateAndFund } = require("../utils/signers"); + +const deployName = "085_deploy_l2_governance_proxies"; + +const main = async (hre) => { + console.log(`Running ${deployName} deployment on ${hre.network.name}...`); + + const { deployerAddr } = await getNamedAccounts(); + + await impersonateAndFund(deployerAddr); + + if (isArbitrumOneOrFork) { + // Deploy L2 Governor on Arbitrum One + const l2GovernanceProxy = await deployWithConfirmation("L2GovernanceProxy"); + console.log("L2GovernanceProxy address:", l2GovernanceProxy.address); + } else if (isMainnetOrFork) { + // Deploy Governance Executor on Mainnet + const mainnetGovernanceExecutorProxy = await deployWithConfirmation( + "MainnetGovernanceExecutorProxy" + ); + console.log( + "MainnetGovernanceExecutorProxy address:", + mainnetGovernanceExecutorProxy.address + ); + } + + console.log(`${deployName} deploy done.`); +}; + +main.id = deployName; +main.skip = !(isArbitrumOneOrFork || isMainnetOrFork); +main.tags = ["arbitrumOne", "mainnet"]; + +module.exports = main; diff --git a/contracts/deploy/086_l2_governor.js b/contracts/deploy/086_l2_governor.js new file mode 100644 index 0000000000..e08da800f8 --- /dev/null +++ b/contracts/deploy/086_l2_governor.js @@ -0,0 +1,60 @@ +const { isCI } = require("../test/helpers"); +const addresses = require("../utils/addresses"); +const { deployOnArb } = require("../utils/delpoy-l2"); +const { deployWithConfirmation, withConfirmation } = require("../utils/deploy"); +const { impersonateAndFund } = require("../utils/signers"); +const { getTxOpts } = require("../utils/tx"); + +module.exports = deployOnArb( + { + deployName: "086_l2_governance", + }, + async ({ ethers }) => { + const { deployerAddr } = await getNamedAccounts(); + + const governanceProxy = await ethers.getContract("L2GovernanceProxy"); + + let sDeployer = await ethers.provider.getSigner(deployerAddr); + if (isCI) { + sDeployer = await impersonateAndFund(await governanceProxy.governor()); + } + + // Deploy L2Governor + await deployWithConfirmation("L2Governor", [ + 86400, // 1d Timelock + [governanceProxy.address], // Only L2Governance can propose + [governanceProxy.address], // Only L2Governance can execute + ]); + const governor = await ethers.getContract("L2Governor"); + console.log("L2Governor deployed: ", governor.address); + + // Deploy L2Governance Implementation + await deployWithConfirmation("L2Governance", [ + addresses.arbitrumOne.CCIPRouter, // CCIP Router on Arbitrum + ]); + const governanceImpl = await ethers.getContract("L2Governance"); + console.log("L2Governance deployed: ", governanceImpl.address); + + // Build initialization data for implementation + const initData = governanceImpl.interface.encodeFunctionData( + "initialize(address,address)", + [ + governor.address, // Timelock/L2Governor + addresses.mainnet.MainnetGovernanceExecutorProxy, + ] + ); + // Initialize Proxy + const initFunction = "initialize(address,address,bytes)"; + await withConfirmation( + governanceProxy.connect(sDeployer)[initFunction]( + governanceImpl.address, + // Governor (For proxy upgrades, unused in the implementation) + // L2Governor (Timelock) can upgrade L2Governance + governor.address, + initData, // Implementation initialization + await getTxOpts() + ) + ); + console.log("Initialized L2Governance proxy and implementation"); + } +); diff --git a/contracts/deploy/087_mainnet_executor.js b/contracts/deploy/087_mainnet_executor.js new file mode 100644 index 0000000000..d17dea8294 --- /dev/null +++ b/contracts/deploy/087_mainnet_executor.js @@ -0,0 +1,63 @@ +const { isCI } = require("../test/helpers"); +const addresses = require("../utils/addresses"); +const { CCIPChainSelectors } = require("../utils/constants"); +const { + deploymentWithGovernanceProposal, + deployWithConfirmation, + withConfirmation, +} = require("../utils/deploy"); +const { impersonateAndFund } = require("../utils/signers"); +const { getTxOpts } = require("../utils/tx"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "087_mainnet_executor", + deployerIsProposer: false, + forceSkip: false, + forceDeploy: false, + // proposalId: "", + }, + async ({ ethers }) => { + const { deployerAddr, timelockAddr } = await getNamedAccounts(); + + const executorProxy = await ethers.getContract( + "MainnetGovernanceExecutorProxy" + ); + + let sDeployer = await ethers.provider.getSigner(deployerAddr); + if (isCI) { + sDeployer = await impersonateAndFund(await executorProxy.governor()); + } + + // Deploy MainnetGovernanceExecutor + await deployWithConfirmation("MainnetGovernanceExecutor", [ + addresses.mainnet.CCIPRouter, + ]); + const executorImpl = await ethers.getContract("MainnetGovernanceExecutor"); + + // Build initialization data for implementation + const initData = executorImpl.interface.encodeFunctionData( + "initialize(uint64[],address[])", + [ + // Arbitrum One Chain Config + [CCIPChainSelectors.ArbitrumOne], + [addresses.arbitrumOne.L2GovernanceProxy], + ] + ); + + // Initialize Proxy + const initFunction = "initialize(address,address,bytes)"; + await withConfirmation( + executorProxy.connect(sDeployer)[initFunction]( + executorImpl.address, + timelockAddr, + initData, // Implementation initialization + await getTxOpts() + ) + ); + + return { + actions: [], + }; + } +); diff --git a/contracts/deployments/arbitrumOne/solcInputs/b0ede3bd4858a37c1b3c78a41a5500de.json b/contracts/deployments/arbitrumOne/solcInputs/b0ede3bd4858a37c1b3c78a41a5500de.json deleted file mode 100644 index 1ac9f784f0..0000000000 --- a/contracts/deployments/arbitrumOne/solcInputs/b0ede3bd4858a37c1b3c78a41a5500de.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "language": "Solidity", - "sources": { - "@openzeppelin/contracts/access/AccessControl.sol": { - "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts v4.4.1 (access/AccessControl.sol)\n\npragma solidity ^0.8.0;\n\nimport \"./IAccessControl.sol\";\nimport \"../utils/Context.sol\";\nimport \"../utils/Strings.sol\";\nimport \"../utils/introspection/ERC165.sol\";\n\n/**\n * @dev Contract module that allows children to implement role-based access\n * control mechanisms. This is a lightweight version that doesn't allow enumerating role\n * members except through off-chain means by accessing the contract event logs. Some\n * applications may benefit from on-chain enumerability, for those cases see\n * {AccessControlEnumerable}.\n *\n * Roles are referred to by their `bytes32` identifier. These should be exposed\n * in the external API and be unique. The best way to achieve this is by\n * using `public constant` hash digests:\n *\n * ```\n * bytes32 public constant MY_ROLE = keccak256(\"MY_ROLE\");\n * ```\n *\n * Roles can be used to represent a set of permissions. To restrict access to a\n * function call, use {hasRole}:\n *\n * ```\n * function foo() public {\n * require(hasRole(MY_ROLE, msg.sender));\n * ...\n * }\n * ```\n *\n * Roles can be granted and revoked dynamically via the {grantRole} and\n * {revokeRole} functions. Each role has an associated admin role, and only\n * accounts that have a role's admin role can call {grantRole} and {revokeRole}.\n *\n * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means\n * that only accounts with this role will be able to grant or revoke other\n * roles. More complex role relationships can be created by using\n * {_setRoleAdmin}.\n *\n * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to\n * grant and revoke this role. Extra precautions should be taken to secure\n * accounts that have been granted it.\n */\nabstract contract AccessControl is Context, IAccessControl, ERC165 {\n struct RoleData {\n mapping(address => bool) members;\n bytes32 adminRole;\n }\n\n mapping(bytes32 => RoleData) private _roles;\n\n bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;\n\n /**\n * @dev Modifier that checks that an account has a specific role. Reverts\n * with a standardized message including the required role.\n *\n * The format of the revert reason is given by the following regular expression:\n *\n * /^AccessControl: account (0x[0-9a-f]{40}) is missing role (0x[0-9a-f]{64})$/\n *\n * _Available since v4.1._\n */\n modifier onlyRole(bytes32 role) {\n _checkRole(role, _msgSender());\n _;\n }\n\n /**\n * @dev See {IERC165-supportsInterface}.\n */\n function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {\n return interfaceId == type(IAccessControl).interfaceId || super.supportsInterface(interfaceId);\n }\n\n /**\n * @dev Returns `true` if `account` has been granted `role`.\n */\n function hasRole(bytes32 role, address account) public view override returns (bool) {\n return _roles[role].members[account];\n }\n\n /**\n * @dev Revert with a standard message if `account` is missing `role`.\n *\n * The format of the revert reason is given by the following regular expression:\n *\n * /^AccessControl: account (0x[0-9a-f]{40}) is missing role (0x[0-9a-f]{64})$/\n */\n function _checkRole(bytes32 role, address account) internal view {\n if (!hasRole(role, account)) {\n revert(\n string(\n abi.encodePacked(\n \"AccessControl: account \",\n Strings.toHexString(uint160(account), 20),\n \" is missing role \",\n Strings.toHexString(uint256(role), 32)\n )\n )\n );\n }\n }\n\n /**\n * @dev Returns the admin role that controls `role`. See {grantRole} and\n * {revokeRole}.\n *\n * To change a role's admin, use {_setRoleAdmin}.\n */\n function getRoleAdmin(bytes32 role) public view override returns (bytes32) {\n return _roles[role].adminRole;\n }\n\n /**\n * @dev Grants `role` to `account`.\n *\n * If `account` had not been already granted `role`, emits a {RoleGranted}\n * event.\n *\n * Requirements:\n *\n * - the caller must have ``role``'s admin role.\n */\n function grantRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) {\n _grantRole(role, account);\n }\n\n /**\n * @dev Revokes `role` from `account`.\n *\n * If `account` had been granted `role`, emits a {RoleRevoked} event.\n *\n * Requirements:\n *\n * - the caller must have ``role``'s admin role.\n */\n function revokeRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) {\n _revokeRole(role, account);\n }\n\n /**\n * @dev Revokes `role` from the calling account.\n *\n * Roles are often managed via {grantRole} and {revokeRole}: this function's\n * purpose is to provide a mechanism for accounts to lose their privileges\n * if they are compromised (such as when a trusted device is misplaced).\n *\n * If the calling account had been revoked `role`, emits a {RoleRevoked}\n * event.\n *\n * Requirements:\n *\n * - the caller must be `account`.\n */\n function renounceRole(bytes32 role, address account) public virtual override {\n require(account == _msgSender(), \"AccessControl: can only renounce roles for self\");\n\n _revokeRole(role, account);\n }\n\n /**\n * @dev Grants `role` to `account`.\n *\n * If `account` had not been already granted `role`, emits a {RoleGranted}\n * event. Note that unlike {grantRole}, this function doesn't perform any\n * checks on the calling account.\n *\n * [WARNING]\n * ====\n * This function should only be called from the constructor when setting\n * up the initial roles for the system.\n *\n * Using this function in any other way is effectively circumventing the admin\n * system imposed by {AccessControl}.\n * ====\n *\n * NOTE: This function is deprecated in favor of {_grantRole}.\n */\n function _setupRole(bytes32 role, address account) internal virtual {\n _grantRole(role, account);\n }\n\n /**\n * @dev Sets `adminRole` as ``role``'s admin role.\n *\n * Emits a {RoleAdminChanged} event.\n */\n function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {\n bytes32 previousAdminRole = getRoleAdmin(role);\n _roles[role].adminRole = adminRole;\n emit RoleAdminChanged(role, previousAdminRole, adminRole);\n }\n\n /**\n * @dev Grants `role` to `account`.\n *\n * Internal function without access restriction.\n */\n function _grantRole(bytes32 role, address account) internal virtual {\n if (!hasRole(role, account)) {\n _roles[role].members[account] = true;\n emit RoleGranted(role, account, _msgSender());\n }\n }\n\n /**\n * @dev Revokes `role` from `account`.\n *\n * Internal function without access restriction.\n */\n function _revokeRole(bytes32 role, address account) internal virtual {\n if (hasRole(role, account)) {\n _roles[role].members[account] = false;\n emit RoleRevoked(role, account, _msgSender());\n }\n }\n}\n" - }, - "@openzeppelin/contracts/access/AccessControlEnumerable.sol": { - "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts v4.4.1 (access/AccessControlEnumerable.sol)\n\npragma solidity ^0.8.0;\n\nimport \"./IAccessControlEnumerable.sol\";\nimport \"./AccessControl.sol\";\nimport \"../utils/structs/EnumerableSet.sol\";\n\n/**\n * @dev Extension of {AccessControl} that allows enumerating the members of each role.\n */\nabstract contract AccessControlEnumerable is IAccessControlEnumerable, AccessControl {\n using EnumerableSet for EnumerableSet.AddressSet;\n\n mapping(bytes32 => EnumerableSet.AddressSet) private _roleMembers;\n\n /**\n * @dev See {IERC165-supportsInterface}.\n */\n function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {\n return interfaceId == type(IAccessControlEnumerable).interfaceId || super.supportsInterface(interfaceId);\n }\n\n /**\n * @dev Returns one of the accounts that have `role`. `index` must be a\n * value between 0 and {getRoleMemberCount}, non-inclusive.\n *\n * Role bearers are not sorted in any particular way, and their ordering may\n * change at any point.\n *\n * WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure\n * you perform all queries on the same block. See the following\n * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post]\n * for more information.\n */\n function getRoleMember(bytes32 role, uint256 index) public view override returns (address) {\n return _roleMembers[role].at(index);\n }\n\n /**\n * @dev Returns the number of accounts that have `role`. Can be used\n * together with {getRoleMember} to enumerate all bearers of a role.\n */\n function getRoleMemberCount(bytes32 role) public view override returns (uint256) {\n return _roleMembers[role].length();\n }\n\n /**\n * @dev Overload {_grantRole} to track enumerable memberships\n */\n function _grantRole(bytes32 role, address account) internal virtual override {\n super._grantRole(role, account);\n _roleMembers[role].add(account);\n }\n\n /**\n * @dev Overload {_revokeRole} to track enumerable memberships\n */\n function _revokeRole(bytes32 role, address account) internal virtual override {\n super._revokeRole(role, account);\n _roleMembers[role].remove(account);\n }\n}\n" - }, - "@openzeppelin/contracts/access/IAccessControl.sol": { - "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts v4.4.1 (access/IAccessControl.sol)\n\npragma solidity ^0.8.0;\n\n/**\n * @dev External interface of AccessControl declared to support ERC165 detection.\n */\ninterface IAccessControl {\n /**\n * @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole`\n *\n * `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite\n * {RoleAdminChanged} not being emitted signaling this.\n *\n * _Available since v3.1._\n */\n event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole);\n\n /**\n * @dev Emitted when `account` is granted `role`.\n *\n * `sender` is the account that originated the contract call, an admin role\n * bearer except when using {AccessControl-_setupRole}.\n */\n event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);\n\n /**\n * @dev Emitted when `account` is revoked `role`.\n *\n * `sender` is the account that originated the contract call:\n * - if using `revokeRole`, it is the admin role bearer\n * - if using `renounceRole`, it is the role bearer (i.e. `account`)\n */\n event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);\n\n /**\n * @dev Returns `true` if `account` has been granted `role`.\n */\n function hasRole(bytes32 role, address account) external view returns (bool);\n\n /**\n * @dev Returns the admin role that controls `role`. See {grantRole} and\n * {revokeRole}.\n *\n * To change a role's admin, use {AccessControl-_setRoleAdmin}.\n */\n function getRoleAdmin(bytes32 role) external view returns (bytes32);\n\n /**\n * @dev Grants `role` to `account`.\n *\n * If `account` had not been already granted `role`, emits a {RoleGranted}\n * event.\n *\n * Requirements:\n *\n * - the caller must have ``role``'s admin role.\n */\n function grantRole(bytes32 role, address account) external;\n\n /**\n * @dev Revokes `role` from `account`.\n *\n * If `account` had been granted `role`, emits a {RoleRevoked} event.\n *\n * Requirements:\n *\n * - the caller must have ``role``'s admin role.\n */\n function revokeRole(bytes32 role, address account) external;\n\n /**\n * @dev Revokes `role` from the calling account.\n *\n * Roles are often managed via {grantRole} and {revokeRole}: this function's\n * purpose is to provide a mechanism for accounts to lose their privileges\n * if they are compromised (such as when a trusted device is misplaced).\n *\n * If the calling account had been granted `role`, emits a {RoleRevoked}\n * event.\n *\n * Requirements:\n *\n * - the caller must be `account`.\n */\n function renounceRole(bytes32 role, address account) external;\n}\n" - }, - "@openzeppelin/contracts/access/IAccessControlEnumerable.sol": { - "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts v4.4.1 (access/IAccessControlEnumerable.sol)\n\npragma solidity ^0.8.0;\n\nimport \"./IAccessControl.sol\";\n\n/**\n * @dev External interface of AccessControlEnumerable declared to support ERC165 detection.\n */\ninterface IAccessControlEnumerable is IAccessControl {\n /**\n * @dev Returns one of the accounts that have `role`. `index` must be a\n * value between 0 and {getRoleMemberCount}, non-inclusive.\n *\n * Role bearers are not sorted in any particular way, and their ordering may\n * change at any point.\n *\n * WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure\n * you perform all queries on the same block. See the following\n * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post]\n * for more information.\n */\n function getRoleMember(bytes32 role, uint256 index) external view returns (address);\n\n /**\n * @dev Returns the number of accounts that have `role`. Can be used\n * together with {getRoleMember} to enumerate all bearers of a role.\n */\n function getRoleMemberCount(bytes32 role) external view returns (uint256);\n}\n" - }, - "@openzeppelin/contracts/token/ERC20/ERC20.sol": { - "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts v4.4.1 (token/ERC20/ERC20.sol)\n\npragma solidity ^0.8.0;\n\nimport \"./IERC20.sol\";\nimport \"./extensions/IERC20Metadata.sol\";\nimport \"../../utils/Context.sol\";\n\n/**\n * @dev Implementation of the {IERC20} interface.\n *\n * This implementation is agnostic to the way tokens are created. This means\n * that a supply mechanism has to be added in a derived contract using {_mint}.\n * For a generic mechanism see {ERC20PresetMinterPauser}.\n *\n * TIP: For a detailed writeup see our guide\n * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How\n * to implement supply mechanisms].\n *\n * We have followed general OpenZeppelin Contracts guidelines: functions revert\n * instead returning `false` on failure. This behavior is nonetheless\n * conventional and does not conflict with the expectations of ERC20\n * applications.\n *\n * Additionally, an {Approval} event is emitted on calls to {transferFrom}.\n * This allows applications to reconstruct the allowance for all accounts just\n * by listening to said events. Other implementations of the EIP may not emit\n * these events, as it isn't required by the specification.\n *\n * Finally, the non-standard {decreaseAllowance} and {increaseAllowance}\n * functions have been added to mitigate the well-known issues around setting\n * allowances. See {IERC20-approve}.\n */\ncontract ERC20 is Context, IERC20, IERC20Metadata {\n mapping(address => uint256) private _balances;\n\n mapping(address => mapping(address => uint256)) private _allowances;\n\n uint256 private _totalSupply;\n\n string private _name;\n string private _symbol;\n\n /**\n * @dev Sets the values for {name} and {symbol}.\n *\n * The default value of {decimals} is 18. To select a different value for\n * {decimals} you should overload it.\n *\n * All two of these values are immutable: they can only be set once during\n * construction.\n */\n constructor(string memory name_, string memory symbol_) {\n _name = name_;\n _symbol = symbol_;\n }\n\n /**\n * @dev Returns the name of the token.\n */\n function name() public view virtual override returns (string memory) {\n return _name;\n }\n\n /**\n * @dev Returns the symbol of the token, usually a shorter version of the\n * name.\n */\n function symbol() public view virtual override returns (string memory) {\n return _symbol;\n }\n\n /**\n * @dev Returns the number of decimals used to get its user representation.\n * For example, if `decimals` equals `2`, a balance of `505` tokens should\n * be displayed to a user as `5.05` (`505 / 10 ** 2`).\n *\n * Tokens usually opt for a value of 18, imitating the relationship between\n * Ether and Wei. This is the value {ERC20} uses, unless this function is\n * overridden;\n *\n * NOTE: This information is only used for _display_ purposes: it in\n * no way affects any of the arithmetic of the contract, including\n * {IERC20-balanceOf} and {IERC20-transfer}.\n */\n function decimals() public view virtual override returns (uint8) {\n return 18;\n }\n\n /**\n * @dev See {IERC20-totalSupply}.\n */\n function totalSupply() public view virtual override returns (uint256) {\n return _totalSupply;\n }\n\n /**\n * @dev See {IERC20-balanceOf}.\n */\n function balanceOf(address account) public view virtual override returns (uint256) {\n return _balances[account];\n }\n\n /**\n * @dev See {IERC20-transfer}.\n *\n * Requirements:\n *\n * - `recipient` cannot be the zero address.\n * - the caller must have a balance of at least `amount`.\n */\n function transfer(address recipient, uint256 amount) public virtual override returns (bool) {\n _transfer(_msgSender(), recipient, amount);\n return true;\n }\n\n /**\n * @dev See {IERC20-allowance}.\n */\n function allowance(address owner, address spender) public view virtual override returns (uint256) {\n return _allowances[owner][spender];\n }\n\n /**\n * @dev See {IERC20-approve}.\n *\n * Requirements:\n *\n * - `spender` cannot be the zero address.\n */\n function approve(address spender, uint256 amount) public virtual override returns (bool) {\n _approve(_msgSender(), spender, amount);\n return true;\n }\n\n /**\n * @dev See {IERC20-transferFrom}.\n *\n * Emits an {Approval} event indicating the updated allowance. This is not\n * required by the EIP. See the note at the beginning of {ERC20}.\n *\n * Requirements:\n *\n * - `sender` and `recipient` cannot be the zero address.\n * - `sender` must have a balance of at least `amount`.\n * - the caller must have allowance for ``sender``'s tokens of at least\n * `amount`.\n */\n function transferFrom(\n address sender,\n address recipient,\n uint256 amount\n ) public virtual override returns (bool) {\n _transfer(sender, recipient, amount);\n\n uint256 currentAllowance = _allowances[sender][_msgSender()];\n require(currentAllowance >= amount, \"ERC20: transfer amount exceeds allowance\");\n unchecked {\n _approve(sender, _msgSender(), currentAllowance - amount);\n }\n\n return true;\n }\n\n /**\n * @dev Atomically increases the allowance granted to `spender` by the caller.\n *\n * This is an alternative to {approve} that can be used as a mitigation for\n * problems described in {IERC20-approve}.\n *\n * Emits an {Approval} event indicating the updated allowance.\n *\n * Requirements:\n *\n * - `spender` cannot be the zero address.\n */\n function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {\n _approve(_msgSender(), spender, _allowances[_msgSender()][spender] + addedValue);\n return true;\n }\n\n /**\n * @dev Atomically decreases the allowance granted to `spender` by the caller.\n *\n * This is an alternative to {approve} that can be used as a mitigation for\n * problems described in {IERC20-approve}.\n *\n * Emits an {Approval} event indicating the updated allowance.\n *\n * Requirements:\n *\n * - `spender` cannot be the zero address.\n * - `spender` must have allowance for the caller of at least\n * `subtractedValue`.\n */\n function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {\n uint256 currentAllowance = _allowances[_msgSender()][spender];\n require(currentAllowance >= subtractedValue, \"ERC20: decreased allowance below zero\");\n unchecked {\n _approve(_msgSender(), spender, currentAllowance - subtractedValue);\n }\n\n return true;\n }\n\n /**\n * @dev Moves `amount` of tokens from `sender` to `recipient`.\n *\n * This internal function is equivalent to {transfer}, and can be used to\n * e.g. implement automatic token fees, slashing mechanisms, etc.\n *\n * Emits a {Transfer} event.\n *\n * Requirements:\n *\n * - `sender` cannot be the zero address.\n * - `recipient` cannot be the zero address.\n * - `sender` must have a balance of at least `amount`.\n */\n function _transfer(\n address sender,\n address recipient,\n uint256 amount\n ) internal virtual {\n require(sender != address(0), \"ERC20: transfer from the zero address\");\n require(recipient != address(0), \"ERC20: transfer to the zero address\");\n\n _beforeTokenTransfer(sender, recipient, amount);\n\n uint256 senderBalance = _balances[sender];\n require(senderBalance >= amount, \"ERC20: transfer amount exceeds balance\");\n unchecked {\n _balances[sender] = senderBalance - amount;\n }\n _balances[recipient] += amount;\n\n emit Transfer(sender, recipient, amount);\n\n _afterTokenTransfer(sender, recipient, amount);\n }\n\n /** @dev Creates `amount` tokens and assigns them to `account`, increasing\n * the total supply.\n *\n * Emits a {Transfer} event with `from` set to the zero address.\n *\n * Requirements:\n *\n * - `account` cannot be the zero address.\n */\n function _mint(address account, uint256 amount) internal virtual {\n require(account != address(0), \"ERC20: mint to the zero address\");\n\n _beforeTokenTransfer(address(0), account, amount);\n\n _totalSupply += amount;\n _balances[account] += amount;\n emit Transfer(address(0), account, amount);\n\n _afterTokenTransfer(address(0), account, amount);\n }\n\n /**\n * @dev Destroys `amount` tokens from `account`, reducing the\n * total supply.\n *\n * Emits a {Transfer} event with `to` set to the zero address.\n *\n * Requirements:\n *\n * - `account` cannot be the zero address.\n * - `account` must have at least `amount` tokens.\n */\n function _burn(address account, uint256 amount) internal virtual {\n require(account != address(0), \"ERC20: burn from the zero address\");\n\n _beforeTokenTransfer(account, address(0), amount);\n\n uint256 accountBalance = _balances[account];\n require(accountBalance >= amount, \"ERC20: burn amount exceeds balance\");\n unchecked {\n _balances[account] = accountBalance - amount;\n }\n _totalSupply -= amount;\n\n emit Transfer(account, address(0), amount);\n\n _afterTokenTransfer(account, address(0), amount);\n }\n\n /**\n * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens.\n *\n * This internal function is equivalent to `approve`, and can be used to\n * e.g. set automatic allowances for certain subsystems, etc.\n *\n * Emits an {Approval} event.\n *\n * Requirements:\n *\n * - `owner` cannot be the zero address.\n * - `spender` cannot be the zero address.\n */\n function _approve(\n address owner,\n address spender,\n uint256 amount\n ) internal virtual {\n require(owner != address(0), \"ERC20: approve from the zero address\");\n require(spender != address(0), \"ERC20: approve to the zero address\");\n\n _allowances[owner][spender] = amount;\n emit Approval(owner, spender, amount);\n }\n\n /**\n * @dev Hook that is called before any transfer of tokens. This includes\n * minting and burning.\n *\n * Calling conditions:\n *\n * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens\n * will be transferred to `to`.\n * - when `from` is zero, `amount` tokens will be minted for `to`.\n * - when `to` is zero, `amount` of ``from``'s tokens will be burned.\n * - `from` and `to` are never both zero.\n *\n * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].\n */\n function _beforeTokenTransfer(\n address from,\n address to,\n uint256 amount\n ) internal virtual {}\n\n /**\n * @dev Hook that is called after any transfer of tokens. This includes\n * minting and burning.\n *\n * Calling conditions:\n *\n * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens\n * has been transferred to `to`.\n * - when `from` is zero, `amount` tokens have been minted for `to`.\n * - when `to` is zero, `amount` of ``from``'s tokens have been burned.\n * - `from` and `to` are never both zero.\n *\n * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].\n */\n function _afterTokenTransfer(\n address from,\n address to,\n uint256 amount\n ) internal virtual {}\n}\n" - }, - "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol": { - "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol)\n\npragma solidity ^0.8.0;\n\nimport \"../IERC20.sol\";\n\n/**\n * @dev Interface for the optional metadata functions from the ERC20 standard.\n *\n * _Available since v4.1._\n */\ninterface IERC20Metadata is IERC20 {\n /**\n * @dev Returns the name of the token.\n */\n function name() external view returns (string memory);\n\n /**\n * @dev Returns the symbol of the token.\n */\n function symbol() external view returns (string memory);\n\n /**\n * @dev Returns the decimals places of the token.\n */\n function decimals() external view returns (uint8);\n}\n" - }, - "@openzeppelin/contracts/token/ERC20/IERC20.sol": { - "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts v4.4.1 (token/ERC20/IERC20.sol)\n\npragma solidity ^0.8.0;\n\n/**\n * @dev Interface of the ERC20 standard as defined in the EIP.\n */\ninterface IERC20 {\n /**\n * @dev Returns the amount of tokens in existence.\n */\n function totalSupply() external view returns (uint256);\n\n /**\n * @dev Returns the amount of tokens owned by `account`.\n */\n function balanceOf(address account) external view returns (uint256);\n\n /**\n * @dev Moves `amount` tokens from the caller's account to `recipient`.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * Emits a {Transfer} event.\n */\n function transfer(address recipient, uint256 amount) external returns (bool);\n\n /**\n * @dev Returns the remaining number of tokens that `spender` will be\n * allowed to spend on behalf of `owner` through {transferFrom}. This is\n * zero by default.\n *\n * This value changes when {approve} or {transferFrom} are called.\n */\n function allowance(address owner, address spender) external view returns (uint256);\n\n /**\n * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * IMPORTANT: Beware that changing an allowance with this method brings the risk\n * that someone may use both the old and the new allowance by unfortunate\n * transaction ordering. One possible solution to mitigate this race\n * condition is to first reduce the spender's allowance to 0 and set the\n * desired value afterwards:\n * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729\n *\n * Emits an {Approval} event.\n */\n function approve(address spender, uint256 amount) external returns (bool);\n\n /**\n * @dev Moves `amount` tokens from `sender` to `recipient` using the\n * allowance mechanism. `amount` is then deducted from the caller's\n * allowance.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * Emits a {Transfer} event.\n */\n function transferFrom(\n address sender,\n address recipient,\n uint256 amount\n ) external returns (bool);\n\n /**\n * @dev Emitted when `value` tokens are moved from one account (`from`) to\n * another (`to`).\n *\n * Note that `value` may be zero.\n */\n event Transfer(address indexed from, address indexed to, uint256 value);\n\n /**\n * @dev Emitted when the allowance of a `spender` for an `owner` is set by\n * a call to {approve}. `value` is the new allowance.\n */\n event Approval(address indexed owner, address indexed spender, uint256 value);\n}\n" - }, - "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol": { - "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts v4.4.1 (token/ERC20/utils/SafeERC20.sol)\n\npragma solidity ^0.8.0;\n\nimport \"../IERC20.sol\";\nimport \"../../../utils/Address.sol\";\n\n/**\n * @title SafeERC20\n * @dev Wrappers around ERC20 operations that throw on failure (when the token\n * contract returns false). Tokens that return no value (and instead revert or\n * throw on failure) are also supported, non-reverting calls are assumed to be\n * successful.\n * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,\n * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.\n */\nlibrary SafeERC20 {\n using Address for address;\n\n function safeTransfer(\n IERC20 token,\n address to,\n uint256 value\n ) internal {\n _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));\n }\n\n function safeTransferFrom(\n IERC20 token,\n address from,\n address to,\n uint256 value\n ) internal {\n _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));\n }\n\n /**\n * @dev Deprecated. This function has issues similar to the ones found in\n * {IERC20-approve}, and its usage is discouraged.\n *\n * Whenever possible, use {safeIncreaseAllowance} and\n * {safeDecreaseAllowance} instead.\n */\n function safeApprove(\n IERC20 token,\n address spender,\n uint256 value\n ) internal {\n // safeApprove should only be called when setting an initial allowance,\n // or when resetting it to zero. To increase and decrease it, use\n // 'safeIncreaseAllowance' and 'safeDecreaseAllowance'\n require(\n (value == 0) || (token.allowance(address(this), spender) == 0),\n \"SafeERC20: approve from non-zero to non-zero allowance\"\n );\n _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));\n }\n\n function safeIncreaseAllowance(\n IERC20 token,\n address spender,\n uint256 value\n ) internal {\n uint256 newAllowance = token.allowance(address(this), spender) + value;\n _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));\n }\n\n function safeDecreaseAllowance(\n IERC20 token,\n address spender,\n uint256 value\n ) internal {\n unchecked {\n uint256 oldAllowance = token.allowance(address(this), spender);\n require(oldAllowance >= value, \"SafeERC20: decreased allowance below zero\");\n uint256 newAllowance = oldAllowance - value;\n _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));\n }\n }\n\n /**\n * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement\n * on the return value: the return value is optional (but if data is returned, it must not be false).\n * @param token The token targeted by the call.\n * @param data The call data (encoded using abi.encode or one of its variants).\n */\n function _callOptionalReturn(IERC20 token, bytes memory data) private {\n // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since\n // we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that\n // the target address contains contract code and also asserts for success in the low-level call.\n\n bytes memory returndata = address(token).functionCall(data, \"SafeERC20: low-level call failed\");\n if (returndata.length > 0) {\n // Return data is optional\n require(abi.decode(returndata, (bool)), \"SafeERC20: ERC20 operation did not succeed\");\n }\n }\n}\n" - }, - "@openzeppelin/contracts/utils/Address.sol": { - "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts v4.4.1 (utils/Address.sol)\n\npragma solidity ^0.8.0;\n\n/**\n * @dev Collection of functions related to the address type\n */\nlibrary Address {\n /**\n * @dev Returns true if `account` is a contract.\n *\n * [IMPORTANT]\n * ====\n * It is unsafe to assume that an address for which this function returns\n * false is an externally-owned account (EOA) and not a contract.\n *\n * Among others, `isContract` will return false for the following\n * types of addresses:\n *\n * - an externally-owned account\n * - a contract in construction\n * - an address where a contract will be created\n * - an address where a contract lived, but was destroyed\n * ====\n */\n function isContract(address account) internal view returns (bool) {\n // This method relies on extcodesize, which returns 0 for contracts in\n // construction, since the code is only stored at the end of the\n // constructor execution.\n\n uint256 size;\n assembly {\n size := extcodesize(account)\n }\n return size > 0;\n }\n\n /**\n * @dev Replacement for Solidity's `transfer`: sends `amount` wei to\n * `recipient`, forwarding all available gas and reverting on errors.\n *\n * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost\n * of certain opcodes, possibly making contracts go over the 2300 gas limit\n * imposed by `transfer`, making them unable to receive funds via\n * `transfer`. {sendValue} removes this limitation.\n *\n * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more].\n *\n * IMPORTANT: because control is transferred to `recipient`, care must be\n * taken to not create reentrancy vulnerabilities. Consider using\n * {ReentrancyGuard} or the\n * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].\n */\n function sendValue(address payable recipient, uint256 amount) internal {\n require(address(this).balance >= amount, \"Address: insufficient balance\");\n\n (bool success, ) = recipient.call{value: amount}(\"\");\n require(success, \"Address: unable to send value, recipient may have reverted\");\n }\n\n /**\n * @dev Performs a Solidity function call using a low level `call`. A\n * plain `call` is an unsafe replacement for a function call: use this\n * function instead.\n *\n * If `target` reverts with a revert reason, it is bubbled up by this\n * function (like regular Solidity function calls).\n *\n * Returns the raw returned data. To convert to the expected return value,\n * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].\n *\n * Requirements:\n *\n * - `target` must be a contract.\n * - calling `target` with `data` must not revert.\n *\n * _Available since v3.1._\n */\n function functionCall(address target, bytes memory data) internal returns (bytes memory) {\n return functionCall(target, data, \"Address: low-level call failed\");\n }\n\n /**\n * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with\n * `errorMessage` as a fallback revert reason when `target` reverts.\n *\n * _Available since v3.1._\n */\n function functionCall(\n address target,\n bytes memory data,\n string memory errorMessage\n ) internal returns (bytes memory) {\n return functionCallWithValue(target, data, 0, errorMessage);\n }\n\n /**\n * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],\n * but also transferring `value` wei to `target`.\n *\n * Requirements:\n *\n * - the calling contract must have an ETH balance of at least `value`.\n * - the called Solidity function must be `payable`.\n *\n * _Available since v3.1._\n */\n function functionCallWithValue(\n address target,\n bytes memory data,\n uint256 value\n ) internal returns (bytes memory) {\n return functionCallWithValue(target, data, value, \"Address: low-level call with value failed\");\n }\n\n /**\n * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but\n * with `errorMessage` as a fallback revert reason when `target` reverts.\n *\n * _Available since v3.1._\n */\n function functionCallWithValue(\n address target,\n bytes memory data,\n uint256 value,\n string memory errorMessage\n ) internal returns (bytes memory) {\n require(address(this).balance >= value, \"Address: insufficient balance for call\");\n require(isContract(target), \"Address: call to non-contract\");\n\n (bool success, bytes memory returndata) = target.call{value: value}(data);\n return verifyCallResult(success, returndata, errorMessage);\n }\n\n /**\n * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],\n * but performing a static call.\n *\n * _Available since v3.3._\n */\n function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {\n return functionStaticCall(target, data, \"Address: low-level static call failed\");\n }\n\n /**\n * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],\n * but performing a static call.\n *\n * _Available since v3.3._\n */\n function functionStaticCall(\n address target,\n bytes memory data,\n string memory errorMessage\n ) internal view returns (bytes memory) {\n require(isContract(target), \"Address: static call to non-contract\");\n\n (bool success, bytes memory returndata) = target.staticcall(data);\n return verifyCallResult(success, returndata, errorMessage);\n }\n\n /**\n * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],\n * but performing a delegate call.\n *\n * _Available since v3.4._\n */\n function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {\n return functionDelegateCall(target, data, \"Address: low-level delegate call failed\");\n }\n\n /**\n * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],\n * but performing a delegate call.\n *\n * _Available since v3.4._\n */\n function functionDelegateCall(\n address target,\n bytes memory data,\n string memory errorMessage\n ) internal returns (bytes memory) {\n require(isContract(target), \"Address: delegate call to non-contract\");\n\n (bool success, bytes memory returndata) = target.delegatecall(data);\n return verifyCallResult(success, returndata, errorMessage);\n }\n\n /**\n * @dev Tool to verifies that a low level call was successful, and revert if it wasn't, either by bubbling the\n * revert reason using the provided one.\n *\n * _Available since v4.3._\n */\n function verifyCallResult(\n bool success,\n bytes memory returndata,\n string memory errorMessage\n ) internal pure returns (bytes memory) {\n if (success) {\n return returndata;\n } else {\n // Look for revert reason and bubble it up if present\n if (returndata.length > 0) {\n // The easiest way to bubble the revert reason is using memory via assembly\n\n assembly {\n let returndata_size := mload(returndata)\n revert(add(32, returndata), returndata_size)\n }\n } else {\n revert(errorMessage);\n }\n }\n }\n}\n" - }, - "@openzeppelin/contracts/utils/Context.sol": { - "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)\n\npragma solidity ^0.8.0;\n\n/**\n * @dev Provides information about the current execution context, including the\n * sender of the transaction and its data. While these are generally available\n * via msg.sender and msg.data, they should not be accessed in such a direct\n * manner, since when dealing with meta-transactions the account sending and\n * paying for execution may not be the actual sender (as far as an application\n * is concerned).\n *\n * This contract is only required for intermediate, library-like contracts.\n */\nabstract contract Context {\n function _msgSender() internal view virtual returns (address) {\n return msg.sender;\n }\n\n function _msgData() internal view virtual returns (bytes calldata) {\n return msg.data;\n }\n}\n" - }, - "@openzeppelin/contracts/utils/introspection/ERC165.sol": { - "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts v4.4.1 (utils/introspection/ERC165.sol)\n\npragma solidity ^0.8.0;\n\nimport \"./IERC165.sol\";\n\n/**\n * @dev Implementation of the {IERC165} interface.\n *\n * Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check\n * for the additional interface id that will be supported. For example:\n *\n * ```solidity\n * function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {\n * return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);\n * }\n * ```\n *\n * Alternatively, {ERC165Storage} provides an easier to use but more expensive implementation.\n */\nabstract contract ERC165 is IERC165 {\n /**\n * @dev See {IERC165-supportsInterface}.\n */\n function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {\n return interfaceId == type(IERC165).interfaceId;\n }\n}\n" - }, - "@openzeppelin/contracts/utils/introspection/IERC165.sol": { - "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol)\n\npragma solidity ^0.8.0;\n\n/**\n * @dev Interface of the ERC165 standard, as defined in the\n * https://eips.ethereum.org/EIPS/eip-165[EIP].\n *\n * Implementers can declare support of contract interfaces, which can then be\n * queried by others ({ERC165Checker}).\n *\n * For an implementation, see {ERC165}.\n */\ninterface IERC165 {\n /**\n * @dev Returns true if this contract implements the interface defined by\n * `interfaceId`. See the corresponding\n * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]\n * to learn more about how these ids are created.\n *\n * This function call must use less than 30 000 gas.\n */\n function supportsInterface(bytes4 interfaceId) external view returns (bool);\n}\n" - }, - "@openzeppelin/contracts/utils/Strings.sol": { - "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts v4.4.1 (utils/Strings.sol)\n\npragma solidity ^0.8.0;\n\n/**\n * @dev String operations.\n */\nlibrary Strings {\n bytes16 private constant _HEX_SYMBOLS = \"0123456789abcdef\";\n\n /**\n * @dev Converts a `uint256` to its ASCII `string` decimal representation.\n */\n function toString(uint256 value) internal pure returns (string memory) {\n // Inspired by OraclizeAPI's implementation - MIT licence\n // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol\n\n if (value == 0) {\n return \"0\";\n }\n uint256 temp = value;\n uint256 digits;\n while (temp != 0) {\n digits++;\n temp /= 10;\n }\n bytes memory buffer = new bytes(digits);\n while (value != 0) {\n digits -= 1;\n buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));\n value /= 10;\n }\n return string(buffer);\n }\n\n /**\n * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.\n */\n function toHexString(uint256 value) internal pure returns (string memory) {\n if (value == 0) {\n return \"0x00\";\n }\n uint256 temp = value;\n uint256 length = 0;\n while (temp != 0) {\n length++;\n temp >>= 8;\n }\n return toHexString(value, length);\n }\n\n /**\n * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.\n */\n function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {\n bytes memory buffer = new bytes(2 * length + 2);\n buffer[0] = \"0\";\n buffer[1] = \"x\";\n for (uint256 i = 2 * length + 1; i > 1; --i) {\n buffer[i] = _HEX_SYMBOLS[value & 0xf];\n value >>= 4;\n }\n require(value == 0, \"Strings: hex length insufficient\");\n return string(buffer);\n }\n}\n" - }, - "@openzeppelin/contracts/utils/structs/EnumerableSet.sol": { - "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts v4.4.1 (utils/structs/EnumerableSet.sol)\n\npragma solidity ^0.8.0;\n\n/**\n * @dev Library for managing\n * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive\n * types.\n *\n * Sets have the following properties:\n *\n * - Elements are added, removed, and checked for existence in constant time\n * (O(1)).\n * - Elements are enumerated in O(n). No guarantees are made on the ordering.\n *\n * ```\n * contract Example {\n * // Add the library methods\n * using EnumerableSet for EnumerableSet.AddressSet;\n *\n * // Declare a set state variable\n * EnumerableSet.AddressSet private mySet;\n * }\n * ```\n *\n * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`)\n * and `uint256` (`UintSet`) are supported.\n */\nlibrary EnumerableSet {\n // To implement this library for multiple types with as little code\n // repetition as possible, we write it in terms of a generic Set type with\n // bytes32 values.\n // The Set implementation uses private functions, and user-facing\n // implementations (such as AddressSet) are just wrappers around the\n // underlying Set.\n // This means that we can only create new EnumerableSets for types that fit\n // in bytes32.\n\n struct Set {\n // Storage of set values\n bytes32[] _values;\n // Position of the value in the `values` array, plus 1 because index 0\n // means a value is not in the set.\n mapping(bytes32 => uint256) _indexes;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function _add(Set storage set, bytes32 value) private returns (bool) {\n if (!_contains(set, value)) {\n set._values.push(value);\n // The value is stored at length-1, but we add 1 to all indexes\n // and use 0 as a sentinel value\n set._indexes[value] = set._values.length;\n return true;\n } else {\n return false;\n }\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function _remove(Set storage set, bytes32 value) private returns (bool) {\n // We read and store the value's index to prevent multiple reads from the same storage slot\n uint256 valueIndex = set._indexes[value];\n\n if (valueIndex != 0) {\n // Equivalent to contains(set, value)\n // To delete an element from the _values array in O(1), we swap the element to delete with the last one in\n // the array, and then remove the last element (sometimes called as 'swap and pop').\n // This modifies the order of the array, as noted in {at}.\n\n uint256 toDeleteIndex = valueIndex - 1;\n uint256 lastIndex = set._values.length - 1;\n\n if (lastIndex != toDeleteIndex) {\n bytes32 lastvalue = set._values[lastIndex];\n\n // Move the last value to the index where the value to delete is\n set._values[toDeleteIndex] = lastvalue;\n // Update the index for the moved value\n set._indexes[lastvalue] = valueIndex; // Replace lastvalue's index to valueIndex\n }\n\n // Delete the slot where the moved value was stored\n set._values.pop();\n\n // Delete the index for the deleted slot\n delete set._indexes[value];\n\n return true;\n } else {\n return false;\n }\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function _contains(Set storage set, bytes32 value) private view returns (bool) {\n return set._indexes[value] != 0;\n }\n\n /**\n * @dev Returns the number of values on the set. O(1).\n */\n function _length(Set storage set) private view returns (uint256) {\n return set._values.length;\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function _at(Set storage set, uint256 index) private view returns (bytes32) {\n return set._values[index];\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function _values(Set storage set) private view returns (bytes32[] memory) {\n return set._values;\n }\n\n // Bytes32Set\n\n struct Bytes32Set {\n Set _inner;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function add(Bytes32Set storage set, bytes32 value) internal returns (bool) {\n return _add(set._inner, value);\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) {\n return _remove(set._inner, value);\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) {\n return _contains(set._inner, value);\n }\n\n /**\n * @dev Returns the number of values in the set. O(1).\n */\n function length(Bytes32Set storage set) internal view returns (uint256) {\n return _length(set._inner);\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) {\n return _at(set._inner, index);\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function values(Bytes32Set storage set) internal view returns (bytes32[] memory) {\n return _values(set._inner);\n }\n\n // AddressSet\n\n struct AddressSet {\n Set _inner;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function add(AddressSet storage set, address value) internal returns (bool) {\n return _add(set._inner, bytes32(uint256(uint160(value))));\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function remove(AddressSet storage set, address value) internal returns (bool) {\n return _remove(set._inner, bytes32(uint256(uint160(value))));\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function contains(AddressSet storage set, address value) internal view returns (bool) {\n return _contains(set._inner, bytes32(uint256(uint160(value))));\n }\n\n /**\n * @dev Returns the number of values in the set. O(1).\n */\n function length(AddressSet storage set) internal view returns (uint256) {\n return _length(set._inner);\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function at(AddressSet storage set, uint256 index) internal view returns (address) {\n return address(uint160(uint256(_at(set._inner, index))));\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function values(AddressSet storage set) internal view returns (address[] memory) {\n bytes32[] memory store = _values(set._inner);\n address[] memory result;\n\n assembly {\n result := store\n }\n\n return result;\n }\n\n // UintSet\n\n struct UintSet {\n Set _inner;\n }\n\n /**\n * @dev Add a value to a set. O(1).\n *\n * Returns true if the value was added to the set, that is if it was not\n * already present.\n */\n function add(UintSet storage set, uint256 value) internal returns (bool) {\n return _add(set._inner, bytes32(value));\n }\n\n /**\n * @dev Removes a value from a set. O(1).\n *\n * Returns true if the value was removed from the set, that is if it was\n * present.\n */\n function remove(UintSet storage set, uint256 value) internal returns (bool) {\n return _remove(set._inner, bytes32(value));\n }\n\n /**\n * @dev Returns true if the value is in the set. O(1).\n */\n function contains(UintSet storage set, uint256 value) internal view returns (bool) {\n return _contains(set._inner, bytes32(value));\n }\n\n /**\n * @dev Returns the number of values on the set. O(1).\n */\n function length(UintSet storage set) internal view returns (uint256) {\n return _length(set._inner);\n }\n\n /**\n * @dev Returns the value stored at position `index` in the set. O(1).\n *\n * Note that there are no guarantees on the ordering of values inside the\n * array, and it may change when more values are added or removed.\n *\n * Requirements:\n *\n * - `index` must be strictly less than {length}.\n */\n function at(UintSet storage set, uint256 index) internal view returns (uint256) {\n return uint256(_at(set._inner, index));\n }\n\n /**\n * @dev Return the entire set in an array\n *\n * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed\n * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that\n * this function has an unbounded cost, and using it as part of a state-changing function may render the function\n * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.\n */\n function values(UintSet storage set) internal view returns (uint256[] memory) {\n bytes32[] memory store = _values(set._inner);\n uint256[] memory result;\n\n assembly {\n result := store\n }\n\n return result;\n }\n}\n" - }, - "contracts/buyback/BaseBuyback.sol": { - "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport { Strategizable } from \"../governance/Strategizable.sol\";\nimport \"../interfaces/chainlink/AggregatorV3Interface.sol\";\nimport { IERC20 } from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport { SafeERC20 } from \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\nimport { IUniswapUniversalRouter } from \"../interfaces/uniswap/IUniswapUniversalRouter.sol\";\nimport { ICVXLocker } from \"../interfaces/ICVXLocker.sol\";\n\nimport { Initializable } from \"../utils/Initializable.sol\";\n\nabstract contract BaseBuyback is Initializable, Strategizable {\n using SafeERC20 for IERC20;\n\n event UniswapUniversalRouterUpdated(address indexed _address);\n\n event RewardsSourceUpdated(address indexed _address);\n event TreasuryManagerUpdated(address indexed _address);\n\n // Emitted whenever OUSD/OETH is swapped for OGV/CVX or any other token\n event OTokenBuyback(\n address indexed oToken,\n address indexed swappedFor,\n uint256 swapAmountIn,\n uint256 minExpected\n );\n\n // Address of Uniswap Universal Router\n address public universalRouter;\n\n // slither-disable-next-line constable-states\n address private __deprecated_ousd;\n // slither-disable-next-line constable-states\n address private __deprecated_ogv;\n // slither-disable-next-line constable-states\n address private __deprecated_usdt;\n // slither-disable-next-line constable-states\n address private __deprecated_weth9;\n\n // Address that receives OGV after swaps\n address public rewardsSource;\n\n // Address that receives all other tokens after swaps\n address public treasuryManager;\n\n // slither-disable-next-line constable-states\n uint256 private __deprecated_treasuryBps;\n\n address public immutable oToken;\n address public immutable ogv;\n address public immutable cvx;\n address public immutable cvxLocker;\n\n // Ref: https://docs.uniswap.org/contracts/universal-router/technical-reference#command-structure\n bytes private constant swapCommand = hex\"0000\";\n\n constructor(\n address _oToken,\n address _ogv,\n address _cvx,\n address _cvxLocker\n ) {\n // Make sure nobody owns the implementation contract\n _setGovernor(address(0));\n\n oToken = _oToken;\n ogv = _ogv;\n cvx = _cvx;\n cvxLocker = _cvxLocker;\n }\n\n /**\n * @param _uniswapUniversalRouter Address of Uniswap V3 Router\n * @param _strategistAddr Address of Strategist multi-sig wallet\n * @param _treasuryManagerAddr Address that receives the treasury's share of OUSD\n * @param _rewardsSource Address of RewardsSource contract\n */\n function initialize(\n address _uniswapUniversalRouter,\n address _strategistAddr,\n address _treasuryManagerAddr,\n address _rewardsSource\n ) external onlyGovernor initializer {\n _setStrategistAddr(_strategistAddr);\n\n _setUniswapUniversalRouter(_uniswapUniversalRouter);\n _setRewardsSource(_rewardsSource);\n\n _setTreasuryManager(_treasuryManagerAddr);\n }\n\n /**\n * @dev Set address of Uniswap Universal Router for performing liquidation\n * of platform fee tokens. Setting to 0x0 will pause swaps.\n *\n * @param _router Address of the Uniswap Universal router\n */\n function setUniswapUniversalRouter(address _router) external onlyGovernor {\n _setUniswapUniversalRouter(_router);\n }\n\n function _setUniswapUniversalRouter(address _router) internal {\n universalRouter = _router;\n\n emit UniswapUniversalRouterUpdated(_router);\n }\n\n /**\n * @dev Sets the address that receives the OGV buyback rewards\n * @param _address Address\n */\n function setRewardsSource(address _address) external onlyGovernor {\n _setRewardsSource(_address);\n }\n\n function _setRewardsSource(address _address) internal {\n require(_address != address(0), \"Address not set\");\n rewardsSource = _address;\n emit RewardsSourceUpdated(_address);\n }\n\n /**\n * @dev Sets the address that can receive and manage the funds for Treasury\n * @param _address Address\n */\n function setTreasuryManager(address _address) external onlyGovernor {\n _setTreasuryManager(_address);\n }\n\n function _setTreasuryManager(address _address) internal {\n require(_address != address(0), \"Address not set\");\n treasuryManager = _address;\n emit TreasuryManagerUpdated(_address);\n }\n\n /**\n * @dev Swaps half of `oTokenAmount` to OGV\n * and the rest to CVX and finally lock up CVX\n * @param oTokenAmount Amount of OUSD/OETH to swap\n * @param minOGV Minimum OGV to receive for oTokenAmount/2\n * @param minCVX Minimum CVX to receive for oTokenAmount/2\n */\n function swap(\n uint256 oTokenAmount,\n uint256 minOGV,\n uint256 minCVX\n ) external onlyGovernorOrStrategist nonReentrant {\n require(oTokenAmount > 0, \"Invalid Swap Amount\");\n require(universalRouter != address(0), \"Uniswap Router not set\");\n require(rewardsSource != address(0), \"RewardsSource contract not set\");\n require(minOGV > 0, \"Invalid minAmount for OGV\");\n require(minCVX > 0, \"Invalid minAmount for CVX\");\n\n uint256 ogvBalanceBefore = IERC20(ogv).balanceOf(rewardsSource);\n uint256 cvxBalanceBefore = IERC20(cvx).balanceOf(address(this));\n\n uint256 amountInForOGV = oTokenAmount / 2;\n uint256 amountInForCVX = oTokenAmount - amountInForOGV;\n\n // Build swap input\n bytes[] memory inputs = new bytes[](2);\n\n inputs[0] = abi.encode(\n // Send swapped OGV directly to RewardsSource contract\n rewardsSource,\n amountInForOGV,\n minOGV,\n _getSwapPath(ogv),\n false\n );\n\n inputs[1] = abi.encode(\n // Buyback contract receives the CVX to lock it on\n // behalf of Strategist after the swap\n address(this),\n amountInForCVX,\n minCVX,\n _getSwapPath(cvx),\n false\n );\n\n // Transfer OToken to UniversalRouter for swapping\n // slither-disable-next-line unchecked-transfer unused-return\n IERC20(oToken).transfer(universalRouter, oTokenAmount);\n\n // Execute the swap\n IUniswapUniversalRouter(universalRouter).execute(\n swapCommand,\n inputs,\n block.timestamp\n );\n\n // Uniswap's Universal Router doesn't return the `amountOut` values/\n // So, the events just calculate the tokens received by doing a balance diff\n emit OTokenBuyback(\n oToken,\n ogv,\n amountInForOGV,\n IERC20(ogv).balanceOf(rewardsSource) - ogvBalanceBefore\n );\n emit OTokenBuyback(\n oToken,\n cvx,\n amountInForCVX,\n IERC20(cvx).balanceOf(address(this)) - cvxBalanceBefore\n );\n\n // Lock all CVX\n _lockAllCVX();\n }\n\n /**\n * @dev Locks all CVX held by the contract on behalf of the Treasury Manager\n */\n function lockAllCVX() external onlyGovernorOrStrategist {\n _lockAllCVX();\n }\n\n function _lockAllCVX() internal {\n require(\n treasuryManager != address(0),\n \"Treasury manager address not set\"\n );\n\n // Lock all available CVX on behalf of `treasuryManager`\n ICVXLocker(cvxLocker).lock(\n treasuryManager,\n IERC20(cvx).balanceOf(address(this)),\n 0\n );\n }\n\n /**\n * @dev Approve CVX Locker to move CVX held by this contract\n */\n function safeApproveAllTokens() external onlyGovernorOrStrategist {\n IERC20(cvx).safeApprove(cvxLocker, type(uint256).max);\n // Remove Router's allowance if any\n // slither-disable-next-line unused-return\n IERC20(oToken).approve(universalRouter, 0);\n }\n\n /**\n * @notice Owner function to withdraw a specific amount of a token\n * @param token token to be transferered\n * @param amount amount of the token to be transferred\n */\n function transferToken(address token, uint256 amount)\n external\n onlyGovernor\n nonReentrant\n {\n IERC20(token).safeTransfer(_governor(), amount);\n }\n\n /**\n * @notice Returns the Swap path to use on Uniswap from oToken to `toToken`\n * @param toToken Target token\n */\n function _getSwapPath(address toToken)\n internal\n view\n virtual\n returns (bytes memory);\n}\n" - }, - "contracts/buyback/OETHBuyback.sol": { - "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport { BaseBuyback } from \"./BaseBuyback.sol\";\n\ncontract OETHBuyback is BaseBuyback {\n // abi.encodePacked(oeth, uint24(500), weth9, uint24(3000), ogv);\n bytes public constant ogvPath =\n // solhint-disable-next-line max-line-length\n hex\"856c4efb76c1d1ae02e20ceb03a2a6a08b0b8dc30001f4c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb89c354503c38481a7a7a51629142963f98ecc12d0\";\n\n // abi.encodePacked(oeth, uint24(500), weth9, uint24(10000), cvx);\n bytes public constant cvxPath =\n // solhint-disable-next-line max-line-length\n hex\"856c4efb76c1d1ae02e20ceb03a2a6a08b0b8dc30001f4c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20027104e3fbd56cd56c3e72c1403e103b45db9da5b9d2b\";\n\n constructor(\n address _oToken,\n address _ogv,\n address _cvx,\n address _cvxLocker\n ) BaseBuyback(_oToken, _ogv, _cvx, _cvxLocker) {}\n\n function _getSwapPath(address toToken)\n internal\n view\n override\n returns (bytes memory path)\n {\n if (toToken == ogv) {\n path = ogvPath;\n } else if (toToken == cvx) {\n path = cvxPath;\n } else {\n require(false, \"Invalid toToken\");\n }\n }\n}\n" - }, - "contracts/buyback/OUSDBuyback.sol": { - "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport { BaseBuyback } from \"./BaseBuyback.sol\";\n\ncontract OUSDBuyback is BaseBuyback {\n // abi.encodePacked(ousd, uint24(500), usdt, uint24(500), weth9, uint24(3000), ogv);\n bytes public constant ogvPath =\n // solhint-disable-next-line max-line-length\n hex\"2a8e1e676ec238d8a992307b495b45b3feaa5e860001f4dac17f958d2ee523a2206206994597c13d831ec70001f4c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb89c354503c38481a7a7a51629142963f98ecc12d0\";\n\n // abi.encodePacked(ousd, uint24(500), usdt, uint24(500), weth9, uint24(10000), cvx);\n bytes public constant cvxPath =\n // solhint-disable-next-line max-line-length\n hex\"2a8e1e676ec238d8a992307b495b45b3feaa5e860001f4dac17f958d2ee523a2206206994597c13d831ec70001f4c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20027104e3fbd56cd56c3e72c1403e103b45db9da5b9d2b\";\n\n constructor(\n address _oToken,\n address _ogv,\n address _cvx,\n address _cvxLocker\n ) BaseBuyback(_oToken, _ogv, _cvx, _cvxLocker) {}\n\n function _getSwapPath(address toToken)\n internal\n view\n override\n returns (bytes memory path)\n {\n if (toToken == ogv) {\n path = ogvPath;\n } else if (toToken == cvx) {\n path = cvxPath;\n } else {\n require(false, \"Invalid toToken\");\n }\n }\n}\n" - }, - "contracts/governance/Governable.sol": { - "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\n/**\n * @title Base for contracts that are managed by the Origin Protocol's Governor.\n * @dev Copy of the openzeppelin Ownable.sol contract with nomenclature change\n * from owner to governor and renounce methods removed. Does not use\n * Context.sol like Ownable.sol does for simplification.\n * @author Origin Protocol Inc\n */\ncontract Governable {\n // Storage position of the owner and pendingOwner of the contract\n // keccak256(\"OUSD.governor\");\n bytes32 private constant governorPosition =\n 0x7bea13895fa79d2831e0a9e28edede30099005a50d652d8957cf8a607ee6ca4a;\n\n // keccak256(\"OUSD.pending.governor\");\n bytes32 private constant pendingGovernorPosition =\n 0x44c4d30b2eaad5130ad70c3ba6972730566f3e6359ab83e800d905c61b1c51db;\n\n // keccak256(\"OUSD.reentry.status\");\n bytes32 private constant reentryStatusPosition =\n 0x53bf423e48ed90e97d02ab0ebab13b2a235a6bfbe9c321847d5c175333ac4535;\n\n // See OpenZeppelin ReentrancyGuard implementation\n uint256 constant _NOT_ENTERED = 1;\n uint256 constant _ENTERED = 2;\n\n event PendingGovernorshipTransfer(\n address indexed previousGovernor,\n address indexed newGovernor\n );\n\n event GovernorshipTransferred(\n address indexed previousGovernor,\n address indexed newGovernor\n );\n\n /**\n * @dev Initializes the contract setting the deployer as the initial Governor.\n */\n constructor() {\n _setGovernor(msg.sender);\n emit GovernorshipTransferred(address(0), _governor());\n }\n\n /**\n * @notice Returns the address of the current Governor.\n */\n function governor() public view returns (address) {\n return _governor();\n }\n\n /**\n * @dev Returns the address of the current Governor.\n */\n function _governor() internal view returns (address governorOut) {\n bytes32 position = governorPosition;\n // solhint-disable-next-line no-inline-assembly\n assembly {\n governorOut := sload(position)\n }\n }\n\n /**\n * @dev Returns the address of the pending Governor.\n */\n function _pendingGovernor()\n internal\n view\n returns (address pendingGovernor)\n {\n bytes32 position = pendingGovernorPosition;\n // solhint-disable-next-line no-inline-assembly\n assembly {\n pendingGovernor := sload(position)\n }\n }\n\n /**\n * @dev Throws if called by any account other than the Governor.\n */\n modifier onlyGovernor() {\n require(isGovernor(), \"Caller is not the Governor\");\n _;\n }\n\n /**\n * @notice Returns true if the caller is the current Governor.\n */\n function isGovernor() public view returns (bool) {\n return msg.sender == _governor();\n }\n\n function _setGovernor(address newGovernor) internal {\n bytes32 position = governorPosition;\n // solhint-disable-next-line no-inline-assembly\n assembly {\n sstore(position, newGovernor)\n }\n }\n\n /**\n * @dev Prevents a contract from calling itself, directly or indirectly.\n * Calling a `nonReentrant` function from another `nonReentrant`\n * function is not supported. It is possible to prevent this from happening\n * by making the `nonReentrant` function external, and make it call a\n * `private` function that does the actual work.\n */\n modifier nonReentrant() {\n bytes32 position = reentryStatusPosition;\n uint256 _reentry_status;\n // solhint-disable-next-line no-inline-assembly\n assembly {\n _reentry_status := sload(position)\n }\n\n // On the first call to nonReentrant, _notEntered will be true\n require(_reentry_status != _ENTERED, \"Reentrant call\");\n\n // Any calls to nonReentrant after this point will fail\n // solhint-disable-next-line no-inline-assembly\n assembly {\n sstore(position, _ENTERED)\n }\n\n _;\n\n // By storing the original value once again, a refund is triggered (see\n // https://eips.ethereum.org/EIPS/eip-2200)\n // solhint-disable-next-line no-inline-assembly\n assembly {\n sstore(position, _NOT_ENTERED)\n }\n }\n\n function _setPendingGovernor(address newGovernor) internal {\n bytes32 position = pendingGovernorPosition;\n // solhint-disable-next-line no-inline-assembly\n assembly {\n sstore(position, newGovernor)\n }\n }\n\n /**\n * @notice Transfers Governance of the contract to a new account (`newGovernor`).\n * Can only be called by the current Governor. Must be claimed for this to complete\n * @param _newGovernor Address of the new Governor\n */\n function transferGovernance(address _newGovernor) external onlyGovernor {\n _setPendingGovernor(_newGovernor);\n emit PendingGovernorshipTransfer(_governor(), _newGovernor);\n }\n\n /**\n * @notice Claim Governance of the contract to a new account (`newGovernor`).\n * Can only be called by the new Governor.\n */\n function claimGovernance() external {\n require(\n msg.sender == _pendingGovernor(),\n \"Only the pending Governor can complete the claim\"\n );\n _changeGovernor(msg.sender);\n }\n\n /**\n * @dev Change Governance of the contract to a new account (`newGovernor`).\n * @param _newGovernor Address of the new Governor\n */\n function _changeGovernor(address _newGovernor) internal {\n require(_newGovernor != address(0), \"New Governor is address(0)\");\n emit GovernorshipTransferred(_governor(), _newGovernor);\n _setGovernor(_newGovernor);\n }\n}\n" - }, - "contracts/governance/Strategizable.sol": { - "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport { Governable } from \"./Governable.sol\";\n\ncontract Strategizable is Governable {\n event StrategistUpdated(address _address);\n\n // Address of strategist\n address public strategistAddr;\n\n // For future use\n uint256[50] private __gap;\n\n /**\n * @dev Verifies that the caller is either Governor or Strategist.\n */\n modifier onlyGovernorOrStrategist() {\n require(\n msg.sender == strategistAddr || isGovernor(),\n \"Caller is not the Strategist or Governor\"\n );\n _;\n }\n\n /**\n * @dev Set address of Strategist\n * @param _address Address of Strategist\n */\n function setStrategistAddr(address _address) external onlyGovernor {\n _setStrategistAddr(_address);\n }\n\n /**\n * @dev Set address of Strategist\n * @param _address Address of Strategist\n */\n function _setStrategistAddr(address _address) internal {\n strategistAddr = _address;\n emit StrategistUpdated(_address);\n }\n}\n" - }, - "contracts/interfaces/chainlink/AggregatorV3Interface.sol": { - "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ninterface AggregatorV3Interface {\n function decimals() external view returns (uint8);\n\n function description() external view returns (string memory);\n\n function version() external view returns (uint256);\n\n // getRoundData and latestRoundData should both raise \"No data present\"\n // if they do not have data to report, instead of returning unset values\n // which could be misinterpreted as actual reported values.\n function getRoundData(uint80 _roundId)\n external\n view\n returns (\n uint80 roundId,\n int256 answer,\n uint256 startedAt,\n uint256 updatedAt,\n uint80 answeredInRound\n );\n\n function latestRoundData()\n external\n view\n returns (\n uint80 roundId,\n int256 answer,\n uint256 startedAt,\n uint256 updatedAt,\n uint80 answeredInRound\n );\n}\n" - }, - "contracts/interfaces/ICVXLocker.sol": { - "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ninterface ICVXLocker {\n function lock(\n address _account,\n uint256 _amount,\n uint256 _spendRatio\n ) external;\n\n function lockedBalanceOf(address _account) external view returns (uint256);\n}\n" - }, - "contracts/interfaces/uniswap/IUniswapUniversalRouter.sol": { - "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ninterface IUniswapUniversalRouter {\n /// @notice Executes encoded commands along with provided inputs. Reverts if deadline has expired.\n /// @param commands A set of concatenated commands, each 1 byte in length\n /// @param inputs An array of byte strings containing abi encoded inputs for each command\n /// @param deadline The deadline by which the transaction must be executed\n function execute(\n bytes calldata commands,\n bytes[] calldata inputs,\n uint256 deadline\n ) external payable;\n\n function execute(bytes calldata commands, bytes[] calldata inputs)\n external\n payable;\n}\n" - }, - "contracts/mocks/MockBuyback.sol": { - "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport { BaseBuyback } from \"../buyback/BaseBuyback.sol\";\n\ncontract MockBuyback is BaseBuyback {\n constructor(\n address _oToken,\n address _ogv,\n address _cvx,\n address _cvxLocker\n ) BaseBuyback(_oToken, _ogv, _cvx, _cvxLocker) {}\n\n function _getSwapPath(address toToken)\n internal\n view\n override\n returns (bytes memory path)\n {\n return abi.encodePacked(oToken, uint24(500), toToken);\n }\n}\n" - }, - "contracts/token/BridgedWOETH.sol": { - "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport { ERC20 } from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport { AccessControlEnumerable } from \"@openzeppelin/contracts/access/AccessControlEnumerable.sol\";\nimport { Governable } from \"../governance/Governable.sol\";\nimport { Initializable } from \"../utils/Initializable.sol\";\n\ncontract BridgedWOETH is\n Governable,\n AccessControlEnumerable,\n Initializable,\n ERC20\n{\n bytes32 public constant MINTER_ROLE = keccak256(\"MINTER_ROLE\");\n bytes32 public constant BURNER_ROLE = keccak256(\"BURNER_ROLE\");\n\n constructor() ERC20(\"Wrapped OETH\", \"WOETH\") {\n // Nobody owns the implementation\n _setGovernor(address(0));\n }\n\n /**\n * @dev Initialize the proxy and set the Governor\n */\n function initialize() external initializer {\n // Governor can grant Minter/Burner roles\n _setupRole(DEFAULT_ADMIN_ROLE, _governor());\n }\n\n /**\n * @dev Mint tokens for `account`\n * @param account Address to mint tokens for\n * @param amount Amount of tokens to mint\n */\n function mint(address account, uint256 amount)\n external\n onlyRole(MINTER_ROLE)\n nonReentrant\n {\n _mint(account, amount);\n }\n\n /**\n * @dev Burns tokens from `account`\n * @param account Address to burn tokens from\n * @param amount Amount of tokens to burn\n */\n function burn(address account, uint256 amount)\n external\n onlyRole(BURNER_ROLE)\n nonReentrant\n {\n _burn(account, amount);\n }\n\n /**\n * @dev Burns tokens from `msg.sender`\n * @param amount Amount of tokens to burn\n */\n function burn(uint256 amount) external onlyRole(BURNER_ROLE) nonReentrant {\n _burn(msg.sender, amount);\n }\n\n /**\n * @dev Returns the name of the token.\n */\n function name() public view virtual override returns (string memory) {\n return \"Wrapped OETH\";\n }\n\n /**\n * @dev Returns the symbol of the token, usually a shorter version of the\n * name.\n */\n function symbol() public view virtual override returns (string memory) {\n return \"WOETH\";\n }\n\n /**\n * @dev Returns the decimals of the token\n */\n function decimals() public view virtual override returns (uint8) {\n return 18;\n }\n}\n" - }, - "contracts/utils/Initializable.sol": { - "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\n/**\n * @title Base contract any contracts that need to initialize state after deployment.\n * @author Origin Protocol Inc\n */\nabstract contract Initializable {\n /**\n * @dev Indicates that the contract has been initialized.\n */\n bool private initialized;\n\n /**\n * @dev Indicates that the contract is in the process of being initialized.\n */\n bool private initializing;\n\n /**\n * @dev Modifier to protect an initializer function from being invoked twice.\n */\n modifier initializer() {\n require(\n initializing || !initialized,\n \"Initializable: contract is already initialized\"\n );\n\n bool isTopLevelCall = !initializing;\n if (isTopLevelCall) {\n initializing = true;\n initialized = true;\n }\n\n _;\n\n if (isTopLevelCall) {\n initializing = false;\n }\n }\n\n uint256[50] private ______gap;\n}\n" - } - }, - "settings": { - "optimizer": { - "enabled": true, - "runs": 200 - }, - "outputSelection": { - "*": { - "*": [ - "abi", - "evm.bytecode", - "evm.deployedBytecode", - "evm.methodIdentifiers", - "metadata", - "devdoc", - "userdoc", - "storageLayout", - "evm.gasEstimates" - ], - "": [ - "ast" - ] - } - }, - "metadata": { - "useLiteralContent": true - } - } -} \ No newline at end of file diff --git a/contracts/node.sh b/contracts/node.sh index da3d9880c7..cd8997318f 100755 --- a/contracts/node.sh +++ b/contracts/node.sh @@ -66,6 +66,7 @@ main() fi done printf "\n" + echo "🟢 Node initialized" FORK_NETWORK_NAME=$FORK_NETWORK_NAME FORK=true npx hardhat fund --amount 100000 --network localhost --accountsfromenv true & @@ -76,7 +77,6 @@ main() wait $job || let "FAIL+=1" done - else npx --no-install hardhat node fi diff --git a/contracts/storageLayout/mainnet/MainnetGovernanceExecutorProxy.json b/contracts/storageLayout/mainnet/MainnetGovernanceExecutorProxy.json new file mode 100644 index 0000000000..244ebbe46d --- /dev/null +++ b/contracts/storageLayout/mainnet/MainnetGovernanceExecutorProxy.json @@ -0,0 +1,5 @@ +{ + "solcVersion": "0.8.7", + "storage": [], + "types": {} +} \ No newline at end of file diff --git a/contracts/tasks/account.js b/contracts/tasks/account.js index 85e91fe8ad..3ef8a0f2cb 100644 --- a/contracts/tasks/account.js +++ b/contracts/tasks/account.js @@ -23,7 +23,6 @@ async function accounts(taskArguments, hre, privateKeys) { const isMainnet = hre.network.name == "mainnet"; const isArbitrum = hre.network.name == "arbitrumOne"; - if (isMainnet || isArbitrum) { privateKeys = [process.env.DEPLOYER_PK, process.env.GOVERNOR_PK]; } diff --git a/contracts/test/_fixture-arb.js b/contracts/test/_fixture-arb.js index 12a4b16d0c..d58fe41da6 100644 --- a/contracts/test/_fixture-arb.js +++ b/contracts/test/_fixture-arb.js @@ -4,9 +4,13 @@ const mocha = require("mocha"); const { isFork, isArbFork, oethUnits } = require("./helpers"); const { impersonateAndFund } = require("../utils/signers"); const { nodeRevert, nodeSnapshot } = require("./_fixture"); +const addresses = require("../utils/addresses"); +const { setStorageAt } = require("@nomicfoundation/hardhat-network-helpers"); const log = require("../utils/logger")("test:fixtures-arb"); +const ADMIN_ROLE = + "0x0000000000000000000000000000000000000000000000000000000000000000"; const MINTER_ROLE = "0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6"; const BURNER_ROLE = @@ -40,20 +44,50 @@ const defaultArbitrumFixture = deployments.createFixture(async () => { const signers = await hre.ethers.getSigners(); + const mainnetGovernor = signers[1]; + const [minter, burner, rafael, nick] = signers.slice(4); // Skip first 4 addresses to avoid conflict - const governor = await ethers.getSigner(await woeth.governor()); - if (isArbFork) { - await impersonateAndFund(governor.address); + // L2 Governance + const l2GovernanceProxy = await ethers.getContract("L2GovernanceProxy"); + const l2Governance = await ethers.getContractAt( + "L2Governance", + l2GovernanceProxy.address + ); + + // The actual L2Governor contract + const l2Governor = await ethers.getContract("L2Governor"); - const woethImplAddr = await woethProxy.implementation(); - const latestImplAddr = (await ethers.getContract("BridgedWOETH")).address; + // Impersonated L2Governor contract's signer for tests + const governor = await impersonateAndFund(l2Governor.address); - if (woethImplAddr != latestImplAddr) { - await woethProxy.connect(governor).upgradeTo(latestImplAddr); + if (isFork) { + let woethGov = await woeth.governor(); + if (woethGov != governor.address) { + woethGov = await impersonateAndFund(woethGov); + + // Transfer WOETH governance on fork + await setStorageAt( + woethProxy.address, + "0x7bea13895fa79d2831e0a9e28edede30099005a50d652d8957cf8a607ee6ca4a", + ethers.utils.defaultAbiCoder.encode(["address"], [l2Governor.address]) + ); + + // Grant admin role + await woeth.connect(woethGov).grantRole(ADMIN_ROLE, l2Governor.address); } } + // Executor + const executor = await ethers.getContractAt( + "MainnetGovernanceExecutor", + isFork + ? addresses.mainnet.MainnetGovernanceExecutorProxy + : ( + await ethers.getContract("MainnetGovernanceExecutorProxy") + ).address + ); + await woeth.connect(governor).grantRole(MINTER_ROLE, minter.address); await woeth.connect(governor).grantRole(BURNER_ROLE, burner.address); @@ -61,16 +95,33 @@ const defaultArbitrumFixture = deployments.createFixture(async () => { await woeth.connect(minter).mint(rafael.address, oethUnits("1")); await woeth.connect(minter).mint(nick.address, oethUnits("1")); + let mockCCIPRouter; + if (!isFork) { + mockCCIPRouter = await ethers.getContract("MockCCIPRouter"); + } + + const ccipRouterSigner = !isFork + ? undefined + : await impersonateAndFund(addresses.arbitrumOne.CCIPRouter); + return { + l2GovernanceProxy, + l2Governance, + l2Governor, + executor, + woeth, woethProxy, governor, minter, burner, + mainnetGovernor, rafael, nick, + ccipRouterSigner, + mockCCIPRouter, }; }); diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index efdaac2e94..e547f3707a 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -225,7 +225,8 @@ const defaultFixture = deployments.createFixture(async () => { convexEthMetaStrategy, fluxStrategy, vaultValueChecker, - oethVaultValueChecker; + oethVaultValueChecker, + mainnetGovernanceExecutor; if (isFork) { usdt = await ethers.getContractAt(usdtAbi, addresses.mainnet.USDT); @@ -361,6 +362,14 @@ const defaultFixture = deployments.createFixture(async () => { vaultValueChecker = await ethers.getContract("VaultValueChecker"); oethVaultValueChecker = await ethers.getContract("OETHVaultValueChecker"); + + const mainnetGovernanceExecutorProxy = await ethers.getContract( + "MainnetGovernanceExecutorProxy" + ); + mainnetGovernanceExecutor = await ethers.getContractAt( + "MainnetGovernanceExecutor", + mainnetGovernanceExecutorProxy.address + ); } else { usdt = await ethers.getContract("MockUSDT"); dai = await ethers.getContract("MockDAI"); @@ -617,6 +626,9 @@ const defaultFixture = deployments.createFixture(async () => { mock1InchSwapRouter, aura, bal, + + // L2 Governance + mainnetGovernanceExecutor, }; }); diff --git a/contracts/test/l2-governance/l2-governance.arb.fork-test.js b/contracts/test/l2-governance/l2-governance.arb.fork-test.js new file mode 100644 index 0000000000..9d0514d55b --- /dev/null +++ b/contracts/test/l2-governance/l2-governance.arb.fork-test.js @@ -0,0 +1,382 @@ +const { createFixtureLoader } = require("../_fixture"); +const { defaultArbitrumFixture, MINTER_ROLE } = require("../_fixture-arb"); +const { expect } = require("chai"); +const addresses = require("../../utils/addresses"); +const { utils } = require("ethers"); +const { advanceBlocks } = require("../helpers"); +const { + CCIPChainSelectors, + L2GovernanceCommands, +} = require("../../utils/constants"); + +const arbFixture = createFixtureLoader(defaultArbitrumFixture); + +describe("L2 Governance", function () { + let fixture; + + this.timeout(0); + + beforeEach(async () => { + fixture = await arbFixture(); + }); + + async function makeProposal() { + const { l2Governance, rafael, nick, woeth } = fixture; + + // Make a proposal (simulating grantRole on wOETH) + const tx = await l2Governance + .connect(rafael) + .propose( + [woeth.address], + [0], + ["grantRole(bytes32,address)"], + [ + utils.defaultAbiCoder.encode( + ["bytes32", "address"], + [MINTER_ROLE, nick.address] + ), + ], + "" + ); + + const receipt = await tx.wait(); + const ev = receipt.events.find((e) => e.event == "ProposalCreated"); + return ev.args[0]; + } + + describe("Proposals", function () { + it("Should allow anyone to propose", async () => { + const { l2Governance, woeth } = fixture; + + const proposalId = await makeProposal(); + + // Check and verify proposal + expect(await l2Governance.state(proposalId)).to.equal(0); // Pending + + const [targets, values, signatures, calldata] = + await l2Governance.getActions(proposalId); + expect(targets.length).to.equal(1); + expect(targets[0]).to.equal(woeth.address); + expect(values.length).to.equal(1); + expect(values[0]).to.eq(0); + expect(signatures.length).to.equal(1); + expect(signatures[0]).to.equal("grantRole(bytes32,address)"); + expect(calldata.length).to.equal(1); + }); + + it("Should allow Mainnet Governance to queue through CCIP Router", async () => { + const { l2Governance, ccipRouterSigner, executor, l2Governor } = fixture; + + const proposalId = await makeProposal(); + + // Check and verify proposal state + expect(await l2Governance.state(proposalId)).to.equal(0); // Pending + + const tx = await l2Governance.connect(ccipRouterSigner).ccipReceive({ + messageId: + "0xdeadfeed00000000000000000000000000000000000000000000000000000000", + sourceChainSelector: CCIPChainSelectors.Mainnet, + sender: utils.defaultAbiCoder.encode(["address"], [executor.address]), + data: utils.defaultAbiCoder.encode( + ["bytes2", "bytes"], + [ + L2GovernanceCommands.Queue, + utils.defaultAbiCoder.encode(["uint256"], [proposalId]), + ] + ), + destTokenAmounts: [], + }); + + const [scheduledEv] = (await tx.wait()).events; + + // Check and verify proposal state + expect(await l2Governance.getTimelockHash(proposalId)).to.eq( + scheduledEv.topics[1] + ); + expect(await l2Governance.state(proposalId)).to.equal(1); // Queued + expect(await l2Governor.isOperation(scheduledEv.topics[1])).to.be.true; // Scheduled + }); + + it("Should allow anyone to executed after timelock", async () => { + const { + l2Governance, + l2Governor, + ccipRouterSigner, + executor, + nick, + woeth, + } = fixture; + + const proposalId = await makeProposal(); + + // Check and verify proposal state + expect(await l2Governance.state(proposalId)).to.equal(0); // Pending + + const tx = await l2Governance.connect(ccipRouterSigner).ccipReceive({ + messageId: + "0xdeadfeed00000000000000000000000000000000000000000000000000000000", + sourceChainSelector: CCIPChainSelectors.Mainnet, + sender: utils.defaultAbiCoder.encode(["address"], [executor.address]), + data: utils.defaultAbiCoder.encode( + ["bytes2", "bytes"], + [ + L2GovernanceCommands.Queue, + utils.defaultAbiCoder.encode(["uint256"], [proposalId]), + ] + ), + destTokenAmounts: [], + }); + + const [scheduledEv] = (await tx.wait()).events; + + // Check and verify proposal state + expect(await l2Governance.getTimelockHash(proposalId)).to.eq( + scheduledEv.topics[1] + ); + expect(await l2Governance.state(proposalId)).to.equal(1); // Queued + expect(await l2Governor.isOperation(scheduledEv.topics[1])).to.be.true; // Scheduled + + await advanceBlocks((await l2Governor.getMinDelay()).add(10)); + + // Check and verify proposal state + expect(await l2Governance.state(proposalId)).to.equal(2); // Ready + + await l2Governance.connect(nick).execute(proposalId); + + // Check and verify proposal state + expect(await l2Governance.state(proposalId)).to.equal(3); // Executed + + // Ensure that the actions have been executed + expect(await woeth.hasRole(MINTER_ROLE, nick.address)).to.be.true; + }); + + it("Should allow Mainnet Governance to cancel queued transaction", async () => { + const { l2Governance, l2Governor, ccipRouterSigner, executor } = fixture; + + const proposalId = await makeProposal(); + + // Check and verify proposal state + expect(await l2Governance.state(proposalId)).to.equal(0); // Pending + + const tx = await l2Governance.connect(ccipRouterSigner).ccipReceive({ + messageId: + "0xdeadfeed00000000000000000000000000000000000000000000000000000000", + sourceChainSelector: CCIPChainSelectors.Mainnet, + sender: utils.defaultAbiCoder.encode(["address"], [executor.address]), + data: utils.defaultAbiCoder.encode( + ["bytes2", "bytes"], + [ + L2GovernanceCommands.Queue, + utils.defaultAbiCoder.encode(["uint256"], [proposalId]), + ] + ), + destTokenAmounts: [], + }); + + const [scheduledEv] = (await tx.wait()).events; + + // Check and verify proposal state + expect(await l2Governance.getTimelockHash(proposalId)).to.eq( + scheduledEv.topics[1] + ); + expect(await l2Governance.state(proposalId)).to.equal(1); // Queued + expect(await l2Governor.isOperation(scheduledEv.topics[1])).to.be.true; // Scheduled + + await l2Governance.connect(ccipRouterSigner).ccipReceive({ + messageId: + "0xdeadfeed00000000000000000000000000000000000000000000000000000000", + sourceChainSelector: CCIPChainSelectors.Mainnet, + sender: utils.defaultAbiCoder.encode(["address"], [executor.address]), + data: utils.defaultAbiCoder.encode( + ["bytes2", "bytes"], + [ + L2GovernanceCommands.Cancel, + utils.defaultAbiCoder.encode(["uint256"], [proposalId]), + ] + ), + destTokenAmounts: [], + }); + + expect((await l2Governance.proposalDetails(proposalId)).exists).to.be + .false; + await expect(l2Governance.state(proposalId)).to.be.revertedWith( + "InvalidProposal" + ); + }); + }); + + describe("CCIP Receiver", function () { + it("Should revert when not called by CCIP Router", async () => { + const { l2Governance, executor, rafael } = fixture; + const tx = l2Governance.connect(rafael).ccipReceive({ + messageId: + "0xdeadfeed00000000000000000000000000000000000000000000000000000000", + sourceChainSelector: CCIPChainSelectors.Mainnet, + sender: utils.defaultAbiCoder.encode(["address"], [executor.address]), + data: utils.defaultAbiCoder.encode( + ["bytes2", "bytes"], + [ + L2GovernanceCommands.Queue, + utils.defaultAbiCoder.encode(["uint256"], [1]), + ] + ), + destTokenAmounts: [], + }); + + await expect(tx).to.be.revertedWith("InvalidRouter"); + }); + + it("Should revert when message is not from Mainnet", async () => { + const { l2Governance, executor, ccipRouterSigner } = fixture; + const tx = l2Governance.connect(ccipRouterSigner).ccipReceive({ + messageId: + "0xdeadfeed00000000000000000000000000000000000000000000000000000000", + sourceChainSelector: "1234", + sender: utils.defaultAbiCoder.encode(["address"], [executor.address]), + data: utils.defaultAbiCoder.encode( + ["bytes2", "bytes"], + [ + L2GovernanceCommands.Queue, + utils.defaultAbiCoder.encode(["uint256"], [1]), + ] + ), + destTokenAmounts: [], + }); + + await expect(tx).to.be.revertedWith("InvalidSourceChainSelector"); + }); + + it("Should revert when message is not from MainnetGovernanceExecutor", async () => { + const { l2Governance, nick, ccipRouterSigner } = fixture; + const tx = l2Governance.connect(ccipRouterSigner).ccipReceive({ + messageId: + "0xdeadfeed00000000000000000000000000000000000000000000000000000000", + sourceChainSelector: CCIPChainSelectors.Mainnet, + sender: utils.defaultAbiCoder.encode(["address"], [nick.address]), + data: utils.defaultAbiCoder.encode( + ["bytes2", "bytes"], + [ + L2GovernanceCommands.Queue, + utils.defaultAbiCoder.encode(["uint256"], [1]), + ] + ), + destTokenAmounts: [], + }); + + await expect(tx).to.be.revertedWith("NotMainnetExecutor"); + }); + + it("Should revert on invalid command selector", async () => { + const { l2Governance, executor, ccipRouterSigner } = fixture; + const tx = l2Governance.connect(ccipRouterSigner).ccipReceive({ + messageId: + "0xdeadfeed00000000000000000000000000000000000000000000000000000000", + sourceChainSelector: CCIPChainSelectors.Mainnet, + sender: utils.defaultAbiCoder.encode(["address"], [executor.address]), + data: utils.defaultAbiCoder.encode( + ["bytes2", "bytes"], + ["0x0011", utils.defaultAbiCoder.encode(["uint256"], [1])] + ), + destTokenAmounts: [], + }); + + await expect(tx).to.be.revertedWith("InvalidGovernanceCommand"); + }); + }); + + describe("Permissions", function () { + it("Should allow L2Governance to be upgraded by Timelock", async () => { + const { l2GovernanceProxy, governor, woeth } = fixture; + + // Pretend WOETH is the new implementation + await l2GovernanceProxy.connect(governor).upgradeTo(woeth.address); + }); + + it("Should allow Timelock to be changed by Timelock", async () => { + const { l2Governance, governor, woeth } = fixture; + + // Pretend WOETH is the new implementation + await l2Governance.connect(governor).setTimelock(woeth.address); + + expect(await l2Governance.executor()).to.equal(woeth.address); + }); + + it("Should not allow anyone else to update Timelock", async () => { + const { l2Governance, ccipRouterSigner } = fixture; + + await expect( + l2Governance.connect(ccipRouterSigner).setTimelock(addresses.zero) + ).to.be.revertedWith("NotL2Executor"); + }); + + it("Should not allow empty address for Timelock", async () => { + const { l2Governance, governor } = fixture; + + await expect( + l2Governance.connect(governor).setTimelock(addresses.zero) + ).to.be.revertedWith("EmptyAddress"); + }); + + it("Should allow Executor to be changed by Timelock", async () => { + const { l2Governance, governor, woeth } = fixture; + + // Pretend WOETH is the new implementation + await l2Governance.connect(governor).setMainnetExecutor(woeth.address); + + expect(await l2Governance.mainnetExecutor()).to.equal(woeth.address); + }); + + it("Should not allow anyone else to update Executor", async () => { + const { l2Governance, ccipRouterSigner } = fixture; + + await expect( + l2Governance + .connect(ccipRouterSigner) + .setMainnetExecutor(addresses.zero) + ).to.be.revertedWith("NotL2Executor"); + }); + + it("Should not allow empty address for Executor", async () => { + const { l2Governance, governor } = fixture; + + await expect( + l2Governance.connect(governor).setMainnetExecutor(addresses.zero) + ).to.be.revertedWith("EmptyAddress"); + }); + }); + + describe("Config", function () { + it("Should have correct Executor address", async () => { + const { l2Governance } = fixture; + expect((await l2Governance.mainnetExecutor()).toLowerCase()).to.eq( + addresses.mainnet.MainnetGovernanceExecutorProxy.toLowerCase() + ); + }); + + it("Should have correct L2 Executor address", async () => { + const { l2Governance, l2Governor } = fixture; + expect((await l2Governance.executor()).toLowerCase()).to.eq( + l2Governor.address.toLowerCase() + ); + }); + + it("Should have correct CCIP Router address", async () => { + const { l2Governance } = fixture; + expect((await l2Governance.getRouter()).toLowerCase()).to.eq( + addresses.arbitrumOne.CCIPRouter.toLowerCase() + ); + }); + + it("Should be owned by timelock", async () => { + const { l2GovernanceProxy, l2Governor } = fixture; + const proxyOwner = await l2GovernanceProxy.governor(); + + expect(proxyOwner.toLowerCase()).to.eq(l2Governor.address.toLowerCase()); + }); + + it("Should have a 1 day timelock", async () => { + const { l2Governor } = fixture; + expect(await l2Governor.getMinDelay()).to.eq(86400); + }); + }); +}); diff --git a/contracts/test/l2-governance/l2-governance.js b/contracts/test/l2-governance/l2-governance.js new file mode 100644 index 0000000000..f3d771bc7e --- /dev/null +++ b/contracts/test/l2-governance/l2-governance.js @@ -0,0 +1,712 @@ +const { createFixtureLoader } = require("../_fixture"); +const { defaultArbitrumFixture, MINTER_ROLE } = require("../_fixture-arb"); +const { expect } = require("chai"); +const addresses = require("../../utils/addresses"); +const { utils } = require("ethers"); +const { advanceBlocks } = require("../helpers"); +const { + CCIPChainSelectors, + L2GovernanceCommands, +} = require("../../utils/constants"); + +const arbFixture = createFixtureLoader(defaultArbitrumFixture); + +describe("L2 Governance", function () { + let fixture; + + beforeEach(async () => { + fixture = await arbFixture(); + }); + + async function makeProposal() { + const { l2Governance, rafael, nick, woeth } = fixture; + + // Make a proposal (simulating grantRole on wOETH) + const tx = await l2Governance + .connect(rafael) + .propose( + [woeth.address], + [0], + ["grantRole(bytes32,address)"], + [ + utils.defaultAbiCoder.encode( + ["bytes32", "address"], + [MINTER_ROLE, nick.address] + ), + ], + "" + ); + + const receipt = await tx.wait(); + const ev = receipt.events.find((e) => e.event == "ProposalCreated"); + return ev.args[0]; + } + + const getProposalData = (proposalId, commandId) => { + return utils.defaultAbiCoder.encode( + ["bytes2", "bytes"], + [commandId, utils.defaultAbiCoder.encode(["uint256"], [proposalId])] + ); + }; + + describe("Create Proposal", () => { + it("should allow anyone to propose", async () => { + const { l2Governance, woeth } = fixture; + + const proposalId = await makeProposal(); + + // Check and verify proposal + expect(await l2Governance.state(proposalId)).to.equal(0); // Pending + + const [targets, values, signatures, calldata] = + await l2Governance.getActions(proposalId); + expect(targets.length).to.equal(1); + expect(targets[0]).to.equal(woeth.address); + expect(values.length).to.equal(1); + expect(values[0]).to.eq(0); + expect(signatures.length).to.equal(1); + expect(signatures[0]).to.equal("grantRole(bytes32,address)"); + expect(calldata.length).to.equal(1); + }); + + it("should revert if no actions", async () => { + const { l2Governance, nick } = fixture; + + const tx = l2Governance.connect(nick).propose([], [], [], [], ""); + + await expect(tx).to.be.revertedWith("EmptyProposal"); + }); + + it("should revert on duplicate proposal", async () => { + const { l2Governance, nick } = fixture; + + const proposalId = await makeProposal(); + const [targets, values, signatures, calldatas] = + await l2Governance.getActions(proposalId); + + const tx = l2Governance + .connect(nick) + .propose(targets, values, signatures, calldatas, ""); + + await expect(tx).to.be.revertedWith("DuplicateProposal"); + }); + + it("should revert if args are invalid", async () => { + const { l2Governance, rafael, nick, woeth } = fixture; + + let tx = l2Governance + .connect(rafael) + .propose( + [woeth.address, woeth.address], + [0], + ["grantRole(bytes32,address)"], + [ + utils.defaultAbiCoder.encode( + ["bytes32", "address"], + [MINTER_ROLE, nick.address] + ), + ], + "" + ); + + await expect(tx).to.be.revertedWith("InvalidProposalLength"); + + tx = l2Governance + .connect(rafael) + .propose( + [woeth.address], + [0, 0], + ["grantRole(bytes32,address)"], + [ + utils.defaultAbiCoder.encode( + ["bytes32", "address"], + [MINTER_ROLE, nick.address] + ), + ], + "" + ); + + await expect(tx).to.be.revertedWith("InvalidProposalLength"); + + tx = l2Governance + .connect(rafael) + .propose( + [woeth.address], + [0], + [], + [ + utils.defaultAbiCoder.encode( + ["bytes32", "address"], + [MINTER_ROLE, nick.address] + ), + ], + "" + ); + + await expect(tx).to.be.revertedWith("InvalidProposalLength"); + + tx = l2Governance + .connect(rafael) + .propose( + [woeth.address, woeth.address], + [0], + ["grantRole(bytes32,address)"], + [], + "" + ); + await expect(tx).to.be.revertedWith("InvalidProposalLength"); + }); + }); + + describe("Queue Proposal", () => { + it("should allow Mainnet Governance Executor to queue through CCIP", async () => { + const { mockCCIPRouter, l2Governance, l2Governor, executor, rafael } = + fixture; + + const proposalId = await makeProposal(); + + // Check and verify proposal + expect(await l2Governance.state(proposalId)).to.equal(0); // Pending + + // Queue + const tx = await mockCCIPRouter + .connect(rafael) + .mockSend( + l2Governance.address, + CCIPChainSelectors.Mainnet, + executor.address, + getProposalData(proposalId, L2GovernanceCommands.Queue), + [] + ); + + // Should've queued it on Timelock/L2Governor + const [timelockEvents] = (await tx.wait()).events; + expect(await l2Governance.getTimelockHash(proposalId)).to.eq( + timelockEvents.topics[1] + ); + expect(await l2Governance.state(proposalId)).to.equal(1); // Queued + expect(await l2Governor.isOperation(timelockEvents.topics[1])).to.be.true; // Scheduled + }); + + it("should not queue proposal if it does not exists", async () => { + const { mockCCIPRouter, l2Governance, executor, rafael } = fixture; + + const tx = mockCCIPRouter + .connect(rafael) + .mockSend( + l2Governance.address, + CCIPChainSelectors.Mainnet, + executor.address, + getProposalData(1010, L2GovernanceCommands.Queue), + [] + ); + + await expect(tx).to.be.revertedWith("InvalidProposal"); + }); + + it("should not queue proposal if it's already queued", async () => { + const { mockCCIPRouter, l2Governance, executor, rafael } = fixture; + + const proposalId = await makeProposal(); + expect(await l2Governance.state(proposalId)).to.equal(0); // Pending + + // Queue + await mockCCIPRouter + .connect(rafael) + .mockSend( + l2Governance.address, + CCIPChainSelectors.Mainnet, + executor.address, + getProposalData(proposalId, L2GovernanceCommands.Queue), + [] + ); + expect(await l2Governance.state(proposalId)).to.equal(1); // Queued + + // Try Queueing again + const tx = mockCCIPRouter + .connect(rafael) + .mockSend( + l2Governance.address, + CCIPChainSelectors.Mainnet, + executor.address, + getProposalData(proposalId, L2GovernanceCommands.Queue), + [] + ); + await expect(tx).to.be.revertedWith("InvalidProposalState"); + }); + + it("should not queue proposal if it's already ready", async () => { + const { mockCCIPRouter, l2Governance, l2Governor, executor, rafael } = + fixture; + + const proposalId = await makeProposal(); + expect(await l2Governance.state(proposalId)).to.equal(0); // Pending + + // Queue + await mockCCIPRouter + .connect(rafael) + .mockSend( + l2Governance.address, + CCIPChainSelectors.Mainnet, + executor.address, + getProposalData(proposalId, L2GovernanceCommands.Queue), + [] + ); + expect(await l2Governance.state(proposalId)).to.equal(1); // Queued + + // Wait for timelock + await advanceBlocks((await l2Governor.getMinDelay()).add(10)); + + // Check and verify proposal state + expect(await l2Governance.state(proposalId)).to.equal(2); // Ready + + // Try Queueing again + const tx = mockCCIPRouter + .connect(rafael) + .mockSend( + l2Governance.address, + CCIPChainSelectors.Mainnet, + executor.address, + getProposalData(proposalId, L2GovernanceCommands.Queue), + [] + ); + await expect(tx).to.be.revertedWith("InvalidProposalState"); + }); + + it("should not queue proposal if it's already executed", async () => { + const { mockCCIPRouter, l2Governance, l2Governor, executor, nick } = + fixture; + + const proposalId = await makeProposal(); + expect(await l2Governance.state(proposalId)).to.equal(0); // Pending + + // Queue + await mockCCIPRouter + .connect(nick) + .mockSend( + l2Governance.address, + CCIPChainSelectors.Mainnet, + executor.address, + getProposalData(proposalId, L2GovernanceCommands.Queue), + [] + ); + expect(await l2Governance.state(proposalId)).to.equal(1); // Queued + + // Wait for timelock + await advanceBlocks((await l2Governor.getMinDelay()).add(10)); + + // Check and verify proposal state + expect(await l2Governance.state(proposalId)).to.equal(2); // Ready + + // Execute + await l2Governance.connect(nick).execute(proposalId); + + // Check and verify proposal state + expect(await l2Governance.state(proposalId)).to.equal(3); // Executed + + // Try Queueing again + const tx = mockCCIPRouter + .connect(nick) + .mockSend( + l2Governance.address, + CCIPChainSelectors.Mainnet, + executor.address, + getProposalData(proposalId, L2GovernanceCommands.Queue), + [] + ); + await expect(tx).to.be.revertedWith("InvalidProposalState"); + }); + }); + + describe("Proposal Execution", () => { + it("should show correct state after timelock has lapsed", async () => { + const { mockCCIPRouter, l2Governance, l2Governor, executor, rafael } = + fixture; + + const proposalId = await makeProposal(); + expect(await l2Governance.state(proposalId)).to.equal(0); // Pending + + // Queue + await mockCCIPRouter + .connect(rafael) + .mockSend( + l2Governance.address, + CCIPChainSelectors.Mainnet, + executor.address, + getProposalData(proposalId, L2GovernanceCommands.Queue), + [] + ); + expect(await l2Governance.state(proposalId)).to.equal(1); // Queued + + // Wait for timelock + await advanceBlocks((await l2Governor.getMinDelay()).add(10)); + + // Check and verify proposal state + expect(await l2Governance.state(proposalId)).to.equal(2); // Ready + }); + + it("should allow execution after timelock", async () => { + const { + mockCCIPRouter, + l2Governance, + l2Governor, + executor, + woeth, + nick, + rafael, + } = fixture; + + const proposalId = await makeProposal(); + expect(await l2Governance.state(proposalId)).to.equal(0); // Pending + + // Queue + await mockCCIPRouter + .connect(rafael) + .mockSend( + l2Governance.address, + CCIPChainSelectors.Mainnet, + executor.address, + getProposalData(proposalId, L2GovernanceCommands.Queue), + [] + ); + expect(await l2Governance.state(proposalId)).to.equal(1); // Queued + + // Wait for timelock + await advanceBlocks((await l2Governor.getMinDelay()).add(10)); + + // Check and verify proposal state + expect(await l2Governance.state(proposalId)).to.equal(2); // Ready + + // Execute + await l2Governance.connect(nick).execute(proposalId); + + // Check and verify proposal state + expect(await l2Governance.state(proposalId)).to.equal(3); // Executed + + // Ensure that the actions have been executed + expect(await woeth.hasRole(MINTER_ROLE, nick.address)).to.be.true; + }); + + it("should not allow execution if proposal isn't ready", async () => { + const { mockCCIPRouter, l2Governance, executor, nick, rafael } = fixture; + + const proposalId = await makeProposal(); + expect(await l2Governance.state(proposalId)).to.equal(0); // Pending + + // Try Execute + await expect( + l2Governance.connect(nick).execute(proposalId) + ).to.be.revertedWith("InvalidProposalState"); + + // Queue + await mockCCIPRouter + .connect(rafael) + .mockSend( + l2Governance.address, + CCIPChainSelectors.Mainnet, + executor.address, + getProposalData(proposalId, L2GovernanceCommands.Queue), + [] + ); + expect(await l2Governance.state(proposalId)).to.equal(1); // Queued + + // Try Execute + await expect( + l2Governance.connect(nick).execute(proposalId) + ).to.be.revertedWith("InvalidProposalState"); + }); + + it("should not allow execution of unknown proposal", async () => { + const { l2Governance, nick } = fixture; + + // Try Execute + await expect(l2Governance.connect(nick).execute(1000)).to.be.revertedWith( + "InvalidProposal" + ); + }); + }); + + describe("Proposal Cancelation", () => { + it("should allow cancelling of queued proposals", async () => { + const { mockCCIPRouter, l2Governance, executor, rafael } = fixture; + + const proposalId = await makeProposal(); + expect(await l2Governance.state(proposalId)).to.equal(0); // Pending + + // Queue + await mockCCIPRouter + .connect(rafael) + .mockSend( + l2Governance.address, + CCIPChainSelectors.Mainnet, + executor.address, + getProposalData(proposalId, L2GovernanceCommands.Queue), + [] + ); + expect(await l2Governance.state(proposalId)).to.equal(1); // Queued + + // Cancel + await mockCCIPRouter + .connect(rafael) + .mockSend( + l2Governance.address, + CCIPChainSelectors.Mainnet, + executor.address, + getProposalData(proposalId, L2GovernanceCommands.Cancel), + [] + ); + await expect(l2Governance.state(proposalId)).to.be.revertedWith( + "InvalidProposal" + ); + }); + + it("should not cancel proposal if it's already executed", async () => { + const { mockCCIPRouter, l2Governance, l2Governor, executor, nick } = + fixture; + + const proposalId = await makeProposal(); + expect(await l2Governance.state(proposalId)).to.equal(0); // Pending + + // Queue + await mockCCIPRouter + .connect(nick) + .mockSend( + l2Governance.address, + CCIPChainSelectors.Mainnet, + executor.address, + getProposalData(proposalId, L2GovernanceCommands.Queue), + [] + ); + expect(await l2Governance.state(proposalId)).to.equal(1); // Queued + + // Wait for timelock + await advanceBlocks((await l2Governor.getMinDelay()).add(10)); + + // Check and verify proposal state + expect(await l2Governance.state(proposalId)).to.equal(2); // Ready + + // Execute + await l2Governance.connect(nick).execute(proposalId); + + // Check and verify proposal state + expect(await l2Governance.state(proposalId)).to.equal(3); // Executed + + // Try Queueing again + const tx = mockCCIPRouter + .connect(nick) + .mockSend( + l2Governance.address, + CCIPChainSelectors.Mainnet, + executor.address, + getProposalData(proposalId, L2GovernanceCommands.Cancel), + [] + ); + await expect(tx).to.be.revertedWith("InvalidProposalState"); + }); + + it("should not cancel proposal if it doesn't exist", async () => { + const { mockCCIPRouter, l2Governance, executor, rafael } = fixture; + + // Try Queueing again + const tx = mockCCIPRouter + .connect(rafael) + .mockSend( + l2Governance.address, + CCIPChainSelectors.Mainnet, + executor.address, + getProposalData(10023, L2GovernanceCommands.Cancel), + [] + ); + await expect(tx).to.be.revertedWith("InvalidProposal"); + }); + }); + + describe("CCIP Message Handling", () => { + it("should not accept message if not from mainnet", async () => { + const { mockCCIPRouter, l2Governance, executor, rafael } = fixture; + + const tx = mockCCIPRouter + .connect(rafael) + .mockSend( + l2Governance.address, + CCIPChainSelectors.ArbitrumOne, + executor.address, + getProposalData(1010, L2GovernanceCommands.Queue), + [] + ); + + await expect(tx).to.be.revertedWith("InvalidSourceChainSelector"); + }); + + it("should not accept messages if not from mainnet executor", async () => { + const { mockCCIPRouter, l2Governance, l2Governor, rafael } = fixture; + + for (const signer of [l2Governor, rafael, mockCCIPRouter]) { + const tx = mockCCIPRouter + .connect(rafael) + .mockSend( + l2Governance.address, + CCIPChainSelectors.Mainnet, + signer.address, + getProposalData(1010, L2GovernanceCommands.Queue), + [] + ); + + await expect(tx).to.be.revertedWith("NotMainnetExecutor"); + } + }); + + it("should not accept messages if bridge is cursed", async () => { + const { mockCCIPRouter, executor, l2Governance, rafael } = fixture; + + // Curse the Router + await mockCCIPRouter.connect(rafael).setIsCursed(true); + + const tx = mockCCIPRouter + .connect(rafael) + .mockSend( + l2Governance.address, + CCIPChainSelectors.Mainnet, + executor.address, + getProposalData(1010, L2GovernanceCommands.Queue), + [] + ); + + await expect(tx).to.be.revertedWith("CCIPRouterIsCursed"); + }); + + it("should not accept messages if there are token transfers", async () => { + const { mockCCIPRouter, executor, l2Governance, rafael, woeth } = fixture; + + const tx = mockCCIPRouter + .connect(rafael) + .mockSend( + l2Governance.address, + CCIPChainSelectors.Mainnet, + executor.address, + getProposalData(1010, L2GovernanceCommands.Queue), + [[woeth.address, "100"]] + ); + + await expect(tx).to.be.revertedWith("TokenTransfersNotAccepted"); + }); + + it("should not accept messages with invalid governance commands", async () => { + const { mockCCIPRouter, executor, l2Governance, rafael } = fixture; + + const tx = mockCCIPRouter + .connect(rafael) + .mockSend( + l2Governance.address, + CCIPChainSelectors.Mainnet, + executor.address, + getProposalData(1010, "0x0003"), + [] + ); + + await expect(tx).to.be.revertedWith("InvalidGovernanceCommand"); + }); + }); + + describe("Proposal State", function () { + it("should revert for non-existent proposals", async () => { + const { l2Governance } = fixture; + + await expect(l2Governance.state(1000)).to.be.revertedWith( + "InvalidProposal" + ); + }); + + it("should not return actions for non-existent proposals", async () => { + const { l2Governance } = fixture; + + await expect(l2Governance.getActions(1000)).to.be.revertedWith( + "InvalidProposal" + ); + }); + + it("should not return hash for non-existent proposals", async () => { + const { l2Governance } = fixture; + + await expect(l2Governance.getTimelockHash(1000)).to.be.revertedWith( + "InvalidProposal" + ); + }); + }); + + describe("Config & Permissions", function () { + it("Should allow L2Governance to be upgraded by Timelock", async () => { + const { l2GovernanceProxy, governor, woeth } = fixture; + + // Pretend WOETH is the new implementation + await l2GovernanceProxy.connect(governor).upgradeTo(woeth.address); + }); + + it("Should allow Timelock to be changed by Timelock", async () => { + const { l2Governance, governor, woeth } = fixture; + + // Pretend WOETH is the new implementation + await l2Governance.connect(governor).setTimelock(woeth.address); + + expect(await l2Governance.executor()).to.equal(woeth.address); + }); + + it("Should not allow anyone else to update Timelock", async () => { + const { l2Governance, rafael } = fixture; + + await expect( + l2Governance.connect(rafael).setTimelock(addresses.dead) + ).to.be.revertedWith("NotL2Executor"); + }); + + it("Should not allow empty address for Timelock", async () => { + const { l2Governance, governor } = fixture; + + await expect( + l2Governance.connect(governor).setTimelock(addresses.zero) + ).to.be.revertedWith("EmptyAddress"); + }); + + it("Should allow Executor to be changed by Timelock", async () => { + const { l2Governance, governor, woeth } = fixture; + + // Pretend WOETH is the new implementation + await l2Governance.connect(governor).setMainnetExecutor(woeth.address); + + expect(await l2Governance.mainnetExecutor()).to.equal(woeth.address); + }); + + it("Should not allow anyone else to update Executor", async () => { + const { l2Governance, rafael } = fixture; + + await expect( + l2Governance.connect(rafael).setMainnetExecutor(addresses.dead) + ).to.be.revertedWith("NotL2Executor"); + }); + + it("Should not allow empty address for Executor", async () => { + const { l2Governance, governor } = fixture; + + await expect( + l2Governance.connect(governor).setMainnetExecutor(addresses.zero) + ).to.be.revertedWith("EmptyAddress"); + }); + + it("initialization should revert if empty address is passed", async () => { + const { nick } = fixture; + const l2GovernanceImpl = await hre.ethers.getContract("L2Governance"); + + await expect( + l2GovernanceImpl + .connect(nick) + .initialize(addresses.zero, addresses.dead) + ).to.be.revertedWith("EmptyAddress"); + + await expect( + l2GovernanceImpl + .connect(nick) + .initialize(addresses.dead, addresses.zero) + ).to.be.revertedWith("EmptyAddress"); + }); + }); +}); diff --git a/contracts/test/l2-governance/mainnet-executor.fork-test.js b/contracts/test/l2-governance/mainnet-executor.fork-test.js new file mode 100644 index 0000000000..c3e4e8c536 --- /dev/null +++ b/contracts/test/l2-governance/mainnet-executor.fork-test.js @@ -0,0 +1,141 @@ +const addresses = require("../../utils/addresses"); +const { CCIPChainSelectors } = require("../../utils/constants"); +const { createFixtureLoader, defaultFixture } = require("../_fixture"); +const { fundAccount } = require("../helpers"); +const { expect } = require("chai"); + +const loadFixture = createFixtureLoader(defaultFixture); + +describe("Mainnet Governance Executor", function () { + let fixture; + beforeEach(async () => { + fixture = await loadFixture(); + + // Send some ETH to the executor + await fundAccount(fixture.mainnetGovernanceExecutor.address); + }); + + it("Should forward commands to CCIPRouter from Governance", async () => { + const { mainnetGovernanceExecutor, timelock } = fixture; + + await mainnetGovernanceExecutor + .connect(timelock) + .queueL2Proposal(CCIPChainSelectors.ArbitrumOne, 1, 0); + }); + + it("Should revert if sent to unconfigured chain", async () => { + const { mainnetGovernanceExecutor, timelock } = fixture; + + const tx = mainnetGovernanceExecutor + .connect(timelock) + .cancelL2Proposal(123, 1, 0); + + await expect(tx).to.be.revertedWith("UnsupportedChain"); + }); + + it("Should revert if there isn't enough balance to cover for fees", async () => { + const { mainnetGovernanceExecutor, timelock } = fixture; + + await fundAccount(mainnetGovernanceExecutor.address, "0.0000001"); + + const tx = mainnetGovernanceExecutor + .connect(timelock) + .queueL2Proposal(CCIPChainSelectors.ArbitrumOne, 1, 0); + + await expect(tx).to.be.revertedWith("InsufficientBalanceForFees"); + }); + + it("Should not allow anyone (other than Governance) to send commands", async () => { + const { mainnetGovernanceExecutor, governor, strategist, domen, daniel } = + fixture; + + for (const signer of [governor, strategist, domen, daniel]) { + await expect( + mainnetGovernanceExecutor + .connect(signer) + .queueL2Proposal(CCIPChainSelectors.ArbitrumOne, 1, 0) + ).to.be.revertedWith("Caller is not the Governor"); + } + }); + + it("Should allow governance to add chain config", async () => { + const { mainnetGovernanceExecutor, timelock } = fixture; + + // Pretend timelock is L2 Governance + await mainnetGovernanceExecutor + .connect(timelock) + .addChainConfig(1010, addresses.dead); + + const config = await mainnetGovernanceExecutor.chainConfig(1010); + + expect(config.isSupported).to.be.true; + expect(config.l2Governance).to.eq(addresses.dead); + }); + + it("Should not allow anyone else to add chain config", async () => { + const { mainnetGovernanceExecutor, governor, strategist, domen, daniel } = + fixture; + + for (const signer of [governor, strategist, domen, daniel]) { + await expect( + mainnetGovernanceExecutor + .connect(signer) + .addChainConfig(1012, addresses.dead) + ).to.be.revertedWith("Caller is not the Governor"); + } + }); + + it("Should not allow misconfiguration", async () => { + const { mainnetGovernanceExecutor, timelock } = fixture; + + const tx = mainnetGovernanceExecutor + .connect(timelock) + .addChainConfig(1011, addresses.zero); + + await expect(tx).to.be.revertedWith("InvalidGovernanceAddress"); + }); + + it("Should fail if already supported", async () => { + const { mainnetGovernanceExecutor, timelock } = fixture; + + const tx = mainnetGovernanceExecutor + .connect(timelock) + .addChainConfig(CCIPChainSelectors.ArbitrumOne, addresses.dead); + + await expect(tx).to.be.revertedWith("DuplicateChainConfig"); + }); + + it("Should allow governance to remove chain config", async () => { + const { mainnetGovernanceExecutor, timelock } = fixture; + + // Pretend timelock is L2 Governance + await mainnetGovernanceExecutor + .connect(timelock) + .removeChainConfig(CCIPChainSelectors.ArbitrumOne); + + const config = await mainnetGovernanceExecutor.chainConfig(1010); + + expect(config.isSupported).to.be.false; + expect(config.l2Governance).to.eq(addresses.zero); + }); + + it("Should not allow anyone else to remove chain config", async () => { + const { mainnetGovernanceExecutor, governor, strategist, domen, daniel } = + fixture; + + for (const signer of [governor, strategist, domen, daniel]) { + await expect( + mainnetGovernanceExecutor + .connect(signer) + .removeChainConfig(CCIPChainSelectors.ArbitrumOne) + ).to.be.revertedWith("Caller is not the Governor"); + } + }); + it("Should fail for unsupported chain config", async () => { + const { mainnetGovernanceExecutor, timelock } = fixture; + + const tx = mainnetGovernanceExecutor.connect(timelock).removeChainConfig(1); + + await expect(tx).to.be.revertedWith("UnsupportedChain"); + }); +}); diff --git a/contracts/test/l2-governance/mainnet-executor.js b/contracts/test/l2-governance/mainnet-executor.js new file mode 100644 index 0000000000..23b468ed81 --- /dev/null +++ b/contracts/test/l2-governance/mainnet-executor.js @@ -0,0 +1,238 @@ +const { createFixtureLoader } = require("../_fixture"); +const { defaultArbitrumFixture } = require("../_fixture-arb"); +const { expect } = require("chai"); +const addresses = require("../../utils/addresses"); +const { utils } = require("ethers"); +const { + CCIPChainSelectors, + L2GovernanceCommands, +} = require("../../utils/constants"); +const { setBalance } = require("@nomicfoundation/hardhat-network-helpers"); + +const arbFixture = createFixtureLoader(defaultArbitrumFixture); + +describe("Mainnet Governance Executor", function () { + let fixture; + + beforeEach(async () => { + fixture = await arbFixture(); + }); + + const getProposalData = (proposalId, commandId) => { + return utils.defaultAbiCoder.encode( + ["bytes2", "bytes"], + [commandId, utils.defaultAbiCoder.encode(["uint256"], [proposalId])] + ); + }; + + describe("CCIP Message Forwarding", () => { + it("Should allow governance to send queue proposal command", async () => { + const { mainnetGovernor, l2Governance, executor, mockCCIPRouter } = + fixture; + + await executor + .connect(mainnetGovernor) + .queueL2Proposal(CCIPChainSelectors.ArbitrumOne, 1001, 0); + + expect(await mockCCIPRouter.lastChainSelector()).to.eq( + CCIPChainSelectors.ArbitrumOne + ); + + const msg = await mockCCIPRouter.lastMessage(); + expect(msg.receiver).to.eq( + utils.defaultAbiCoder.encode(["address"], [l2Governance.address]) + ); + expect(msg.feeToken).to.eq(addresses.zero); + expect(msg.extraArgs).to.eq("0x"); + expect(msg.data).to.eq(getProposalData(1001, L2GovernanceCommands.Queue)); + }); + + it("Should allow governance to send cancel proposal command", async () => { + const { mainnetGovernor, l2Governance, executor, mockCCIPRouter } = + fixture; + + await executor + .connect(mainnetGovernor) + .cancelL2Proposal(CCIPChainSelectors.ArbitrumOne, 1001, 0); + + expect(await mockCCIPRouter.lastChainSelector()).to.eq( + CCIPChainSelectors.ArbitrumOne + ); + + const msg = await mockCCIPRouter.lastMessage(); + expect(msg.receiver).to.eq( + utils.defaultAbiCoder.encode(["address"], [l2Governance.address]) + ); + expect(msg.feeToken).to.eq(addresses.zero); + expect(msg.extraArgs).to.eq("0x"); + expect(msg.data).to.eq( + getProposalData(1001, L2GovernanceCommands.Cancel) + ); + }); + + it("Should allow governance to set gas limits", async () => { + const { mainnetGovernor, l2Governance, executor, mockCCIPRouter } = + fixture; + + await executor + .connect(mainnetGovernor) + .cancelL2Proposal(CCIPChainSelectors.ArbitrumOne, 1001, 400000); + + expect(await mockCCIPRouter.lastChainSelector()).to.eq( + CCIPChainSelectors.ArbitrumOne + ); + + const msg = await mockCCIPRouter.lastMessage(); + expect(msg.receiver).to.eq( + utils.defaultAbiCoder.encode(["address"], [l2Governance.address]) + ); + expect(msg.feeToken).to.eq(addresses.zero); + expect(msg.data).to.eq( + getProposalData(1001, L2GovernanceCommands.Cancel) + ); + expect(msg.extraArgs).to.eq( + "0x97a657c90000000000000000000000000000000000000000000000000000000000061a80" + ); + }); + + it("Should not allow anyone else to send commands", async () => { + const { rafael, nick, governor, executor } = fixture; + + for (const signer of [rafael, nick, governor]) { + const tx = executor + .connect(signer) + .cancelL2Proposal(CCIPChainSelectors.ArbitrumOne, 1001, 400000); + + await expect(tx).to.be.revertedWith("Caller is not the Governor"); + } + }); + + it("Should fetch fees from CCIPRouter", async () => { + const { mockCCIPRouter, executor, nick } = fixture; + + await mockCCIPRouter.connect(nick).setFee("10000"); + + const fee = await executor.getCCIPFees( + L2GovernanceCommands.Queue, + CCIPChainSelectors.ArbitrumOne, + 1001, + 400000 + ); + + expect(fee).to.eq("10000"); + }); + + it("Should revert if destination chain is unsupported", async () => { + const { mainnetGovernor, executor } = fixture; + + const tx = executor + .connect(mainnetGovernor) + .cancelL2Proposal(CCIPChainSelectors.Mainnet, 1001, 0); + + await expect(tx).to.be.revertedWith("UnsupportedChain"); + }); + + it("Should revert if executor doesn't have enough balance", async () => { + const { mainnetGovernor, executor, nick, mockCCIPRouter } = fixture; + + await mockCCIPRouter.connect(nick).setFee("10000"); + + await setBalance(executor.address, "0x1"); + + const tx = executor + .connect(mainnetGovernor) + .cancelL2Proposal(CCIPChainSelectors.ArbitrumOne, 1001, 0); + + await expect(tx).to.be.revertedWith("InsufficientBalanceForFees"); + }); + }); + + describe("Chain Config", () => { + it("Should allow governance to add chain config", async () => { + const { mainnetGovernor, executor } = fixture; + + await executor + .connect(mainnetGovernor) + .addChainConfig(10023, addresses.dead); + + const [isSupported, destGovernance] = await executor.chainConfig(10023); + expect(isSupported).to.be.true; + expect(destGovernance).to.equal(addresses.dead); + }); + + it("Should revert on duplicate chain config", async () => { + const { mainnetGovernor, executor } = fixture; + + const tx = executor + .connect(mainnetGovernor) + .addChainConfig(CCIPChainSelectors.ArbitrumOne, addresses.dead); + + await expect(tx).to.be.revertedWith("DuplicateChainConfig"); + }); + + it("Should disallow null address", async () => { + const { mainnetGovernor, executor } = fixture; + + const tx = executor + .connect(mainnetGovernor) + .addChainConfig(10234, addresses.zero); + + await expect(tx).to.be.revertedWith("InvalidGovernanceAddress"); + }); + + it("Should disallow anyone else to add", async () => { + const { executor, nick, rafael } = fixture; + + for (const signer of [nick, rafael]) { + const tx = executor + .connect(signer) + .addChainConfig(10234, addresses.dead); + + await expect(tx).to.be.revertedWith("Caller is not the Governor"); + } + }); + + it("Should allow governance to remove chain config", async () => { + const { mainnetGovernor, executor } = fixture; + + await executor + .connect(mainnetGovernor) + .removeChainConfig(CCIPChainSelectors.ArbitrumOne); + + const [isSupported, destGovernance] = await executor.chainConfig( + CCIPChainSelectors.ArbitrumOne + ); + expect(isSupported).to.be.false; + expect(destGovernance).to.equal(addresses.zero); + }); + + it("Should disallow removing unsupported config", async () => { + const { mainnetGovernor, executor } = fixture; + + const tx = executor.connect(mainnetGovernor).removeChainConfig(10234); + + await expect(tx).to.be.revertedWith("UnsupportedChain"); + }); + + it("Should disallow anyone else to remove", async () => { + const { executor, nick, rafael } = fixture; + + for (const signer of [nick, rafael]) { + const tx = executor + .connect(signer) + .removeChainConfig(CCIPChainSelectors.ArbitrumOne); + + await expect(tx).to.be.revertedWith("Caller is not the Governor"); + } + }); + + it("Should revert initilization with invalid config", async () => { + const { rafael } = fixture; + const impl = await hre.ethers.getContract("MainnetGovernanceExecutor"); + + const tx = impl.connect(rafael).initialize([1], []); + + await expect(tx).to.be.revertedWith("InvalidInitializationArgLength"); + }); + }); +}); diff --git a/contracts/test/token/woeth.arb.fork-test.js b/contracts/test/token/woeth.arb.fork-test.js index d47523f19d..0a0e2572ee 100644 --- a/contracts/test/token/woeth.arb.fork-test.js +++ b/contracts/test/token/woeth.arb.fork-test.js @@ -41,9 +41,9 @@ describe("ForkTest: WOETH", function () { }); it("Should not allow anyone else to mint", async () => { - const { woeth, governor, rafael, nick, burner } = fixture; + const { woeth, rafael, nick, burner } = fixture; - for (const signer of [governor, rafael, nick, burner]) { + for (const signer of [rafael, nick, burner]) { await expect( woeth.connect(signer).mint(signer.address, oethUnits("1")) ).to.be.revertedWith( @@ -90,9 +90,9 @@ describe("ForkTest: WOETH", function () { }); it("Should not allow anyone else to burn", async () => { - const { woeth, governor, rafael, nick, minter } = fixture; + const { woeth, rafael, nick, minter } = fixture; - for (const signer of [governor, rafael, nick, minter]) { + for (const signer of [rafael, nick, minter]) { // prettier-ignore await expect( woeth diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 9cb338e9bd..af8fdbeedd 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -244,4 +244,14 @@ addresses.mainnet.CurveCVXPool = "0xB576491F1E6e5E62f1d8F26062Ee822B40B0E0d4"; addresses.arbitrumOne = {}; addresses.arbitrumOne.WOETHProxy = "0xD8724322f44E5c58D7A815F542036fb17DbbF839"; +// CCIP +addresses.mainnet.CCIPRouter = "0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D"; +addresses.arbitrumOne.CCIPRouter = "0x141fa059441E0ca23ce184B6A78bafD2A517DdE8"; +// TODO: Update after deployment +addresses.arbitrumOne.L2GovernanceProxy = + "0x0000000000000000000000000000000000000024"; +addresses.mainnet.MainnetGovernanceExecutorProxy = + "0x0000000000000000000000000000000000000012"; +addresses.mainnet.L2Governor = "0x0000000000000000000000000000000000000048"; + module.exports = addresses; diff --git a/contracts/utils/constants.js b/contracts/utils/constants.js index 095e19481a..16490f292c 100644 --- a/contracts/utils/constants.js +++ b/contracts/utils/constants.js @@ -23,6 +23,16 @@ const aura_rETH_WETH_PID = 109; const balancer_rETH_WETH_PID = "0x1e19cf2d73a72ef1332c882f20534b6519be0276000200000000000000000112"; +const CCIPChainSelectors = { + ArbitrumOne: "4949039107694359620", + Mainnet: "5009297550715157269", +}; + +const L2GovernanceCommands = { + Queue: "0x0001", + Cancel: "0x0002", +}; + module.exports = { threeCRVPid, metapoolLPCRVPid, @@ -35,6 +45,8 @@ module.exports = { balancer_wstETH_sfrxETH_rETH_PID, aura_rETH_WETH_PID, balancer_rETH_WETH_PID, + CCIPChainSelectors, + L2GovernanceCommands, }; // These are all the metapool ids. For easier future reference diff --git a/contracts/utils/deploy.js b/contracts/utils/deploy.js index bdc13fcf0e..8937fd454d 100644 --- a/contracts/utils/deploy.js +++ b/contracts/utils/deploy.js @@ -16,6 +16,7 @@ const { isSmokeTest, isForkTest, getBlockTimestamp, + isArbitrumOneOrFork, isArbitrumOne, } = require("../test/helpers.js"); @@ -132,12 +133,21 @@ const _verifyProxyInitializedWithCorrectGovernor = (transactionData) => { const initProxyGovernor = ( "0x" + transactionData.slice(10 + 64 + 24, 10 + 64 + 64) ).toLowerCase(); - if ( - ![ - addresses.mainnet.Timelock.toLowerCase(), - addresses.mainnet.OldTimelock.toLowerCase(), - ].includes(initProxyGovernor) - ) { + + if (isArbitrumOneOrFork) { + // TODO: Skip for now + // Update after deployment + return; + } + + const governors = isArbitrumOneOrFork + ? [addresses.arbitrumOne.L2Governor.toLowerCase()] + : [ + addresses.mainnet.Timelock.toLowerCase(), + addresses.mainnet.OldTimelock.toLowerCase(), + ]; + + if (!governors.includes(initProxyGovernor)) { throw new Error( `Proxy contract initialised with unexpected governor: ${initProxyGovernor}` ); @@ -701,7 +711,9 @@ const submitProposalToOgvGovernance = async ( /** * Sanity checks to perform before running the deploy */ -const sanityCheckOgvGovernance = async ({ deployerIsProposer = false }) => { +const sanityCheckOgvGovernance = async ({ + deployerIsProposer = false, +} = {}) => { if (isMainnet) { // only applicable when OGV governance is the governor if (deployerIsProposer) { @@ -954,6 +966,12 @@ function deploymentWithGovernanceProposal(opts, fn) { await sanityCheckOgvGovernance({ deployerIsProposer }); const proposal = await fn(tools); + + if (proposal?.actions?.length == 0) { + // No proposal + return; + } + const propDescription = proposal.name; const propArgs = await proposeGovernanceArgs(proposal.actions); const propOpts = proposal.opts || {};