Skip to content

Commit

Permalink
Add migration contract
Browse files Browse the repository at this point in the history
  • Loading branch information
0xkorin committed Nov 29, 2024
1 parent 2d9da21 commit 89027e3
Show file tree
Hide file tree
Showing 3 changed files with 341 additions and 0 deletions.
117 changes: 117 additions & 0 deletions contracts/Migrate.vy
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
65 changes: 65 additions & 0 deletions contracts/NewRateProvider.vy
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
159 changes: 159 additions & 0 deletions tests/test_migrate.py
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

0 comments on commit 89027e3

Please sign in to comment.