From b4b5aa4f7bdd02b43a3f8848df4610bbe2dd65bc Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 26 May 2024 22:01:13 +0530 Subject: [PATCH 1/2] Deployment with Governance proposal (#425) * Deployment with Governance proposal * Fix launch date * Split proposal * Full proposal execution flow * Get rid of hardcoded timestamps * Update readme * Tweaks * Fix rewards fork test * Round up * Fix tests * Make proposal desc more detailed * Add deployment (#428) --- README.md | 5 + build/deployments.json | 6 +- contracts/utils/GovProposalHelper.sol | 186 ++++++++++++++++++ contracts/utils/VmHelper.sol | 21 ++ foundry.toml | 1 + script/ExtraOGNForMigration.s.sol | 31 +++ script/deploy/DeployManager.sol | 93 ++++----- .../mainnet/011_OgnOgvMigrationScript.sol | 153 +++++++------- .../mainnet/012_xOGNGovernanceScript.sol | 78 ++++---- script/deploy/mainnet/BaseMainnetScript.sol | 25 ++- tests/governance/XOGNGovernanceForkTest.t.sol | 2 +- tests/staking/OGNRewardsSourceForkTest.t.sol | 10 +- tests/staking/XOGNStakingForkTest.t..sol | 2 +- 13 files changed, 442 insertions(+), 171 deletions(-) create mode 100644 contracts/utils/GovProposalHelper.sol create mode 100644 contracts/utils/VmHelper.sol create mode 100644 script/ExtraOGNForMigration.s.sol diff --git a/README.md b/README.md index 6efe32b3..282470a5 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,11 @@ In another terminal: brownie console --network hardhat-fork ``` +## Deploying contracts (with Foundry) +``` +$ DEPLOYER_PRIVATE_KEY=$DEPLOYER_PRIVATE_KEY forge script script/deploy/DeployManager.sol:DeployManager --fork-url $ALCHEMY_PROVIDER_URL --slow --legacy --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY +``` + ## Deploying contracts Setup environment variables: diff --git a/build/deployments.json b/build/deployments.json index 61dc458c..6d4d45b8 100644 --- a/build/deployments.json +++ b/build/deployments.json @@ -1,11 +1,15 @@ { "1": { "executions": { - "010_xOGNSetup": 1716312107 + "010_xOGNSetup": 1716312107, + "011_OgnOgvMigration": 1716485925 }, "contracts": { + "MIGRATOR": "0x95c347D6214614A780847b8aAF4f96Eb84f4da6d", + "MIGRATOR_IMPL": "0x946e9BED9EDebEBCE95Dea72bDD38F8c3F6efd2E", "OGN_REWARDS_SOURCE": "0x7609c88E5880e934dd3A75bCFef44E31b1Badb8b", "OGN_REWARDS_SOURCE_IMPL": "0x16890bdd817Ed1c4654430d67329CB20b0B71bB0", + "VEOGV_IMPL": "0x2D86E0342a0d263Dff712CD0Aa96d075F61974ed", "XOGN": "0x63898b3b6Ef3d39332082178656E9862bee45C57", "XOGN_IMPL": "0x97711c7a5D64A064a95d10e37f786d2bD8b1F3c8" } diff --git a/contracts/utils/GovProposalHelper.sol b/contracts/utils/GovProposalHelper.sol new file mode 100644 index 00000000..13ba78aa --- /dev/null +++ b/contracts/utils/GovProposalHelper.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.10; + +import {Addresses} from "contracts/utils/Addresses.sol"; +import "forge-std/console.sol"; + +import "OpenZeppelin/openzeppelin-contracts@4.6.0/contracts/utils/Strings.sol"; +import {IGovernor} from "OpenZeppelin/openzeppelin-contracts@4.6.0/contracts/governance/IGovernor.sol"; +import {Governance} from "../Governance.sol"; + +import "contracts/utils/VmHelper.sol"; + +struct GovAction { + address target; + uint256 value; + string fullsig; + bytes data; +} + +struct GovProposal { + string description; + GovAction[] actions; +} + +library GovProposalHelper { + using VmHelper for Vm; + + function id(GovProposal memory prop) internal view returns (uint256 proposalId) { + bytes32 descriptionHash = keccak256(bytes(prop.description)); + (address[] memory targets, uint256[] memory values, bytes[] memory calldatas) = getParams(prop); + + proposalId = uint256(keccak256(abi.encode(targets, values, calldatas, descriptionHash))); + } + + function getParams(GovProposal memory prop) + internal + view + returns (address[] memory targets, uint256[] memory values, bytes[] memory calldatas) + { + uint256 actionLen = prop.actions.length; + targets = new address[](actionLen); + values = new uint256[](actionLen); + + string[] memory sigs = new string[](actionLen); + bytes[] memory data = new bytes[](actionLen); + + for (uint256 i = 0; i < actionLen; ++i) { + targets[i] = prop.actions[i].target; + sigs[i] = prop.actions[i].fullsig; + data[i] = prop.actions[i].data; + values[i] = prop.actions[i].value; + } + + calldatas = _encodeCalldata(sigs, data); + } + + function _encodeCalldata(string[] memory signatures, bytes[] memory calldatas) + private + pure + returns (bytes[] memory) + { + bytes[] memory fullcalldatas = new bytes[](calldatas.length); + + for (uint256 i = 0; i < signatures.length; ++i) { + fullcalldatas[i] = bytes(signatures[i]).length == 0 + ? calldatas[i] + : abi.encodePacked(bytes4(keccak256(bytes(signatures[i]))), calldatas[i]); + } + + return fullcalldatas; + } + + function setDescription(GovProposal storage prop, string memory description) internal { + prop.description = description; + } + + function action(GovProposal storage prop, address target, string memory fullsig, bytes memory data) internal { + prop.actions.push(GovAction({target: target, fullsig: fullsig, data: data, value: 0})); + } + + function getProposeCalldata(GovProposal memory prop) internal view returns (bytes memory proposeCalldata) { + (address[] memory targets, uint256[] memory values, bytes[] memory calldatas) = getParams(prop); + + proposeCalldata = abi.encodeWithSignature( + "propose(address[],uint256[],bytes[],string)", targets, values, calldatas, prop.description + ); + } + + function impersonateAndSimulate(GovProposal memory prop) internal { + address VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); + Vm vm = Vm(VM_ADDRESS); + console.log("Impersonating timelock to simulate governance proposal..."); + vm.startPrank(Addresses.TIMELOCK); + for (uint256 i = 0; i < prop.actions.length; i++) { + GovAction memory propAction = prop.actions[i]; + bytes memory sig = abi.encodePacked(bytes4(keccak256(bytes(propAction.fullsig)))); + (bool success, bytes memory data) = propAction.target.call(abi.encodePacked(sig, propAction.data)); + if (!success) { + console.log(propAction.fullsig); + revert("Governance action failed"); + } + } + vm.stopPrank(); + console.log("Governance proposal simulation complete"); + } + + function simulate(GovProposal memory prop, address governanceAddr) internal { + address VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); + Vm vm = Vm(VM_ADDRESS); + + uint256 proposalId = id(prop); + + Governance governance = Governance(payable(governanceAddr)); + + vm.startPrank(Addresses.GOV_MULTISIG); + + uint256 snapshot = governance.proposalSnapshot(proposalId); + + if (snapshot == 0) { + bytes memory proposeData = getProposeCalldata(prop); + + console.log("----------------------------------"); + console.log("Create following tx on Governance:"); + console.log("To:", governanceAddr); + console.log("Data:"); + console.logBytes(proposeData); + console.log("----------------------------------"); + + // Proposal doesn't exists, create it + console.log("Creating proposal on fork..."); + (bool success, bytes memory data) = governanceAddr.call(proposeData); + } + + IGovernor.ProposalState state = governance.state(proposalId); + + if (state == IGovernor.ProposalState.Executed) { + // Skipping executed proposal + return; + } + + if (state == IGovernor.ProposalState.Pending) { + console.log("Waiting for voting period..."); + // Wait for voting to start + vm.roll(block.number + 10); + vm.warp(block.timestamp + 10 minutes); + + state = governance.state(proposalId); + } + + if (state == IGovernor.ProposalState.Active) { + console.log("Voting on proposal..."); + // Vote on proposal + governance.castVote(proposalId, 1); + // Wait for voting to end + vm.roll(governance.proposalDeadline(proposalId) + 20); + vm.warp(block.timestamp + 2 days); + + state = governance.state(proposalId); + } + + if (state == IGovernor.ProposalState.Succeeded) { + console.log("Queuing proposal..."); + governance.queue(proposalId); + + state = governance.state(proposalId); + } + + if (state == IGovernor.ProposalState.Queued) { + console.log("Executing proposal"); + // Wait for timelock + vm.roll(governance.proposalEta(proposalId) + 20); + vm.warp(block.timestamp + 2 days); + + governance.execute(proposalId); + + state = governance.state(proposalId); + } + + if (state != IGovernor.ProposalState.Executed) { + revert("Unexpected proposal state"); + } + + vm.stopPrank(); + } +} diff --git a/contracts/utils/VmHelper.sol b/contracts/utils/VmHelper.sol new file mode 100644 index 00000000..0c964780 --- /dev/null +++ b/contracts/utils/VmHelper.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.10; + +import "forge-std/Vm.sol"; + +library VmHelper { + function getVM() internal view returns (Vm vm) { + address VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); + vm = Vm(VM_ADDRESS); + } + + function isForkEnv(Vm vm) public view returns (bool) { + return vm.isContext(VmSafe.ForgeContext.ScriptDryRun) || vm.isContext(VmSafe.ForgeContext.Test) + || vm.isContext(VmSafe.ForgeContext.TestGroup); + } + + function isTestEnv(Vm vm) public view returns (bool) { + return vm.isContext(VmSafe.ForgeContext.Test) || vm.isContext(VmSafe.ForgeContext.TestGroup); + } +} diff --git a/foundry.toml b/foundry.toml index b214348a..8aaafdf8 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,6 +5,7 @@ remappings = [ "contracts/=./contracts", "script/=./script", "tests/=./tests", + "utils/=./contracts/utils", "OpenZeppelin/openzeppelin-contracts@02fcc75bb7f35376c22def91b0fb9bc7a50b9458/=./lib/openzeppelin-contracts", "OpenZeppelin/openzeppelin-contracts-upgradeable@a16f26a063cd018c4c986832c3df332a131f53b9/=./lib/openzeppelin-contracts-upgradeable", "OpenZeppelin/openzeppelin-contracts@4.6.0/=./lib/openzeppelin-contracts", diff --git a/script/ExtraOGNForMigration.s.sol b/script/ExtraOGNForMigration.s.sol new file mode 100644 index 00000000..efa59dd0 --- /dev/null +++ b/script/ExtraOGNForMigration.s.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.10; + +import "forge-std/Script.sol"; +import {Addresses} from "contracts/utils/Addresses.sol"; +import {IMintableERC20} from "contracts/interfaces/IMintableERC20.sol"; +import {RewardsSource} from "contracts/RewardsSource.sol"; + +contract ExtraOGNForMigration is Script { + uint256 constant OGN_EPOCH = 1717041600; // May 30, 2024 GMT + + // Ref: https://snapshot.org/#/origingov.eth/proposal/0x741893a4d9838c0b69fac03650756e21fe00ec35b5309626bb0d6b816f861f9b + uint256 public constant OGN_MINTED = 409_664_846 ether; + + function run() external { + vm.warp(OGN_EPOCH); + + IMintableERC20 ogv = IMintableERC20(Addresses.OGV); + + uint256 rewards = RewardsSource(Addresses.OGV_REWARDS_PROXY).previewRewards(); + + uint256 ogvSupply = ogv.totalSupply(); + uint256 maxOgnNeeded = ((ogvSupply + rewards) * 0.09137 ether) / 1 ether; + + console.log("OGV Supply", ogvSupply / 1 ether); + console.log("Pending OGV Rewards", rewards / 1 ether); + console.log("Max OGN Needed", maxOgnNeeded / 1 ether); + console.log("OGN from Treasury", (maxOgnNeeded - OGN_MINTED) / 1 ether); + } +} diff --git a/script/deploy/DeployManager.sol b/script/deploy/DeployManager.sol index 6cf75bac..9e77a949 100644 --- a/script/deploy/DeployManager.sol +++ b/script/deploy/DeployManager.sol @@ -10,19 +10,18 @@ import {XOGNSetupScript} from "./mainnet/010_xOGNSetupScript.sol"; import {OgnOgvMigrationScript} from "./mainnet/011_OgnOgvMigrationScript.sol"; import {XOGNGovernanceScript} from "./mainnet/012_xOGNGovernanceScript.sol"; +import "contracts/utils/VmHelper.sol"; + contract DeployManager is Script { + using VmHelper for Vm; + mapping(string => address) public deployedContracts; mapping(string => bool) public scriptsExecuted; string internal forkFileId = ""; - function isForked() public view returns (bool) { - return vm.isContext(VmSafe.ForgeContext.ScriptDryRun) || vm.isContext(VmSafe.ForgeContext.Test) - || vm.isContext(VmSafe.ForgeContext.TestGroup); - } - function getDeploymentFilePath() public view returns (string memory) { - return isForked() ? getForkDeploymentFilePath() : getMainnetDeploymentFilePath(); + return vm.isForkEnv() ? getForkDeploymentFilePath() : getMainnetDeploymentFilePath(); } function getMainnetDeploymentFilePath() public view returns (string memory) { @@ -30,7 +29,7 @@ contract DeployManager is Script { } function getForkDeploymentFilePath() public view returns (string memory) { - return string(abi.encodePacked(vm.projectRoot(), "/build/deployments-fork-", forkFileId, ".json")); + return string(abi.encodePacked(vm.projectRoot(), "/build/deployments-fork", forkFileId, ".json")); } function setUp() external { @@ -53,7 +52,7 @@ contract DeployManager is Script { ); } - if (isForked()) { + if (vm.isForkEnv()) { // Duplicate Mainnet File vm.writeFile(getForkDeploymentFilePath(), vm.readFile(mainnetFilePath)); } @@ -67,6 +66,10 @@ contract DeployManager is Script { } function _runDeployFile(BaseMainnetScript deployScript) internal { + if (deployScript.skip()) { + return; + } + string memory chainIdStr = Strings.toString(block.chainid); string memory chainIdKey = string(abi.encodePacked(".", chainIdStr)); @@ -90,12 +93,6 @@ contract DeployManager is Script { scriptsExecuted[executionKeys[i]] = true; } - if (scriptsExecuted[deployScript.DEPLOY_NAME()]) { - // TODO: Handle any active governance proposal - console.log("Skipping already deployed script"); - return; - } - /** * Pre-deployment */ @@ -112,38 +109,44 @@ contract DeployManager is Script { deployScript.preloadDeployedContract(existingContracts[i], deployedAddr); } - // Deployment - deployScript.setUp(); - deployScript.run(); - - /** - * Post-deployment - */ - BaseMainnetScript.DeployRecord[] memory records = deployScript.getAllDeployRecords(); - - for (uint256 i = 0; i < records.length; ++i) { - string memory name = records[i].name; - address addr = records[i].addr; - - console.log(string(abi.encodePacked("> Recorded Deploy of ", name, " at")), addr); - networkDeployments = vm.serializeAddress(contractsKey, name, addr); - deployedContracts[name] = addr; + if (scriptsExecuted[deployScript.DEPLOY_NAME()]) { + // Governance handling + deployScript.handleGovernanceProposal(); + console.log("Skipping already deployed script"); + } else { + // Deployment + deployScript.setUp(); + deployScript.run(); + + /** + * Post-deployment + */ + BaseMainnetScript.DeployRecord[] memory records = deployScript.getAllDeployRecords(); + + for (uint256 i = 0; i < records.length; ++i) { + string memory name = records[i].name; + address addr = records[i].addr; + + console.log(string(abi.encodePacked("> Recorded Deploy of ", name, " at")), addr); + networkDeployments = vm.serializeAddress(contractsKey, name, addr); + deployedContracts[name] = addr; + } + + // Sleep 0.5s so that the previous write is complete + vm.sleep(500); + vm.writeJson(networkDeployments, deploymentsFilePath, contractsKey); + console.log("> Deployment addresses stored."); + + /** + * Write Execution History + */ + currentExecutions = vm.serializeUint(executionsKey, deployScript.DEPLOY_NAME(), block.timestamp); + + // Sleep 0.5s so that the previous write is complete + vm.sleep(500); + vm.writeJson(currentExecutions, deploymentsFilePath, executionsKey); + console.log("> Deploy script execution complete."); } - - // Sleep 0.5s so that the previous write is complete - vm.sleep(500); - vm.writeJson(networkDeployments, deploymentsFilePath, contractsKey); - console.log("> Deployment addresses stored."); - - /** - * Write Execution History - */ - currentExecutions = vm.serializeUint(executionsKey, deployScript.DEPLOY_NAME(), block.timestamp); - - // Sleep 0.5s so that the previous write is complete - vm.sleep(500); - vm.writeJson(currentExecutions, deploymentsFilePath, executionsKey); - console.log("> Deploy script execution complete."); } function getDeployment(string calldata contractName) external view returns (address) { diff --git a/script/deploy/mainnet/011_OgnOgvMigrationScript.sol b/script/deploy/mainnet/011_OgnOgvMigrationScript.sol index f1f5c9f8..b2cfbdce 100644 --- a/script/deploy/mainnet/011_OgnOgvMigrationScript.sol +++ b/script/deploy/mainnet/011_OgnOgvMigrationScript.sol @@ -7,26 +7,34 @@ import {Vm} from "forge-std/Vm.sol"; import {Addresses} from "contracts/utils/Addresses.sol"; -import {FixedRateRewardsSourceProxy} from "contracts/upgrades/FixedRateRewardsSourceProxy.sol"; -import {ExponentialStakingProxy} from "contracts/upgrades/ExponentialStakingProxy.sol"; import {MigratorProxy} from "contracts/upgrades/MigratorProxy.sol"; -import {ExponentialStaking} from "contracts/ExponentialStaking.sol"; -import {FixedRateRewardsSource} from "contracts/FixedRateRewardsSource.sol"; import {OgvStaking} from "contracts/OgvStaking.sol"; import {Migrator} from "contracts/Migrator.sol"; import {Timelock} from "contracts/Timelock.sol"; -import {GovFive} from "contracts/utils/GovFive.sol"; import {IMintableERC20} from "contracts/interfaces/IMintableERC20.sol"; -import {IOGNGovernance} from "contracts/interfaces/IOGNGovernance.sol"; -contract OgnOgvMigrationScript is BaseMainnetScript { - using GovFive for GovFive.GovFiveProposal; +import {GovProposal, GovProposalHelper} from "contracts/utils/GovProposalHelper.sol"; - GovFive.GovFiveProposal govFive; +contract OgnOgvMigrationScript is BaseMainnetScript { + using GovProposalHelper for GovProposal; string public constant override DEPLOY_NAME = "011_OgnOgvMigration"; + GovProposal public govProposal; + + uint256 public constant OGN_EPOCH = 1717041600; // May 30, 2024 GMT + + // Ref: https://snapshot.org/#/origingov.eth/proposal/0x741893a4d9838c0b69fac03650756e21fe00ec35b5309626bb0d6b816f861f9b + uint256 public constant OGN_TO_MINT = 409_664_846 ether; + + // From `script/ExtraOGNForMigration.s.sol`, rounded off + uint256 public constant EXTRA_OGN_FOR_MIGRATION = 3_010_000 ether; + + uint256 public constant EXTRA_OGN_FOR_REWARDS = 344_736 ether; + + uint256 public constant REWARDS_PER_SECOND = 0.57 ether; + constructor() {} function _execute() internal override { @@ -54,102 +62,99 @@ contract OgnOgvMigrationScript is BaseMainnetScript { console.log("- Migrator init"); migratorProxy.initialize(address(migratorImpl), Addresses.TIMELOCK, ""); - _buildGnosisTx(); + // _buildGovernanceProposal(); } - function _buildGnosisTx() internal { + function _buildGovernanceProposal() internal override { Timelock timelock = Timelock(payable(Addresses.TIMELOCK)); address ognRewardsSourceProxy = deployedContracts["OGN_REWARDS_SOURCE"]; address veOgvImpl = deployedContracts["VEOGV_IMPL"]; - govFive.setName("OGV Migration to OGN"); - // Todo: Fuller description - govFive.setDescription("Deploy OGV-OGN migration contracts and revoke OGV Governance roles"); + govProposal.setDescription( + "OGV>OGN Migration Contracts" + "\n\nThis proposal deploys, funds and enables the Migrator contract which can be used to migrate OGV to OGN and also veOGV to xOGN." + "\n\nThe proposal mints 409,664,846 OGN (as specificed in the previous off-chain snapshot governance proposal). It also uses some OGN from the treasury multisig to account for the increase in OGV supply due to inflation since the snapshot proposal was posted." + "\n\nThis proposal also revokes all roles that the OGV Governance has on the Timelock. Buyback contracts are upgraded to buy OGN instead of OGV" + ); - console.log(address(veOgvImpl)); - govFive.action(Addresses.VEOGV, "upgradeTo(address)", abi.encode(veOgvImpl)); + // Realize any pending rewards + govProposal.action(Addresses.VEOGV, "collectRewards()", ""); + // Upgrade veOGV implementation + govProposal.action(Addresses.VEOGV, "upgradeTo(address)", abi.encode(veOgvImpl)); - govFive.action( + // Revoke access from OGV governance + govProposal.action( Addresses.TIMELOCK, "revokeRole(bytes32,address)", abi.encode(timelock.PROPOSER_ROLE(), Addresses.GOVERNOR_FIVE) ); - govFive.action( + govProposal.action( Addresses.TIMELOCK, "revokeRole(bytes32,address)", abi.encode(timelock.CANCELLER_ROLE(), Addresses.GOVERNOR_FIVE) ); - govFive.action( + govProposal.action( Addresses.TIMELOCK, "revokeRole(bytes32,address)", abi.encode(timelock.EXECUTOR_ROLE(), Addresses.GOVERNOR_FIVE) ); - govFive.action(Addresses.OUSD_BUYBACK, "upgradeTo(address)", abi.encode(Addresses.OUSD_BUYBACK_IMPL)); // Todo, use latest deployed address - govFive.action(Addresses.OUSD_BUYBACK, "setRewardsSource(address)", abi.encode(ognRewardsSourceProxy)); - govFive.action(Addresses.OETH_BUYBACK, "upgradeTo(address)", abi.encode(Addresses.OETH_BUYBACK_IMPL)); // Todo, use latest deployed address - govFive.action(Addresses.OETH_BUYBACK, "setRewardsSource(address)", abi.encode(ognRewardsSourceProxy)); - - // Mint token proposal from OGN governance - IMintableERC20 ogv = IMintableERC20(Addresses.OGV); - // Mint additional OGN, will get returned after starting migration - uint256 ognToMint = ((ogv.totalSupply() * 0.09137 ether) / 1 ether) + 1_000_000 ether; - - address[] memory targets = new address[](1); - string[] memory sigs = new string[](1); - bytes[] memory calldatas = new bytes[](1); - - // OGN Gov 1: Mint OGN - targets[0] = Addresses.OGN; - sigs[0] = "mint(address,uint256)"; - calldatas[0] = abi.encode(deployedContracts["MIGRATOR"], ognToMint); - - govFive.action( - Addresses.OGN_GOVERNOR, - "propose(address[],string[],bytes[],string)", - abi.encode(targets, sigs, calldatas, "") + // Upgrade buyback contracts & configure them + govProposal.action(Addresses.OUSD_BUYBACK, "upgradeTo(address)", abi.encode(Addresses.OUSD_BUYBACK_IMPL)); + govProposal.action(Addresses.OUSD_BUYBACK, "setRewardsSource(address)", abi.encode(ognRewardsSourceProxy)); + govProposal.action(Addresses.OETH_BUYBACK, "upgradeTo(address)", abi.encode(Addresses.OETH_BUYBACK_IMPL)); + govProposal.action(Addresses.OETH_BUYBACK, "setRewardsSource(address)", abi.encode(ognRewardsSourceProxy)); + + // Mint OGN required + govProposal.action( + Addresses.OGN, "mint(address,uint256)", abi.encode(deployedContracts["MIGRATOR"], OGN_TO_MINT) ); - if (!isForked) { - govFive.printTxData(); - } - } + // Transfer in excess OGN from Multisig (for migration) + govProposal.action( + Addresses.OGN, + "transferFrom(address,address,uint256)", + abi.encode(Addresses.GOV_MULTISIG, deployedContracts["MIGRATOR"], EXTRA_OGN_FOR_MIGRATION) + ); - function _fork() internal override { - // Simulate execute on fork - govFive.execute(); + // Transfer in OGN from Multisig (for rewards) + govProposal.action( + Addresses.OGN, + "transferFrom(address,address,uint256)", + abi.encode(Addresses.GOV_MULTISIG, deployedContracts["OGN_REWARDS_SOURCE"], EXTRA_OGN_FOR_REWARDS) + ); - vm.startPrank(Addresses.GOV_MULTISIG); + // Enable rewards for staking + govProposal.action( + deployedContracts["OGN_REWARDS_SOURCE"], + "setRewardsPerSecond(uint192)", + abi.encode(uint192(REWARDS_PER_SECOND)) + ); - IOGNGovernance ognGovernance = IOGNGovernance(Addresses.OGN_GOVERNOR); - uint256 proposalId = ognGovernance.proposalCount(); + // Start migration + govProposal.action(deployedContracts["MIGRATOR"], "start()", ""); - uint256 state = ognGovernance.state(proposalId); + // Ensure solvency and transfer out excess OGN + govProposal.action( + deployedContracts["MIGRATOR"], "transferExcessTokens(address)", abi.encode(Addresses.GOV_MULTISIG) + ); + } - if (state == 0) { - console.log("Queueing OGN multisig proposal..."); - ognGovernance.queue(proposalId); - state = ognGovernance.state(proposalId); - } + function _fork() internal override { + IMintableERC20 ogn = IMintableERC20(Addresses.OGN); - if (state == 1) { - console.log("Executing OGN multisig proposal..."); - vm.warp(block.timestamp + 2 days); - ognGovernance.execute(proposalId); - } - vm.stopPrank(); + // Make sure multisig has enough of OGN + // to fund migration and rewards + uint256 additionalOGN = EXTRA_OGN_FOR_MIGRATION + EXTRA_OGN_FOR_REWARDS; + vm.prank(Addresses.TIMELOCK); + ogn.mint(Addresses.GOV_MULTISIG, additionalOGN); - IMintableERC20 ogn = IMintableERC20(Addresses.OGN); + // And timelock can move it + vm.prank(Addresses.GOV_MULTISIG); + ogn.approve(Addresses.TIMELOCK, additionalOGN); - // Start migration - vm.startPrank(Addresses.TIMELOCK); - // TODO: To be called by multisig after mint proposal is executed - Migrator migrator = Migrator(deployedContracts["MIGRATOR"]); - migrator.start(); - migrator.transferExcessTokens(Addresses.GOV_MULTISIG); - vm.stopPrank(); - - console.log("Migration started"); + // Simulate proposal on OGV Governance + govProposal.simulate(Addresses.GOVERNOR_FIVE); } } diff --git a/script/deploy/mainnet/012_xOGNGovernanceScript.sol b/script/deploy/mainnet/012_xOGNGovernanceScript.sol index 14b6c37d..e37993c9 100644 --- a/script/deploy/mainnet/012_xOGNGovernanceScript.sol +++ b/script/deploy/mainnet/012_xOGNGovernanceScript.sol @@ -7,33 +7,25 @@ import {Vm} from "forge-std/Vm.sol"; import {Addresses} from "contracts/utils/Addresses.sol"; -import {FixedRateRewardsSourceProxy} from "contracts/upgrades/FixedRateRewardsSourceProxy.sol"; -import {ExponentialStakingProxy} from "contracts/upgrades/ExponentialStakingProxy.sol"; -import {MigratorProxy} from "contracts/upgrades/MigratorProxy.sol"; - -import {ExponentialStaking} from "contracts/ExponentialStaking.sol"; -import {FixedRateRewardsSource} from "contracts/FixedRateRewardsSource.sol"; -import {OgvStaking} from "contracts/OgvStaking.sol"; -import {Migrator} from "contracts/Migrator.sol"; import {Timelock} from "contracts/Timelock.sol"; import {Governance} from "contracts/Governance.sol"; import {GovFive} from "contracts/utils/GovFive.sol"; -import {IMintableERC20} from "contracts/interfaces/IMintableERC20.sol"; +import {VmHelper} from "utils/VmHelper.sol"; import "OpenZeppelin/openzeppelin-contracts@4.6.0/contracts/token/ERC20/extensions/ERC20Votes.sol"; import "OpenZeppelin/openzeppelin-contracts@4.6.0/contracts/governance/TimelockController.sol"; contract XOGNGovernanceScript is BaseMainnetScript { using GovFive for GovFive.GovFiveProposal; + using VmHelper for Vm; - GovFive.GovFiveProposal govFive; + GovFive.GovFiveProposal public govProposal; string public constant override DEPLOY_NAME = "012_xOGNGovernance"; - uint256 constant OGN_EPOCH = 1717041600; // May 30, 2024 GMT - uint256 constant REWARDS_PER_SECOND = 300000 ether / uint256(24 * 60 * 60); // 300k per day + uint256 public constant OGN_EPOCH = 1717041600; // May 30, 2024 GMT constructor() {} @@ -47,45 +39,57 @@ contract XOGNGovernanceScript is BaseMainnetScript { _recordDeploy("XOGN_GOV", address(governance)); - _buildGnosisTx(); + _buildGovernanceProposal(); } - function _buildGnosisTx() internal { + function _buildGovernanceProposal() internal override { Timelock timelock = Timelock(payable(Addresses.TIMELOCK)); address xognGov = deployedContracts["XOGN_GOV"]; - govFive.setName("Enable OGN Governance & Begin Rewards"); + address ognRewardsSourceProxy = deployedContracts["OGN_REWARDS_SOURCE"]; + address veOgvImpl = deployedContracts["VEOGV_IMPL"]; - govFive.setDescription("Grant roles on Timelock to OGN Governance"); + govProposal.setName("Grant access to OGN Governance"); - govFive.action(Addresses.TIMELOCK, "grantRole(bytes32,address)", abi.encode(timelock.PROPOSER_ROLE(), xognGov)); - govFive.action(Addresses.TIMELOCK, "grantRole(bytes32,address)", abi.encode(timelock.CANCELLER_ROLE(), xognGov)); - govFive.action(Addresses.TIMELOCK, "grantRole(bytes32,address)", abi.encode(timelock.EXECUTOR_ROLE(), xognGov)); + govProposal.setDescription("Grant access to OGN Governance"); - // Enable rewards for staking - govFive.action( - deployedContracts["OGN_REWARDS_SOURCE"], - "setRewardsPerSecond(uint192)", - abi.encode(uint192(REWARDS_PER_SECOND)) + // Grant access to OGN Governance + govProposal.action( + Addresses.TIMELOCK, "grantRole(bytes32,address)", abi.encode(timelock.PROPOSER_ROLE(), xognGov) + ); + govProposal.action( + Addresses.TIMELOCK, "grantRole(bytes32,address)", abi.encode(timelock.CANCELLER_ROLE(), xognGov) + ); + govProposal.action( + Addresses.TIMELOCK, "grantRole(bytes32,address)", abi.encode(timelock.EXECUTOR_ROLE(), xognGov) ); - if (!isForked) { - govFive.printTxData(); - } + // Revoke access from Multisig + govProposal.action( + Addresses.TIMELOCK, + "revokeRole(bytes32,address)", + abi.encode(timelock.PROPOSER_ROLE(), Addresses.GOV_MULTISIG) + ); + govProposal.action( + Addresses.TIMELOCK, + "revokeRole(bytes32,address)", + abi.encode(timelock.CANCELLER_ROLE(), Addresses.GOV_MULTISIG) + ); + govProposal.action( + Addresses.TIMELOCK, + "revokeRole(bytes32,address)", + abi.encode(timelock.EXECUTOR_ROLE(), Addresses.GOV_MULTISIG) + ); } function _fork() internal override { - IMintableERC20 ogn = IMintableERC20(Addresses.OGN); - - // Mint enough OGN to fund 100 days of rewards - vm.prank(Addresses.OGN_GOVERNOR); - ogn.mint(deployedContracts["OGN_REWARDS_SOURCE"], 30_000_000 ether); - - // Go to the start of everything - vm.warp(OGN_EPOCH); + // Simulate execute on fork by impersonating Timelock + govProposal.execute(); + } - // Simulate execute on fork - govFive.execute(); + function skip() external view override returns (bool) { + // Don't deploy on Mainnet for now + return !vm.isForkEnv(); } } diff --git a/script/deploy/mainnet/BaseMainnetScript.sol b/script/deploy/mainnet/BaseMainnetScript.sol index 5effa75c..15fe1aad 100644 --- a/script/deploy/mainnet/BaseMainnetScript.sol +++ b/script/deploy/mainnet/BaseMainnetScript.sol @@ -6,8 +6,14 @@ import "forge-std/Script.sol"; import "OpenZeppelin/openzeppelin-contracts@4.6.0/contracts/utils/Strings.sol"; import {Addresses} from "contracts/utils/Addresses.sol"; +import {GovProposal, GovProposalHelper} from "contracts/utils/GovProposalHelper.sol"; + +import "utils/VmHelper.sol"; abstract contract BaseMainnetScript is Script { + using VmHelper for Vm; + using GovProposalHelper for GovProposal; + uint256 public deployBlockNum = type(uint256).max; bool isForked = false; @@ -48,8 +54,7 @@ abstract contract BaseMainnetScript is Script { return; } - isForked = vm.isContext(VmSafe.ForgeContext.ScriptDryRun) || vm.isContext(VmSafe.ForgeContext.Test) - || vm.isContext(VmSafe.ForgeContext.TestGroup); + isForked = vm.isForkEnv(); if (isForked) { address impersonator = Addresses.INITIAL_DEPLOYER; @@ -66,6 +71,7 @@ abstract contract BaseMainnetScript is Script { if (isForked) { vm.stopPrank(); + _buildGovernanceProposal(); _fork(); } else { vm.stopBroadcast(); @@ -74,7 +80,18 @@ abstract contract BaseMainnetScript is Script { function DEPLOY_NAME() external view virtual returns (string memory); - function _execute() internal virtual {} + function skip() external view virtual returns (bool) { + return false; + } + + function _execute() internal virtual; - function _fork() internal virtual {} + function _fork() internal virtual; + + function _buildGovernanceProposal() internal virtual {} + + function handleGovernanceProposal() external virtual { + _buildGovernanceProposal(); + _fork(); + } } diff --git a/tests/governance/XOGNGovernanceForkTest.t.sol b/tests/governance/XOGNGovernanceForkTest.t.sol index dca93607..37a1f7fc 100644 --- a/tests/governance/XOGNGovernanceForkTest.t.sol +++ b/tests/governance/XOGNGovernanceForkTest.t.sol @@ -48,7 +48,7 @@ contract XOGNGovernanceForkTest is Test { ognRewardsSource = deployManager.getDeployment("OGN_REWARDS_SOURCE"); - vm.startPrank(Addresses.OGN_GOVERNOR); + vm.startPrank(Addresses.TIMELOCK); ogn.mint(alice, 200000 ether); ogn.mint(bob, 200000 ether); ogn.mint(xognWhale, 1000_000_000 ether); diff --git a/tests/staking/OGNRewardsSourceForkTest.t.sol b/tests/staking/OGNRewardsSourceForkTest.t.sol index 0578fab0..296864e9 100644 --- a/tests/staking/OGNRewardsSourceForkTest.t.sol +++ b/tests/staking/OGNRewardsSourceForkTest.t.sol @@ -27,7 +27,7 @@ contract OGNRewardsSourceForkTest is Test { uint256 constant OGN_EPOCH = 1717041600; // May 30, 2024 GMT - uint256 constant REWARDS_PER_SECOND = 300000 ether / uint256(24 * 60 * 60); // 300k per day + uint256 constant REWARDS_PER_SECOND = 0.57 ether; int256 constant NEW_STAKE = -1; @@ -47,7 +47,7 @@ contract OGNRewardsSourceForkTest is Test { ognRewardsSource = FixedRateRewardsSource(deployManager.getDeployment("OGN_REWARDS_SOURCE")); - vm.startPrank(Addresses.OGN_GOVERNOR); + vm.startPrank(Addresses.TIMELOCK); ogn.mint(alice, 200000 ether); ogn.mint(bob, 200000 ether); vm.stopPrank(); @@ -67,12 +67,6 @@ contract OGNRewardsSourceForkTest is Test { } function testRewardDistribution() external { - if (block.timestamp < OGN_EPOCH) { - // If it's post launch date, skip this test - (uint64 lastColect,) = ognRewardsSource.rewardConfig(); - assertEq(lastColect, OGN_EPOCH, "last collect not updated (before deploy)"); - } - uint256 rewardsBefore = ognRewardsSource.previewRewards(); vm.warp(block.timestamp + 1 days); assertEq( diff --git a/tests/staking/XOGNStakingForkTest.t..sol b/tests/staking/XOGNStakingForkTest.t..sol index db1a188c..89fcadf6 100644 --- a/tests/staking/XOGNStakingForkTest.t..sol +++ b/tests/staking/XOGNStakingForkTest.t..sol @@ -48,7 +48,7 @@ contract XOGNStakingForkTest is Test { ognRewardsSource = deployManager.getDeployment("OGN_REWARDS_SOURCE"); - vm.startPrank(Addresses.OGN_GOVERNOR); + vm.startPrank(Addresses.TIMELOCK); ogn.mint(alice, 200000 ether); ogn.mint(bob, 200000 ether); ogn.mint(xognWhale, 1000_000_000 ether); From a53241534bbad0a1d6bd77349f31919fb08c7bff Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Mon, 27 May 2024 08:46:51 -0400 Subject: [PATCH 2/2] One day delay before voting (#430) --- contracts/Governance.sol | 6 +++++- tests/governance/XOGNGovernanceForkTest.t.sol | 10 +++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/contracts/Governance.sol b/contracts/Governance.sol index 79251d6f..df35dcb7 100644 --- a/contracts/Governance.sol +++ b/contracts/Governance.sol @@ -17,7 +17,11 @@ contract Governance is { constructor(ERC20Votes _token, TimelockController _timelock) Governor("Origin DeFi Governance") - GovernorSettings(1, /* 1 block */ 14416, /* ~2 days (86400 / 12) * 2 */ 100000 * 1e18 /* 100k xOGN */ ) + GovernorSettings( + 7200, /* ~1 day (86400 / 12) */ + 14416, /* ~2 days (86400 / 12) * 2 */ + 100000 * 1e18 /* 100k xOGN */ + ) GovernorVotes(_token) GovernorVotesQuorumFraction(20) // Default quorum denominator is 100, so 20/100 or 20% GovernorTimelockControl(_timelock) diff --git a/tests/governance/XOGNGovernanceForkTest.t.sol b/tests/governance/XOGNGovernanceForkTest.t.sol index 37a1f7fc..831fd0bc 100644 --- a/tests/governance/XOGNGovernanceForkTest.t.sol +++ b/tests/governance/XOGNGovernanceForkTest.t.sol @@ -68,7 +68,7 @@ contract XOGNGovernanceForkTest is Test { } function testVotingDelay() external view { - assertEq(xognGov.votingDelay(), 1, "Incorrect voting delay"); + assertEq(xognGov.votingDelay(), 7200, "Incorrect voting delay"); } function testVotingPeriod() external view { @@ -201,8 +201,8 @@ contract XOGNGovernanceForkTest is Test { assertEq(uint256(xognGov.state(proposalId)), 0, "Proposal wasn't created"); // Wait for voting to start - vm.warp(block.timestamp + 10 minutes); - vm.roll(block.number + 100); + vm.warp(block.timestamp + 1 days); + vm.roll(block.number + 7300); assertEq(uint256(xognGov.state(proposalId)), 1, "Proposal isn't active"); // Vote on proposal @@ -258,8 +258,8 @@ contract XOGNGovernanceForkTest is Test { assertEq(uint256(xognGov.state(proposalId)), 0, "Proposal wasn't created"); // Wait for voting to start - vm.warp(block.timestamp + 10 minutes); - vm.roll(block.number + 100); + vm.warp(block.timestamp + 1 days); + vm.roll(block.number + 7300); assertEq(uint256(xognGov.state(proposalId)), 1, "Proposal isn't active"); // Vote on proposal