UNPKG

@airdao/astra-cl-periphery

Version:

🎚 Peripheral smart contracts for interacting with AstraDEX Concentrated Liquidity version

445 lines (383 loc) • 15.8 kB
import { Fixture } from 'ethereum-waffle' import { constants, Contract, Wallet } from 'ethers' import { ethers, waffle } from 'hardhat' import { IAstraPair, IAstraCLFactory, ISAMB, MockTimeNonfungiblePositionManager, TestERC20, CLMigrator, } from '../typechain' import completeFixture from './shared/completeFixture' import { ClassicFactoryFixture } from './shared/externalFixtures' import { abi as PAIR_CLASSIC_ABI } from './contracts/AstraPair.json' import { expect } from 'chai' import { FeeAmount } from './shared/constants' import { encodePriceSqrt } from './shared/encodePriceSqrt' import snapshotGasCost from './shared/snapshotGasCost' import { sortedTokens } from './shared/tokenSort' import { getMaxTick, getMinTick } from './shared/ticks' describe('CLMigrator', () => { let wallet: Wallet const migratorFixture: Fixture<{ factoryClassic: Contract factoryCL: IAstraCLFactory token: TestERC20 samb: ISAMB nft: MockTimeNonfungiblePositionManager migrator: CLMigrator }> = async (wallets, provider) => { const { factory, tokens, nft, samb } = await completeFixture(wallets, provider) const { factory: factoryClassic } = await ClassicFactoryFixture(wallets, provider) const token = tokens[0] await token.approve(factoryClassic.address, constants.MaxUint256) await samb.deposit({ value: 10000 }) await samb.approve(nft.address, constants.MaxUint256) // deploy the migrator const migrator = (await ( await ethers.getContractFactory('CLMigrator') ).deploy(factory.address, samb.address, nft.address)) as CLMigrator return { factoryClassic, factoryCL: factory, token, samb, nft, migrator, } } let factoryClassic: Contract let factoryCL: IAstraCLFactory let token: TestERC20 let samb: ISAMB let nft: MockTimeNonfungiblePositionManager let migrator: CLMigrator let pair: IAstraPair let loadFixture: ReturnType<typeof waffle.createFixtureLoader> const expectedLiquidity = 10000 - 1000 before('create fixture loader', async () => { const wallets = await (ethers as any).getSigners() wallet = wallets[0] loadFixture = waffle.createFixtureLoader(wallets) }) beforeEach('load fixture', async () => { ;({ factoryClassic, factoryCL, token, samb, nft, migrator } = await loadFixture(migratorFixture)) }) beforeEach('add Classic liquidity', async () => { await factoryClassic.createPair(token.address, samb.address) const pairAddress = await factoryClassic.getPair(token.address, samb.address) pair = new ethers.Contract(pairAddress, PAIR_CLASSIC_ABI, wallet) as IAstraPair await token.transfer(pair.address, 10000) await samb.transfer(pair.address, 10000) await pair.mint(wallet.address) expect(await pair.balanceOf(wallet.address)).to.be.eq(expectedLiquidity) }) afterEach('ensure allowances are cleared', async () => { const allowanceToken = await token.allowance(migrator.address, nft.address) const allowanceSAMB = await samb.allowance(migrator.address, nft.address) expect(allowanceToken).to.be.eq(0) expect(allowanceSAMB).to.be.eq(0) }) afterEach('ensure balances are cleared', async () => { const balanceToken = await token.balanceOf(migrator.address) const balanceSAMB = await samb.balanceOf(migrator.address) expect(balanceToken).to.be.eq(0) expect(balanceSAMB).to.be.eq(0) }) afterEach('ensure eth balance is cleared', async () => { const balanceAMB = await ethers.provider.getBalance(migrator.address) expect(balanceAMB).to.be.eq(0) }) describe('#migrate', () => { let tokenLower: boolean beforeEach(() => { tokenLower = token.address.toLowerCase() < samb.address.toLowerCase() }) it('fails if CL pool is not initialized', async () => { await pair.approve(migrator.address, expectedLiquidity) await expect( migrator.migrate({ pair: pair.address, liquidityToMigrate: expectedLiquidity, percentageToMigrate: 100, token0: tokenLower ? token.address : samb.address, token1: tokenLower ? samb.address : token.address, fee: FeeAmount.MEDIUM, tickLower: -1, tickUpper: 1, amount0Min: 9000, amount1Min: 9000, recipient: wallet.address, deadline: 1, refundAsAMB: false, }) ).to.be.reverted }) it('works once CL pool is initialized', async () => { const [token0, token1] = sortedTokens(samb, token) await migrator.createAndInitializePoolIfNecessary( token0.address, token1.address, FeeAmount.MEDIUM, encodePriceSqrt(1, 1) ) await pair.approve(migrator.address, expectedLiquidity) await migrator.migrate({ pair: pair.address, liquidityToMigrate: expectedLiquidity, percentageToMigrate: 100, token0: tokenLower ? token.address : samb.address, token1: tokenLower ? samb.address : token.address, fee: FeeAmount.MEDIUM, tickLower: getMinTick(FeeAmount.MEDIUM), tickUpper: getMaxTick(FeeAmount.MEDIUM), amount0Min: 9000, amount1Min: 9000, recipient: wallet.address, deadline: 1, refundAsAMB: false, }) const position = await nft.positions(1) expect(position.liquidity).to.be.eq(9000) const poolAddress = await factoryCL.getPool(token.address, samb.address, FeeAmount.MEDIUM) expect(await token.balanceOf(poolAddress)).to.be.eq(9000) expect(await samb.balanceOf(poolAddress)).to.be.eq(9000) }) it('works for partial', async () => { const [token0, token1] = sortedTokens(samb, token) await migrator.createAndInitializePoolIfNecessary( token0.address, token1.address, FeeAmount.MEDIUM, encodePriceSqrt(1, 1) ) const tokenBalanceBefore = await token.balanceOf(wallet.address) const sambBalanceBefore = await samb.balanceOf(wallet.address) await pair.approve(migrator.address, expectedLiquidity) await migrator.migrate({ pair: pair.address, liquidityToMigrate: expectedLiquidity, percentageToMigrate: 50, token0: tokenLower ? token.address : samb.address, token1: tokenLower ? samb.address : token.address, fee: FeeAmount.MEDIUM, tickLower: getMinTick(FeeAmount.MEDIUM), tickUpper: getMaxTick(FeeAmount.MEDIUM), amount0Min: 4500, amount1Min: 4500, recipient: wallet.address, deadline: 1, refundAsAMB: false, }) const tokenBalanceAfter = await token.balanceOf(wallet.address) const sambBalanceAfter = await samb.balanceOf(wallet.address) expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(4500) expect(sambBalanceAfter.sub(sambBalanceBefore)).to.be.eq(4500) const position = await nft.positions(1) expect(position.liquidity).to.be.eq(4500) const poolAddress = await factoryCL.getPool(token.address, samb.address, FeeAmount.MEDIUM) expect(await token.balanceOf(poolAddress)).to.be.eq(4500) expect(await samb.balanceOf(poolAddress)).to.be.eq(4500) }) it('double the price', async () => { const [token0, token1] = sortedTokens(samb, token) await migrator.createAndInitializePoolIfNecessary( token0.address, token1.address, FeeAmount.MEDIUM, encodePriceSqrt(2, 1) ) const tokenBalanceBefore = await token.balanceOf(wallet.address) const sambBalanceBefore = await samb.balanceOf(wallet.address) await pair.approve(migrator.address, expectedLiquidity) await migrator.migrate({ pair: pair.address, liquidityToMigrate: expectedLiquidity, percentageToMigrate: 100, token0: tokenLower ? token.address : samb.address, token1: tokenLower ? samb.address : token.address, fee: FeeAmount.MEDIUM, tickLower: getMinTick(FeeAmount.MEDIUM), tickUpper: getMaxTick(FeeAmount.MEDIUM), amount0Min: 4500, amount1Min: 8999, recipient: wallet.address, deadline: 1, refundAsAMB: false, }) const tokenBalanceAfter = await token.balanceOf(wallet.address) const sambBalanceAfter = await samb.balanceOf(wallet.address) const position = await nft.positions(1) expect(position.liquidity).to.be.eq(6363) const poolAddress = await factoryCL.getPool(token.address, samb.address, FeeAmount.MEDIUM) if (token.address.toLowerCase() < samb.address.toLowerCase()) { expect(await token.balanceOf(poolAddress)).to.be.eq(4500) expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(4500) expect(await samb.balanceOf(poolAddress)).to.be.eq(8999) expect(sambBalanceAfter.sub(sambBalanceBefore)).to.be.eq(1) } else { expect(await token.balanceOf(poolAddress)).to.be.eq(8999) expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(1) expect(await samb.balanceOf(poolAddress)).to.be.eq(4500) expect(sambBalanceAfter.sub(sambBalanceBefore)).to.be.eq(4500) } }) it('half the price', async () => { const [token0, token1] = sortedTokens(samb, token) await migrator.createAndInitializePoolIfNecessary( token0.address, token1.address, FeeAmount.MEDIUM, encodePriceSqrt(1, 2) ) const tokenBalanceBefore = await token.balanceOf(wallet.address) const sambBalanceBefore = await samb.balanceOf(wallet.address) await pair.approve(migrator.address, expectedLiquidity) await migrator.migrate({ pair: pair.address, liquidityToMigrate: expectedLiquidity, percentageToMigrate: 100, token0: tokenLower ? token.address : samb.address, token1: tokenLower ? samb.address : token.address, fee: FeeAmount.MEDIUM, tickLower: getMinTick(FeeAmount.MEDIUM), tickUpper: getMaxTick(FeeAmount.MEDIUM), amount0Min: 8999, amount1Min: 4500, recipient: wallet.address, deadline: 1, refundAsAMB: false, }) const tokenBalanceAfter = await token.balanceOf(wallet.address) const sambBalanceAfter = await samb.balanceOf(wallet.address) const position = await nft.positions(1) expect(position.liquidity).to.be.eq(6363) const poolAddress = await factoryCL.getPool(token.address, samb.address, FeeAmount.MEDIUM) if (token.address.toLowerCase() < samb.address.toLowerCase()) { expect(await token.balanceOf(poolAddress)).to.be.eq(8999) expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(1) expect(await samb.balanceOf(poolAddress)).to.be.eq(4500) expect(sambBalanceAfter.sub(sambBalanceBefore)).to.be.eq(4500) } else { expect(await token.balanceOf(poolAddress)).to.be.eq(4500) expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(4500) expect(await samb.balanceOf(poolAddress)).to.be.eq(8999) expect(sambBalanceAfter.sub(sambBalanceBefore)).to.be.eq(1) } }) it('double the price - as AMB', async () => { const [token0, token1] = sortedTokens(samb, token) await migrator.createAndInitializePoolIfNecessary( token0.address, token1.address, FeeAmount.MEDIUM, encodePriceSqrt(2, 1) ) const tokenBalanceBefore = await token.balanceOf(wallet.address) await pair.approve(migrator.address, expectedLiquidity) await expect( migrator.migrate({ pair: pair.address, liquidityToMigrate: expectedLiquidity, percentageToMigrate: 100, token0: tokenLower ? token.address : samb.address, token1: tokenLower ? samb.address : token.address, fee: FeeAmount.MEDIUM, tickLower: getMinTick(FeeAmount.MEDIUM), tickUpper: getMaxTick(FeeAmount.MEDIUM), amount0Min: 4500, amount1Min: 8999, recipient: wallet.address, deadline: 1, refundAsAMB: true, }) ) .to.emit(samb, 'Withdrawal') .withArgs(migrator.address, tokenLower ? 1 : 4500) const tokenBalanceAfter = await token.balanceOf(wallet.address) const position = await nft.positions(1) expect(position.liquidity).to.be.eq(6363) const poolAddress = await factoryCL.getPool(token.address, samb.address, FeeAmount.MEDIUM) if (tokenLower) { expect(await token.balanceOf(poolAddress)).to.be.eq(4500) expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(4500) expect(await samb.balanceOf(poolAddress)).to.be.eq(8999) } else { expect(await token.balanceOf(poolAddress)).to.be.eq(8999) expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(1) expect(await samb.balanceOf(poolAddress)).to.be.eq(4500) } }) it('half the price - as AMB', async () => { const [token0, token1] = sortedTokens(samb, token) await migrator.createAndInitializePoolIfNecessary( token0.address, token1.address, FeeAmount.MEDIUM, encodePriceSqrt(1, 2) ) const tokenBalanceBefore = await token.balanceOf(wallet.address) await pair.approve(migrator.address, expectedLiquidity) await expect( migrator.migrate({ pair: pair.address, liquidityToMigrate: expectedLiquidity, percentageToMigrate: 100, token0: tokenLower ? token.address : samb.address, token1: tokenLower ? samb.address : token.address, fee: FeeAmount.MEDIUM, tickLower: getMinTick(FeeAmount.MEDIUM), tickUpper: getMaxTick(FeeAmount.MEDIUM), amount0Min: 8999, amount1Min: 4500, recipient: wallet.address, deadline: 1, refundAsAMB: true, }) ) .to.emit(samb, 'Withdrawal') .withArgs(migrator.address, tokenLower ? 4500 : 1) const tokenBalanceAfter = await token.balanceOf(wallet.address) const position = await nft.positions(1) expect(position.liquidity).to.be.eq(6363) const poolAddress = await factoryCL.getPool(token.address, samb.address, FeeAmount.MEDIUM) if (tokenLower) { expect(await token.balanceOf(poolAddress)).to.be.eq(8999) expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(1) expect(await samb.balanceOf(poolAddress)).to.be.eq(4500) } else { expect(await token.balanceOf(poolAddress)).to.be.eq(4500) expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(4500) expect(await samb.balanceOf(poolAddress)).to.be.eq(8999) } }) it('gas', async () => { const [token0, token1] = sortedTokens(samb, token) await migrator.createAndInitializePoolIfNecessary( token0.address, token1.address, FeeAmount.MEDIUM, encodePriceSqrt(1, 1) ) await pair.approve(migrator.address, expectedLiquidity) await snapshotGasCost( migrator.migrate({ pair: pair.address, liquidityToMigrate: expectedLiquidity, percentageToMigrate: 100, token0: tokenLower ? token.address : samb.address, token1: tokenLower ? samb.address : token.address, fee: FeeAmount.MEDIUM, tickLower: getMinTick(FeeAmount.MEDIUM), tickUpper: getMaxTick(FeeAmount.MEDIUM), amount0Min: 9000, amount1Min: 9000, recipient: wallet.address, deadline: 1, refundAsAMB: false, }) ) }) }) })