-
Notifications
You must be signed in to change notification settings - Fork 4
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
Add MigrationZapper #432
Changes from 4 commits
5565133
d385dc3
3eae9e9
882f297
5b940c3
0b12f75
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 { | ||
// 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); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we could add a:
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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, yes. Good catch There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is fixed as well There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
} | ||
} |
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; | ||
} |
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; | ||
} |
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 |
---|---|---|
@@ -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(); | ||
} | ||
} |
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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.