-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
341 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
# pragma version 0.3.10 | ||
# pragma optimize gas | ||
# pragma evm-version cancun | ||
|
||
from vyper.interfaces import ERC20 | ||
|
||
interface Token: | ||
def mint(_account: address, _amount: uint256): nonpayable | ||
def burn(_account: address, _amount: uint256): nonpayable | ||
|
||
interface Pool: | ||
def killed() -> bool: view | ||
def paused() -> bool: view | ||
def unpause(): nonpayable | ||
def set_management(_management: address): nonpayable | ||
def accept_management(): nonpayable | ||
|
||
def supply() -> uint256: view | ||
def assets(_i: uint256) -> address: view | ||
def update_rates(_assets: DynArray[uint256, 32]): nonpayable | ||
def add_liquidity(_amounts: DynArray[uint256, 32], _min_lp: uint256) -> uint256: nonpayable | ||
def remove_liquidity(_lp: uint256, _min: DynArray[uint256, 32]): nonpayable | ||
|
||
interface MevEth: | ||
def withdrawQueue(_amount: uint256, _receiver: address, _owner: address) -> uint256: nonpayable | ||
|
||
token: public(immutable(address)) | ||
old: public(immutable(Pool)) | ||
new: public(immutable(Pool)) | ||
meveth: public(immutable(MevEth)) | ||
|
||
management: public(address) | ||
pending_management: public(address) | ||
operator: public(address) | ||
debt: public(uint256) | ||
|
||
@external | ||
def __init__(_token: address, _old: address, _new: address, _meveth: address): | ||
token = _token | ||
old = Pool(_old) | ||
new = Pool(_new) | ||
meveth = MevEth(_meveth) | ||
self.management = msg.sender | ||
self.operator = msg.sender | ||
|
||
@external | ||
def migrate(): | ||
assert msg.sender == self.operator | ||
assert old.killed() and new.paused() | ||
|
||
supply: uint256 = old.supply() | ||
assert supply > 0 | ||
|
||
# unpause new pool | ||
new.accept_management() | ||
new.unpause() | ||
|
||
# mint yETH | ||
Token(token).mint(self, supply) | ||
self.debt += supply | ||
|
||
# withdraw LSTs from old pool | ||
old.remove_liquidity(supply, [0, 0, 0, 0, 0, 0, 0, 0]) | ||
|
||
# deposit LSTs in new pool | ||
amounts: DynArray[uint256, 32] = [] | ||
for i in range(7): | ||
asset: ERC20 = ERC20(new.assets(i)) | ||
assert asset.approve(new.address, max_value(uint256), default_return_value=True) | ||
amounts.append(asset.balanceOf(self)) | ||
new.add_liquidity(amounts, 0) | ||
|
||
@external | ||
def repay(_amount: uint256): | ||
assert msg.sender == self.operator | ||
|
||
# burn yETH | ||
self.debt -= _amount | ||
Token(token).burn(self, _amount) | ||
|
||
@external | ||
def withdraw(_amount: uint256): | ||
assert msg.sender == self.operator | ||
|
||
# queue mevETH withdrawal | ||
meveth.withdrawQueue(_amount, self.management, self) | ||
|
||
@external | ||
def rescue(_token: address, _amount: uint256): | ||
assert msg.sender == self.management | ||
|
||
if _token == token: | ||
# can only withdraw excessive yETH | ||
assert self.debt == 0 | ||
|
||
assert ERC20(_token).transfer(msg.sender, _amount, default_return_value=True) | ||
|
||
@external | ||
def transfer_pool_management(_management: address): | ||
assert msg.sender == self.management | ||
new.set_management(_management) | ||
|
||
@external | ||
def set_operator(_operator: address): | ||
assert msg.sender == self.management | ||
self.operator = _operator | ||
|
||
@external | ||
def set_management(_management: address): | ||
assert msg.sender == self.management | ||
self.pending_management = _management | ||
|
||
@external | ||
def accept_management(): | ||
assert msg.sender == self.pending_management | ||
self.pending_management = empty(address) | ||
self.management = msg.sender |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
# pragma version 0.3.10 | ||
# pragma optimize gas | ||
# pragma evm-version cancun | ||
|
||
from vyper.interfaces import ERC4626 | ||
|
||
interface CoinbaseToken: | ||
def exchangeRate() -> uint256: view | ||
|
||
interface LidoToken: | ||
def getPooledEthByShares(_shares: uint256) -> uint256: view | ||
|
||
struct StaderExchangeRate: | ||
block_number: uint256 | ||
eth_balance: uint256 | ||
ethx_supply: uint256 | ||
|
||
interface StaderOracle: | ||
def getExchangeRate() -> StaderExchangeRate: view | ||
|
||
interface SwellToken: | ||
def swETHToETHRate() -> uint256: view | ||
|
||
interface RocketPoolBalances: | ||
def getTotalRETHSupply() -> uint256: view | ||
def getTotalETHBalance() -> uint256: view | ||
|
||
interface RocketPoolStorage(): | ||
def getAddress(_key: bytes32) -> RocketPoolBalances: view | ||
|
||
COINBASE_ASSET: constant(address) = 0xBe9895146f7AF43049ca1c1AE358B0541Ea49704 # cbETH | ||
FRAX_ASSET: constant(address) = 0xac3E018457B222d93114458476f3E3416Abbe38F # sfrxETH | ||
LIDO_ASSET: constant(address) = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0 # wstETH | ||
STADER_ASSET: constant(address) = 0xA35b1B31Ce002FBF2058D22F30f95D405200A15b # ETHx | ||
SWELL_ASSET: constant(address) = 0xf951E335afb289353dc249e82926178EaC7DEd78 # swETH | ||
ROCKET_POOL_ASSET: constant(address) = 0xae78736Cd615f374D3085123A210448E74Fc6393 # rETH | ||
PIREX_ASSET: constant(address) = 0x9Ba021B0a9b958B5E75cE9f6dff97C7eE52cb3E6 # apxETH | ||
|
||
LIDO_UDERLYING: constant(address) = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84 # stETH | ||
STADER_ORACLE: constant(address) = 0xF64bAe65f6f2a5277571143A24FaaFDFC0C2a737 | ||
ROCKET_POOL_STORAGE: constant(address) = 0x1d8f8f00cfa6758d7bE78336684788Fb0ee0Fa46 | ||
UNIT: constant(uint256) = 1_000_000_000_000_000_000 | ||
|
||
@external | ||
@view | ||
def rate(_asset: address) -> uint256: | ||
if _asset == COINBASE_ASSET: | ||
return CoinbaseToken(COINBASE_ASSET).exchangeRate() | ||
if _asset == FRAX_ASSET: | ||
return ERC4626(FRAX_ASSET).convertToAssets(UNIT) | ||
if _asset == LIDO_ASSET: | ||
return LidoToken(LIDO_UDERLYING).getPooledEthByShares(UNIT) | ||
if _asset == STADER_ASSET: | ||
res: StaderExchangeRate = StaderOracle(STADER_ORACLE).getExchangeRate() | ||
return res.eth_balance * UNIT / res.ethx_supply | ||
if _asset == SWELL_ASSET: | ||
return SwellToken(SWELL_ASSET).swETHToETHRate() | ||
if _asset == ROCKET_POOL_ASSET: | ||
balances: RocketPoolBalances = RocketPoolStorage(ROCKET_POOL_STORAGE).getAddress( | ||
keccak256('contract.addressrocketNetworkBalances') | ||
) | ||
return balances.getTotalETHBalance() * UNIT / balances.getTotalRETHSupply() | ||
if _asset == PIREX_ASSET: | ||
return ERC4626(PIREX_ASSET).convertToAssets(UNIT) | ||
raise |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
from ape import Contract, reverts | ||
from pytest import fixture | ||
|
||
TOKEN = '0x1BED97CBC3c24A4fb5C069C6E311a967386131f7' | ||
OLD_POOL = '0x2cced4ffA804ADbe1269cDFc22D7904471aBdE63' | ||
NEW_POOL = '0x0Ca1bd1301191576Bea9b9afCFD4649dD1Ba6822' | ||
MEVETH = '0x24Ae2dA0f361AA4BE46b48EB19C91e02c5e4f27E' | ||
YCHAD = '0xFEB4acf3df3cDEA7399794D0869ef76A6EfAff52' | ||
|
||
NUM_OLD_ASSETS = 8 | ||
MEVETH_IDX = 5 | ||
UNIT = 10**18 | ||
|
||
@fixture | ||
def token(): | ||
return Contract(TOKEN) | ||
|
||
@fixture | ||
def old(): | ||
return Contract(OLD_POOL) | ||
|
||
@fixture | ||
def new(): | ||
return Contract(NEW_POOL) | ||
|
||
@fixture | ||
def meveth(): | ||
return Contract(MEVETH) | ||
|
||
@fixture | ||
def ychad(accounts): | ||
return accounts[YCHAD] | ||
|
||
@fixture | ||
def governance(networks, accounts, old): | ||
account = accounts[old.management()] | ||
networks.active_provider.set_balance(account.address, UNIT) | ||
return account | ||
|
||
@fixture | ||
def management(networks, accounts, new): | ||
account = accounts[new.management()] | ||
networks.active_provider.set_balance(account.address, UNIT) | ||
return account | ||
|
||
@fixture | ||
def operator(accounts): | ||
return accounts[0] | ||
|
||
@fixture | ||
def alice(accounts): | ||
return accounts[1] | ||
|
||
@fixture | ||
def migrate(project, governance, management, operator, ychad, token, old, new, meveth): | ||
migrate = project.Migrate.deploy(token, old, new, meveth, sender=management) | ||
migrate.set_operator(operator, sender=management) | ||
new.set_management(migrate, sender=management) | ||
old.pause(sender=governance) | ||
old.kill(sender=governance) | ||
token.set_minter(new, sender=ychad) | ||
token.set_minter(migrate, sender=ychad) | ||
return migrate | ||
|
||
def test_migrate(management, operator, old, new, migrate, token, meveth): | ||
yeth_amt = old.supply() | ||
assert yeth_amt > 0 and new.supply() == 0 | ||
assert migrate.debt() == 0 | ||
assert meveth.balanceOf(migrate) == 0 | ||
assert token.balanceOf(migrate) == 0 | ||
|
||
balances = [] | ||
for i in range(NUM_OLD_ASSETS): | ||
if i == MEVETH_IDX: | ||
continue | ||
asset = Contract(old.assets(i)) | ||
balances.append(asset.balanceOf(old)) | ||
|
||
# migrate | ||
migrate.migrate(sender=operator) | ||
|
||
migrate_yeth_amt = token.balanceOf(migrate) | ||
meveth_amt = meveth.balanceOf(migrate) | ||
|
||
assert old.supply() == 0 and new.supply() > 2684 * UNIT | ||
assert migrate.debt() == yeth_amt | ||
assert meveth_amt > 587 * UNIT | ||
assert migrate_yeth_amt > 2684 * UNIT | ||
|
||
dust = 2000 | ||
for i in range(NUM_OLD_ASSETS-1): | ||
asset = Contract(new.assets(i)) | ||
assert asset.balanceOf(old) < dust | ||
assert asset.balanceOf(new) > balances[i] - dust | ||
|
||
# repay debt | ||
migrate.repay(migrate_yeth_amt, sender=operator) | ||
assert token.balanceOf(migrate) == 0 | ||
assert migrate.debt() < 620 * UNIT | ||
|
||
# queue withdrawal | ||
meveth_underlying_amt = meveth.convertToAssets(meveth_amt) * 10_000 // 10_001 | ||
length = meveth.queueLength() | ||
migrate.withdraw(meveth_underlying_amt, sender=operator) | ||
assert meveth.balanceOf(migrate) <= 2 | ||
assert meveth.queueLength() == length + 1 | ||
assert meveth.withdrawalQueue(length + 1)['receiver'] == management | ||
|
||
def test_operator_permissions(migrate, operator, alice): | ||
with reverts(): | ||
migrate.migrate(sender=alice) | ||
migrate.migrate(sender=operator) | ||
|
||
with reverts(): | ||
migrate.repay(UNIT, sender=alice) | ||
migrate.repay(UNIT, sender=operator) | ||
|
||
with reverts(): | ||
migrate.withdraw(UNIT, sender=alice) | ||
migrate.withdraw(UNIT, sender=operator) | ||
|
||
def test_management_permissions(migrate, new, meveth, management, operator, alice): | ||
migrate.migrate(sender=operator) | ||
|
||
with reverts(): | ||
migrate.rescue(meveth, UNIT, sender=alice) | ||
migrate.rescue(meveth, UNIT, sender=management) | ||
assert meveth.balanceOf(management) == UNIT | ||
|
||
with reverts(): | ||
migrate.transfer_pool_management(alice, sender=alice) | ||
migrate.transfer_pool_management(alice, sender=management) | ||
assert new.pending_management() == alice | ||
|
||
with reverts(): | ||
migrate.set_operator(alice, sender=alice) | ||
migrate.set_operator(alice, sender=management) | ||
assert migrate.operator() == alice | ||
|
||
with reverts(): | ||
migrate.set_management(alice, sender=alice) | ||
with reverts(): | ||
migrate.accept_management(sender=alice) | ||
migrate.set_management(alice, sender=management) | ||
assert migrate.pending_management() == alice | ||
migrate.accept_management(sender=alice) | ||
assert migrate.management() == alice | ||
|
||
def test_rate_provider(project, deployer, old): | ||
provider = project.NewRateProvider.deploy(sender=deployer) | ||
|
||
for i in range(NUM_OLD_ASSETS): | ||
if i == MEVETH_IDX: | ||
continue | ||
asset = old.assets(i) | ||
old_provider = Contract(old.rate_providers(i)) | ||
old_rate = old_provider.rate(asset) | ||
new_rate = provider.rate(asset) | ||
assert new_rate > 0 and new_rate == old_rate |