github.com/gravity-devs/liquidity@v1.5.3/x/liquidity/keeper/swap_test.go (about)

     1  package keeper_test
     2  
     3  import (
     4  	"math/rand"
     5  	"testing"
     6  
     7  	sdk "github.com/cosmos/cosmos-sdk/types"
     8  	"github.com/stretchr/testify/require"
     9  
    10  	"github.com/gravity-devs/liquidity/app"
    11  	"github.com/gravity-devs/liquidity/x/liquidity"
    12  	"github.com/gravity-devs/liquidity/x/liquidity/types"
    13  )
    14  
    15  func TestSimulationSwapExecutionFindEdgeCase(t *testing.T) {
    16  	for seed := int64(0); seed < 20; seed++ {
    17  		r := rand.New(rand.NewSource(seed))
    18  
    19  		simapp, ctx := createTestInput()
    20  		params := simapp.LiquidityKeeper.GetParams(ctx)
    21  
    22  		// define test denom X, Y for Liquidity Pool
    23  		denomX := "denomX"
    24  		denomY := "denomY"
    25  		denomX, denomY = types.AlphabeticalDenomPair(denomX, denomY)
    26  
    27  		// get random X, Y amount for create pool
    28  		param := simapp.LiquidityKeeper.GetParams(ctx)
    29  		X, Y := app.GetRandPoolAmt(r, param.MinInitDepositAmount)
    30  		deposit := sdk.NewCoins(sdk.NewCoin(denomX, X), sdk.NewCoin(denomY, Y))
    31  
    32  		// set pool creator account, balance for deposit
    33  		addrs := app.AddTestAddrs(simapp, ctx, 3, params.PoolCreationFee)
    34  		app.SaveAccount(simapp, ctx, addrs[0], deposit) // pool creator
    35  		depositA := simapp.BankKeeper.GetBalance(ctx, addrs[0], denomX)
    36  		depositB := simapp.BankKeeper.GetBalance(ctx, addrs[0], denomY)
    37  		depositBalance := sdk.NewCoins(depositA, depositB)
    38  		require.Equal(t, deposit, depositBalance)
    39  
    40  		// create Liquidity pool
    41  		poolTypeID := types.DefaultPoolTypeID
    42  		msg := types.NewMsgCreatePool(addrs[0], poolTypeID, depositBalance)
    43  		_, err := simapp.LiquidityKeeper.CreatePool(ctx, msg)
    44  		require.NoError(t, err)
    45  
    46  		for i := 0; i < 20; i++ {
    47  			ctx = ctx.WithBlockHeight(int64(i))
    48  			testSwapEdgeCases(t, r, simapp, ctx, X, Y, depositBalance, addrs)
    49  		}
    50  	}
    51  }
    52  
    53  func TestSwapExecution(t *testing.T) {
    54  	for seed := int64(0); seed < 50; seed++ {
    55  		s := rand.NewSource(seed)
    56  		r := rand.New(s)
    57  		simapp, ctx := createTestInput()
    58  		simapp.LiquidityKeeper.SetParams(ctx, types.DefaultParams())
    59  		params := simapp.LiquidityKeeper.GetParams(ctx)
    60  
    61  		// define test denom X, Y for Liquidity Pool
    62  		denomX := "denomX"
    63  		denomY := "denomY"
    64  		denomX, denomY = types.AlphabeticalDenomPair(denomX, denomY)
    65  
    66  		// get random X, Y amount for create pool
    67  		X, Y := app.GetRandPoolAmt(r, params.MinInitDepositAmount)
    68  		deposit := sdk.NewCoins(sdk.NewCoin(denomX, X), sdk.NewCoin(denomY, Y))
    69  
    70  		// set pool creator account, balance for deposit
    71  		addrs := app.AddTestAddrs(simapp, ctx, 3, params.PoolCreationFee)
    72  		app.SaveAccount(simapp, ctx, addrs[0], deposit) // pool creator
    73  		depositA := simapp.BankKeeper.GetBalance(ctx, addrs[0], denomX)
    74  		depositB := simapp.BankKeeper.GetBalance(ctx, addrs[0], denomY)
    75  		depositBalance := sdk.NewCoins(depositA, depositB)
    76  		require.Equal(t, deposit, depositBalance)
    77  
    78  		// create Liquidity pool
    79  		poolTypeID := types.DefaultPoolTypeID
    80  		msg := types.NewMsgCreatePool(addrs[0], poolTypeID, depositBalance)
    81  		_, err := simapp.LiquidityKeeper.CreatePool(ctx, msg)
    82  		require.NoError(t, err)
    83  
    84  		// verify created liquidity pool
    85  		pools := simapp.LiquidityKeeper.GetAllPools(ctx)
    86  		poolID := pools[0].Id
    87  		require.Equal(t, 1, len(pools))
    88  		require.Equal(t, uint64(1), poolID)
    89  		require.Equal(t, denomX, pools[0].ReserveCoinDenoms[0])
    90  		require.Equal(t, denomY, pools[0].ReserveCoinDenoms[1])
    91  
    92  		// verify minted pool coin
    93  		poolCoin := simapp.LiquidityKeeper.GetPoolCoinTotalSupply(ctx, pools[0])
    94  		creatorBalance := simapp.BankKeeper.GetBalance(ctx, addrs[0], pools[0].PoolCoinDenom)
    95  		require.Equal(t, poolCoin, creatorBalance.Amount)
    96  
    97  		var xToY []*types.MsgSwapWithinBatch // buying Y from X
    98  		var yToX []*types.MsgSwapWithinBatch // selling Y for X
    99  
   100  		// make random orders, set buyer, seller accounts for the orders
   101  		xToY, yToX = app.GetRandomSizeOrders(denomX, denomY, X, Y, r, 250, 250)
   102  		buyerAddrs := app.AddTestAddrsIncremental(simapp, ctx, len(xToY), sdk.NewInt(0))
   103  		sellerAddrs := app.AddTestAddrsIncremental(simapp, ctx, len(yToX), sdk.NewInt(0))
   104  
   105  		for i, msg := range xToY {
   106  			app.SaveAccountWithFee(simapp, ctx, buyerAddrs[i], sdk.NewCoins(msg.OfferCoin), msg.OfferCoin)
   107  			msg.SwapRequesterAddress = buyerAddrs[i].String()
   108  			msg.PoolId = poolID
   109  			msg.OfferCoinFee = types.GetOfferCoinFee(msg.OfferCoin, params.SwapFeeRate)
   110  		}
   111  		for i, msg := range yToX {
   112  			app.SaveAccountWithFee(simapp, ctx, sellerAddrs[i], sdk.NewCoins(msg.OfferCoin), msg.OfferCoin)
   113  			msg.SwapRequesterAddress = sellerAddrs[i].String()
   114  			msg.PoolId = poolID
   115  			msg.OfferCoinFee = types.GetOfferCoinFee(msg.OfferCoin, params.SwapFeeRate)
   116  		}
   117  
   118  		// begin block, delete and init pool batch
   119  		liquidity.BeginBlocker(ctx, simapp.LiquidityKeeper)
   120  
   121  		// handle msgs, set order msgs to batch
   122  		for _, msg := range xToY {
   123  			_, err := simapp.LiquidityKeeper.SwapWithinBatch(ctx, msg, 0)
   124  			require.NoError(t, err)
   125  		}
   126  		for _, msg := range yToX {
   127  			_, err := simapp.LiquidityKeeper.SwapWithinBatch(ctx, msg, 0)
   128  			require.NoError(t, err)
   129  		}
   130  
   131  		// verify pool batch
   132  		liquidityPoolBatch, found := simapp.LiquidityKeeper.GetPoolBatch(ctx, poolID)
   133  		require.True(t, found)
   134  		require.NotNil(t, liquidityPoolBatch)
   135  
   136  		// end block, swap execution
   137  		liquidity.EndBlocker(ctx, simapp.LiquidityKeeper)
   138  	}
   139  }
   140  
   141  func testSwapEdgeCases(t *testing.T, r *rand.Rand, simapp *app.LiquidityApp, ctx sdk.Context, X, Y sdk.Int, depositBalance sdk.Coins, addrs []sdk.AccAddress) {
   142  	//simapp, ctx := createTestInput()
   143  	simapp.LiquidityKeeper.SetParams(ctx, types.DefaultParams())
   144  	params := simapp.LiquidityKeeper.GetParams(ctx)
   145  
   146  	denomX := depositBalance[0].Denom
   147  	denomY := depositBalance[1].Denom
   148  
   149  	// verify created liquidity pool
   150  	pools := simapp.LiquidityKeeper.GetAllPools(ctx)
   151  	poolID := pools[0].Id
   152  	require.Equal(t, 1, len(pools))
   153  	require.Equal(t, uint64(1), poolID)
   154  	require.Equal(t, denomX, pools[0].ReserveCoinDenoms[0])
   155  	require.Equal(t, denomY, pools[0].ReserveCoinDenoms[1])
   156  
   157  	// verify minted pool coin
   158  	poolCoin := simapp.LiquidityKeeper.GetPoolCoinTotalSupply(ctx, pools[0])
   159  	creatorBalance := simapp.BankKeeper.GetBalance(ctx, addrs[0], pools[0].PoolCoinDenom)
   160  	require.Equal(t, poolCoin, creatorBalance.Amount)
   161  
   162  	var xToY []*types.MsgSwapWithinBatch // buying Y from X
   163  	var yToX []*types.MsgSwapWithinBatch // selling Y for X
   164  
   165  	batch, found := simapp.LiquidityKeeper.GetPoolBatch(ctx, poolID)
   166  	require.True(t, found)
   167  
   168  	remainingSwapMsgs := simapp.LiquidityKeeper.GetAllNotProcessedPoolBatchSwapMsgStates(ctx, batch)
   169  	if ctx.BlockHeight() == 0 || len(remainingSwapMsgs) == 0 {
   170  		// make random orders, set buyer, seller accounts for the orders
   171  		xToY, yToX = app.GetRandomSizeOrders(denomX, denomY, X, Y, r, 100, 100)
   172  		buyerAddrs := app.AddTestAddrsIncremental(simapp, ctx, len(xToY), sdk.NewInt(0))
   173  		sellerAddrs := app.AddTestAddrsIncremental(simapp, ctx, len(yToX), sdk.NewInt(0))
   174  
   175  		for i, msg := range xToY {
   176  			app.SaveAccountWithFee(simapp, ctx, buyerAddrs[i], sdk.NewCoins(msg.OfferCoin), msg.OfferCoin)
   177  			msg.SwapRequesterAddress = buyerAddrs[i].String()
   178  			msg.PoolId = poolID
   179  			msg.OfferCoinFee = types.GetOfferCoinFee(msg.OfferCoin, params.SwapFeeRate)
   180  		}
   181  		for i, msg := range yToX {
   182  			app.SaveAccountWithFee(simapp, ctx, sellerAddrs[i], sdk.NewCoins(msg.OfferCoin), msg.OfferCoin)
   183  			msg.SwapRequesterAddress = sellerAddrs[i].String()
   184  			msg.PoolId = poolID
   185  			msg.OfferCoinFee = types.GetOfferCoinFee(msg.OfferCoin, params.SwapFeeRate)
   186  		}
   187  	}
   188  
   189  	// begin block, delete and init pool batch
   190  	liquidity.BeginBlocker(ctx, simapp.LiquidityKeeper)
   191  
   192  	// handle msgs, set order msgs to batch
   193  	for _, msg := range xToY {
   194  		_, err := simapp.LiquidityKeeper.SwapWithinBatch(ctx, msg, int64(r.Intn(4)))
   195  		require.NoError(t, err)
   196  	}
   197  	for _, msg := range yToX {
   198  		_, err := simapp.LiquidityKeeper.SwapWithinBatch(ctx, msg, int64(r.Intn(4)))
   199  		require.NoError(t, err)
   200  	}
   201  
   202  	// verify pool batch
   203  	liquidityPoolBatch, found := simapp.LiquidityKeeper.GetPoolBatch(ctx, poolID)
   204  	require.True(t, found)
   205  	require.NotNil(t, liquidityPoolBatch)
   206  
   207  	// end block, swap execution
   208  	liquidity.EndBlocker(ctx, simapp.LiquidityKeeper)
   209  }
   210  
   211  func TestBadSwapExecution(t *testing.T) {
   212  	r := rand.New(rand.NewSource(0))
   213  
   214  	simapp, ctx := app.CreateTestInput()
   215  	params := simapp.LiquidityKeeper.GetParams(ctx)
   216  	denomX, denomY := types.AlphabeticalDenomPair("denomX", "denomY")
   217  
   218  	// add pool creator account
   219  	X, Y := app.GetRandPoolAmt(r, params.MinInitDepositAmount)
   220  	deposit := sdk.NewCoins(sdk.NewCoin(denomX, X), sdk.NewCoin(denomY, Y))
   221  	creatorAddr := app.AddRandomTestAddr(simapp, ctx, deposit.Add(params.PoolCreationFee...))
   222  	balanceX := simapp.BankKeeper.GetBalance(ctx, creatorAddr, denomX)
   223  	balanceY := simapp.BankKeeper.GetBalance(ctx, creatorAddr, denomY)
   224  	creatorBalance := sdk.NewCoins(balanceX, balanceY)
   225  	require.Equal(t, deposit, creatorBalance)
   226  
   227  	// create pool
   228  	createPoolMsg := types.NewMsgCreatePool(creatorAddr, types.DefaultPoolTypeID, creatorBalance)
   229  	_, err := simapp.LiquidityKeeper.CreatePool(ctx, createPoolMsg)
   230  	require.NoError(t, err)
   231  
   232  	liquidity.BeginBlocker(ctx, simapp.LiquidityKeeper)
   233  
   234  	offerCoin := sdk.NewCoin(denomX, sdk.NewInt(10000))
   235  	offerCoinFee := types.GetOfferCoinFee(offerCoin, params.SwapFeeRate)
   236  	testAddr := app.AddRandomTestAddr(simapp, ctx, sdk.NewCoins(offerCoin.Add(offerCoinFee)))
   237  
   238  	currentPrice := X.ToDec().Quo(Y.ToDec())
   239  	swapMsg := types.NewMsgSwapWithinBatch(testAddr, 0, types.DefaultSwapTypeID, offerCoin, denomY, currentPrice, params.SwapFeeRate)
   240  	_, err = simapp.LiquidityKeeper.SwapWithinBatch(ctx, swapMsg, 0)
   241  	require.ErrorIs(t, err, types.ErrPoolNotExists)
   242  
   243  	liquidity.EndBlocker(ctx, simapp.LiquidityKeeper)
   244  }
   245  
   246  func TestBalancesAfterSwap(t *testing.T) {
   247  	for price := int64(9800); price < 10000; price++ {
   248  		simapp, ctx := app.CreateTestInput()
   249  		params := simapp.LiquidityKeeper.GetParams(ctx)
   250  		denomX, denomY := types.AlphabeticalDenomPair("denomx", "denomy")
   251  		X, Y := sdk.NewInt(100_000_000), sdk.NewInt(100_000_000)
   252  
   253  		creatorCoins := sdk.NewCoins(sdk.NewCoin(denomX, X), sdk.NewCoin(denomY, Y))
   254  		creatorAddr := app.AddRandomTestAddr(simapp, ctx, creatorCoins.Add(params.PoolCreationFee...))
   255  
   256  		orderPrice := sdk.NewDecWithPrec(price, 4)
   257  		aliceCoin := sdk.NewCoin(denomY, sdk.NewInt(10_000_000))
   258  		aliceAddr := app.AddRandomTestAddr(simapp, ctx, sdk.NewCoins(aliceCoin))
   259  
   260  		pool, err := simapp.LiquidityKeeper.CreatePool(ctx, types.NewMsgCreatePool(creatorAddr, types.DefaultPoolTypeID, creatorCoins))
   261  		require.NoError(t, err)
   262  
   263  		liquidity.BeginBlocker(ctx, simapp.LiquidityKeeper)
   264  
   265  		offerAmt := aliceCoin.Amount.ToDec().Quo(sdk.MustNewDecFromStr("1.0015")).TruncateInt()
   266  		offerCoin := sdk.NewCoin(denomY, offerAmt)
   267  
   268  		_, err = simapp.LiquidityKeeper.SwapWithinBatch(ctx, types.NewMsgSwapWithinBatch(
   269  			aliceAddr, pool.Id, types.DefaultSwapTypeID, offerCoin, denomX, orderPrice, params.SwapFeeRate), 0)
   270  		require.NoError(t, err)
   271  
   272  		liquidity.EndBlocker(ctx, simapp.LiquidityKeeper)
   273  
   274  		deltaX := simapp.BankKeeper.GetBalance(ctx, aliceAddr, denomX).Amount
   275  		deltaY := simapp.BankKeeper.GetBalance(ctx, aliceAddr, denomY).Amount.Sub(aliceCoin.Amount)
   276  		require.Truef(t, !deltaX.IsNegative(), "deltaX should not be negative: %s", deltaX)
   277  		require.Truef(t, deltaY.IsNegative(), "deltaY should be negative: %s", deltaY)
   278  
   279  		deltaXWithoutFee := deltaX.ToDec().Quo(sdk.MustNewDecFromStr("0.9985"))
   280  		deltaYWithoutFee := deltaY.ToDec().Quo(sdk.MustNewDecFromStr("1.0015"))
   281  		effectivePrice := deltaXWithoutFee.Quo(deltaYWithoutFee.Neg())
   282  		priceDiffRatio := orderPrice.Sub(effectivePrice).Abs().Quo(orderPrice)
   283  		require.Truef(t, priceDiffRatio.LT(sdk.MustNewDecFromStr("0.01")), "effectivePrice differs too much from orderPrice")
   284  	}
   285  }
   286  
   287  func TestRefundEscrow(t *testing.T) {
   288  	for seed := int64(0); seed < 100; seed++ {
   289  		r := rand.New(rand.NewSource(seed))
   290  
   291  		X := sdk.NewInt(1_000_000)
   292  		Y := app.GetRandRange(r, 10_000_000_000_000_000, 1_000_000_000_000_000_000)
   293  
   294  		simapp, ctx := createTestInput()
   295  		params := simapp.LiquidityKeeper.GetParams(ctx)
   296  
   297  		addr := app.AddRandomTestAddr(simapp, ctx, sdk.NewCoins())
   298  
   299  		pool, err := createPool(simapp, ctx, X, Y, DenomX, DenomY)
   300  		require.NoError(t, err)
   301  
   302  		for i := 0; i < 100; i++ {
   303  			poolBalances := simapp.BankKeeper.GetAllBalances(ctx, pool.GetReserveAccount())
   304  			RX := poolBalances.AmountOf(DenomX)
   305  			RY := poolBalances.AmountOf(DenomY)
   306  			poolPrice := RX.ToDec().Quo(RY.ToDec())
   307  
   308  			offerAmt := RY.ToDec().Mul(sdk.NewDecFromIntWithPrec(app.GetRandRange(r, 1, 100_000_000_000_000_000), sdk.Precision))    // RY * (0, 0.1)
   309  			offerAmtWithFee := offerAmt.Quo(sdk.OneDec().Add(params.SwapFeeRate.QuoInt64(2))).TruncateInt()                          // offerAmt / (1 + swapFeeRate/2)
   310  			orderPrice := poolPrice.Mul(sdk.NewDecFromIntWithPrec(app.GetRandRange(r, 1, 1_000_000_000_000_000_000), sdk.Precision)) // poolPrice * (0, 1)
   311  
   312  			app.SaveAccount(simapp, ctx, addr, sdk.NewCoins(sdk.NewCoin(DenomY, offerAmt.Ceil().TruncateInt())))
   313  
   314  			liquidity.BeginBlocker(ctx, simapp.LiquidityKeeper)
   315  
   316  			_, err := simapp.LiquidityKeeper.SwapWithinBatch(ctx, types.NewMsgSwapWithinBatch(
   317  				addr, pool.Id, types.DefaultSwapTypeID, sdk.NewCoin(DenomY, offerAmtWithFee), DenomX, orderPrice, params.SwapFeeRate), 0)
   318  			require.NoError(t, err)
   319  
   320  			liquidity.EndBlocker(ctx, simapp.LiquidityKeeper)
   321  		}
   322  
   323  		require.True(t, simapp.BankKeeper.GetAllBalances(ctx, simapp.AccountKeeper.GetModuleAddress(types.ModuleName)).IsZero(), "there must be no remaining coin escrow")
   324  	}
   325  }
   326  
   327  func TestSwapWithDepletedPool(t *testing.T) {
   328  	simapp, ctx, pool, creatorAddr, err := createTestPool(sdk.NewInt64Coin(DenomX, 1000000), sdk.NewInt64Coin(DenomY, 1000000))
   329  	require.NoError(t, err)
   330  	params := simapp.LiquidityKeeper.GetParams(ctx)
   331  
   332  	liquidity.BeginBlocker(ctx, simapp.LiquidityKeeper)
   333  	pc := simapp.BankKeeper.GetBalance(ctx, creatorAddr, pool.PoolCoinDenom)
   334  	_, err = simapp.LiquidityKeeper.WithdrawWithinBatch(ctx, types.NewMsgWithdrawWithinBatch(creatorAddr, pool.Id, pc))
   335  	require.NoError(t, err)
   336  	liquidity.EndBlocker(ctx, simapp.LiquidityKeeper)
   337  
   338  	addr := app.AddRandomTestAddr(simapp, ctx, sdk.NewCoins(sdk.NewInt64Coin(DenomX, 100000)))
   339  	offerCoin := sdk.NewInt64Coin(DenomX, 10000)
   340  	orderPrice := sdk.MustNewDecFromStr("1.0")
   341  	liquidity.BeginBlocker(ctx, simapp.LiquidityKeeper)
   342  	_, err = simapp.LiquidityKeeper.SwapWithinBatch(
   343  		ctx,
   344  		types.NewMsgSwapWithinBatch(addr, pool.Id, types.DefaultSwapTypeID, offerCoin, DenomY, orderPrice, params.SwapFeeRate),
   345  		0)
   346  	require.ErrorIs(t, err, types.ErrDepletedPool)
   347  	liquidity.EndBlocker(ctx, simapp.LiquidityKeeper)
   348  }
   349  
   350  func createPool(simapp *app.LiquidityApp, ctx sdk.Context, X, Y sdk.Int, denomX, denomY string) (types.Pool, error) {
   351  	params := simapp.LiquidityKeeper.GetParams(ctx)
   352  
   353  	coins := sdk.NewCoins(sdk.NewCoin(denomX, X), sdk.NewCoin(denomY, Y))
   354  	addr := app.AddRandomTestAddr(simapp, ctx, coins.Add(params.PoolCreationFee...))
   355  
   356  	return simapp.LiquidityKeeper.CreatePool(ctx, types.NewMsgCreatePool(addr, types.DefaultPoolTypeID, coins))
   357  }