Skip to content

Commit

Permalink
Migrator changes (#435)
Browse files Browse the repository at this point in the history
* Add support for migrating rewards

* Update deploy script

* Remove unused error and move around lines

* Change OGV whale address

* Fix lockupIds

* Fix failing tests
  • Loading branch information
shahthepro authored Jun 5, 2024
1 parent 9a44bd7 commit db9d7ba
Show file tree
Hide file tree
Showing 15 changed files with 295 additions and 56 deletions.
24 changes: 12 additions & 12 deletions contracts/Migrator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ contract Migrator is Governable {

error MigrationAlreadyStarted();
error ContractInsolvent(uint256 expectedOGN, uint256 availableOGN);
error LockupIdsRequired();
error InvalidStakeAmount();

constructor(address _ogv, address _ogn, address _ogvStaking, address _ognStaking) {
Expand Down Expand Up @@ -127,20 +126,21 @@ contract Migrator is Governable {
uint256 newStakeAmount,
uint256 newStakeDuration
) external isSolvent {
if (lockupIds.length == 0) {
revert LockupIdsRequired();
}

// Unstake
(uint256 ogvAmountUnlocked, uint256 rewardsCollected) = ogvStaking.unstakeFrom(msg.sender, lockupIds);

if (migrateRewards) {
// Include rewards if needed
if (lockupIds.length > 0) {
// Unstake if there are any lockups
(uint256 ogvAmountUnlocked, uint256 rewardsCollected) = ogvStaking.unstakeFrom(msg.sender, lockupIds);

ogvAmountFromWallet += ogvAmountUnlocked;

if (migrateRewards) {
// Include rewards if needed
ogvAmountFromWallet += rewardsCollected;
}
} else if (migrateRewards) {
uint256 rewardsCollected = ogvStaking.collectRewardsFrom(msg.sender);
ogvAmountFromWallet += rewardsCollected;
}

ogvAmountFromWallet += ogvAmountUnlocked;

if (ognAmountFromWallet > 0) {
// Transfer in additional OGN to stake from user's wallet
ogn.transferFrom(msg.sender, address(this), ognAmountFromWallet);
Expand Down
6 changes: 6 additions & 0 deletions contracts/OgvStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,12 @@ contract OgvStaking is ERC20Votes {
_burn(staker, unstakedPoints);
}

/// @notice Collects rewards from an user
/// @param staker Address of the user
function collectRewardsFrom(address staker) external onlyMigrator returns (uint256 rewardCollected) {
rewardCollected = _collectRewards(staker);
}

/// @notice Extend a stake lockup for additional points.
///
/// The stake end time is computed from the current time + duration, just
Expand Down
3 changes: 3 additions & 0 deletions contracts/interfaces/IStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ interface IStaking {
// From OGVStaking.sol
function unstakeFrom(address staker, uint256[] memory lockupIds) external returns (uint256, uint256);

// From OGVStaking.sol
function collectRewardsFrom(address staker) external returns (uint256);

// From ExponentialStaking.sol
function stake(uint256 amountIn, uint256 duration, address to, bool stakeRewards, int256 lockupId) external;
}
96 changes: 87 additions & 9 deletions contracts/utils/GovFive.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {Vm} from "forge-std/Vm.sol";
import {Addresses} from "contracts/utils/Addresses.sol";
import "forge-std/console.sol";

import {TimelockController} from "OpenZeppelin/openzeppelin-contracts@4.6.0/contracts/governance/TimelockController.sol";
import "OpenZeppelin/openzeppelin-contracts@4.6.0/contracts/utils/Strings.sol";

library GovFive {
Expand Down Expand Up @@ -35,22 +36,99 @@ library GovFive {
prop.actions.push(GovFiveAction({receiver: receiver, fullsig: fullsig, data: data}));
}

function printTxData(GovFiveProposal storage prop) internal {
function getSafeTxData(GovFiveProposal memory prop)
internal
returns (address receiver, bytes memory payload, bytes32 opHash)
{
uint256 actionCount = prop.actions.length;

address[] memory targets = new address[](actionCount);
uint256[] memory values = new uint256[](actionCount);
bytes[] memory payloads = new bytes[](actionCount);

for (uint256 i = 0; i < prop.actions.length; i++) {
GovFiveAction memory propAction = prop.actions[i];

targets[i] = propAction.receiver;
payloads[i] =
abi.encodePacked(abi.encodePacked(bytes4(keccak256(bytes(propAction.fullsig)))), propAction.data);
}

bytes32 salt = keccak256(bytes(prop.description));

TimelockController timelock = TimelockController(payable(Addresses.TIMELOCK));
receiver = Addresses.TIMELOCK;

opHash = timelock.hashOperationBatch(targets, values, payloads, hex"", salt);

if (timelock.isOperation(opHash)) {
bytes4 executeSig = bytes4(keccak256(bytes("executeBatch(address[],uint256[],bytes[],bytes32,bytes32)")));

payload = abi.encodePacked(executeSig, abi.encode(targets, values, payloads, hex"", salt));
} else {
bytes4 scheduleSig =
bytes4(keccak256(bytes("scheduleBatch(address[],uint256[],bytes[],bytes32,bytes32,delay)")));

payload = abi.encodePacked(scheduleSig, abi.encode(targets, values, payloads, hex"", salt, 2 days));
}
}

function printTxData(GovFiveProposal memory prop) internal {
uint256 actionCount = prop.actions.length;

if (actionCount == 0) {
return;
}

(address receiver, bytes memory payload, bytes32 opHash) = getSafeTxData(prop);

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("Address:", receiver);
console.log("Data:");
console.logBytes(payload);
}

function executeWithTimelock(GovFiveProposal memory prop) internal {
address VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code"))));
Vm vm = Vm(VM_ADDRESS);

uint256 actionCount = prop.actions.length;

if (actionCount == 0) {
return;
}

console.log("### Tx", i + 1);
console.log("Address:", propAction.receiver);
console.log("Data:");
console.logBytes(abi.encodePacked(sig, propAction.data));
vm.startPrank(Addresses.GOV_MULTISIG);
(address receiver, bytes memory payload, bytes32 opHash) = getSafeTxData(prop);

TimelockController timelock = TimelockController(payable(Addresses.TIMELOCK));

if (timelock.isOperationDone(opHash)) {
return;
}

if (!timelock.isOperation(opHash)) {
console.log("Scheduling...");
(bool success, bytes memory data) = receiver.call(payload);

(receiver, payload, opHash) = getSafeTxData(prop);
}

if (!timelock.isOperationReady(opHash)) {
vm.roll(1);
vm.warp(timelock.getTimestamp(opHash) + 10);
}

console.log("Executing...");

(bool success, bytes memory data) = receiver.call(payload);

vm.stopPrank();
}

function execute(GovFiveProposal storage prop) internal {
function execute(GovFiveProposal memory 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++) {
Expand Down
4 changes: 3 additions & 1 deletion script/deploy/DeployManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {BaseMainnetScript} from "./mainnet/BaseMainnetScript.sol";
import {XOGNSetupScript} from "./mainnet/010_xOGNSetupScript.sol";
import {OgnOgvMigrationScript} from "./mainnet/011_OgnOgvMigrationScript.sol";
import {MigrationZapperScript} from "./mainnet/012_MigrationZapperScript.sol";
import {XOGNGovernanceScript} from "./mainnet/013_xOGNGovernanceScript.sol";
import {UpgradeMigratorScript} from "./mainnet/013_UpgradeMigratorScript.sol";
import {XOGNGovernanceScript} from "./mainnet/014_xOGNGovernanceScript.sol";

import "contracts/utils/VmHelper.sol";

Expand Down Expand Up @@ -64,6 +65,7 @@ contract DeployManager is Script {
_runDeployFile(new XOGNSetupScript());
_runDeployFile(new OgnOgvMigrationScript());
_runDeployFile(new MigrationZapperScript());
_runDeployFile(new UpgradeMigratorScript());
_runDeployFile(new XOGNGovernanceScript());
}

Expand Down
2 changes: 0 additions & 2 deletions script/deploy/mainnet/011_OgnOgvMigrationScript.sol
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ contract OgnOgvMigrationScript is BaseMainnetScript {

console.log("- Migrator init");
migratorProxy.initialize(address(migratorImpl), Addresses.TIMELOCK, "");

// _buildGovernanceProposal();
}

function _buildGovernanceProposal() internal override {
Expand Down
65 changes: 65 additions & 0 deletions script/deploy/mainnet/013_UpgradeMigratorScript.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// 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 {Timelock} from "contracts/Timelock.sol";
import {Governance} from "contracts/Governance.sol";

import {GovFive} from "contracts/utils/GovFive.sol";

import {VmHelper} from "utils/VmHelper.sol";

import {Migrator} from "contracts/Migrator.sol";
import {OgvStaking} from "contracts/OgvStaking.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 UpgradeMigratorScript is BaseMainnetScript {
using GovFive for GovFive.GovFiveProposal;
using VmHelper for Vm;

GovFive.GovFiveProposal public govProposal;

string public constant override DEPLOY_NAME = "013_UpgradeMigrator";

constructor() {}

function _execute() internal override {
console.log("Deploy:");
console.log("------------");

// Deploy veOGV implementation
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, deployedContracts["MIGRATOR"]
);
_recordDeploy("VEOGV_IMPL", address(veOgvImpl));

// Deploy migrator implementation
Migrator migratorImpl = new Migrator(Addresses.OGV, Addresses.OGN, Addresses.VEOGV, deployedContracts["XOGN"]);
_recordDeploy("MIGRATOR_IMPL", address(migratorImpl));
}

function _buildGovernanceProposal() internal override {
govProposal.action(
deployedContracts["MIGRATOR"], "upgradeTo(address)", abi.encode(deployedContracts["MIGRATOR_IMPL"])
);

govProposal.action(
deployedContracts["VEOGV"], "upgradeTo(address)", abi.encode(deployedContracts["VEOGV_IMPL"])
);
}

function _fork() internal override {
// Simulate proposal
govProposal.printTxData();
govProposal.executeWithTimelock();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ contract XOGNGovernanceScript is BaseMainnetScript {

GovFive.GovFiveProposal public govProposal;

string public constant override DEPLOY_NAME = "013_xOGNGovernance";
string public constant override DEPLOY_NAME = "014_xOGNGovernance";

uint256 public constant OGN_EPOCH = 1717041600; // May 30, 2024 GMT

Expand All @@ -38,8 +38,6 @@ contract XOGNGovernanceScript is BaseMainnetScript {
Governance governance = new Governance(ERC20Votes(xOgnProxy), TimelockController(payable(Addresses.TIMELOCK)));

_recordDeploy("XOGN_GOV", address(governance));

_buildGovernanceProposal();
}

function _buildGovernanceProposal() internal override {
Expand Down
2 changes: 2 additions & 0 deletions tests/governance/XOGNGovernanceForkTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ contract XOGNGovernanceForkTest is Test {
vm.startPrank(bob);
ogn.approve(address(xogn), 1e70);
vm.stopPrank();

vm.warp(OGN_EPOCH + 100 days);
}

function testGovernanceName() external view {
Expand Down
75 changes: 65 additions & 10 deletions tests/staking/Migrator.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -272,16 +272,6 @@ contract MigratorTest is Test {
vm.stopPrank();
}

function testMigrateRevertOnEmptyLockups() public {
vm.startPrank(alice);
uint256[] memory lockupIds = new uint256[](0);

vm.expectRevert(bytes4(keccak256("LockupIdsRequired()")));
migrator.migrate(lockupIds, 500 ether, 0, false, 9000 ether, 300 days);

vm.stopPrank();
}

function testMigrateWithRewards() public {
vm.startPrank(alice);
ogvStaking.mockStake(10000 ether, 365 days);
Expand Down Expand Up @@ -400,4 +390,69 @@ contract MigratorTest is Test {

vm.stopPrank();
}

function testMigrateOnlyRewards() public {
vm.startPrank(alice);
ogvStaking.mockStake(10000 ether, 365 days);
ogvStaking.mockStake(1000 ether, 20 days);

uint256[] memory lockupIds = new uint256[](0);

// Arbitrary reward
ogvStaking.setRewardShare(2 * 1e11);
uint256 expectedRewards = ogvStaking.previewRewards(alice);
uint256 expectedOgn = (expectedRewards * 0.09137 ether) / 1 ether;

uint256 balanceBefore = ogn.balanceOf(alice);

migrator.migrate(
lockupIds,
0,
0,
true, // Include reward
0,
300 days
);

vm.expectRevert();
(uint128 amount, uint128 end, uint256 points) = ognStaking.lockups(alice, 0);

assertEq(ogvStaking.previewRewards(alice), 0, "Rewards not claimed");
assertEq(ogn.balanceOf(alice), balanceBefore + expectedOgn, "Rewards not migrated");

vm.stopPrank();
}

function testMigrateOnlyRewardsToLockup() public {
vm.startPrank(alice);
ogvStaking.mockStake(10000 ether, 365 days);
ogvStaking.mockStake(1000 ether, 20 days);

uint256[] memory lockupIds = new uint256[](0);

// Arbitrary reward
ogvStaking.setRewardShare(2 * 1e11);
uint256 expectedRewards = ogvStaking.previewRewards(alice);
uint256 stakeAmount = (expectedRewards * 0.09137 ether) / 1 ether;

migrator.migrate(
lockupIds,
0,
0,
true, // Include reward
stakeAmount,
300 days
);

// Should have merged it in a single OGN lockup
(uint128 amount, uint128 end, uint256 points) = ognStaking.lockups(alice, 0);
assertEq(amount, stakeAmount, "Lockup not migrated");

vm.expectRevert();
(amount, end, points) = ognStaking.lockups(alice, 1);

assertEq(ogvStaking.previewRewards(alice), 0, "Rewards not claimed");

vm.stopPrank();
}
}
Loading

0 comments on commit db9d7ba

Please sign in to comment.