diff --git a/contracts/collections/tickmap.ral b/contracts/collections/tickmap.ral index 310cd2a..8ae29a0 100644 --- a/contracts/collections/tickmap.ral +++ b/contracts/collections/tickmap.ral @@ -168,6 +168,7 @@ Abstract Contract Tickmap() extends Decimal(), BatchHelper() { } @using(checkExternalCaller = false) + pub fn getCloserLimit(sqrtPriceLimit: U256, xToY: Bool, currentTick: I256, tickSpacing: U256, poolKey: PoolKey) -> (U256, Bool, I256, Bool) { let mut closesTickBool = false let mut closesTickIndex = 0i @@ -184,11 +185,10 @@ Abstract Contract Tickmap() extends Decimal(), BatchHelper() { if (closesTickBool) { let sqrtPriceExist = calculateSqrtPrice(closesTickIndex) - if ((xToY && sqrtPriceExist > sqrtPriceLimit) || (!xToY && sqrtPriceExist < sqrtPriceLimit)) { return sqrtPriceExist, true, closesTickIndex, true } else { - return sqrtPriceExist, false, 0i, false + return sqrtPriceLimit, false, 0i, false } } else { let index = getSearchLimit(currentTick, tickSpacing, !xToY) @@ -199,11 +199,13 @@ Abstract Contract Tickmap() extends Decimal(), BatchHelper() { if ((xToY && sqrtPriceNotExist > sqrtPriceLimit) || (!xToY && sqrtPriceNotExist < sqrtPriceLimit)) { return sqrtPriceNotExist, true, index, false } else { - return sqrtPriceNotExist, false, 0i, false + return sqrtPriceLimit, false, 0i, false } } } + + fn getChunk(chunk: U256, poolKey: PoolKey) -> U256 { let key = poolKeyBytes(poolKey) ++ toByteVec!(getKey(chunk)) let exists = bitmap.contains!(key) diff --git a/src/snippets.ts b/src/snippets.ts index d9dc505..0925efe 100644 --- a/src/snippets.ts +++ b/src/snippets.ts @@ -1,6 +1,7 @@ import { Address, SignerProvider } from '@alephium/web3' import { InvariantInstance, TokenFaucetInstance } from '../artifacts/ts' -import { MinSqrtPrice, PercentageScale } from './consts' +import { MinSqrtPrice, MaxSqrtPrice, PercentageScale } from './consts' +import { PoolKey } from '../artifacts/ts/types' import { getPool, getReserveBalances, @@ -11,7 +12,8 @@ import { initTokensXY, transferPosition, verifyPositionList, - withdrawTokens + withdrawTokens, + quote } from './testUtils' import { balanceOf, deployInvariant, newFeeTier, newPoolKey } from './utils' import { PrivateKeyWallet } from '@alephium/web3-wallet' @@ -135,6 +137,25 @@ export const initBasicSwap = async ( return tx } +export const swapExactLimit = async ( + invariant: InvariantInstance, + signer: SignerProvider, + poolKey: PoolKey, + xToY: boolean, + amount: bigint, + byAmountIn: boolean +) => { + const sqrtPriceLimit: bigint = xToY ? MinSqrtPrice : MaxSqrtPrice + + const quoteResult = await quote(invariant, poolKey, xToY, amount, byAmountIn, sqrtPriceLimit) + + await initSwap(invariant, signer, poolKey, xToY, amount, byAmountIn, quoteResult.targetSqrtPrice) + + const poolAfter = await getPool(invariant, poolKey) + + expect(poolAfter.sqrtPrice).toBe(quoteResult.targetSqrtPrice) +} + export const transferAndVerifyPosition = async ( invariant: InvariantInstance, owner: PrivateKeyWallet, diff --git a/test/contract/e2e/max-tick-cross.test.ts b/test/contract/e2e/max-tick-cross.test.ts index 804b6b8..44062ae 100644 --- a/test/contract/e2e/max-tick-cross.test.ts +++ b/test/contract/e2e/max-tick-cross.test.ts @@ -50,7 +50,7 @@ describe('max tick cross spec', () => { test('max tick cross swap xToY and ByAmountIn, no liquidity gap between positions', async () => { const lastInitializedTick = -250n - const amount = 40300n + const amount = 40282n const xToY = true const slippage = MinSqrtPrice const byAmountIn = true @@ -95,7 +95,7 @@ describe('max tick cross spec', () => { }, 100000) test('max tick cross swap yToX and ByAmountIn, no liquidity gap between positions', async () => { const lastInitializedTick = 120n - const amount = 45000n + const amount = 44998n const xToY = false const slippage = MaxSqrtPrice const byAmountIn = true @@ -120,9 +120,10 @@ describe('max tick cross spec', () => { await withdrawTokens(swapper, [tokenY, amount]) + const poolBefore = await getPool(invariant, poolKey) + const { targetSqrtPrice } = await quote(invariant, poolKey, xToY, amount, byAmountIn, slippage) - const poolBefore = await getPool(invariant, poolKey) const { gasAmount } = await initSwap( invariant, swapper, @@ -229,7 +230,7 @@ describe('max tick cross spec', () => { }, 100000) test('max tick cross swap xToY and ByAmountIn, positions between search limit range', async () => { const lastInitializedTick = -35000n - const amount = 13570000n + const amount = 13569916n const xToY = true const slippage = MinSqrtPrice const byAmountIn = true @@ -254,9 +255,8 @@ describe('max tick cross spec', () => { await withdrawTokens(swapper, [tokenX, amount]) - const { targetSqrtPrice } = await quote(invariant, poolKey, xToY, amount, byAmountIn, slippage) - const poolBefore = await getPool(invariant, poolKey) + const { gasAmount } = await initSwap( invariant, swapper, @@ -264,7 +264,7 @@ describe('max tick cross spec', () => { xToY, amount, byAmountIn, - targetSqrtPrice + slippage ) const poolAfter = await getPool(invariant, poolKey) const crosses = (poolAfter.currentTickIndex - poolBefore.currentTickIndex) / -searchLimit @@ -273,7 +273,7 @@ describe('max tick cross spec', () => { }, 100000) test('max tick cross swap yToX and ByAmountIn, positions between search limit range', async () => { const lastInitializedTick = 25000n - const amount = 17947900n + const amount = 17947500n const xToY = false const slippage = MaxSqrtPrice const byAmountIn = true @@ -298,8 +298,6 @@ describe('max tick cross spec', () => { await withdrawTokens(swapper, [tokenY, amount]) - const { targetSqrtPrice } = await quote(invariant, poolKey, xToY, amount, byAmountIn, slippage) - const poolBefore = await getPool(invariant, poolKey) const { gasAmount } = await initSwap( invariant, @@ -308,7 +306,7 @@ describe('max tick cross spec', () => { xToY, amount, byAmountIn, - targetSqrtPrice + slippage ) const poolAfter = await getPool(invariant, poolKey) diff --git a/test/contract/e2e/slippage.test.ts b/test/contract/e2e/slippage.test.ts new file mode 100644 index 0000000..70f3e92 --- /dev/null +++ b/test/contract/e2e/slippage.test.ts @@ -0,0 +1,146 @@ +import { ONE_ALPH, web3 } from '@alephium/web3' +import { getSigner } from '@alephium/web3-test' +import { PrivateKeyWallet } from '@alephium/web3-wallet' +import { TokenFaucetInstance } from '../../../artifacts/ts/' +import { InvariantInstance } from '../../../artifacts/ts' +import { deployInvariant, newFeeTier, newPoolKey } from '../../../src/utils' +import { + getBasicFeeTickSpacing, + initBasicPool, + initBasicPosition, + swapExactLimit +} from '../../../src/snippets' +import { + initFeeTier, + initSwap, + quote, + expectError, + withdrawTokens, + initPosition, + getPool, + initTokensXY, + initPool +} from '../../../src/testUtils' +import { InvariantError, MaxSqrtPrice } from '../../../src/consts' +import { calculateSqrtPrice, toLiquidity } from '../../../src/math' +import { PoolKey } from '../../../artifacts/ts/types' + +web3.setCurrentNodeProvider('http://127.0.0.1:22973') +let admin: PrivateKeyWallet +let positionOwner: PrivateKeyWallet +let invariant: InvariantInstance +let tokenX: TokenFaucetInstance +let tokenY: TokenFaucetInstance +let poolKey: PoolKey + +describe('Invariant Swap Tests', () => { + const swapAmount = 10n ** 8n + const [fee, tickSpacing] = getBasicFeeTickSpacing() + + const withdrawAmount = 10n ** 10n + + beforeAll(async () => { + admin = await getSigner(ONE_ALPH * 1000n, 0) + }) + + beforeEach(async () => { + invariant = await deployInvariant(admin, 10n ** 10n) + const feeTier = await newFeeTier(fee, tickSpacing) + await initFeeTier(invariant, admin, feeTier) + + const tokenSupply = 10n ** 23n + ;[tokenX, tokenY] = await initTokensXY(admin, tokenSupply) + positionOwner = await getSigner(ONE_ALPH * 1000n, 0) + await withdrawTokens(positionOwner, [tokenX, withdrawAmount], [tokenY, withdrawAmount]) + + const initTick = 0n + const initSqrtPrice = await calculateSqrtPrice(initTick) + + poolKey = await newPoolKey(tokenX.address, tokenY.address, feeTier) + await initPool(invariant, positionOwner, tokenX, tokenY, feeTier, initSqrtPrice, initTick) + + const [lowerTick, upperTick] = [-1000n, 1000n] + + const liquidityDelta = toLiquidity(10_000_000_000n) + + const poolBefore = await getPool(invariant, poolKey) + + const slippageLimitLower = poolBefore.sqrtPrice + const slippageLimitUpper = poolBefore.sqrtPrice + + await initPosition( + invariant, + positionOwner, + poolKey, + withdrawAmount, + withdrawAmount, + lowerTick, + upperTick, + liquidityDelta, + slippageLimitLower, + slippageLimitUpper + ) + + expect(await getPool(invariant, poolKey)).toMatchObject({ + liquidity: liquidityDelta, + poolKey, + currentTickIndex: 0n + }) + }) + + test('test_basic_slippage', async () => { + const swapper = await getSigner(ONE_ALPH * 1000n, 0) + + const swapAmount = 10n ** 8n + await withdrawTokens(swapper, [tokenY, swapAmount]) + + const targetSqrtPrice = 1009940000000000000000001n + await initSwap(invariant, swapper, poolKey, false, swapAmount, true, targetSqrtPrice) + + let expectedSqrtPrice = 1009940000000000000000000n + + const pool = await getPool(invariant, poolKey) + + expect(pool.sqrtPrice).toBe(expectedSqrtPrice) + }) + + test('test_swap_close_to_limit', async () => { + const feeTier = await newFeeTier(fee, tickSpacing) + + const swapper = await getSigner(ONE_ALPH * 1000n, 0) + await withdrawTokens(swapper, [tokenX, withdrawAmount], [tokenY, withdrawAmount]) + const poolKey = await newPoolKey(tokenX.address, tokenY.address, feeTier) + + const quoteResult = await quote(invariant, poolKey, false, swapAmount, true, MaxSqrtPrice) + + const targetSqrtPrice = quoteResult.targetSqrtPrice - 1n + + await expectError( + InvariantError.PriceLimitReached, + initSwap(invariant, positionOwner, poolKey, false, swapAmount, true, targetSqrtPrice), + invariant + ) + }) + + test('test_swap_exact_limit', async () => { + invariant = await deployInvariant(admin, 10n ** 10n) + const tokenSupply = 10n ** 23n + ;[tokenX, tokenY] = await initTokensXY(admin, tokenSupply) + + const feeTier = await newFeeTier(fee, tickSpacing) + await initFeeTier(invariant, admin, feeTier) + + await initBasicPool(invariant, admin, tokenX, tokenY) + + await initBasicPosition(invariant, positionOwner, tokenX, tokenY) + + const swapAmount = 1000n + const swapper = await getSigner(ONE_ALPH * 1000n, 0) + + await withdrawTokens(swapper, [tokenX, swapAmount]) + + const poolKey = await newPoolKey(tokenX.address, tokenY.address, feeTier) + + await swapExactLimit(invariant, swapper, poolKey, true, swapAmount, true) + }) +})