diff --git a/integration-tests/runtime-tests/.mocharc.json b/integration-tests/runtime-tests/.mocharc.json index 59f790ff2b9..a70c8eb46b3 100644 --- a/integration-tests/runtime-tests/.mocharc.json +++ b/integration-tests/runtime-tests/.mocharc.json @@ -12,7 +12,7 @@ }, "slow": 600000, "timeout": 2000, - "retries": 3, + "retries": 0, "ui": "bdd", "trace-warnings": true, "watch-files": ["src/**/*.ts"], diff --git a/integration-tests/runtime-tests/src/types/interfaces/crowdloanRewards/definitions.ts b/integration-tests/runtime-tests/src/types/interfaces/crowdloanRewards/definitions.ts index f3ffab00b3b..3af03ef3cc8 100644 --- a/integration-tests/runtime-tests/src/types/interfaces/crowdloanRewards/definitions.ts +++ b/integration-tests/runtime-tests/src/types/interfaces/crowdloanRewards/definitions.ts @@ -77,7 +77,11 @@ export default { PalletSchedulerReleases: "Null", PalletSchedulerScheduledV3: "Null", DaliRuntimeOpaqueSessionKeys: "Null", - OrmlTokensAccountData: "Null", + OrmlTokensAccountData: { + free: 'u128', + reserved: 'u128', + frozen: 'u128' + }, OrmlTokensBalanceLock: "Null", PalletTreasuryProposal: "Null", PalletVaultModelsStrategyOverview: "Null", @@ -120,15 +124,27 @@ export default { PalletCurrencyFactoryRanges: "Null", PalletCurrencyFactoryRangesRange: "Null", PalletLiquidationsLiquidationStrategyConfiguration: "Null", - ComposableTraitsDefiCurrencyPair: "Null", + ComposableTraitsDefiCurrencyPair: { + base: "u128", + quote: "u128" + }, CommonMosaicRemoteAssetId: "Null", ComposableTraitsDexConsantProductPoolInfo: "Null", ComposableTraitsLendingMarketConfig: "Null", ComposableTraitsLendingCreateInput: "Null", ComposableTraitsLendingUpdateInput: "Null", - PalletLiquidityBootstrappingPool: "Null", - ComposableTraitsOraclePrice: "Null", ComposableTraitsDexStableSwapPoolInfo: "Null", - ComposableTraitsDexConstantProductPoolInfo: "Null" + ComposableTraitsOraclePrice: "Null", + PalletLiquidityBootstrappingPool: "Null", + ComposableTraitsDexConstantProductPoolInfo: { + owner: "AccountId32", + pair: { + base: "u128", + quote: "u128" + }, + lpToken: "u128", + fee: "Permill", + ownerFee: "Permill" + }, }, }; diff --git a/integration-tests/runtime-tests/src/types/interfaces/crowdloanRewards/types.ts b/integration-tests/runtime-tests/src/types/interfaces/crowdloanRewards/types.ts index 0e89597266e..2fbafc15963 100644 --- a/integration-tests/runtime-tests/src/types/interfaces/crowdloanRewards/types.ts +++ b/integration-tests/runtime-tests/src/types/interfaces/crowdloanRewards/types.ts @@ -6,7 +6,13 @@ import type { ITuple } from '@polkadot/types-codec/types'; import type { EthereumAccountId } from '@polkadot/types/interfaces/eth'; import type { EcdsaSignature, MultiSignature } from '@polkadot/types/interfaces/extrinsics'; import type { ParachainInherentData, PersistedValidationData } from '@polkadot/types/interfaces/parachains'; -import type { AccountId32, Balance } from '@polkadot/types/interfaces/runtime'; +import type { AccountId32, Permill } from '@polkadot/types/interfaces/runtime'; + +/** @name Balance */ +export interface Balance extends u128 {} + +/** @name CommonMosaicRemoteAssetId */ +export interface CommonMosaicRemoteAssetId extends Null {} /** @name CommonMosaicRemoteAssetId */ export interface CommonMosaicRemoteAssetId extends Null {} @@ -46,7 +52,10 @@ export interface ComposableTraitsBondedFinanceBondOfferReward extends Struct { export interface ComposableTraitsCallFilterCallFilterEntry extends Null {} /** @name ComposableTraitsDefiCurrencyPair */ -export interface ComposableTraitsDefiCurrencyPair extends Null {} +export interface ComposableTraitsDefiCurrencyPair extends Struct { + readonly base: u128; + readonly quote: u128; +} /** @name ComposableTraitsDefiSell */ export interface ComposableTraitsDefiSell extends Null {} @@ -58,7 +67,16 @@ export interface ComposableTraitsDefiTake extends Null {} export interface ComposableTraitsDexConsantProductPoolInfo extends Null {} /** @name ComposableTraitsDexConstantProductPoolInfo */ -export interface ComposableTraitsDexConstantProductPoolInfo extends Null {} +export interface ComposableTraitsDexConstantProductPoolInfo extends Struct { + readonly owner: AccountId32; + readonly pair: { + readonly base: u128; + readonly quote: u128; + } & Struct; + readonly lpToken: u128; + readonly fee: Permill; + readonly ownerFee: Permill; +} /** @name ComposableTraitsDexStableSwapPoolInfo */ export interface ComposableTraitsDexStableSwapPoolInfo extends Null {} @@ -127,7 +145,11 @@ export interface FrameSupportScheduleLookupError extends Null {} export interface FrameSupportScheduleMaybeHashed extends Null {} /** @name OrmlTokensAccountData */ -export interface OrmlTokensAccountData extends Null {} +export interface OrmlTokensAccountData extends Struct { + readonly free: u128; + readonly reserved: u128; + readonly frozen: u128; +} /** @name OrmlTokensBalanceLock */ export interface OrmlTokensBalanceLock extends Null {} @@ -279,6 +301,9 @@ export interface PolkadotPrimitivesV1AbridgedHostConfiguration extends Null {} /** @name PolkadotPrimitivesV1PersistedValidationData */ export interface PolkadotPrimitivesV1PersistedValidationData extends PersistedValidationData {} +/** @name PoolId */ +export interface PoolId extends u128 {} + /** @name SpConsensusAuraSr25519AppSr25519Public */ export interface SpConsensusAuraSr25519AppSr25519Public extends Null {} diff --git a/integration-tests/runtime-tests/test/tests/dexTests/constantProductDex/constantProductTests.ts b/integration-tests/runtime-tests/test/tests/dexTests/constantProductDex/constantProductTests.ts new file mode 100644 index 00000000000..507fd9f8602 --- /dev/null +++ b/integration-tests/runtime-tests/test/tests/dexTests/constantProductDex/constantProductTests.ts @@ -0,0 +1,167 @@ +import testConfiguration from './test_configuration.json'; +import {expect} from "chai"; +import {KeyringPair} from "@polkadot/keyring/types"; +import { addFundstoThePool, buyFromPool, createPool, getOwnerFee, getUserTokens, removeLiquidityFromPool, sellToPool, swapTokenPairs } from './testHandlers/constantProductDexHelper'; +import { mintAssetsToWallet } from '@composable/utils/mintingHelper'; + +/** + * This suite includes tests for the constantProductDex Pallet. + * Tested functionalities are: + * Create - AddLiquidity - Buy - Sell - Swap - RemoveLiquidity with basic calculations with constantProductFormula and OwnerFee. + * Mainly consists of happy path testing. +*/ +describe('tx.constantProductDex Tests', function () { + + let walletId1: KeyringPair, + walletId2: KeyringPair; + let poolId: number, + baseAssetId: number, + quoteAssetId: number, + wallet1LpTokens: number, + baseAmount: number, + quoteAmount: number, + ownerFee: number, + walletId1Account: string, + walletId2Account: string; + + before('Initialize variables', function() { + walletId1 = walletEve.derive("/test/constantProductDex/walletId1"); + walletId2 = walletBob.derive("/test/constantProductDex/walletId2"); + walletId1Account = api.createType('AccountId32', walletId1.address).toString(); + walletId2Account = api.createType('AccountId32', walletId2.address).toString(); + baseAssetId = 2; + quoteAssetId = 3; + baseAmount = 2500; + quoteAmount = 2500; + //sets the owner fee to 1.00%/Type Permill + ownerFee = 10000; + }); + + before('Minting assets', async function() { + this.timeout(8*60*1000); + await mintAssetsToWallet(walletId1, walletAlice, [1, baseAssetId, quoteAssetId]); + await mintAssetsToWallet(walletId2, walletAlice, [1, baseAssetId, quoteAssetId]); + }); + + describe('tx.constantProductDex Success Tests', function() { + if(!testConfiguration.enabledTests.successTests.enabled){ + return; + } + + it('Users can create a constantProduct pool', async function() { + if(!testConfiguration.enabledTests.successTests.createPool.enabled){ + return; + } + this.timeout(2*60*1000); + poolId = await createPool(walletId1, + baseAssetId, + quoteAssetId, + ownerFee + ); + let returnedOwnerFee = await getOwnerFee(poolId); + //verify if the pool is created + expect(poolId).to.be.a('number'); + //Verify if the pool is created with specified owner Fee + expect(returnedOwnerFee).to.be.equal(ownerFee); + }) + + it('Given that users has sufficient balance, User1 can send funds to pool', async function(){ + if(!testConfiguration.enabledTests.successTests.addLiquidityTests.enabled){ + return; + } + this.timeout(2*60*1000); + const result = await addFundstoThePool(walletId1, + baseAmount, + quoteAmount + ); + //Once funds added to the pool, User is deposited with LP Tokens. + wallet1LpTokens = result.returnedLPTokens.toNumber(); + expect(result.baseAdded.toNumber()).to.be.equal(baseAmount); + expect(result.quoteAdded.toNumber()).to.be.equal(quoteAmount); + expect(result.walletIdResult.toString()).to.be.equal(walletId1Account); + }); + + it('User2 can send funds to pool and router adjusts deposited amounts based on constantProductFormula to prevent arbitrage', async function(){ + if(!testConfiguration.enabledTests.successTests.addLiquidityTests.enabled){ + return; + } + this.timeout(2*60*1000); + const assetAmount = 30; + const quoteAmount = 100; + const result = await addFundstoThePool(walletId2, assetAmount, quoteAmount); + //The deposited amount should be maintained by the dex router hence should maintain 1:1. + expect(result.quoteAdded.toNumber()).to.be.equal(assetAmount); + expect(result.walletIdResult.toString()).to.be.equal(walletId2Account); + }); + + it("Given the pool has the sufficient funds, User1 can't completely drain the funds", async function(){ + if(!testConfiguration.enabledTests.successTests.poolDrainTest.enabled){ + return; + } + this.timeout(2*60*1000); + await buyFromPool(walletId1, baseAssetId, 2800).catch(error=>{ + expect(error.message).to.contain('arithmetic'); + }); + }); + + it('User1 can buy from the pool and router respects the constantProductFormula', async function() { + if(!testConfiguration.enabledTests.successTests.buyTest.enabled){ + return; + } + this.timeout(2 * 60 * 1000); + const result = await buyFromPool(walletId1, baseAssetId, 30); + expect(result.accountId.toString()).to.be.equal(walletId1Account); + //Expected amount is calculated based on the constantProductFormula which is 1:1 for this case. + expect(result.quoteAmount.toNumber()).to.be.equal(result.expectedConversion); + }); + + it('User1 can sell on the pool', async function(){ + if(!testConfiguration.enabledTests.successTests.sellTest.enabled){ + return; + } + this.timeout(2*60*1000); + const accountIdSeller = await sellToPool(walletId1, baseAssetId, 20); + expect(accountIdSeller).to.be.equal(walletId1Account); + }); + + it('User2 can swap from the pool', async function(){ + if(!testConfiguration.enabledTests.successTests.swapTest.enabled){ + return; + } + this.timeout(2*60*1000); + const quotedAmount = 12; + const result = await swapTokenPairs(walletId2, + baseAssetId, + quoteAssetId, + quotedAmount, + ); + expect(result.returnedQuoteAmount.toNumber()).to.be.equal(quotedAmount); + }); + + it('Owner of the pool receives owner fee on the transactions happened in the pool', async function(){ + if(!testConfiguration.enabledTests.successTests.ownerFeeTest.enabled){ + return; + } + this.timeout(2*60*1000); + let ownerInitialTokens = await getUserTokens(walletId1, quoteAssetId); + const result = await buyFromPool(walletId2, baseAssetId, 500); + let ownerAfterTokens = await getUserTokens(walletId1, quoteAssetId); + //verifies the ownerFee to be added in the owner account. + expect(ownerAfterTokens).to.be.equal(ownerInitialTokens+(result.ownerFee.toNumber())) + }); + + it('User1 can remove liquidity from the pool by using LP Tokens', async function(){ + if(!testConfiguration.enabledTests.successTests.removeLiquidityTest.enabled){ + return; + } + this.timeout(2*60*1000); + //Randomly checks an integer value that is always < mintedLPTokens. + const result = await removeLiquidityFromPool(walletId1, Math.floor(Math.random()*wallet1LpTokens)); + expect(result.remainingLpTokens.toNumber()).to.be.equal(result.expectedLPTokens); + }); + }); +}) + + + + diff --git a/integration-tests/runtime-tests/test/tests/dexTests/constantProductDex/testHandlers/constantProductDexHelper.ts b/integration-tests/runtime-tests/test/tests/dexTests/constantProductDex/testHandlers/constantProductDexHelper.ts new file mode 100644 index 00000000000..18e3c07b0a3 --- /dev/null +++ b/integration-tests/runtime-tests/test/tests/dexTests/constantProductDex/testHandlers/constantProductDexHelper.ts @@ -0,0 +1,152 @@ +import { sendAndWaitForSuccess } from '@composable/utils/polkadotjs'; +import {KeyringPair} from "@polkadot/keyring/types"; +import { u128 } from '@polkadot/types-codec'; + +/** + *Contains handler methods for the constantProductDex Tests. + */ +let poolId: number; +let constantProductk: number; +let baseAmountTotal: number; +let quoteAmountTotal: number; +let mintedLPTokens: number; +baseAmountTotal = 0; +quoteAmountTotal = 0; +mintedLPTokens = 0; + +export async function createPool(walletId: KeyringPair, baseAssetId: number, quoteAssetId: number, ownerFee: number){ + const pair = api.createType('ComposableTraitsDefiCurrencyPair', { + base: api.createType('u128', baseAssetId), + quote: api.createType('u128', quoteAssetId) + }); + const fee = api.createType('Permill', 0); + const ownerFees = api.createType('Permill', ownerFee); + const {data: [resultPoolId],} = await sendAndWaitForSuccess( + api, + walletId, + api.events.constantProductDex.PoolCreated.is, + api.tx.constantProductDex.create(pair, fee, ownerFees) + ); + poolId = resultPoolId.toNumber(); + return poolId; +} +export async function addFundstoThePool(walletId:KeyringPair, baseAmount:number, quoteAmount:number){ + const baseAmountParam = api.createType('u128', baseAmount); + const quoteAmountParam = api.createType('u128', quoteAmount); + const keepAliveParam = api.createType('bool', true); + const minMintAmountParam = api.createType('u128', 0); + const {data: [,walletIdResult,baseAdded, quoteAdded,returnedLPTokens]} =await sendAndWaitForSuccess( + api, + walletId, + api.events.constantProductDex.LiquidityAdded.is, + api.tx.constantProductDex.addLiquidity(poolId, + baseAmountParam, + quoteAmountParam, + minMintAmountParam, + keepAliveParam + ) + ); + mintedLPTokens += returnedLPTokens.toNumber(); + baseAmountTotal += baseAdded.toNumber(); + quoteAmountTotal += quoteAdded.toNumber(); + return {walletIdResult, baseAdded, quoteAdded, returnedLPTokens}; +} + +export async function buyFromPool(walletId: KeyringPair, assetId:number, amountToBuy: number){ + const poolIdParam = api.createType('u128', poolId); + const assetIdParam = api.createType('u128', assetId); + const amountParam = api.createType('u128', amountToBuy); + const keepAlive = api.createType('bool', true); + constantProductk = baseAmountTotal*quoteAmountTotal; + let expectedConversion = Math.floor((constantProductk/(baseAmountTotal-amountToBuy)))-quoteAmountTotal; + const {data: [accountId,poolArg,quoteArg,swapArg,amountgathered,quoteAmount,ownerFee] } = await sendAndWaitForSuccess( + api, + walletId, + api.events.constantProductDex.Swapped.is, + api.tx.constantProductDex.buy( + poolIdParam, + assetIdParam, + amountParam, + keepAlive + ) + ); + return {accountId, quoteAmount, expectedConversion, ownerFee}; +} + +export async function sellToPool(walletId: KeyringPair, assetId: number, amount:number){ + const poolIdParam = api.createType('u128', poolId); + const assetIdParam = api.createType('u128', assetId); + const amountParam = api.createType('u128', amount); + const keepAliveParam = api.createType('bool', false); + const {data: [result, ...rest]} = await sendAndWaitForSuccess( + api, + walletId, + api.events.constantProductDex.Swapped.is, + api.tx.constantProductDex.sell( + poolIdParam, + assetIdParam, + amountParam, + keepAliveParam + ) + ) + return result.toString(); +} + +export async function removeLiquidityFromPool(walletId: KeyringPair, lpTokens:number){ + const expectedLPTokens = mintedLPTokens-lpTokens; + const poolIdParam = api.createType('u128', poolId); + const lpTokenParam = api.createType('u128', lpTokens); + const minBaseParam = api.createType('u128', 0); + const minQuoteAmountParam = api.createType('u128', 0); + const {data: [resultPoolId,resultWallet,resultBase,resultQuote,remainingLpTokens]}=await sendAndWaitForSuccess( + api, + walletId, + api.events.constantProductDex.LiquidityRemoved.is, + api.tx.constantProductDex.removeLiquidity( + poolIdParam, + lpTokenParam, + minBaseParam, + minQuoteAmountParam + ) + ); + return {remainingLpTokens, expectedLPTokens} +} + +export async function swapTokenPairs(wallet: KeyringPair, + baseAssetId: number, + quoteAssetId:number, + quoteAmount: number, + minReceiveAmount: number = 0 + ){ + const poolIdParam = api.createType('u128', poolId); + const currencyPair = api.createType('ComposableTraitsDefiCurrencyPair', { + base: api.createType('u128', baseAssetId), + quote: api.createType('u128',quoteAssetId) + }); + const quoteAmountParam = api.createType('u128', quoteAmount); + const minReceiveParam = api.createType('u128', minReceiveAmount); + const keepAliveParam = api.createType('bool', true); + const {data: [resultPoolId,resultWallet,resultQuote,resultBase,resultBaseAmount,returnedQuoteAmount,]}= await sendAndWaitForSuccess( + api, + wallet, + api.events.constantProductDex.Swapped.is, + api.tx.constantProductDex.swap( + poolIdParam, + currencyPair, + quoteAmountParam, + minReceiveParam, + keepAliveParam + ) + ); + return {returnedQuoteAmount}; +} + +export async function getUserTokens(walletId: KeyringPair, assetId: number){ + const {free, reserved, frozen} = await api.query.tokens.accounts(walletId.address, assetId); + return free.toNumber(); +} + +export async function getOwnerFee(poolId: number){ + const result = await api.query.constantProductDex.pools(api.createType('u128', poolId)); + return result.unwrap().ownerFee.toNumber(); +} \ No newline at end of file diff --git a/integration-tests/runtime-tests/test/tests/dexTests/constantProductDex/test_configuration.json b/integration-tests/runtime-tests/test/tests/dexTests/constantProductDex/test_configuration.json new file mode 100644 index 00000000000..7ad92d3b661 --- /dev/null +++ b/integration-tests/runtime-tests/test/tests/dexTests/constantProductDex/test_configuration.json @@ -0,0 +1,33 @@ +{ + "enabledTests": { + "enabled": true, + + "successTests": { + "enabled": true, + "createPool": { + "enabled": true + }, + "addLiquidityTests": { + "enabled": true + }, + "poolDrainTest": { + "enabled": true + }, + "buyTest": { + "enabled": true + }, + "sellTest": { + "enabled": true + }, + "swapTest": { + "enabled": true + }, + "ownerFeeTest": { + "enabled": true + }, + "removeLiquidityTest": { + "enabled": true + } + } + } +} \ No newline at end of file