From df1e6f4be2973117fc0390034327dcd3ed66b6e4 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 15 May 2024 22:42:41 +0400 Subject: [PATCH 01/12] Add fork test tooling --- .github/workflows/main.yaml | 18 ++- .gitignore | 1 + .gitmodules | 3 +- build/deployments.json | 3 + foundry.toml | 1 + script/deploy/DeploymentManager.sol | 105 ++++++++++++++++++ .../mainnet/010_OgnOgvMigrationScript.sol | 71 +++++------- script/deploy/mainnet/BaseMainnetScript.sol | 25 ++++- tests/XOGNForkTest.sol | 24 ++++ 9 files changed, 207 insertions(+), 44 deletions(-) create mode 100644 build/deployments.json create mode 100644 script/deploy/DeploymentManager.sol create mode 100644 tests/XOGNForkTest.sol diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 73c7fa6c..4607776d 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -80,4 +80,20 @@ jobs: uses: foundry-rs/foundry-toolchain@v1 - name: Run tests - run: forge test -vvv + run: forge test --no-match-contract "(Fork)" -vvv + + foundry-fork-tests: + name: Foundry Fork tests + runs-on: ubuntu-latest + env: + PROVIDER_URL: ${{ secrets.PROVIDER_URL }} + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Run tests + run: forge test --match-contract "ForkTest" --fork-url $PROVIDER_URL diff --git a/.gitignore b/.gitignore index c9f2a5f0..f24ceb7a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ out/ .vscode brownie-deploy/ .idea +deployments-fork.json diff --git a/.gitmodules b/.gitmodules index fcce7e3b..302b7f0d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -9,7 +9,8 @@ url = https://github.com/openzeppelin/openzeppelin-contracts-upgradeable [submodule "lib/forge-std"] path = lib/forge-std - url = https://github.com/brockelmore/forge-std + url = https://github.com/foundry-rs/forge-std + branch = 978ac6fadb62f5f0b723c996f64be52eddba6801 [submodule "lib/prb-math"] path = lib/prb-math url = https://github.com/paulrberg/prb-math diff --git a/build/deployments.json b/build/deployments.json new file mode 100644 index 00000000..6a906e10 --- /dev/null +++ b/build/deployments.json @@ -0,0 +1,3 @@ +{ + "1": {} +} \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index b89cc459..4bc0442c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -9,3 +9,4 @@ remappings = [ "OpenZeppelin/openzeppelin-contracts-upgradeable@4.6.0/=./lib/openzeppelin-contracts-upgradeable", "paulrberg/prb-math@2.5.0/=./lib/prb-math" ] +fs_permissions = [{ access = "read-write", path = "./build"}] \ No newline at end of file diff --git a/script/deploy/DeploymentManager.sol b/script/deploy/DeploymentManager.sol new file mode 100644 index 00000000..7f956704 --- /dev/null +++ b/script/deploy/DeploymentManager.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.10; + +import "forge-std/Script.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.6.0/contracts/utils/Strings.sol"; + +import {OgnOgvMigrationScript} from "./mainnet/010_OgnOgvMigrationScript.sol"; + +contract DeploymentManager is Script { + mapping(string => address) public deployments; + + 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(); + } + + function getMainnetDeploymentFilePath() public view returns (string memory) { + return string(abi.encodePacked(vm.projectRoot(), "/build/deployments.json")); + } + + function getForkDeploymentFilePath() public view returns (string memory) { + return string(abi.encodePacked(vm.projectRoot(), "/build/deployments-fork.json")); + } + + function setUp() external { + string memory mainnetFilePath = getMainnetDeploymentFilePath(); + string memory chainIdStr = Strings.toString(block.chainid); + string memory chainIdKey = string(abi.encodePacked(".", chainIdStr)); + + if (!vm.isFile(mainnetFilePath)) { + // Create mainnet file if it doesn't exist + vm.writeFile(mainnetFilePath, string(abi.encodePacked('{ "', chainIdStr, '": {} }'))); + } else if (!vm.keyExistsJson(vm.readFile(mainnetFilePath), chainIdKey)) { + // Create network entry if it doesn't exist + vm.writeJson(vm.serializeJson(chainIdStr, "{}"), mainnetFilePath, chainIdKey); + } + + if (isForked()) { + // Duplicate Mainnet File + string memory mainnetDeployments = vm.readFile(mainnetFilePath); + vm.writeFile(getForkDeploymentFilePath(), mainnetDeployments); + } + } + + function run() external { + _runDeployFile(IDeployScript(address(new OgnOgvMigrationScript()))); + } + + function _runDeployFile(IDeployScript deployScript) internal { + // Run deployment + deployScript.setUp(); + deployScript.run(); + + // Save artifacts + string memory deploymentsFilePath = getDeploymentFilePath(); + string memory allDeployments = vm.readFile(deploymentsFilePath); + string memory chainIdStr = Strings.toString(block.chainid); + string memory chainIdKey = string(abi.encodePacked(".", chainIdStr)); + + IDeployScript.DeployRecord[] memory records = deployScript.getAllDeployRecords(); + + string memory networkDeployments = ""; + + string[] memory existingContracts = vm.parseJsonKeys(allDeployments, chainIdKey); + for (uint256 i = 0; i < existingContracts.length; ++i) { + address deployedAddr = + vm.parseJsonAddress(allDeployments, string(abi.encodePacked(chainIdKey, ".", existingContracts[i]))); + + networkDeployments = vm.serializeAddress(chainIdKey, existingContracts[i], deployedAddr); + + deployments[existingContracts[i]] = deployedAddr; + } + + 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(chainIdKey, name, addr); + deployments[name] = addr; + } + + vm.writeJson(networkDeployments, deploymentsFilePath, chainIdKey); + } + + function getDeployment(string calldata contractName) external view returns (address) { + return deployments[contractName]; + } +} + +interface IDeployScript { + // DeployerRecord stuff to be extracted as well + struct DeployRecord { + string name; + address addr; + } + + function setUp() external; + function run() external; + function getAllDeployRecords() external view returns (DeployRecord[] memory); +} diff --git a/script/deploy/mainnet/010_OgnOgvMigrationScript.sol b/script/deploy/mainnet/010_OgnOgvMigrationScript.sol index 780fa290..8b04276b 100644 --- a/script/deploy/mainnet/010_OgnOgvMigrationScript.sol +++ b/script/deploy/mainnet/010_OgnOgvMigrationScript.sol @@ -5,7 +5,6 @@ pragma solidity 0.8.10; import "./BaseMainnetScript.sol"; import {Vm} from "forge-std/Vm.sol"; - import {Addresses} from "contracts/utils/Addresses.sol"; import {FixedRateRewardsSourceProxy} from "contracts/upgrades/FixedRateRewardsSourceProxy.sol"; @@ -18,7 +17,6 @@ import {OgvStaking} from "contracts/OgvStaking.sol"; import {Migrator} from "contracts/Migrator.sol"; import {Timelock} from "contracts/Timelock.sol"; - // To be extracted library GovFive { struct GovFiveAction { @@ -26,6 +24,7 @@ library GovFive { string fullsig; bytes data; } + struct GovFiveProposal { string name; string description; @@ -40,58 +39,37 @@ library GovFive { prop.description = description; } - function action(GovFiveProposal storage prop, address receiver, string memory fullsig, bytes memory data) internal { - prop.actions.push(GovFiveAction({ - receiver:receiver, - fullsig: fullsig, - data: data - })); + function action(GovFiveProposal storage prop, address receiver, string memory fullsig, bytes memory data) + internal + { + prop.actions.push(GovFiveAction({receiver: receiver, fullsig: fullsig, data: data})); } function execute(GovFiveProposal storage prop) internal { address VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); Vm vm = Vm(VM_ADDRESS); - for(uint256 i = 0; i < prop.actions.length; i++ ){ + for (uint256 i = 0; i < prop.actions.length; i++) { GovFiveAction memory action = prop.actions[i]; bytes memory sig = abi.encodePacked(bytes4(keccak256(bytes(action.fullsig)))); vm.prank(Addresses.TIMELOCK); action.receiver.call(abi.encodePacked(sig, action.data)); } } - - } contract OgnOgvMigrationScript is BaseMainnetScript { - - // DeployerRecord stuff to be extracted as well - struct DeployRecord { - string name; - address addr; - } - DeployRecord[] public deploys; - function _recordDeploy(string memory name, address addr) internal { - deploys.push(DeployRecord({name: name, addr: addr})); - console.log(string(abi.encodePacked("> Deployed ", name, " at")), addr); - } - // End DeployRecord - - using GovFive for GovFive.GovFiveProposal; + GovFive.GovFiveProposal govFive; - FixedRateRewardsSourceProxy ognRewardsSourceProxy; OgvStaking veOgvImpl; - constructor() { // Will only execute script before this block number // deployBlockNum = ; } - - function _execute() internal override { console.log("Deploy:"); console.log("------------"); @@ -136,7 +114,8 @@ contract OgnOgvMigrationScript is BaseMainnetScript { // 3. veOGV implimentation contract to upgrade to uint256 ogvMinStaking = 30 * 24 * 60 * 60; // 2592000 -> 30 days uint256 ogvEpoch = OgvStaking(Addresses.VEOGV).epoch(); // Use old value. - veOgvImpl = new OgvStaking(Addresses.OGV, ogvEpoch, ogvMinStaking, Addresses.OGV_REWARDS_PROXY, address(migratorProxy)); + veOgvImpl = + new OgvStaking(Addresses.OGV, ogvEpoch, ogvMinStaking, Addresses.OGV_REWARDS_PROXY, address(migratorProxy)); _recordDeploy("VEOGV_IMPL", address(veOgvImpl)); // @@ -150,25 +129,35 @@ contract OgnOgvMigrationScript is BaseMainnetScript { function _fork() internal override { Timelock timelock = Timelock(payable(Addresses.TIMELOCK)); - + govFive.setName("OGV Migration to OGN"); // Todo: Fuller description govFive.setDescription("Deploy OGV-OGN migration contracts and revoke OGV Governance roles"); console.log(address(veOgvImpl)); - govFive.action(Addresses.VEOGV, 'upgradeTo(address)', abi.encode(address(veOgvImpl))); + govFive.action(Addresses.VEOGV, "upgradeTo(address)", abi.encode(address(veOgvImpl))); - govFive.action(Addresses.TIMELOCK, 'revokeRole(bytes32,address)', abi.encode(timelock.PROPOSER_ROLE(), Addresses.GOVERNOR_FIVE)); - govFive.action(Addresses.TIMELOCK, 'revokeRole(bytes32,address)', abi.encode(timelock.CANCELLER_ROLE(), Addresses.GOVERNOR_FIVE)); - govFive.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(address(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(address(ognRewardsSourceProxy))); + govFive.action( + Addresses.TIMELOCK, + "revokeRole(bytes32,address)", + abi.encode(timelock.PROPOSER_ROLE(), Addresses.GOVERNOR_FIVE) + ); + govFive.action( + Addresses.TIMELOCK, + "revokeRole(bytes32,address)", + abi.encode(timelock.CANCELLER_ROLE(), Addresses.GOVERNOR_FIVE) + ); + govFive.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(address(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(address(ognRewardsSourceProxy))); govFive.execute(); // One day lives up a level, and this contract returns a generic governance struct with a function pointers - } } diff --git a/script/deploy/mainnet/BaseMainnetScript.sol b/script/deploy/mainnet/BaseMainnetScript.sol index 76a1a77a..0770df01 100644 --- a/script/deploy/mainnet/BaseMainnetScript.sol +++ b/script/deploy/mainnet/BaseMainnetScript.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.10; import "forge-std/Script.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.6.0/contracts/utils/Strings.sol"; import {Addresses} from "contracts/utils/Addresses.sol"; @@ -10,6 +11,26 @@ abstract contract BaseMainnetScript is Script { uint256 public deployBlockNum = type(uint256).max; bool isForked = false; + // DeployerRecord stuff to be extracted as well + struct DeployRecord { + string name; + address addr; + } + + DeployRecord[] public deploys; + + function _recordDeploy(string memory name, address addr) internal { + deploys.push(DeployRecord({name: name, addr: addr})); + console.log(string(abi.encodePacked("> Deployed ", name, " at")), addr); + } + // End DeployRecord + + function getAllDeployRecords() external view returns (DeployRecord[] memory) { + return deploys; + } + + function setUp() external {} + function run() external { if (block.chainid != 1) { revert("Not Mainnet"); @@ -20,7 +41,9 @@ abstract contract BaseMainnetScript is Script { return; } - isForked = vm.envOr("IS_FORK", false); + isForked = vm.isContext(VmSafe.ForgeContext.ScriptDryRun) || vm.isContext(VmSafe.ForgeContext.Test) + || vm.isContext(VmSafe.ForgeContext.TestGroup); + if (isForked) { address impersonator = Addresses.INITIAL_DEPLOYER; console.log("Running script on mainnet fork impersonating: %s", impersonator); diff --git a/tests/XOGNForkTest.sol b/tests/XOGNForkTest.sol new file mode 100644 index 00000000..6a79947b --- /dev/null +++ b/tests/XOGNForkTest.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.10; + +import "forge-std/Test.sol"; +import {DeploymentManager} from "../script/deploy/DeploymentManager.sol"; +import {ExponentialStaking} from "../contracts/ExponentialStaking.sol"; + +contract XOGNForkTest is Test { + DeploymentManager public deploymentManager; + ExponentialStaking public xogn; + + function setUp() public { + deploymentManager = new DeploymentManager(); + + deploymentManager.setUp(); + deploymentManager.run(); + + xogn = ExponentialStaking(deploymentManager.getDeployment("XOGN")); + } + + function testStakingName() public { + assertEq(xogn.symbol(), "xOGN", "Incorrect symbol"); + } +} From aaae7d6a5b2374c8f45938410a85d4ce41e843dd Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 16 May 2024 22:14:21 +0400 Subject: [PATCH 02/12] Update tooling to record script executions --- build/deployments.json | 4 +- contracts/utils/Addresses.sol | 8 +- script/deploy/DeployManager.sol | 141 ++++++++++++++++++ script/deploy/DeploymentManager.sol | 105 ------------- script/deploy/mainnet/010_xOGNSetup.sol | 67 +++++++++ ...ript.sol => 011_OgnOgvMigrationScript.sol} | 54 ++----- script/deploy/mainnet/BaseMainnetScript.sol | 9 ++ 7 files changed, 233 insertions(+), 155 deletions(-) create mode 100644 script/deploy/DeployManager.sol delete mode 100644 script/deploy/DeploymentManager.sol create mode 100644 script/deploy/mainnet/010_xOGNSetup.sol rename script/deploy/mainnet/{010_OgnOgvMigrationScript.sol => 011_OgnOgvMigrationScript.sol} (68%) diff --git a/build/deployments.json b/build/deployments.json index 6a906e10..5eba78bf 100644 --- a/build/deployments.json +++ b/build/deployments.json @@ -1,3 +1 @@ -{ - "1": {} -} \ No newline at end of file +{ "1": { "executions": {}, "contracts": {} } } \ No newline at end of file diff --git a/contracts/utils/Addresses.sol b/contracts/utils/Addresses.sol index c2bdf149..8636e592 100644 --- a/contracts/utils/Addresses.sol +++ b/contracts/utils/Addresses.sol @@ -12,8 +12,8 @@ library Addresses { address public constant OGV_REWARDS_PROXY = 0x7d82E86CF1496f9485a8ea04012afeb3C7489397; address public constant VEOGV = 0x0C4576Ca1c365868E162554AF8e385dc3e7C66D9; - address public constant OUSD_BUYBACK = address(34); - address public constant OETH_BUYBACK = address(35); - address public constant OUSD_BUYBACK_IMPL = address(34); - address public constant OETH_BUYBACK_IMPL = address(35); + address public constant OUSD_BUYBACK = 0xD7B28d06365b85933c64E11e639EA0d3bC0e3BaB; + address public constant OETH_BUYBACK = 0xFD6c58850caCF9cCF6e8Aee479BFb4Df14a362D2; + address public constant OUSD_BUYBACK_IMPL = 0xbc77B8EFafabdF46f94Dfb4A422d541c5037799C; + address public constant OETH_BUYBACK_IMPL = 0x69D343A52bC13Dc19cBD0d2A77baC320CCB69B9a; } diff --git a/script/deploy/DeployManager.sol b/script/deploy/DeployManager.sol new file mode 100644 index 00000000..0027e571 --- /dev/null +++ b/script/deploy/DeployManager.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.10; + +import "forge-std/Script.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.6.0/contracts/utils/Strings.sol"; + +import {BaseMainnetScript} from "./mainnet/BaseMainnetScript.sol"; + +import {XOGNSetup} from "./mainnet/010_xOGNSetup.sol"; +import {OgnOgvMigrationScript} from "./mainnet/011_OgnOgvMigrationScript.sol"; + +contract DeployManager is Script { + mapping(string => address) public deployedContracts; + mapping(string => bool) public scriptsExecuted; + + 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(); + } + + function getMainnetDeploymentFilePath() public view returns (string memory) { + return string(abi.encodePacked(vm.projectRoot(), "/build/deployments.json")); + } + + function getForkDeploymentFilePath() public view returns (string memory) { + return string(abi.encodePacked(vm.projectRoot(), "/build/deployments-fork.json")); + } + + function setUp() external { + string memory chainIdStr = Strings.toString(block.chainid); + string memory chainIdKey = string(abi.encodePacked(".", chainIdStr)); + + string memory mainnetFilePath = getMainnetDeploymentFilePath(); + if (!vm.isFile(mainnetFilePath)) { + // Create mainnet deployment file if it doesn't exist + vm.writeFile( + mainnetFilePath, + string(abi.encodePacked('{ "', chainIdStr, '": { "executions": {}, "contracts": {} } }')) + ); + } else if (!vm.keyExistsJson(vm.readFile(mainnetFilePath), chainIdKey)) { + // Create network entry if it doesn't exist + vm.writeJson( + vm.serializeJson(chainIdStr, '{ "executions": {}, "contracts": {} }'), mainnetFilePath, chainIdKey + ); + } + + if (isForked()) { + // Duplicate Mainnet File + vm.writeFile(getForkDeploymentFilePath(), vm.readFile(mainnetFilePath)); + } + } + + function run() external { + // TODO: Use vm.readDir to recursively build this? + _runDeployFile(new XOGNSetup()); + _runDeployFile(new OgnOgvMigrationScript()); + } + + function _runDeployFile(BaseMainnetScript deployScript) internal { + string memory chainIdStr = Strings.toString(block.chainid); + string memory chainIdKey = string(abi.encodePacked(".", chainIdStr)); + + string memory contractsKey = string(abi.encodePacked(chainIdKey, ".contracts")); + string memory executionsKey = string(abi.encodePacked(chainIdKey, ".executions")); + + string memory deploymentsFilePath = getDeploymentFilePath(); + string memory fileContents = vm.readFile(deploymentsFilePath); + + /** + * Execution History + */ + string memory currentExecutions = ""; + string[] memory executionKeys = vm.parseJsonKeys(fileContents, executionsKey); + + for (uint256 i = 0; i < executionKeys.length; ++i) { + uint256 deployedTimestamp = + vm.parseJsonUint(fileContents, string(abi.encodePacked(executionsKey, ".", executionKeys[i]))); + + currentExecutions = vm.serializeUint(executionsKey, executionKeys[i], deployedTimestamp); + scriptsExecuted[executionKeys[i]] = true; + } + + if (scriptsExecuted[deployScript.DEPLOY_NAME()]) { + // TODO: Handle any active governance proposal + console.log("Skipping already deployed script"); + return; + } + + /** + * Pre-deployment + */ + BaseMainnetScript.DeployRecord[] memory deploys; + string memory networkDeployments = ""; + string[] memory existingContracts = vm.parseJsonKeys(fileContents, contractsKey); + for (uint256 i = 0; i < existingContracts.length; ++i) { + address deployedAddr = + vm.parseJsonAddress(fileContents, string(abi.encodePacked(contractsKey, ".", existingContracts[i]))); + + networkDeployments = vm.serializeAddress(contractsKey, existingContracts[i], deployedAddr); + + deployedContracts[existingContracts[i]] = deployedAddr; + + 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; + } + + vm.writeJson(networkDeployments, deploymentsFilePath, contractsKey); + + /** + * Write Execution History + */ + currentExecutions = vm.serializeUint(executionsKey, deployScript.DEPLOY_NAME(), block.timestamp); + + vm.writeJson(currentExecutions, deploymentsFilePath, executionsKey); + } + + function getDeployment(string calldata contractName) external view returns (address) { + return deployedContracts[contractName]; + } +} diff --git a/script/deploy/DeploymentManager.sol b/script/deploy/DeploymentManager.sol deleted file mode 100644 index 7f956704..00000000 --- a/script/deploy/DeploymentManager.sol +++ /dev/null @@ -1,105 +0,0 @@ -// SPDX-License-Identifier: Unlicense -pragma solidity 0.8.10; - -import "forge-std/Script.sol"; -import "OpenZeppelin/openzeppelin-contracts@4.6.0/contracts/utils/Strings.sol"; - -import {OgnOgvMigrationScript} from "./mainnet/010_OgnOgvMigrationScript.sol"; - -contract DeploymentManager is Script { - mapping(string => address) public deployments; - - 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(); - } - - function getMainnetDeploymentFilePath() public view returns (string memory) { - return string(abi.encodePacked(vm.projectRoot(), "/build/deployments.json")); - } - - function getForkDeploymentFilePath() public view returns (string memory) { - return string(abi.encodePacked(vm.projectRoot(), "/build/deployments-fork.json")); - } - - function setUp() external { - string memory mainnetFilePath = getMainnetDeploymentFilePath(); - string memory chainIdStr = Strings.toString(block.chainid); - string memory chainIdKey = string(abi.encodePacked(".", chainIdStr)); - - if (!vm.isFile(mainnetFilePath)) { - // Create mainnet file if it doesn't exist - vm.writeFile(mainnetFilePath, string(abi.encodePacked('{ "', chainIdStr, '": {} }'))); - } else if (!vm.keyExistsJson(vm.readFile(mainnetFilePath), chainIdKey)) { - // Create network entry if it doesn't exist - vm.writeJson(vm.serializeJson(chainIdStr, "{}"), mainnetFilePath, chainIdKey); - } - - if (isForked()) { - // Duplicate Mainnet File - string memory mainnetDeployments = vm.readFile(mainnetFilePath); - vm.writeFile(getForkDeploymentFilePath(), mainnetDeployments); - } - } - - function run() external { - _runDeployFile(IDeployScript(address(new OgnOgvMigrationScript()))); - } - - function _runDeployFile(IDeployScript deployScript) internal { - // Run deployment - deployScript.setUp(); - deployScript.run(); - - // Save artifacts - string memory deploymentsFilePath = getDeploymentFilePath(); - string memory allDeployments = vm.readFile(deploymentsFilePath); - string memory chainIdStr = Strings.toString(block.chainid); - string memory chainIdKey = string(abi.encodePacked(".", chainIdStr)); - - IDeployScript.DeployRecord[] memory records = deployScript.getAllDeployRecords(); - - string memory networkDeployments = ""; - - string[] memory existingContracts = vm.parseJsonKeys(allDeployments, chainIdKey); - for (uint256 i = 0; i < existingContracts.length; ++i) { - address deployedAddr = - vm.parseJsonAddress(allDeployments, string(abi.encodePacked(chainIdKey, ".", existingContracts[i]))); - - networkDeployments = vm.serializeAddress(chainIdKey, existingContracts[i], deployedAddr); - - deployments[existingContracts[i]] = deployedAddr; - } - - 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(chainIdKey, name, addr); - deployments[name] = addr; - } - - vm.writeJson(networkDeployments, deploymentsFilePath, chainIdKey); - } - - function getDeployment(string calldata contractName) external view returns (address) { - return deployments[contractName]; - } -} - -interface IDeployScript { - // DeployerRecord stuff to be extracted as well - struct DeployRecord { - string name; - address addr; - } - - function setUp() external; - function run() external; - function getAllDeployRecords() external view returns (DeployRecord[] memory); -} diff --git a/script/deploy/mainnet/010_xOGNSetup.sol b/script/deploy/mainnet/010_xOGNSetup.sol new file mode 100644 index 00000000..644a122b --- /dev/null +++ b/script/deploy/mainnet/010_xOGNSetup.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.10; + +import "./BaseMainnetScript.sol"; +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"; + +contract XOGNSetup is BaseMainnetScript { + FixedRateRewardsSourceProxy ognRewardsSourceProxy; + OgvStaking veOgvImpl; + + string public constant override DEPLOY_NAME = "010_xOGNSetup"; + + constructor() {} + + function _execute() internal override { + console.log("Deploy:"); + console.log("------------"); + + // 1. Deploy proxy contracts + // Since these contracts reference each other, we deploy all the proxies first + // so that the addresses are available in implimentation constructors. + FixedRateRewardsSourceProxy ognRewardsSourceProxy = new FixedRateRewardsSourceProxy(); + _recordDeploy("OGN_REWARDS_SOURCE", address(ognRewardsSourceProxy)); + + ExponentialStakingProxy xOgnProxy = new ExponentialStakingProxy(); + _recordDeploy("XOGN", address(xOgnProxy)); + + // + // 2. XOGN implimentation and init + uint256 ognEpoch = 1717041600; // May 30, 2024 GMT + uint256 ognMinStaking = 30 * 24 * 60 * 60; // 30 days + ExponentialStaking xognImpl = + new ExponentialStaking(Addresses.OGN, ognEpoch, ognMinStaking, address(ognRewardsSourceProxy)); + _recordDeploy("XOGN_IMPL", address(xognImpl)); + + console.log("- xOgnProxy init"); + xOgnProxy.initialize(address(xognImpl), Addresses.TIMELOCK, ""); + + // + // 2. Rewards implimentation and init + uint256 rewardsPerSecond = 0; //TODO: Decide on the params + FixedRateRewardsSource fixedRateRewardsSourceImpl = new FixedRateRewardsSource(Addresses.OGN); + _recordDeploy("OGN_REWARDS_SOURCE_IMPL", address(fixedRateRewardsSourceImpl)); + + console.log("- OGN rewards init"); + bytes memory implInitData = string.concat( + fixedRateRewardsSourceImpl.initialize.selector, + abi.encode(Addresses.STRATEGIST, address(xOgnProxy), rewardsPerSecond) + ); + ognRewardsSourceProxy.initialize(address(fixedRateRewardsSourceImpl), Addresses.TIMELOCK, implInitData); + } + + function _fork() internal override {} +} diff --git a/script/deploy/mainnet/010_OgnOgvMigrationScript.sol b/script/deploy/mainnet/011_OgnOgvMigrationScript.sol similarity index 68% rename from script/deploy/mainnet/010_OgnOgvMigrationScript.sol rename to script/deploy/mainnet/011_OgnOgvMigrationScript.sol index 8b04276b..687b3aff 100644 --- a/script/deploy/mainnet/010_OgnOgvMigrationScript.sol +++ b/script/deploy/mainnet/011_OgnOgvMigrationScript.sol @@ -62,65 +62,30 @@ contract OgnOgvMigrationScript is BaseMainnetScript { GovFive.GovFiveProposal govFive; - FixedRateRewardsSourceProxy ognRewardsSourceProxy; - OgvStaking veOgvImpl; + string public constant override DEPLOY_NAME = "011_OgnOgvMigrationScript"; - constructor() { - // Will only execute script before this block number - // deployBlockNum = ; - } + constructor() {} function _execute() internal override { console.log("Deploy:"); console.log("------------"); - // 1. Deploy proxy contracts - // Since these contracts reference each other, we deploy all the proxies first - // so that the addresses are available in implimentation constructors. - FixedRateRewardsSourceProxy ognRewardsSourceProxy = new FixedRateRewardsSourceProxy(); - _recordDeploy("OGN_REWARDS_SOURCE", address(ognRewardsSourceProxy)); - - ExponentialStakingProxy xOgnProxy = new ExponentialStakingProxy(); - _recordDeploy("XOGN", address(xOgnProxy)); + address xOgnProxy = deployedContracts["XOGN"]; MigratorProxy migratorProxy = new MigratorProxy(); _recordDeploy("MIGRATOR", address(migratorProxy)); - // - // 2. XOGN implimentation and init - uint256 ognEpoch = 1717041600; // May 30, 2024 GMT - uint256 ognMinStaking = 30 * 24 * 60 * 60; // 30 days - ExponentialStaking xognImpl = - new ExponentialStaking(Addresses.OGN, ognEpoch, ognMinStaking, address(ognRewardsSourceProxy)); - _recordDeploy("XOGN_IMPL", address(xognImpl)); - - console.log("- xOgnProxy init"); - xOgnProxy.initialize(address(xognImpl), Addresses.TIMELOCK, ""); - - // - // 2. Rewards implimentation and init - uint256 rewardsPerSecond = 0; //TODO: Decide on the params - FixedRateRewardsSource fixedRateRewardsSourceImpl = new FixedRateRewardsSource(Addresses.OGN); - _recordDeploy("OGN_REWARDS_SOURCE_IMPL", address(fixedRateRewardsSourceImpl)); - - console.log("- OGN rewards init"); - bytes memory implInitData = string.concat( - fixedRateRewardsSourceImpl.initialize.selector, - abi.encode(Addresses.STRATEGIST, address(xOgnProxy), rewardsPerSecond) - ); - ognRewardsSourceProxy.initialize(address(fixedRateRewardsSourceImpl), Addresses.TIMELOCK, implInitData); - // // 3. veOGV implimentation contract to upgrade to uint256 ogvMinStaking = 30 * 24 * 60 * 60; // 2592000 -> 30 days uint256 ogvEpoch = OgvStaking(Addresses.VEOGV).epoch(); // Use old value. - veOgvImpl = + OgvStaking veOgvImpl = new OgvStaking(Addresses.OGV, ogvEpoch, ogvMinStaking, Addresses.OGV_REWARDS_PROXY, address(migratorProxy)); _recordDeploy("VEOGV_IMPL", address(veOgvImpl)); // // 4. Migrator Contract - Migrator migratorImpl = new Migrator(Addresses.OGV, Addresses.OGN, Addresses.VEOGV, address(xOgnProxy)); + Migrator migratorImpl = new Migrator(Addresses.OGV, Addresses.OGN, Addresses.VEOGV, xOgnProxy); _recordDeploy("MIGRATOR_IMPL", address(migratorImpl)); console.log("- Migrator init"); @@ -130,12 +95,15 @@ contract OgnOgvMigrationScript is BaseMainnetScript { function _fork() 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"); console.log(address(veOgvImpl)); - govFive.action(Addresses.VEOGV, "upgradeTo(address)", abi.encode(address(veOgvImpl))); + govFive.action(Addresses.VEOGV, "upgradeTo(address)", abi.encode(veOgvImpl)); govFive.action( Addresses.TIMELOCK, @@ -154,9 +122,9 @@ contract OgnOgvMigrationScript is BaseMainnetScript { ); 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(address(ognRewardsSourceProxy))); + 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(address(ognRewardsSourceProxy))); + govFive.action(Addresses.OETH_BUYBACK, "setRewardsSource(address)", abi.encode(ognRewardsSourceProxy)); govFive.execute(); // One day lives up a level, and this contract returns a generic governance struct with a function pointers } diff --git a/script/deploy/mainnet/BaseMainnetScript.sol b/script/deploy/mainnet/BaseMainnetScript.sol index 0770df01..5effa75c 100644 --- a/script/deploy/mainnet/BaseMainnetScript.sol +++ b/script/deploy/mainnet/BaseMainnetScript.sol @@ -19,9 +19,12 @@ abstract contract BaseMainnetScript is Script { DeployRecord[] public deploys; + mapping(string => address) public deployedContracts; + function _recordDeploy(string memory name, address addr) internal { deploys.push(DeployRecord({name: name, addr: addr})); console.log(string(abi.encodePacked("> Deployed ", name, " at")), addr); + deployedContracts[name] = addr; } // End DeployRecord @@ -29,6 +32,10 @@ abstract contract BaseMainnetScript is Script { return deploys; } + function preloadDeployedContract(string memory name, address addr) external { + deployedContracts[name] = addr; + } + function setUp() external {} function run() external { @@ -65,6 +72,8 @@ abstract contract BaseMainnetScript is Script { } } + function DEPLOY_NAME() external view virtual returns (string memory); + function _execute() internal virtual {} function _fork() internal virtual {} From 28e29a754a2633876b04562c5c170e3c4211c44d Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 16 May 2024 23:41:54 +0400 Subject: [PATCH 03/12] Add a bit of tests --- build/deployments.json | 7 +- contracts/Governance.sol | 6 +- contracts/utils/Addresses.sol | 3 + script/deploy/DeployManager.sol | 6 +- ..._xOGNSetup.sol => 010_xOGNSetupScript.sol} | 6 +- .../mainnet/012_xOGNGovernanceScript.sol | 99 +++++++++++ tests/XOGNForkTest.sol | 161 +++++++++++++++++- tests/governance/test_initial_state.py | 2 +- 8 files changed, 271 insertions(+), 19 deletions(-) rename script/deploy/mainnet/{010_xOGNSetup.sol => 010_xOGNSetupScript.sol} (95%) create mode 100644 script/deploy/mainnet/012_xOGNGovernanceScript.sol diff --git a/build/deployments.json b/build/deployments.json index 5eba78bf..121ed2bd 100644 --- a/build/deployments.json +++ b/build/deployments.json @@ -1 +1,6 @@ -{ "1": { "executions": {}, "contracts": {} } } \ No newline at end of file +{ + "1": { + "executions": {}, + "contracts": {} + } +} \ No newline at end of file diff --git a/contracts/Governance.sol b/contracts/Governance.sol index b291542f..79251d6f 100644 --- a/contracts/Governance.sol +++ b/contracts/Governance.sol @@ -16,12 +16,12 @@ contract Governance is GovernorPreventLateQuorum { constructor(ERC20Votes _token, TimelockController _timelock) - Governor("OUSD Governance") - GovernorSettings(1, /* 1 block */ 17280, /* ~3 days (86400 / 15) * 3 */ 10000000 * 1e18 /* 10 mio veOgv */ ) + Governor("Origin DeFi Governance") + GovernorSettings(1, /* 1 block */ 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) - GovernorPreventLateQuorum(11520) // ~2 days (86400 / 15) * 2 + GovernorPreventLateQuorum(7208) // ~1 days (86400 / 12) {} // The following functions are overrides required by Solidity. diff --git a/contracts/utils/Addresses.sol b/contracts/utils/Addresses.sol index 8636e592..a6c27503 100644 --- a/contracts/utils/Addresses.sol +++ b/contracts/utils/Addresses.sol @@ -6,6 +6,9 @@ library Addresses { address public constant STRATEGIST = 0xF14BBdf064E3F67f51cd9BD646aE3716aD938FDC; address public constant GOVERNOR_FIVE = 0x3cdD07c16614059e66344a7b579DAB4f9516C0b6; + address public constant OGN_GOVERNOR = 0x72426BA137DEC62657306b12B1E869d43FeC6eC7; + address public constant GOV_MULTISIG = 0xbe2AB3d3d8F6a32b96414ebbd865dBD276d3d899; + address public constant INITIAL_DEPLOYER = address(0x1001); address public constant OGN = 0x8207c1FfC5B6804F6024322CcF34F29c3541Ae26; address public constant OGV = 0x9c354503C38481a7A7a51629142963F98eCC12D0; diff --git a/script/deploy/DeployManager.sol b/script/deploy/DeployManager.sol index 0027e571..29d92172 100644 --- a/script/deploy/DeployManager.sol +++ b/script/deploy/DeployManager.sol @@ -6,8 +6,9 @@ import "OpenZeppelin/openzeppelin-contracts@4.6.0/contracts/utils/Strings.sol"; import {BaseMainnetScript} from "./mainnet/BaseMainnetScript.sol"; -import {XOGNSetup} from "./mainnet/010_xOGNSetup.sol"; +import {XOGNSetupScript} from "./mainnet/010_xOGNSetupScript.sol"; import {OgnOgvMigrationScript} from "./mainnet/011_OgnOgvMigrationScript.sol"; +import {XOGNGovernanceScript} from "./mainnet/012_xOGNGovernanceScript.sol"; contract DeployManager is Script { mapping(string => address) public deployedContracts; @@ -56,8 +57,9 @@ contract DeployManager is Script { function run() external { // TODO: Use vm.readDir to recursively build this? - _runDeployFile(new XOGNSetup()); + _runDeployFile(new XOGNSetupScript()); _runDeployFile(new OgnOgvMigrationScript()); + _runDeployFile(new XOGNGovernanceScript()); } function _runDeployFile(BaseMainnetScript deployScript) internal { diff --git a/script/deploy/mainnet/010_xOGNSetup.sol b/script/deploy/mainnet/010_xOGNSetupScript.sol similarity index 95% rename from script/deploy/mainnet/010_xOGNSetup.sol rename to script/deploy/mainnet/010_xOGNSetupScript.sol index 644a122b..f0c7d9d4 100644 --- a/script/deploy/mainnet/010_xOGNSetup.sol +++ b/script/deploy/mainnet/010_xOGNSetupScript.sol @@ -17,7 +17,7 @@ import {OgvStaking} from "contracts/OgvStaking.sol"; import {Migrator} from "contracts/Migrator.sol"; import {Timelock} from "contracts/Timelock.sol"; -contract XOGNSetup is BaseMainnetScript { +contract XOGNSetupScript is BaseMainnetScript { FixedRateRewardsSourceProxy ognRewardsSourceProxy; OgvStaking veOgvImpl; @@ -50,7 +50,7 @@ contract XOGNSetup is BaseMainnetScript { xOgnProxy.initialize(address(xognImpl), Addresses.TIMELOCK, ""); // - // 2. Rewards implimentation and init + // 3. Rewards implimentation and init uint256 rewardsPerSecond = 0; //TODO: Decide on the params FixedRateRewardsSource fixedRateRewardsSourceImpl = new FixedRateRewardsSource(Addresses.OGN); _recordDeploy("OGN_REWARDS_SOURCE_IMPL", address(fixedRateRewardsSourceImpl)); @@ -62,6 +62,4 @@ contract XOGNSetup is BaseMainnetScript { ); ognRewardsSourceProxy.initialize(address(fixedRateRewardsSourceImpl), Addresses.TIMELOCK, implInitData); } - - function _fork() internal override {} } diff --git a/script/deploy/mainnet/012_xOGNGovernanceScript.sol b/script/deploy/mainnet/012_xOGNGovernanceScript.sol new file mode 100644 index 00000000..a7584263 --- /dev/null +++ b/script/deploy/mainnet/012_xOGNGovernanceScript.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.10; + +import "./BaseMainnetScript.sol"; +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 "OpenZeppelin/openzeppelin-contracts@4.6.0/contracts/token/ERC20/extensions/ERC20Votes.sol"; +import "OpenZeppelin/openzeppelin-contracts@4.6.0/contracts/governance/TimelockController.sol"; + +// To be extracted +library GovFive { + struct GovFiveAction { + address receiver; + string fullsig; + bytes data; + } + + struct GovFiveProposal { + string name; + string description; + GovFiveAction[] actions; + } + + function setName(GovFiveProposal storage prop, string memory name) internal { + prop.name = name; + } + + function setDescription(GovFiveProposal storage prop, string memory description) internal { + prop.description = description; + } + + function action(GovFiveProposal storage prop, address receiver, string memory fullsig, bytes memory data) + internal + { + prop.actions.push(GovFiveAction({receiver: receiver, fullsig: fullsig, data: data})); + } + + function execute(GovFiveProposal storage prop) internal { + address VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); + Vm vm = Vm(VM_ADDRESS); + for (uint256 i = 0; i < prop.actions.length; i++) { + GovFiveAction memory action = prop.actions[i]; + bytes memory sig = abi.encodePacked(bytes4(keccak256(bytes(action.fullsig)))); + vm.prank(Addresses.TIMELOCK); + action.receiver.call(abi.encodePacked(sig, action.data)); + } + } +} + +contract XOGNGovernanceScript is BaseMainnetScript { + using GovFive for GovFive.GovFiveProposal; + + GovFive.GovFiveProposal govFive; + + string public constant override DEPLOY_NAME = "012_xOGNGovernance"; + + constructor() {} + + function _execute() internal override { + console.log("Deploy:"); + console.log("------------"); + + address xOgnProxy = deployedContracts["XOGN"]; + + Governance governance = new Governance(ERC20Votes(xOgnProxy), TimelockController(payable(Addresses.TIMELOCK))); + + _recordDeploy("XOGN_GOV", address(governance)); + } + + function _fork() internal override { + Timelock timelock = Timelock(payable(Addresses.TIMELOCK)); + + address xognGov = deployedContracts["XOGN_GOV"]; + + govFive.setName("Enable OGN Governance"); + // Todo: Fuller description + govFive.setDescription("Grant roles on Timelock 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)); + + govFive.execute(); // One day lives up a level, and this contract returns a generic governance struct with a function pointers + } +} diff --git a/tests/XOGNForkTest.sol b/tests/XOGNForkTest.sol index 6a79947b..94ef38e5 100644 --- a/tests/XOGNForkTest.sol +++ b/tests/XOGNForkTest.sol @@ -2,23 +2,168 @@ pragma solidity 0.8.10; import "forge-std/Test.sol"; -import {DeploymentManager} from "../script/deploy/DeploymentManager.sol"; + +import {Addresses} from "contracts/utils/Addresses.sol"; +import {DeployManager} from "../script/deploy/DeployManager.sol"; + import {ExponentialStaking} from "../contracts/ExponentialStaking.sol"; +import {Timelock} from "contracts/Timelock.sol"; +import {Governance} from "contracts/Governance.sol"; + +// import "OpenZeppelin/openzeppelin-contracts@4.6.0/contracts/token/ERC20/IERC20.sol"; + +interface IMintableERC20 { + function mint(address to, uint256 amount) external; + function balanceOf(address owner) external view returns (uint256); + function transfer(address to, uint256 amount) external returns (bool); + function approve(address spender, uint256 allowance) external; +} contract XOGNForkTest is Test { - DeploymentManager public deploymentManager; + DeployManager public deployManager; ExponentialStaking public xogn; + Timelock public timelock; + Governance public xognGov; + + IMintableERC20 public ogn; + address public ognRewardsSource; + + address public constant OGN_GOVERNOR = 0x72426BA137DEC62657306b12B1E869d43FeC6eC7; + address public constant GOV_MULTISIG = 0xbe2AB3d3d8F6a32b96414ebbd865dBD276d3d899; + + address public alice = address(101); + address public bob = address(102); + + uint256 constant OGN_EPOCH = 1717041600; // May 30, 2024 GMT + + int256 constant NEW_STAKE = -1; + + function setUp() external { + deployManager = new DeployManager(); - function setUp() public { - deploymentManager = new DeploymentManager(); + deployManager.setUp(); + deployManager.run(); - deploymentManager.setUp(); - deploymentManager.run(); + xogn = ExponentialStaking(deployManager.getDeployment("XOGN")); + timelock = Timelock(payable(Addresses.TIMELOCK)); + xognGov = Governance(payable(deployManager.getDeployment("XOGN_GOV"))); - xogn = ExponentialStaking(deploymentManager.getDeployment("XOGN")); + ogn = IMintableERC20(Addresses.OGN); + + ognRewardsSource = deployManager.getDeployment("OGN_REWARDS_SOURCE"); + console.log(ognRewardsSource); + + vm.startPrank(OGN_GOVERNOR); + ogn.mint(alice, 200000 ether); + ogn.mint(bob, 200000 ether); + ogn.mint(ognRewardsSource, 200000 ether); + vm.stopPrank(); + + vm.startPrank(alice); + ogn.approve(address(xogn), 1e70); + vm.stopPrank(); + + vm.startPrank(bob); + ogn.approve(address(xogn), 1e70); + vm.stopPrank(); } - function testStakingName() public { + function testTokenName() external { assertEq(xogn.symbol(), "xOGN", "Incorrect symbol"); } + + function testStake() external { + vm.startPrank(alice); + (uint256 previewPoints, uint256 previewEnd) = xogn.previewPoints(1000 ether, 30 days); + + xogn.stake( + 1000 ether, // 1000 OGN + 30 days, + alice, + false, + NEW_STAKE + ); + + assertEq(xogn.lockupsCount(alice), 1, "Invalid lockup count"); + assertEq(ogn.balanceOf(alice), 199000 ether, "Incorrect OGN balance"); + + (uint128 lockupAmount, uint128 lockupEnd, uint256 lockupPoints) = xogn.lockups(alice, 0); + assertEq(lockupAmount, 1000 ether); + assertEq(lockupEnd, OGN_EPOCH + 30 days); + assertEq(lockupEnd, previewEnd); + assertEq(lockupPoints, previewPoints); + + assertEq(xogn.accRewardPerShare(), xogn.rewardDebtPerShare(alice)); + + vm.stopPrank(); + } + + function testUnstake() external { + vm.startPrank(alice); + + xogn.stake( + 1000 ether, // 1000 OGN + 30 days, + alice, + false, + NEW_STAKE + ); + + assertEq(xogn.lockupsCount(alice), 1, "Invalid lockup count"); + + vm.warp(OGN_EPOCH + 31 days); + xogn.unstake(0); + + assertEq(ogn.balanceOf(alice), 200000 ether, "Incorrect OGN balance"); + + (uint128 lockupAmount, uint128 lockupEnd, uint256 lockupPoints) = xogn.lockups(alice, 0); + assertEq(lockupAmount, 0 ether); + assertEq(lockupEnd, 0); + assertEq(lockupPoints, 0); + + assertEq(xogn.accRewardPerShare(), xogn.rewardDebtPerShare(alice)); + + vm.stopPrank(); + } + + /** + * Governance + */ + function testGovernanceName() external { + assertEq(xognGov.name(), "Origin DeFi Governance", "Incorrect symbol"); + } + + function testVotingDelay() external { + assertEq(xognGov.votingDelay(), 1, "Incorrect voting delay"); + } + + function testVotingPeriod() external { + assertEq(xognGov.votingPeriod(), 14416, "Incorrect voting period"); + } + + function testPermissions() external { + assertEq( + timelock.hasRole(keccak256("TIMELOCK_ADMIN_ROLE"), address(xognGov)), + false, + "Governance shouldn't have admin role on Timelock" + ); + + assertEq( + timelock.hasRole(keccak256("PROPOSER_ROLE"), address(xognGov)), + true, + "Governance doesn't have proposer role on Timelock" + ); + + assertEq( + timelock.hasRole(keccak256("EXECUTOR_ROLE"), address(xognGov)), + true, + "Governance doesn't have executor role on Timelock" + ); + + assertEq( + timelock.hasRole(keccak256("CANCELLER_ROLE"), address(xognGov)), + true, + "Governance doesn't have cancellor role on Timelock" + ); + } } diff --git a/tests/governance/test_initial_state.py b/tests/governance/test_initial_state.py index 703e8d5c..27d92c7d 100644 --- a/tests/governance/test_initial_state.py +++ b/tests/governance/test_initial_state.py @@ -16,7 +16,7 @@ def test_voting_delay(governance): def test_voting_period(governance): - assert governance.votingPeriod() == 17280 # ~3 days in blocks + assert governance.votingPeriod() == 14416 # ~2 days in blocks def test_quorum(governance, web3): assert governance.quorum(web3.eth.block_number - 1) == 0 From cc01b2813e4b13295cd89e42a37200b1ef53441c Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Fri, 17 May 2024 17:22:41 +0400 Subject: [PATCH 04/12] Fix brownie tests --- tests/governance/test_initial_state.py | 2 +- tests/governance/test_vote.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/governance/test_initial_state.py b/tests/governance/test_initial_state.py index 27d92c7d..a89c5805 100644 --- a/tests/governance/test_initial_state.py +++ b/tests/governance/test_initial_state.py @@ -4,7 +4,7 @@ def test_name(governance): - assert governance.name() == "OUSD Governance" + assert governance.name() == "Origin DeFi Governance" def test_counting_mode(governance): diff --git a/tests/governance/test_vote.py b/tests/governance/test_vote.py index c995aa88..29ef3f8d 100644 --- a/tests/governance/test_vote.py +++ b/tests/governance/test_vote.py @@ -140,12 +140,12 @@ def test_late_vote_extends_quorum( "Set voting delay", {"from": whale_voter}, ) - mine_blocks(web3, "0x4371") # 16 less than is required for vote end + mine_blocks(web3, "0x3840") # 16 less than is required for vote end assert governance.state(tx.return_value) == 1 governance.castVote(tx.return_value, 1, {"from": whale_voter}) proposal = governance.proposals(tx.return_value) - # Extends for 2 days beyond the current block - assert proposal[4] == (86400 / 15) * 2 + web3.eth.block_number + # Extends for 1 day beyond the current block + assert proposal[4] == 7208 + web3.eth.block_number def test_timelock_proposal_can_be_cancelled( From 516937b5eb97b6e40b2653e373d80eb43b47f872 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 19 May 2024 20:44:13 +0400 Subject: [PATCH 05/12] Add fork tests --- contracts/FixedRateRewardsSource.sol | 9 +- contracts/OgvStaking.sol | 2 +- contracts/interfaces/IMintableERC20.sol | 9 + contracts/tests/MockRewardsSource.sol | 2 +- contracts/tests/TestToken.sol | 2 +- contracts/utils/Addresses.sol | 4 +- contracts/utils/GovFive.sol | 52 ++++ foundry.toml | 2 + script/deploy/DeployManager.sol | 1 - script/deploy/mainnet/010_xOGNSetupScript.sol | 16 +- .../mainnet/011_OgnOgvMigrationScript.sol | 41 +-- .../mainnet/012_xOGNGovernanceScript.sol | 59 ++-- tests/governance/XOGNGovernanceForkTest.t.sol | 273 ++++++++++++++++++ tests/staking/OGNRewardsSourceForkTest.t.sol | 82 ++++++ .../XOGNStakingForkTest.t..sol} | 77 +---- 15 files changed, 474 insertions(+), 157 deletions(-) create mode 100644 contracts/interfaces/IMintableERC20.sol create mode 100644 contracts/utils/GovFive.sol create mode 100644 tests/governance/XOGNGovernanceForkTest.t.sol create mode 100644 tests/staking/OGNRewardsSourceForkTest.t.sol rename tests/{XOGNForkTest.sol => staking/XOGNStakingForkTest.t..sol} (57%) diff --git a/contracts/FixedRateRewardsSource.sol b/contracts/FixedRateRewardsSource.sol index 4ef9f431..2b251efa 100644 --- a/contracts/FixedRateRewardsSource.sol +++ b/contracts/FixedRateRewardsSource.sol @@ -90,10 +90,6 @@ contract FixedRateRewardsSource is Governable, Initializable { function previewRewards() public view returns (uint256 rewardAmount) { RewardConfig memory _config = rewardConfig; - if (_config.lastCollect == 0) { - return 0; - } - rewardAmount = (block.timestamp - _config.lastCollect) * _config.rewardsPerSecond; uint256 balance = IERC20(rewardToken).balanceOf(address(this)); if (rewardAmount > balance) { @@ -139,6 +135,11 @@ contract FixedRateRewardsSource is Governable, Initializable { // Update storage RewardConfig storage _config = rewardConfig; emit RewardsPerSecondChanged(_rewardsPerSecond, _config.rewardsPerSecond); + if (_config.rewardsPerSecond == 0) { + // When changing rate from zero to non-zero, + // Update lastCollect timestamp as well + _config.lastCollect = uint64(block.timestamp); + } _config.rewardsPerSecond = _rewardsPerSecond; } } diff --git a/contracts/OgvStaking.sol b/contracts/OgvStaking.sol index f467b5f9..85bd7289 100644 --- a/contracts/OgvStaking.sol +++ b/contracts/OgvStaking.sol @@ -228,7 +228,7 @@ contract OgvStaking is ERC20Votes { /// @param duration number of seconds to stake for /// @return points staking points that would be returned /// @return end staking period end date - function previewPoints(uint256 amount, uint256 duration) public view returns (uint256, uint256) { + function previewPoints(uint256 amount, uint256 duration) public pure returns (uint256, uint256) { revert StakingDisabled(); } diff --git a/contracts/interfaces/IMintableERC20.sol b/contracts/interfaces/IMintableERC20.sol new file mode 100644 index 00000000..9b3bd7a1 --- /dev/null +++ b/contracts/interfaces/IMintableERC20.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.10; + +interface IMintableERC20 { + function mint(address to, uint256 amount) external; + function balanceOf(address owner) external view returns (uint256); + function transfer(address to, uint256 amount) external returns (bool); + function approve(address spender, uint256 allowance) external; +} diff --git a/contracts/tests/MockRewardsSource.sol b/contracts/tests/MockRewardsSource.sol index 2c6b7f38..4c3f5bf8 100644 --- a/contracts/tests/MockRewardsSource.sol +++ b/contracts/tests/MockRewardsSource.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.10; contract MockRewardsSource { constructor() {} - function previewRewards() external view returns (uint256) { + function previewRewards() external pure returns (uint256) { return 0; } diff --git a/contracts/tests/TestToken.sol b/contracts/tests/TestToken.sol index 334c49fe..ea754e16 100644 --- a/contracts/tests/TestToken.sol +++ b/contracts/tests/TestToken.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.4; import "../GovernanceToken.sol"; contract TestToken is OriginDollarGovernance { - function proof() public { + function proof() public pure { revert("Upgraded"); } } diff --git a/contracts/utils/Addresses.sol b/contracts/utils/Addresses.sol index a6c27503..2513a9c1 100644 --- a/contracts/utils/Addresses.sol +++ b/contracts/utils/Addresses.sol @@ -17,6 +17,6 @@ library Addresses { address public constant OUSD_BUYBACK = 0xD7B28d06365b85933c64E11e639EA0d3bC0e3BaB; address public constant OETH_BUYBACK = 0xFD6c58850caCF9cCF6e8Aee479BFb4Df14a362D2; - address public constant OUSD_BUYBACK_IMPL = 0xbc77B8EFafabdF46f94Dfb4A422d541c5037799C; - address public constant OETH_BUYBACK_IMPL = 0x69D343A52bC13Dc19cBD0d2A77baC320CCB69B9a; + address public constant OUSD_BUYBACK_IMPL = 0x386d8fEC5b6d5B5E36a48A376644e36239dB65d6; + address public constant OETH_BUYBACK_IMPL = 0x4F11d31f781B57051764a3823b24d520626b4833; } diff --git a/contracts/utils/GovFive.sol b/contracts/utils/GovFive.sol new file mode 100644 index 00000000..8bbedb60 --- /dev/null +++ b/contracts/utils/GovFive.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.10; + +import {Vm} from "forge-std/Vm.sol"; +import {Addresses} from "contracts/utils/Addresses.sol"; +import "forge-std/console.sol"; + +import "OpenZeppelin/openzeppelin-contracts@4.6.0/contracts/utils/Strings.sol"; + +library GovFive { + struct GovFiveAction { + address receiver; + string fullsig; + bytes data; + } + + struct GovFiveProposal { + string name; + string description; + GovFiveAction[] actions; + } + + function setName(GovFiveProposal storage prop, string memory name) internal { + prop.name = name; + } + + function setDescription(GovFiveProposal storage prop, string memory description) internal { + prop.description = description; + } + + function action(GovFiveProposal storage prop, address receiver, string memory fullsig, bytes memory data) + internal + { + prop.actions.push(GovFiveAction({receiver: receiver, fullsig: fullsig, data: data})); + } + + function execute(GovFiveProposal storage prop) internal { + address VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); + Vm vm = Vm(VM_ADDRESS); + for (uint256 i = 0; i < prop.actions.length; i++) { + GovFiveAction memory propAction = prop.actions[i]; + bytes memory sig = abi.encodePacked(bytes4(keccak256(bytes(propAction.fullsig)))); + vm.prank(Addresses.TIMELOCK); + (bool success, bytes memory data) = propAction.receiver.call(abi.encodePacked(sig, propAction.data)); + if (!success) { + console.log(propAction.fullsig); + revert("Multisig action failed"); + } + } + } +} diff --git a/foundry.toml b/foundry.toml index 4bc0442c..ae1848cb 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,6 +3,8 @@ src = 'contracts' test = 'tests' remappings = [ "contracts/=./contracts", + "script/=./script", + "tests/=./tests", "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/deploy/DeployManager.sol b/script/deploy/DeployManager.sol index 29d92172..1f2a5c28 100644 --- a/script/deploy/DeployManager.sol +++ b/script/deploy/DeployManager.sol @@ -95,7 +95,6 @@ contract DeployManager is Script { /** * Pre-deployment */ - BaseMainnetScript.DeployRecord[] memory deploys; string memory networkDeployments = ""; string[] memory existingContracts = vm.parseJsonKeys(fileContents, contractsKey); for (uint256 i = 0; i < existingContracts.length; ++i) { diff --git a/script/deploy/mainnet/010_xOGNSetupScript.sol b/script/deploy/mainnet/010_xOGNSetupScript.sol index f0c7d9d4..78f084e9 100644 --- a/script/deploy/mainnet/010_xOGNSetupScript.sol +++ b/script/deploy/mainnet/010_xOGNSetupScript.sol @@ -17,10 +17,9 @@ import {OgvStaking} from "contracts/OgvStaking.sol"; import {Migrator} from "contracts/Migrator.sol"; import {Timelock} from "contracts/Timelock.sol"; -contract XOGNSetupScript is BaseMainnetScript { - FixedRateRewardsSourceProxy ognRewardsSourceProxy; - OgvStaking veOgvImpl; +import {IMintableERC20} from "contracts/interfaces/IMintableERC20.sol"; +contract XOGNSetupScript is BaseMainnetScript { string public constant override DEPLOY_NAME = "010_xOGNSetup"; constructor() {} @@ -51,7 +50,8 @@ contract XOGNSetupScript is BaseMainnetScript { // // 3. Rewards implimentation and init - uint256 rewardsPerSecond = 0; //TODO: Decide on the params + // Reward rate is 0, Will be set later when we want initiate rewards + uint256 rewardsPerSecond = 0; FixedRateRewardsSource fixedRateRewardsSourceImpl = new FixedRateRewardsSource(Addresses.OGN); _recordDeploy("OGN_REWARDS_SOURCE_IMPL", address(fixedRateRewardsSourceImpl)); @@ -62,4 +62,12 @@ contract XOGNSetupScript is BaseMainnetScript { ); ognRewardsSourceProxy.initialize(address(fixedRateRewardsSourceImpl), Addresses.TIMELOCK, implInitData); } + + 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); + } } diff --git a/script/deploy/mainnet/011_OgnOgvMigrationScript.sol b/script/deploy/mainnet/011_OgnOgvMigrationScript.sol index 687b3aff..6d3aef6b 100644 --- a/script/deploy/mainnet/011_OgnOgvMigrationScript.sol +++ b/script/deploy/mainnet/011_OgnOgvMigrationScript.sol @@ -16,46 +16,7 @@ import {FixedRateRewardsSource} from "contracts/FixedRateRewardsSource.sol"; import {OgvStaking} from "contracts/OgvStaking.sol"; import {Migrator} from "contracts/Migrator.sol"; import {Timelock} from "contracts/Timelock.sol"; - -// To be extracted -library GovFive { - struct GovFiveAction { - address receiver; - string fullsig; - bytes data; - } - - struct GovFiveProposal { - string name; - string description; - GovFiveAction[] actions; - } - - function setName(GovFiveProposal storage prop, string memory name) internal { - prop.name = name; - } - - function setDescription(GovFiveProposal storage prop, string memory description) internal { - prop.description = description; - } - - function action(GovFiveProposal storage prop, address receiver, string memory fullsig, bytes memory data) - internal - { - prop.actions.push(GovFiveAction({receiver: receiver, fullsig: fullsig, data: data})); - } - - function execute(GovFiveProposal storage prop) internal { - address VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); - Vm vm = Vm(VM_ADDRESS); - for (uint256 i = 0; i < prop.actions.length; i++) { - GovFiveAction memory action = prop.actions[i]; - bytes memory sig = abi.encodePacked(bytes4(keccak256(bytes(action.fullsig)))); - vm.prank(Addresses.TIMELOCK); - action.receiver.call(abi.encodePacked(sig, action.data)); - } - } -} +import {GovFive} from "contracts/utils/GovFive.sol"; contract OgnOgvMigrationScript is BaseMainnetScript { using GovFive for GovFive.GovFiveProposal; diff --git a/script/deploy/mainnet/012_xOGNGovernanceScript.sol b/script/deploy/mainnet/012_xOGNGovernanceScript.sol index a7584263..2e7b41de 100644 --- a/script/deploy/mainnet/012_xOGNGovernanceScript.sol +++ b/script/deploy/mainnet/012_xOGNGovernanceScript.sol @@ -18,49 +18,11 @@ 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 "OpenZeppelin/openzeppelin-contracts@4.6.0/contracts/token/ERC20/extensions/ERC20Votes.sol"; import "OpenZeppelin/openzeppelin-contracts@4.6.0/contracts/governance/TimelockController.sol"; -// To be extracted -library GovFive { - struct GovFiveAction { - address receiver; - string fullsig; - bytes data; - } - - struct GovFiveProposal { - string name; - string description; - GovFiveAction[] actions; - } - - function setName(GovFiveProposal storage prop, string memory name) internal { - prop.name = name; - } - - function setDescription(GovFiveProposal storage prop, string memory description) internal { - prop.description = description; - } - - function action(GovFiveProposal storage prop, address receiver, string memory fullsig, bytes memory data) - internal - { - prop.actions.push(GovFiveAction({receiver: receiver, fullsig: fullsig, data: data})); - } - - function execute(GovFiveProposal storage prop) internal { - address VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); - Vm vm = Vm(VM_ADDRESS); - for (uint256 i = 0; i < prop.actions.length; i++) { - GovFiveAction memory action = prop.actions[i]; - bytes memory sig = abi.encodePacked(bytes4(keccak256(bytes(action.fullsig)))); - vm.prank(Addresses.TIMELOCK); - action.receiver.call(abi.encodePacked(sig, action.data)); - } - } -} - contract XOGNGovernanceScript is BaseMainnetScript { using GovFive for GovFive.GovFiveProposal; @@ -68,6 +30,9 @@ contract XOGNGovernanceScript is BaseMainnetScript { 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 + constructor() {} function _execute() internal override { @@ -86,14 +51,24 @@ contract XOGNGovernanceScript is BaseMainnetScript { address xognGov = deployedContracts["XOGN_GOV"]; - govFive.setName("Enable OGN Governance"); - // Todo: Fuller description + govFive.setName("Enable OGN Governance & Begin Rewards"); + govFive.setDescription("Grant roles on Timelock 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)); + // Enable rewards + govFive.action( + deployedContracts["OGN_REWARDS_SOURCE"], + "setRewardsPerSecond(uint192)", + abi.encode(uint192(REWARDS_PER_SECOND)) + ); + + // Go to the start of everything + vm.warp(OGN_EPOCH); + govFive.execute(); // One day lives up a level, and this contract returns a generic governance struct with a function pointers } } diff --git a/tests/governance/XOGNGovernanceForkTest.t.sol b/tests/governance/XOGNGovernanceForkTest.t.sol new file mode 100644 index 00000000..5d4b43cf --- /dev/null +++ b/tests/governance/XOGNGovernanceForkTest.t.sol @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.10; + +import "forge-std/Test.sol"; + +import {Addresses} from "contracts/utils/Addresses.sol"; +import {DeployManager} from "script/deploy/DeployManager.sol"; + +import {ExponentialStaking} from "contracts/ExponentialStaking.sol"; +import {Timelock} from "contracts/Timelock.sol"; +import {Governance} from "contracts/Governance.sol"; + +import {IMintableERC20} from "contracts/interfaces/IMintableERC20.sol"; +import {FixedRateRewardsSource} from "contracts/FixedRateRewardsSource.sol"; + +contract XOGNGovernanceForkTest is Test { + DeployManager public deployManager; + ExponentialStaking public xogn; + Timelock public timelock; + Governance public xognGov; + + IMintableERC20 public ogn; + address public ognRewardsSource; + + address public alice = address(101); + address public bob = address(102); + address public xognWhale = address(103); + + uint256 constant OGN_EPOCH = 1717041600; // May 30, 2024 GMT + + uint256 constant REWARDS_PER_SECOND = 300000 ether / uint256(24 * 60 * 60); // 300k per day + + int256 constant NEW_STAKE = -1; + + function setUp() external { + deployManager = new DeployManager(); + + deployManager.setUp(); + deployManager.run(); + + xogn = ExponentialStaking(deployManager.getDeployment("XOGN")); + timelock = Timelock(payable(Addresses.TIMELOCK)); + xognGov = Governance(payable(deployManager.getDeployment("XOGN_GOV"))); + + ogn = IMintableERC20(Addresses.OGN); + + ognRewardsSource = deployManager.getDeployment("OGN_REWARDS_SOURCE"); + + vm.startPrank(Addresses.OGN_GOVERNOR); + ogn.mint(alice, 200000 ether); + ogn.mint(bob, 200000 ether); + ogn.mint(xognWhale, 1000_000_000 ether); + vm.stopPrank(); + + vm.startPrank(alice); + ogn.approve(address(xogn), 1e70); + vm.stopPrank(); + + vm.startPrank(bob); + ogn.approve(address(xogn), 1e70); + vm.stopPrank(); + } + + function testGovernanceName() external view { + assertEq(xognGov.name(), "Origin DeFi Governance", "Incorrect symbol"); + } + + function testVotingDelay() external view { + assertEq(xognGov.votingDelay(), 1, "Incorrect voting delay"); + } + + function testVotingPeriod() external view { + assertEq(xognGov.votingPeriod(), 14416, "Incorrect voting period"); + } + + function testProposalThreshold() external view { + assertEq(xognGov.proposalThreshold(), 100000 ether, "Incorrect voting period"); + } + + function testPermissions() external view { + assertEq( + timelock.hasRole(keccak256("TIMELOCK_ADMIN_ROLE"), address(xognGov)), + false, + "Governance shouldn't have admin role on Timelock" + ); + + assertEq( + timelock.hasRole(keccak256("PROPOSER_ROLE"), address(xognGov)), + true, + "Governance doesn't have proposer role on Timelock" + ); + + assertEq( + timelock.hasRole(keccak256("EXECUTOR_ROLE"), address(xognGov)), + true, + "Governance doesn't have executor role on Timelock" + ); + + assertEq( + timelock.hasRole(keccak256("CANCELLER_ROLE"), address(xognGov)), + true, + "Governance doesn't have cancellor role on Timelock" + ); + } + + function testCreateProposal() external { + vm.startPrank(alice); + + xogn.stake( + 100000 ether, // 100k OGN + 30 days, + alice, + false, + NEW_STAKE + ); + vm.roll(block.number + 1); + + assertEq(xogn.lockupsCount(alice), 1, "Invalid lockup count"); + + address[] memory targets = new address[](1); + targets[0] = address(1234); + uint256[] memory values = new uint256[](1); + bytes[] memory calldatas = new bytes[](1); + calldatas[0] = abi.encodePacked(bytes("Hello world")); + + // Test create proposal + uint256 proposalId = xognGov.propose(targets, values, calldatas, ""); + + assertEq(uint256(xognGov.state(proposalId)), uint256(0), "Proposal not created"); + (address[] memory targets2, uint256[] memory values2, string[] memory signatures, bytes[] memory calldatas2) = + xognGov.getActions(proposalId); + + assertEq(targets2.length, 1, "Invalid targets count"); + assertEq(targets2[0], address(1234), "Invalid targets"); + + assertEq(values2.length, 1, "Invalid values count"); + assertEq(values2[0], 0, "Invalid values"); + + assertEq(calldatas2.length, 1, "Invalid calldata count"); + assertEq(calldatas2[0], abi.encodePacked(bytes("Hello world")), "Invalid calldata"); + + assertEq(signatures.length, 1, "Invalid signatures count"); + assertEq(signatures[0], "", "Invalid signatures"); + + vm.stopPrank(); + } + + function testRevertOnProposalThreshold() external { + vm.startPrank(bob); + + xogn.stake( + 10000 ether, // 10k OGN + 30 days, + bob, + false, + NEW_STAKE + ); + vm.roll(block.number + 1); + + assertEq(xogn.lockupsCount(bob), 1, "Invalid lockup count"); + + address[] memory targets = new address[](1); + targets[0] = address(1234); + uint256[] memory values = new uint256[](1); + bytes[] memory calldatas = new bytes[](1); + calldatas[0] = abi.encodePacked(bytes("Hello world")); + + // Test create proposal + vm.expectRevert("GovernorCompatibilityBravo: proposer votes below proposal threshold"); + xognGov.propose(targets, values, calldatas, ""); + + vm.stopPrank(); + } + + function testFullProposalFlow() external { + vm.startPrank(xognWhale); + // xOGN Whale + ogn.approve(address(xogn), 1e70); + xogn.stake( + 1000_000_000 ether, // 1B OGN + 365 days, + xognWhale, + false, + NEW_STAKE + ); + vm.roll(block.number + 1); + + // Test grantRole to address(1010) through governance proposal + address[] memory targets = new address[](1); + targets[0] = Addresses.TIMELOCK; + uint256[] memory values = new uint256[](1); + bytes[] memory calldatas = new bytes[](1); + calldatas[0] = abi.encodePacked( + bytes4(keccak256("grantRole(bytes32,address)")), abi.encode(keccak256("PROPOSER_ROLE"), address(1010)) + ); + + // Create proposal + uint256 proposalId = xognGov.propose(targets, values, calldatas, ""); + 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); + assertEq(uint256(xognGov.state(proposalId)), 1, "Proposal isn't active"); + + // Vote on proposal + xognGov.castVote(proposalId, 1); + + // Wait for quorum + vm.warp(block.timestamp + 2 days); + vm.roll(block.number + 2 days); + assertEq(uint256(xognGov.state(proposalId)), 4, "Proposal didn't succeed"); + + // Queue proposal + xognGov.queue(proposalId); + assertEq(uint256(xognGov.state(proposalId)), 5, "Proposal isn't queued"); + + // Wait for timelock + vm.warp(block.timestamp + 2 days); + vm.roll(block.number + 2 days); + + // Execute proposal + xognGov.execute(proposalId); + assertEq(uint256(xognGov.state(proposalId)), 7, "Proposal isn't executed"); + + // Check state + assertEq(timelock.hasRole(keccak256("PROPOSER_ROLE"), address(1010)), true, "Permission not granted"); + + vm.stopPrank(); + } + + function testProposalDefeat() external { + vm.startPrank(xognWhale); + // xOGN Whale + ogn.approve(address(xogn), 1e70); + xogn.stake( + 1000_000_000 ether, // 1B OGN + 365 days, + xognWhale, + false, + NEW_STAKE + ); + vm.roll(block.number + 1); + + // Test grantRole to address(1010) through governance proposal + address[] memory targets = new address[](1); + targets[0] = Addresses.TIMELOCK; + uint256[] memory values = new uint256[](1); + bytes[] memory calldatas = new bytes[](1); + calldatas[0] = abi.encodePacked( + bytes4(keccak256("grantRole(bytes32,address)")), abi.encode(keccak256("PROPOSER_ROLE"), address(1010)) + ); + + // Create proposal + uint256 proposalId = xognGov.propose(targets, values, calldatas, ""); + 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); + assertEq(uint256(xognGov.state(proposalId)), 1, "Proposal isn't active"); + + // Vote on proposal + xognGov.castVote(proposalId, 0); + + // Wait for quorum + vm.warp(block.timestamp + 2 days); + vm.roll(block.number + 2 days); + assertEq(uint256(xognGov.state(proposalId)), 3, "Proposal wasn't defeated"); + + vm.stopPrank(); + } +} diff --git a/tests/staking/OGNRewardsSourceForkTest.t.sol b/tests/staking/OGNRewardsSourceForkTest.t.sol new file mode 100644 index 00000000..f066f340 --- /dev/null +++ b/tests/staking/OGNRewardsSourceForkTest.t.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.10; + +import "forge-std/Test.sol"; + +import {Addresses} from "contracts/utils/Addresses.sol"; +import {DeployManager} from "script/deploy/DeployManager.sol"; + +import {ExponentialStaking} from "contracts/ExponentialStaking.sol"; +import {Timelock} from "contracts/Timelock.sol"; +import {Governance} from "contracts/Governance.sol"; + +import {IMintableERC20} from "contracts/interfaces/IMintableERC20.sol"; +import {FixedRateRewardsSource} from "contracts/FixedRateRewardsSource.sol"; + +contract OGNRewardsSourceForkTest is Test { + DeployManager public deployManager; + ExponentialStaking public xogn; + Timelock public timelock; + Governance public xognGov; + + IMintableERC20 public ogn; + FixedRateRewardsSource public ognRewardsSource; + + address public alice = address(101); + address public bob = address(102); + + uint256 constant OGN_EPOCH = 1717041600; // May 30, 2024 GMT + + uint256 constant REWARDS_PER_SECOND = 300000 ether / uint256(24 * 60 * 60); // 300k per day + + int256 constant NEW_STAKE = -1; + + function setUp() external { + deployManager = new DeployManager(); + + deployManager.setUp(); + deployManager.run(); + + xogn = ExponentialStaking(deployManager.getDeployment("XOGN")); + timelock = Timelock(payable(Addresses.TIMELOCK)); + xognGov = Governance(payable(deployManager.getDeployment("XOGN_GOV"))); + + ogn = IMintableERC20(Addresses.OGN); + + ognRewardsSource = FixedRateRewardsSource(deployManager.getDeployment("OGN_REWARDS_SOURCE")); + + vm.startPrank(Addresses.OGN_GOVERNOR); + ogn.mint(alice, 200000 ether); + ogn.mint(bob, 200000 ether); + vm.stopPrank(); + + vm.startPrank(alice); + ogn.approve(address(xogn), 1e70); + vm.stopPrank(); + + vm.startPrank(bob); + ogn.approve(address(xogn), 1e70); + vm.stopPrank(); + } + + function testRewardRate() external view { + (, uint192 rewardsPerSecond) = ognRewardsSource.rewardConfig(); + assertEq(rewardsPerSecond, REWARDS_PER_SECOND, "Invalid reward rate"); + } + + 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( + rewardsBefore + ognRewardsSource.previewRewards(), + uint256(REWARDS_PER_SECOND * 60 * 60 * 24), + "Invalid reward after 1d" + ); + } +} diff --git a/tests/XOGNForkTest.sol b/tests/staking/XOGNStakingForkTest.t..sol similarity index 57% rename from tests/XOGNForkTest.sol rename to tests/staking/XOGNStakingForkTest.t..sol index 94ef38e5..f9d84e7a 100644 --- a/tests/XOGNForkTest.sol +++ b/tests/staking/XOGNStakingForkTest.t..sol @@ -4,22 +4,16 @@ pragma solidity 0.8.10; import "forge-std/Test.sol"; import {Addresses} from "contracts/utils/Addresses.sol"; -import {DeployManager} from "../script/deploy/DeployManager.sol"; +import {DeployManager} from "script/deploy/DeployManager.sol"; -import {ExponentialStaking} from "../contracts/ExponentialStaking.sol"; +import {ExponentialStaking} from "contracts/ExponentialStaking.sol"; import {Timelock} from "contracts/Timelock.sol"; import {Governance} from "contracts/Governance.sol"; -// import "OpenZeppelin/openzeppelin-contracts@4.6.0/contracts/token/ERC20/IERC20.sol"; +import {IMintableERC20} from "contracts/interfaces/IMintableERC20.sol"; +import {FixedRateRewardsSource} from "contracts/FixedRateRewardsSource.sol"; -interface IMintableERC20 { - function mint(address to, uint256 amount) external; - function balanceOf(address owner) external view returns (uint256); - function transfer(address to, uint256 amount) external returns (bool); - function approve(address spender, uint256 allowance) external; -} - -contract XOGNForkTest is Test { +contract XOGNStakingForkTest is Test { DeployManager public deployManager; ExponentialStaking public xogn; Timelock public timelock; @@ -28,14 +22,14 @@ contract XOGNForkTest is Test { IMintableERC20 public ogn; address public ognRewardsSource; - address public constant OGN_GOVERNOR = 0x72426BA137DEC62657306b12B1E869d43FeC6eC7; - address public constant GOV_MULTISIG = 0xbe2AB3d3d8F6a32b96414ebbd865dBD276d3d899; - address public alice = address(101); address public bob = address(102); + address public xognWhale = address(103); uint256 constant OGN_EPOCH = 1717041600; // May 30, 2024 GMT + uint256 constant REWARDS_PER_SECOND = 300000 ether / uint256(24 * 60 * 60); // 300k per day + int256 constant NEW_STAKE = -1; function setUp() external { @@ -51,12 +45,11 @@ contract XOGNForkTest is Test { ogn = IMintableERC20(Addresses.OGN); ognRewardsSource = deployManager.getDeployment("OGN_REWARDS_SOURCE"); - console.log(ognRewardsSource); - vm.startPrank(OGN_GOVERNOR); + vm.startPrank(Addresses.OGN_GOVERNOR); ogn.mint(alice, 200000 ether); ogn.mint(bob, 200000 ether); - ogn.mint(ognRewardsSource, 200000 ether); + ogn.mint(xognWhale, 1000_000_000 ether); vm.stopPrank(); vm.startPrank(alice); @@ -68,7 +61,7 @@ contract XOGNForkTest is Test { vm.stopPrank(); } - function testTokenName() external { + function testTokenName() external view { assertEq(xogn.symbol(), "xOGN", "Incorrect symbol"); } @@ -101,8 +94,10 @@ contract XOGNForkTest is Test { function testUnstake() external { vm.startPrank(alice); + console.log(FixedRateRewardsSource(ognRewardsSource).previewRewards()); + xogn.stake( - 1000 ether, // 1000 OGN + 1000 ether, // 1k OGN 30 days, alice, false, @@ -112,9 +107,10 @@ contract XOGNForkTest is Test { assertEq(xogn.lockupsCount(alice), 1, "Invalid lockup count"); vm.warp(OGN_EPOCH + 31 days); + uint256 netRewards = xogn.previewRewards(alice); xogn.unstake(0); - assertEq(ogn.balanceOf(alice), 200000 ether, "Incorrect OGN balance"); + assertEq(ogn.balanceOf(alice), netRewards + 200000 ether, "Incorrect OGN balance"); (uint128 lockupAmount, uint128 lockupEnd, uint256 lockupPoints) = xogn.lockups(alice, 0); assertEq(lockupAmount, 0 ether); @@ -125,45 +121,4 @@ contract XOGNForkTest is Test { vm.stopPrank(); } - - /** - * Governance - */ - function testGovernanceName() external { - assertEq(xognGov.name(), "Origin DeFi Governance", "Incorrect symbol"); - } - - function testVotingDelay() external { - assertEq(xognGov.votingDelay(), 1, "Incorrect voting delay"); - } - - function testVotingPeriod() external { - assertEq(xognGov.votingPeriod(), 14416, "Incorrect voting period"); - } - - function testPermissions() external { - assertEq( - timelock.hasRole(keccak256("TIMELOCK_ADMIN_ROLE"), address(xognGov)), - false, - "Governance shouldn't have admin role on Timelock" - ); - - assertEq( - timelock.hasRole(keccak256("PROPOSER_ROLE"), address(xognGov)), - true, - "Governance doesn't have proposer role on Timelock" - ); - - assertEq( - timelock.hasRole(keccak256("EXECUTOR_ROLE"), address(xognGov)), - true, - "Governance doesn't have executor role on Timelock" - ); - - assertEq( - timelock.hasRole(keccak256("CANCELLER_ROLE"), address(xognGov)), - true, - "Governance doesn't have cancellor role on Timelock" - ); - } } From 383f2dfa9f6d249b86904bf569d08cfeaa916da2 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 19 May 2024 21:02:38 +0400 Subject: [PATCH 06/12] Print safe tx data --- contracts/utils/GovFive.sol | 15 +++++++++++++++ .../deploy/mainnet/011_OgnOgvMigrationScript.sol | 15 +++++++++++---- .../deploy/mainnet/012_xOGNGovernanceScript.sol | 13 ++++++++++--- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/contracts/utils/GovFive.sol b/contracts/utils/GovFive.sol index 8bbedb60..efcbdf22 100644 --- a/contracts/utils/GovFive.sol +++ b/contracts/utils/GovFive.sol @@ -35,6 +35,21 @@ library GovFive { prop.actions.push(GovFiveAction({receiver: receiver, fullsig: fullsig, data: data})); } + function printTxData(GovFiveProposal storage prop) internal { + console.log("-----------------------------------"); + console.log("Create following tx on Gnosis safe:"); + console.log("-----------------------------------"); + for (uint256 i = 0; i < prop.actions.length; i++) { + GovFiveAction memory propAction = prop.actions[i]; + bytes memory sig = abi.encodePacked(bytes4(keccak256(bytes(propAction.fullsig)))); + + console.log("### Tx", i + 1); + console.log("Address:", propAction.receiver); + console.log("Data:"); + console.logBytes(abi.encodePacked(sig, propAction.data)); + } + } + function execute(GovFiveProposal storage prop) internal { address VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); Vm vm = Vm(VM_ADDRESS); diff --git a/script/deploy/mainnet/011_OgnOgvMigrationScript.sol b/script/deploy/mainnet/011_OgnOgvMigrationScript.sol index 6d3aef6b..8bd3587c 100644 --- a/script/deploy/mainnet/011_OgnOgvMigrationScript.sol +++ b/script/deploy/mainnet/011_OgnOgvMigrationScript.sol @@ -37,7 +37,7 @@ contract OgnOgvMigrationScript is BaseMainnetScript { _recordDeploy("MIGRATOR", address(migratorProxy)); // - // 3. veOGV implimentation contract to upgrade to + // 1. veOGV implimentation contract to upgrade to uint256 ogvMinStaking = 30 * 24 * 60 * 60; // 2592000 -> 30 days uint256 ogvEpoch = OgvStaking(Addresses.VEOGV).epoch(); // Use old value. OgvStaking veOgvImpl = @@ -45,15 +45,17 @@ contract OgnOgvMigrationScript is BaseMainnetScript { _recordDeploy("VEOGV_IMPL", address(veOgvImpl)); // - // 4. Migrator Contract + // 2. Migrator Contract Migrator migratorImpl = new Migrator(Addresses.OGV, Addresses.OGN, Addresses.VEOGV, xOgnProxy); _recordDeploy("MIGRATOR_IMPL", address(migratorImpl)); console.log("- Migrator init"); migratorProxy.initialize(address(migratorImpl), Addresses.TIMELOCK, ""); + + _buildGnosisTx(); } - function _fork() internal override { + function _buildGnosisTx() internal { Timelock timelock = Timelock(payable(Addresses.TIMELOCK)); address ognRewardsSourceProxy = deployedContracts["OGN_REWARDS_SOURCE"]; @@ -87,6 +89,11 @@ contract OgnOgvMigrationScript is BaseMainnetScript { 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)); - govFive.execute(); // One day lives up a level, and this contract returns a generic governance struct with a function pointers + govFive.printTxData(); + } + + function _fork() internal override { + // Simulate execute on fork + govFive.execute(); } } diff --git a/script/deploy/mainnet/012_xOGNGovernanceScript.sol b/script/deploy/mainnet/012_xOGNGovernanceScript.sol index 2e7b41de..35d3f3c5 100644 --- a/script/deploy/mainnet/012_xOGNGovernanceScript.sol +++ b/script/deploy/mainnet/012_xOGNGovernanceScript.sol @@ -44,9 +44,11 @@ contract XOGNGovernanceScript is BaseMainnetScript { Governance governance = new Governance(ERC20Votes(xOgnProxy), TimelockController(payable(Addresses.TIMELOCK))); _recordDeploy("XOGN_GOV", address(governance)); + + _buildGnosisTx(); } - function _fork() internal override { + function _buildGnosisTx() internal { Timelock timelock = Timelock(payable(Addresses.TIMELOCK)); address xognGov = deployedContracts["XOGN_GOV"]; @@ -59,16 +61,21 @@ contract XOGNGovernanceScript is BaseMainnetScript { 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)); - // Enable rewards + // Enable rewards for staking govFive.action( deployedContracts["OGN_REWARDS_SOURCE"], "setRewardsPerSecond(uint192)", abi.encode(uint192(REWARDS_PER_SECOND)) ); + govFive.printTxData(); + } + + function _fork() internal override { // Go to the start of everything vm.warp(OGN_EPOCH); - govFive.execute(); // One day lives up a level, and this contract returns a generic governance struct with a function pointers + // Simulate execute on fork + govFive.execute(); } } From e5582e1a9505f0129769c3a04906a600bb0c2221 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 19 May 2024 21:07:02 +0400 Subject: [PATCH 07/12] Print tx data only on mainnet --- script/deploy/mainnet/011_OgnOgvMigrationScript.sol | 4 +++- script/deploy/mainnet/012_xOGNGovernanceScript.sol | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/script/deploy/mainnet/011_OgnOgvMigrationScript.sol b/script/deploy/mainnet/011_OgnOgvMigrationScript.sol index 8bd3587c..7ce17d6c 100644 --- a/script/deploy/mainnet/011_OgnOgvMigrationScript.sol +++ b/script/deploy/mainnet/011_OgnOgvMigrationScript.sol @@ -89,7 +89,9 @@ contract OgnOgvMigrationScript is BaseMainnetScript { 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)); - govFive.printTxData(); + if (!isForked) { + govFive.printTxData(); + } } function _fork() internal override { diff --git a/script/deploy/mainnet/012_xOGNGovernanceScript.sol b/script/deploy/mainnet/012_xOGNGovernanceScript.sol index 35d3f3c5..dccac9ad 100644 --- a/script/deploy/mainnet/012_xOGNGovernanceScript.sol +++ b/script/deploy/mainnet/012_xOGNGovernanceScript.sol @@ -68,7 +68,9 @@ contract XOGNGovernanceScript is BaseMainnetScript { abi.encode(uint192(REWARDS_PER_SECOND)) ); - govFive.printTxData(); + if (!isForked) { + govFive.printTxData(); + } } function _fork() internal override { From 8526d1de6e75f69ed40bf903c2bd10b83bf290c0 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Sun, 19 May 2024 23:52:03 +0400 Subject: [PATCH 08/12] Add Migrator tests --- contracts/interfaces/IMintableERC20.sol | 1 + contracts/interfaces/IOGNGovernance.sol | 15 ++ .../mainnet/011_OgnOgvMigrationScript.sol | 54 ++++++ tests/staking/FixedRateRewardsSource.t.sol | 4 + tests/staking/Migrator.t.sol | 16 +- tests/staking/MigratorForkTest.t.sol | 161 ++++++++++++++++++ tests/staking/RewardsSource.t.sol | 4 + 7 files changed, 249 insertions(+), 6 deletions(-) create mode 100644 contracts/interfaces/IOGNGovernance.sol create mode 100644 tests/staking/MigratorForkTest.t.sol diff --git a/contracts/interfaces/IMintableERC20.sol b/contracts/interfaces/IMintableERC20.sol index 9b3bd7a1..c872368f 100644 --- a/contracts/interfaces/IMintableERC20.sol +++ b/contracts/interfaces/IMintableERC20.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.10; interface IMintableERC20 { function mint(address to, uint256 amount) external; function balanceOf(address owner) external view returns (uint256); + function totalSupply() external view returns (uint256); function transfer(address to, uint256 amount) external returns (bool); function approve(address spender, uint256 allowance) external; } diff --git a/contracts/interfaces/IOGNGovernance.sol b/contracts/interfaces/IOGNGovernance.sol new file mode 100644 index 00000000..7a298243 --- /dev/null +++ b/contracts/interfaces/IOGNGovernance.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.10; + +interface IOGNGovernance { + function state(uint256 proposalId) external view returns (uint256); + function proposalCount() external view returns (uint256); + function queue(uint256 proposalId) external; + function execute(uint256 proposalId) external; + function propose( + address[] memory targets, + string[] memory signatures, + bytes[] memory calldatas, + string memory description + ) external returns (uint256); +} diff --git a/script/deploy/mainnet/011_OgnOgvMigrationScript.sol b/script/deploy/mainnet/011_OgnOgvMigrationScript.sol index 7ce17d6c..ab149d4b 100644 --- a/script/deploy/mainnet/011_OgnOgvMigrationScript.sol +++ b/script/deploy/mainnet/011_OgnOgvMigrationScript.sol @@ -17,6 +17,8 @@ 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; @@ -89,6 +91,26 @@ contract OgnOgvMigrationScript is BaseMainnetScript { 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, "") + ); + if (!isForked) { govFive.printTxData(); } @@ -97,5 +119,37 @@ contract OgnOgvMigrationScript is BaseMainnetScript { function _fork() internal override { // Simulate execute on fork govFive.execute(); + + vm.startPrank(Addresses.GOV_MULTISIG); + + IOGNGovernance ognGovernance = IOGNGovernance(Addresses.OGN_GOVERNOR); + uint256 proposalId = ognGovernance.proposalCount(); + + uint256 state = ognGovernance.state(proposalId); + + if (state == 0) { + console.log("Queueing OGN multisig proposal..."); + ognGovernance.queue(proposalId); + state = ognGovernance.state(proposalId); + } + + if (state == 1) { + console.log("Executing OGN multisig proposal..."); + vm.warp(block.timestamp + 2 days); + ognGovernance.execute(proposalId); + } + vm.stopPrank(); + + IMintableERC20 ogn = IMintableERC20(Addresses.OGN); + + // 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"); } } diff --git a/tests/staking/FixedRateRewardsSource.t.sol b/tests/staking/FixedRateRewardsSource.t.sol index 07db745d..242a45dd 100644 --- a/tests/staking/FixedRateRewardsSource.t.sol +++ b/tests/staking/FixedRateRewardsSource.t.sol @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.10; + import "forge-std/Test.sol"; import "contracts/upgrades/FixedRateRewardsSourceProxy.sol"; import "contracts/FixedRateRewardsSource.sol"; diff --git a/tests/staking/Migrator.t.sol b/tests/staking/Migrator.t.sol index 203ca8b7..b5a473f5 100644 --- a/tests/staking/Migrator.t.sol +++ b/tests/staking/Migrator.t.sol @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.10; + import "forge-std/Test.sol"; import "contracts/Migrator.sol"; @@ -190,9 +194,9 @@ contract MigratorTest is Test { // Should have removed OGV staked for (uint256 i = 0; i < lockupIds.length; ++i) { (amount, end, points) = ogvStaking.lockups(alice, lockupIds[i]); - assertEq(amount, 0, "Lockup still exists"); - assertEq(end, 0, "Lockup still exists"); - assertEq(points, 0, "Lockup still exists"); + assertEq(amount, 0, "Amount: Lockup still exists"); + assertEq(end, 0, "End: Lockup still exists"); + assertEq(points, 0, "Points: Lockup still exists"); } vm.stopPrank(); @@ -220,9 +224,9 @@ contract MigratorTest is Test { // Should have removed OGV staked for (uint256 i = 0; i < lockupIds.length; ++i) { (amount, end, points) = ogvStaking.lockups(alice, lockupIds[i]); - assertEq(amount, 0, "Lockup still exists"); - assertEq(end, 0, "Lockup still exists"); - assertEq(points, 0, "Lockup still exists"); + assertEq(amount, 0, "Amount: Lockup still exists"); + assertEq(end, 0, "End: Lockup still exists"); + assertEq(points, 0, "Points: Lockup still exists"); } // Shouldn't have deleted other migration diff --git a/tests/staking/MigratorForkTest.t.sol b/tests/staking/MigratorForkTest.t.sol new file mode 100644 index 00000000..2a310896 --- /dev/null +++ b/tests/staking/MigratorForkTest.t.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.10; + +import "forge-std/Test.sol"; + +import {Addresses} from "contracts/utils/Addresses.sol"; +import {DeployManager} from "script/deploy/DeployManager.sol"; + +import {Migrator} from "contracts/Migrator.sol"; +import {OgvStaking} from "contracts/OgvStaking.sol"; +import {ExponentialStaking} from "contracts/ExponentialStaking.sol"; + +import {IMintableERC20} from "contracts/interfaces/IMintableERC20.sol"; + +contract MigratorForkTest is Test { + DeployManager public deployManager; + + Migrator public migrator; + OgvStaking public veogv; + ExponentialStaking public xogn; + IMintableERC20 public ogv; + IMintableERC20 public ogn; + + address public ogvWhale = Addresses.GOV_MULTISIG; + + function setUp() external { + deployManager = new DeployManager(); + + deployManager.setUp(); + deployManager.run(); + + migrator = Migrator(deployManager.getDeployment("MIGRATOR")); + + veogv = OgvStaking(Addresses.VEOGV); + ogv = IMintableERC20(Addresses.OGV); + ogn = IMintableERC20(Addresses.OGN); + xogn = ExponentialStaking(deployManager.getDeployment("XOGN")); + + vm.startPrank(ogvWhale); + ogv.approve(address(migrator), type(uint256).max); + ogn.approve(address(migrator), type(uint256).max); + ogv.approve(address(veogv), type(uint256).max); + vm.stopPrank(); + } + + function testBalanceMigration() external { + vm.startPrank(ogvWhale); + + uint256 migratorOGNReserve = ogn.balanceOf(address(migrator)); + uint256 ogvSupply = ogv.totalSupply(); + uint256 ogvBalanceBefore = ogv.balanceOf(ogvWhale); + uint256 ognBalanceBefore = ogn.balanceOf(ogvWhale); + + // Should be able to swap OGV to OGN at fixed rate + migrator.migrate(100 ether); + + assertEq(ogv.balanceOf(ogvWhale), ogvBalanceBefore - 100 ether, "More OGV burnt"); + assertEq(ogv.totalSupply(), ogvSupply - 100 ether, "OGV supply mismatch"); + + assertEq(ogn.balanceOf(ogvWhale), ognBalanceBefore + 9.137 ether, "Less OGN received"); + assertEq(ogn.balanceOf(address(migrator)), migratorOGNReserve - 9.137 ether, "More OGN sent"); + + vm.stopPrank(); + } + + function testDustBalanceMigration() public { + vm.startPrank(ogvWhale); + migrator.migrate(1); + vm.stopPrank(); + } + + function testUnstakingOGVLockups() public { + vm.startPrank(ogvWhale); + + // Collect rewards + veogv.collectRewards(); + + uint256 migratorOGNReserve = ogn.balanceOf(address(migrator)); + uint256 ogvSupply = ogv.totalSupply(); + uint256 ogvBalanceBefore = ogv.balanceOf(ogvWhale); + uint256 ognBalanceBefore = ogn.balanceOf(ogvWhale); + + (uint128 amount, uint128 end, uint256 points) = veogv.lockups(ogvWhale, 13); + uint256 ognTransferred = (amount * 9137e8) / 1e13; + + uint256[] memory lockupIds = new uint256[](1); + lockupIds[0] = 13; + migrator.migrate(lockupIds, 0, 0, false, 0, 0); + assertEq(ogv.totalSupply(), ogvSupply - amount, "OGV supply mismatch"); + assertEq(ogn.balanceOf(address(migrator)), migratorOGNReserve - ognTransferred, "OGN reserve balance mismatch"); + assertEq(ogv.balanceOf(ogvWhale), ogvBalanceBefore, "Change in OGV balance"); + assertEq(ogn.balanceOf(ogvWhale), ognBalanceBefore + ognTransferred, "No change in OGN balance"); + + (amount, end, points) = veogv.lockups(ogvWhale, 13); + assertEq(amount, 0, "Amount: Lockup still exists"); + assertEq(end, 0, "End: Lockup still exists"); + assertEq(points, 0, "Points: Lockup still exists"); + + vm.stopPrank(); + } + + function testMigrateSelectedStakes() public { + vm.startPrank(ogvWhale); + + uint256[] memory lockupIds = new uint256[](1); + lockupIds[0] = 13; + + (uint128 amount, uint128 end, uint256 points) = veogv.lockups(ogvWhale, 13); + + uint256 stakeAmount = (amount * 9137e8) / 1e13; + + migrator.migrate(lockupIds, 0, 0, false, stakeAmount, 300 days); + + // Should have merged it in a single OGN lockup + (amount, end, points) = xogn.lockups(ogvWhale, 0); + assertEq(amount, stakeAmount, "Lockup not migrated"); + + (amount, end, points) = veogv.lockups(ogvWhale, 13); + assertEq(amount, 0, "Amount: Lockup still exists"); + assertEq(end, 0, "End: Lockup still exists"); + assertEq(points, 0, "Points: Lockup still exists"); + + // Shouldn't have deleted other migration + (amount, end, points) = veogv.lockups(ogvWhale, 14); + assertEq(amount > 0, true, "Other lockup deleted"); + + vm.stopPrank(); + } + + function testBurnOnDecomission() public { + uint256 maxOgnAmount = ogn.balanceOf(address(migrator)); + + vm.warp(migrator.endTime() + 100); + vm.prank(Addresses.TIMELOCK); + migrator.transferExcessTokens(address(0x22dead)); + + assertEq(ogn.balanceOf(address(migrator)), 0 ether, "OGN leftover"); + assertEq(ogn.balanceOf(address(0x22dead)), maxOgnAmount, "OGN not sent to burn address"); + } + + function testMigrateAfterTimelimit() public { + // Should allow migration even after timelimit + // but before decommission + vm.startPrank(ogvWhale); + + vm.warp(migrator.endTime() + 100); + + assertEq(migrator.isMigrationActive(), false, "Migration state not changed"); + + migrator.migrate(1 ether); + + // Check migrating stakes as well + uint256[] memory lockupIds = new uint256[](2); + lockupIds[0] = 13; + lockupIds[1] = 14; + migrator.migrate(lockupIds, 0, 0, false, 0, 0); + + vm.stopPrank(); + } +} diff --git a/tests/staking/RewardsSource.t.sol b/tests/staking/RewardsSource.t.sol index c5454fe3..56729190 100644 --- a/tests/staking/RewardsSource.t.sol +++ b/tests/staking/RewardsSource.t.sol @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.10; + import "forge-std/Test.sol"; import "contracts/upgrades/RewardsSourceProxy.sol"; import "contracts/RewardsSource.sol"; From 9728d78f9c3f504505bab011c9d013527480bfc3 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 20 May 2024 00:18:26 +0400 Subject: [PATCH 09/12] Fix CI --- script/deploy/DeployManager.sol | 4 ++++ script/deploy/mainnet/011_OgnOgvMigrationScript.sol | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/script/deploy/DeployManager.sol b/script/deploy/DeployManager.sol index 1f2a5c28..fdb90ee0 100644 --- a/script/deploy/DeployManager.sol +++ b/script/deploy/DeployManager.sol @@ -127,13 +127,17 @@ contract DeployManager is Script { } 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 ab149d4b..f1f5c9f8 100644 --- a/script/deploy/mainnet/011_OgnOgvMigrationScript.sol +++ b/script/deploy/mainnet/011_OgnOgvMigrationScript.sol @@ -25,7 +25,7 @@ contract OgnOgvMigrationScript is BaseMainnetScript { GovFive.GovFiveProposal govFive; - string public constant override DEPLOY_NAME = "011_OgnOgvMigrationScript"; + string public constant override DEPLOY_NAME = "011_OgnOgvMigration"; constructor() {} From 5a7e530d7c2d24f8d3dc8d0ad7b3ffb2f7d3f796 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Mon, 20 May 2024 00:25:41 +0400 Subject: [PATCH 10/12] Brownie remap --- brownie-config.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/brownie-config.yaml b/brownie-config.yaml index 82f7f3be..e31354c1 100644 --- a/brownie-config.yaml +++ b/brownie-config.yaml @@ -3,3 +3,7 @@ dependencies: - OpenZeppelin/openzeppelin-contracts@4.6.0 - OpenZeppelin/openzeppelin-contracts-upgradeable@4.6.0 - paulrberg/prb-math@2.5.0 +compiler: + solc: + remappings: + - forge-std/=./lib/forge-std/src/ \ No newline at end of file From efd24a5d0803312df13a7f0646f6c7b1769b2e62 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Tue, 21 May 2024 22:54:02 +0530 Subject: [PATCH 11/12] Cherry pick CR comments --- contracts/FixedRateRewardsSource.sol | 16 +++++++--------- foundry.toml | 5 ++++- script/deploy/mainnet/010_xOGNSetupScript.sol | 5 +---- tests/staking/FixedRateRewardsSource.t.sol | 4 +++- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/contracts/FixedRateRewardsSource.sol b/contracts/FixedRateRewardsSource.sol index 2b251efa..c3491c7f 100644 --- a/contracts/FixedRateRewardsSource.sol +++ b/contracts/FixedRateRewardsSource.sol @@ -46,18 +46,12 @@ contract FixedRateRewardsSource is Governable, Initializable { /// @dev Initialize the proxy implementation /// @param _strategistAddr Address of the Strategist /// @param _rewardsTarget Address that receives rewards - /// @param _rewardsPerSecond Rate of reward emission - function initialize(address _strategistAddr, address _rewardsTarget, uint192 _rewardsPerSecond) - external - initializer - { + function initialize(address _strategistAddr, address _rewardsTarget) external initializer { _setStrategistAddr(_strategistAddr); _setRewardsTarget(_rewardsTarget); // Rewards start from the moment the contract is initialized rewardConfig.lastCollect = uint64(block.timestamp); - - _setRewardsPerSecond(_rewardsPerSecond); } /// @dev Collect pending rewards @@ -136,8 +130,12 @@ contract FixedRateRewardsSource is Governable, Initializable { RewardConfig storage _config = rewardConfig; emit RewardsPerSecondChanged(_rewardsPerSecond, _config.rewardsPerSecond); if (_config.rewardsPerSecond == 0) { - // When changing rate from zero to non-zero, - // Update lastCollect timestamp as well + /* This contract code allows for contract deployment & initialization and then the contract can be live for quite + * some time before it is funded and `_rewardsPerSecond` are set to non 0 value. In that case the vesting period + * from contract initialization until now would be taken into account instead of the time since the contract has been + * "activated" by setting the `setRewardsPerSecond`. To mitigate the issue we update the `_config.lastCollect` + * to current time. + */ _config.lastCollect = uint64(block.timestamp); } _config.rewardsPerSecond = _rewardsPerSecond; diff --git a/foundry.toml b/foundry.toml index ae1848cb..b214348a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -11,4 +11,7 @@ remappings = [ "OpenZeppelin/openzeppelin-contracts-upgradeable@4.6.0/=./lib/openzeppelin-contracts-upgradeable", "paulrberg/prb-math@2.5.0/=./lib/prb-math" ] -fs_permissions = [{ access = "read-write", path = "./build"}] \ No newline at end of file +fs_permissions = [{ access = "read-write", path = "./build"}] +extra_output_files = [ + "metadata" +] \ No newline at end of file diff --git a/script/deploy/mainnet/010_xOGNSetupScript.sol b/script/deploy/mainnet/010_xOGNSetupScript.sol index 78f084e9..3e017247 100644 --- a/script/deploy/mainnet/010_xOGNSetupScript.sol +++ b/script/deploy/mainnet/010_xOGNSetupScript.sol @@ -50,15 +50,12 @@ contract XOGNSetupScript is BaseMainnetScript { // // 3. Rewards implimentation and init - // Reward rate is 0, Will be set later when we want initiate rewards - uint256 rewardsPerSecond = 0; FixedRateRewardsSource fixedRateRewardsSourceImpl = new FixedRateRewardsSource(Addresses.OGN); _recordDeploy("OGN_REWARDS_SOURCE_IMPL", address(fixedRateRewardsSourceImpl)); console.log("- OGN rewards init"); bytes memory implInitData = string.concat( - fixedRateRewardsSourceImpl.initialize.selector, - abi.encode(Addresses.STRATEGIST, address(xOgnProxy), rewardsPerSecond) + fixedRateRewardsSourceImpl.initialize.selector, abi.encode(Addresses.STRATEGIST, address(xOgnProxy)) ); ognRewardsSourceProxy.initialize(address(fixedRateRewardsSourceImpl), Addresses.TIMELOCK, implInitData); } diff --git a/tests/staking/FixedRateRewardsSource.t.sol b/tests/staking/FixedRateRewardsSource.t.sol index 242a45dd..eb691b15 100644 --- a/tests/staking/FixedRateRewardsSource.t.sol +++ b/tests/staking/FixedRateRewardsSource.t.sol @@ -26,8 +26,10 @@ contract FixedRateRewardsSourceTest is Test { rewardsProxy.initialize(address(rewards), governor, ""); rewards = FixedRateRewardsSource(address(rewardsProxy)); + // Initialize + rewards.initialize(strategist, staking); // Configure Rewards - rewards.initialize(strategist, staking, uint192(100 ether)); // 100 OGN per second + rewards.setRewardsPerSecond(uint192(100 ether)); // 100 OGN per second // Make sure contract has enough OGN for rewards ogn.mint(address(rewardsProxy), 1000000 ether); From 14d1073318eaaef2082f7b5712aba819810fe150 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Wed, 22 May 2024 16:41:20 +0530 Subject: [PATCH 12/12] Deploy 010 - OGN Rewards Source and xOGN (#420) * Deploy 010 * Fix fork tests * Deploy only once for fork tests * Add another sleep --- .gitignore | 3 ++- README.md | 7 +++++++ build/deployments.json | 11 +++++++++-- script/deploy/DeployManager.sol | 8 +++++++- script/deploy/mainnet/010_xOGNSetupScript.sol | 8 +------- script/deploy/mainnet/012_xOGNGovernanceScript.sol | 8 ++++++++ tests/governance/XOGNGovernanceForkTest.t.sol | 4 +++- tests/staking/MigratorForkTest.t.sol | 4 +++- tests/staking/OGNRewardsSourceForkTest.t.sol | 4 +++- tests/staking/XOGNStakingForkTest.t..sol | 4 +++- 10 files changed, 46 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index f24ceb7a..e9064c85 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ out/ .vscode brownie-deploy/ .idea -deployments-fork.json +deployments-fork*.json +broadcast/* \ No newline at end of file diff --git a/README.md b/README.md index 8ca04492..6efe32b3 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,13 @@ forge install forge test ``` +## Running fork tests (forge) + +```bash +forge install +forge test --fork-url $ALCHEMY_PROVIDER_URL -vvv --mc "ForkTest" +``` + ## Running a local node Copy `dev.env` to `.env` and fill out the `PROVIDER_URL` diff --git a/build/deployments.json b/build/deployments.json index 121ed2bd..61dc458c 100644 --- a/build/deployments.json +++ b/build/deployments.json @@ -1,6 +1,13 @@ { "1": { - "executions": {}, - "contracts": {} + "executions": { + "010_xOGNSetup": 1716312107 + }, + "contracts": { + "OGN_REWARDS_SOURCE": "0x7609c88E5880e934dd3A75bCFef44E31b1Badb8b", + "OGN_REWARDS_SOURCE_IMPL": "0x16890bdd817Ed1c4654430d67329CB20b0B71bB0", + "XOGN": "0x63898b3b6Ef3d39332082178656E9862bee45C57", + "XOGN_IMPL": "0x97711c7a5D64A064a95d10e37f786d2bD8b1F3c8" + } } } \ No newline at end of file diff --git a/script/deploy/DeployManager.sol b/script/deploy/DeployManager.sol index fdb90ee0..6cf75bac 100644 --- a/script/deploy/DeployManager.sol +++ b/script/deploy/DeployManager.sol @@ -14,6 +14,8 @@ contract DeployManager is Script { 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); @@ -28,10 +30,12 @@ contract DeployManager is Script { } function getForkDeploymentFilePath() public view returns (string memory) { - return string(abi.encodePacked(vm.projectRoot(), "/build/deployments-fork.json")); + return string(abi.encodePacked(vm.projectRoot(), "/build/deployments-fork-", forkFileId, ".json")); } function setUp() external { + forkFileId = Strings.toString(block.timestamp); + string memory chainIdStr = Strings.toString(block.chainid); string memory chainIdKey = string(abi.encodePacked(".", chainIdStr)); @@ -126,6 +130,8 @@ contract DeployManager is Script { 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."); diff --git a/script/deploy/mainnet/010_xOGNSetupScript.sol b/script/deploy/mainnet/010_xOGNSetupScript.sol index 3e017247..5f159417 100644 --- a/script/deploy/mainnet/010_xOGNSetupScript.sol +++ b/script/deploy/mainnet/010_xOGNSetupScript.sol @@ -60,11 +60,5 @@ contract XOGNSetupScript is BaseMainnetScript { ognRewardsSourceProxy.initialize(address(fixedRateRewardsSourceImpl), Addresses.TIMELOCK, implInitData); } - 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); - } + function _fork() internal override {} } diff --git a/script/deploy/mainnet/012_xOGNGovernanceScript.sol b/script/deploy/mainnet/012_xOGNGovernanceScript.sol index dccac9ad..14b6c37d 100644 --- a/script/deploy/mainnet/012_xOGNGovernanceScript.sol +++ b/script/deploy/mainnet/012_xOGNGovernanceScript.sol @@ -20,6 +20,8 @@ import {Governance} from "contracts/Governance.sol"; import {GovFive} from "contracts/utils/GovFive.sol"; +import {IMintableERC20} from "contracts/interfaces/IMintableERC20.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"; @@ -74,6 +76,12 @@ contract XOGNGovernanceScript is BaseMainnetScript { } 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); diff --git a/tests/governance/XOGNGovernanceForkTest.t.sol b/tests/governance/XOGNGovernanceForkTest.t.sol index 5d4b43cf..dca93607 100644 --- a/tests/governance/XOGNGovernanceForkTest.t.sol +++ b/tests/governance/XOGNGovernanceForkTest.t.sol @@ -32,12 +32,14 @@ contract XOGNGovernanceForkTest is Test { int256 constant NEW_STAKE = -1; - function setUp() external { + constructor() { deployManager = new DeployManager(); deployManager.setUp(); deployManager.run(); + } + function setUp() external { xogn = ExponentialStaking(deployManager.getDeployment("XOGN")); timelock = Timelock(payable(Addresses.TIMELOCK)); xognGov = Governance(payable(deployManager.getDeployment("XOGN_GOV"))); diff --git a/tests/staking/MigratorForkTest.t.sol b/tests/staking/MigratorForkTest.t.sol index 2a310896..b6c8fb6b 100644 --- a/tests/staking/MigratorForkTest.t.sol +++ b/tests/staking/MigratorForkTest.t.sol @@ -24,12 +24,14 @@ contract MigratorForkTest is Test { address public ogvWhale = Addresses.GOV_MULTISIG; - function setUp() external { + constructor() { deployManager = new DeployManager(); deployManager.setUp(); deployManager.run(); + } + function setUp() external { migrator = Migrator(deployManager.getDeployment("MIGRATOR")); veogv = OgvStaking(Addresses.VEOGV); diff --git a/tests/staking/OGNRewardsSourceForkTest.t.sol b/tests/staking/OGNRewardsSourceForkTest.t.sol index f066f340..0578fab0 100644 --- a/tests/staking/OGNRewardsSourceForkTest.t.sol +++ b/tests/staking/OGNRewardsSourceForkTest.t.sol @@ -31,12 +31,14 @@ contract OGNRewardsSourceForkTest is Test { int256 constant NEW_STAKE = -1; - function setUp() external { + constructor() { deployManager = new DeployManager(); deployManager.setUp(); deployManager.run(); + } + function setUp() external { xogn = ExponentialStaking(deployManager.getDeployment("XOGN")); timelock = Timelock(payable(Addresses.TIMELOCK)); xognGov = Governance(payable(deployManager.getDeployment("XOGN_GOV"))); diff --git a/tests/staking/XOGNStakingForkTest.t..sol b/tests/staking/XOGNStakingForkTest.t..sol index f9d84e7a..db1a188c 100644 --- a/tests/staking/XOGNStakingForkTest.t..sol +++ b/tests/staking/XOGNStakingForkTest.t..sol @@ -32,12 +32,14 @@ contract XOGNStakingForkTest is Test { int256 constant NEW_STAKE = -1; - function setUp() external { + constructor() { deployManager = new DeployManager(); deployManager.setUp(); deployManager.run(); + } + function setUp() external { xogn = ExponentialStaking(deployManager.getDeployment("XOGN")); timelock = Timelock(payable(Addresses.TIMELOCK)); xognGov = Governance(payable(deployManager.getDeployment("XOGN_GOV")));