diff --git a/scripts/package-lock.json b/scripts/package-lock.json index 2f5424b5..64168032 100644 --- a/scripts/package-lock.json +++ b/scripts/package-lock.json @@ -18,13 +18,14 @@ }, "../sdk": { "name": "@invariant-labs/a0-sdk", - "version": "0.2.19", + "version": "0.2.21", "license": "ISC", "dependencies": { - "@invariant-labs/a0-sdk-wasm": "^0.1.23", + "@invariant-labs/a0-sdk-wasm": "0.1.24", "@polkadot/api": "^10.12.4", "@polkadot/api-contract": "^10.12.4", "@scio-labs/use-inkathon": "^0.6.3", + "mocha": "^10.7.3", "ts-node": "^10.9.2", "typescript": "^5.3.3" }, diff --git a/scripts/src/setup.ts b/scripts/src/setup.ts index 35a822fc..265921b7 100644 --- a/scripts/src/setup.ts +++ b/scripts/src/setup.ts @@ -97,6 +97,7 @@ const main = async () => { 10 ** 24 try { const poolSqrtPrice = priceToSqrtPrice(BigInt(Math.round(price))) + console.log(poolKey) await invariant.createPool(account, poolKey, poolSqrtPrice) } catch (e) { console.log('Create pool error', poolKey, e) diff --git a/scripts/src/swap.ts b/scripts/src/swap.ts index 424da0c4..78d5546b 100644 --- a/scripts/src/swap.ts +++ b/scripts/src/swap.ts @@ -83,7 +83,7 @@ const main = async () => { ) const firstSwapEvent = firstSwapResult.events[0] as SwapEvent assert(firstSimualtion.globalInsufficientLiquidity === false) - assert(firstSimualtion.maxTicksCrossed === false) + assert(firstSimualtion.maxSwapStepsReached === false) assert(firstSimualtion.stateOutdated === false) assert(firstSimualtion.amountIn == firstSwapEvent.amountIn) assert(firstSimualtion.amountOut == firstSwapEvent.amountOut) @@ -117,7 +117,7 @@ const main = async () => { ) const secondSwapEvent = secondSwapResult.events[0] as SwapEvent assert(secondSimulation.globalInsufficientLiquidity === false) - assert(secondSimulation.maxTicksCrossed === false) + assert(secondSimulation.maxSwapStepsReached === false) assert(secondSimulation.stateOutdated === false) assert(secondSimulation.amountIn === secondSwapEvent.amountIn) assert(secondSimulation.amountOut === secondSwapEvent.amountOut) diff --git a/sdk/contracts/invariant/invariant.json b/sdk/contracts/invariant/invariant.json index c8d575c4..9375830a 100644 --- a/sdk/contracts/invariant/invariant.json +++ b/sdk/contracts/invariant/invariant.json @@ -1,6 +1,6 @@ { "source": { - "hash": "0x4f19a0152491660087fd1d38d163ab436967e96ac14a07220133afc86e2bd9ec", + "hash": "0xcc659012169a620a3cd6565f4f6c238646150a3d846fc4849c1a5718f803cda5", "language": "ink! 5.0.0", "compiler": "rustc 1.77.0", "build_info": { @@ -176,6 +176,102 @@ "module_path": "invariant::contracts::events", "signature_topic": "0x50a25822f8984babdbc09246e1d170630167a27235d98a5ff8ac7516a5cdab15" }, + { + "args": [ + { + "docs": [], + "indexed": true, + "label": "timestamp", + "type": { + "displayName": [ + "u64" + ], + "type": 9 + } + }, + { + "docs": [], + "indexed": false, + "label": "address", + "type": { + "displayName": [ + "AccountId" + ], + "type": 2 + } + }, + { + "docs": [], + "indexed": false, + "label": "pool", + "type": { + "displayName": [ + "PoolKey" + ], + "type": 16 + } + }, + { + "docs": [], + "indexed": false, + "label": "delta_liquidity", + "type": { + "displayName": [ + "Liquidity" + ], + "type": 19 + } + }, + { + "docs": [], + "indexed": false, + "label": "add_liquidity", + "type": { + "displayName": [ + "bool" + ], + "type": 34 + } + }, + { + "docs": [], + "indexed": false, + "label": "lower_tick", + "type": { + "displayName": [ + "i32" + ], + "type": 12 + } + }, + { + "docs": [], + "indexed": false, + "label": "upper_tick", + "type": { + "displayName": [ + "i32" + ], + "type": 12 + } + }, + { + "docs": [], + "indexed": false, + "label": "current_sqrt_price", + "type": { + "displayName": [ + "SqrtPrice" + ], + "type": 27 + } + } + ], + "docs": [], + "label": "ChangeLiquidityEvent", + "module_path": "invariant::contracts::events", + "signature_topic": "0x46cd3c5dbfeaa26a33c451719cec81defa409942d31339858154c409c72b6d5a" + }, { "args": [ { @@ -701,6 +797,68 @@ }, "selector": "0x0a1ca76b" }, + { + "args": [ + { + "label": "index", + "type": { + "displayName": [ + "u32" + ], + "type": 0 + } + }, + { + "label": "delta_liquidity", + "type": { + "displayName": [ + "Liquidity" + ], + "type": 19 + } + }, + { + "label": "add_liquidity", + "type": { + "displayName": [ + "bool" + ], + "type": 34 + } + }, + { + "label": "slippage_limit_lower", + "type": { + "displayName": [ + "SqrtPrice" + ], + "type": 27 + } + }, + { + "label": "slippage_limit_upper", + "type": { + "displayName": [ + "SqrtPrice" + ], + "type": 27 + } + } + ], + "default": false, + "docs": [], + "label": "InvariantTrait::change_liquidity", + "mutates": true, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 58 + }, + "selector": "0x19b443b7" + }, { "args": [ { @@ -4140,6 +4298,10 @@ { "index": 34, "name": "SetCodeHashError" + }, + { + "index": 35, + "name": "LiquidityChangeZero" } ] } diff --git a/sdk/contracts/invariant/invariant.wasm b/sdk/contracts/invariant/invariant.wasm index 957a6047..efa44caa 100644 Binary files a/sdk/contracts/invariant/invariant.wasm and b/sdk/contracts/invariant/invariant.wasm differ diff --git a/sdk/package-lock.json b/sdk/package-lock.json index 206431b9..7abc93f2 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -1,15 +1,15 @@ { "name": "@invariant-labs/a0-sdk", - "version": "0.2.20", + "version": "0.2.21", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@invariant-labs/a0-sdk", - "version": "0.2.20", + "version": "0.2.21", "license": "ISC", "dependencies": { - "@invariant-labs/a0-sdk-wasm": "file:./src/wasm/pkg", + "@invariant-labs/a0-sdk-wasm": "0.1.24", "@polkadot/api": "^10.12.4", "@polkadot/api-contract": "^10.12.4", "@scio-labs/use-inkathon": "^0.6.3", @@ -5414,8 +5414,8 @@ } }, "src/wasm/pkg": { - "name": "invariant-a0-wasm", - "version": "0.1.1" + "name": "@invariant-labs/a0-sdk-wasm", + "version": "0.1.24" }, "wasm/pkg": { "name": "invariant-a0-wasm", diff --git a/sdk/package.json b/sdk/package.json index ed71d182..34bab75d 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@invariant-labs/a0-sdk", - "version": "0.2.20", + "version": "0.2.21", "collaborators": [ "Invariant Labs" ], @@ -36,7 +36,7 @@ "docs:copy": "cp ../README.md README.md", "build:copy-wasm": "cd target && mkdir wasm && cp -r ../src/wasm/pkg ./wasm/pkg", "test:all": "ts-mocha", - "test:local": "npm run test:utils && npm run test:wazero && npm run test:psp22 && npm run test:protocol-fee && npm run test:math && npm run test:invariant && npm run test:example && npm run test:events && npm run test:tx && npm run test:position && npm run test:get-position-with-associates && npm run test:get-positions && npm run test:get-all && npm run test:query-on-pair && npm run test:get-liquidity-ticks", + "test:local": "npm run test:utils && npm run test:wazero && npm run test:psp22 && npm run test:protocol-fee && npm run test:math && npm run test:invariant && npm run test:example && npm run test:events && npm run test:tx && npm run test:position && npm run test:get-position-with-associates && npm run test:get-positions && npm run test:get-all && npm run test:query-on-pair && npm run test:change-liquidity && npm run test:get-liquidity-ticks && npm run test:update-position-seconds-per-liquidity", "test:utils": "mocha ./tests/utils.test.ts -g utils", "test:wazero": "mocha ./tests/wrapped-azero.test.ts -g wrapped-azero", "test:psp22": "mocha ./tests/psp22.test.ts -g psp22", @@ -56,10 +56,11 @@ "test:simulate-invariant-swap": "mocha ./tests/simulate-invariant-swap.test.ts -g simulateInvariantSwap", "test:query-on-pair": "mocha ./tests/query-on-pair.test.ts -g query-on-pair", "test:get-all": "mocha ./tests/get-all.test.ts -g get-all", + "test:change-liquidity": "mocha ./tests/change-liquidity.test.ts -g change-liquidity", "test:update-position-seconds-per-liquidity": "mocha ./tests/update-position-seconds-per-liquidity.test.ts -g update-position-seconds-per-liquidity" }, "dependencies": { - "@invariant-labs/a0-sdk-wasm": "file:./src/wasm/pkg", + "@invariant-labs/a0-sdk-wasm": "0.1.24", "@polkadot/api": "^10.12.4", "@polkadot/api-contract": "^10.12.4", "@scio-labs/use-inkathon": "^0.6.3", diff --git a/sdk/src/abis/invariant.ts b/sdk/src/abis/invariant.ts index a418f7f1..23a5ad49 100644 --- a/sdk/src/abis/invariant.ts +++ b/sdk/src/abis/invariant.ts @@ -1,7 +1,7 @@ export const abi = ` { "source": { - "hash": "0x4f19a0152491660087fd1d38d163ab436967e96ac14a07220133afc86e2bd9ec", + "hash": "0xcc659012169a620a3cd6565f4f6c238646150a3d846fc4849c1a5718f803cda5", "language": "ink! 5.0.0", "compiler": "rustc 1.77.0", "build_info": { @@ -177,6 +177,102 @@ export const abi = ` "module_path": "invariant::contracts::events", "signature_topic": "0x50a25822f8984babdbc09246e1d170630167a27235d98a5ff8ac7516a5cdab15" }, + { + "args": [ + { + "docs": [], + "indexed": true, + "label": "timestamp", + "type": { + "displayName": [ + "u64" + ], + "type": 9 + } + }, + { + "docs": [], + "indexed": false, + "label": "address", + "type": { + "displayName": [ + "AccountId" + ], + "type": 2 + } + }, + { + "docs": [], + "indexed": false, + "label": "pool", + "type": { + "displayName": [ + "PoolKey" + ], + "type": 16 + } + }, + { + "docs": [], + "indexed": false, + "label": "delta_liquidity", + "type": { + "displayName": [ + "Liquidity" + ], + "type": 19 + } + }, + { + "docs": [], + "indexed": false, + "label": "add_liquidity", + "type": { + "displayName": [ + "bool" + ], + "type": 34 + } + }, + { + "docs": [], + "indexed": false, + "label": "lower_tick", + "type": { + "displayName": [ + "i32" + ], + "type": 12 + } + }, + { + "docs": [], + "indexed": false, + "label": "upper_tick", + "type": { + "displayName": [ + "i32" + ], + "type": 12 + } + }, + { + "docs": [], + "indexed": false, + "label": "current_sqrt_price", + "type": { + "displayName": [ + "SqrtPrice" + ], + "type": 27 + } + } + ], + "docs": [], + "label": "ChangeLiquidityEvent", + "module_path": "invariant::contracts::events", + "signature_topic": "0x46cd3c5dbfeaa26a33c451719cec81defa409942d31339858154c409c72b6d5a" + }, { "args": [ { @@ -702,6 +798,68 @@ export const abi = ` }, "selector": "0x0a1ca76b" }, + { + "args": [ + { + "label": "index", + "type": { + "displayName": [ + "u32" + ], + "type": 0 + } + }, + { + "label": "delta_liquidity", + "type": { + "displayName": [ + "Liquidity" + ], + "type": 19 + } + }, + { + "label": "add_liquidity", + "type": { + "displayName": [ + "bool" + ], + "type": 34 + } + }, + { + "label": "slippage_limit_lower", + "type": { + "displayName": [ + "SqrtPrice" + ], + "type": 27 + } + }, + { + "label": "slippage_limit_upper", + "type": { + "displayName": [ + "SqrtPrice" + ], + "type": 27 + } + } + ], + "default": false, + "docs": [], + "label": "InvariantTrait::change_liquidity", + "mutates": true, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 58 + }, + "selector": "0x19b443b7" + }, { "args": [ { @@ -4141,6 +4299,10 @@ export const abi = ` { "index": 34, "name": "SetCodeHashError" + }, + { + "index": 35, + "name": "LiquidityChangeZero" } ] } diff --git a/sdk/src/consts.ts b/sdk/src/consts.ts index 154f86bd..e4bfe025 100644 --- a/sdk/src/consts.ts +++ b/sdk/src/consts.ts @@ -44,32 +44,32 @@ export const WAZERO_ADDRESS = { } export const INVARIANT_ADDRESS = { - [Network.Testnet]: '5CQ4E1PnyPTURTCioBqka1CEhfJ5g4wwK8dVLtw97YnLGrkV', + [Network.Testnet]: '5FrYjo3Y7CmzvLveD96F66SMYJdcBPcJh9kSxq214y4LTXdj', [Network.Mainnet]: '5CvocBcChFccUkNGZpYf1mThQQDaY7ZxXEmdTXbTLqt1SaYQ', [Network.Local]: '' } export const BTC_ADDRESS = { - [Network.Testnet]: '5E195YpqcMmBKc8nCyfZS2zvq2Fzwx5QEvi7cwwzVv34KLkq', + [Network.Testnet]: '5DHLwE9znALML2kaKbHz1X4W243ae3ijqCQjbVoVwVpsEGnp', [Network.Mainnet]: '5HW9QeCifdKt8gXwXVSE8z56njDQBhGfses1KJNFL68qius9', [Network.Local]: '' } export const ETH_ADDRESS = { - [Network.Testnet]: '5FfgLxfPM1NPoJ9FoZWuAgeeS8dQFrEmhP7axuqqygAr6SMz', + [Network.Testnet]: '5Cqosz5NxwvuudycTmphjmjBaGnUf25wnu9CSbDAR2616j7h', [Network.Mainnet]: '5EEzffpXkfYKkdtmqNh9UNctYTjmbi9GfKKAWRTKKFh6F1FU', [Network.Local]: '' } export const USDC_ADDRESS = { - [Network.Testnet]: '5GMmP8aBpq1xEeh7vjm7fNuo9kGcUGA2gLyA7YRTRP2waKFD', + [Network.Testnet]: '5HGDNuKD1knU8ZQ3zQXZCMniKiPWuN18dr2RkhPDkwifPS2R', [Network.Mainnet]: '5GDsB8Qm6CAoBi7rmM6TCKMQQUg8CiRzuH9YVyfcrwDKWoqB', [Network.Local]: '' } export const USDT_ADDRESS = { - [Network.Testnet]: '5GfpcwQmcqtXBy4NkFCQJ2udZeLSfRwUz1FESozDrNFXakHs', + [Network.Testnet]: '5DnXKCPAqTMWWqDoC8NhBo6V8SFh2mqfk3NaRfs6pbQbCZHg', [Network.Mainnet]: '5HX57YoV7h51NEKhpfXZAJk8RzLX4Uutp36S23RDMPZ424LY', [Network.Local]: '' } export const SOL_ADDRESS = { - [Network.Testnet]: '5EKM5zqpysKscQhnMYBMR7cjWEZcm2SiEUoNojNrAAEYUYMc', + [Network.Testnet]: '5Cym4cb86pGxP1NcuXqXMgUMy12Bqeat587Bi5YNQNHKfNQY', [Network.Mainnet]: '5F2xiTnahG1tFY3ZHyghh25JsjuCaamRnN7ddQjPEzwvdd3j', [Network.Local]: '' } diff --git a/sdk/src/invariant.ts b/sdk/src/invariant.ts index 8680ec36..ded3d0ba 100644 --- a/sdk/src/invariant.ts +++ b/sdk/src/invariant.ts @@ -36,6 +36,7 @@ import { } from './consts.js' import { Network } from './network.js' import { + ChangeLiquidityTxResult, ContractOptions, CreatePositionTxResult, InvariantEvent, @@ -64,7 +65,7 @@ import { } from './utils.js' type Page = { index: number; entries: [Position, Pool][] } - +export type DepositDirection = 'deposit' | 'withdraw' export class Invariant { contract: ContractPromise api: ApiPromise @@ -661,6 +662,83 @@ export class Invariant { ) as Promise } + changeLiquidityTx( + index: bigint, + deltaLiquidity: Liquidity, + isDeposit: boolean, + spotSqrtPrice: SqrtPrice, + slippageTolerance: Percentage, + options: ContractOptions = { + storageDepositLimit: this.storageDepositLimit, + refTime: this.gasLimit.refTime.toNumber(), + proofSize: this.gasLimit.proofSize.toNumber() + } + ): SubmittableExtrinsic<'promise'> { + const slippageLimitLower = calculateSqrtPriceAfterSlippage( + spotSqrtPrice, + slippageTolerance, + false + ) + const slippageLimitUpper = calculateSqrtPriceAfterSlippage( + spotSqrtPrice, + slippageTolerance, + true + ) + + return createTx( + this.contract, + this.api.registry.createType('WeightV2', { + refTime: options.refTime, + proofSize: options.proofSize + }) as WeightV2, + options.storageDepositLimit, + 0n, + InvariantTx.ChangeLiquidity, + [index, deltaLiquidity, isDeposit, slippageLimitLower, slippageLimitUpper] + ) + } + + async changeLiquidity( + account: IKeyringPair, + index: bigint, + deltaLiquidity: Liquidity, + isDeposit: boolean, + spotSqrtPrice: SqrtPrice, + slippageTolerance: Percentage, + options: ContractOptions = { + storageDepositLimit: this.storageDepositLimit, + refTime: this.gasLimit.refTime.toNumber(), + proofSize: this.gasLimit.proofSize.toNumber() + }, + block: boolean = true + ): Promise { + const slippageLimitLower = calculateSqrtPriceAfterSlippage( + spotSqrtPrice, + slippageTolerance, + false + ) + const slippageLimitUpper = calculateSqrtPriceAfterSlippage( + spotSqrtPrice, + slippageTolerance, + true + ) + + return createSignAndSendTx( + this.contract, + this.api.registry.createType('WeightV2', { + refTime: options.refTime, + proofSize: options.proofSize + }) as WeightV2, + options.storageDepositLimit, + 0n, + account, + InvariantTx.ChangeLiquidity, + [index, deltaLiquidity, isDeposit, slippageLimitLower, slippageLimitUpper], + this.waitForFinalization, + block + ) as Promise + } + transferPositionTx( index: bigint, receiver: string, diff --git a/sdk/src/schema.ts b/sdk/src/schema.ts index 917ad0f7..e7fee707 100644 --- a/sdk/src/schema.ts +++ b/sdk/src/schema.ts @@ -26,6 +26,7 @@ export enum InvariantTx { ChangeProtocolFee = `${invariantActionPrefix}changeProtocolFee`, CreatePool = `${invariantActionPrefix}createPool`, CreatePosition = `${invariantActionPrefix}createPosition`, + ChangeLiquidity = `${invariantActionPrefix}changeLiquidity`, TransferPosition = `${invariantActionPrefix}transferPosition`, RemovePosition = `${invariantActionPrefix}removePosition`, ClaimFee = `${invariantActionPrefix}claimFee`, @@ -67,6 +68,7 @@ const invariantEventPrefix = 'invariant::contracts::events::' export enum InvariantEvent { CreatePositionEvent = `${invariantEventPrefix}CreatePositionEvent`, + ChangeLiquidityEvent = `${invariantEventPrefix}ChangeLiquidityEvent`, CrossTickEvent = `${invariantEventPrefix}CrossTickEvent`, RemovePositionEvent = `${invariantEventPrefix}RemovePositionEvent`, SwapEvent = `${invariantEventPrefix}SwapEvent` @@ -81,6 +83,7 @@ export interface EventTxResult extends TxResult { } export type CreatePositionTxResult = EventTxResult +export type ChangeLiquidityTxResult = EventTxResult export type RemovePositionTxResult = EventTxResult export type SwapTxResult = EventTxResult export type SwapRouteTxResult = EventTxResult diff --git a/sdk/src/wasm/storage/events.rs b/sdk/src/wasm/storage/events.rs index f0bdb6db..3fc9865f 100644 --- a/sdk/src/wasm/storage/events.rs +++ b/sdk/src/wasm/storage/events.rs @@ -24,6 +24,23 @@ pub struct CreatePositionEvent { current_sqrt_price: SqrtPrice, } +#[derive(Default, Debug, PartialEq, Serialize, Deserialize, Tsify)] +#[serde(rename_all = "camelCase")] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct ChangeLiquidityEvent { + #[tsify(type = "bigint")] + timestamp: u64, + address: String, + pool: PoolKey, + delta_liquidity: Liquidity, + add_liquidity: bool, + #[tsify(type = "bigint")] + lower_tick: i32, + #[tsify(type = "bigint")] + upper_tick: i32, + current_sqrt_price: SqrtPrice, +} + #[derive(Default, Debug, PartialEq, Serialize, Deserialize, Tsify)] #[serde(rename_all = "camelCase")] #[tsify(into_wasm_abi, from_wasm_abi)] diff --git a/sdk/tests/change-liquidity.test.ts b/sdk/tests/change-liquidity.test.ts new file mode 100644 index 00000000..21a60e28 --- /dev/null +++ b/sdk/tests/change-liquidity.test.ts @@ -0,0 +1,106 @@ +import { Pool, Position } from '@invariant-labs/a0-sdk-wasm/invariant_a0_wasm.js' +import { Keyring } from '@polkadot/api' +import { describe, it } from 'mocha' +import { Invariant } from '../src/invariant' +import { Network } from '../src/network' +import { PSP22 } from '../src/psp22' +import { objectEquals } from '../src/testUtils' +import { initPolkadotApi, newFeeTier, newPoolKey } from '../src/utils' + +const api = await initPolkadotApi(Network.Local) + +const keyring = new Keyring({ type: 'sr25519' }) +const account = await keyring.addFromUri('//Alice') + +let invariant = await Invariant.deploy(api, Network.Local, account, 10000000000n) +let token0Address = await PSP22.deploy(api, account, 1000000000n, 'Coin', 'COIN', 0n) +let token1Address = await PSP22.deploy(api, account, 1000000000n, 'Coin', 'COIN', 0n) +const psp22 = await PSP22.load(api, Network.Local) + +const lowerTickIndex = -20n +const upperTickIndex = 10n +const feeTier = newFeeTier(6000000000n, 10n) + +let poolKey = newPoolKey(token0Address, token1Address, feeTier) +let pool: Pool + +describe('change-liquidity', async () => { + beforeEach(async () => { + invariant = await Invariant.deploy(api, Network.Local, account, 10000000000n) + token0Address = await PSP22.deploy(api, account, 1000000000n, 'Coin', 'COIN', 0n) + token1Address = await PSP22.deploy(api, account, 1000000000n, 'Coin', 'COIN', 0n) + + poolKey = newPoolKey(token0Address, token1Address, feeTier) + + await invariant.addFeeTier(account, feeTier) + + await invariant.createPool(account, poolKey, 1000000000000000000000000n) + + await psp22.approve(account, invariant.contract.address.toString(), 10000000000n, token0Address) + await psp22.approve(account, invariant.contract.address.toString(), 10000000000n, token1Address) + + pool = await invariant.getPool(token0Address, token1Address, feeTier) + + await invariant.createPosition( + account, + poolKey, + lowerTickIndex, + upperTickIndex, + 1000000000000n, + pool.sqrtPrice, + 0n + ) + }) + + it('change liquidity', async () => { + const pool = await invariant.getPool(poolKey.tokenX, poolKey.tokenY, feeTier) + { + const positionBefore = await invariant.getPosition(account.address, 0n) + await invariant.changeLiquidity(account, 0n, 1000000000000n, true, pool.sqrtPrice, 0n) + const positionAfter = await invariant.getPosition(account.address, 0n) + const expectedPosition: Position = { + poolKey: positionBefore.poolKey, + liquidity: 2000000000000n, + lowerTickIndex: positionBefore.lowerTickIndex, + upperTickIndex: positionBefore.upperTickIndex, + feeGrowthInsideX: 0n, + feeGrowthInsideY: 0n, + lastBlockNumber: 0n, + tokensOwedX: 0n, + tokensOwedY: 0n, + createdAt: 0n, + secondsPerLiquidityInside: 0n + } + + objectEquals(expectedPosition, positionAfter, [ + 'createdAt', + 'lastBlockNumber', + 'secondsPerLiquidityInside' + ]) + } + { + const positionBefore = await invariant.getPosition(account.address, 0n) + await invariant.changeLiquidity(account, 0n, 1000000000000n, false, pool.sqrtPrice, 0n) + const positionAfter = await invariant.getPosition(account.address, 0n) + const expectedPosition: Position = { + poolKey: positionBefore.poolKey, + liquidity: 1000000000000n, + lowerTickIndex: positionBefore.lowerTickIndex, + upperTickIndex: positionBefore.upperTickIndex, + feeGrowthInsideX: 0n, + feeGrowthInsideY: 0n, + lastBlockNumber: 0n, + tokensOwedX: 0n, + tokensOwedY: 0n, + createdAt: 0n, + secondsPerLiquidityInside: 0n + } + + objectEquals(expectedPosition, positionAfter, [ + 'createdAt', + 'lastBlockNumber', + 'secondsPerLiquidityInside' + ]) + } + }) +}) diff --git a/sdk/tests/events.test.ts b/sdk/tests/events.test.ts index abcf2e53..8762f7e2 100644 --- a/sdk/tests/events.test.ts +++ b/sdk/tests/events.test.ts @@ -1,6 +1,7 @@ import { CreatePositionEvent, CrossTickEvent, + ChangeLiquidityEvent, RemovePositionEvent, SwapEvent, getGlobalMinSqrtPrice, @@ -258,6 +259,63 @@ describe('events', async () => { assert.deepEqual(wasFired, true) }) + it('change liquidity event', async () => { + let wasFired = false + + await psp22.approve( + account, + invariant.contract.address.toString(), + 1000000000000n, + token0Address + ) + await psp22.approve( + account, + invariant.contract.address.toString(), + 1000000000000n, + token1Address + ) + + await invariant.createPosition( + account, + poolKey, + -10n, + 10n, + 1000000000000n, + toSqrtPrice(1n, 0n), + 0n + ) + + const expectedLiquidityChangeEvent: ChangeLiquidityEvent = { + address: account.address.toString(), + currentSqrtPrice: toSqrtPrice(1n, 0n), + deltaLiquidity: 1000000000000n, + addLiquidity: true, + lowerTick: -10n, + upperTick: 10n, + pool: poolKey, + timestamp: 0n + } + + invariant.on(InvariantEvent.ChangeLiquidityEvent, (event: any) => { + wasFired = true + + objectEquals(event, expectedLiquidityChangeEvent, ['timestamp']) + }) + + const result = await invariant.changeLiquidity( + account, + 0n, + 1000000000000n, + true, + toSqrtPrice(1n, 0), + 0n + ) + + assert.deepEqual(result.events.length, 5) + objectEquals(result.events[4], expectedLiquidityChangeEvent, ['timestamp']) + assert.deepEqual(wasFired, true) + }) + it('on and off methods', async () => { let timesFired = 0 diff --git a/sdk/tests/update-position-seconds-per-liquidity.test.ts b/sdk/tests/update-position-seconds-per-liquidity.test.ts index 381eaf9e..91e7413a 100644 --- a/sdk/tests/update-position-seconds-per-liquidity.test.ts +++ b/sdk/tests/update-position-seconds-per-liquidity.test.ts @@ -6,6 +6,7 @@ import { Invariant } from '../src/invariant' import { Network } from '../src/network' import { PSP22 } from '../src/psp22' import { delay, initPolkadotApi, newFeeTier, newPoolKey } from '../src/utils' +import { LIQUIDITY_DENOMINATOR, SECONDS_PER_LIQUIDITY_DENOMINATOR } from '../src/consts' const api = await initPolkadotApi(Network.Local) @@ -67,6 +68,7 @@ describe('update-position-seconds-per-liquidity', async () => { const poolAfter = await invariant.getPool(poolKey.tokenX, poolKey.tokenY, poolKey.feeTier) const positionAfter = await invariant.getPosition(account.address, positionIndex) + assert.notEqual(poolAfter.secondsPerLiquidityGlobal, 0n) assert.equal(poolAfter.secondsPerLiquidityGlobal, positionAfter.secondsPerLiquidityInside) }) @@ -104,4 +106,76 @@ describe('update-position-seconds-per-liquidity', async () => { assert.equal(lowerTickAfter.secondsPerLiquidityOutside, poolAfter.secondsPerLiquidityGlobal) assert.equal(positionAfter.secondsPerLiquidityInside, 0n) }) + + it('position inside liquidity changed', async function () { + this.timeout(60000) + + await invariant.createPosition( + account, + poolKey, + lowerTickIndex, + upperTickIndex, + 1000000000000n, + pool.sqrtPrice, + 0n + ) + + const positionIndex = 0n + const poolBefore = await invariant.getPool(poolKey.tokenX, poolKey.tokenY, poolKey.feeTier) + const positionBefore = await invariant.getPosition(account.address, positionIndex) + assert.equal(poolBefore.secondsPerLiquidityGlobal, 0n) + assert.equal(positionBefore.secondsPerLiquidityInside, 0n) + + console.log(positionBefore, poolBefore) + await delay(5000) + + await invariant.changeLiquidity( + account, + positionIndex, + positionBefore.liquidity * 4n, + true, + poolBefore.sqrtPrice, + 0n + ) + + const poolAfterChange = await invariant.getPool(poolKey.tokenX, poolKey.tokenY, poolKey.feeTier) + const positionAfterChange = await invariant.getPosition(account.address, positionIndex) + + assert.equal( + poolAfterChange.secondsPerLiquidityGlobal, + positionAfterChange.secondsPerLiquidityInside + ) + + const secondsPassed = + (poolAfterChange.secondsPerLiquidityGlobal * poolBefore.liquidity) / + SECONDS_PER_LIQUIDITY_DENOMINATOR / + LIQUIDITY_DENOMINATOR + + await delay(Number(secondsPassed * 1000n * 4n)) + + await invariant.updatePositionSecondsPerLiquidity(account, positionIndex) + + const poolAfterUpdate = await invariant.getPool(poolKey.tokenX, poolKey.tokenY, poolKey.feeTier) + const positionAfterUpdate = await invariant.getPosition(account.address, positionIndex) + + const precision = poolAfterChange.secondsPerLiquidityGlobal / 4n + + if ( + positionAfterChange.secondsPerLiquidityInside * 2n > + positionAfterUpdate.secondsPerLiquidityInside + precision || + poolAfterChange.secondsPerLiquidityGlobal * 2n < + positionAfterUpdate.secondsPerLiquidityInside - precision + ) { + throw new Error( + `result outside of precision range actual: ${ + positionAfterUpdate.secondsPerLiquidityInside + }, expected: ${positionAfterChange.secondsPerLiquidityInside * 2n}` + ) + } + + assert.equal( + poolAfterUpdate.secondsPerLiquidityGlobal, + positionAfterUpdate.secondsPerLiquidityInside + ) + }) }) diff --git a/src/contracts/entrypoints.rs b/src/contracts/entrypoints.rs index 6130d0a4..2adc8b58 100644 --- a/src/contracts/entrypoints.rs +++ b/src/contracts/entrypoints.rs @@ -89,6 +89,33 @@ pub trait InvariantTrait { slippage_limit_upper: SqrtPrice, ) -> Result; + /// Changes a liquidity of a position. + /// + /// # Parameters + /// - `index`: Index of the position to update + /// - `add_liquidity`: Determines whether the liquidity should be increased or decreased + /// - `delta_liquidity`: Liquidity that the position should be taken or added + /// - `slippage_limit_lower`: The price limit for downward movement to execute the position update. + /// - `slippage_limit_upper`: The price limit for upward movement to execute the position update. + /// + /// # Errors + /// - Fails if the user attempts to update a position with zero liquidity. + /// - Fails if the user attempts to update a position with liquidity that would not result in a token transfer. + /// - Fails if the price has reached the slippage limit. + /// - Fails if the allowance is insufficient or the user balance transfer fails. + /// - Fails if position does not exist + /// + /// # External contracts + /// - PSP22 + #[ink(message)] + fn change_liquidity( + &mut self, + index: u32, + delta_liquidity: Liquidity, + add_liquidity: bool, + slippage_limit_lower: SqrtPrice, + slippage_limit_upper: SqrtPrice, + ) -> Result<(), InvariantError>; /// Performs a single swap based on the provided parameters. /// /// # Parameters diff --git a/src/contracts/error.rs b/src/contracts/error.rs index 6f86c411..9e6da14e 100644 --- a/src/contracts/error.rs +++ b/src/contracts/error.rs @@ -36,4 +36,5 @@ pub enum InvariantError { DivByZero, WAZEROWithdrawError, SetCodeHashError, + LiquidityChangeZero, } diff --git a/src/contracts/events.rs b/src/contracts/events.rs index 6b7516bf..de9319c7 100644 --- a/src/contracts/events.rs +++ b/src/contracts/events.rs @@ -16,6 +16,19 @@ pub struct CreatePositionEvent { pub current_sqrt_price: SqrtPrice, } +#[ink::event] +pub struct ChangeLiquidityEvent { + #[ink(topic)] + pub timestamp: u64, + pub address: AccountId, + pub pool: PoolKey, + pub delta_liquidity: Liquidity, + pub add_liquidity: bool, + pub lower_tick: i32, + pub upper_tick: i32, + pub current_sqrt_price: SqrtPrice, +} + #[ink::event] pub struct CrossTickEvent { #[ink(topic)] diff --git a/src/contracts/storage/position.rs b/src/contracts/storage/position.rs index 12cd4f1a..42e38958 100644 --- a/src/contracts/storage/position.rs +++ b/src/contracts/storage/position.rs @@ -6,7 +6,7 @@ use crate::{ types::{ fee_growth::{calculate_fee_growth_inside, FeeGrowth}, liquidity::Liquidity, - seconds_per_liquidity::{calculate_seconds_per_liquidity_inside, SecondsPerLiquidity}, + seconds_per_liquidity::SecondsPerLiquidity, sqrt_price::SqrtPrice, token_amount::TokenAmount, }, @@ -255,25 +255,19 @@ impl Position { lower_tick: Tick, upper_tick: Tick, current_timestamp: u64, + current_block_number: u64, ) { - pool.update_seconds_per_liquidity_inside( - lower_tick.index, - lower_tick.seconds_per_liquidity_outside, - upper_tick.index, - upper_tick.seconds_per_liquidity_outside, - current_timestamp, - ) - .unwrap(); + self.seconds_per_liquidity_inside = pool + .update_seconds_per_liquidity_inside( + lower_tick.index, + lower_tick.seconds_per_liquidity_outside, + upper_tick.index, + upper_tick.seconds_per_liquidity_outside, + current_timestamp, + ) + .unwrap(); - self.seconds_per_liquidity_inside = unwrap!(calculate_seconds_per_liquidity_inside( - lower_tick.index, - upper_tick.index, - pool.current_tick_index, - lower_tick.seconds_per_liquidity_outside, - upper_tick.seconds_per_liquidity_outside, - pool.seconds_per_liquidity_global, - )); - self.last_block_number = current_timestamp; + self.last_block_number = current_block_number; } } @@ -536,7 +530,13 @@ mod tests { .unwrap(); assert_eq!(pos.seconds_per_liquidity_inside, SecondsPerLiquidity(0)); - pos.update_seconds_per_liquidity(&mut pool, lower_tick, upper_tick, current_timestamp); + pos.update_seconds_per_liquidity( + &mut pool, + lower_tick, + upper_tick, + current_timestamp, + 0, + ); assert_eq!( pos.seconds_per_liquidity_inside, @@ -563,6 +563,7 @@ mod tests { lower_tick, upper_tick, current_timestamp, + 0, ); assert_eq!( @@ -597,6 +598,7 @@ mod tests { lower_tick, upper_tick, current_timestamp + 1, + 0, ); assert_eq!( diff --git a/src/e2e/change_liquidity.rs b/src/e2e/change_liquidity.rs new file mode 100644 index 00000000..a6fff58a --- /dev/null +++ b/src/e2e/change_liquidity.rs @@ -0,0 +1,597 @@ +#[cfg(test)] +pub mod e2e_tests { + use crate::contracts::InvariantError; + use crate::invariant::Invariant; + use crate::{ + contracts::{entrypoints::InvariantTrait, FeeTier, PoolKey}, + invariant::InvariantRef, + math::types::{ + liquidity::Liquidity, + percentage::Percentage, + sqrt_price::{calculate_sqrt_price, SqrtPrice}, + }, + }; + use decimal::*; + use ink::primitives::AccountId; + use ink_e2e::ContractsBackend; + use test_helpers::{ + add_fee_tier, address_of, approve, balance_of, change_liquidity, create_dex, create_pool, + create_position, create_tokens, get_pool, get_position, get_tick, remove_position, + }; + use token::Token; + use token::TokenRef; + use token::PSP22; + + type E2EResult = Result>; + + #[ink_e2e::test] + async fn test_change_liquidity(mut client: ink_e2e::Client) -> E2EResult<()> { + let dex = create_dex!(client, Percentage::new(0)); + let (token_x, token_y) = create_tokens!(client, 500, 500); + + let alice = ink_e2e::alice(); + + let fee_tier = FeeTier::new(Percentage::new(0), 1).unwrap(); + + add_fee_tier!(client, dex, fee_tier, alice).unwrap(); + + let init_tick = 0; + let init_sqrt_price = calculate_sqrt_price(init_tick).unwrap(); + create_pool!( + client, + dex, + token_x.account_id, + token_y.account_id, + fee_tier, + init_sqrt_price, + init_tick, + alice + ) + .unwrap(); + + approve!(client, token_x, dex.account_id, 500, alice).unwrap(); + approve!(client, token_y, dex.account_id, 500, alice).unwrap(); + + let pool_key = PoolKey::new(token_x.account_id, token_y.account_id, fee_tier).unwrap(); + + create_position!( + client, + dex, + pool_key, + -10, + 10, + Liquidity::from_integer(10000), + SqrtPrice::new(0), + SqrtPrice::max_instance(), + alice + ) + .unwrap(); + + let position = get_position!(client, dex, 0, alice).unwrap(); + let pool = get_pool!( + client, + dex, + token_x.account_id, + token_y.account_id, + fee_tier + ) + .unwrap(); + let lower_tick = get_tick!(client, dex, pool_key, -10).unwrap(); + let upper_tick = get_tick!(client, dex, pool_key, 10).unwrap(); + + assert_eq!(position.liquidity, Liquidity::from_integer(10000)); + assert_eq!(pool.liquidity, Liquidity::from_integer(10000)); + assert_eq!(lower_tick.liquidity_change, Liquidity::from_integer(10000)); + assert!(lower_tick.sign); + + assert_eq!(upper_tick.liquidity_change, Liquidity::from_integer(10000)); + assert!(!upper_tick.sign); + // increase liquidity + { + let dex_balance_x_before = balance_of!(client, token_x, dex.account_id); + let dex_balance_y_before = balance_of!(client, token_y, dex.account_id); + assert_eq!(dex_balance_x_before, 5); + assert_eq!(dex_balance_y_before, 5); + + let user_balance_x_before = balance_of!(client, token_x, address_of!(Alice)); + let user_balance_y_before = balance_of!(client, token_y, address_of!(Alice)); + + change_liquidity!( + client, + dex, + 0, + Liquidity::from_integer(10000), + true, + SqrtPrice::new(0), + SqrtPrice::max_instance(), + alice + ) + .unwrap(); + let dex_balance_x = balance_of!(client, token_x, dex.account_id); + let dex_balance_y = balance_of!(client, token_y, dex.account_id); + assert_eq!(dex_balance_x, 10); + assert_eq!(dex_balance_y, 10); + let user_balance_x = balance_of!(client, token_x, address_of!(Alice)); + let user_balance_y = balance_of!(client, token_y, address_of!(Alice)); + + assert_eq!(user_balance_x_before - user_balance_x, 5); + assert_eq!(user_balance_y_before - user_balance_y, 5); + + let position = get_position!(client, dex, 0, alice).unwrap(); + let pool = get_pool!( + client, + dex, + token_x.account_id, + token_y.account_id, + fee_tier + ) + .unwrap(); + let lower_tick = get_tick!(client, dex, pool_key, -10).unwrap(); + let upper_tick = get_tick!(client, dex, pool_key, 10).unwrap(); + + assert_eq!(position.liquidity, Liquidity::from_integer(20000)); + assert_eq!(pool.liquidity, Liquidity::from_integer(20000)); + assert_eq!(lower_tick.liquidity_change, Liquidity::from_integer(20000)); + assert!(lower_tick.sign); + + assert_eq!(upper_tick.liquidity_change, Liquidity::from_integer(20000)); + assert!(!upper_tick.sign); + } + // decrease liquidity + { + let dex_balance_x_before = balance_of!(client, token_x, dex.account_id); + let dex_balance_y_before = balance_of!(client, token_y, dex.account_id); + assert_eq!(dex_balance_x_before, 10); + assert_eq!(dex_balance_y_before, 10); + + let user_balance_x_before = balance_of!(client, token_x, address_of!(Alice)); + let user_balance_y_before = balance_of!(client, token_y, address_of!(Alice)); + + change_liquidity!( + client, + dex, + 0, + Liquidity::from_integer(10000), + false, + SqrtPrice::new(0), + SqrtPrice::max_instance(), + alice + ) + .unwrap(); + let dex_balance_x = balance_of!(client, token_x, dex.account_id); + let dex_balance_y = balance_of!(client, token_y, dex.account_id); + let user_balance_x = balance_of!(client, token_x, address_of!(Alice)); + let user_balance_y = balance_of!(client, token_y, address_of!(Alice)); + + assert_eq!(dex_balance_x, 6); + assert_eq!(dex_balance_y, 6); + assert_eq!(user_balance_x - user_balance_x_before, 4); + assert_eq!(user_balance_y - user_balance_y_before, 4); + + let position = get_position!(client, dex, 0, alice).unwrap(); + let pool = get_pool!( + client, + dex, + token_x.account_id, + token_y.account_id, + fee_tier + ) + .unwrap(); + let lower_tick = get_tick!(client, dex, pool_key, -10).unwrap(); + let upper_tick = get_tick!(client, dex, pool_key, 10).unwrap(); + + assert_eq!(position.liquidity, Liquidity::from_integer(10000)); + assert_eq!(pool.liquidity, Liquidity::from_integer(10000)); + assert_eq!(lower_tick.liquidity_change, Liquidity::from_integer(10000)); + assert!(lower_tick.sign); + + assert_eq!(upper_tick.liquidity_change, Liquidity::from_integer(10000)); + assert!(!upper_tick.sign); + } + + Ok(()) + } + + #[ink_e2e::test] + async fn test_change_liquidity_amount_is_zero( + mut client: ink_e2e::Client, + ) -> E2EResult<()> { + let dex = create_dex!(client, Percentage::new(0)); + let (token_x, token_y) = create_tokens!(client, 500, 500); + + let alice = ink_e2e::alice(); + + let fee_tier = FeeTier::new(Percentage::new(0), 1).unwrap(); + + add_fee_tier!(client, dex, fee_tier, alice).unwrap(); + + let init_tick = 0; + let init_sqrt_price = calculate_sqrt_price(init_tick).unwrap(); + create_pool!( + client, + dex, + token_x.account_id, + token_y.account_id, + fee_tier, + init_sqrt_price, + init_tick, + alice + ) + .unwrap(); + + approve!(client, token_x, dex.account_id, 500, alice).unwrap(); + approve!(client, token_y, dex.account_id, 500, alice).unwrap(); + + let pool_key = PoolKey::new(token_x.account_id, token_y.account_id, fee_tier).unwrap(); + + create_position!( + client, + dex, + pool_key, + -10, + 10, + Liquidity::from_integer(10000), + SqrtPrice::new(0), + SqrtPrice::max_instance(), + alice + ) + .unwrap(); + + let position = get_position!(client, dex, 0, alice).unwrap(); + let pool = get_pool!( + client, + dex, + token_x.account_id, + token_y.account_id, + fee_tier + ) + .unwrap(); + let lower_tick = get_tick!(client, dex, pool_key, -10).unwrap(); + let upper_tick = get_tick!(client, dex, pool_key, 10).unwrap(); + + assert_eq!(position.liquidity, Liquidity::from_integer(10000)); + assert_eq!(pool.liquidity, Liquidity::from_integer(10000)); + assert_eq!(lower_tick.liquidity_change, Liquidity::from_integer(10000)); + assert!(lower_tick.sign); + + assert_eq!(upper_tick.liquidity_change, Liquidity::from_integer(10000)); + assert!(!upper_tick.sign); + + let result = change_liquidity!( + client, + dex, + 0, + Liquidity::new(1), + false, + SqrtPrice::new(0), + SqrtPrice::max_instance(), + alice + ); + assert_eq!(result, Err(InvariantError::AmountIsZero)); + + Ok(()) + } + + #[ink_e2e::test] + async fn test_change_liquidity_and_remove_position( + mut client: ink_e2e::Client, + ) -> E2EResult<()> { + let dex = create_dex!(client, Percentage::new(0)); + let (token_x, token_y) = create_tokens!(client, 500, 500); + + let alice = ink_e2e::alice(); + + let fee_tier = FeeTier::new(Percentage::new(0), 1).unwrap(); + + add_fee_tier!(client, dex, fee_tier, alice).unwrap(); + + let init_tick = 0; + let init_sqrt_price = calculate_sqrt_price(init_tick).unwrap(); + create_pool!( + client, + dex, + token_x.account_id, + token_y.account_id, + fee_tier, + init_sqrt_price, + init_tick, + alice + ) + .unwrap(); + + approve!(client, token_x, dex.account_id, 500, alice).unwrap(); + approve!(client, token_y, dex.account_id, 500, alice).unwrap(); + + let pool_key = PoolKey::new(token_x.account_id, token_y.account_id, fee_tier).unwrap(); + + create_position!( + client, + dex, + pool_key, + -10, + 10, + Liquidity::from_integer(10000), + SqrtPrice::new(0), + SqrtPrice::max_instance(), + alice + ) + .unwrap(); + + let position = get_position!(client, dex, 0, alice).unwrap(); + let pool = get_pool!( + client, + dex, + token_x.account_id, + token_y.account_id, + fee_tier + ) + .unwrap(); + let lower_tick = get_tick!(client, dex, pool_key, -10).unwrap(); + let upper_tick = get_tick!(client, dex, pool_key, 10).unwrap(); + + assert_eq!(position.liquidity, Liquidity::from_integer(10000)); + assert_eq!(pool.liquidity, Liquidity::from_integer(10000)); + assert_eq!(lower_tick.liquidity_change, Liquidity::from_integer(10000)); + assert!(lower_tick.sign); + + assert_eq!(upper_tick.liquidity_change, Liquidity::from_integer(10000)); + assert!(!upper_tick.sign); + + change_liquidity!( + client, + dex, + 0, + position.liquidity - Liquidity::new(1), + false, + SqrtPrice::new(0), + SqrtPrice::max_instance(), + alice + ) + .unwrap(); + + remove_position!(client, dex, 0, alice).unwrap(); + + Ok(()) + } + + #[ink_e2e::test] + async fn test_change_liquidity_zero_liquidity( + mut client: ink_e2e::Client, + ) -> E2EResult<()> { + let dex = create_dex!(client, Percentage::new(0)); + let (token_x, token_y) = create_tokens!(client, 500, 500); + + let alice = ink_e2e::alice(); + + let fee_tier = FeeTier::new(Percentage::new(0), 1).unwrap(); + + add_fee_tier!(client, dex, fee_tier, alice).unwrap(); + + let init_tick = 0; + let init_sqrt_price = calculate_sqrt_price(init_tick).unwrap(); + create_pool!( + client, + dex, + token_x.account_id, + token_y.account_id, + fee_tier, + init_sqrt_price, + init_tick, + alice + ) + .unwrap(); + + approve!(client, token_x, dex.account_id, 500, alice).unwrap(); + approve!(client, token_y, dex.account_id, 500, alice).unwrap(); + + let pool_key = PoolKey::new(token_x.account_id, token_y.account_id, fee_tier).unwrap(); + + create_position!( + client, + dex, + pool_key, + -10, + 10, + Liquidity::from_integer(10000), + SqrtPrice::new(0), + SqrtPrice::max_instance(), + alice + ) + .unwrap(); + + let position = get_position!(client, dex, 0, alice).unwrap(); + let pool = get_pool!( + client, + dex, + token_x.account_id, + token_y.account_id, + fee_tier + ) + .unwrap(); + let lower_tick = get_tick!(client, dex, pool_key, -10).unwrap(); + let upper_tick = get_tick!(client, dex, pool_key, 10).unwrap(); + + assert_eq!(position.liquidity, Liquidity::from_integer(10000)); + assert_eq!(pool.liquidity, Liquidity::from_integer(10000)); + assert_eq!(lower_tick.liquidity_change, Liquidity::from_integer(10000)); + assert!(lower_tick.sign); + + assert_eq!(upper_tick.liquidity_change, Liquidity::from_integer(10000)); + assert!(!upper_tick.sign); + + let result = change_liquidity!( + client, + dex, + 0, + position.liquidity, + false, + SqrtPrice::new(0), + SqrtPrice::max_instance(), + alice + ); + assert_eq!(result, Err(InvariantError::ZeroLiquidity)); + + Ok(()) + } + + #[ink_e2e::test] + async fn test_change_liquidity_zero_liquidity_change( + mut client: ink_e2e::Client, + ) -> E2EResult<()> { + let dex = create_dex!(client, Percentage::new(0)); + let (token_x, token_y) = create_tokens!(client, 500, 500); + + let alice = ink_e2e::alice(); + + let fee_tier = FeeTier::new(Percentage::new(0), 1).unwrap(); + + add_fee_tier!(client, dex, fee_tier, alice).unwrap(); + + let init_tick = 0; + let init_sqrt_price = calculate_sqrt_price(init_tick).unwrap(); + create_pool!( + client, + dex, + token_x.account_id, + token_y.account_id, + fee_tier, + init_sqrt_price, + init_tick, + alice + ) + .unwrap(); + + approve!(client, token_x, dex.account_id, 500, alice).unwrap(); + approve!(client, token_y, dex.account_id, 500, alice).unwrap(); + + let pool_key = PoolKey::new(token_x.account_id, token_y.account_id, fee_tier).unwrap(); + + create_position!( + client, + dex, + pool_key, + -10, + 10, + Liquidity::from_integer(10000), + SqrtPrice::new(0), + SqrtPrice::max_instance(), + alice + ) + .unwrap(); + + let position = get_position!(client, dex, 0, alice).unwrap(); + let pool = get_pool!( + client, + dex, + token_x.account_id, + token_y.account_id, + fee_tier + ) + .unwrap(); + let lower_tick = get_tick!(client, dex, pool_key, -10).unwrap(); + let upper_tick = get_tick!(client, dex, pool_key, 10).unwrap(); + + assert_eq!(position.liquidity, Liquidity::from_integer(10000)); + assert_eq!(pool.liquidity, Liquidity::from_integer(10000)); + assert_eq!(lower_tick.liquidity_change, Liquidity::from_integer(10000)); + assert!(lower_tick.sign); + + assert_eq!(upper_tick.liquidity_change, Liquidity::from_integer(10000)); + assert!(!upper_tick.sign); + + let result = change_liquidity!( + client, + dex, + 0, + Liquidity::from_integer(0), + true, + SqrtPrice::new(0), + SqrtPrice::max_instance(), + alice + ); + assert_eq!(result, Err(InvariantError::LiquidityChangeZero)); + + Ok(()) + } + + #[ink_e2e::test] + async fn test_change_liquidity_insufficient_balance( + mut client: ink_e2e::Client, + ) -> E2EResult<()> { + let dex = create_dex!(client, Percentage::new(0)); + let (token_x, token_y) = create_tokens!(client, 500, 500); + + let alice = ink_e2e::alice(); + + let fee_tier = FeeTier::new(Percentage::new(0), 1).unwrap(); + + add_fee_tier!(client, dex, fee_tier, alice).unwrap(); + + let init_tick = 0; + let init_sqrt_price = calculate_sqrt_price(init_tick).unwrap(); + create_pool!( + client, + dex, + token_x.account_id, + token_y.account_id, + fee_tier, + init_sqrt_price, + init_tick, + alice + ) + .unwrap(); + + approve!(client, token_x, dex.account_id, u128::MAX, alice).unwrap(); + approve!(client, token_y, dex.account_id, u128::MAX, alice).unwrap(); + + let pool_key = PoolKey::new(token_x.account_id, token_y.account_id, fee_tier).unwrap(); + + create_position!( + client, + dex, + pool_key, + -10, + 10, + Liquidity::from_integer(10000), + SqrtPrice::new(0), + SqrtPrice::max_instance(), + alice + ) + .unwrap(); + + let result = change_liquidity!( + client, + dex, + 0, + Liquidity::from_integer(1000000000), + true, + SqrtPrice::new(0), + SqrtPrice::max_instance(), + alice + ); + assert_eq!(result, Err(InvariantError::TransferError)); + + Ok(()) + } + + #[ink_e2e::test] + async fn test_change_liquidity_no_position(mut client: ink_e2e::Client) -> E2EResult<()> { + let dex = create_dex!(client, Percentage::new(0)); + + let alice = ink_e2e::alice(); + + let result = change_liquidity!( + client, + dex, + 0, + Liquidity::from_integer(10000), + true, + SqrtPrice::new(0), + SqrtPrice::max_instance(), + alice + ); + assert_eq!(result, Err(InvariantError::PositionNotFound)); + + Ok(()) + } +} diff --git a/src/e2e/mod.rs b/src/e2e/mod.rs index e2d376af..4ee8e50b 100644 --- a/src/e2e/mod.rs +++ b/src/e2e/mod.rs @@ -1,5 +1,6 @@ pub mod add_fee_tier; pub mod change_fee_receiver; +pub mod change_liquidity; pub mod change_protocol_fee; pub mod claim; pub mod constructor; diff --git a/src/lib.rs b/src/lib.rs index f1e3d79e..1286c6c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,10 +9,11 @@ pub mod math; #[ink::contract] pub mod invariant { use crate::contracts::{ - tick_to_position, CalculateSwapResult, CreatePositionEvent, CrossTickEvent, FeeTier, - FeeTiers, InvariantConfig, InvariantTrait, LiquidityTick, Pool, PoolKey, PoolKeys, Pools, - Position, Positions, QuoteResult, RemovePositionEvent, SwapEvent, SwapHop, Tick, Tickmap, - Ticks, UpdatePoolTick, CHUNK_SIZE, LIQUIDITY_TICK_LIMIT, MAX_TICKMAP_QUERY_SIZE, + tick_to_position, CalculateSwapResult, ChangeLiquidityEvent, CreatePositionEvent, + CrossTickEvent, FeeTier, FeeTiers, InvariantConfig, InvariantTrait, LiquidityTick, Pool, + PoolKey, PoolKeys, Pools, Position, Positions, QuoteResult, RemovePositionEvent, SwapEvent, + SwapHop, Tick, Tickmap, Ticks, UpdatePoolTick, CHUNK_SIZE, LIQUIDITY_TICK_LIMIT, + MAX_TICKMAP_QUERY_SIZE, }; use crate::math::calculate_min_amount_out; use crate::math::check_tick; @@ -356,6 +357,30 @@ pub mod invariant { }); } + #[allow(clippy::too_many_arguments)] + fn emit_change_liquidity_event( + &self, + address: AccountId, + pool: PoolKey, + delta_liquidity: Liquidity, + add_liquidity: bool, + lower_tick: i32, + upper_tick: i32, + current_sqrt_price: SqrtPrice, + ) { + let timestamp = self.get_timestamp(); + self.env().emit_event(ChangeLiquidityEvent { + timestamp, + address, + pool, + delta_liquidity, + add_liquidity, + lower_tick, + upper_tick, + current_sqrt_price, + }); + } + fn emit_remove_position_event( &self, address: AccountId, @@ -547,6 +572,97 @@ pub mod invariant { Ok(position) } + #[ink(message)] + fn change_liquidity( + &mut self, + index: u32, + delta_liquidity: Liquidity, + add_liquidity: bool, + slippage_limit_lower: SqrtPrice, + slippage_limit_upper: SqrtPrice, + ) -> Result<(), InvariantError> { + let caller = self.env().caller(); + let current_timestamp = self.get_timestamp(); + let current_block_number = self.env().block_number() as u64; + let contract = self.env().account_id(); + + let mut position = self.positions.get(caller, index)?; + let pool_key = position.pool_key; + let mut pool = self.pools.get(pool_key)?; + let mut lower_tick = self.ticks.get(pool_key, position.lower_tick_index)?; + let mut upper_tick = self.ticks.get(pool_key, position.upper_tick_index)?; + + if !add_liquidity && delta_liquidity == position.liquidity { + return Err(InvariantError::ZeroLiquidity); + } + + if delta_liquidity.get() == 0 { + return Err(InvariantError::LiquidityChangeZero); + } + + if pool.sqrt_price < slippage_limit_lower || pool.sqrt_price > slippage_limit_upper { + return Err(InvariantError::PriceLimitReached); + } + + position.update_seconds_per_liquidity( + &mut pool, + lower_tick, + upper_tick, + current_timestamp, + current_block_number, + ); + + let (x, y) = unwrap!(position.modify( + &mut pool, + &mut upper_tick, + &mut lower_tick, + delta_liquidity, + add_liquidity, + current_timestamp, + pool_key.fee_tier.tick_spacing, + )); + + self.pools.update(pool_key, &pool)?; + self.positions.update(caller, index, &position)?; + self.ticks.update(pool_key, lower_tick.index, &lower_tick)?; + self.ticks.update(pool_key, upper_tick.index, &upper_tick)?; + + let x_is_zero = x.get() == 0; + let y_is_zero = y.get() == 0; + + if y_is_zero && x_is_zero { + return Err(InvariantError::AmountIsZero); + } + + if !x_is_zero { + if add_liquidity { + transfer_from_v1!(pool_key.token_x, caller, contract, x.get()); + } else { + transfer_v1!(pool_key.token_x, caller, x.get()); + } + } + + if !y_is_zero { + if add_liquidity { + transfer_from_v1!(pool_key.token_y, caller, contract, y.get()); + } else { + transfer_v1!(pool_key.token_y, caller, y.get()); + } + } + + self.emit_change_liquidity_event( + caller, + pool_key, + delta_liquidity, + add_liquidity, + lower_tick.index, + upper_tick.index, + pool.sqrt_price, + ); + + Ok(()) + } + #[ink(message)] fn swap( &mut self, @@ -1118,6 +1234,7 @@ pub mod invariant { ) -> Result<(), InvariantError> { let caller = self.env().caller(); let current_timestamp = self.get_timestamp(); + let current_block_number = self.env().block_number() as u64; let mut position = self.positions.get(caller, index)?; @@ -1129,7 +1246,13 @@ pub mod invariant { let pool = &mut self.pools.get(pool_key)?; - position.update_seconds_per_liquidity(pool, lower_tick, upper_tick, current_timestamp); + position.update_seconds_per_liquidity( + pool, + lower_tick, + upper_tick, + current_timestamp, + current_block_number, + ); self.pools.update(pool_key, pool)?; self.positions.update(caller, index, &position)?; diff --git a/src/test_helpers/entrypoints.rs b/src/test_helpers/entrypoints.rs index 9a9bd25a..50675201 100644 --- a/src/test_helpers/entrypoints.rs +++ b/src/test_helpers/entrypoints.rs @@ -145,6 +145,39 @@ macro_rules! create_position { }}; } +#[macro_export] +macro_rules! change_liquidity { + ($client:ident, $dex:ident, $index:expr, $liquidity_delta:expr, $add_liquidity:expr, $slippage_limit_lower:expr, $slippage_limit_upper:expr, $caller:ident) => {{ + let mut call_builder = $dex.call_builder::(); + let call = call_builder.change_liquidity( + $index, + $liquidity_delta, + $add_liquidity, + $slippage_limit_lower, + $slippage_limit_upper, + ); + let result = $client + .call(&$caller, &call) + .extra_gas_portion(1000) + .dry_run() + .await + .unwrap() + .return_value(); + + if result.is_ok() { + $client + .call(&$caller, &call) + .extra_gas_portion(1000) + .submit() + .await + .unwrap() + .return_value() + } else { + result + } + }}; +} + #[macro_export] macro_rules! swap { ($client:ident, $dex:ident, $pool_key:expr, $x_to_y:expr, $amount:expr, $by_amount_in:expr, $sqrt_price_limit:expr, $caller:ident) => {{