Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MigrationZapper #432

Merged
merged 6 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions contracts/MigrationZapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;

import {IStaking} from "./interfaces/IStaking.sol";
import {IMigrator} from "./interfaces/IMigrator.sol";
import {IMintableERC20} from "./interfaces/IMintableERC20.sol";

contract MigrationZapper {
IMintableERC20 public immutable ogv;
IMintableERC20 public immutable ogn;

IMigrator public immutable migrator;
IStaking public immutable ognStaking;

address public immutable governor;

error NotGovernor();

constructor(address _ogv, address _ogn, address _migrator, address _ognStaking, address _governor) {
ogv = IMintableERC20(_ogv);
ogn = IMintableERC20(_ogn);
migrator = IMigrator(_migrator);
ognStaking = IStaking(_ognStaking);

governor = _governor;
}

function initialize() external {
Copy link
Contributor

@DanielVF DanielVF May 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fortunately, initialize can't do any damage, so it's okay if it's called multiple times, but this should have an initializer modifier.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, if no proxy, this initialize code could move into constructor.

// Migrator can move OGV and OGN from this contract
ogv.approve(address(migrator), type(uint256).max);
ogn.approve(address(ognStaking), type(uint256).max);
}

/**
* @notice Migrates the specified amount of OGV to OGN.
* @param ogvAmount Amount of OGV to migrate
*/
function migrate(uint256 ogvAmount) external {
// Take tokens in
ogv.transferFrom(msg.sender, address(this), ogvAmount);

// Proxy migrate call
uint256 ognReceived = migrator.migrate(ogvAmount);

// Transfer OGN to the receiver
ogn.transfer(msg.sender, ognReceived);
}

/**
* @notice Migrates the specified amount of OGV to OGN
* and stakes it.
* @param ogvAmount Amount of OGV to migrate
*/
function migrate(uint256 ogvAmount, uint256 newStakeAmount, uint256 newStakeDuration) external {
// Take tokens in
ogv.transferFrom(msg.sender, address(this), ogvAmount);

// Migrate
uint256 ognReceived = migrator.migrate(ogvAmount);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could add a:

require(ognReceived >= newStakeAmount, "Not enough OGN received");

Afaik ogn/ogv conversion rates should be locked, and such check should only be triggered by faulty math. Though it is still nice to have an understandable error message

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, we were just running into this error (DApp was sending a higher amount due to a rounding issue)

// Stake on behalf of user
ognStaking.stake(
newStakeAmount,
Copy link

@pandadefi pandadefi May 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • The ognReceived might be greater than the newStakeAmount, the difference should be sent back to msg.sender.
  • If some ogn are "staying" on the contract, anyone could stake it on his behalf using a really small amount of ogv to migrate and a larger stake amount to swipe the balance of the contract.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes. Good catch

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fixed as well

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great.

newStakeDuration,
msg.sender,
false,
-1 // New stake
);

// Transfer remaining OGN to the receiver
if (ognReceived > newStakeAmount) {
ogn.transfer(msg.sender, ognReceived - newStakeAmount);
}
}

/**
* Transfers any tokens sent by mistake out of the contract
* @param token Token address
* @param amount Amount of token to transfer
*/
function transferTokens(address token, uint256 amount) external {
if (msg.sender != governor) {
revert NotGovernor();
}

IMintableERC20(token).transfer(governor, amount);
Copy link
Contributor

@DanielVF DanielVF May 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nobody should send us USDC, but a generic token transfer method should usually use safeMath, since this call will fail on USDC, anything else that does not return a boolean on transfer.

}
}
10 changes: 1 addition & 9 deletions contracts/Migrator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,7 @@ pragma solidity 0.8.10;
import "OpenZeppelin/openzeppelin-contracts@4.6.0/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "./Governable.sol";

interface IStaking {
function delegates(address staker) external view returns (address);

// From OGVStaking.sol
function unstakeFrom(address staker, uint256[] memory lockupIds) external returns (uint256, uint256);

// From ExponentialStaking.sol
function stake(uint256 amountIn, uint256 duration, address to, bool stakeRewards, int256 lockupId) external;
}
import {IStaking} from "./interfaces/IStaking.sol";

contract Migrator is Governable {
ERC20Burnable public immutable ogv;
Expand Down
15 changes: 15 additions & 0 deletions contracts/interfaces/IMigrator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;

interface IMigrator {
function migrate(uint256 ogvAmount) external returns (uint256);

function migrate(
uint256[] calldata lockupIds,
uint256 ogvAmountFromWallet,
uint256 ognAmountFromWallet,
bool migrateRewards,
uint256 newStakeAmount,
uint256 newStakeDuration
) external;
}
1 change: 1 addition & 0 deletions contracts/interfaces/IMintableERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ interface IMintableERC20 {
function balanceOf(address owner) external view returns (uint256);
function totalSupply() external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address _from, address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 allowance) external;
}
12 changes: 12 additions & 0 deletions contracts/interfaces/IStaking.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;

interface IStaking {
function delegates(address staker) external view returns (address);

// From OGVStaking.sol
function unstakeFrom(address staker, uint256[] memory lockupIds) external returns (uint256, uint256);

// From ExponentialStaking.sol
function stake(uint256 amountIn, uint256 duration, address to, bool stakeRewards, int256 lockupId) external;
}
4 changes: 3 additions & 1 deletion script/deploy/DeployManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ 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";
import {MigrationZapperScript} from "./mainnet/012_MigrationZapperScript.sol";
import {XOGNGovernanceScript} from "./mainnet/013_xOGNGovernanceScript.sol";

import "contracts/utils/VmHelper.sol";

Expand Down Expand Up @@ -62,6 +63,7 @@ contract DeployManager is Script {
// TODO: Use vm.readDir to recursively build this?
_runDeployFile(new XOGNSetupScript());
_runDeployFile(new OgnOgvMigrationScript());
_runDeployFile(new MigrationZapperScript());
_runDeployFile(new XOGNGovernanceScript());
}

Expand Down
48 changes: 48 additions & 0 deletions script/deploy/mainnet/012_MigrationZapperScript.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// 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 {MigrationZapper} from "contracts/MigrationZapper.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 MigrationZapperScript is BaseMainnetScript {
using GovFive for GovFive.GovFiveProposal;
using VmHelper for Vm;

GovFive.GovFiveProposal public govProposal;

string public constant override DEPLOY_NAME = "012_MigrationZapper";

constructor() {}

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

MigrationZapper zapper = new MigrationZapper(
Addresses.OGV, Addresses.OGN, deployedContracts["MIGRATOR"], deployedContracts["XOGN"], Addresses.TIMELOCK
);
_recordDeploy("MIGRATION_ZAPPER", address(zapper));

// Make sure Migrator can move OGV and OGN
zapper.initialize();
}

function _buildGovernanceProposal() internal override {}

function _fork() internal override {}
}
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 = "012_xOGNGovernance";
string public constant override DEPLOY_NAME = "013_xOGNGovernance";

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

Expand Down
121 changes: 121 additions & 0 deletions tests/staking/ZapperForkTest.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// 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 {MigrationZapper} from "contracts/MigrationZapper.sol";
import {OgvStaking} from "contracts/OgvStaking.sol";
import {ExponentialStaking} from "contracts/ExponentialStaking.sol";

import {IMintableERC20} from "contracts/interfaces/IMintableERC20.sol";

contract ZapperForkTest is Test {
DeployManager public deployManager;

Migrator public migrator;
MigrationZapper public zapper;
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"));
zapper = MigrationZapper(deployManager.getDeployment("MIGRATION_ZAPPER"));

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);
ogv.approve(address(zapper), 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
zapper.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 testMigrateBalanceAndStake() public {
vm.startPrank(ogvWhale);

uint256[] memory lockupIds = new uint256[](0);
uint256 stakeAmount = (100 ether * 0.09137 ether) / 1 ether;

zapper.migrate(100 ether, stakeAmount, 300 days);

// Should have it in a single OGN lockup
(uint128 amount, uint128 end, uint256 points) = xogn.lockups(ogvWhale, 0);
assertEq(amount, stakeAmount, "Balance not staked");

vm.stopPrank();
}

function testMigrateBalanceAndPartialStake() public {
vm.startPrank(ogvWhale);

uint256[] memory lockupIds = new uint256[](0);
uint256 stakeAmount = (50 ether * 0.09137 ether) / 1 ether;

uint256 ognBalanceBefore = ogn.balanceOf(ogvWhale);

zapper.migrate(100 ether, stakeAmount, 300 days);

// Should have it in a single OGN lockup
(uint128 amount, uint128 end, uint256 points) = xogn.lockups(ogvWhale, 0);
assertEq(amount, stakeAmount, "Balance not staked");

assertEq(ogn.balanceOf(ogvWhale), ognBalanceBefore + stakeAmount, "Less OGN received");

vm.stopPrank();
}

function testTransferTokens() public {
vm.startPrank(ogvWhale);
ogn.transfer(address(zapper), 100 ether);
vm.stopPrank();

vm.startPrank(Addresses.TIMELOCK);
zapper.transferTokens(address(ogn), 100 ether);
vm.stopPrank();

vm.startPrank(ogvWhale);
vm.expectRevert(bytes4(keccak256("NotGovernor()")));
zapper.transferTokens(address(ogn), 100 ether);
vm.stopPrank();
}
}
Loading