github.com/Finschia/finschia-sdk@v0.49.1/x/fswap/keeper/keeper_test.go (about) 1 package keeper_test 2 3 import ( 4 "context" 5 "testing" 6 7 "github.com/golang/mock/gomock" 8 "github.com/stretchr/testify/require" 9 "github.com/stretchr/testify/suite" 10 abci "github.com/tendermint/tendermint/abci/types" 11 tmproto "github.com/tendermint/tendermint/proto/tendermint/types" 12 13 "github.com/Finschia/finschia-sdk/crypto/keys/secp256k1" 14 "github.com/Finschia/finschia-sdk/simapp" 15 "github.com/Finschia/finschia-sdk/testutil/testdata" 16 sdk "github.com/Finschia/finschia-sdk/types" 17 sdkerrors "github.com/Finschia/finschia-sdk/types/errors" 18 authtypes "github.com/Finschia/finschia-sdk/x/auth/types" 19 bank "github.com/Finschia/finschia-sdk/x/bank/types" 20 "github.com/Finschia/finschia-sdk/x/foundation" 21 "github.com/Finschia/finschia-sdk/x/fswap/keeper" 22 "github.com/Finschia/finschia-sdk/x/fswap/testutil" 23 "github.com/Finschia/finschia-sdk/x/fswap/types" 24 govtypes "github.com/Finschia/finschia-sdk/x/gov/types" 25 minttypes "github.com/Finschia/finschia-sdk/x/mint/types" 26 ) 27 28 type KeeperTestSuite struct { 29 suite.Suite 30 31 ctx sdk.Context 32 goCtx context.Context 33 keeper keeper.Keeper 34 queryServer types.QueryServer 35 msgServer types.MsgServer 36 37 accWithFromCoin sdk.AccAddress 38 accWithToCoin sdk.AccAddress 39 initBalance sdk.Int 40 41 swap types.Swap 42 toDenomMetadata bank.Metadata 43 } 44 45 func (s *KeeperTestSuite) createRandomAccounts(n int) []sdk.AccAddress { 46 seenAddresses := make(map[string]bool, n) 47 addresses := make([]sdk.AccAddress, n) 48 for i := range addresses { 49 var address sdk.AccAddress 50 for { 51 pk := secp256k1.GenPrivKey().PubKey() 52 address = sdk.AccAddress(pk.Address()) 53 if !seenAddresses[address.String()] { 54 seenAddresses[address.String()] = true 55 break 56 } 57 } 58 addresses[i] = address 59 } 60 return addresses 61 } 62 63 func (s *KeeperTestSuite) SetupTest() { 64 checkTx := false 65 app := simapp.Setup(checkTx) 66 testdata.RegisterInterfaces(app.InterfaceRegistry()) 67 testdata.RegisterMsgServer(app.MsgServiceRouter(), testdata.MsgServerImpl{}) 68 s.ctx = app.BaseApp.NewContext(checkTx, tmproto.Header{}) 69 s.goCtx = sdk.WrapSDKContext(s.ctx) 70 s.keeper = app.FswapKeeper 71 s.queryServer = keeper.NewQueryServer(s.keeper) 72 s.msgServer = keeper.NewMsgServer(s.keeper) 73 74 numAcc := int64(2) 75 s.initBalance = sdk.NewInt(123456789) 76 pebSwapRateForCony, err := sdk.NewDecFromStr("148079656000000") 77 s.Require().NoError(err) 78 swapCap := sdk.NewIntFromBigInt(pebSwapRateForCony.Mul(s.initBalance.ToDec()).BigInt()) 79 swapCap = swapCap.Mul(sdk.NewInt(numAcc)) 80 s.Require().NoError(err) 81 s.swap = types.Swap{ 82 FromDenom: "fromdenom", 83 ToDenom: "todenom", 84 AmountCapForToDenom: swapCap, 85 SwapRate: pebSwapRateForCony, 86 } 87 s.toDenomMetadata = bank.Metadata{ 88 Description: "This is metadata for to-coin", 89 DenomUnits: []*bank.DenomUnit{ 90 {Denom: s.swap.ToDenom, Exponent: 0}, 91 }, 92 Base: s.swap.ToDenom, 93 Display: s.swap.ToDenom, 94 Name: "DUMMY", 95 Symbol: "DUM", 96 } 97 err = s.toDenomMetadata.Validate() 98 s.Require().NoError(err) 99 100 fromDenom := bank.Metadata{ 101 Description: "This is metadata for from-coin", 102 DenomUnits: []*bank.DenomUnit{ 103 {Denom: s.swap.FromDenom, Exponent: 0}, 104 }, 105 Base: s.swap.FromDenom, 106 Display: s.swap.FromDenom, 107 Name: "FROM", 108 Symbol: "FROM", 109 } 110 err = fromDenom.Validate() 111 s.Require().NoError(err) 112 113 app.BankKeeper.SetDenomMetaData(s.ctx, fromDenom) 114 s.createAccountsWithInitBalance(app) 115 app.AccountKeeper.GetModuleAccount(s.ctx, types.ModuleName) 116 } 117 118 func (s *KeeperTestSuite) createAccountsWithInitBalance(app *simapp.SimApp) { 119 addresses := []*sdk.AccAddress{ 120 &s.accWithFromCoin, 121 &s.accWithToCoin, 122 } 123 for i, address := range s.createRandomAccounts(len(addresses)) { 124 *addresses[i] = address 125 } 126 minter := app.AccountKeeper.GetModuleAccount(s.ctx, minttypes.ModuleName).GetAddress() 127 fromAmount := sdk.NewCoins(sdk.NewCoin(s.swap.GetFromDenom(), s.initBalance)) 128 s.Require().NoError(app.BankKeeper.MintCoins(s.ctx, minttypes.ModuleName, fromAmount)) 129 s.Require().NoError(app.BankKeeper.SendCoins(s.ctx, minter, s.accWithFromCoin, fromAmount)) 130 131 toAmount := sdk.NewCoins(sdk.NewCoin(s.swap.GetToDenom(), s.initBalance)) 132 s.Require().NoError(app.BankKeeper.MintCoins(s.ctx, minttypes.ModuleName, toAmount)) 133 s.Require().NoError(app.BankKeeper.SendCoins(s.ctx, minter, s.accWithToCoin, toAmount)) 134 } 135 136 func TestKeeperTestSuite(t *testing.T) { 137 suite.Run(t, &KeeperTestSuite{}) 138 } 139 140 func TestNewKeeper(t *testing.T) { 141 app := simapp.Setup(false) 142 143 ctrl := gomock.NewController(t) 144 defer ctrl.Finish() 145 authKeeper := testutil.NewMockAccountKeeper(ctrl) 146 testCases := map[string]struct { 147 malleate func() 148 isPanic bool 149 }{ 150 "fswap module account has not been set": { 151 malleate: func() { 152 authKeeper.EXPECT().GetModuleAddress(types.ModuleName).Return(nil).Times(1) 153 keeper.NewKeeper(app.AppCodec(), sdk.NewKVStoreKey(types.StoreKey), types.DefaultConfig(), types.DefaultAuthority().String(), authKeeper, app.BankKeeper) 154 }, 155 isPanic: true, 156 }, 157 "fswap authority must be the gov or foundation module account": { 158 malleate: func() { 159 authKeeper.EXPECT().GetModuleAddress(types.ModuleName).Return(authtypes.NewModuleAddress(types.ModuleName)).Times(1) 160 keeper.NewKeeper(app.AppCodec(), sdk.NewKVStoreKey(types.StoreKey), types.DefaultConfig(), authtypes.NewModuleAddress("invalid").String(), authKeeper, app.BankKeeper) 161 }, 162 isPanic: true, 163 }, 164 "success - gov authority": { 165 malleate: func() { 166 authKeeper.EXPECT().GetModuleAddress(types.ModuleName).Return(authtypes.NewModuleAddress(types.ModuleName)).Times(1) 167 keeper.NewKeeper(app.AppCodec(), sdk.NewKVStoreKey(types.StoreKey), types.DefaultConfig(), authtypes.NewModuleAddress(govtypes.ModuleName).String(), authKeeper, app.BankKeeper) 168 }, 169 isPanic: false, 170 }, 171 "success - foundation authority": { 172 malleate: func() { 173 authKeeper.EXPECT().GetModuleAddress(types.ModuleName).Return(authtypes.NewModuleAddress(types.ModuleName)).Times(1) 174 keeper.NewKeeper(app.AppCodec(), sdk.NewKVStoreKey(types.StoreKey), types.DefaultConfig(), authtypes.NewModuleAddress(foundation.ModuleName).String(), authKeeper, app.BankKeeper) 175 }, 176 isPanic: false, 177 }, 178 } 179 for name, tc := range testCases { 180 t.Run(name, func(t *testing.T) { 181 if tc.isPanic { 182 require.Panics(t, tc.malleate) 183 } else { 184 tc.malleate() 185 } 186 }) 187 } 188 } 189 190 func (s *KeeperTestSuite) TestSwap() { 191 swap2ExpectedAmount, ok := sdk.NewIntFromString("296159312000000") 192 s.Require().True(ok) 193 swap100ExpectedAmount, ok := sdk.NewIntFromString("14807965600000000") 194 s.Require().True(ok) 195 swapAllExpectedBalance, ok := sdk.NewIntFromString("18281438845984584000000") 196 s.Require().True(ok) 197 testCases := map[string]struct { 198 from sdk.AccAddress 199 amountToSwap sdk.Coin 200 toDenom string 201 expectedAmount sdk.Int 202 shouldThrowError bool 203 expectedError error 204 }{ 205 "swap 2 from-denom": { 206 s.accWithFromCoin, 207 sdk.NewCoin(s.swap.GetFromDenom(), sdk.NewInt(2)), 208 s.swap.GetToDenom(), 209 swap2ExpectedAmount, 210 false, 211 nil, 212 }, 213 "swap some": { 214 s.accWithFromCoin, 215 sdk.NewCoin(s.swap.GetFromDenom(), sdk.NewInt(100)), 216 s.swap.GetToDenom(), 217 swap100ExpectedAmount, 218 false, 219 nil, 220 }, 221 "swap all the balance": { 222 s.accWithFromCoin, 223 sdk.NewCoin(s.swap.GetFromDenom(), s.initBalance), 224 s.swap.GetToDenom(), 225 swapAllExpectedBalance, 226 false, 227 nil, 228 }, 229 "swap without holding enough balance": { 230 s.accWithFromCoin, 231 sdk.NewCoin(s.swap.GetFromDenom(), sdk.OneInt().Add(s.initBalance)), 232 s.swap.GetToDenom(), 233 sdk.ZeroInt(), 234 true, 235 sdkerrors.ErrInsufficientFunds, 236 }, 237 "account holding new coin only": { 238 s.accWithToCoin, 239 sdk.NewCoin(s.swap.GetFromDenom(), sdk.NewInt(100)), 240 s.swap.GetToDenom(), 241 sdk.ZeroInt(), 242 true, 243 sdkerrors.ErrInsufficientFunds, 244 }, 245 "invalid: unregistered swap": { 246 s.accWithToCoin, 247 sdk.NewCoin(s.swap.GetFromDenom(), sdk.NewInt(100)), 248 "nono", 249 sdk.ZeroInt(), 250 true, 251 sdkerrors.ErrNotFound, 252 }, 253 "invalid: amount exceed": { 254 s.accWithToCoin, 255 sdk.NewCoin(s.swap.GetFromDenom(), s.swap.AmountCapForToDenom.Add(sdk.OneInt())), 256 s.swap.GetToDenom(), 257 sdk.ZeroInt(), 258 true, 259 types.ErrExceedSwappableToCoinAmount, 260 }, 261 } 262 for name, tc := range testCases { 263 s.Run(name, func() { 264 ctx, _ := s.ctx.CacheContext() 265 err := s.keeper.SetSwap(ctx, s.swap, s.toDenomMetadata) 266 s.Require().NoError(err) 267 268 err = s.keeper.Swap(ctx, tc.from, tc.amountToSwap, tc.toDenom) 269 if tc.shouldThrowError { 270 s.Require().ErrorIs(err, tc.expectedError) 271 return 272 } 273 s.Require().NoError(err) 274 275 actualAmount := s.keeper.GetBalance(ctx, tc.from, s.swap.GetToDenom()).Amount 276 s.Require().Equal(tc.expectedAmount, actualAmount) 277 }) 278 } 279 } 280 281 func (s *KeeperTestSuite) TestSetSwap() { 282 ctrl := gomock.NewController(s.T()) 283 defer ctrl.Finish() 284 bankKeeper := testutil.NewMockBankKeeper(ctrl) 285 286 testCases := map[string]struct { 287 swap types.Swap 288 toDenomMeta bank.Metadata 289 malleate func() 290 expectedError error 291 expectedEvents sdk.Events 292 }{ 293 "valid": { 294 types.Swap{ 295 FromDenom: "fromdenom", 296 ToDenom: "todenom", 297 AmountCapForToDenom: sdk.OneInt(), 298 SwapRate: sdk.OneDec(), 299 }, 300 s.toDenomMetadata, 301 func() { 302 bankKeeper.EXPECT().GetDenomMetaData(gomock.Any(), "fromdenom").Return(bank.Metadata{}, true).Times(1) 303 bankKeeper.EXPECT().GetDenomMetaData(gomock.Any(), "todenom").Return(bank.Metadata{}, false).Times(1) 304 bankKeeper.EXPECT().SetDenomMetaData(gomock.Any(), s.toDenomMetadata).Times(1) 305 }, 306 nil, 307 sdk.Events{ 308 sdk.Event{ 309 Type: "lbm.fswap.v1.EventSetSwap", 310 Attributes: []abci.EventAttribute{ 311 { 312 Key: []byte("swap"), 313 Value: []uint8{0x7b, 0x22, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x64, 0x65, 0x6e, 0x6f, 0x6d, 0x22, 0x3a, 0x22, 0x66, 0x72, 0x6f, 0x6d, 0x64, 0x65, 0x6e, 0x6f, 0x6d, 0x22, 0x2c, 0x22, 0x74, 0x6f, 0x5f, 0x64, 0x65, 0x6e, 0x6f, 0x6d, 0x22, 0x3a, 0x22, 0x74, 0x6f, 0x64, 0x65, 0x6e, 0x6f, 0x6d, 0x22, 0x2c, 0x22, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x63, 0x61, 0x70, 0x5f, 0x66, 0x6f, 0x72, 0x5f, 0x74, 0x6f, 0x5f, 0x64, 0x65, 0x6e, 0x6f, 0x6d, 0x22, 0x3a, 0x22, 0x31, 0x22, 0x2c, 0x22, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x22, 0x3a, 0x22, 0x31, 0x2e, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x22, 0x7d}, 314 Index: false, 315 }, 316 }, 317 }, 318 sdk.Event{ 319 Type: "lbm.fswap.v1.EventAddDenomMetadata", 320 Attributes: []abci.EventAttribute{ 321 { 322 Key: []byte("metadata"), 323 Value: []uint8{0x7b, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x22, 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x74, 0x6f, 0x2d, 0x63, 0x6f, 0x69, 0x6e, 0x22, 0x2c, 0x22, 0x64, 0x65, 0x6e, 0x6f, 0x6d, 0x5f, 0x75, 0x6e, 0x69, 0x74, 0x73, 0x22, 0x3a, 0x5b, 0x7b, 0x22, 0x64, 0x65, 0x6e, 0x6f, 0x6d, 0x22, 0x3a, 0x22, 0x74, 0x6f, 0x64, 0x65, 0x6e, 0x6f, 0x6d, 0x22, 0x2c, 0x22, 0x65, 0x78, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x22, 0x3a, 0x30, 0x2c, 0x22, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x65, 0x73, 0x22, 0x3a, 0x5b, 0x5d, 0x7d, 0x5d, 0x2c, 0x22, 0x62, 0x61, 0x73, 0x65, 0x22, 0x3a, 0x22, 0x74, 0x6f, 0x64, 0x65, 0x6e, 0x6f, 0x6d, 0x22, 0x2c, 0x22, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x22, 0x3a, 0x22, 0x74, 0x6f, 0x64, 0x65, 0x6e, 0x6f, 0x6d, 0x22, 0x2c, 0x22, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x22, 0x44, 0x55, 0x4d, 0x4d, 0x59, 0x22, 0x2c, 0x22, 0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x22, 0x3a, 0x22, 0x44, 0x55, 0x4d, 0x22, 0x7d}, 324 Index: false, 325 }, 326 }, 327 }, 328 }, 329 }, 330 "to-denom metadata has been stored": { 331 types.Swap{ 332 FromDenom: "fromdenom", 333 ToDenom: "todenom", 334 AmountCapForToDenom: sdk.OneInt(), 335 SwapRate: sdk.OneDec(), 336 }, 337 s.toDenomMetadata, 338 func() { 339 bankKeeper.EXPECT().GetDenomMetaData(gomock.Any(), "fromdenom").Return(bank.Metadata{}, true).Times(1) 340 bankKeeper.EXPECT().GetDenomMetaData(gomock.Any(), "todenom").Return(s.toDenomMetadata, true).Times(1) 341 }, 342 nil, 343 sdk.Events{ 344 sdk.Event{ 345 Type: "lbm.fswap.v1.EventSetSwap", 346 Attributes: []abci.EventAttribute{ 347 { 348 Key: []byte("swap"), 349 Value: []uint8{0x7b, 0x22, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x64, 0x65, 0x6e, 0x6f, 0x6d, 0x22, 0x3a, 0x22, 0x66, 0x72, 0x6f, 0x6d, 0x64, 0x65, 0x6e, 0x6f, 0x6d, 0x22, 0x2c, 0x22, 0x74, 0x6f, 0x5f, 0x64, 0x65, 0x6e, 0x6f, 0x6d, 0x22, 0x3a, 0x22, 0x74, 0x6f, 0x64, 0x65, 0x6e, 0x6f, 0x6d, 0x22, 0x2c, 0x22, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x63, 0x61, 0x70, 0x5f, 0x66, 0x6f, 0x72, 0x5f, 0x74, 0x6f, 0x5f, 0x64, 0x65, 0x6e, 0x6f, 0x6d, 0x22, 0x3a, 0x22, 0x31, 0x22, 0x2c, 0x22, 0x73, 0x77, 0x61, 0x70, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x22, 0x3a, 0x22, 0x31, 0x2e, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x22, 0x7d}, 350 Index: false, 351 }, 352 }, 353 }, 354 }, 355 }, 356 "from-denom does not exist": { 357 types.Swap{ 358 FromDenom: "fakedenom", 359 ToDenom: "todenom", 360 AmountCapForToDenom: sdk.OneInt(), 361 SwapRate: sdk.OneDec(), 362 }, 363 s.toDenomMetadata, 364 func() { 365 bankKeeper.EXPECT().GetDenomMetaData(gomock.Any(), "fakedenom").Return(bank.Metadata{}, false).Times(1) 366 }, 367 sdkerrors.ErrInvalidRequest, 368 sdk.Events{}, 369 }, 370 "to-denom metadata change not allowed": { 371 types.Swap{ 372 FromDenom: "fromdenom", 373 ToDenom: "change", 374 AmountCapForToDenom: sdk.OneInt(), 375 SwapRate: sdk.OneDec(), 376 }, 377 bank.Metadata{ 378 Description: s.toDenomMetadata.Description, 379 DenomUnits: s.toDenomMetadata.DenomUnits, 380 Base: "change", 381 Display: "change", 382 Name: s.toDenomMetadata.Name, 383 Symbol: s.toDenomMetadata.Symbol, 384 }, 385 func() { 386 bankKeeper.EXPECT().GetDenomMetaData(gomock.Any(), "fromdenom").Return(bank.Metadata{}, true).Times(1) 387 bankKeeper.EXPECT().GetDenomMetaData(gomock.Any(), "change").Return(s.toDenomMetadata, true).Times(1) 388 }, 389 sdkerrors.ErrInvalidRequest, 390 sdk.Events{}, 391 }, 392 } 393 for name, tc := range testCases { 394 s.Run(name, func() { 395 ctx, _ := s.ctx.CacheContext() 396 tc.malleate() 397 s.keeper.BankKeeper = bankKeeper 398 399 err := s.keeper.SetSwap(ctx, tc.swap, tc.toDenomMeta) 400 if tc.expectedError != nil { 401 s.Require().ErrorIs(err, tc.expectedError) 402 return 403 } 404 events := ctx.EventManager().Events() 405 s.Require().Equal(tc.expectedEvents, events) 406 }) 407 } 408 } 409 410 func (s *KeeperTestSuite) TestSwapValidateBasic() { 411 testCases := map[string]struct { 412 swap types.Swap 413 shouldThrowError bool 414 expectedError error 415 }{ 416 "valid": { 417 types.Swap{ 418 FromDenom: "fromD", 419 ToDenom: "toD", 420 AmountCapForToDenom: sdk.OneInt(), 421 SwapRate: sdk.OneDec(), 422 }, 423 false, 424 nil, 425 }, 426 "invalid empty from-denom": { 427 types.Swap{ 428 FromDenom: "", 429 ToDenom: "toD", 430 AmountCapForToDenom: sdk.OneInt(), 431 SwapRate: sdk.OneDec(), 432 }, 433 true, 434 sdkerrors.ErrInvalidRequest, 435 }, 436 "invalid empty to-denom": { 437 types.Swap{ 438 FromDenom: "fromD", 439 ToDenom: "", 440 AmountCapForToDenom: sdk.OneInt(), 441 SwapRate: sdk.OneDec(), 442 }, 443 true, 444 sdkerrors.ErrInvalidRequest, 445 }, 446 "invalid zero amount cap for to-denom": { 447 types.Swap{ 448 FromDenom: "fromD", 449 ToDenom: "toD", 450 AmountCapForToDenom: sdk.ZeroInt(), 451 SwapRate: sdk.OneDec(), 452 }, 453 true, 454 sdkerrors.ErrInvalidRequest, 455 }, 456 "invalid zero swap-rate": { 457 types.Swap{ 458 FromDenom: "fromD", 459 ToDenom: "toD", 460 AmountCapForToDenom: sdk.OneInt(), 461 SwapRate: sdk.ZeroDec(), 462 }, 463 true, 464 sdkerrors.ErrInvalidRequest, 465 }, 466 "invalid the same from-denom and to-denom": { 467 types.Swap{ 468 FromDenom: "same", 469 ToDenom: "same", 470 AmountCapForToDenom: sdk.OneInt(), 471 SwapRate: sdk.OneDec(), 472 }, 473 true, 474 sdkerrors.ErrInvalidRequest, 475 }, 476 } 477 for name, tc := range testCases { 478 s.Run(name, func() { 479 err := tc.swap.ValidateBasic() 480 if tc.shouldThrowError { 481 s.Require().ErrorIs(err, tc.expectedError) 482 return 483 } 484 s.Require().NoError(err) 485 }) 486 } 487 }