From 5de2155a63ae97e5bc5c81a839c779b60fadbb5c Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Mon, 1 Mar 2021 19:28:59 +0400 Subject: [PATCH 1/2] feat: migrator --- contracts/PoolMigrator.vy | 44 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 contracts/PoolMigrator.vy diff --git a/contracts/PoolMigrator.vy b/contracts/PoolMigrator.vy new file mode 100644 index 0000000..5c12f7a --- /dev/null +++ b/contracts/PoolMigrator.vy @@ -0,0 +1,44 @@ +# @version 0.2.11 +""" +@title Pool Migrator +@author Curve.fi +@notice Zap for moving liquidity between Curve factory pools in a single transaction +@license MIT +""" + +interface ERC20: + def approve(_spender: address, _amount: uint256): nonpayable + +interface Swap: + def transferFrom(_owner: address, _spender: address, _amount: uint256) -> bool: nonpayable + def add_liquidity(_amounts: uint256[2], _min_mint_amount: uint256, _receiver: address) -> uint256: nonpayable + def remove_liquidity(_burn_amount: uint256, _min_amounts: uint256[2]) -> uint256[2]: nonpayable + def coins(i: uint256) -> address: view + + +# pool -> coins are approved? +is_approved: HashMap[address, bool] + + +@external +def migrate_to_new_pool(_old_pool: address, _new_pool: address, _amount: uint256) -> uint256: + """ + @notice Migrate liquidity between two pools + @dev Each pool must be deployed by the curve factory and contain identical + assets. The migrator must have approval to transfer `_old_pool` tokens + on behalf of the caller. + @param _old_pool Address of the pool to migrate from + @param _new_pool Address of the pool to migrate into + @param _amount Number of `_old_pool` LP tokens to migrate + @return uint256 Number of `_new_pool` LP tokens received + """ + Swap(_old_pool).transferFrom(msg.sender, self, _amount) + amounts: uint256[2] = Swap(_old_pool).remove_liquidity(_amount, [0, 0]) + + if not self.is_approved[_new_pool]: + for i in range(2): + coin: address = Swap(_new_pool).coins(i) + ERC20(coin).approve(_new_pool, MAX_UINT256) + self.is_approved[_new_pool] = True + + return Swap(_new_pool).add_liquidity(amounts, 0, msg.sender) From 380dfae56d857d56a6f6b213e0e103e3b656b2d5 Mon Sep 17 00:00:00 2001 From: Ben Hauser Date: Mon, 1 Mar 2021 19:29:05 +0400 Subject: [PATCH 2/2] tests: migrator --- tests/test_migrator.py | 78 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 tests/test_migrator.py diff --git a/tests/test_migrator.py b/tests/test_migrator.py new file mode 100644 index 0000000..f5909b1 --- /dev/null +++ b/tests/test_migrator.py @@ -0,0 +1,78 @@ +import brownie +import pytest + +pytestmark = pytest.mark.usefixtures("add_initial_liquidity", "mint_bob", "approve_bob") + + +@pytest.fixture(scope="module") +def swap2(MetaImplementationUSD, alice, base_pool, factory, coin): + tx = factory.deploy_metapool( + base_pool, "Test Swap", "TST", coin, 200, 4000000, {"from": alice} + ) + yield MetaImplementationUSD.at(tx.return_value) + + +@pytest.fixture(scope="module") +def migrator(PoolMigrator, alice, swap): + contract = PoolMigrator.deploy({"from": alice}) + swap.approve(contract, 2 ** 256 - 1, {"from": alice}) + yield contract + + +def test_migrate(alice, migrator, swap, swap2): + balance = swap.balanceOf(alice) + migrator.migrate_to_new_pool(swap, swap2, balance, {"from": alice}) + + assert swap.balanceOf(alice) == 0 + assert swap2.balanceOf(alice) == balance + + assert swap.balanceOf(migrator) == 0 + assert swap2.balanceOf(migrator) == 0 + + +def test_migrate_partial(alice, migrator, swap, swap2, coin): + balance = swap.balanceOf(alice) + migrator.migrate_to_new_pool(swap, swap2, balance // 4, {"from": alice}) + + assert swap.balanceOf(alice) == balance - balance // 4 + assert abs(swap2.balanceOf(alice) - balance // 4) < 8 + (10 ** 18 - coin.decimals()) + + assert swap.balanceOf(migrator) == 0 + assert swap2.balanceOf(migrator) == 0 + + +def test_migrate_multiple(alice, migrator, swap, swap2, coin): + balance = swap.balanceOf(alice) + for i in range(4): + migrator.migrate_to_new_pool(swap, swap2, balance // 4, {"from": alice}) + + assert swap.balanceOf(alice) < 5 + assert abs(swap2.balanceOf(alice) - balance) < 8 + (10 ** 18 - coin.decimals()) + + assert swap.balanceOf(migrator) == 0 + assert swap2.balanceOf(migrator) == 0 + + +def test_migrate_bidirectional(alice, migrator, swap, swap2): + balance = swap.balanceOf(alice) + migrator.migrate_to_new_pool(swap, swap2, balance, {"from": alice}) + swap2.approve(migrator, 2 ** 256 - 1, {"from": alice}) + migrator.migrate_to_new_pool(swap2, swap, balance, {"from": alice}) + + assert abs(swap.balanceOf(alice) - balance) < 4 + assert swap2.balanceOf(alice) == 0 + + assert swap.balanceOf(migrator) == 0 + assert swap2.balanceOf(migrator) == 0 + + +def test_migration_wrong_pool(alice, migrator, swap, swap_btc): + balance = swap.balanceOf(alice) + with brownie.reverts(): + migrator.migrate_to_new_pool(swap, swap_btc, balance, {"from": alice}) + + +def test_insufficient_balance(alice, migrator, swap, swap2): + balance = swap.balanceOf(alice) + with brownie.reverts(): + migrator.migrate_to_new_pool(swap, swap2, balance + 1, {"from": alice})