github.com/KiraCore/sekai@v0.3.43/x/basket/keeper/mint_burn_swap_test.go (about)

     1  package keeper_test
     2  
     3  import (
     4  	"time"
     5  
     6  	"github.com/KiraCore/sekai/x/basket/types"
     7  	"github.com/cometbft/cometbft/crypto/ed25519"
     8  	sdk "github.com/cosmos/cosmos-sdk/types"
     9  	minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
    10  )
    11  
    12  func (suite *KeeperTestSuite) TestMintBasketToken() {
    13  	testCases := map[string]struct {
    14  		basketId          uint64
    15  		mintDisabled      bool
    16  		userBalance       sdk.Coins
    17  		depositBalance    sdk.Coins
    18  		denomDisabled     bool
    19  		minDepositAmount  sdk.Int
    20  		maxDepositAmount  sdk.Int
    21  		limitPeriod       uint64
    22  		prevDepositAmount sdk.Int
    23  		tokensCap         sdk.Dec
    24  		expectErr         bool
    25  		expectedOutAmount sdk.Int
    26  	}{
    27  		"case not available basket": {
    28  			basketId:          0,
    29  			mintDisabled:      false,
    30  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin("ukex", 1000_000)),
    31  			depositBalance:    sdk.NewCoins(sdk.NewInt64Coin("ukex", 1000_000)),
    32  			denomDisabled:     false,
    33  			minDepositAmount:  sdk.NewInt(1000_000),
    34  			maxDepositAmount:  sdk.NewInt(100_000_000),
    35  			limitPeriod:       3600,
    36  			prevDepositAmount: sdk.NewInt(0),
    37  			tokensCap:         sdk.NewDec(1),
    38  			expectErr:         true,
    39  			expectedOutAmount: sdk.NewInt(0),
    40  		},
    41  		"case mint disabled basket": {
    42  			basketId:          1,
    43  			mintDisabled:      true,
    44  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin("ukex", 1000_000)),
    45  			depositBalance:    sdk.NewCoins(sdk.NewInt64Coin("ukex", 1000_000)),
    46  			denomDisabled:     false,
    47  			minDepositAmount:  sdk.NewInt(1000_000),
    48  			maxDepositAmount:  sdk.NewInt(100_000_000),
    49  			limitPeriod:       3600,
    50  			prevDepositAmount: sdk.NewInt(0),
    51  			tokensCap:         sdk.NewDec(1),
    52  			expectErr:         true,
    53  			expectedOutAmount: sdk.NewInt(0),
    54  		},
    55  		"case not enough balance on user": {
    56  			basketId:          1,
    57  			mintDisabled:      true,
    58  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin("ukex", 100_000)),
    59  			depositBalance:    sdk.NewCoins(sdk.NewInt64Coin("ukex", 1000_000)),
    60  			denomDisabled:     false,
    61  			minDepositAmount:  sdk.NewInt(1000_000),
    62  			maxDepositAmount:  sdk.NewInt(100_000_000),
    63  			limitPeriod:       3600,
    64  			prevDepositAmount: sdk.NewInt(0),
    65  			tokensCap:         sdk.NewDec(1),
    66  			expectErr:         true,
    67  			expectedOutAmount: sdk.NewInt(0),
    68  		},
    69  		"case not deposit denom": {
    70  			basketId:          1,
    71  			mintDisabled:      false,
    72  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin("xxx", 1000_000)),
    73  			depositBalance:    sdk.NewCoins(sdk.NewInt64Coin("xxx", 1000_000)),
    74  			denomDisabled:     false,
    75  			minDepositAmount:  sdk.NewInt(1000_000),
    76  			maxDepositAmount:  sdk.NewInt(100_000_000),
    77  			limitPeriod:       3600,
    78  			prevDepositAmount: sdk.NewInt(0),
    79  			tokensCap:         sdk.NewDec(1),
    80  			expectErr:         true,
    81  			expectedOutAmount: sdk.NewInt(0),
    82  		},
    83  		"case deposit denom disabled": {
    84  			basketId:          1,
    85  			mintDisabled:      false,
    86  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin("ukex", 1000_000)),
    87  			depositBalance:    sdk.NewCoins(sdk.NewInt64Coin("ukex", 1000_000)),
    88  			denomDisabled:     true,
    89  			minDepositAmount:  sdk.NewInt(1000_000),
    90  			maxDepositAmount:  sdk.NewInt(100_000_000),
    91  			limitPeriod:       3600,
    92  			prevDepositAmount: sdk.NewInt(0),
    93  			tokensCap:         sdk.NewDec(1),
    94  			expectErr:         true,
    95  			expectedOutAmount: sdk.NewInt(0),
    96  		},
    97  		"case lower than deposit min amount": {
    98  			basketId:          1,
    99  			mintDisabled:      false,
   100  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin("ukex", 100_000)),
   101  			depositBalance:    sdk.NewCoins(sdk.NewInt64Coin("ukex", 100_000)),
   102  			denomDisabled:     false,
   103  			minDepositAmount:  sdk.NewInt(1000_000),
   104  			maxDepositAmount:  sdk.NewInt(100_000_000),
   105  			limitPeriod:       3600,
   106  			prevDepositAmount: sdk.NewInt(0),
   107  			tokensCap:         sdk.NewDec(1),
   108  			expectErr:         true,
   109  			expectedOutAmount: sdk.NewInt(0),
   110  		},
   111  		"case exceeding deposit max amount during limit period": {
   112  			basketId:          1,
   113  			mintDisabled:      false,
   114  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin("ukex", 2000_000)),
   115  			depositBalance:    sdk.NewCoins(sdk.NewInt64Coin("ukex", 2000_000)),
   116  			denomDisabled:     false,
   117  			minDepositAmount:  sdk.NewInt(1000_000),
   118  			maxDepositAmount:  sdk.NewInt(100_000_000),
   119  			limitPeriod:       3600,
   120  			prevDepositAmount: sdk.NewInt(99_000_000),
   121  			tokensCap:         sdk.NewDec(1),
   122  			expectErr:         true,
   123  			expectedOutAmount: sdk.NewInt(0),
   124  		},
   125  		"case tokens cap is broken": {
   126  			basketId:          1,
   127  			mintDisabled:      false,
   128  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin("ukex", 2000_000)),
   129  			depositBalance:    sdk.NewCoins(sdk.NewInt64Coin("ukex", 2000_000)),
   130  			denomDisabled:     false,
   131  			minDepositAmount:  sdk.NewInt(1000_000),
   132  			maxDepositAmount:  sdk.NewInt(100_000_000),
   133  			limitPeriod:       3600,
   134  			prevDepositAmount: sdk.NewInt(0),
   135  			tokensCap:         sdk.NewDecWithPrec(8, 1), // 80%
   136  			expectErr:         true,
   137  			expectedOutAmount: sdk.NewInt(0),
   138  		},
   139  		"case one token successful deposit": {
   140  			basketId:          1,
   141  			mintDisabled:      false,
   142  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin("ukex", 1000_000)),
   143  			depositBalance:    sdk.NewCoins(sdk.NewInt64Coin("ukex", 1000_000)),
   144  			denomDisabled:     false,
   145  			minDepositAmount:  sdk.NewInt(1000_000),
   146  			maxDepositAmount:  sdk.NewInt(100_000_000),
   147  			limitPeriod:       3600,
   148  			prevDepositAmount: sdk.NewInt(0),
   149  			tokensCap:         sdk.NewDec(1),
   150  			expectErr:         false,
   151  			expectedOutAmount: sdk.NewInt(1000_000),
   152  		},
   153  		"case two tokens successful deposit": {
   154  			basketId:          1,
   155  			mintDisabled:      false,
   156  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin("ukex", 1000_000), sdk.NewInt64Coin("ueth", 100_000)),
   157  			depositBalance:    sdk.NewCoins(sdk.NewInt64Coin("ukex", 1000_000), sdk.NewInt64Coin("ueth", 100_000)),
   158  			denomDisabled:     false,
   159  			minDepositAmount:  sdk.NewInt(1000_000),
   160  			maxDepositAmount:  sdk.NewInt(100_000_000),
   161  			limitPeriod:       3600,
   162  			prevDepositAmount: sdk.NewInt(0),
   163  			tokensCap:         sdk.NewDec(1),
   164  			expectErr:         false,
   165  			expectedOutAmount: sdk.NewInt(2000_000),
   166  		},
   167  	}
   168  
   169  	for name, tc := range testCases {
   170  		tc := tc
   171  
   172  		suite.Run(name, func() {
   173  			suite.SetupTest()
   174  			now := time.Now().UTC()
   175  			suite.ctx = suite.ctx.WithBlockTime(now)
   176  
   177  			addr1 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address().Bytes())
   178  			basket := types.Basket{
   179  				Id:              1,
   180  				Suffix:          "usd",
   181  				Description:     "usd basket",
   182  				LimitsPeriod:    tc.limitPeriod,
   183  				Amount:          sdk.NewInt(0),
   184  				SwapFee:         sdk.NewDecWithPrec(1, 2), // 1%
   185  				SlipppageFeeMin: sdk.NewDecWithPrec(1, 2), // 1%
   186  				TokensCap:       tc.tokensCap,
   187  				MintsMin:        tc.minDepositAmount,
   188  				MintsMax:        tc.maxDepositAmount,
   189  				MintsDisabled:   tc.mintDisabled,
   190  				BurnsMin:        sdk.NewInt(1),
   191  				BurnsMax:        sdk.NewInt(100000000),
   192  				BurnsDisabled:   false,
   193  				SwapsMin:        sdk.NewInt(1),
   194  				SwapsMax:        sdk.NewInt(100000000),
   195  				SwapsDisabled:   false,
   196  				Tokens: []types.BasketToken{
   197  					{
   198  						Denom:     "ukex",
   199  						Weight:    sdk.NewDec(1),
   200  						Amount:    sdk.NewInt(0),
   201  						Deposits:  !tc.denomDisabled,
   202  						Withdraws: !tc.denomDisabled,
   203  						Swaps:     !tc.denomDisabled,
   204  					},
   205  					{
   206  						Denom:     "ueth",
   207  						Weight:    sdk.NewDec(10),
   208  						Amount:    sdk.NewInt(0),
   209  						Deposits:  !tc.denomDisabled,
   210  						Withdraws: !tc.denomDisabled,
   211  						Swaps:     !tc.denomDisabled,
   212  					},
   213  				},
   214  				Surplus: sdk.NewCoins(sdk.NewInt64Coin("usd2", 1)),
   215  			}
   216  			suite.app.BasketKeeper.SetBasket(suite.ctx, basket)
   217  
   218  			suite.app.BasketKeeper.SetMintAmount(suite.ctx, now.Add(time.Second*60), tc.basketId, tc.prevDepositAmount)
   219  
   220  			err := suite.app.BankKeeper.MintCoins(suite.ctx, minttypes.ModuleName, tc.userBalance)
   221  			suite.Require().NoError(err)
   222  			err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, minttypes.ModuleName, addr1, tc.userBalance)
   223  			suite.Require().NoError(err)
   224  
   225  			err = suite.app.BasketKeeper.MintBasketToken(suite.ctx, &types.MsgBasketTokenMint{
   226  				Sender:   addr1.String(),
   227  				BasketId: tc.basketId,
   228  				Deposit:  tc.depositBalance,
   229  			})
   230  			if tc.expectErr {
   231  				suite.Require().Error(err)
   232  			} else {
   233  				suite.Require().NoError(err)
   234  
   235  				// check basket tokens balance increased
   236  				balance := suite.app.BankKeeper.GetBalance(suite.ctx, addr1, basket.GetBasketDenom())
   237  				suite.Require().Equal(balance.Amount, tc.expectedOutAmount)
   238  
   239  				// check user's deposit balance decrease
   240  				balances := suite.app.BankKeeper.GetAllBalances(suite.ctx, addr1)
   241  				suite.Require().Equal(balances, sdk.Coins{balance})
   242  
   243  				// check basket total amount increased
   244  				savedBasket, err := suite.app.BasketKeeper.GetBasketById(suite.ctx, basket.Id)
   245  				suite.Require().NoError(err)
   246  				suite.Require().Equal(savedBasket.Amount, tc.expectedOutAmount)
   247  
   248  				// check basket tokens balance increase
   249  				basketUnderlyingCoins := sdk.Coins{}
   250  				for _, token := range savedBasket.Tokens {
   251  					basketUnderlyingCoins = basketUnderlyingCoins.Add(sdk.NewCoin(token.Denom, token.Amount))
   252  				}
   253  				suite.Require().Equal(basketUnderlyingCoins, tc.depositBalance)
   254  
   255  				// check limit period amount increased
   256  				historicalAmount := suite.app.BasketKeeper.GetLimitsPeriodMintAmount(suite.ctx, 1, tc.limitPeriod)
   257  				suite.Require().Equal(historicalAmount, tc.prevDepositAmount.Add(tc.expectedOutAmount))
   258  			}
   259  		})
   260  	}
   261  }
   262  
   263  func (suite *KeeperTestSuite) TestBurnBasketToken() {
   264  	sampleBasket := types.Basket{
   265  		Id:          1,
   266  		Suffix:      "usd",
   267  		Description: "usd basket",
   268  	}
   269  	basketDenom := sampleBasket.GetBasketDenom()
   270  
   271  	testCases := map[string]struct {
   272  		basketId          uint64
   273  		burnDisabled      bool
   274  		userBalance       sdk.Coins
   275  		burnBalance       sdk.Coin
   276  		denomDisabled     bool
   277  		minBurnAmount     sdk.Int
   278  		maxBurnAmount     sdk.Int
   279  		limitPeriod       uint64
   280  		prevBurnAmount    sdk.Int
   281  		tokensCap         sdk.Dec
   282  		expectErr         bool
   283  		expectedOutAmount sdk.Coins
   284  	}{
   285  		"case not available basket": {
   286  			basketId:          0,
   287  			burnDisabled:      false,
   288  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin(basketDenom, 1000_000)),
   289  			burnBalance:       sdk.NewInt64Coin(basketDenom, 1000_000),
   290  			denomDisabled:     false,
   291  			minBurnAmount:     sdk.NewInt(1000_000),
   292  			maxBurnAmount:     sdk.NewInt(100_000_000),
   293  			limitPeriod:       3600,
   294  			prevBurnAmount:    sdk.NewInt(0),
   295  			tokensCap:         sdk.NewDec(1),
   296  			expectErr:         true,
   297  			expectedOutAmount: sdk.NewCoins(),
   298  		},
   299  		"case burn disabled basket": {
   300  			basketId:          1,
   301  			burnDisabled:      true,
   302  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin(basketDenom, 1000_000)),
   303  			burnBalance:       sdk.NewInt64Coin(basketDenom, 1000_000),
   304  			denomDisabled:     false,
   305  			minBurnAmount:     sdk.NewInt(1000_000),
   306  			maxBurnAmount:     sdk.NewInt(100_000_000),
   307  			limitPeriod:       3600,
   308  			prevBurnAmount:    sdk.NewInt(0),
   309  			tokensCap:         sdk.NewDec(1),
   310  			expectErr:         true,
   311  			expectedOutAmount: sdk.NewCoins(),
   312  		},
   313  		"case not enough balance on user": {
   314  			basketId:          1,
   315  			burnDisabled:      true,
   316  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin(basketDenom, 100_000)),
   317  			burnBalance:       sdk.NewInt64Coin(basketDenom, 1000_000),
   318  			denomDisabled:     false,
   319  			minBurnAmount:     sdk.NewInt(1000_000),
   320  			maxBurnAmount:     sdk.NewInt(100_000_000),
   321  			limitPeriod:       3600,
   322  			prevBurnAmount:    sdk.NewInt(0),
   323  			tokensCap:         sdk.NewDec(1),
   324  			expectErr:         true,
   325  			expectedOutAmount: sdk.NewCoins(),
   326  		},
   327  		"case not basket denom": {
   328  			basketId:          1,
   329  			burnDisabled:      false,
   330  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin("xxx", 1000_000)),
   331  			burnBalance:       sdk.NewInt64Coin("xxx", 1000_000),
   332  			denomDisabled:     false,
   333  			minBurnAmount:     sdk.NewInt(1000_000),
   334  			maxBurnAmount:     sdk.NewInt(100_000_000),
   335  			limitPeriod:       3600,
   336  			prevBurnAmount:    sdk.NewInt(0),
   337  			tokensCap:         sdk.NewDec(1),
   338  			expectErr:         true,
   339  			expectedOutAmount: sdk.NewCoins(),
   340  		},
   341  		"case withdraw denom disabled": {
   342  			basketId:          1,
   343  			burnDisabled:      false,
   344  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin(basketDenom, 1000_000)),
   345  			burnBalance:       sdk.NewInt64Coin(basketDenom, 1000_000),
   346  			denomDisabled:     true,
   347  			minBurnAmount:     sdk.NewInt(1000_000),
   348  			maxBurnAmount:     sdk.NewInt(100_000_000),
   349  			limitPeriod:       3600,
   350  			prevBurnAmount:    sdk.NewInt(0),
   351  			tokensCap:         sdk.NewDec(1),
   352  			expectErr:         true,
   353  			expectedOutAmount: sdk.NewCoins(),
   354  		},
   355  		"case lower than withdraw min amount": {
   356  			basketId:          1,
   357  			burnDisabled:      false,
   358  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin(basketDenom, 100_000)),
   359  			burnBalance:       sdk.NewInt64Coin(basketDenom, 100_000),
   360  			denomDisabled:     false,
   361  			minBurnAmount:     sdk.NewInt(1000_000),
   362  			maxBurnAmount:     sdk.NewInt(100_000_000),
   363  			limitPeriod:       3600,
   364  			prevBurnAmount:    sdk.NewInt(0),
   365  			tokensCap:         sdk.NewDec(1),
   366  			expectErr:         true,
   367  			expectedOutAmount: sdk.NewCoins(),
   368  		},
   369  		"case exceeding withdraw max amount during limit period": {
   370  			basketId:          1,
   371  			burnDisabled:      false,
   372  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin(basketDenom, 2000_000)),
   373  			burnBalance:       sdk.NewInt64Coin(basketDenom, 2000_000),
   374  			denomDisabled:     false,
   375  			minBurnAmount:     sdk.NewInt(1000_000),
   376  			maxBurnAmount:     sdk.NewInt(100_000_000),
   377  			limitPeriod:       3600,
   378  			prevBurnAmount:    sdk.NewInt(99_000_000),
   379  			tokensCap:         sdk.NewDec(1),
   380  			expectErr:         true,
   381  			expectedOutAmount: sdk.NewCoins(),
   382  		},
   383  		"case successful withdraw": {
   384  			basketId:          1,
   385  			burnDisabled:      false,
   386  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin(basketDenom, 1000_000)),
   387  			burnBalance:       sdk.NewInt64Coin(basketDenom, 1000_000),
   388  			denomDisabled:     false,
   389  			minBurnAmount:     sdk.NewInt(1000_000),
   390  			maxBurnAmount:     sdk.NewInt(100_000_000),
   391  			limitPeriod:       3600,
   392  			prevBurnAmount:    sdk.NewInt(0),
   393  			tokensCap:         sdk.NewDec(1),
   394  			expectErr:         false,
   395  			expectedOutAmount: sdk.NewCoins(sdk.NewInt64Coin("ukex", 500_000), sdk.NewInt64Coin("ueth", 50_000)),
   396  		},
   397  	}
   398  
   399  	for name, tc := range testCases {
   400  		tc := tc
   401  
   402  		suite.Run(name, func() {
   403  			suite.SetupTest()
   404  			now := time.Now().UTC()
   405  			suite.ctx = suite.ctx.WithBlockTime(now)
   406  
   407  			addr1 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address().Bytes())
   408  			addr2 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address().Bytes())
   409  			basket := types.Basket{
   410  				Id:              1,
   411  				Suffix:          "usd",
   412  				Description:     "usd basket",
   413  				LimitsPeriod:    tc.limitPeriod,
   414  				Amount:          sdk.NewInt(0),
   415  				SwapFee:         sdk.NewDecWithPrec(1, 2), // 1%
   416  				SlipppageFeeMin: sdk.NewDecWithPrec(1, 2), // 1%
   417  				TokensCap:       tc.tokensCap,
   418  				MintsMin:        sdk.NewInt(1),
   419  				MintsMax:        sdk.NewInt(100000000),
   420  				MintsDisabled:   false,
   421  				BurnsMin:        tc.minBurnAmount,
   422  				BurnsMax:        tc.maxBurnAmount,
   423  				BurnsDisabled:   tc.burnDisabled,
   424  				SwapsMin:        sdk.NewInt(1),
   425  				SwapsMax:        sdk.NewInt(100000000),
   426  				SwapsDisabled:   false,
   427  				Tokens: []types.BasketToken{
   428  					{
   429  						Denom:     "ukex",
   430  						Weight:    sdk.NewDec(1),
   431  						Amount:    sdk.NewInt(0),
   432  						Deposits:  true,
   433  						Withdraws: !tc.denomDisabled,
   434  						Swaps:     !tc.denomDisabled,
   435  					},
   436  					{
   437  						Denom:     "ueth",
   438  						Weight:    sdk.NewDec(10),
   439  						Amount:    sdk.NewInt(0),
   440  						Deposits:  true,
   441  						Withdraws: !tc.denomDisabled,
   442  						Swaps:     !tc.denomDisabled,
   443  					},
   444  				},
   445  				Surplus: sdk.NewCoins(sdk.NewInt64Coin("usd2", 1)),
   446  			}
   447  			suite.app.BasketKeeper.SetBasket(suite.ctx, basket)
   448  
   449  			depositCoins := sdk.NewCoins(sdk.NewInt64Coin("ukex", 1000_000), sdk.NewInt64Coin("ueth", 100_000))
   450  			err := suite.app.BankKeeper.MintCoins(suite.ctx, minttypes.ModuleName, depositCoins)
   451  			suite.Require().NoError(err)
   452  			err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, minttypes.ModuleName, addr1, depositCoins)
   453  			suite.Require().NoError(err)
   454  
   455  			err = suite.app.BasketKeeper.MintBasketToken(suite.ctx, &types.MsgBasketTokenMint{
   456  				Sender:   addr1.String(),
   457  				BasketId: 1,
   458  				Deposit:  depositCoins,
   459  			})
   460  			suite.Require().NoError(err)
   461  
   462  			err = suite.app.BankKeeper.MintCoins(suite.ctx, minttypes.ModuleName, tc.userBalance)
   463  			suite.Require().NoError(err)
   464  			err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, minttypes.ModuleName, addr2, tc.userBalance)
   465  			suite.Require().NoError(err)
   466  
   467  			suite.app.BasketKeeper.SetBurnAmount(suite.ctx, now.Add(time.Second*60), tc.basketId, tc.prevBurnAmount)
   468  
   469  			err = suite.app.BasketKeeper.BurnBasketToken(suite.ctx, &types.MsgBasketTokenBurn{
   470  				Sender:     addr2.String(),
   471  				BasketId:   tc.basketId,
   472  				BurnAmount: tc.burnBalance,
   473  			})
   474  
   475  			if tc.expectErr {
   476  				suite.Require().Error(err)
   477  			} else {
   478  				suite.Require().NoError(err)
   479  
   480  				// check basket tokens balance decreased
   481  				balance := suite.app.BankKeeper.GetBalance(suite.ctx, addr2, basket.GetBasketDenom())
   482  				suite.Require().Equal(balance.Amount, sdk.ZeroInt())
   483  
   484  				// check user's withdraw balance increase
   485  				balances := suite.app.BankKeeper.GetAllBalances(suite.ctx, addr2)
   486  				suite.Require().Equal(balances, tc.expectedOutAmount)
   487  
   488  				// check basket total amount increased
   489  				savedBasket, err := suite.app.BasketKeeper.GetBasketById(suite.ctx, basket.Id)
   490  				suite.Require().NoError(err)
   491  				suite.Require().Equal(savedBasket.Amount.Add(tc.burnBalance.Amount), sdk.NewInt(2000_000))
   492  
   493  				// check basket tokens balance increase
   494  				basketUnderlyingCoins := sdk.Coins{}
   495  				for _, token := range savedBasket.Tokens {
   496  					basketUnderlyingCoins = basketUnderlyingCoins.Add(sdk.NewCoin(token.Denom, token.Amount))
   497  				}
   498  				suite.Require().Equal(basketUnderlyingCoins.Add(tc.expectedOutAmount...).String(), depositCoins.String())
   499  
   500  				// check limit period amount increased
   501  				historicalAmount := suite.app.BasketKeeper.GetLimitsPeriodBurnAmount(suite.ctx, 1, tc.limitPeriod)
   502  				suite.Require().Equal(historicalAmount, tc.prevBurnAmount.Add(tc.burnBalance.Amount))
   503  			}
   504  		})
   505  	}
   506  }
   507  
   508  func (suite *KeeperTestSuite) TestBasketSwap() {
   509  	testCases := map[string]struct {
   510  		basketId          uint64
   511  		swapDisabled      bool
   512  		userBalance       sdk.Coins
   513  		swapBalance       sdk.Coin
   514  		denomDisabled     bool
   515  		minSwapAmount     sdk.Int
   516  		maxSwapAmount     sdk.Int
   517  		limitPeriod       uint64
   518  		prevSwapAmount    sdk.Int
   519  		tokensCap         sdk.Dec
   520  		expectErr         bool
   521  		expectedOutAmount sdk.Coins
   522  	}{
   523  		"case not available basket": {
   524  			basketId:          0,
   525  			swapDisabled:      false,
   526  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin("ukex", 1000_000)),
   527  			swapBalance:       sdk.NewInt64Coin("ukex", 100_000),
   528  			denomDisabled:     false,
   529  			minSwapAmount:     sdk.NewInt(100_000),
   530  			maxSwapAmount:     sdk.NewInt(100_000_000),
   531  			limitPeriod:       3600,
   532  			prevSwapAmount:    sdk.NewInt(0),
   533  			tokensCap:         sdk.NewDec(1),
   534  			expectErr:         true,
   535  			expectedOutAmount: sdk.NewCoins(),
   536  		},
   537  		"case swap disabled basket": {
   538  			basketId:          1,
   539  			swapDisabled:      true,
   540  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin("ukex", 1000_000)),
   541  			swapBalance:       sdk.NewInt64Coin("ukex", 100_000),
   542  			denomDisabled:     false,
   543  			minSwapAmount:     sdk.NewInt(100_000),
   544  			maxSwapAmount:     sdk.NewInt(100_000_000),
   545  			limitPeriod:       3600,
   546  			prevSwapAmount:    sdk.NewInt(0),
   547  			tokensCap:         sdk.NewDec(1),
   548  			expectErr:         true,
   549  			expectedOutAmount: sdk.NewCoins(),
   550  		},
   551  		"case not enough balance on user": {
   552  			basketId:          1,
   553  			swapDisabled:      true,
   554  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin("ukex", 100)),
   555  			swapBalance:       sdk.NewInt64Coin("ukex", 1000),
   556  			denomDisabled:     false,
   557  			minSwapAmount:     sdk.NewInt(100_000),
   558  			maxSwapAmount:     sdk.NewInt(100_000_000),
   559  			limitPeriod:       3600,
   560  			prevSwapAmount:    sdk.NewInt(0),
   561  			tokensCap:         sdk.NewDec(1),
   562  			expectErr:         true,
   563  			expectedOutAmount: sdk.NewCoins(),
   564  		},
   565  		"case not basket denom": {
   566  			basketId:          1,
   567  			swapDisabled:      false,
   568  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin("xxx", 100_000)),
   569  			swapBalance:       sdk.NewInt64Coin("xxx", 100_000),
   570  			denomDisabled:     false,
   571  			minSwapAmount:     sdk.NewInt(100_000),
   572  			maxSwapAmount:     sdk.NewInt(100_000_000),
   573  			limitPeriod:       3600,
   574  			prevSwapAmount:    sdk.NewInt(0),
   575  			tokensCap:         sdk.NewDec(1),
   576  			expectErr:         true,
   577  			expectedOutAmount: sdk.NewCoins(),
   578  		},
   579  		"case swap denom disabled": {
   580  			basketId:          1,
   581  			swapDisabled:      false,
   582  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin("ukex", 100_000)),
   583  			swapBalance:       sdk.NewInt64Coin("ukex", 100_000),
   584  			denomDisabled:     true,
   585  			minSwapAmount:     sdk.NewInt(100_000),
   586  			maxSwapAmount:     sdk.NewInt(100_000_000),
   587  			limitPeriod:       3600,
   588  			prevSwapAmount:    sdk.NewInt(0),
   589  			tokensCap:         sdk.NewDec(1),
   590  			expectErr:         true,
   591  			expectedOutAmount: sdk.NewCoins(),
   592  		},
   593  		"case lower than swap min amount": {
   594  			basketId:          1,
   595  			swapDisabled:      false,
   596  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin("ukex", 10_000)),
   597  			swapBalance:       sdk.NewInt64Coin("ukex", 10_000),
   598  			denomDisabled:     false,
   599  			minSwapAmount:     sdk.NewInt(100_000),
   600  			maxSwapAmount:     sdk.NewInt(100_000_000),
   601  			limitPeriod:       3600,
   602  			prevSwapAmount:    sdk.NewInt(0),
   603  			tokensCap:         sdk.NewDec(1),
   604  			expectErr:         true,
   605  			expectedOutAmount: sdk.NewCoins(),
   606  		},
   607  		"case exceeding swap max amount during limit period": {
   608  			basketId:          1,
   609  			swapDisabled:      false,
   610  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin("ukex", 2000_000)),
   611  			swapBalance:       sdk.NewInt64Coin("ukex", 200_000),
   612  			denomDisabled:     false,
   613  			minSwapAmount:     sdk.NewInt(100_000),
   614  			maxSwapAmount:     sdk.NewInt(100_000_000),
   615  			limitPeriod:       3600,
   616  			prevSwapAmount:    sdk.NewInt(99_900_000),
   617  			tokensCap:         sdk.NewDec(1),
   618  			expectErr:         true,
   619  			expectedOutAmount: sdk.NewCoins(),
   620  		},
   621  		"case tokens cap broken": {
   622  			basketId:          1,
   623  			swapDisabled:      false,
   624  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin("ukex", 500_000)),
   625  			swapBalance:       sdk.NewInt64Coin("ukex", 500_000),
   626  			denomDisabled:     false,
   627  			minSwapAmount:     sdk.NewInt(100_000),
   628  			maxSwapAmount:     sdk.NewInt(100_000_000),
   629  			limitPeriod:       3600,
   630  			prevSwapAmount:    sdk.NewInt(99_000_000),
   631  			tokensCap:         sdk.NewDecWithPrec(6, 1), // 60%
   632  			expectErr:         true,
   633  			expectedOutAmount: sdk.NewCoins(),
   634  		},
   635  		"case successful swap": {
   636  			basketId:          1,
   637  			swapDisabled:      false,
   638  			userBalance:       sdk.NewCoins(sdk.NewInt64Coin("ukex", 100_000)),
   639  			swapBalance:       sdk.NewInt64Coin("ukex", 100_000),
   640  			denomDisabled:     false,
   641  			minSwapAmount:     sdk.NewInt(100_000),
   642  			maxSwapAmount:     sdk.NewInt(100_000_000),
   643  			limitPeriod:       3600,
   644  			prevSwapAmount:    sdk.NewInt(0),
   645  			tokensCap:         sdk.NewDec(1),
   646  			expectErr:         false,
   647  			expectedOutAmount: sdk.NewCoins(sdk.NewInt64Coin("ueth", 8_919)),
   648  		},
   649  	}
   650  
   651  	for name, tc := range testCases {
   652  		tc := tc
   653  
   654  		suite.Run(name, func() {
   655  			suite.SetupTest()
   656  			now := time.Now().UTC()
   657  			suite.ctx = suite.ctx.WithBlockTime(now)
   658  
   659  			addr1 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address().Bytes())
   660  			addr2 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address().Bytes())
   661  			basket := types.Basket{
   662  				Id:              1,
   663  				Suffix:          "usd",
   664  				Description:     "usd basket",
   665  				LimitsPeriod:    tc.limitPeriod,
   666  				Amount:          sdk.NewInt(0),
   667  				SwapFee:         sdk.NewDecWithPrec(1, 2), // 1%
   668  				SlipppageFeeMin: sdk.NewDecWithPrec(1, 2), // 1%
   669  				TokensCap:       tc.tokensCap,
   670  				MintsMin:        sdk.NewInt(1),
   671  				MintsMax:        sdk.NewInt(100000000),
   672  				MintsDisabled:   false,
   673  				BurnsMin:        sdk.NewInt(1),
   674  				BurnsMax:        sdk.NewInt(100000000),
   675  				BurnsDisabled:   false,
   676  				SwapsMin:        tc.minSwapAmount,
   677  				SwapsMax:        tc.maxSwapAmount,
   678  				SwapsDisabled:   tc.swapDisabled,
   679  				Tokens: []types.BasketToken{
   680  					{
   681  						Denom:     "ukex",
   682  						Weight:    sdk.NewDec(1),
   683  						Amount:    sdk.NewInt(0),
   684  						Deposits:  true,
   685  						Withdraws: !tc.denomDisabled,
   686  						Swaps:     !tc.denomDisabled,
   687  					},
   688  					{
   689  						Denom:     "ueth",
   690  						Weight:    sdk.NewDec(10),
   691  						Amount:    sdk.NewInt(0),
   692  						Deposits:  true,
   693  						Withdraws: !tc.denomDisabled,
   694  						Swaps:     !tc.denomDisabled,
   695  					},
   696  				},
   697  				Surplus: sdk.NewCoins(sdk.NewInt64Coin("usd2", 1)),
   698  			}
   699  			suite.app.BasketKeeper.SetBasket(suite.ctx, basket)
   700  
   701  			depositCoins := sdk.NewCoins(sdk.NewInt64Coin("ukex", 1000_000), sdk.NewInt64Coin("ueth", 100_000))
   702  			err := suite.app.BankKeeper.MintCoins(suite.ctx, minttypes.ModuleName, depositCoins)
   703  			suite.Require().NoError(err)
   704  			err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, minttypes.ModuleName, addr1, depositCoins)
   705  			suite.Require().NoError(err)
   706  
   707  			err = suite.app.BasketKeeper.MintBasketToken(suite.ctx, &types.MsgBasketTokenMint{
   708  				Sender:   addr1.String(),
   709  				BasketId: 1,
   710  				Deposit:  depositCoins,
   711  			})
   712  			suite.Require().NoError(err)
   713  
   714  			err = suite.app.BankKeeper.MintCoins(suite.ctx, minttypes.ModuleName, tc.userBalance)
   715  			suite.Require().NoError(err)
   716  			err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, minttypes.ModuleName, addr2, tc.userBalance)
   717  			suite.Require().NoError(err)
   718  
   719  			suite.app.BasketKeeper.SetSwapAmount(suite.ctx, now.Add(time.Second*60), tc.basketId, tc.prevSwapAmount)
   720  
   721  			err = suite.app.BasketKeeper.BasketSwap(suite.ctx, &types.MsgBasketTokenSwap{
   722  				Sender:   addr2.String(),
   723  				BasketId: tc.basketId,
   724  				Pairs: []types.SwapPair{
   725  					{
   726  						InAmount: tc.swapBalance,
   727  						OutToken: "ueth",
   728  					},
   729  				},
   730  			})
   731  
   732  			if tc.expectErr {
   733  				suite.Require().Error(err)
   734  			} else {
   735  				suite.Require().NoError(err)
   736  
   737  				// check in token balance decreased
   738  				balance := suite.app.BankKeeper.GetBalance(suite.ctx, addr2, "ukex")
   739  				suite.Require().Equal(balance.Amount, sdk.ZeroInt())
   740  
   741  				// check user's withdraw balance increase
   742  				balances := suite.app.BankKeeper.GetAllBalances(suite.ctx, addr2)
   743  				suite.Require().Equal(balances.String(), tc.expectedOutAmount.String())
   744  
   745  				// check basket total amount kept as it is
   746  				savedBasket, err := suite.app.BasketKeeper.GetBasketById(suite.ctx, basket.Id)
   747  				suite.Require().NoError(err)
   748  				suite.Require().Equal(savedBasket.Amount, sdk.NewInt(2000_000))
   749  
   750  				// check basket tokens balance changes
   751  				basketUnderlyingCoins := sdk.Coins{}
   752  				for _, token := range savedBasket.Tokens {
   753  					basketUnderlyingCoins = basketUnderlyingCoins.Add(sdk.NewCoin(token.Denom, token.Amount))
   754  				}
   755  				suite.Require().True(basketUnderlyingCoins.Add(tc.expectedOutAmount...).Sub(tc.swapBalance).IsAllLTE(depositCoins))
   756  
   757  				// check limit period amount increased
   758  				historicalAmount := suite.app.BasketKeeper.GetLimitsPeriodSwapAmount(suite.ctx, 1, tc.limitPeriod)
   759  				suite.Require().Equal(historicalAmount, tc.prevSwapAmount.Add(tc.swapBalance.Amount))
   760  
   761  				// check correct slippage amount + surplus
   762  				suite.Require().True(sdk.Coins(savedBasket.Surplus).Sub(basket.Surplus...).IsAllPositive())
   763  			}
   764  		})
   765  	}
   766  }
   767  
   768  func (suite *KeeperTestSuite) TestBasketWithdrawSurplus() {
   769  	suite.SetupTest()
   770  	now := time.Now().UTC()
   771  	suite.ctx = suite.ctx.WithBlockTime(now)
   772  
   773  	addr1 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address().Bytes())
   774  	addr2 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address().Bytes())
   775  	basket := types.Basket{
   776  		Id:              1,
   777  		Suffix:          "usd",
   778  		Description:     "usd basket",
   779  		LimitsPeriod:    3600,
   780  		Amount:          sdk.NewInt(0),
   781  		SwapFee:         sdk.NewDecWithPrec(1, 2), // 1%
   782  		SlipppageFeeMin: sdk.NewDecWithPrec(1, 2), // 1%
   783  		TokensCap:       sdk.NewDecWithPrec(9, 1), // 90%
   784  		MintsMin:        sdk.NewInt(1),
   785  		MintsMax:        sdk.NewInt(100000000),
   786  		MintsDisabled:   false,
   787  		BurnsMin:        sdk.NewInt(1),
   788  		BurnsMax:        sdk.NewInt(100000000),
   789  		BurnsDisabled:   false,
   790  		SwapsMin:        sdk.NewInt(1),
   791  		SwapsMax:        sdk.NewInt(100000000),
   792  		SwapsDisabled:   false,
   793  		Tokens: []types.BasketToken{
   794  			{
   795  				Denom:     "ukex",
   796  				Weight:    sdk.NewDec(1),
   797  				Amount:    sdk.NewInt(0),
   798  				Deposits:  true,
   799  				Withdraws: true,
   800  				Swaps:     true,
   801  			},
   802  			{
   803  				Denom:     "ueth",
   804  				Weight:    sdk.NewDec(10),
   805  				Amount:    sdk.NewInt(0),
   806  				Deposits:  true,
   807  				Withdraws: true,
   808  				Swaps:     true,
   809  			},
   810  		},
   811  		Surplus: sdk.NewCoins(sdk.NewInt64Coin("ueth", 1)),
   812  	}
   813  	suite.app.BasketKeeper.SetBasket(suite.ctx, basket)
   814  
   815  	depositCoins := sdk.NewCoins(sdk.NewInt64Coin("ukex", 1000_000), sdk.NewInt64Coin("ueth", 100_000))
   816  	err := suite.app.BankKeeper.MintCoins(suite.ctx, minttypes.ModuleName, depositCoins)
   817  	suite.Require().NoError(err)
   818  	err = suite.app.BankKeeper.SendCoinsFromModuleToAccount(suite.ctx, minttypes.ModuleName, addr1, depositCoins)
   819  	suite.Require().NoError(err)
   820  
   821  	err = suite.app.BasketKeeper.MintBasketToken(suite.ctx, &types.MsgBasketTokenMint{
   822  		Sender:   addr1.String(),
   823  		BasketId: 1,
   824  		Deposit:  depositCoins,
   825  	})
   826  	suite.Require().NoError(err)
   827  
   828  	err = suite.app.BasketKeeper.BasketWithdrawSurplus(suite.ctx, types.ProposalBasketWithdrawSurplus{
   829  		BasketIds:      []uint64{1},
   830  		WithdrawTarget: addr2.String(),
   831  	})
   832  	suite.Require().NoError(err)
   833  
   834  	// check account balance increased
   835  	balance := suite.app.BankKeeper.GetAllBalances(suite.ctx, addr2)
   836  	suite.Require().Equal(balance, sdk.Coins(basket.Surplus))
   837  
   838  	// check surplus removal
   839  	savedBasket, err := suite.app.BasketKeeper.GetBasketById(suite.ctx, basket.Id)
   840  	suite.Require().NoError(err)
   841  	suite.Require().Nil(savedBasket.Surplus)
   842  }