From 012b8abbc68fcf83dbab7af835abcca936445903 Mon Sep 17 00:00:00 2001 From: Daniel Von Fange Date: Wed, 22 May 2024 08:40:20 -0400 Subject: [PATCH] Token Merger Deploy Script (#418) * Add OGNRewardsSource contract * Make collectRewards only callable by RewardsTarget * Draft xOGN staking contract * Correct maxStakeDuration * Add penalty event * Change names * Fix lockup ID * Revert change and cast properly * Gas opts * Remove casting * Add `getLockupsCount` method (#411) * Allow non-duration change amount increase staking extends * Add tests, add move lockupid code * Add Migrator (#410) * Add Migrator contract * Fix some tests * Code review changes * Update OgvStaking tests * Disable delegation tests * Allow just unstakes * Fix comment * More cleanup * Fix brownie tests * Return excess OGN rather than burn * Simplify calculation * Return 0 if uninitialized (#415) * Check available balance in `previewRewards` (#413) * Check available balance in `previewRewards` * Chore: forge fmt --------- Co-authored-by: Daniel Von Fange * Fix: Remove unused errors (#416) * First draft of deploy file * Add fork test tooling (#419) --------- Co-authored-by: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> --- .github/workflows/main.yaml | 18 +- .gitignore | 2 + .gitmodules | 3 +- README.md | 7 + brownie-config.yaml | 4 + build/deployments.json | 13 + contracts/FixedRateRewardsSource.sol | 21 +- contracts/Governance.sol | 6 +- contracts/OgvStaking.sol | 2 +- contracts/Timelock.sol | 2 +- contracts/interfaces/IMintableERC20.sol | 10 + contracts/interfaces/IOGNGovernance.sol | 15 + contracts/tests/MockRewardsSource.sol | 2 +- contracts/tests/TestToken.sol | 2 +- .../upgrades/ExponentialStakingProxy.sol | 7 + contracts/utils/Addresses.sol | 22 ++ contracts/utils/GovFive.sol | 67 +++++ foundry.toml | 6 + lib/forge-std | 2 +- script/deploy/DeployManager.sol | 152 ++++++++++ script/deploy/mainnet/010_xOGNSetupScript.sol | 64 ++++ .../mainnet/011_OgnOgvMigrationScript.sol | 155 ++++++++++ .../mainnet/012_xOGNGovernanceScript.sol | 91 ++++++ script/deploy/mainnet/BaseMainnetScript.sol | 80 +++++ tests/governance/XOGNGovernanceForkTest.t.sol | 275 ++++++++++++++++++ tests/governance/test_initial_state.py | 4 +- tests/governance/test_vote.py | 6 +- tests/staking/FixedRateRewardsSource.t.sol | 8 +- tests/staking/Migrator.t.sol | 40 +-- tests/staking/MigratorForkTest.t.sol | 163 +++++++++++ tests/staking/OGNRewardsSourceForkTest.t.sol | 84 ++++++ tests/staking/RewardsSource.t.sol | 4 + tests/staking/XOGNStakingForkTest.t..sol | 126 ++++++++ 33 files changed, 1418 insertions(+), 45 deletions(-) create mode 100644 build/deployments.json create mode 100644 contracts/interfaces/IMintableERC20.sol create mode 100644 contracts/interfaces/IOGNGovernance.sol create mode 100644 contracts/upgrades/ExponentialStakingProxy.sol create mode 100644 contracts/utils/Addresses.sol create mode 100644 contracts/utils/GovFive.sol create mode 100644 script/deploy/DeployManager.sol create mode 100644 script/deploy/mainnet/010_xOGNSetupScript.sol create mode 100644 script/deploy/mainnet/011_OgnOgvMigrationScript.sol create mode 100644 script/deploy/mainnet/012_xOGNGovernanceScript.sol create mode 100644 script/deploy/mainnet/BaseMainnetScript.sol create mode 100644 tests/governance/XOGNGovernanceForkTest.t.sol create mode 100644 tests/staking/MigratorForkTest.t.sol create mode 100644 tests/staking/OGNRewardsSourceForkTest.t.sol create mode 100644 tests/staking/XOGNStakingForkTest.t..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..e9064c85 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ out/ .vscode brownie-deploy/ .idea +deployments-fork*.json +broadcast/* \ No newline at end of file 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/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/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 diff --git a/build/deployments.json b/build/deployments.json new file mode 100644 index 00000000..61dc458c --- /dev/null +++ b/build/deployments.json @@ -0,0 +1,13 @@ +{ + "1": { + "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/contracts/FixedRateRewardsSource.sol b/contracts/FixedRateRewardsSource.sol index 4ef9f431..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 @@ -90,10 +84,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 +129,15 @@ contract FixedRateRewardsSource is Governable, Initializable { // Update storage RewardConfig storage _config = rewardConfig; emit RewardsPerSecondChanged(_rewardsPerSecond, _config.rewardsPerSecond); + if (_config.rewardsPerSecond == 0) { + /* 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/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/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/Timelock.sol b/contracts/Timelock.sol index d7ef85b7..6bffcb9a 100644 --- a/contracts/Timelock.sol +++ b/contracts/Timelock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; +pragma solidity ^0.8.10; import "OpenZeppelin/openzeppelin-contracts@4.6.0/contracts/governance/TimelockController.sol"; diff --git a/contracts/interfaces/IMintableERC20.sol b/contracts/interfaces/IMintableERC20.sol new file mode 100644 index 00000000..c872368f --- /dev/null +++ b/contracts/interfaces/IMintableERC20.sol @@ -0,0 +1,10 @@ +// 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 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/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/upgrades/ExponentialStakingProxy.sol b/contracts/upgrades/ExponentialStakingProxy.sol new file mode 100644 index 00000000..6c24dbac --- /dev/null +++ b/contracts/upgrades/ExponentialStakingProxy.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.10; + +import {InitializeGovernedUpgradeabilityProxy} from "./InitializeGovernedUpgradeabilityProxy.sol"; + +contract ExponentialStakingProxy is InitializeGovernedUpgradeabilityProxy {} diff --git a/contracts/utils/Addresses.sol b/contracts/utils/Addresses.sol new file mode 100644 index 00000000..2513a9c1 --- /dev/null +++ b/contracts/utils/Addresses.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.10; + +library Addresses { + address public constant TIMELOCK = 0x35918cDE7233F2dD33fA41ae3Cb6aE0e42E0e69F; + 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; + address public constant OGV_REWARDS_PROXY = 0x7d82E86CF1496f9485a8ea04012afeb3C7489397; + address public constant VEOGV = 0x0C4576Ca1c365868E162554AF8e385dc3e7C66D9; + + address public constant OUSD_BUYBACK = 0xD7B28d06365b85933c64E11e639EA0d3bC0e3BaB; + address public constant OETH_BUYBACK = 0xFD6c58850caCF9cCF6e8Aee479BFb4Df14a362D2; + 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..efcbdf22 --- /dev/null +++ b/contracts/utils/GovFive.sol @@ -0,0 +1,67 @@ +// 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 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); + 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 b89cc459..b214348a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,9 +3,15 @@ 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", "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"}] +extra_output_files = [ + "metadata" +] \ No newline at end of file diff --git a/lib/forge-std b/lib/forge-std index be5c649c..978ac6fa 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit be5c649c897519cdae29c5a0ad00d4c1351e4dcc +Subproject commit 978ac6fadb62f5f0b723c996f64be52eddba6801 diff --git a/script/deploy/DeployManager.sol b/script/deploy/DeployManager.sol new file mode 100644 index 00000000..6cf75bac --- /dev/null +++ b/script/deploy/DeployManager.sol @@ -0,0 +1,152 @@ +// 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 {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; + mapping(string => bool) public scriptsExecuted; + + string internal forkFileId = ""; + + function isForked() public view returns (bool) { + return vm.isContext(VmSafe.ForgeContext.ScriptDryRun) || vm.isContext(VmSafe.ForgeContext.Test) + || vm.isContext(VmSafe.ForgeContext.TestGroup); + } + + function getDeploymentFilePath() public view returns (string memory) { + return isForked() ? getForkDeploymentFilePath() : getMainnetDeploymentFilePath(); + } + + 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-", forkFileId, ".json")); + } + + function setUp() external { + forkFileId = Strings.toString(block.timestamp); + + 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 XOGNSetupScript()); + _runDeployFile(new OgnOgvMigrationScript()); + _runDeployFile(new XOGNGovernanceScript()); + } + + 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 + */ + 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; + } + + // Sleep 0.5s so that the previous write is complete + vm.sleep(500); + vm.writeJson(networkDeployments, deploymentsFilePath, contractsKey); + console.log("> Deployment addresses stored."); + + /** + * Write Execution History + */ + currentExecutions = vm.serializeUint(executionsKey, deployScript.DEPLOY_NAME(), block.timestamp); + + // Sleep 0.5s so that the previous write is complete + vm.sleep(500); + vm.writeJson(currentExecutions, deploymentsFilePath, executionsKey); + console.log("> Deploy script execution complete."); + } + + function getDeployment(string calldata contractName) external view returns (address) { + return deployedContracts[contractName]; + } +} diff --git a/script/deploy/mainnet/010_xOGNSetupScript.sol b/script/deploy/mainnet/010_xOGNSetupScript.sol new file mode 100644 index 00000000..5f159417 --- /dev/null +++ b/script/deploy/mainnet/010_xOGNSetupScript.sol @@ -0,0 +1,64 @@ +// 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 {IMintableERC20} from "contracts/interfaces/IMintableERC20.sol"; + +contract XOGNSetupScript is BaseMainnetScript { + 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, ""); + + // + // 3. Rewards implimentation and init + 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)) + ); + ognRewardsSourceProxy.initialize(address(fixedRateRewardsSourceImpl), Addresses.TIMELOCK, implInitData); + } + + function _fork() internal override {} +} diff --git a/script/deploy/mainnet/011_OgnOgvMigrationScript.sol b/script/deploy/mainnet/011_OgnOgvMigrationScript.sol new file mode 100644 index 00000000..f1f5c9f8 --- /dev/null +++ b/script/deploy/mainnet/011_OgnOgvMigrationScript.sol @@ -0,0 +1,155 @@ +// 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 {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; + + GovFive.GovFiveProposal govFive; + + string public constant override DEPLOY_NAME = "011_OgnOgvMigration"; + + constructor() {} + + function _execute() internal override { + console.log("Deploy:"); + console.log("------------"); + + address xOgnProxy = deployedContracts["XOGN"]; + + MigratorProxy migratorProxy = new MigratorProxy(); + _recordDeploy("MIGRATOR", address(migratorProxy)); + + // + // 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 = + new OgvStaking(Addresses.OGV, ogvEpoch, ogvMinStaking, Addresses.OGV_REWARDS_PROXY, address(migratorProxy)); + _recordDeploy("VEOGV_IMPL", address(veOgvImpl)); + + // + // 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 _buildGnosisTx() internal { + 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(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(ognRewardsSourceProxy)); + govFive.action(Addresses.OETH_BUYBACK, "upgradeTo(address)", abi.encode(Addresses.OETH_BUYBACK_IMPL)); // Todo, use latest deployed address + govFive.action(Addresses.OETH_BUYBACK, "setRewardsSource(address)", abi.encode(ognRewardsSourceProxy)); + + // Mint token proposal from OGN governance + IMintableERC20 ogv = IMintableERC20(Addresses.OGV); + // Mint additional OGN, will get returned after starting migration + uint256 ognToMint = ((ogv.totalSupply() * 0.09137 ether) / 1 ether) + 1_000_000 ether; + + address[] memory targets = new address[](1); + string[] memory sigs = new string[](1); + bytes[] memory calldatas = new bytes[](1); + + // OGN Gov 1: Mint OGN + targets[0] = Addresses.OGN; + sigs[0] = "mint(address,uint256)"; + calldatas[0] = abi.encode(deployedContracts["MIGRATOR"], ognToMint); + + govFive.action( + Addresses.OGN_GOVERNOR, + "propose(address[],string[],bytes[],string)", + abi.encode(targets, sigs, calldatas, "") + ); + + if (!isForked) { + govFive.printTxData(); + } + } + + 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/script/deploy/mainnet/012_xOGNGovernanceScript.sol b/script/deploy/mainnet/012_xOGNGovernanceScript.sol new file mode 100644 index 00000000..14b6c37d --- /dev/null +++ b/script/deploy/mainnet/012_xOGNGovernanceScript.sol @@ -0,0 +1,91 @@ +// 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 {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"; + +contract XOGNGovernanceScript is BaseMainnetScript { + using GovFive for GovFive.GovFiveProposal; + + GovFive.GovFiveProposal govFive; + + 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 { + console.log("Deploy:"); + console.log("------------"); + + address xOgnProxy = deployedContracts["XOGN"]; + + Governance governance = new Governance(ERC20Votes(xOgnProxy), TimelockController(payable(Addresses.TIMELOCK))); + + _recordDeploy("XOGN_GOV", address(governance)); + + _buildGnosisTx(); + } + + function _buildGnosisTx() internal { + Timelock timelock = Timelock(payable(Addresses.TIMELOCK)); + + address xognGov = deployedContracts["XOGN_GOV"]; + + 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 for staking + govFive.action( + deployedContracts["OGN_REWARDS_SOURCE"], + "setRewardsPerSecond(uint192)", + abi.encode(uint192(REWARDS_PER_SECOND)) + ); + + if (!isForked) { + govFive.printTxData(); + } + } + + function _fork() internal override { + IMintableERC20 ogn = IMintableERC20(Addresses.OGN); + + // Mint enough OGN to fund 100 days of rewards + vm.prank(Addresses.OGN_GOVERNOR); + ogn.mint(deployedContracts["OGN_REWARDS_SOURCE"], 30_000_000 ether); + + // Go to the start of everything + vm.warp(OGN_EPOCH); + + // Simulate execute on fork + govFive.execute(); + } +} diff --git a/script/deploy/mainnet/BaseMainnetScript.sol b/script/deploy/mainnet/BaseMainnetScript.sol new file mode 100644 index 00000000..5effa75c --- /dev/null +++ b/script/deploy/mainnet/BaseMainnetScript.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT + +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"; + +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; + + 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 + + function getAllDeployRecords() external view returns (DeployRecord[] memory) { + return deploys; + } + + function preloadDeployedContract(string memory name, address addr) external { + deployedContracts[name] = addr; + } + + function setUp() external {} + + function run() external { + if (block.chainid != 1) { + revert("Not Mainnet"); + } + // Will not execute script if after this block number + if (block.number > deployBlockNum) { + // console.log("Current block %s, script block %s", block.number, deployBlockNum); + return; + } + + 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); + vm.startPrank(impersonator); + } else { + uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); + address deployer = vm.rememberKey(deployerPrivateKey); + vm.startBroadcast(deployer); + console.log("Deploying on mainnet with deployer: %s", deployer); + } + + _execute(); + + if (isForked) { + vm.stopPrank(); + _fork(); + } else { + vm.stopBroadcast(); + } + } + + function DEPLOY_NAME() external view virtual returns (string memory); + + function _execute() internal virtual {} + + function _fork() internal virtual {} +} diff --git a/tests/governance/XOGNGovernanceForkTest.t.sol b/tests/governance/XOGNGovernanceForkTest.t.sol new file mode 100644 index 00000000..dca93607 --- /dev/null +++ b/tests/governance/XOGNGovernanceForkTest.t.sol @@ -0,0 +1,275 @@ +// 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; + + 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"))); + + 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/governance/test_initial_state.py b/tests/governance/test_initial_state.py index 703e8d5c..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): @@ -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 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( diff --git a/tests/staking/FixedRateRewardsSource.t.sol b/tests/staking/FixedRateRewardsSource.t.sol index 07db745d..eb691b15 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"; @@ -22,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); diff --git a/tests/staking/Migrator.t.sol b/tests/staking/Migrator.t.sol index 203ca8b7..383a854b 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 @@ -260,9 +264,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(); @@ -311,9 +315,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(); @@ -349,9 +353,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(); @@ -389,9 +393,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(); diff --git a/tests/staking/MigratorForkTest.t.sol b/tests/staking/MigratorForkTest.t.sol new file mode 100644 index 00000000..b6c8fb6b --- /dev/null +++ b/tests/staking/MigratorForkTest.t.sol @@ -0,0 +1,163 @@ +// 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; + + constructor() { + deployManager = new DeployManager(); + + deployManager.setUp(); + deployManager.run(); + } + + function setUp() external { + 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/OGNRewardsSourceForkTest.t.sol b/tests/staking/OGNRewardsSourceForkTest.t.sol new file mode 100644 index 00000000..0578fab0 --- /dev/null +++ b/tests/staking/OGNRewardsSourceForkTest.t.sol @@ -0,0 +1,84 @@ +// 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; + + 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"))); + + 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/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"; diff --git a/tests/staking/XOGNStakingForkTest.t..sol b/tests/staking/XOGNStakingForkTest.t..sol new file mode 100644 index 00000000..db1a188c --- /dev/null +++ b/tests/staking/XOGNStakingForkTest.t..sol @@ -0,0 +1,126 @@ +// 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 XOGNStakingForkTest 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; + + 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"))); + + 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 testTokenName() external view { + 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); + + console.log(FixedRateRewardsSource(ognRewardsSource).previewRewards()); + + xogn.stake( + 1000 ether, // 1k OGN + 30 days, + alice, + false, + NEW_STAKE + ); + + 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), netRewards + 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(); + } +}