From fd56a1f922ae343871e627ee5e0efd8a44b80903 Mon Sep 17 00:00:00 2001 From: Phil <30321052+philipjames44@users.noreply.github.com> Date: Thu, 3 Nov 2022 15:09:09 -0400 Subject: [PATCH] Auction Module Unit Tests (#180) * Add in base test auction keeper with mocked expected keepers * Fix bug in keystore lookup & do happy path for BeginAuction * Minor bug fix and happy path for finishing auction * lint * test * Msg server happy path unit test * Partial implementation for abci test * Abci unit test happy path * Partially fulfilled bid test * Partial unhappy path for msg server * Finish unhappy path tests for msg server * Make linter happy * Finish unhappy paths for keeper, msg server, & abci * Add genesis unit testing and fix up genesis import + add validate basic ccheck to token price setting * Proposal handler unit testing * Query server tests * Add auction types validation testing and remove some unnecessary errors * Genesis validate test * Add in msg validation test * Add params validate test * Proposal validation test * Cli query tests * Tx unit tests * Add auction module account keeper * Added mock account keeper * Partial unit test update to account for new expected keeper * Update unit tests to account for latest updates in integration test sister PR * lint * Tidy * Partial address of comments * More feedback being addressed * Unit test workflow * Address review comments --- .github/workflows/unit-test.yml | 23 + app/app.go | 6 +- proto/auction/v1/tx.proto | 3 +- testutil/codec.go | 45 ++ x/auction/client/cli/query_test.go | 337 ++++++++++ x/auction/client/cli/tx_test.go | 126 ++++ x/auction/keeper/abci_test.go | 117 ++++ x/auction/keeper/genesis.go | 7 + x/auction/keeper/genesis_test.go | 291 +++++++++ x/auction/keeper/keeper.go | 14 +- x/auction/keeper/keeper_test.go | 445 +++++++++++++ x/auction/keeper/msg_server.go | 20 +- x/auction/keeper/msg_server_test.go | 306 +++++++++ x/auction/keeper/proposal_handler.go | 1 + x/auction/keeper/proposal_handler_test.go | 116 ++++ x/auction/keeper/query_server_test.go | 205 ++++++ x/auction/keeper/sdk_module_mocks_test.go | 28 + x/auction/module.go | 10 +- x/auction/testutil/expected_keepers_mocks.go | 173 +++++ x/auction/types/auction.go | 22 +- x/auction/types/auction_test.go | 635 +++++++++++++++++++ x/auction/types/errors.go | 72 ++- x/auction/types/events.go | 1 - x/auction/types/expected_keepers.go | 7 + x/auction/types/genesis_test.go | 257 ++++++++ x/auction/types/msgs.go | 22 +- x/auction/types/msgs_test.go | 120 ++++ x/auction/types/params.go | 9 +- x/auction/types/params_test.go | 51 ++ x/auction/types/proposal.go | 16 +- x/auction/types/proposal_test.go | 157 +++++ x/auction/types/tx.pb.go | 124 ++-- x/cellarfees/keeper/genesis.go | 2 - x/cellarfees/types/expected_keepers.go | 2 - 34 files changed, 3585 insertions(+), 185 deletions(-) create mode 100644 .github/workflows/unit-test.yml create mode 100644 testutil/codec.go create mode 100644 x/auction/client/cli/query_test.go create mode 100644 x/auction/client/cli/tx_test.go create mode 100644 x/auction/keeper/abci_test.go create mode 100644 x/auction/keeper/genesis_test.go create mode 100644 x/auction/keeper/keeper_test.go create mode 100644 x/auction/keeper/msg_server_test.go create mode 100644 x/auction/keeper/proposal_handler_test.go create mode 100644 x/auction/keeper/query_server_test.go create mode 100644 x/auction/keeper/sdk_module_mocks_test.go create mode 100755 x/auction/testutil/expected_keepers_mocks.go create mode 100644 x/auction/types/auction_test.go create mode 100644 x/auction/types/genesis_test.go create mode 100644 x/auction/types/msgs_test.go create mode 100644 x/auction/types/params_test.go create mode 100644 x/auction/types/proposal_test.go diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml new file mode 100644 index 00000000..26124c8f --- /dev/null +++ b/.github/workflows/unit-test.yml @@ -0,0 +1,23 @@ +name: Unit tests + +on: + push: + branches: + - main + tags: + - "v*.*.*" + pull_request: + +jobs: + test: + strategy: + matrix: + go-version: [1.16] + os: [ubuntu-latest] + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.go-version }} + - uses: actions/checkout@v3 + - run: go test github.com/peggyjv/sommelier/.../x/... diff --git a/app/app.go b/app/app.go index ec18e11a..df8a3192 100644 --- a/app/app.go +++ b/app/app.go @@ -364,7 +364,7 @@ func NewSommelierApp( app.AuctionKeeper = auctionkeeper.NewKeeper( appCodec, keys[auctiontypes.StoreKey], app.GetSubspace(auctiontypes.ModuleName), - app.BankKeeper, map[string]bool{cellarfeestypes.ModuleName: true}, + app.BankKeeper, app.AccountKeeper, map[string]bool{cellarfeestypes.ModuleName: true}, map[string]bool{cellarfeestypes.ModuleName: true}, ) @@ -445,7 +445,7 @@ func NewSommelierApp( authzmodule.NewAppModule(appCodec, app.AuthzKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry), cork.NewAppModule(app.CorkKeeper, appCodec), cellarfees.NewAppModule(app.CellarFeesKeeper, appCodec, app.AccountKeeper, app.BankKeeper), - auction.NewAppModule(app.AuctionKeeper, app.BankKeeper, appCodec), + auction.NewAppModule(app.AuctionKeeper, app.BankKeeper, app.AccountKeeper, appCodec), ) // During begin block slashing happens after distr.BeginBlocker so that @@ -506,7 +506,7 @@ func NewSommelierApp( authzmodule.NewAppModule(appCodec, app.AuthzKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry), cork.NewAppModule(app.CorkKeeper, appCodec), cellarfees.NewAppModule(app.CellarFeesKeeper, appCodec, app.AccountKeeper, app.BankKeeper), - auction.NewAppModule(app.AuctionKeeper, app.BankKeeper, appCodec), + auction.NewAppModule(app.AuctionKeeper, app.BankKeeper, app.AccountKeeper, appCodec), ) app.sm.RegisterStoreDecoders() diff --git a/proto/auction/v1/tx.proto b/proto/auction/v1/tx.proto index 9913d9c1..c0dbf61b 100644 --- a/proto/auction/v1/tx.proto +++ b/proto/auction/v1/tx.proto @@ -14,10 +14,9 @@ service Msg { message MsgSubmitBidRequest { uint32 auction_id = 1; - string bidder = 2; + string signer = 2; cosmos.base.v1beta1.Coin max_bid_in_usomm = 3 [ (gogoproto.nullable) = false ]; cosmos.base.v1beta1.Coin sale_token_minimum_amount = 4 [ (gogoproto.nullable) = false ]; - string signer = 5; } message MsgSubmitBidResponse { diff --git a/testutil/codec.go b/testutil/codec.go new file mode 100644 index 00000000..2f8b816f --- /dev/null +++ b/testutil/codec.go @@ -0,0 +1,45 @@ +package testutil + +import ( + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/std" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/x/auth/tx" +) + +// TODO(pbal): This is copied from an unreleased version of the cosmos sdk. Upon upgrading our sdk version to 0.46.1 we should import this file + +// TestEncodingConfig defines an encoding configuration that is used for testing +// purposes. Note, MakeTestEncodingConfig takes a series of AppModuleBasic types +// which should only contain the relevant module being tested and any potential +// dependencies. +type TestEncodingConfig struct { + InterfaceRegistry types.InterfaceRegistry + Codec codec.Codec + TxConfig client.TxConfig + Amino *codec.LegacyAmino +} + +func MakeTestEncodingConfig(modules ...module.AppModuleBasic) TestEncodingConfig { + cdc := codec.NewLegacyAmino() + interfaceRegistry := types.NewInterfaceRegistry() + codec := codec.NewProtoCodec(interfaceRegistry) + + encCfg := TestEncodingConfig{ + InterfaceRegistry: interfaceRegistry, + Codec: codec, + TxConfig: tx.NewTxConfig(codec, tx.DefaultSignModes), + Amino: cdc, + } + + mb := module.NewBasicManager(modules...) + + std.RegisterLegacyAminoCodec(encCfg.Amino) + std.RegisterInterfaces(encCfg.InterfaceRegistry) + mb.RegisterLegacyAminoCodec(encCfg.Amino) + mb.RegisterInterfaces(encCfg.InterfaceRegistry) + + return encCfg +} diff --git a/x/auction/client/cli/query_test.go b/x/auction/client/cli/query_test.go new file mode 100644 index 00000000..59340646 --- /dev/null +++ b/x/auction/client/cli/query_test.go @@ -0,0 +1,337 @@ +package cli + +import ( + "testing" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/stretchr/testify/require" +) + +func TestQueryParamsCmd(t *testing.T) { + testCases := []struct { + name string + args []string + err error + }{ + { + name: "Does not accept args", + args: []string{ + "1", + }, + err: sdkerrors.New("", uint32(1), "unknown command \"1\" for \"parameters\""), + }, + } + + for _, tc := range testCases { + cmd := *queryParams() + cmd.SetArgs(tc.args) + err := cmd.Execute() + + require.Equal(t, tc.err.Error(), err.Error()) + } +} + +func TestQueryActiveAuction(t *testing.T) { + testCases := []struct { + name string + args []string + err error + }{ + { + name: "Insufficient args", + args: []string{}, + err: sdkerrors.New("", uint32(1), "accepts 1 arg(s), received 0"), + }, + { + name: "Too many args", + args: []string{ + "1", + "2", + }, + err: sdkerrors.New("", uint32(1), "accepts 1 arg(s), received 2"), + }, + { + name: "Auction ID overflow", + args: []string{ + "4294967296", + }, + err: sdkerrors.New("", uint32(1), "strconv.ParseUint: parsing \"4294967296\": value out of range"), + }, + { + name: "Auction ID invalid type", + args: []string{ + "one hundred and twenty", + }, + err: sdkerrors.New("", uint32(1), "strconv.ParseUint: parsing \"one hundred and twenty\": invalid syntax"), + }, + } + + for _, tc := range testCases { + cmd := *queryActiveAuction() + cmd.SetArgs(tc.args) + err := cmd.Execute() + + require.Equal(t, tc.err.Error(), err.Error()) + } +} + +func TestQueryEndedAuction(t *testing.T) { + testCases := []struct { + name string + args []string + err error + }{ + { + name: "Insufficient args", + args: []string{}, + err: sdkerrors.New("", uint32(1), "accepts 1 arg(s), received 0"), + }, + { + name: "Too many args", + args: []string{ + "1", + "2", + }, + err: sdkerrors.New("", uint32(1), "accepts 1 arg(s), received 2"), + }, + { + name: "Auction ID invalid type", + args: []string{ + "one hundred and twenty", + }, + err: sdkerrors.New("", uint32(1), "strconv.ParseUint: parsing \"one hundred and twenty\": invalid syntax"), + }, + { + name: "Auction ID overflow", + args: []string{ + "4294967296", + }, + err: sdkerrors.New("", uint32(1), "strconv.ParseUint: parsing \"4294967296\": value out of range"), + }, + } + + for _, tc := range testCases { + cmd := *queryEndedAuction() + cmd.SetArgs(tc.args) + err := cmd.Execute() + + require.Equal(t, tc.err.Error(), err.Error()) + } +} + +func TestActiveAuctionsCmd(t *testing.T) { + testCases := []struct { + name string + args []string + err error + }{ + { + name: "Does not accept args", + args: []string{ + "1", + }, + err: sdkerrors.New("", uint32(1), "unknown command \"1\" for \"active-auctions\""), + }, + } + + for _, tc := range testCases { + cmd := *queryActiveAuctions() + cmd.SetArgs(tc.args) + err := cmd.Execute() + + require.Equal(t, tc.err.Error(), err.Error()) + } +} + +func TestEndedAuctionsCmd(t *testing.T) { + testCases := []struct { + name string + args []string + err error + }{ + { + name: "Does not accept args", + args: []string{ + "1", + }, + err: sdkerrors.New("", uint32(1), "unknown command \"1\" for \"ended-auctions\""), + }, + } + + for _, tc := range testCases { + cmd := *queryEndedAuctions() + cmd.SetArgs(tc.args) + err := cmd.Execute() + + require.Equal(t, tc.err.Error(), err.Error()) + } +} + +func TestQueryActiveAuctionsByDenom(t *testing.T) { + testCases := []struct { + name string + args []string + err error + }{ + { + name: "Insufficient args", + args: []string{}, + err: sdkerrors.New("", uint32(1), "accepts 1 arg(s), received 0"), + }, + { + name: "Too many args", + args: []string{ + "1", + "2", + }, + err: sdkerrors.New("", uint32(1), "accepts 1 arg(s), received 2"), + }, + } + + for _, tc := range testCases { + cmd := *queryActiveAuctionsByDenom() + cmd.SetArgs(tc.args) + err := cmd.Execute() + + require.Equal(t, tc.err.Error(), err.Error()) + } +} + +func TestQueryEndedAuctionsByDenom(t *testing.T) { + testCases := []struct { + name string + args []string + err error + }{ + { + name: "Insufficient args", + args: []string{}, + err: sdkerrors.New("", uint32(1), "accepts 1 arg(s), received 0"), + }, + { + name: "Too many args", + args: []string{ + "1", + "2", + }, + err: sdkerrors.New("", uint32(1), "accepts 1 arg(s), received 2"), + }, + } + + for _, tc := range testCases { + cmd := *queryEndedAuctionsByDenom() + cmd.SetArgs(tc.args) + err := cmd.Execute() + + require.Equal(t, tc.err.Error(), err.Error()) + } +} + +func TestQueryBids(t *testing.T) { + testCases := []struct { + name string + args []string + err error + }{ + { + name: "Insufficient args", + args: []string{ + "1", + }, + err: sdkerrors.New("", uint32(1), "accepts 2 arg(s), received 1"), + }, + { + name: "Too many args", + args: []string{ + "1", + "2", + "3", + }, + err: sdkerrors.New("", uint32(1), "accepts 2 arg(s), received 3"), + }, + { + name: "Auction ID overflow", + args: []string{ + "4294967296", + "2", + }, + err: sdkerrors.New("", uint32(1), "strconv.ParseUint: parsing \"4294967296\": value out of range"), + }, + { + name: "Bid ID overflow", + args: []string{ + "1", + "18446744073709551616", + }, + err: sdkerrors.New("", uint32(1), "strconv.ParseUint: parsing \"18446744073709551616\": value out of range"), + }, + { + name: "Auction ID invalid type", + args: []string{ + "one hundred and twenty", + "2", + }, + err: sdkerrors.New("", uint32(1), "strconv.ParseUint: parsing \"one hundred and twenty\": invalid syntax"), + }, + { + name: "Bid ID invalid type", + args: []string{ + "1", + "four", + }, + err: sdkerrors.New("", uint32(1), "strconv.ParseUint: parsing \"four\": invalid syntax"), + }, + } + + for _, tc := range testCases { + cmd := *queryBid() + cmd.SetArgs(tc.args) + err := cmd.Execute() + + require.Equal(t, tc.err.Error(), err.Error()) + } +} + +func TestQueryBidByAuction(t *testing.T) { + testCases := []struct { + name string + args []string + err error + }{ + { + name: "Insufficient args", + args: []string{}, + err: sdkerrors.New("", uint32(1), "accepts 1 arg(s), received 0"), + }, + { + name: "Too many args", + args: []string{ + "1", + "2", + }, + err: sdkerrors.New("", uint32(1), "accepts 1 arg(s), received 2"), + }, + { + name: "Auction ID overflow", + args: []string{ + "4294967296", + }, + err: sdkerrors.New("", uint32(1), "strconv.ParseUint: parsing \"4294967296\": value out of range"), + }, + { + name: "Auction ID invalid type", + args: []string{ + "one hundred and twenty", + }, + err: sdkerrors.New("", uint32(1), "strconv.ParseUint: parsing \"one hundred and twenty\": invalid syntax"), + }, + } + + for _, tc := range testCases { + cmd := *queryBidsByAuction() + cmd.SetArgs(tc.args) + err := cmd.Execute() + + require.Equal(t, tc.err.Error(), err.Error()) + } +} diff --git a/x/auction/client/cli/tx_test.go b/x/auction/client/cli/tx_test.go new file mode 100644 index 00000000..592b0265 --- /dev/null +++ b/x/auction/client/cli/tx_test.go @@ -0,0 +1,126 @@ +package cli + +import ( + "fmt" + "io/ioutil" + "testing" + + "github.com/cosmos/cosmos-sdk/simapp/params" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/peggyjv/sommelier/v4/x/auction/types" + + "github.com/cosmos/cosmos-sdk/testutil" + "github.com/stretchr/testify/require" +) + +func TestParseSetTokenPricesProposal(t *testing.T) { + encodingConfig := params.MakeTestEncodingConfig() + + okJSON := testutil.WriteToNewTempFile(t, ` +{ + "title": "My token proposal", + "description": "Contains a usomm price update", + "token_prices": [ { "denom" : "usomm", "usd_price" : "4.200000000000000000"} ], + "deposit": "10000usomm" +} +`) + + proposal := types.SetTokenPricesProposalWithDeposit{} + contents, err := ioutil.ReadFile(okJSON.Name()) + require.NoError(t, err) + + err = encodingConfig.Marshaler.UnmarshalJSON(contents, &proposal) + require.NoError(t, err) + + require.Equal(t, "My token proposal", proposal.Title) + require.Equal(t, "Contains a usomm price update", proposal.Description) + require.Equal(t, "denom:\"usomm\" usd_price:\"4200000000000000000\" ", proposal.TokenPrices[0].String()) + require.Equal(t, "10000usomm", proposal.Deposit) +} + +func TestSubmitBid(t *testing.T) { + testCases := []struct { + name string + args []string + err error + }{ + { + name: "Valid cmd", + args: []string{ + "1", + "10000usomm", + "50000gravity0xdac17f958d2ee523a2206206994597c13d831ec7", + fmt.Sprintf("--%s=%s", "from", "cosmos16zrkzad482haunrn25ywvwy6fclh3vh7r0hcny"), + }, + err: sdkerrors.New("", uint32(1), "key with addressD0876175B53AAFDE4C735508E6389A4E3F78B2FEnot found: key not found"), // Expect key not found error since this is just a mock request + }, + { + name: "Insufficient args", + args: []string{}, + err: sdkerrors.New("", uint32(1), "accepts 3 arg(s), received 0"), + }, + { + name: "Too many args", + args: []string{ + "1", + "2", + "3", + "4", + }, + err: sdkerrors.New("", uint32(1), "accepts 3 arg(s), received 4"), + }, + { + name: "Missing 'from' field", + args: []string{ + "1", + "10000usomm", + "50000gravity0xdac17f958d2ee523a2206206994597c13d831ec7", + }, + err: sdkerrors.New("", uint32(1), "must include `--from` flag"), + }, + { + name: "Auction ID overflow", + args: []string{ + "4294967296", + "10000usomm", + "50000gravity0xdac17f958d2ee523a2206206994597c13d831ec7", + }, + err: sdkerrors.New("", uint32(1), "strconv.ParseUint: parsing \"4294967296\": value out of range"), + }, + { + name: "Auction ID invalid type", + args: []string{ + "one hundred and twenty", + "10000usomm", + "50000gravity0xdac17f958d2ee523a2206206994597c13d831ec7", + }, + err: sdkerrors.New("", uint32(1), "strconv.ParseUint: parsing \"one hundred and twenty\": invalid syntax"), + }, + { + name: "Invalid bid", + args: []string{ + "1", + "10000", + "50000gravity0xdac17f958d2ee523a2206206994597c13d831ec7", + }, + err: sdkerrors.New("", uint32(1), "invalid decimal coin expression: 10000"), + }, + { + name: "Invalid minimum amount", + args: []string{ + "1", + "10000usomm", + "50000", + }, + err: sdkerrors.New("", uint32(1), "invalid decimal coin expression: 50000"), + }, + } + + for _, tc := range testCases { + cmd := GetCmdSubmitBid() + cmd.SetArgs(tc.args) + err := cmd.Execute() + + require.Equal(t, tc.err.Error(), err.Error()) + } +} diff --git a/x/auction/keeper/abci_test.go b/x/auction/keeper/abci_test.go new file mode 100644 index 00000000..5ba685c5 --- /dev/null +++ b/x/auction/keeper/abci_test.go @@ -0,0 +1,117 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + auctionTypes "github.com/peggyjv/sommelier/v4/x/auction/types" +) + +// Tests abci +func (suite *KeeperTestSuite) TestAbci() { + ctx, auctionKeeper := suite.ctx, suite.auctionKeeper + require := suite.Require() + + // Test base case of no auctions + // Note BeginBlocker is only run once for completeness, since it has no code in it + require.NotPanics(func() { auctionKeeper.BeginBlocker(ctx) }) + require.NotPanics(func() { auctionKeeper.EndBlocker(ctx) }) + + // Create an auction + params := auctionTypes.Params{PriceMaxBlockAge: 10} + auctionKeeper.setParams(ctx, params) + + sommPrice := auctionTypes.TokenPrice{Denom: auctionTypes.UsommDenom, UsdPrice: sdk.MustNewDecFromStr("0.01"), LastUpdatedBlock: 5} + + /* #nosec */ + saleToken := "gravity0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE" + saleTokenPrice := auctionTypes.TokenPrice{Denom: saleToken, UsdPrice: sdk.MustNewDecFromStr("0.01"), LastUpdatedBlock: 5} + auctionedSaleTokens := sdk.NewCoin(saleToken, sdk.NewInt(10000)) + + auctionKeeper.setTokenPrice(ctx, sommPrice) + auctionKeeper.setTokenPrice(ctx, saleTokenPrice) + + // Mock bank keeper fund transfer + suite.mockSendCoinsFromModuleToModule(ctx, permissionedFunder.GetName(), auctionTypes.ModuleName, sdk.NewCoins(auctionedSaleTokens)) + + // Start auction + decreaseRate := sdk.MustNewDecFromStr("0.4") + blockDecreaseInterval := uint64(5) + err := auctionKeeper.BeginAuction(ctx, auctionedSaleTokens, decreaseRate, blockDecreaseInterval, permissionedFunder.GetName(), permissionedReciever.GetName()) + require.Nil(err) + + auctionID := uint32(1) + + expectedAuction := auctionTypes.Auction{ + Id: auctionID, + StartingTokensForSale: auctionedSaleTokens, + StartBlock: uint64(ctx.BlockHeight()), + EndBlock: 0, + InitialPriceDecreaseRate: decreaseRate, + CurrentPriceDecreaseRate: decreaseRate, + PriceDecreaseBlockInterval: blockDecreaseInterval, + InitialUnitPriceInUsomm: sdk.NewDec(1), + CurrentUnitPriceInUsomm: sdk.NewDec(1), + RemainingTokensForSale: auctionedSaleTokens, + FundingModuleAccount: permissionedFunder.GetName(), + ProceedsModuleAccount: permissionedReciever.GetName(), + } + + // Run EndBlocker as base case with NO block change + // Assure expected auction prices have not changed + require.NotPanics(func() { auctionKeeper.EndBlocker(ctx) }) + + foundAuction, found := auctionKeeper.GetActiveAuctionByID(ctx, auctionID) + require.True(found) + require.Equal(expectedAuction, foundAuction) + + // Run EndBlocker again with ~insufficient~ block change to induce price decrease + require.NotPanics(func() { + auctionKeeper.EndBlocker(ctx.WithBlockHeight(ctx.BlockHeight() + int64(blockDecreaseInterval) - 1)) + }) + + foundAuction, found = auctionKeeper.GetActiveAuctionByID(ctx, auctionID) + require.True(found) + require.Equal(expectedAuction, foundAuction) + + // Run EndBlocker with block interval ~sufficient~ to induce decrease + require.NotPanics(func() { + auctionKeeper.EndBlocker(ctx.WithBlockHeight(ctx.BlockHeight() + int64(blockDecreaseInterval))) + }) + + foundAuction, found = auctionKeeper.GetActiveAuctionByID(ctx, auctionID) + require.True(found) + expectedAuction.CurrentUnitPriceInUsomm = sdk.MustNewDecFromStr("0.6") + require.Equal(expectedAuction, foundAuction) + + // Run EndBlocker with block interval ~sufficient~ to induce decrease again + require.NotPanics(func() { + auctionKeeper.EndBlocker(ctx.WithBlockHeight(ctx.BlockHeight() + int64(blockDecreaseInterval))) + }) + + foundAuction, found = auctionKeeper.GetActiveAuctionByID(ctx, auctionID) + require.True(found) + expectedAuction.CurrentUnitPriceInUsomm = sdk.MustNewDecFromStr("0.2") + require.Equal(expectedAuction, foundAuction) + + // Run EndBlocker with block interval ~sufficient~ to induce decrease to a ~negative~ price, and thus the auction must finish + // Since we expect auction to end here without ever transfering any funds to bidders, we need to mock bank balance checks & transfers + // back to funding module + finalCtx := ctx.WithBlockHeight(ctx.BlockHeight() + int64(blockDecreaseInterval)) + suite.mockGetBalance(finalCtx, authtypes.NewModuleAddress(auctionTypes.ModuleName), saleToken, auctionedSaleTokens) + suite.mockSendCoinsFromModuleToModule(finalCtx, auctionTypes.ModuleName, permissionedFunder.GetName(), sdk.NewCoins(auctionedSaleTokens)) + + require.NotPanics(func() { + auctionKeeper.EndBlocker(finalCtx) + }) + + _, found = auctionKeeper.GetActiveAuctionByID(finalCtx, auctionID) + require.False(found) + + expectedAuction.EndBlock = uint64(finalCtx.BlockHeight()) + + // Check expected auction wound up in ended auctions + foundAuction, found = auctionKeeper.GetEndedAuctionByID(finalCtx, auctionID) + require.True(found) + + require.Equal(expectedAuction, foundAuction) +} \ No newline at end of file diff --git a/x/auction/keeper/genesis.go b/x/auction/keeper/genesis.go index 95554809..927c49fb 100644 --- a/x/auction/keeper/genesis.go +++ b/x/auction/keeper/genesis.go @@ -1,6 +1,8 @@ package keeper import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/peggyjv/sommelier/v4/x/auction/types" ) @@ -10,6 +12,11 @@ import ( func InitGenesis(ctx sdk.Context, k Keeper, gs types.GenesisState) { k.setParams(ctx, gs.Params) + auctionAccount := k.GetAuctionAccount(ctx) + if auctionAccount == nil { + panic(fmt.Sprintf("%s module account has not been set", types.ModuleName)) + } + for _, auction := range gs.Auctions { if auction.EndBlock > 0 { k.setEndedAuction(ctx, *auction) diff --git a/x/auction/keeper/genesis_test.go b/x/auction/keeper/genesis_test.go new file mode 100644 index 00000000..0360ef43 --- /dev/null +++ b/x/auction/keeper/genesis_test.go @@ -0,0 +1,291 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + auctionTypes "github.com/peggyjv/sommelier/v4/x/auction/types" +) + +// Tests Importing of as empty a genesis as possible +func (suite *KeeperTestSuite) TestImportingEmptyGenesis() { + ctx, auctionKeeper := suite.ctx, suite.auctionKeeper + require := suite.Require() + + testGenesis := auctionTypes.GenesisState{} + + // Canary to make sure validate basic is being run + require.Panics(func() { InitGenesis(ctx, auctionKeeper, testGenesis) }) + + testGenesis.Params.PriceMaxBlockAge = uint64(10) + require.NotPanics(func() { + suite.mockGetModuleAccount(ctx) + InitGenesis(ctx, auctionKeeper, testGenesis) + }) + + activeAuctions := auctionKeeper.GetActiveAuctions(ctx) + endedAuctions := auctionKeeper.GetEndedAuctions(ctx) + bids := auctionKeeper.GetBids(ctx) + tokenPrices := auctionKeeper.GetTokenPrices(ctx) + lastAuctionID := auctionKeeper.GetLastAuctionID(ctx) + lastBidID := auctionKeeper.GetLastBidID(ctx) + + require.Len(activeAuctions, 0) + require.Len(endedAuctions, 0) + require.Len(bids, 0) + require.Len(tokenPrices, 0) + require.Zero(lastAuctionID) + require.Zero(lastBidID) +} + +// Test Importing a populated Genesis +func (suite *KeeperTestSuite) TestImportingPopulatedGenesis() { + ctx, auctionKeeper := suite.ctx, suite.auctionKeeper + require := suite.Require() + + expectedEndedAuctions := make([]*auctionTypes.Auction, 1) + auction0 := &auctionTypes.Auction{ + Id: uint32(1), + StartingTokensForSale: sdk.NewCoin("weth", sdk.NewInt(17000)), + StartBlock: uint64(1), + EndBlock: uint64(5), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + PriceDecreaseBlockInterval: uint64(20), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("20"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("10"), + RemainingTokensForSale: sdk.NewCoin("weth", sdk.NewInt(10000)), + FundingModuleAccount: "madeUpModule1", + ProceedsModuleAccount: "madeUpModule1", + } + + expectedEndedAuctions[0] = auction0 + + expectedActiveAuctions := make([]*auctionTypes.Auction, 2) + auction1 := &auctionTypes.Auction{ + Id: uint32(2), + StartingTokensForSale: sdk.NewCoin("weth", sdk.NewInt(17000)), + StartBlock: uint64(6), + EndBlock: uint64(0), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + PriceDecreaseBlockInterval: uint64(20), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("20"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("10"), + RemainingTokensForSale: sdk.NewCoin("weth", sdk.NewInt(10000)), + FundingModuleAccount: "madeUpModule1", + ProceedsModuleAccount: "madeUpModule1", + } + auction2 := &auctionTypes.Auction{ + Id: uint32(3), + StartingTokensForSale: sdk.NewCoin("usdc", sdk.NewInt(5000)), + StartBlock: uint64(5), + EndBlock: uint64(0), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.1"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.1"), + PriceDecreaseBlockInterval: uint64(10), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("1"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("0.9"), + RemainingTokensForSale: sdk.NewCoin("usdc", sdk.NewInt(2000)), + FundingModuleAccount: "madeUpModule2", + ProceedsModuleAccount: "madeUpModule2", + } + expectedActiveAuctions[0] = auction1 + expectedActiveAuctions[1] = auction2 + + allAuctions := make([]*auctionTypes.Auction, 3) + allAuctions[0] = expectedEndedAuctions[0] + allAuctions[1] = expectedActiveAuctions[0] + allAuctions[2] = expectedActiveAuctions[1] + + expectedBids := make([]*auctionTypes.Bid, 2) + bid1 := &auctionTypes.Bid{ + Id: uint64(1), + AuctionId: uint32(1), + Bidder: cosmosAddress1, + MaxBidInUsomm: sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(2000)), + SaleTokenMinimumAmount: sdk.NewCoin("weth", sdk.NewInt(20)), + TotalFulfilledSaleTokens: sdk.NewCoin("weth", sdk.NewInt(100)), + SaleTokenUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + TotalUsommPaid: sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(2000)), + } + bid2 := &auctionTypes.Bid{ + Id: uint64(2), + AuctionId: uint32(2), + Bidder: cosmosAddress2, + MaxBidInUsomm: sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(1000)), + SaleTokenMinimumAmount: sdk.NewCoin("usdc", sdk.NewInt(100)), + TotalFulfilledSaleTokens: sdk.NewCoin("usdc", sdk.NewInt(1000)), + SaleTokenUnitPriceInUsomm: sdk.MustNewDecFromStr("1.0"), + TotalUsommPaid: sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(2000)), + } + expectedBids[0] = bid1 + expectedBids[1] = bid2 + + expectedTokenPrices := make([]*auctionTypes.TokenPrice, 3) + tokenPrice1 := &auctionTypes.TokenPrice{ + Denom: "usdc", + UsdPrice: sdk.MustNewDecFromStr("1.0"), + LastUpdatedBlock: uint64(4), + } + tokenPrice2 := &auctionTypes.TokenPrice{ + Denom: "usomm", + UsdPrice: sdk.MustNewDecFromStr("0.2"), + LastUpdatedBlock: uint64(2), + } + tokenPrice3 := &auctionTypes.TokenPrice{ + Denom: "weth", + UsdPrice: sdk.MustNewDecFromStr("17.45"), + LastUpdatedBlock: uint64(3), + } + expectedTokenPrices[0] = tokenPrice1 + expectedTokenPrices[1] = tokenPrice2 + expectedTokenPrices[2] = tokenPrice3 + + testGenesis := auctionTypes.GenesisState{ + Params: auctionTypes.DefaultGenesisState().Params, + Auctions: allAuctions, + Bids: expectedBids, + TokenPrices: expectedTokenPrices, + LastAuctionId: uint32(2), + LastBidId: uint64(2), + } + + require.NotPanics(func() { + suite.mockGetModuleAccount(ctx) + InitGenesis(ctx, auctionKeeper, testGenesis) + }) + + // Verify value sets + foundActiveAuctions := auctionKeeper.GetActiveAuctions(ctx) + foundEndedAuctions := auctionKeeper.GetEndedAuctions(ctx) + foundBids := auctionKeeper.GetBids(ctx) + foundTokenPrices := auctionKeeper.GetTokenPrices(ctx) + foundLastAuctionID := auctionKeeper.GetLastAuctionID(ctx) + foundLastBidID := auctionKeeper.GetLastBidID(ctx) + + require.Equal(expectedActiveAuctions, foundActiveAuctions) + require.Equal(expectedEndedAuctions, foundEndedAuctions) + require.Equal(expectedBids, foundBids) + require.Equal(expectedTokenPrices, foundTokenPrices) + require.Equal(uint32(2), foundLastAuctionID) + require.Equal(uint64(2), foundLastBidID) +} + +// Tests Exportng of an empty/default genesis +func (suite *KeeperTestSuite) TestExportingEmptyGenesis() { + ctx, auctionKeeper := suite.ctx, suite.auctionKeeper + require := suite.Require() + + auctionKeeper.setParams(ctx, auctionTypes.DefaultParams()) + exportedGenesis := ExportGenesis(ctx, auctionKeeper) + + require.Equal(auctionTypes.DefaultGenesisState(), exportedGenesis) + + require.Equal(auctionTypes.DefaultGenesisState().Params, exportedGenesis.Params) + require.Len(exportedGenesis.Auctions, 0) + require.Len(exportedGenesis.Bids, 0) + require.Len(exportedGenesis.TokenPrices, 0) + require.Zero(exportedGenesis.GetAuctions()) + require.Zero(exportedGenesis.LastBidId) +} + +// Test Exporting a populated Genesis +func (suite *KeeperTestSuite) TestExportingPopulatedGenesis() { + ctx, auctionKeeper := suite.ctx, suite.auctionKeeper + require := suite.Require() + + auctionKeeper.setParams(ctx, auctionTypes.DefaultParams()) + + expectedEndedAuctions := make([]*auctionTypes.Auction, 1) + auction0 := &auctionTypes.Auction{ + Id: uint32(1), + StartingTokensForSale: sdk.NewCoin("weth", sdk.NewInt(17000)), + StartBlock: uint64(1), + EndBlock: uint64(5), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + PriceDecreaseBlockInterval: uint64(20), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("20"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("10"), + RemainingTokensForSale: sdk.NewCoin("weth", sdk.NewInt(10000)), + FundingModuleAccount: "madeUpModule1", + ProceedsModuleAccount: "madeUpModule1", + } + expectedEndedAuctions[0] = auction0 + auctionKeeper.setEndedAuction(ctx, *auction0) + + expectedActiveAuctions := make([]*auctionTypes.Auction, 2) + auction1 := &auctionTypes.Auction{ + Id: uint32(1), + StartingTokensForSale: sdk.NewCoin("weth", sdk.NewInt(17000)), + StartBlock: uint64(6), + EndBlock: uint64(0), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + PriceDecreaseBlockInterval: uint64(20), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("20"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("10"), + RemainingTokensForSale: sdk.NewCoin("weth", sdk.NewInt(10000)), + FundingModuleAccount: "madeUpModule1", + ProceedsModuleAccount: "madeUpModule1", + } + expectedActiveAuctions[0] = auction1 + auctionKeeper.setActiveAuction(ctx, *auction1) + + allAuctions := make([]*auctionTypes.Auction, 2) + allAuctions[0] = expectedActiveAuctions[0] + allAuctions[1] = expectedEndedAuctions[0] + auctionKeeper.setLastAuctionID(ctx, uint32(1)) + + expectedBids := make([]*auctionTypes.Bid, 1) + bid1 := &auctionTypes.Bid{ + Id: uint64(1), + AuctionId: uint32(1), + Bidder: cosmosAddress1, + MaxBidInUsomm: sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(2000)), + SaleTokenMinimumAmount: sdk.NewCoin("weth", sdk.NewInt(20)), + TotalFulfilledSaleTokens: sdk.NewCoin("weth", sdk.NewInt(100)), + SaleTokenUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + TotalUsommPaid: sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(2000)), + } + expectedBids[0] = bid1 + auctionKeeper.setBid(ctx, *bid1) + auctionKeeper.setLastBidID(ctx, uint64(1)) + + expectedTokenPrices := make([]*auctionTypes.TokenPrice, 2) + tokenPrice1 := &auctionTypes.TokenPrice{ + Denom: "usomm", + UsdPrice: sdk.MustNewDecFromStr("0.2"), + LastUpdatedBlock: uint64(2), + } + tokenPrice2 := &auctionTypes.TokenPrice{ + Denom: "weth", + UsdPrice: sdk.MustNewDecFromStr("17.45"), + LastUpdatedBlock: uint64(3), + } + expectedTokenPrices[0] = tokenPrice1 + expectedTokenPrices[1] = tokenPrice2 + auctionKeeper.setTokenPrice(ctx, *tokenPrice1) + auctionKeeper.setTokenPrice(ctx, *tokenPrice2) + + expectedGenesis := auctionTypes.GenesisState{ + Params: auctionTypes.DefaultGenesisState().Params, + Auctions: allAuctions, + Bids: expectedBids, + TokenPrices: expectedTokenPrices, + LastAuctionId: uint32(1), + LastBidId: uint64(1), + } + + exportedGenesis := ExportGenesis(ctx, auctionKeeper) + + // Verify values + require.Equal(expectedGenesis, exportedGenesis) + + require.Equal(auctionTypes.DefaultGenesisState().Params, exportedGenesis.Params) + require.Equal(allAuctions, exportedGenesis.Auctions) + require.Equal(expectedBids, exportedGenesis.Bids) + require.Equal(expectedTokenPrices, exportedGenesis.TokenPrices) + require.Equal(uint32(1), exportedGenesis.LastAuctionId) + require.Equal(uint64(1), exportedGenesis.LastBidId) +} diff --git a/x/auction/keeper/keeper.go b/x/auction/keeper/keeper.go index ab0fda36..6cc09d74 100644 --- a/x/auction/keeper/keeper.go +++ b/x/auction/keeper/keeper.go @@ -21,6 +21,7 @@ type Keeper struct { cdc codec.BinaryCodec paramSpace paramtypes.Subspace bankKeeper types.BankKeeper + accountKeeper types.AccountKeeper fundingModuleAccounts map[string]bool proceedsModuleAccounts map[string]bool } @@ -28,7 +29,7 @@ type Keeper struct { // NewKeeper creates a new auction Keeper instance func NewKeeper( cdc codec.BinaryCodec, key sdk.StoreKey, paramSpace paramtypes.Subspace, - bankKeeper types.BankKeeper, fundingModuleAccounts map[string]bool, proceedsModuleAccounts map[string]bool, + bankKeeper types.BankKeeper, accountKeeper types.AccountKeeper, fundingModuleAccounts map[string]bool, proceedsModuleAccounts map[string]bool, ) Keeper { // set KeyTable if it has not already been set if !paramSpace.HasKeyTable() { @@ -40,6 +41,7 @@ func NewKeeper( cdc: cdc, paramSpace: paramSpace, bankKeeper: bankKeeper, + accountKeeper: accountKeeper, fundingModuleAccounts: fundingModuleAccounts, proceedsModuleAccounts: proceedsModuleAccounts, } @@ -264,6 +266,7 @@ func (k Keeper) tokenPriceTooOld(ctx sdk.Context, tokenPrice *types.TokenPrice) func (k Keeper) FinishAuction(ctx sdk.Context, auction *types.Auction) error { // Figure out how many funds we have left over, if any, to send // Since we can only have 1 auction per denom active at a time, we can just query the balance + saleTokenBalance := k.bankKeeper.GetBalance(ctx, authtypes.NewModuleAddress(types.ModuleName), auction.StartingTokensForSale.Denom) if saleTokenBalance.Amount.IsPositive() { @@ -497,3 +500,12 @@ func (k Keeper) GetLastBidID(ctx sdk.Context) uint64 { return binary.BigEndian.Uint64(bz) } + +// /////////////////// +// Module Accounts // +// /////////////////// + +// Get the auction module account +func (k Keeper) GetAuctionAccount(ctx sdk.Context) authtypes.ModuleAccountI { + return k.accountKeeper.GetModuleAccount(ctx, types.ModuleName) +} diff --git a/x/auction/keeper/keeper_test.go b/x/auction/keeper/keeper_test.go new file mode 100644 index 00000000..29c9253d --- /dev/null +++ b/x/auction/keeper/keeper_test.go @@ -0,0 +1,445 @@ +package keeper + +import ( + "fmt" + "testing" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/testutil" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/suite" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + moduletestutil "github.com/peggyjv/sommelier/v4/testutil" + auctionTypes "github.com/peggyjv/sommelier/v4/x/auction/types" + + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + paramskeeper "github.com/cosmos/cosmos-sdk/x/params/keeper" + auctiontestutil "github.com/peggyjv/sommelier/v4/x/auction/testutil" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +var ( + permissionedFunder = authtypes.NewEmptyModuleAccount("permissionedFunder") + permissionedReciever = authtypes.NewEmptyModuleAccount("permissionedReciever") + cosmosAddress1 string = "cosmos16zrkzad482haunrn25ywvwy6fclh3vh7r0hcny" + cosmosAddress2 string = "cosmos18ld4633yswcyjdklej3att6aw93nhlf7ce4v8u" +) + +type BeginAuctionRequest struct { + ctx sdk.Context + startingTokensForSale sdk.Coin + initialPriceDecreaseRate sdk.Dec + priceDecreaseBlockInterval uint64 + fundingModuleAccount string + proceedsModuleAccount string +} + +type KeeperTestSuite struct { + suite.Suite + + ctx sdk.Context + auctionKeeper Keeper + bankKeeper *auctiontestutil.MockBankKeeper + accountKeeper *auctiontestutil.MockAccountKeeper + + queryClient auctionTypes.QueryClient + + encCfg moduletestutil.TestEncodingConfig +} + +func (suite *KeeperTestSuite) SetupTest() { + key := sdk.NewKVStoreKey(auctionTypes.StoreKey) + tkey := sdk.NewTransientStoreKey("transient_test") + testCtx := testutil.DefaultContext(key, tkey) + ctx := testCtx.WithBlockHeader(tmproto.Header{Height: 5, Time: tmtime.Now()}) + encCfg := moduletestutil.MakeTestEncodingConfig() + + // gomock initializations + ctrl := gomock.NewController(suite.T()) + suite.bankKeeper = auctiontestutil.NewMockBankKeeper(ctrl) + suite.accountKeeper = auctiontestutil.NewMockAccountKeeper(ctrl) + suite.ctx = ctx + + params := paramskeeper.NewKeeper( + encCfg.Codec, + codec.NewLegacyAmino(), + key, + tkey, + ) + + params.Subspace(auctionTypes.ModuleName) + subSpace, found := params.GetSubspace(auctionTypes.ModuleName) + suite.Assertions.True(found) + + suite.auctionKeeper = NewKeeper( + encCfg.Codec, + key, + subSpace, + suite.bankKeeper, + suite.accountKeeper, + map[string]bool{permissionedFunder.GetName(): true}, + map[string]bool{permissionedReciever.GetName(): true}, + ) + + auctionTypes.RegisterInterfaces(encCfg.InterfaceRegistry) + + queryHelper := baseapp.NewQueryServerTestHelper(ctx, encCfg.InterfaceRegistry) + auctionTypes.RegisterQueryServer(queryHelper, suite.auctionKeeper) + queryClient := auctionTypes.NewQueryClient(queryHelper) + + suite.queryClient = queryClient + suite.encCfg = encCfg +} + +func TestKeeperTestSuite(t *testing.T) { + suite.Run(t, new(KeeperTestSuite)) +} + +// Happy path for BeginAuction call +func (suite *KeeperTestSuite) TestHappyPathBeginAuction() { + ctx, auctionKeeper := suite.ctx, suite.auctionKeeper + require := suite.Require() + + params := auctionTypes.Params{PriceMaxBlockAge: 10} + auctionKeeper.setParams(ctx, params) + + sommPrice := auctionTypes.TokenPrice{Denom: auctionTypes.UsommDenom, UsdPrice: sdk.MustNewDecFromStr("0.01"), LastUpdatedBlock: 5} + + /* #nosec */ + saleToken := "gravity0xdac17f958d2ee523a2206206994597c13d831ec7" + saleTokenPrice := auctionTypes.TokenPrice{Denom: saleToken, UsdPrice: sdk.MustNewDecFromStr("0.02"), LastUpdatedBlock: 5} + auctionedSaleTokens := sdk.NewCoin(saleToken, sdk.NewInt(10000)) + + auctionKeeper.setTokenPrice(ctx, sommPrice) + auctionKeeper.setTokenPrice(ctx, saleTokenPrice) + + // Mock bank keeper fund transfer + suite.mockSendCoinsFromModuleToModule(ctx, permissionedFunder.GetName(), auctionTypes.ModuleName, sdk.NewCoins(auctionedSaleTokens)) + + // Start auction + decreaseRate := sdk.MustNewDecFromStr("0.05") + blockDecreaseInterval := uint64(5) + err := auctionKeeper.BeginAuction(ctx, auctionedSaleTokens, decreaseRate, blockDecreaseInterval, permissionedFunder.GetName(), permissionedReciever.GetName()) + require.Nil(err) + + // Verify auction got added to active auction store + auctionID := uint32(1) + createdAuction, found := auctionKeeper.GetActiveAuctionByID(ctx, auctionID) + require.True(found) + + expectedActiveAuction := auctionTypes.Auction{ + Id: auctionID, + StartingTokensForSale: auctionedSaleTokens, + StartBlock: uint64(ctx.BlockHeight()), + EndBlock: 0, + InitialPriceDecreaseRate: decreaseRate, + CurrentPriceDecreaseRate: decreaseRate, + PriceDecreaseBlockInterval: blockDecreaseInterval, + InitialUnitPriceInUsomm: sdk.NewDec(2), + CurrentUnitPriceInUsomm: sdk.NewDec(2), + RemainingTokensForSale: auctionedSaleTokens, + FundingModuleAccount: permissionedFunder.GetName(), + ProceedsModuleAccount: permissionedReciever.GetName(), + } + + require.Equal(expectedActiveAuction, createdAuction) +} + +// Happy path for FinishAuction (with some remaining funds) +func (suite *KeeperTestSuite) TestHappyPathFinishAuction() { + ctx, auctionKeeper := suite.ctx, suite.auctionKeeper + require := suite.Require() + + // 1. --------> Create an auction first so we can finish it + params := auctionTypes.Params{PriceMaxBlockAge: 10} + auctionKeeper.setParams(ctx, params) + + sommPrice := auctionTypes.TokenPrice{Denom: auctionTypes.UsommDenom, UsdPrice: sdk.MustNewDecFromStr("0.02"), LastUpdatedBlock: 2} + + /* #nosec */ + saleToken := "gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" + saleTokenPrice := auctionTypes.TokenPrice{Denom: saleToken, UsdPrice: sdk.MustNewDecFromStr("0.01"), LastUpdatedBlock: 2} + auctionedSaleTokens := sdk.NewCoin(saleToken, sdk.NewInt(10000)) + + auctionKeeper.setTokenPrice(ctx, sommPrice) + auctionKeeper.setTokenPrice(ctx, saleTokenPrice) + + // Mock bank keeper fund transfer + suite.mockSendCoinsFromModuleToModule(ctx, permissionedFunder.GetName(), auctionTypes.ModuleName, sdk.NewCoins(auctionedSaleTokens)) + + // Start auction + decreaseRate := sdk.MustNewDecFromStr("0.05") + blockDecreaseInterval := uint64(5) + err := auctionKeeper.BeginAuction(ctx, auctionedSaleTokens, decreaseRate, blockDecreaseInterval, permissionedFunder.GetName(), permissionedReciever.GetName()) + require.Nil(err) + + // 2. --------> We can now attempt to finish the created auction + auctionID := uint32(1) + createdAuction, found := auctionKeeper.GetActiveAuctionByID(ctx, auctionID) + require.True(found) + + // Mock bank keeper balance and transfers (say only 75% got sold (25% remaining) to test funder returns & proceeds transfers) + remainingSaleTokens := sdk.NewCoin(saleToken, auctionedSaleTokens.Amount.Quo(sdk.NewInt(4))) + suite.mockGetBalance(ctx, authtypes.NewModuleAddress(auctionTypes.ModuleName), saleToken, remainingSaleTokens) + + // First transfer to return funding tokens + suite.mockSendCoinsFromModuleToModule(ctx, auctionTypes.ModuleName, permissionedFunder.GetName(), sdk.NewCoins(remainingSaleTokens)) + + // Add a couple of fake bids into store (note none of these fields matter for this test aside from TotalUsommPaid) + amountPaid1 := sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(2500)) + auctionKeeper.setBid(ctx, auctionTypes.Bid{ + Id: 1, + AuctionId: 1, + Bidder: "bidder1", + MaxBidInUsomm: sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(2500)), + SaleTokenMinimumAmount: sdk.NewCoin(saleToken, sdk.NewInt(0)), + TotalFulfilledSaleTokens: sdk.NewCoin(saleToken, sdk.NewInt(5000)), + TotalUsommPaid: amountPaid1, + }) + + amountPaid2 := sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(1250)) + auctionKeeper.setBid(ctx, auctionTypes.Bid{ + Id: 2, + AuctionId: 1, + Bidder: "bidder2", + MaxBidInUsomm: sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(1250)), + SaleTokenMinimumAmount: sdk.NewCoin(saleToken, sdk.NewInt(0)), + TotalFulfilledSaleTokens: sdk.NewCoin(saleToken, sdk.NewInt(2500)), + TotalUsommPaid: amountPaid2, + }) + + // Second transfer to return proceeds from bids + totalUsommExpected := sdk.NewCoin(auctionTypes.UsommDenom, amountPaid1.Amount.Add(amountPaid2.Amount)) + suite.mockSendCoinsFromModuleToModule(ctx, auctionTypes.ModuleName, permissionedReciever.GetName(), sdk.NewCoins(totalUsommExpected)) + + // Change active auction tokens remaining before finishing auction to pretend tokens were sold + createdAuction.RemainingTokensForSale = remainingSaleTokens + + // Finally actually finish the auction + auctionKeeper.FinishAuction(ctx, &createdAuction) + + // Verify actual ended auction equals expected one + expectedEndedAuction := auctionTypes.Auction{ + Id: auctionID, + StartingTokensForSale: auctionedSaleTokens, + StartBlock: createdAuction.StartBlock, + EndBlock: uint64(ctx.BlockHeight()), + InitialPriceDecreaseRate: decreaseRate, + CurrentPriceDecreaseRate: decreaseRate, // Monotonic decrease rates for now + PriceDecreaseBlockInterval: blockDecreaseInterval, + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("0.5"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("0.5"), // Started and ended on the same block + RemainingTokensForSale: remainingSaleTokens, + FundingModuleAccount: permissionedFunder.GetName(), + ProceedsModuleAccount: permissionedReciever.GetName(), + } + actualEndedAuction, found := auctionKeeper.GetEndedAuctionByID(ctx, auctionID) + require.True(found) + + require.Equal(expectedEndedAuction, actualEndedAuction) + + // Make sure no active auctions exist anymore + require.Zero(len(auctionKeeper.GetActiveAuctions(ctx))) +} + +// Unhappy path tests for BeginAuction +func (suite *KeeperTestSuite) TestUnhappyPathsForBeginAuction() { + ctx, auctionKeeper := suite.ctx, suite.auctionKeeper + require := suite.Require() + + // Define basic param(s) + params := auctionTypes.Params{PriceMaxBlockAge: 10} + auctionKeeper.setParams(ctx, params) + + // Setup some token prices + sommPrice := auctionTypes.TokenPrice{Denom: auctionTypes.UsommDenom, UsdPrice: sdk.MustNewDecFromStr("0.01"), LastUpdatedBlock: 2} + + /* #nosec */ + saleToken := "gravity0xaaaebe6fe48e54f431b0c390cfaf0b017d09d42d" + saleTokenPrice := auctionTypes.TokenPrice{Denom: saleToken, UsdPrice: sdk.MustNewDecFromStr("0.01"), LastUpdatedBlock: 5} + auctionedSaleTokens := sdk.NewCoin(saleToken, sdk.NewInt(10000)) + + tests := []struct { + name string + beginAuctionRequest BeginAuctionRequest + expectedError error + runsBefore runsBeforeWrapper + }{ + { + name: "Unpermissioned funder module account", + beginAuctionRequest: BeginAuctionRequest{ + ctx: ctx, + startingTokensForSale: auctionedSaleTokens, + initialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + priceDecreaseBlockInterval: uint64(10), + fundingModuleAccount: "cork", + proceedsModuleAccount: permissionedReciever.GetName(), + }, + expectedError: sdkerrors.Wrapf(auctionTypes.ErrUnauthorizedFundingModule, "Module Account: cork"), + runsBefore: func() {}, + }, + { + name: "Unpermissioned proceeds module account", + beginAuctionRequest: BeginAuctionRequest{ + ctx: ctx, + startingTokensForSale: auctionedSaleTokens, + initialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + priceDecreaseBlockInterval: uint64(10), + fundingModuleAccount: permissionedFunder.GetName(), + proceedsModuleAccount: "gravity", + }, + expectedError: sdkerrors.Wrapf(auctionTypes.ErrUnauthorizedProceedsModule, "Module Account: gravity"), + runsBefore: func() {}, + }, + { + name: "Starting denom price not found", + beginAuctionRequest: BeginAuctionRequest{ + ctx: ctx, + startingTokensForSale: sdk.NewCoin("anvil", sdk.NewInt(7)), + initialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + priceDecreaseBlockInterval: uint64(10), + fundingModuleAccount: permissionedFunder.GetName(), + proceedsModuleAccount: permissionedReciever.GetName(), + }, + expectedError: sdkerrors.Wrapf(auctionTypes.ErrCouldNotFindSaleTokenPrice, "starting amount denom: anvil"), + runsBefore: func() {}, + }, + { + name: "Starting denom price update too old", + beginAuctionRequest: BeginAuctionRequest{ + ctx: ctx.WithBlockHeight(int64(saleTokenPrice.LastUpdatedBlock) + int64(params.PriceMaxBlockAge) + 1), + startingTokensForSale: auctionedSaleTokens, + initialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + priceDecreaseBlockInterval: uint64(10), + fundingModuleAccount: permissionedFunder.GetName(), + proceedsModuleAccount: permissionedReciever.GetName(), + }, + expectedError: sdkerrors.Wrapf(auctionTypes.ErrLastSaleTokenPriceTooOld, "starting amount denom: %s", saleToken), + runsBefore: func() { + auctionKeeper.setTokenPrice(ctx, saleTokenPrice) + }, + }, + { + name: "Usomm price not found", + beginAuctionRequest: BeginAuctionRequest{ + ctx: ctx, + startingTokensForSale: auctionedSaleTokens, + initialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + priceDecreaseBlockInterval: uint64(10), + fundingModuleAccount: permissionedFunder.GetName(), + proceedsModuleAccount: permissionedReciever.GetName(), + }, + expectedError: sdkerrors.Wrap(auctionTypes.ErrCouldNotFindSommTokenPrice, auctionTypes.UsommDenom), + runsBefore: func() {}, + }, + { + name: "Usomm price update too old", + beginAuctionRequest: BeginAuctionRequest{ + ctx: ctx.WithBlockHeight(int64(sommPrice.LastUpdatedBlock) + int64(params.PriceMaxBlockAge) + 1), + startingTokensForSale: auctionedSaleTokens, + initialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + priceDecreaseBlockInterval: uint64(10), + fundingModuleAccount: permissionedFunder.GetName(), + proceedsModuleAccount: permissionedReciever.GetName(), + }, + expectedError: sdkerrors.Wrap(auctionTypes.ErrLastSommTokenPriceTooOld, auctionTypes.UsommDenom), + runsBefore: func() { + auctionKeeper.setTokenPrice(ctx, sommPrice) + }, + }, + { + name: "Validate basic canary 1 -- invalid initialPriceDecreaseRate lower bound", + beginAuctionRequest: BeginAuctionRequest{ + ctx: ctx, + startingTokensForSale: auctionedSaleTokens, + initialPriceDecreaseRate: sdk.MustNewDecFromStr("0.0"), + priceDecreaseBlockInterval: uint64(10), + fundingModuleAccount: permissionedFunder.GetName(), + proceedsModuleAccount: permissionedReciever.GetName(), + }, + expectedError: sdkerrors.Wrapf(auctionTypes.ErrInvalidInitialDecreaseRate, "Inital price decrease rate 0.000000000000000000"), + runsBefore: func() {}, + }, + { + name: "Validate basic canary 2 -- invalid initialPriceDecreaseRate upper bound", + beginAuctionRequest: BeginAuctionRequest{ + ctx: ctx, + startingTokensForSale: auctionedSaleTokens, + initialPriceDecreaseRate: sdk.MustNewDecFromStr("1.0"), + priceDecreaseBlockInterval: uint64(10), + fundingModuleAccount: permissionedFunder.GetName(), + proceedsModuleAccount: permissionedReciever.GetName(), + }, + expectedError: sdkerrors.Wrapf(auctionTypes.ErrInvalidInitialDecreaseRate, "Inital price decrease rate 1.000000000000000000"), + runsBefore: func() {}, + }, + { + name: "Cannot have 2 ongoing auctions for the same denom", + beginAuctionRequest: BeginAuctionRequest{ + ctx: ctx, + startingTokensForSale: auctionedSaleTokens, + initialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + priceDecreaseBlockInterval: uint64(10), + fundingModuleAccount: permissionedFunder.GetName(), + proceedsModuleAccount: permissionedReciever.GetName(), + }, + expectedError: sdkerrors.Wrapf(auctionTypes.ErrCannotStartTwoAuctionsForSameDenomSimultaneously, "Denom: %s", auctionedSaleTokens.Denom), + runsBefore: func() { + // Mock initial bank keeper fund transfer + suite.mockSendCoinsFromModuleToModule(ctx, permissionedFunder.GetName(), auctionTypes.ModuleName, sdk.NewCoins(auctionedSaleTokens)) + + // Start auction + decreaseRate := sdk.MustNewDecFromStr("0.05") + blockDecreaseInterval := uint64(5) + err := auctionKeeper.BeginAuction(ctx, auctionedSaleTokens, decreaseRate, blockDecreaseInterval, permissionedFunder.GetName(), permissionedReciever.GetName()) + require.Nil(err) + + // Verify auction got added to active auction store + auctionID := uint32(1) + createdAuction, found := auctionKeeper.GetActiveAuctionByID(ctx, auctionID) + require.True(found) + + expectedActiveAuction := auctionTypes.Auction{ + Id: auctionID, + StartingTokensForSale: auctionedSaleTokens, + StartBlock: uint64(ctx.BlockHeight()), + EndBlock: 0, + InitialPriceDecreaseRate: decreaseRate, + CurrentPriceDecreaseRate: decreaseRate, + PriceDecreaseBlockInterval: blockDecreaseInterval, + InitialUnitPriceInUsomm: sdk.NewDec(1), + CurrentUnitPriceInUsomm: sdk.NewDec(1), + RemainingTokensForSale: auctionedSaleTokens, + FundingModuleAccount: permissionedFunder.GetName(), + ProceedsModuleAccount: permissionedReciever.GetName(), + } + require.Equal(expectedActiveAuction, createdAuction) + }, + }, + } + + for _, tc := range tests { + tc := tc // Redefine variable here due to passing it to function literal below (scopelint) + suite.T().Run(fmt.Sprint(tc.name), func(t *testing.T) { + // Run expected bank keeper functions, if any + tc.runsBefore() + + err := auctionKeeper.BeginAuction( + tc.beginAuctionRequest.ctx, + tc.beginAuctionRequest.startingTokensForSale, + tc.beginAuctionRequest.initialPriceDecreaseRate, + tc.beginAuctionRequest.priceDecreaseBlockInterval, + tc.beginAuctionRequest.fundingModuleAccount, + tc.beginAuctionRequest.proceedsModuleAccount, + ) + + // Verify errors are as expected + require.Equal(tc.expectedError.Error(), err.Error()) + }) + } +} diff --git a/x/auction/keeper/msg_server.go b/x/auction/keeper/msg_server.go index 1bf85429..6df98429 100644 --- a/x/auction/keeper/msg_server.go +++ b/x/auction/keeper/msg_server.go @@ -6,7 +6,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/peggyjv/sommelier/v4/x/auction/types" ) @@ -17,11 +16,7 @@ var _ types.MsgServer = Keeper{} func (k Keeper) SubmitBid(c context.Context, msg *types.MsgSubmitBidRequest) (*types.MsgSubmitBidResponse, error) { ctx := sdk.UnwrapSDKContext(c) - // Verify signer is the same as the bidder (this validates both the bidder and signer addresses) signer := msg.MustGetSigner() - if !signer.Equals(sdk.AccAddress(msg.Bidder)) { - return &types.MsgSubmitBidResponse{}, sdkerrors.Wrapf(types.ErrSignerDifferentFromBidder, "Signer: %s, Bidder: %s", msg.GetSigner(), msg.GetBidder()) - } // Verify auction auction, found := k.GetActiveAuctionByID(ctx, msg.GetAuctionId()) @@ -31,11 +26,11 @@ func (k Keeper) SubmitBid(c context.Context, msg *types.MsgSubmitBidRequest) (*t // Verify auction coin type and bidder coin type are equal if auction.GetStartingTokensForSale().Denom != msg.GetSaleTokenMinimumAmount().Denom { - return &types.MsgSubmitBidResponse{}, sdkerrors.Wrapf(types.ErrBidAuctionDenomMismatch, "Bid denom: %s, Auction denom: %s", msg.GetSaleTokenMinimumAmount(), auction.GetStartingTokensForSale().Denom) + return &types.MsgSubmitBidResponse{}, sdkerrors.Wrapf(types.ErrBidAuctionDenomMismatch, "Bid denom: %s, Auction denom: %s", msg.GetSaleTokenMinimumAmount().Denom, auction.GetStartingTokensForSale().Denom) } // Query our module address for funds - totalSaleTokenBalance := k.bankKeeper.GetBalance(ctx, authtypes.NewModuleAddress(types.ModuleName), auction.StartingTokensForSale.Denom) + totalSaleTokenBalance := k.bankKeeper.GetBalance(ctx, k.GetAuctionAccount(ctx).GetAddress(), auction.StartingTokensForSale.Denom) // Convert & standardize types for use below minimumSaleTokenPurchaseAmount := msg.SaleTokenMinimumAmount.Amount @@ -66,7 +61,7 @@ func (k Keeper) SubmitBid(c context.Context, msg *types.MsgSubmitBidRequest) (*t totalFulfilledSaleTokens.Amount = totalSaleTokenBalance.Amount } else { - return &types.MsgSubmitBidResponse{}, sdkerrors.Wrapf(types.ErrMinimumPurchaseAmountLargerThanTokensRemaining, "Minimum purchase: %s, amount remaining: %s", msg.SaleTokenMinimumAmount.String(), auction.RemainingTokensForSale.String()) + return &types.MsgSubmitBidResponse{}, sdkerrors.Wrapf(types.ErrMinimumPurchaseAmountLargerThanTokensRemaining, "Minimum purchase: %s, amount remaining: %s", minimumSaleTokenPurchaseAmount.String(), auction.RemainingTokensForSale.String()) } // Round up to prevent exploitability; ensure you can't get more than you pay for @@ -77,7 +72,7 @@ func (k Keeper) SubmitBid(c context.Context, msg *types.MsgSubmitBidRequest) (*t bid := types.Bid{ Id: newBidID, AuctionId: msg.GetAuctionId(), - Bidder: msg.GetBidder(), + Bidder: signer.String(), MaxBidInUsomm: msg.GetMaxBidInUsomm(), SaleTokenMinimumAmount: msg.GetSaleTokenMinimumAmount(), TotalFulfilledSaleTokens: totalFulfilledSaleTokens, @@ -91,12 +86,12 @@ func (k Keeper) SubmitBid(c context.Context, msg *types.MsgSubmitBidRequest) (*t } // Transfer payment first - if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sdk.AccAddress(msg.GetBidder()), types.ModuleName, sdk.NewCoins(totalUsommPaid)); err != nil { + if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, signer, types.ModuleName, sdk.NewCoins(totalUsommPaid)); err != nil { return &types.MsgSubmitBidResponse{}, err } // Transfer purchase to bidder - if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, sdk.AccAddress(msg.GetBidder()), sdk.NewCoins(totalFulfilledSaleTokens)); err != nil { + if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, signer, sdk.NewCoins(totalFulfilledSaleTokens)); err != nil { // TODO(pbal): Audit if we should panic here return &types.MsgSubmitBidResponse{}, err } @@ -130,9 +125,8 @@ func (k Keeper) SubmitBid(c context.Context, msg *types.MsgSubmitBidRequest) (*t types.EventTypeBid, sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprint(msg.GetAuctionId())), sdk.NewAttribute(types.AttributeKeyBidID, fmt.Sprint(newBidID)), - sdk.NewAttribute(types.AttributeKeyBidder, msg.GetBidder()), + sdk.NewAttribute(types.AttributeKeyBidder, signer.String()), sdk.NewAttribute(types.AttributeKeyMinimumAmount, msg.GetSaleTokenMinimumAmount().String()), - sdk.NewAttribute(types.AttributeKeySigner, msg.GetSigner()), sdk.NewAttribute(types.AttributeKeyFulfilledPrice, auction.CurrentUnitPriceInUsomm.String()), sdk.NewAttribute(types.AttributeKeyTotalPayment, totalUsommPaid.String()), sdk.NewAttribute(types.AttributeKeyFulfilledAmount, totalFulfilledSaleTokens.String()), diff --git a/x/auction/keeper/msg_server_test.go b/x/auction/keeper/msg_server_test.go new file mode 100644 index 00000000..ed32b78f --- /dev/null +++ b/x/auction/keeper/msg_server_test.go @@ -0,0 +1,306 @@ +package keeper + +import ( + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + auctionTypes "github.com/peggyjv/sommelier/v4/x/auction/types" +) + +type runsBeforeWrapper func() + +// Happy path test for submitting a bid both fully and partially +func (suite *KeeperTestSuite) TestHappyPathSubmitBidAndFulfillFully() { + ctx, auctionKeeper := suite.ctx, suite.auctionKeeper + require := suite.Require() + + // -----> Create an auction we can bid on first + params := auctionTypes.Params{PriceMaxBlockAge: 10} + auctionKeeper.setParams(ctx, params) + + sommPrice := auctionTypes.TokenPrice{Denom: auctionTypes.UsommDenom, UsdPrice: sdk.MustNewDecFromStr("0.01"), LastUpdatedBlock: 5} + + /* #nosec */ + saleToken := "gravity0xdac17f958d2ee523a2206206994597c13d831ec7" + saleTokenPrice := auctionTypes.TokenPrice{Denom: saleToken, UsdPrice: sdk.MustNewDecFromStr("0.02"), LastUpdatedBlock: 5} + auctionedSaleTokens := sdk.NewCoin(saleToken, sdk.NewInt(10000)) + + auctionKeeper.setTokenPrice(ctx, sommPrice) + auctionKeeper.setTokenPrice(ctx, saleTokenPrice) + + // Mock bank keeper fund transfer + suite.mockSendCoinsFromModuleToModule(ctx, permissionedFunder.GetName(), auctionTypes.ModuleName, sdk.NewCoins(auctionedSaleTokens)) + + // Start auction + decreaseRate := sdk.MustNewDecFromStr("0.05") + blockDecreaseInterval := uint64(5) + err := auctionKeeper.BeginAuction(ctx, auctionedSaleTokens, decreaseRate, blockDecreaseInterval, permissionedFunder.GetName(), permissionedReciever.GetName()) + require.Nil(err) + + // Submit a bid + auctionID := uint32(1) + bidder := cosmosAddress1 + require.Nil(err) + + bid := sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(5000)) + minAmount := sdk.NewCoin(saleToken, sdk.NewInt(1)) + + fulfilledBid := sdk.NewCoin(saleToken, sdk.NewInt(2500)) + + // Mock out bank keeper calls + bidderAcc, _ := sdk.AccAddressFromBech32(bidder) + suite.mockGetModuleAccount(ctx) + suite.mockGetBalance(ctx, authtypes.NewEmptyModuleAccount("mock").GetAddress(), saleToken, auctionedSaleTokens) + suite.mockSendCoinsFromAccountToModule(ctx, bidderAcc, auctionTypes.ModuleName, sdk.NewCoins(bid)) + suite.mockSendCoinsFromModuleToAccount(ctx, auctionTypes.ModuleName, bidderAcc, sdk.NewCoins(fulfilledBid)) + + // ~Actually~ submit the bid now + response, err := auctionKeeper.SubmitBid(sdk.WrapSDKContext(ctx), &auctionTypes.MsgSubmitBidRequest{ + AuctionId: auctionID, + Signer: bidder, + MaxBidInUsomm: bid, + SaleTokenMinimumAmount: minAmount, + }) + require.Nil(err) + + // Assert bid store and response bids are both equal to expected bid + expectedBid := auctionTypes.Bid{ + Id: uint64(1), + AuctionId: uint32(1), + Bidder: bidder, + MaxBidInUsomm: bid, + SaleTokenMinimumAmount: minAmount, + TotalFulfilledSaleTokens: fulfilledBid, + SaleTokenUnitPriceInUsomm: sdk.NewDec(2), + TotalUsommPaid: bid, + } + require.Equal(&expectedBid, response.Bid) + + storedBid, found := auctionKeeper.GetBid(ctx, uint32(1), uint64(1)) + require.True(found) + require.Equal(expectedBid, storedBid) + + // Assert auction token amounts are updated + expectedUpdatedAuction := auctionTypes.Auction{ + Id: auctionID, + StartingTokensForSale: auctionedSaleTokens, + StartBlock: uint64(ctx.BlockHeight()), + EndBlock: 0, + InitialPriceDecreaseRate: decreaseRate, + CurrentPriceDecreaseRate: decreaseRate, + PriceDecreaseBlockInterval: blockDecreaseInterval, + InitialUnitPriceInUsomm: sdk.NewDec(2), + CurrentUnitPriceInUsomm: sdk.NewDec(2), + RemainingTokensForSale: sdk.NewCoin(saleToken, sdk.NewInt(7500)), // this is the important part, need to make sure it decremented + FundingModuleAccount: permissionedFunder.GetName(), + ProceedsModuleAccount: permissionedReciever.GetName(), + } + + activeAuction, found := auctionKeeper.GetActiveAuctionByID(ctx, auctionID) + require.True(found) + require.Equal(expectedUpdatedAuction, activeAuction) + + // Now check flow of a bid that can only be partially fulfilled, and verify it finishes the auction + newBidder := cosmosAddress2 + newBid := sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(50000)) + newFulfilledAmt := sdk.NewCoin(saleToken, sdk.NewInt(7500)) + paidAmt := sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(15000)) + + // Mock out necessary bank keeper calls for bid completion + newBidderAcc, _ := sdk.AccAddressFromBech32(newBidder) + suite.mockGetModuleAccount(ctx) + suite.mockGetBalance(ctx, authtypes.NewEmptyModuleAccount("mock").GetAddress(), saleToken, expectedUpdatedAuction.RemainingTokensForSale) + suite.mockSendCoinsFromAccountToModule(ctx, newBidderAcc, auctionTypes.ModuleName, sdk.NewCoins(paidAmt)) + suite.mockSendCoinsFromModuleToAccount(ctx, auctionTypes.ModuleName, newBidderAcc, sdk.NewCoins(newFulfilledAmt)) + + // Mock out final keeper calls necessary to finish the auction due to bid draining the availible supply + suite.mockGetBalance(ctx, authtypes.NewModuleAddress(auctionTypes.ModuleName), saleToken, sdk.NewCoin(saleToken, sdk.NewInt(0))) + totalUsommExpected := sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(20000)) + suite.mockSendCoinsFromModuleToModule(ctx, auctionTypes.ModuleName, permissionedReciever.GetName(), sdk.NewCoins(totalUsommExpected)) + + // Submit the partially fulfillable bid now + response, err = auctionKeeper.SubmitBid(sdk.WrapSDKContext(ctx), &auctionTypes.MsgSubmitBidRequest{ + AuctionId: auctionID, + Signer: newBidder, + MaxBidInUsomm: newBid, + SaleTokenMinimumAmount: minAmount, + }) + require.Nil(err) + + // Assert bid store and response bids are both equal to the new expected bid + newExpectedBid := auctionTypes.Bid{ + Id: uint64(2), + AuctionId: uint32(1), + Bidder: newBidder, + MaxBidInUsomm: newBid, + SaleTokenMinimumAmount: minAmount, + TotalFulfilledSaleTokens: newFulfilledAmt, + SaleTokenUnitPriceInUsomm: sdk.NewDec(2), + TotalUsommPaid: paidAmt, + } + require.Equal(&newExpectedBid, response.Bid) + + storedBid, found = auctionKeeper.GetBid(ctx, uint32(1), uint64(2)) + require.True(found) + require.Equal(newExpectedBid, storedBid) + + // Verify bid caused auction to finish + expectedUpdatedAuction.RemainingTokensForSale.Amount = sdk.NewInt(0) + expectedUpdatedAuction.EndBlock = uint64(ctx.BlockHeight()) + + _, found = auctionKeeper.GetActiveAuctionByID(ctx, auctionID) + require.False(found) + + endedAuction, found := auctionKeeper.GetEndedAuctionByID(ctx, auctionID) + require.True(found) + require.Equal(expectedUpdatedAuction, endedAuction) +} + +// Unhappy path tests for all failure modes of SubmitBid +func (suite *KeeperTestSuite) TestUnhappyPathsForSubmitBid() { + ctx, auctionKeeper := suite.ctx, suite.auctionKeeper + require := suite.Require() + + // Create an active auction for bids to test against + params := auctionTypes.Params{PriceMaxBlockAge: 10} + auctionKeeper.setParams(ctx, params) + + sommPrice := auctionTypes.TokenPrice{Denom: auctionTypes.UsommDenom, UsdPrice: sdk.MustNewDecFromStr("0.01"), LastUpdatedBlock: 5} + + /* #nosec */ + saleToken := "gravity0x853d955acef822db058eb8505911ed77f175b99e" + saleTokenPrice := auctionTypes.TokenPrice{Denom: saleToken, UsdPrice: sdk.MustNewDecFromStr("0.02"), LastUpdatedBlock: 5} + auctionedSaleTokens := sdk.NewCoin(saleToken, sdk.NewInt(10000)) + + auctionKeeper.setTokenPrice(ctx, sommPrice) + auctionKeeper.setTokenPrice(ctx, saleTokenPrice) + + // Mock bank keeper fund transfer + suite.mockSendCoinsFromModuleToModule(ctx, permissionedFunder.GetName(), auctionTypes.ModuleName, sdk.NewCoins(auctionedSaleTokens)) + + // Start auction + decreaseRate := sdk.MustNewDecFromStr("0.05") + blockDecreaseInterval := uint64(5) + err := auctionKeeper.BeginAuction(ctx, auctionedSaleTokens, decreaseRate, blockDecreaseInterval, permissionedFunder.GetName(), permissionedReciever.GetName()) + require.Nil(err) + + // Verify auction got added to active auction store + auctionID := uint32(1) + originalAuction, found := auctionKeeper.GetActiveAuctionByID(ctx, auctionID) + require.True(found) + + tests := []struct { + name string + bid auctionTypes.MsgSubmitBidRequest + expectedError error + runsBefore runsBeforeWrapper + submitBidResponse *auctionTypes.MsgSubmitBidResponse + }{ + { + name: "Auction ID not found", + bid: auctionTypes.MsgSubmitBidRequest{ + AuctionId: uint32(420), + Signer: cosmosAddress1, + MaxBidInUsomm: sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(100)), + SaleTokenMinimumAmount: sdk.NewCoin(saleToken, sdk.NewInt(1)), + }, + expectedError: sdkerrors.Wrapf(auctionTypes.ErrAuctionNotFound, "Auction id: %d", uint32(420)), + runsBefore: func() {}, + submitBidResponse: &auctionTypes.MsgSubmitBidResponse{}, + }, + { + name: "Denom mismatch", + bid: auctionTypes.MsgSubmitBidRequest{ + AuctionId: auctionID, + Signer: cosmosAddress1, + MaxBidInUsomm: sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(100)), + SaleTokenMinimumAmount: sdk.NewCoin("blemflarcks", sdk.NewInt(1)), + }, + expectedError: sdkerrors.Wrapf(auctionTypes.ErrBidAuctionDenomMismatch, "Bid denom: blemflarcks, Auction denom: %s", saleToken), + runsBefore: func() {}, + submitBidResponse: &auctionTypes.MsgSubmitBidResponse{}, + }, + { + name: "Minimum amount to purchase larger than bid can obtain", + bid: auctionTypes.MsgSubmitBidRequest{ + AuctionId: auctionID, + Signer: cosmosAddress1, + MaxBidInUsomm: sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(1)), + SaleTokenMinimumAmount: sdk.NewCoin(saleToken, sdk.NewInt(1)), + }, + expectedError: sdkerrors.Wrapf(auctionTypes.ErrInsufficientBid, "minimum purchase price: 2, max bid: 1"), + runsBefore: func() { + suite.mockGetModuleAccount(ctx) + suite.mockGetBalance(ctx, authtypes.NewEmptyModuleAccount("mock").GetAddress(), saleToken, originalAuction.RemainingTokensForSale) + }, + submitBidResponse: &auctionTypes.MsgSubmitBidResponse{}, + }, + { + name: "Minimum amount larger than remaining tokens in auction", + bid: auctionTypes.MsgSubmitBidRequest{ + AuctionId: auctionID, + Signer: cosmosAddress1, + MaxBidInUsomm: sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(40000)), + SaleTokenMinimumAmount: sdk.NewCoin(saleToken, sdk.NewInt(10002)), + }, + expectedError: sdkerrors.Wrapf(auctionTypes.ErrMinimumPurchaseAmountLargerThanTokensRemaining, "Minimum purchase: %s, amount remaining: %s", sdk.NewInt(10002), originalAuction.RemainingTokensForSale.String()), + runsBefore: func() { + suite.mockGetModuleAccount(ctx) + suite.mockGetBalance(ctx, authtypes.NewEmptyModuleAccount("mock").GetAddress(), saleToken, originalAuction.RemainingTokensForSale) + }, + submitBidResponse: &auctionTypes.MsgSubmitBidResponse{}, + }, + { + name: "Validate Basic canary 1 -- bid denom must be in usomm", + bid: auctionTypes.MsgSubmitBidRequest{ + AuctionId: auctionID, + Signer: cosmosAddress1, + MaxBidInUsomm: sdk.NewCoin("cinnamonRollCoin", sdk.NewInt(200)), + SaleTokenMinimumAmount: sdk.NewCoin(saleToken, sdk.NewInt(100)), + }, + expectedError: sdkerrors.Wrapf(auctionTypes.ErrBidMustBeInUsomm, "bid: %s", sdk.NewCoin("cinnamonRollCoin", sdk.NewInt(200)).String()), + runsBefore: func() { + suite.mockGetModuleAccount(ctx) + suite.mockGetBalance(ctx, authtypes.NewEmptyModuleAccount("mock").GetAddress(), saleToken, originalAuction.RemainingTokensForSale) + }, + submitBidResponse: &auctionTypes.MsgSubmitBidResponse{}, + }, + { + name: "Validate Basic canary 2 -- minimum amount of sale tokens cannot be 0", + bid: auctionTypes.MsgSubmitBidRequest{ + AuctionId: auctionID, + Signer: cosmosAddress1, + MaxBidInUsomm: sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(200)), + SaleTokenMinimumAmount: sdk.NewCoin(saleToken, sdk.NewInt(0)), + }, + expectedError: sdkerrors.Wrapf(auctionTypes.ErrMinimumAmountMustBePositive, "sale token amount: %s", sdk.NewCoin(saleToken, sdk.NewInt(0)).String()), + runsBefore: func() { + suite.mockGetModuleAccount(ctx) + suite.mockGetBalance(ctx, authtypes.NewEmptyModuleAccount("mock").GetAddress(), saleToken, originalAuction.RemainingTokensForSale) + }, + submitBidResponse: &auctionTypes.MsgSubmitBidResponse{}, + }, + } + + for _, tc := range tests { + tc := tc // Redefine variable here due to passing it to function literal below (scopelint) + suite.T().Run(fmt.Sprint(tc.name), func(t *testing.T) { + // Run expected bank keeper functions, if any + tc.runsBefore() + response, err := auctionKeeper.SubmitBid(sdk.WrapSDKContext(ctx), &tc.bid) + + // Verify bid errors are as expected + require.Equal(tc.expectedError.Error(), err.Error()) + require.Equal(tc.submitBidResponse, response) + + // Verify original auction not changed + foundAuction, found := auctionKeeper.GetActiveAuctionByID(ctx, auctionID) + require.True(found) + require.Equal(originalAuction, foundAuction) + }) + } +} diff --git a/x/auction/keeper/proposal_handler.go b/x/auction/keeper/proposal_handler.go index 74132977..89951d5a 100644 --- a/x/auction/keeper/proposal_handler.go +++ b/x/auction/keeper/proposal_handler.go @@ -2,6 +2,7 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/peggyjv/sommelier/v4/x/auction/types" ) diff --git a/x/auction/keeper/proposal_handler_test.go b/x/auction/keeper/proposal_handler_test.go new file mode 100644 index 00000000..889e40f3 --- /dev/null +++ b/x/auction/keeper/proposal_handler_test.go @@ -0,0 +1,116 @@ +package keeper + +import ( + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + govTypes "github.com/cosmos/cosmos-sdk/x/gov/types" + auctionTypes "github.com/peggyjv/sommelier/v4/x/auction/types" +) + +// Happy path test for proposal handler +func (suite *KeeperTestSuite) TestHappPathForProposalHandler() { + ctx, auctionKeeper := suite.ctx, suite.auctionKeeper + require := suite.Require() + + tokenPrices := make([]*auctionTypes.ProposedTokenPrice, 3) + tokenPrices[0] = &auctionTypes.ProposedTokenPrice{ + Denom: "gravity0x3506424f91fd33084466f402d5d97f05f8e3b4af", + UsdPrice: sdk.MustNewDecFromStr("2.5"), + } + tokenPrices[1] = &auctionTypes.ProposedTokenPrice{ + Denom: "gravity0x5a98fcbea516cf06857215779fd812ca3bef1b32", + UsdPrice: sdk.MustNewDecFromStr("1.7"), + } + tokenPrices[2] = &auctionTypes.ProposedTokenPrice{ + Denom: auctionTypes.UsommDenom, + UsdPrice: sdk.MustNewDecFromStr("1000.0"), + } + + proposal := auctionTypes.SetTokenPricesProposal{ + Title: "Super cool and exciting token update proposal", + Description: "NYC style pizza >>> Chicago style pizza", + TokenPrices: tokenPrices, + } + + err := HandleSetTokenPricesProposal(ctx, auctionKeeper, proposal) + require.Nil(err) + + // Verify token prices set + foundTokenPrices := auctionKeeper.GetTokenPrices(ctx) + require.Len(foundTokenPrices, 3) + + require.Equal(tokenPrices[0].Denom, foundTokenPrices[0].Denom) + require.Equal(tokenPrices[0].UsdPrice, foundTokenPrices[0].UsdPrice) + + require.Equal(tokenPrices[1].Denom, foundTokenPrices[1].Denom) + require.Equal(tokenPrices[1].UsdPrice, foundTokenPrices[1].UsdPrice) + + require.Equal(tokenPrices[2].Denom, foundTokenPrices[2].Denom) + require.Equal(tokenPrices[2].UsdPrice, foundTokenPrices[2].UsdPrice) + +} + +// Unhappy path test for proposal handler +func (suite *KeeperTestSuite) TestUnhappPathForProposalHandler() { + ctx, auctionKeeper := suite.ctx, suite.auctionKeeper + require := suite.Require() + + tests := []struct { + name string + proposal auctionTypes.SetTokenPricesProposal + expectedError error + }{ + { + name: "Validate basic canary 1 -- Govtypes validate abstract canary -- title length cannot be 0", + proposal: auctionTypes.SetTokenPricesProposal{ + Title: "", + Description: "Description", + TokenPrices: []*auctionTypes.ProposedTokenPrice{}, + }, + expectedError: sdkerrors.Wrap(govTypes.ErrInvalidProposalContent, "proposal title cannot be blank"), + }, + { + name: "Validate basic canary 2 -- cannot have non usomm & non gravity denom", + proposal: auctionTypes.SetTokenPricesProposal{ + Title: "Title", + Description: "Description", + TokenPrices: []*auctionTypes.ProposedTokenPrice{ + { + Denom: "weth", + UsdPrice: sdk.MustNewDecFromStr("17.0"), + }, + }, + }, + expectedError: sdkerrors.Wrapf(auctionTypes.ErrInvalidTokenPriceDenom, "denom: weth"), + }, + { + name: "Cannot attempt to update prices twice for a denom in one proposal", + proposal: auctionTypes.SetTokenPricesProposal{ + Title: "Title", + Description: "Description", + TokenPrices: []*auctionTypes.ProposedTokenPrice{ + { + Denom: "gravity0x761d38e5ddf6ccf6cf7c55759d5210750b5d60f3", + UsdPrice: sdk.MustNewDecFromStr("0.01"), + }, + { + Denom: "gravity0x761d38e5ddf6ccf6cf7c55759d5210750b5d60f3", + UsdPrice: sdk.MustNewDecFromStr("0.02"), + }, + }, + }, + expectedError: sdkerrors.Wrapf(auctionTypes.ErrTokenPriceProposalAttemptsToUpdateTokenPriceMoreThanOnce, "denom: gravity0x761d38e5ddf6ccf6cf7c55759d5210750b5d60f3"), + }, + } + + for _, tc := range tests { + tc := tc // Redefine variable here due to passing it to function literal below (scopelint) + suite.T().Run(fmt.Sprint(tc.name), func(t *testing.T) { + err := HandleSetTokenPricesProposal(ctx, auctionKeeper, tc.proposal) + require.Equal(tc.expectedError.Error(), err.Error()) + }) + } +} diff --git a/x/auction/keeper/query_server_test.go b/x/auction/keeper/query_server_test.go new file mode 100644 index 00000000..7c3b2a8a --- /dev/null +++ b/x/auction/keeper/query_server_test.go @@ -0,0 +1,205 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + auctionTypes "github.com/peggyjv/sommelier/v4/x/auction/types" +) + +// Happy path test for query server functions +func (suite *KeeperTestSuite) TestHappyPathsForQueryServer() { + ctx, auctionKeeper := suite.ctx, suite.auctionKeeper + require := suite.Require() + + params := auctionTypes.DefaultParams() + auctionKeeper.setParams(ctx, params) + + // Create some active auctions + activeAuction1 := &auctionTypes.Auction{ + Id: uint32(2), + StartingTokensForSale: sdk.NewCoin("weth", sdk.NewInt(17000)), + StartBlock: uint64(1), + EndBlock: uint64(0), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + PriceDecreaseBlockInterval: uint64(20), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("20"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("10"), + RemainingTokensForSale: sdk.NewCoin("weth", sdk.NewInt(10000)), + FundingModuleAccount: permissionedFunder.GetName(), + ProceedsModuleAccount: permissionedReciever.GetName(), + } + activeAuction2 := &auctionTypes.Auction{ + Id: uint32(3), + StartingTokensForSale: sdk.NewCoin("usdc", sdk.NewInt(9000)), + StartBlock: uint64(3), + EndBlock: uint64(0), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.02"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.02"), + PriceDecreaseBlockInterval: uint64(10), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("1"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("1"), + RemainingTokensForSale: sdk.NewCoin("usdc", sdk.NewInt(5000)), + FundingModuleAccount: permissionedFunder.GetName(), + ProceedsModuleAccount: permissionedReciever.GetName(), + } + auctionKeeper.setActiveAuction(ctx, *activeAuction1) + auctionKeeper.setActiveAuction(ctx, *activeAuction2) + + // Create an ended auction + endedAuction := &auctionTypes.Auction{ + Id: uint32(1), + StartingTokensForSale: sdk.NewCoin("matic", sdk.NewInt(3000)), + StartBlock: uint64(1), + EndBlock: uint64(5), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.07"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.02"), + PriceDecreaseBlockInterval: uint64(3), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("17"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("12.5"), + RemainingTokensForSale: sdk.NewCoin("weth", sdk.NewInt(1000)), + FundingModuleAccount: permissionedFunder.GetName(), + ProceedsModuleAccount: permissionedReciever.GetName(), + } + auctionKeeper.setEndedAuction(ctx, *endedAuction) + + // Create some bids for active auctions + bid1 := &auctionTypes.Bid{ + Id: uint64(2), + AuctionId: uint32(2), + Bidder: cosmosAddress1, + MaxBidInUsomm: sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(2000)), + SaleTokenMinimumAmount: sdk.NewCoin("weth", sdk.NewInt(20)), + TotalFulfilledSaleTokens: sdk.NewCoin("weth", sdk.NewInt(100)), + SaleTokenUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + TotalUsommPaid: sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(2000)), + } + bid2 := &auctionTypes.Bid{ + Id: uint64(3), + AuctionId: uint32(2), + Bidder: cosmosAddress2, + MaxBidInUsomm: sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(1500)), + SaleTokenMinimumAmount: sdk.NewCoin("weth", sdk.NewInt(10)), + TotalFulfilledSaleTokens: sdk.NewCoin("weth", sdk.NewInt(500)), + SaleTokenUnitPriceInUsomm: sdk.MustNewDecFromStr("10.07"), + TotalUsommPaid: sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(1370)), + } + bid3 := &auctionTypes.Bid{ + Id: uint64(4), + AuctionId: uint32(3), + Bidder: cosmosAddress2, + MaxBidInUsomm: sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(500)), + SaleTokenMinimumAmount: sdk.NewCoin("usdc", sdk.NewInt(1)), + TotalFulfilledSaleTokens: sdk.NewCoin("usdc", sdk.NewInt(20)), + SaleTokenUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + TotalUsommPaid: sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(20)), + } + + auctionKeeper.setBid(ctx, *bid1) + auctionKeeper.setBid(ctx, *bid2) + auctionKeeper.setBid(ctx, *bid3) + + // Create a bids for the ended auction + bid0 := &auctionTypes.Bid{ + Id: uint64(1), + AuctionId: uint32(1), + Bidder: cosmosAddress1, + MaxBidInUsomm: sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(1500)), + SaleTokenMinimumAmount: sdk.NewCoin("matic", sdk.NewInt(100)), + TotalFulfilledSaleTokens: sdk.NewCoin("matic", sdk.NewInt(1500)), + SaleTokenUnitPriceInUsomm: sdk.MustNewDecFromStr("1.0"), + TotalUsommPaid: sdk.NewCoin(auctionTypes.UsommDenom, sdk.NewInt(1500)), + } + auctionKeeper.setBid(ctx, *bid0) + + auctionKeeper.setLastAuctionID(ctx, uint32(3)) + auctionKeeper.setLastBidID(ctx, uint64(4)) + + // -- Actually begin testing + + // QueryParams + paramsResponse, err := auctionKeeper.QueryParams(sdk.WrapSDKContext(ctx), &auctionTypes.QueryParamsRequest{}) + require.Nil(err) + require.Equal(&auctionTypes.QueryParamsResponse{Params: auctionTypes.DefaultParams()}, paramsResponse) + + // QueryActiveAuction + activeAuctionResponse, err := auctionKeeper.QueryActiveAuction(sdk.WrapSDKContext(ctx), &auctionTypes.QueryActiveAuctionRequest{AuctionId: uint32(3)}) + require.Nil(err) + require.Equal(&auctionTypes.QueryActiveAuctionResponse{Auction: activeAuction2}, activeAuctionResponse) + + // QueryEndedAuction + endedAuctionResponse, err := auctionKeeper.QueryEndedAuction(sdk.WrapSDKContext(ctx), &auctionTypes.QueryEndedAuctionRequest{AuctionId: uint32(1)}) + require.Nil(err) + require.Equal(&auctionTypes.QueryEndedAuctionResponse{Auction: endedAuction}, endedAuctionResponse) + + // QueryActiveAuctions + activeAuctionsResponse, err := auctionKeeper.QueryActiveAuctions(sdk.WrapSDKContext(ctx), &auctionTypes.QueryActiveAuctionsRequest{}) + require.Nil(err) + require.Equal(&auctionTypes.QueryActiveAuctionsResponse{Auctions: []*auctionTypes.Auction{activeAuction1, activeAuction2}}, activeAuctionsResponse) + + // QueryEndedAuctions + endedAuctionsResponse, err := auctionKeeper.QueryEndedAuctions(sdk.WrapSDKContext(ctx), &auctionTypes.QueryEndedAuctionsRequest{}) + require.Nil(err) + require.Equal(&auctionTypes.QueryEndedAuctionsResponse{Auctions: []*auctionTypes.Auction{endedAuction}}, endedAuctionsResponse) + + // QueryBid -- active auction + activeBidResponse, err := auctionKeeper.QueryBid(sdk.WrapSDKContext(ctx), &auctionTypes.QueryBidRequest{BidId: uint64(4), AuctionId: uint32(3)}) + require.Nil(err) + require.Equal(&auctionTypes.QueryBidResponse{Bid: bid3}, activeBidResponse) + + // QueryBid -- ended auction + endedBidResponse, err := auctionKeeper.QueryBid(sdk.WrapSDKContext(ctx), &auctionTypes.QueryBidRequest{BidId: uint64(1), AuctionId: uint32(1)}) + require.Nil(err) + require.Equal(&auctionTypes.QueryBidResponse{Bid: bid0}, endedBidResponse) + + // QueryBidsByAuction -- active auction + activeBidsResponse, err := auctionKeeper.QueryBidsByAuction(sdk.WrapSDKContext(ctx), &auctionTypes.QueryBidsByAuctionRequest{AuctionId: uint32(2)}) + require.Nil(err) + require.Equal(&auctionTypes.QueryBidsByAuctionResponse{Bids: []*auctionTypes.Bid{bid1, bid2}}, activeBidsResponse) + + // QueryBidsByAuction -- ended auction + endedBidsResponse, err := auctionKeeper.QueryBidsByAuction(sdk.WrapSDKContext(ctx), &auctionTypes.QueryBidsByAuctionRequest{AuctionId: uint32(1)}) + require.Nil(err) + require.Equal(&auctionTypes.QueryBidsByAuctionResponse{Bids: []*auctionTypes.Bid{bid0}}, endedBidsResponse) +} + +// Unhappy path test for query server functions +func (suite *KeeperTestSuite) TestUnhappPathsForQueryServer() { + ctx, auctionKeeper := suite.ctx, suite.auctionKeeper + require := suite.Require() + + // Note we dont unhappy path test QueryParams bc it cannot return an error currently + + // QueryActiveAuction + activeAuctionResponse, err := auctionKeeper.QueryActiveAuction(sdk.WrapSDKContext(ctx), &auctionTypes.QueryActiveAuctionRequest{AuctionId: uint32(3)}) + require.Equal(status.Errorf(codes.NotFound, "No active auction found for id: 3"), err) + require.Equal(&auctionTypes.QueryActiveAuctionResponse{}, activeAuctionResponse) + + // QueryEndedAuction + endedAuctionResponse, err := auctionKeeper.QueryEndedAuction(sdk.WrapSDKContext(ctx), &auctionTypes.QueryEndedAuctionRequest{AuctionId: uint32(1)}) + require.Equal(status.Errorf(codes.NotFound, "No ended auction found for id: 1"), err) + require.Equal(&auctionTypes.QueryEndedAuctionResponse{}, endedAuctionResponse) + + // QueryActiveAuctions + activeAuctionsResponse, err := auctionKeeper.QueryActiveAuctions(sdk.WrapSDKContext(ctx), &auctionTypes.QueryActiveAuctionsRequest{}) + require.Equal(status.Error(codes.NotFound, "No active auctions found"), err) + require.Equal(&auctionTypes.QueryActiveAuctionsResponse{}, activeAuctionsResponse) + + // QueryEndedAuctions + endedAuctionsResponse, err := auctionKeeper.QueryEndedAuctions(sdk.WrapSDKContext(ctx), &auctionTypes.QueryEndedAuctionsRequest{}) + require.Equal(status.Error(codes.NotFound, "No ended auctions found"), err) + require.Equal(&auctionTypes.QueryEndedAuctionsResponse{}, endedAuctionsResponse) + + // QueryBid + bidResponse, err := auctionKeeper.QueryBid(sdk.WrapSDKContext(ctx), &auctionTypes.QueryBidRequest{BidId: uint64(4), AuctionId: uint32(3)}) + require.Equal(status.Errorf(codes.NotFound, "No bid found for specified bid id: 4, and auction id: 3"), err) + require.Equal(&auctionTypes.QueryBidResponse{}, bidResponse) + + // QueryBidsByAuction + bidsResponse, err := auctionKeeper.QueryBidsByAuction(sdk.WrapSDKContext(ctx), &auctionTypes.QueryBidsByAuctionRequest{AuctionId: uint32(1)}) + require.Equal(status.Errorf(codes.NotFound, "No bids found for auction id: 1"), err) + require.Equal(&auctionTypes.QueryBidsByAuctionResponse{}, bidsResponse) + +} diff --git a/x/auction/keeper/sdk_module_mocks_test.go b/x/auction/keeper/sdk_module_mocks_test.go new file mode 100644 index 00000000..65480a09 --- /dev/null +++ b/x/auction/keeper/sdk_module_mocks_test.go @@ -0,0 +1,28 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + auctionTypes "github.com/peggyjv/sommelier/v4/x/auction/types" +) + +func (suite *KeeperTestSuite) mockGetModuleAccount(ctx sdk.Context) { + suite.accountKeeper.EXPECT().GetModuleAccount(ctx, auctionTypes.ModuleName).Return(authtypes.NewEmptyModuleAccount("mock")) +} + +func (suite *KeeperTestSuite) mockGetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string, expectedOutput sdk.Coin) { + suite.bankKeeper.EXPECT().GetBalance(ctx, addr, denom).Return(expectedOutput) +} + +func (suite *KeeperTestSuite) mockSendCoinsFromModuleToModule(ctx sdk.Context, sender string, receiver string, amt sdk.Coins) { + suite.bankKeeper.EXPECT().SendCoinsFromModuleToModule(ctx, sender, receiver, amt).Return(nil) +} + +func (suite *KeeperTestSuite) mockSendCoinsFromAccountToModule(ctx sdk.Context, senderAcct sdk.AccAddress, receiverModule string, amt sdk.Coins) { + suite.bankKeeper.EXPECT().SendCoinsFromAccountToModule(ctx, senderAcct, receiverModule, amt).Return(nil) +} + +func (suite *KeeperTestSuite) mockSendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, receiverAcct sdk.AccAddress, amt sdk.Coins) { + suite.bankKeeper.EXPECT().SendCoinsFromModuleToAccount(ctx, senderModule, receiverAcct, amt).Return(nil) +} \ No newline at end of file diff --git a/x/auction/module.go b/x/auction/module.go index 5bb04d4e..7e2c2731 100644 --- a/x/auction/module.go +++ b/x/auction/module.go @@ -81,17 +81,19 @@ func (b AppModuleBasic) RegisterInterfaces(registry codectypes.InterfaceRegistry // AppModule implements an application module for the auction module. type AppModule struct { AppModuleBasic - keeper keeper.Keeper - bankKeeper types.BankKeeper - cdc codec.Codec + keeper keeper.Keeper + bankKeeper types.BankKeeper + accountKeeper types.AccountKeeper + cdc codec.Codec } // NewAppModule creates a new AppModule object -func NewAppModule(keeper keeper.Keeper, bankKeeper types.BankKeeper, cdc codec.Codec) AppModule { +func NewAppModule(keeper keeper.Keeper, bankKeeper types.BankKeeper, accountKeeper types.AccountKeeper, cdc codec.Codec) AppModule { return AppModule{ AppModuleBasic: AppModuleBasic{}, keeper: keeper, bankKeeper: bankKeeper, + accountKeeper: accountKeeper, cdc: cdc, } } diff --git a/x/auction/testutil/expected_keepers_mocks.go b/x/auction/testutil/expected_keepers_mocks.go new file mode 100755 index 00000000..8bb35f89 --- /dev/null +++ b/x/auction/testutil/expected_keepers_mocks.go @@ -0,0 +1,173 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: /Users/phil/Desktop/peggyJV/sommelier/x/auction/types/expected_keepers.go + +// Package mock_types is a generated GoMock package. +package mock_types + +import ( + reflect "reflect" + + types "github.com/cosmos/cosmos-sdk/types" + types0 "github.com/cosmos/cosmos-sdk/x/auth/types" + types1 "github.com/cosmos/cosmos-sdk/x/bank/types" + gomock "github.com/golang/mock/gomock" +) + +// MockAccountKeeper is a mock of AccountKeeper interface. +type MockAccountKeeper struct { + ctrl *gomock.Controller + recorder *MockAccountKeeperMockRecorder +} + +// MockAccountKeeperMockRecorder is the mock recorder for MockAccountKeeper. +type MockAccountKeeperMockRecorder struct { + mock *MockAccountKeeper +} + +// NewMockAccountKeeper creates a new mock instance. +func NewMockAccountKeeper(ctrl *gomock.Controller) *MockAccountKeeper { + mock := &MockAccountKeeper{ctrl: ctrl} + mock.recorder = &MockAccountKeeperMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAccountKeeper) EXPECT() *MockAccountKeeperMockRecorder { + return m.recorder +} + +// GetModuleAccount mocks base method. +func (m *MockAccountKeeper) GetModuleAccount(ctx types.Context, name string) types0.ModuleAccountI { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetModuleAccount", ctx, name) + ret0, _ := ret[0].(types0.ModuleAccountI) + return ret0 +} + +// GetModuleAccount indicates an expected call of GetModuleAccount. +func (mr *MockAccountKeeperMockRecorder) GetModuleAccount(ctx, name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetModuleAccount", reflect.TypeOf((*MockAccountKeeper)(nil).GetModuleAccount), ctx, name) +} + +// MockBankKeeper is a mock of BankKeeper interface. +type MockBankKeeper struct { + ctrl *gomock.Controller + recorder *MockBankKeeperMockRecorder +} + +// MockBankKeeperMockRecorder is the mock recorder for MockBankKeeper. +type MockBankKeeperMockRecorder struct { + mock *MockBankKeeper +} + +// NewMockBankKeeper creates a new mock instance. +func NewMockBankKeeper(ctrl *gomock.Controller) *MockBankKeeper { + mock := &MockBankKeeper{ctrl: ctrl} + mock.recorder = &MockBankKeeperMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBankKeeper) EXPECT() *MockBankKeeperMockRecorder { + return m.recorder +} + +// GetAllBalances mocks base method. +func (m *MockBankKeeper) GetAllBalances(ctx types.Context, addr types.AccAddress) types.Coins { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllBalances", ctx, addr) + ret0, _ := ret[0].(types.Coins) + return ret0 +} + +// GetAllBalances indicates an expected call of GetAllBalances. +func (mr *MockBankKeeperMockRecorder) GetAllBalances(ctx, addr interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllBalances", reflect.TypeOf((*MockBankKeeper)(nil).GetAllBalances), ctx, addr) +} + +// GetBalance mocks base method. +func (m *MockBankKeeper) GetBalance(ctx types.Context, addr types.AccAddress, denom string) types.Coin { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBalance", ctx, addr, denom) + ret0, _ := ret[0].(types.Coin) + return ret0 +} + +// GetBalance indicates an expected call of GetBalance. +func (mr *MockBankKeeperMockRecorder) GetBalance(ctx, addr, denom interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBalance", reflect.TypeOf((*MockBankKeeper)(nil).GetBalance), ctx, addr, denom) +} + +// GetDenomMetaData mocks base method. +func (m *MockBankKeeper) GetDenomMetaData(ctx types.Context, denom string) (types1.Metadata, bool) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDenomMetaData", ctx, denom) + ret0, _ := ret[0].(types1.Metadata) + ret1, _ := ret[1].(bool) + return ret0, ret1 +} + +// GetDenomMetaData indicates an expected call of GetDenomMetaData. +func (mr *MockBankKeeperMockRecorder) GetDenomMetaData(ctx, denom interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDenomMetaData", reflect.TypeOf((*MockBankKeeper)(nil).GetDenomMetaData), ctx, denom) +} + +// GetSupply mocks base method. +func (m *MockBankKeeper) GetSupply(ctx types.Context, denom string) types.Coin { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSupply", ctx, denom) + ret0, _ := ret[0].(types.Coin) + return ret0 +} + +// GetSupply indicates an expected call of GetSupply. +func (mr *MockBankKeeperMockRecorder) GetSupply(ctx, denom interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSupply", reflect.TypeOf((*MockBankKeeper)(nil).GetSupply), ctx, denom) +} + +// SendCoinsFromAccountToModule mocks base method. +func (m *MockBankKeeper) SendCoinsFromAccountToModule(ctx types.Context, senderAddr types.AccAddress, recipientModule string, amt types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendCoinsFromAccountToModule", ctx, senderAddr, recipientModule, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendCoinsFromAccountToModule indicates an expected call of SendCoinsFromAccountToModule. +func (mr *MockBankKeeperMockRecorder) SendCoinsFromAccountToModule(ctx, senderAddr, recipientModule, amt interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoinsFromAccountToModule", reflect.TypeOf((*MockBankKeeper)(nil).SendCoinsFromAccountToModule), ctx, senderAddr, recipientModule, amt) +} + +// SendCoinsFromModuleToAccount mocks base method. +func (m *MockBankKeeper) SendCoinsFromModuleToAccount(ctx types.Context, senderModule string, recipientAddr types.AccAddress, amt types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendCoinsFromModuleToAccount", ctx, senderModule, recipientAddr, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendCoinsFromModuleToAccount indicates an expected call of SendCoinsFromModuleToAccount. +func (mr *MockBankKeeperMockRecorder) SendCoinsFromModuleToAccount(ctx, senderModule, recipientAddr, amt interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoinsFromModuleToAccount", reflect.TypeOf((*MockBankKeeper)(nil).SendCoinsFromModuleToAccount), ctx, senderModule, recipientAddr, amt) +} + +// SendCoinsFromModuleToModule mocks base method. +func (m *MockBankKeeper) SendCoinsFromModuleToModule(ctx types.Context, senderModule, recipientModule string, amt types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendCoinsFromModuleToModule", ctx, senderModule, recipientModule, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendCoinsFromModuleToModule indicates an expected call of SendCoinsFromModuleToModule. +func (mr *MockBankKeeperMockRecorder) SendCoinsFromModuleToModule(ctx, senderModule, recipientModule, amt interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoinsFromModuleToModule", reflect.TypeOf((*MockBankKeeper)(nil).SendCoinsFromModuleToModule), ctx, senderModule, recipientModule, amt) +} diff --git a/x/auction/types/auction.go b/x/auction/types/auction.go index fa9ef523..dafc67dc 100644 --- a/x/auction/types/auction.go +++ b/x/auction/types/auction.go @@ -19,10 +19,6 @@ func (a *Auction) ValidateBasic() error { return sdkerrors.Wrapf(ErrAuctionStartingAmountMustBePositve, "Starting tokens for sale: %s", a.StartingTokensForSale.String()) } - if a.StartingTokensForSale.Denom == "" { - return sdkerrors.Wrapf(ErrAuctionDenomInvalid, "Starting denom tokens for sale: %s", a.StartingTokensForSale.String()) - } - if a.StartingTokensForSale.Denom == UsommDenom { return sdkerrors.Wrapf(ErrCannotAuctionUsomm, "Starting denom tokens for sale: %s", UsommDenom) } @@ -40,7 +36,7 @@ func (a *Auction) ValidateBasic() error { } if a.PriceDecreaseBlockInterval == 0 { - return sdkerrors.Wrapf(ErrInvalidBlockDecreaeInterval, "price decrease block interval: %d", a.PriceDecreaseBlockInterval) + return sdkerrors.Wrapf(ErrInvalidBlockDecreaseInterval, "price decrease block interval: %d", a.PriceDecreaseBlockInterval) } if !a.InitialUnitPriceInUsomm.IsPositive() { @@ -51,10 +47,6 @@ func (a *Auction) ValidateBasic() error { return sdkerrors.Wrapf(ErrPriceMustBePositive, "current unit price in usomm: %s", a.CurrentUnitPriceInUsomm.String()) } - if a.RemainingTokensForSale.Denom == "" { - return sdkerrors.Wrapf(ErrDenomCannotBeEmpty, "token for sale remaining: %s", a.RemainingTokensForSale.String()) - } - if a.FundingModuleAccount == "" { return sdkerrors.Wrapf(ErrUnauthorizedFundingModule, "funding module account: %s", a.FundingModuleAccount) } @@ -84,7 +76,7 @@ func (b *Bid) ValidateBasic() error { } if !b.MaxBidInUsomm.IsPositive() { - return sdkerrors.Wrapf(ErrBidIDAmountMustBePositive, "bid amount in usomm: %s", b.MaxBidInUsomm.String()) + return sdkerrors.Wrapf(ErrBidAmountMustBePositive, "bid amount in usomm: %s", b.MaxBidInUsomm.String()) } if b.MaxBidInUsomm.Denom != UsommDenom { @@ -92,7 +84,7 @@ func (b *Bid) ValidateBasic() error { } if !strings.HasPrefix(b.SaleTokenMinimumAmount.Denom, gravitytypes.GravityDenomPrefix) { - return sdkerrors.Wrapf(ErrInvalidTokenBeingBidOn, "sale token: %s", b.SaleTokenMinimumAmount) + return sdkerrors.Wrapf(ErrInvalidTokenBeingBidOn, "sale token: %s", b.SaleTokenMinimumAmount.String()) } if !b.SaleTokenMinimumAmount.IsPositive() { @@ -131,6 +123,10 @@ func (t *TokenPrice) ValidateBasic() error { return sdkerrors.Wrapf(ErrInvalidLastUpdatedBlock, "block: %d", t.LastUpdatedBlock) } + if !strings.HasPrefix(t.Denom, gravitytypes.GravityDenomPrefix) && t.Denom != UsommDenom { + return sdkerrors.Wrapf(ErrInvalidTokenPriceDenom, "denom: %s", t.Denom) + } + return nil } @@ -143,5 +139,9 @@ func (t *ProposedTokenPrice) ValidateBasic() error { return sdkerrors.Wrapf(ErrPriceMustBePositive, "usd price: %s", t.UsdPrice.String()) } + if !strings.HasPrefix(t.Denom, gravitytypes.GravityDenomPrefix) && t.Denom != UsommDenom { + return sdkerrors.Wrapf(ErrInvalidTokenPriceDenom, "denom: %s", t.Denom) + } + return nil } diff --git a/x/auction/types/auction_test.go b/x/auction/types/auction_test.go new file mode 100644 index 00000000..facdb416 --- /dev/null +++ b/x/auction/types/auction_test.go @@ -0,0 +1,635 @@ +package types + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/stretchr/testify/require" +) + +var ( + cosmosAddress1 string = "cosmos16zrkzad482haunrn25ywvwy6fclh3vh7r0hcny" + cosmosAddress2 string = "cosmos12svsksqaakc6r0gyxasf5el84946mp0svdl603" +) + +func TestAuctionValidate(t *testing.T) { + testCases := []struct { + name string + auction Auction + expPass bool + err error + }{ + { + name: "Happy path", + auction: Auction{ + Id: uint32(1), + StartingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(1000)), + StartBlock: uint64(200), + EndBlock: uint64(0), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + PriceDecreaseBlockInterval: uint64(10), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + RemainingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(900)), + FundingModuleAccount: "someModule", + ProceedsModuleAccount: "someModule", + }, + expPass: true, + err: nil, + }, + { + name: "Auction ID cannot be 0", + auction: Auction{ + Id: uint32(0), + StartingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(1000)), + StartBlock: uint64(200), + EndBlock: uint64(0), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + PriceDecreaseBlockInterval: uint64(10), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + RemainingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(900)), + FundingModuleAccount: "someModule", + ProceedsModuleAccount: "someModule", + }, + expPass: false, + err: sdkerrors.Wrapf(ErrAuctionIDMustBeNonZero, "id: 0"), + }, + { + name: "Starting tokens for sale must be positive", + auction: Auction{ + Id: uint32(1), + StartingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(0)), + StartBlock: uint64(200), + EndBlock: uint64(0), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + PriceDecreaseBlockInterval: uint64(10), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + RemainingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(900)), + FundingModuleAccount: "someModule", + ProceedsModuleAccount: "someModule", + }, + expPass: false, + err: sdkerrors.Wrapf(ErrAuctionStartingAmountMustBePositve, "Starting tokens for sale: %s", sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(0)).String()), + }, + { + name: "Starting tokens for sale cannot be usomm", + auction: Auction{ + Id: uint32(1), + StartingTokensForSale: sdk.NewCoin("usomm", sdk.NewIntFromUint64(1000)), + StartBlock: uint64(200), + EndBlock: uint64(0), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + PriceDecreaseBlockInterval: uint64(10), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + RemainingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(900)), + FundingModuleAccount: "someModule", + ProceedsModuleAccount: "someModule", + }, + expPass: false, + err: sdkerrors.Wrapf(ErrCannotAuctionUsomm, "Starting denom tokens for sale: %s", UsommDenom), + }, + { + name: "Start block must be positive", + auction: Auction{ + Id: uint32(1), + StartingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(1000)), + StartBlock: uint64(0), + EndBlock: uint64(0), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + PriceDecreaseBlockInterval: uint64(10), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + RemainingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(900)), + FundingModuleAccount: "someModule", + ProceedsModuleAccount: "someModule", + }, + expPass: false, + err: sdkerrors.Wrapf(ErrInvalidStartBlock, "start block: 0"), + }, + { + name: "Initial decrease rate cannot be <= 0", + auction: Auction{ + Id: uint32(1), + StartingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(1000)), + StartBlock: uint64(200), + EndBlock: uint64(0), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.0"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + PriceDecreaseBlockInterval: uint64(10), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + RemainingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(900)), + FundingModuleAccount: "someModule", + ProceedsModuleAccount: "someModule", + }, + expPass: false, + err: sdkerrors.Wrapf(ErrInvalidInitialDecreaseRate, "Inital price decrease rate %s", sdk.MustNewDecFromStr("0.0").String()), + }, + { + name: "Initial decrease rate cannot be >= 0", + auction: Auction{ + Id: uint32(1), + StartingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(1000)), + StartBlock: uint64(200), + EndBlock: uint64(0), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("1.0"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + PriceDecreaseBlockInterval: uint64(10), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + RemainingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(900)), + FundingModuleAccount: "someModule", + ProceedsModuleAccount: "someModule", + }, + expPass: false, + err: sdkerrors.Wrapf(ErrInvalidInitialDecreaseRate, "Inital price decrease rate %s", sdk.MustNewDecFromStr("1.0").String()), + }, + { + name: "Current decrease rate cannot be <= 0", + auction: Auction{ + Id: uint32(1), + StartingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(1000)), + StartBlock: uint64(200), + EndBlock: uint64(0), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.0"), + PriceDecreaseBlockInterval: uint64(10), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + RemainingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(900)), + FundingModuleAccount: "someModule", + ProceedsModuleAccount: "someModule", + }, + expPass: false, + err: sdkerrors.Wrapf(ErrInvalidCurrentDecreaseRate, "Current price decrease rate %s", sdk.MustNewDecFromStr("0.0").String()), + }, + { + name: "Current decrease rate cannot be >= 0", + auction: Auction{ + Id: uint32(1), + StartingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(1000)), + StartBlock: uint64(200), + EndBlock: uint64(0), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("1.0"), + PriceDecreaseBlockInterval: uint64(10), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + RemainingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(900)), + FundingModuleAccount: "someModule", + ProceedsModuleAccount: "someModule", + }, + expPass: false, + err: sdkerrors.Wrapf(ErrInvalidCurrentDecreaseRate, "Current price decrease rate %s", sdk.MustNewDecFromStr("1.0").String()), + }, + { + name: "Price decrease block interval cannot be 0", + auction: Auction{ + Id: uint32(1), + StartingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(1000)), + StartBlock: uint64(200), + EndBlock: uint64(0), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + PriceDecreaseBlockInterval: uint64(0), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + RemainingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(900)), + FundingModuleAccount: "someModule", + ProceedsModuleAccount: "someModule", + }, + expPass: false, + err: sdkerrors.Wrapf(ErrInvalidBlockDecreaseInterval, "price decrease block interval: 0"), + }, + { + name: "Initial unit price in usomm must be postive", + auction: Auction{ + Id: uint32(1), + StartingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(1000)), + StartBlock: uint64(200), + EndBlock: uint64(10), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + PriceDecreaseBlockInterval: uint64(10), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("0.0"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + RemainingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(900)), + FundingModuleAccount: "someModule", + ProceedsModuleAccount: "someModule", + }, + expPass: false, + err: sdkerrors.Wrapf(ErrPriceMustBePositive, "initial unit price in usomm: %s", sdk.MustNewDecFromStr("0.0").String()), + }, + { + name: "Current unit price in usomm must be postive", + auction: Auction{ + Id: uint32(1), + StartingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(1000)), + StartBlock: uint64(200), + EndBlock: uint64(10), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + PriceDecreaseBlockInterval: uint64(10), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("0.0"), + RemainingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(900)), + FundingModuleAccount: "someModule", + ProceedsModuleAccount: "someModule", + }, + expPass: false, + err: sdkerrors.Wrapf(ErrPriceMustBePositive, "current unit price in usomm: %s", sdk.MustNewDecFromStr("0.0").String()), + }, + + { + name: "Funding Module account cannot be empty", + auction: Auction{ + Id: uint32(1), + StartingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(1000)), + StartBlock: uint64(200), + EndBlock: uint64(10), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + PriceDecreaseBlockInterval: uint64(10), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + RemainingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(900)), + FundingModuleAccount: "", + ProceedsModuleAccount: "someModule", + }, + expPass: false, + err: sdkerrors.Wrapf(ErrUnauthorizedFundingModule, "funding module account: "), + }, + { + name: "Proceeds Module account cannot be empty", + auction: Auction{ + Id: uint32(1), + StartingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(1000)), + StartBlock: uint64(200), + EndBlock: uint64(10), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + PriceDecreaseBlockInterval: uint64(10), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + RemainingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(900)), + FundingModuleAccount: "someModule", + ProceedsModuleAccount: "", + }, + expPass: false, + err: sdkerrors.Wrapf(ErrUnauthorizedFundingModule, "proceeds module account: "), + }, + } + + for _, tc := range testCases { + err := tc.auction.ValidateBasic() + if tc.expPass { + require.NoError(t, err, tc.name) + require.Nil(t, err) + } else { + require.Error(t, err, tc.name) + require.Equal(t, tc.err.Error(), err.Error()) + } + } +} + +func TestBidValidate(t *testing.T) { + testCases := []struct { + name string + bid Bid + expPass bool + err error + }{ + { + name: "Happy path", + bid: Bid{ + Id: uint64(1), + AuctionId: uint32(1), + Bidder: cosmosAddress2, + MaxBidInUsomm: sdk.NewCoin("usomm", sdk.NewInt(100)), + SaleTokenMinimumAmount: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + TotalFulfilledSaleTokens: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + SaleTokenUnitPriceInUsomm: sdk.MustNewDecFromStr("2.0"), + TotalUsommPaid: sdk.NewCoin("usomm", sdk.NewInt(100)), + }, + expPass: true, + err: nil, + }, + { + name: "Bid ID cannot be 0", + bid: Bid{ + Id: uint64(0), + AuctionId: uint32(1), + Bidder: cosmosAddress2, + MaxBidInUsomm: sdk.NewCoin("usomm", sdk.NewInt(100)), + SaleTokenMinimumAmount: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + TotalFulfilledSaleTokens: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + SaleTokenUnitPriceInUsomm: sdk.MustNewDecFromStr("2.0"), + TotalUsommPaid: sdk.NewCoin("usomm", sdk.NewInt(100)), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrBidIDMustBeNonZero, "id: 0"), + }, + { + name: "Auction ID cannot be 0", + bid: Bid{ + Id: uint64(1), + AuctionId: uint32(0), + Bidder: cosmosAddress2, + MaxBidInUsomm: sdk.NewCoin("usomm", sdk.NewInt(100)), + SaleTokenMinimumAmount: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + TotalFulfilledSaleTokens: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + SaleTokenUnitPriceInUsomm: sdk.MustNewDecFromStr("2.0"), + TotalUsommPaid: sdk.NewCoin("usomm", sdk.NewInt(100)), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrAuctionIDMustBeNonZero, "id: 0"), + }, + { + name: "Bidder cannot be empty", + bid: Bid{ + Id: uint64(1), + AuctionId: uint32(1), + Bidder: "", + MaxBidInUsomm: sdk.NewCoin("usomm", sdk.NewInt(100)), + SaleTokenMinimumAmount: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + TotalFulfilledSaleTokens: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + SaleTokenUnitPriceInUsomm: sdk.MustNewDecFromStr("2.0"), + TotalUsommPaid: sdk.NewCoin("usomm", sdk.NewInt(100)), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrAddressExpected, "bidder: "), + }, + { + name: "Bidder must be a valid bech32 address", + bid: Bid{ + Id: uint64(1), + AuctionId: uint32(1), + Bidder: "ironman", + MaxBidInUsomm: sdk.NewCoin("usomm", sdk.NewInt(100)), + SaleTokenMinimumAmount: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + TotalFulfilledSaleTokens: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + SaleTokenUnitPriceInUsomm: sdk.MustNewDecFromStr("2.0"), + TotalUsommPaid: sdk.NewCoin("usomm", sdk.NewInt(100)), + }, + expPass: false, + err: sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "decoding bech32 failed: invalid bech32 string length 7"), + }, + { + name: "Bid must be positive", + bid: Bid{ + Id: uint64(1), + AuctionId: uint32(1), + Bidder: cosmosAddress2, + MaxBidInUsomm: sdk.NewCoin("usomm", sdk.NewInt(0)), + SaleTokenMinimumAmount: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + TotalFulfilledSaleTokens: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + SaleTokenUnitPriceInUsomm: sdk.MustNewDecFromStr("2.0"), + TotalUsommPaid: sdk.NewCoin("usomm", sdk.NewInt(100)), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrBidAmountMustBePositive, "bid amount in usomm: %s", sdk.NewCoin("usomm", sdk.NewInt(0)).String()), + }, + { + name: "Bid must be in usomm", + bid: Bid{ + Id: uint64(1), + AuctionId: uint32(1), + Bidder: cosmosAddress2, + MaxBidInUsomm: sdk.NewCoin("usdc", sdk.NewInt(100)), + SaleTokenMinimumAmount: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + TotalFulfilledSaleTokens: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + SaleTokenUnitPriceInUsomm: sdk.MustNewDecFromStr("2.0"), + TotalUsommPaid: sdk.NewCoin("usomm", sdk.NewInt(100)), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrBidMustBeInUsomm, "bid: %s", sdk.NewCoin("usdc", sdk.NewInt(100)).String()), + }, + { + name: "Sale token must be gravity prefixed", + bid: Bid{ + Id: uint64(1), + AuctionId: uint32(1), + Bidder: cosmosAddress2, + MaxBidInUsomm: sdk.NewCoin("usomm", sdk.NewInt(100)), + SaleTokenMinimumAmount: sdk.NewCoin("usdc", sdk.NewInt(50)), + TotalFulfilledSaleTokens: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + SaleTokenUnitPriceInUsomm: sdk.MustNewDecFromStr("2.0"), + TotalUsommPaid: sdk.NewCoin("usomm", sdk.NewInt(100)), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrInvalidTokenBeingBidOn, "sale token: %s", sdk.NewCoin("usdc", sdk.NewInt(50)).String()), + }, + { + name: "Sale token amount must be positive", + bid: Bid{ + Id: uint64(1), + AuctionId: uint32(1), + Bidder: cosmosAddress2, + MaxBidInUsomm: sdk.NewCoin("usomm", sdk.NewInt(100)), + SaleTokenMinimumAmount: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(0)), + TotalFulfilledSaleTokens: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + SaleTokenUnitPriceInUsomm: sdk.MustNewDecFromStr("2.0"), + TotalUsommPaid: sdk.NewCoin("usomm", sdk.NewInt(100)), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrMinimumAmountMustBePositive, "sale token amount: %s", sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(0)).String()), + }, + { + name: "Sale token unit price must be in usomm", + bid: Bid{ + Id: uint64(1), + AuctionId: uint32(1), + Bidder: cosmosAddress2, + MaxBidInUsomm: sdk.NewCoin("usomm", sdk.NewInt(100)), + SaleTokenMinimumAmount: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + TotalFulfilledSaleTokens: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + SaleTokenUnitPriceInUsomm: sdk.MustNewDecFromStr("0.0"), + TotalUsommPaid: sdk.NewCoin("usomm", sdk.NewInt(100)), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrBidUnitPriceInUsommMustBePositive, "sale token unit price: %s", sdk.MustNewDecFromStr("0.0").String()), + }, + { + name: "Total usomm paid denom must be usomm", + bid: Bid{ + Id: uint64(1), + AuctionId: uint32(1), + Bidder: cosmosAddress2, + MaxBidInUsomm: sdk.NewCoin("usomm", sdk.NewInt(100)), + SaleTokenMinimumAmount: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + TotalFulfilledSaleTokens: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + SaleTokenUnitPriceInUsomm: sdk.MustNewDecFromStr("2.0"), + TotalUsommPaid: sdk.NewCoin("usdc", sdk.NewInt(100)), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrBidMustBeInUsomm, "payment denom: usdc"), + }, + } + + for _, tc := range testCases { + err := tc.bid.ValidateBasic() + if tc.expPass { + require.NoError(t, err, tc.name) + require.Nil(t, err) + } else { + require.Error(t, err, tc.name) + require.Equal(t, tc.err.Error(), err.Error()) + } + } +} + +func TestTokenPriceValidate(t *testing.T) { + testCases := []struct { + name string + tokenPrice TokenPrice + expPass bool + err error + }{ + { + name: "Happy path -- usomm", + tokenPrice: TokenPrice{ + Denom: "usomm", + UsdPrice: sdk.MustNewDecFromStr("0.0008"), + LastUpdatedBlock: uint64(321), + }, + expPass: true, + err: nil, + }, + { + name: "Happy path -- gravity denom", + tokenPrice: TokenPrice{ + Denom: "gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + UsdPrice: sdk.MustNewDecFromStr("0.001"), + LastUpdatedBlock: uint64(321), + }, + expPass: true, + err: nil, + }, + { + name: "Denom cannot be empty", + tokenPrice: TokenPrice{ + Denom: "", + UsdPrice: sdk.MustNewDecFromStr("0.0008"), + LastUpdatedBlock: uint64(321), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrDenomCannotBeEmpty, "price denom: "), + }, + { + name: "Price must be positive", + tokenPrice: TokenPrice{ + Denom: "usomm", + UsdPrice: sdk.MustNewDecFromStr("0.0"), + LastUpdatedBlock: uint64(321), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrPriceMustBePositive, "usd price: %s", sdk.MustNewDecFromStr("0.0").String()), + }, + { + name: "Last updated block cannot be 0", + tokenPrice: TokenPrice{ + Denom: "usomm", + UsdPrice: sdk.MustNewDecFromStr("0.0008"), + LastUpdatedBlock: uint64(0), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrInvalidLastUpdatedBlock, "block: 0"), + }, + { + name: "Token price must be usomm or gravity prefixed", + tokenPrice: TokenPrice{ + Denom: "usdc", + UsdPrice: sdk.MustNewDecFromStr("1.0"), + LastUpdatedBlock: uint64(321), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrInvalidTokenPriceDenom, "denom: usdc"), + }, + } + + for _, tc := range testCases { + err := tc.tokenPrice.ValidateBasic() + if tc.expPass { + require.NoError(t, err, tc.name) + require.Nil(t, err) + } else { + require.Error(t, err, tc.name) + require.Equal(t, tc.err.Error(), err.Error()) + } + } +} + +func TestProposedTokenPriceValidate(t *testing.T) { + testCases := []struct { + name string + proposedTokenPrice ProposedTokenPrice + expPass bool + err error + }{ + { + name: "Happy path -- usomm", + proposedTokenPrice: ProposedTokenPrice{ + Denom: "usomm", + UsdPrice: sdk.MustNewDecFromStr("0.0008"), + }, + expPass: true, + err: nil, + }, + { + name: "Happy path -- gravity denom", + proposedTokenPrice: ProposedTokenPrice{ + Denom: "gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + UsdPrice: sdk.MustNewDecFromStr("0.001"), + }, + expPass: true, + err: nil, + }, + { + name: "Denom cannot be empty", + proposedTokenPrice: ProposedTokenPrice{ + Denom: "", + UsdPrice: sdk.MustNewDecFromStr("0.0008"), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrDenomCannotBeEmpty, "price denom: "), + }, + { + name: "Price must be positive", + proposedTokenPrice: ProposedTokenPrice{ + Denom: "usomm", + UsdPrice: sdk.MustNewDecFromStr("0.0"), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrPriceMustBePositive, "usd price: %s", sdk.MustNewDecFromStr("0.0").String()), + }, + { + name: "Token price must be usomm or gravity prefixed", + proposedTokenPrice: ProposedTokenPrice{ + Denom: "usdc", + UsdPrice: sdk.MustNewDecFromStr("1.0"), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrInvalidTokenPriceDenom, "denom: usdc"), + }, + } + + for _, tc := range testCases { + err := tc.proposedTokenPrice.ValidateBasic() + if tc.expPass { + require.NoError(t, err, tc.name) + require.Nil(t, err) + } else { + require.Error(t, err, tc.name) + require.Equal(t, tc.err.Error(), err.Error()) + } + } +} diff --git a/x/auction/types/errors.go b/x/auction/types/errors.go index 7d256ccf..abfe05fd 100644 --- a/x/auction/types/errors.go +++ b/x/auction/types/errors.go @@ -6,39 +6,41 @@ import ( // x/auction module sentinel errors var ( - ErrSignerDifferentFromBidder = sdkerrors.Register(ModuleName, 2, "signer is different from bidder") - ErrCouldNotFindSaleTokenPrice = sdkerrors.Register(ModuleName, 3, "could not find sale token price, need to resubmit token prices and try agian") - ErrCouldNotFindSommTokenPrice = sdkerrors.Register(ModuleName, 4, "could not find usomm token price, need to resubmit token prices and try again") - ErrLastSaleTokenPriceTooOld = sdkerrors.Register(ModuleName, 5, "last sale token price update too long ago, need to resubmit token prices and try again") - ErrLastSommTokenPriceTooOld = sdkerrors.Register(ModuleName, 6, "last usomm token price update too long ago, need to resubmit token prices and try again") - ErrAuctionStartingAmountMustBePositve = sdkerrors.Register(ModuleName, 7, "minimum auction sale token starting amount must be a positive amount of coins") - ErrAuctionDenomInvalid = sdkerrors.Register(ModuleName, 8, "auction sale token starting amount denom must be non-empty") - ErrCannotAuctionUsomm = sdkerrors.Register(ModuleName, 9, "auctioning usomm for usomm is pointless") - ErrInvalidInitialDecreaseRate = sdkerrors.Register(ModuleName, 10, "initial price decrease rate must be a float less than one and greater than zero") - ErrInvalidBlockDecreaeInterval = sdkerrors.Register(ModuleName, 11, "price decrease block interval cannot be 0") - ErrUnauthorizedFundingModule = sdkerrors.Register(ModuleName, 12, "unauthorized funding module account") - ErrUnauthorizedProceedsModule = sdkerrors.Register(ModuleName, 13, "unauthorized proceeds module account") - ErrCannotStartTwoAuctionsForSameDenomSimultaneously = sdkerrors.Register(ModuleName, 14, "auction for this denom is currently ongoing, cannot create another auction for the same denom until completed") - ErrConvertingStringToDec = sdkerrors.Register(ModuleName, 15, "could not convert string to dec") - ErrAuctionNotFound = sdkerrors.Register(ModuleName, 16, "auction not found") - ErrInvalidAddress = sdkerrors.Register(ModuleName, 17, "invalid address") - ErrBidAuctionDenomMismatch = sdkerrors.Register(ModuleName, 18, "auction denom different from bid requested denom") - ErrAuctionEnded = sdkerrors.Register(ModuleName, 19, "auction ended") - ErrInsufficientBid = sdkerrors.Register(ModuleName, 20, "max bid amount is too small to purchase minimum sale tokens requested") - ErrMinimumPurchaseAmountLargerThanTokensRemaining = sdkerrors.Register(ModuleName, 21, "minimum purchase amount is larger then the number of tokens remaining for sale") - ErrAuctionIDMustBeNonZero = sdkerrors.Register(ModuleName, 22, "auction IDs must be non-zero") - ErrInvalidStartBlock = sdkerrors.Register(ModuleName, 23, "start block cannot be 0") - ErrInvalidCurrentDecreaseRate = sdkerrors.Register(ModuleName, 24, "current price decrease rate must be a float less than one and greater than zero") - ErrPriceMustBePositive = sdkerrors.Register(ModuleName, 25, "price must be positive") - ErrDenomCannotBeEmpty = sdkerrors.Register(ModuleName, 26, "denom cannot be empty") - ErrInvalidLastUpdatedBlock = sdkerrors.Register(ModuleName, 27, "last updated block cannot be 0") - ErrBidIDMustBeNonZero = sdkerrors.Register(ModuleName, 28, "bid ID must be non-zero") - ErrBidIDAmountMustBePositive = sdkerrors.Register(ModuleName, 29, "bid amount must be positive") - ErrBidMustBeInUsomm = sdkerrors.Register(ModuleName, 30, "bid must be in usomm") - ErrInvalidTokenBeingBidOn = sdkerrors.Register(ModuleName, 31, "tokens being bid on must have the gravity prefix") - ErrMinimumAmountMustBePositive = sdkerrors.Register(ModuleName, 32, "minimum amount to purchase with bid must be positive") - ErrAddressExpected = sdkerrors.Register(ModuleName, 33, "address cannot be empty") - ErrBidFulfilledSaleTokenAmountMustBeNonNegative = sdkerrors.Register(ModuleName, 34, "total sale token fulfilled amount must be non-negative") - ErrBidUnitPriceInUsommMustBePositive = sdkerrors.Register(ModuleName, 35, "unit price of sale tokens in usomm must be positive") - ErrBidPaymentCannotBeNegative = sdkerrors.Register(ModuleName, 36, "total amount paid in usomm cannot be negative") + ErrCouldNotFindSaleTokenPrice = sdkerrors.Register(ModuleName, 2, "could not find sale token price, need to resubmit token prices and try agian") + ErrCouldNotFindSommTokenPrice = sdkerrors.Register(ModuleName, 3, "could not find usomm token price, need to resubmit token prices and try again") + ErrLastSaleTokenPriceTooOld = sdkerrors.Register(ModuleName, 4, "last sale token price update too long ago, need to resubmit token prices and try again") + ErrLastSommTokenPriceTooOld = sdkerrors.Register(ModuleName, 5, "last usomm token price update too long ago, need to resubmit token prices and try again") + ErrAuctionStartingAmountMustBePositve = sdkerrors.Register(ModuleName, 6, "minimum auction sale token starting amount must be a positive amount of coins") + ErrCannotAuctionUsomm = sdkerrors.Register(ModuleName, 7, "auctioning usomm for usomm is pointless") + ErrInvalidInitialDecreaseRate = sdkerrors.Register(ModuleName, 8, "initial price decrease rate must be a float less than one and greater than zero") + ErrInvalidBlockDecreaseInterval = sdkerrors.Register(ModuleName, 9, "price decrease block interval cannot be 0") + ErrUnauthorizedFundingModule = sdkerrors.Register(ModuleName, 10, "unauthorized funding module account") + ErrUnauthorizedProceedsModule = sdkerrors.Register(ModuleName, 11, "unauthorized proceeds module account") + ErrCannotStartTwoAuctionsForSameDenomSimultaneously = sdkerrors.Register(ModuleName, 12, "auction for this denom is currently ongoing, cannot create another auction for the same denom until completed") + ErrConvertingStringToDec = sdkerrors.Register(ModuleName, 13, "could not convert string to dec") + ErrAuctionNotFound = sdkerrors.Register(ModuleName, 14, "auction not found") + ErrBidAuctionDenomMismatch = sdkerrors.Register(ModuleName, 15, "auction denom different from bid requested denom") + ErrAuctionEnded = sdkerrors.Register(ModuleName, 16, "auction ended") + ErrInsufficientBid = sdkerrors.Register(ModuleName, 17, "max bid amount is too small to purchase minimum sale tokens requested") + ErrMinimumPurchaseAmountLargerThanTokensRemaining = sdkerrors.Register(ModuleName, 18, "minimum purchase amount is larger then the number of tokens remaining for sale") + ErrAuctionIDMustBeNonZero = sdkerrors.Register(ModuleName, 19, "auction IDs must be non-zero") + ErrInvalidStartBlock = sdkerrors.Register(ModuleName, 20, "start block cannot be 0") + ErrInvalidCurrentDecreaseRate = sdkerrors.Register(ModuleName, 21, "current price decrease rate must be a float less than one and greater than zero") + ErrPriceMustBePositive = sdkerrors.Register(ModuleName, 22, "price must be positive") + ErrDenomCannotBeEmpty = sdkerrors.Register(ModuleName, 23, "denom cannot be empty") + ErrInvalidLastUpdatedBlock = sdkerrors.Register(ModuleName, 24, "last updated block cannot be 0") + ErrBidIDMustBeNonZero = sdkerrors.Register(ModuleName, 25, "bid ID must be non-zero") + ErrBidAmountMustBePositive = sdkerrors.Register(ModuleName, 26, "bid amount must be positive") + ErrBidMustBeInUsomm = sdkerrors.Register(ModuleName, 27, "bid must be in usomm") + ErrInvalidTokenBeingBidOn = sdkerrors.Register(ModuleName, 28, "tokens being bid on must have the gravity prefix") + ErrMinimumAmountMustBePositive = sdkerrors.Register(ModuleName, 29, "minimum amount to purchase with bid must be positive") + ErrAddressExpected = sdkerrors.Register(ModuleName, 30, "address cannot be empty") + ErrBidUnitPriceInUsommMustBePositive = sdkerrors.Register(ModuleName, 31, "unit price of sale tokens in usomm must be positive") + ErrInvalidTokenPriceDenom = sdkerrors.Register(ModuleName, 32, "token price denoms must be either usomm or addresses prefixed with 'gravity'") + ErrTokenPriceProposalAttemptsToUpdateTokenPriceMoreThanOnce = sdkerrors.Register(ModuleName, 33, "token price proposals should not attempt to update the same denom's price more than once per proposal") + ErrTokenPriceMaxBlockAgeMustBePositive = sdkerrors.Register(ModuleName, 34, "price max block age must be positive") + ErrInvalidPriceMaxBlockAgeParameterType = sdkerrors.Register(ModuleName, 35, "price max block age type must be uint64") + ErrTokenPriceProposalMustHaveAtLeastOnePrice = sdkerrors.Register(ModuleName, 36, "list of proposed token prices must be non-zero") + ErrBidFulfilledSaleTokenAmountMustBeNonNegative = sdkerrors.Register(ModuleName, 37, "total sale token fulfilled amount must be non-negative") + ErrBidPaymentCannotBeNegative = sdkerrors.Register(ModuleName, 38, "total amount paid in usomm cannot be negative") ) diff --git a/x/auction/types/events.go b/x/auction/types/events.go index a7de4eea..8d7fe1ee 100644 --- a/x/auction/types/events.go +++ b/x/auction/types/events.go @@ -8,7 +8,6 @@ const ( AttributeKeyAuctionID = "auction_id" AttributeKeyBidder = "max_bid" AttributeKeyMinimumAmount = "minimum_amount" - AttributeKeySigner = "signer" AttributeKeyFulfilledPrice = "fulfilled_price_of_sale_token_in_usomm" AttributeKeyTotalPayment = "total_payment_in_usomm" AttributeKeyFulfilledAmount = "total_sale_token_fulfilled_amount" diff --git a/x/auction/types/expected_keepers.go b/x/auction/types/expected_keepers.go index e06f3dec..b289ba1a 100644 --- a/x/auction/types/expected_keepers.go +++ b/x/auction/types/expected_keepers.go @@ -2,9 +2,16 @@ package types import ( sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" ) +// Updates here should also be reflected in the testutil's expected keeper mocks, and can be generated via: +// mockgen -source={ABS_REPO_PATH}/peggyJV/sommelier/x/auction/types/expected_keepers.go -destination={ABS_REPO_PATH}/peggyJV/sommelier/x/auction/testutil/expected_keepers_mocks.go +type AccountKeeper interface { + GetModuleAccount(ctx sdk.Context, name string) authtypes.ModuleAccountI +} + type BankKeeper interface { GetSupply(ctx sdk.Context, denom string) sdk.Coin SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error diff --git a/x/auction/types/genesis_test.go b/x/auction/types/genesis_test.go new file mode 100644 index 00000000..03745c02 --- /dev/null +++ b/x/auction/types/genesis_test.go @@ -0,0 +1,257 @@ +package types + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/stretchr/testify/require" +) + +func TestGenesisValidate(t *testing.T) { + testCases := []struct { + name string + genesisState GenesisState + expPass bool + err error + }{ + { + name: "Happy path -- default genesis", + genesisState: DefaultGenesisState(), + expPass: true, + err: nil, + }, + { + name: "Happy path -- empty genesis", + genesisState: GenesisState{ + Params: Params{ + PriceMaxBlockAge: uint64(20), + }, + Auctions: []*Auction{}, + Bids: []*Bid{}, + TokenPrices: []*TokenPrice{}, + }, + expPass: true, + err: nil, + }, + { + name: "Happy path -- custom populated genesis", + genesisState: GenesisState{ + Params: Params{ + PriceMaxBlockAge: uint64(20), + }, + Auctions: []*Auction{ + { + Id: uint32(1), + StartingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(1000)), + StartBlock: uint64(200), + EndBlock: uint64(0), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + PriceDecreaseBlockInterval: uint64(10), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + RemainingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(900)), + FundingModuleAccount: "someModule", + ProceedsModuleAccount: "someModule", + }, + }, + Bids: []*Bid{ + { + Id: uint64(1), + AuctionId: uint32(1), + Bidder: "cosmos12svsksqaakc6r0gyxasf5el84946mp0svdl603", + MaxBidInUsomm: sdk.NewCoin("usomm", sdk.NewInt(100)), + SaleTokenMinimumAmount: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + TotalFulfilledSaleTokens: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + SaleTokenUnitPriceInUsomm: sdk.MustNewDecFromStr("2.0"), + TotalUsommPaid: sdk.NewCoin("usomm", sdk.NewInt(100)), + }, + }, + TokenPrices: []*TokenPrice{ + { + Denom: "usomm", + UsdPrice: sdk.MustNewDecFromStr("0.0008"), + LastUpdatedBlock: uint64(123), + }, + { + Denom: "gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + UsdPrice: sdk.MustNewDecFromStr("0.032"), + LastUpdatedBlock: uint64(321), + }, + }, + LastAuctionId: uint32(1), + LastBidId: uint64(1), + }, + expPass: true, + err: nil, + }, + { + name: "Validate basic canary -- Invalid auction", + genesisState: GenesisState{ + Params: Params{ + PriceMaxBlockAge: uint64(20), + }, + Auctions: []*Auction{ + { + Id: uint32(0), + StartingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(1000)), + StartBlock: uint64(200), + EndBlock: uint64(0), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + PriceDecreaseBlockInterval: uint64(10), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + RemainingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(900)), + FundingModuleAccount: "someModule", + ProceedsModuleAccount: "someModule", + }, + }, + Bids: []*Bid{ + { + Id: uint64(1), + AuctionId: uint32(1), + Bidder: "cosmos12svsksqaakc6r0gyxasf5el84946mp0svdl603", + MaxBidInUsomm: sdk.NewCoin("usomm", sdk.NewInt(100)), + SaleTokenMinimumAmount: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + TotalFulfilledSaleTokens: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + SaleTokenUnitPriceInUsomm: sdk.MustNewDecFromStr("2.0"), + TotalUsommPaid: sdk.NewCoin("usomm", sdk.NewInt(100)), + }, + }, + TokenPrices: []*TokenPrice{ + { + Denom: "usomm", + UsdPrice: sdk.MustNewDecFromStr("0.0008"), + LastUpdatedBlock: uint64(123), + }, + { + Denom: "gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + UsdPrice: sdk.MustNewDecFromStr("0.032"), + LastUpdatedBlock: uint64(321), + }, + }, + LastAuctionId: uint32(1), + LastBidId: uint64(1), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrAuctionIDMustBeNonZero, "id: 0"), + }, + { + name: "Validate basic canary -- Invalid bid", + genesisState: GenesisState{ + Params: Params{ + PriceMaxBlockAge: uint64(20), + }, + Auctions: []*Auction{ + { + Id: uint32(1), + StartingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(1000)), + StartBlock: uint64(200), + EndBlock: uint64(0), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + PriceDecreaseBlockInterval: uint64(10), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + RemainingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(900)), + FundingModuleAccount: "someModule", + ProceedsModuleAccount: "someModule", + }, + }, + Bids: []*Bid{ + { + Id: uint64(0), + AuctionId: uint32(1), + Bidder: "cosmos12svsksqaakc6r0gyxasf5el84946mp0svdl603", + MaxBidInUsomm: sdk.NewCoin("usomm", sdk.NewInt(100)), + SaleTokenMinimumAmount: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + TotalFulfilledSaleTokens: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + SaleTokenUnitPriceInUsomm: sdk.MustNewDecFromStr("2.0"), + TotalUsommPaid: sdk.NewCoin("usomm", sdk.NewInt(100)), + }, + }, + TokenPrices: []*TokenPrice{ + { + Denom: "usomm", + UsdPrice: sdk.MustNewDecFromStr("0.0008"), + LastUpdatedBlock: uint64(123), + }, + { + Denom: "gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + UsdPrice: sdk.MustNewDecFromStr("0.032"), + LastUpdatedBlock: uint64(321), + }, + }, + LastAuctionId: uint32(1), + LastBidId: uint64(1), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrBidIDMustBeNonZero, "id: 0"), + }, + { + name: "Validate basic canary -- Invalid token price", + genesisState: GenesisState{ + Params: Params{ + PriceMaxBlockAge: uint64(20), + }, + Auctions: []*Auction{ + { + Id: uint32(1), + StartingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(1000)), + StartBlock: uint64(200), + EndBlock: uint64(0), + InitialPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + CurrentPriceDecreaseRate: sdk.MustNewDecFromStr("0.05"), + PriceDecreaseBlockInterval: uint64(10), + InitialUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + CurrentUnitPriceInUsomm: sdk.MustNewDecFromStr("20.0"), + RemainingTokensForSale: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewIntFromUint64(900)), + FundingModuleAccount: "someModule", + ProceedsModuleAccount: "someModule", + }, + }, + Bids: []*Bid{ + { + Id: uint64(1), + AuctionId: uint32(1), + Bidder: "cosmos12svsksqaakc6r0gyxasf5el84946mp0svdl603", + MaxBidInUsomm: sdk.NewCoin("usomm", sdk.NewInt(100)), + SaleTokenMinimumAmount: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + TotalFulfilledSaleTokens: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(50)), + SaleTokenUnitPriceInUsomm: sdk.MustNewDecFromStr("2.0"), + TotalUsommPaid: sdk.NewCoin("usomm", sdk.NewInt(100)), + }, + }, + TokenPrices: []*TokenPrice{ + { + Denom: "usdc", + UsdPrice: sdk.MustNewDecFromStr("0.0008"), + LastUpdatedBlock: uint64(123), + }, + { + Denom: "gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + UsdPrice: sdk.MustNewDecFromStr("0.032"), + LastUpdatedBlock: uint64(321), + }, + }, + LastAuctionId: uint32(1), + LastBidId: uint64(1), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrInvalidTokenPriceDenom, "denom: usdc"), + }, + } + + for _, tc := range testCases { + err := tc.genesisState.Validate() + if tc.expPass { + require.NoError(t, err, tc.name) + require.Nil(t, err) + } else { + require.Error(t, err, tc.name) + require.Equal(t, tc.err.Error(), err.Error()) + } + } +} diff --git a/x/auction/types/msgs.go b/x/auction/types/msgs.go index c086bf32..c8b305c9 100644 --- a/x/auction/types/msgs.go +++ b/x/auction/types/msgs.go @@ -1,7 +1,6 @@ package types import ( - "fmt" "strings" sdk "github.com/cosmos/cosmos-sdk/types" @@ -26,7 +25,6 @@ func NewMsgSubmitBidRequest(auctionID uint32, maxBidInUsomm sdk.Coin, saleTokenM AuctionId: auctionID, MaxBidInUsomm: maxBidInUsomm, SaleTokenMinimumAmount: saleTokenMinimumAmount, - Bidder: signer.String(), Signer: signer.String(), }, nil } @@ -40,33 +38,29 @@ func (m *MsgSubmitBidRequest) Type() string { return TypeMsgSubmitBidRequest } // ValidateBasic implements sdk.Msg func (m *MsgSubmitBidRequest) ValidateBasic() error { if m.AuctionId == 0 { - return fmt.Errorf("auction IDs must be non-zero") + return sdkerrors.Wrapf(ErrAuctionIDMustBeNonZero, "id: %d", m.AuctionId) } - if m.MaxBidInUsomm.Denom != "usomm" { - return fmt.Errorf("max bids must be in usomm") + if m.MaxBidInUsomm.Denom != UsommDenom { + return sdkerrors.Wrapf(ErrBidMustBeInUsomm, "bid: %s", m.MaxBidInUsomm.String()) } if !m.MaxBidInUsomm.IsPositive() { - return fmt.Errorf("bids must be a positive amount of usomm") + return sdkerrors.Wrapf(ErrBidAmountMustBePositive, "bid amount in usomm: %s", m.MaxBidInUsomm.String()) } if !strings.HasPrefix(m.SaleTokenMinimumAmount.Denom, "gravity0x") { - return fmt.Errorf("bids may only be placed for gravity tokens") + return sdkerrors.Wrapf(ErrInvalidTokenBeingBidOn, "sale token: %s", m.SaleTokenMinimumAmount.String()) } if !m.SaleTokenMinimumAmount.IsPositive() { - return fmt.Errorf("minimum amount must be a positive amount of auctioned coins") + return sdkerrors.Wrapf(ErrMinimumAmountMustBePositive, "sale token amount: %s", m.SaleTokenMinimumAmount.String()) } - if _, err := sdk.AccAddressFromBech32(m.Bidder); err != nil { + if _, err := sdk.AccAddressFromBech32(m.Signer); err != nil { return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, err.Error()) } - if m.Signer != m.Bidder { - return sdkerrors.Wrapf(ErrSignerDifferentFromBidder, "signer: %s, bidder: %s", m.Signer, m.Bidder) - } - return nil } @@ -80,7 +74,7 @@ func (m *MsgSubmitBidRequest) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{m.MustGetSigner()} } -// MustGetSigner returns the signer address +// MustGetSigner returns the signer address (which is also the bidder) func (m *MsgSubmitBidRequest) MustGetSigner() sdk.AccAddress { addr, err := sdk.AccAddressFromBech32(m.Signer) if err != nil { diff --git a/x/auction/types/msgs_test.go b/x/auction/types/msgs_test.go new file mode 100644 index 00000000..fc1ed4f3 --- /dev/null +++ b/x/auction/types/msgs_test.go @@ -0,0 +1,120 @@ +package types + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/stretchr/testify/require" +) + +func TestNewMsgSubmitBidRequestFormatting(t *testing.T) { + expectedMsg := &MsgSubmitBidRequest{ + AuctionId: uint32(1), + MaxBidInUsomm: sdk.NewCoin("usomm", sdk.NewInt(200)), + SaleTokenMinimumAmount: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(1)), + Signer: sdk.AccAddress(cosmosAddress1).String(), + } + + createdMsg, err := NewMsgSubmitBidRequest(uint32(1), sdk.NewCoin("usomm", sdk.NewInt(200)), sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(1)), sdk.AccAddress(cosmosAddress1)) + require.Nil(t, err) + require.Equal(t, expectedMsg, createdMsg) +} + +func TestMsgValidate(t *testing.T) { + testCases := []struct { + name string + msgSubmitBidRequest MsgSubmitBidRequest + expPass bool + err error + }{ + { + name: "Happy path", + msgSubmitBidRequest: MsgSubmitBidRequest{ + AuctionId: uint32(1), + MaxBidInUsomm: sdk.NewCoin("usomm", sdk.NewInt(200)), + SaleTokenMinimumAmount: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(1)), + Signer: sdk.AccAddress(cosmosAddress1).String(), + }, + expPass: true, + err: nil, + }, + { + name: "Auction ID cannot be 0", + msgSubmitBidRequest: MsgSubmitBidRequest{ + AuctionId: uint32(0), + MaxBidInUsomm: sdk.NewCoin("usomm", sdk.NewInt(200)), + SaleTokenMinimumAmount: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(1)), + Signer: sdk.AccAddress(cosmosAddress1).String(), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrAuctionIDMustBeNonZero, "id: 0"), + }, + { + name: "Bid must be in usomm", + msgSubmitBidRequest: MsgSubmitBidRequest{ + AuctionId: uint32(1), + MaxBidInUsomm: sdk.NewCoin("usdc", sdk.NewInt(200)), + SaleTokenMinimumAmount: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(1)), + Signer: sdk.AccAddress(cosmosAddress1).String(), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrBidMustBeInUsomm, "bid: %s", sdk.NewCoin("usdc", sdk.NewInt(200))), + }, + { + name: "Bid must be positive", + msgSubmitBidRequest: MsgSubmitBidRequest{ + AuctionId: uint32(1), + MaxBidInUsomm: sdk.NewCoin("usomm", sdk.NewInt(0)), + SaleTokenMinimumAmount: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(1)), + Signer: sdk.AccAddress(cosmosAddress1).String(), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrBidAmountMustBePositive, "bid amount in usomm: %s", sdk.NewCoin("usomm", sdk.NewInt(0))), + }, + { + name: "Sale token must be prefixed with gravity0x", + msgSubmitBidRequest: MsgSubmitBidRequest{ + AuctionId: uint32(1), + MaxBidInUsomm: sdk.NewCoin("usomm", sdk.NewInt(200)), + SaleTokenMinimumAmount: sdk.NewCoin("usdc", sdk.NewInt(1)), + Signer: sdk.AccAddress(cosmosAddress1).String(), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrInvalidTokenBeingBidOn, "sale token: %s", sdk.NewCoin("usdc", sdk.NewInt(1))), + }, + { + name: "Sale token minimum amount must be positive", + msgSubmitBidRequest: MsgSubmitBidRequest{ + AuctionId: uint32(1), + MaxBidInUsomm: sdk.NewCoin("usomm", sdk.NewInt(200)), + SaleTokenMinimumAmount: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(0)), + Signer: sdk.AccAddress(cosmosAddress1).String(), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrMinimumAmountMustBePositive, "sale token amount: %s", sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(0))), + }, + { + name: "Signer address must be in bech32 format", + msgSubmitBidRequest: MsgSubmitBidRequest{ + AuctionId: uint32(1), + MaxBidInUsomm: sdk.NewCoin("usomm", sdk.NewInt(200)), + SaleTokenMinimumAmount: sdk.NewCoin("gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", sdk.NewInt(1)), + Signer: "zoidberg", + }, + expPass: false, + err: sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "decoding bech32 failed: invalid separator index -1"), + }, + } + + for _, tc := range testCases { + err := tc.msgSubmitBidRequest.ValidateBasic() + if tc.expPass { + require.NoError(t, err, tc.name) + require.Nil(t, err) + } else { + require.Error(t, err, tc.name) + require.Equal(t, tc.err.Error(), err.Error()) + } + } +} diff --git a/x/auction/types/params.go b/x/auction/types/params.go index 82b7e211..e3d90bb0 100644 --- a/x/auction/types/params.go +++ b/x/auction/types/params.go @@ -1,8 +1,7 @@ package types import ( - "fmt" - + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" ) @@ -43,13 +42,11 @@ func (p *Params) ValidateBasic() error { func validatePriceMaxBlockAge(i interface{}) error { priceMaxBlockAge, ok := i.(uint64) if !ok { - return fmt.Errorf("invalid price max block age parameter type: %T", i) + return sdkerrors.Wrapf(ErrInvalidPriceMaxBlockAgeParameterType, "type: %T", i) } if priceMaxBlockAge == 0 { - return fmt.Errorf( - "price max block age must be non-zero", - ) + return sdkerrors.Wrapf(ErrTokenPriceMaxBlockAgeMustBePositive, "value: %d", priceMaxBlockAge) } return nil diff --git a/x/auction/types/params_test.go b/x/auction/types/params_test.go new file mode 100644 index 00000000..9c5c05df --- /dev/null +++ b/x/auction/types/params_test.go @@ -0,0 +1,51 @@ +package types + +import ( + "testing" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/stretchr/testify/require" +) + +func TestParamsValidate(t *testing.T) { + testCases := []struct { + name string + params Params + expPass bool + err error + }{ + { + name: "Happy path -- default params", + params: DefaultParams(), + expPass: true, + err: nil, + }, + { + name: "Happy path -- custom params", + params: Params{ + PriceMaxBlockAge: uint64(1000), + }, + expPass: true, + err: nil, + }, + { + name: "Max block age cannot be 0", + params: Params{ + PriceMaxBlockAge: uint64(0), + }, + expPass: false, + err: sdkerrors.Wrapf(ErrTokenPriceMaxBlockAgeMustBePositive, "value: 0"), + }, + } + + for _, tc := range testCases { + err := tc.params.ValidateBasic() + if tc.expPass { + require.NoError(t, err, tc.name) + require.Nil(t, err) + } else { + require.Error(t, err, tc.name) + require.Equal(t, tc.err.Error(), err.Error()) + } + } +} diff --git a/x/auction/types/proposal.go b/x/auction/types/proposal.go index 350a3639..1d9798c4 100644 --- a/x/auction/types/proposal.go +++ b/x/auction/types/proposal.go @@ -1,8 +1,7 @@ package types import ( - "fmt" - + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" ) @@ -39,10 +38,21 @@ func (m *SetTokenPricesProposal) ValidateBasic() error { } if len(m.TokenPrices) == 0 { - return fmt.Errorf("list of proposed token prices must be non-zero") + return sdkerrors.Wrapf(ErrTokenPriceProposalMustHaveAtLeastOnePrice, "prices: %v", m.TokenPrices) } + seenDenomPrices := []string{} + for _, tokenPrice := range m.TokenPrices { + // Check if this price proposal attempts to update the same denom price twice + for _, seenDenom := range seenDenomPrices { + if seenDenom == tokenPrice.Denom { + return sdkerrors.Wrapf(ErrTokenPriceProposalAttemptsToUpdateTokenPriceMoreThanOnce, "denom: %s", tokenPrice.Denom) + } + } + + seenDenomPrices = append(seenDenomPrices, tokenPrice.Denom) + tokenPriceError := tokenPrice.ValidateBasic() if tokenPriceError != nil { return tokenPriceError diff --git a/x/auction/types/proposal_test.go b/x/auction/types/proposal_test.go new file mode 100644 index 00000000..baa9d680 --- /dev/null +++ b/x/auction/types/proposal_test.go @@ -0,0 +1,157 @@ +package types + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + + "github.com/stretchr/testify/require" +) + +func TestNewSetTokenPricesProposal(t *testing.T) { + expectedMsg := &SetTokenPricesProposal{ + Title: "Planet Express", + Description: "Why not zoidberg", + TokenPrices: []*ProposedTokenPrice{ + { + Denom: "usomm", + UsdPrice: sdk.MustNewDecFromStr("4.2"), + }, + }, + } + + createdMsg := NewSetTokenPricesProposal("Planet Express", "Why not zoidberg", []*ProposedTokenPrice{ + { + Denom: "usomm", + UsdPrice: sdk.MustNewDecFromStr("4.2"), + }, + }) + require.Equal(t, expectedMsg, createdMsg) +} + +func TestTokenPriceProposalValidate(t *testing.T) { + testCases := []struct { + name string + setTokenPricesProposal SetTokenPricesProposal + expPass bool + err error + }{ + { + name: "Happy path", + setTokenPricesProposal: SetTokenPricesProposal{ + Title: "Planet Express", + Description: "Why not zoidberg", + TokenPrices: []*ProposedTokenPrice{ + { + Denom: "usomm", + UsdPrice: sdk.MustNewDecFromStr("4.2"), + }, + }, + }, + expPass: true, + err: nil, + }, + { + name: "Gov validate basic canary 1 -- title cannot be empty", + setTokenPricesProposal: SetTokenPricesProposal{ + Title: "", + Description: "Why not zoidberg", + TokenPrices: []*ProposedTokenPrice{ + { + Denom: "usomm", + UsdPrice: sdk.MustNewDecFromStr("4.2"), + }, + }, + }, + expPass: false, + err: sdkerrors.Wrap(govtypes.ErrInvalidProposalContent, "proposal title cannot be blank"), + }, + { + name: "Gov validate basic canary 2 -- description cannot be empty", + setTokenPricesProposal: SetTokenPricesProposal{ + Title: "Planet Express", + Description: "", + TokenPrices: []*ProposedTokenPrice{ + { + Denom: "usomm", + UsdPrice: sdk.MustNewDecFromStr("4.2"), + }, + }, + }, + expPass: false, + err: sdkerrors.Wrap(govtypes.ErrInvalidProposalContent, "proposal description cannot be blank"), + }, + { + name: "Token price proposal must have at least one token price", + setTokenPricesProposal: SetTokenPricesProposal{ + Title: "Planet Express", + Description: "Why not zoidberg", + TokenPrices: []*ProposedTokenPrice{}, + }, + expPass: false, + err: sdkerrors.Wrapf(ErrTokenPriceProposalMustHaveAtLeastOnePrice, "prices: %v", []*ProposedTokenPrice{}), + }, + { + name: "Cannot have duplicate denoms in token price proposal", + setTokenPricesProposal: SetTokenPricesProposal{ + Title: "Planet Express", + Description: "Why not zoidberg", + TokenPrices: []*ProposedTokenPrice{ + { + Denom: "usomm", + UsdPrice: sdk.MustNewDecFromStr("4.2"), + }, + { + Denom: "usomm", + UsdPrice: sdk.MustNewDecFromStr("7.8"), + }, + }, + }, + expPass: false, + err: sdkerrors.Wrapf(ErrTokenPriceProposalAttemptsToUpdateTokenPriceMoreThanOnce, "denom: usomm"), + }, + { + name: "Token price validate basic canary 1 -- cannot have empty denom", + setTokenPricesProposal: SetTokenPricesProposal{ + Title: "Planet Express", + Description: "Why not zoidberg", + TokenPrices: []*ProposedTokenPrice{ + { + Denom: "", + UsdPrice: sdk.MustNewDecFromStr("4.2"), + }, + }, + }, + expPass: false, + err: sdkerrors.Wrapf(ErrDenomCannotBeEmpty, "price denom: "), + }, + { + name: "Token price validate basic canary 2 -- price must be positive", + setTokenPricesProposal: SetTokenPricesProposal{ + Title: "Planet Express", + Description: "Why not zoidberg", + TokenPrices: []*ProposedTokenPrice{ + { + Denom: "usomm", + UsdPrice: sdk.MustNewDecFromStr("0.0"), + }, + }, + }, + expPass: false, + err: sdkerrors.Wrapf(ErrPriceMustBePositive, "usd price: %s", sdk.MustNewDecFromStr("0.0").String()), + }, + } + + for _, tc := range testCases { + err := tc.setTokenPricesProposal.ValidateBasic() + if tc.expPass { + require.NoError(t, err, tc.name) + require.Nil(t, err) + } else { + require.Error(t, err, tc.name) + require.Equal(t, tc.err.Error(), err.Error()) + } + } +} diff --git a/x/auction/types/tx.pb.go b/x/auction/types/tx.pb.go index 45316390..04a282ea 100644 --- a/x/auction/types/tx.pb.go +++ b/x/auction/types/tx.pb.go @@ -32,10 +32,9 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package type MsgSubmitBidRequest struct { AuctionId uint32 `protobuf:"varint,1,opt,name=auction_id,json=auctionId,proto3" json:"auction_id,omitempty"` - Bidder string `protobuf:"bytes,2,opt,name=bidder,proto3" json:"bidder,omitempty"` + Signer string `protobuf:"bytes,2,opt,name=signer,proto3" json:"signer,omitempty"` MaxBidInUsomm types.Coin `protobuf:"bytes,3,opt,name=max_bid_in_usomm,json=maxBidInUsomm,proto3" json:"max_bid_in_usomm"` SaleTokenMinimumAmount types.Coin `protobuf:"bytes,4,opt,name=sale_token_minimum_amount,json=saleTokenMinimumAmount,proto3" json:"sale_token_minimum_amount"` - Signer string `protobuf:"bytes,5,opt,name=signer,proto3" json:"signer,omitempty"` } func (m *MsgSubmitBidRequest) Reset() { *m = MsgSubmitBidRequest{} } @@ -78,9 +77,9 @@ func (m *MsgSubmitBidRequest) GetAuctionId() uint32 { return 0 } -func (m *MsgSubmitBidRequest) GetBidder() string { +func (m *MsgSubmitBidRequest) GetSigner() string { if m != nil { - return m.Bidder + return m.Signer } return "" } @@ -99,13 +98,6 @@ func (m *MsgSubmitBidRequest) GetSaleTokenMinimumAmount() types.Coin { return types.Coin{} } -func (m *MsgSubmitBidRequest) GetSigner() string { - if m != nil { - return m.Signer - } - return "" -} - type MsgSubmitBidResponse struct { Bid *Bid `protobuf:"bytes,1,opt,name=bid,proto3" json:"bid,omitempty"` } @@ -158,33 +150,32 @@ func init() { func init() { proto.RegisterFile("auction/v1/tx.proto", fileDescriptor_a60fe804de30894a) } var fileDescriptor_a60fe804de30894a = []byte{ - // 402 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x52, 0x3d, 0x6f, 0xd4, 0x30, - 0x18, 0x4e, 0x7a, 0xa5, 0xd2, 0xb9, 0xaa, 0x40, 0x6e, 0x55, 0xe5, 0x4e, 0x22, 0x0d, 0x9d, 0x6e, - 0xb2, 0x95, 0x83, 0x85, 0x91, 0xb0, 0x70, 0xc3, 0x31, 0x04, 0xba, 0x74, 0xb1, 0xe2, 0xc4, 0x32, - 0x86, 0xda, 0x0e, 0xb1, 0x13, 0xa5, 0x7f, 0x81, 0x89, 0x9f, 0xd5, 0xb1, 0x23, 0x13, 0x42, 0x77, - 0x7f, 0x04, 0x39, 0x4e, 0xa0, 0x48, 0x20, 0xb1, 0xe5, 0x7d, 0x3e, 0xde, 0x3c, 0x7a, 0x5e, 0x83, - 0xd3, 0xa2, 0x2d, 0xad, 0xd0, 0x0a, 0x77, 0x29, 0xb6, 0x3d, 0xaa, 0x1b, 0x6d, 0x35, 0x04, 0x23, - 0x88, 0xba, 0x74, 0x19, 0x3d, 0x10, 0x4c, 0xf0, 0xa0, 0x5a, 0xc6, 0xa5, 0x36, 0x52, 0x1b, 0x4c, - 0x0b, 0xc3, 0x70, 0x97, 0x52, 0x66, 0x8b, 0x14, 0x97, 0x5a, 0x4c, 0xfc, 0xc2, 0xf3, 0x64, 0x98, - 0xb0, 0x1f, 0x46, 0xea, 0x8c, 0x6b, 0xae, 0x3d, 0xee, 0xbe, 0x3c, 0x7a, 0xf9, 0xe5, 0x00, 0x9c, - 0x6e, 0x0d, 0x7f, 0xd7, 0x52, 0x29, 0x6c, 0x26, 0xaa, 0x9c, 0x7d, 0x6e, 0x99, 0xb1, 0xf0, 0x29, - 0x98, 0x02, 0x11, 0x51, 0x45, 0x61, 0x12, 0xae, 0x4e, 0xf2, 0xf9, 0x88, 0x6c, 0x2a, 0x78, 0x0e, - 0x8e, 0xa8, 0xa8, 0x2a, 0xd6, 0x44, 0x07, 0x49, 0xb8, 0x9a, 0xe7, 0xe3, 0x04, 0xdf, 0x80, 0x27, - 0xb2, 0xe8, 0x09, 0x15, 0x15, 0x11, 0x8a, 0xb4, 0x46, 0x4b, 0x19, 0xcd, 0x92, 0x70, 0x75, 0xbc, - 0x5e, 0xa0, 0x31, 0x8d, 0x8b, 0x8e, 0xc6, 0xe8, 0xe8, 0xb5, 0x16, 0x2a, 0x3b, 0xbc, 0xfb, 0x7e, - 0x11, 0xe4, 0x27, 0xb2, 0xe8, 0x33, 0x51, 0x6d, 0xd4, 0x95, 0x73, 0xc1, 0x6b, 0xb0, 0x30, 0xc5, - 0x0d, 0x23, 0x56, 0x7f, 0x62, 0x8a, 0x48, 0xa1, 0x84, 0x6c, 0x25, 0x29, 0xa4, 0x6e, 0x95, 0x8d, - 0x0e, 0xff, 0x6f, 0xe5, 0xb9, 0xdb, 0xf0, 0xde, 0x2d, 0xd8, 0x7a, 0xff, 0xab, 0xc1, 0xee, 0xd2, - 0x1b, 0xc1, 0x15, 0x6b, 0xa2, 0x47, 0x3e, 0xbd, 0x9f, 0x2e, 0x5f, 0x82, 0xb3, 0x3f, 0xbb, 0x30, - 0xb5, 0x56, 0x86, 0xc1, 0x67, 0x60, 0x46, 0xc7, 0x16, 0x8e, 0xd7, 0x8f, 0xd1, 0xef, 0x4b, 0x21, - 0xa7, 0x72, 0xdc, 0xfa, 0x0a, 0xcc, 0xb6, 0x86, 0xc3, 0xb7, 0x60, 0xfe, 0xcb, 0x0e, 0x2f, 0x1e, - 0x2a, 0xff, 0x52, 0xf2, 0x32, 0xf9, 0xb7, 0xc0, 0xff, 0x39, 0xdb, 0xdc, 0xed, 0xe2, 0xf0, 0x7e, - 0x17, 0x87, 0x3f, 0x76, 0x71, 0xf8, 0x75, 0x1f, 0x07, 0xf7, 0xfb, 0x38, 0xf8, 0xb6, 0x8f, 0x83, - 0x6b, 0xcc, 0x85, 0xfd, 0xd0, 0x52, 0x54, 0x6a, 0x89, 0x6b, 0xc6, 0xf9, 0xed, 0xc7, 0x0e, 0xbb, - 0xe2, 0xd8, 0x8d, 0x60, 0x0d, 0xee, 0x5e, 0xe0, 0x7e, 0x7a, 0x3a, 0xd8, 0xde, 0xd6, 0xcc, 0xd0, - 0xa3, 0xe1, 0xe0, 0xcf, 0x7f, 0x06, 0x00, 0x00, 0xff, 0xff, 0x7d, 0x6d, 0x74, 0x51, 0x7e, 0x02, - 0x00, 0x00, + // 391 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x52, 0xcd, 0x8e, 0xd3, 0x30, + 0x10, 0x8e, 0xe9, 0x6a, 0xa5, 0x7a, 0xb5, 0x02, 0x79, 0x57, 0xab, 0xb4, 0x12, 0xd9, 0xb0, 0xa7, + 0x9e, 0x6c, 0xa5, 0x70, 0xe1, 0x48, 0xb8, 0xd0, 0x43, 0x39, 0x04, 0x7a, 0xe9, 0xc5, 0xca, 0x8f, + 0x65, 0x0c, 0xb5, 0x1d, 0x6a, 0x27, 0x4a, 0xdf, 0x82, 0xc7, 0xea, 0xb1, 0x47, 0x4e, 0x08, 0xb5, + 0x6f, 0xc0, 0x13, 0x20, 0xe7, 0x07, 0x8a, 0x04, 0x12, 0xb7, 0xcc, 0xf7, 0x33, 0xf9, 0x66, 0xc6, + 0xf0, 0x26, 0xad, 0x72, 0x2b, 0xb4, 0x22, 0x75, 0x44, 0x6c, 0x83, 0xcb, 0xad, 0xb6, 0x1a, 0xc1, + 0x1e, 0xc4, 0x75, 0x34, 0xf5, 0xcf, 0x04, 0x03, 0xdc, 0xaa, 0xa6, 0x41, 0xae, 0x8d, 0xd4, 0x86, + 0x64, 0xa9, 0x61, 0xa4, 0x8e, 0x32, 0x66, 0xd3, 0x88, 0xe4, 0x5a, 0x0c, 0xfc, 0xa4, 0xe3, 0x69, + 0x5b, 0x91, 0xae, 0xe8, 0xa9, 0x5b, 0xae, 0xb9, 0xee, 0x70, 0xf7, 0xd5, 0xa1, 0x0f, 0x3f, 0x00, + 0xbc, 0x59, 0x1a, 0xfe, 0xae, 0xca, 0xa4, 0xb0, 0xb1, 0x28, 0x12, 0xf6, 0xb9, 0x62, 0xc6, 0xa2, + 0xa7, 0x70, 0x08, 0x44, 0x45, 0xe1, 0x83, 0x10, 0xcc, 0xae, 0x93, 0x71, 0x8f, 0x2c, 0x0a, 0x74, + 0x07, 0x2f, 0x8d, 0xe0, 0x8a, 0x6d, 0xfd, 0x47, 0x21, 0x98, 0x8d, 0x93, 0xbe, 0x42, 0x6f, 0xe0, + 0x13, 0x99, 0x36, 0x34, 0x13, 0x05, 0x15, 0x8a, 0x56, 0x46, 0x4b, 0xe9, 0x8f, 0x42, 0x30, 0xbb, + 0x9a, 0x4f, 0x70, 0x9f, 0xc6, 0x45, 0xc7, 0x7d, 0x74, 0xfc, 0x5a, 0x0b, 0x15, 0x5f, 0xec, 0xbf, + 0xdd, 0x7b, 0xc9, 0xb5, 0x4c, 0x9b, 0x58, 0x14, 0x0b, 0xb5, 0x72, 0x2e, 0xb4, 0x86, 0x13, 0x93, + 0x6e, 0x18, 0xb5, 0xfa, 0x13, 0x53, 0x54, 0x0a, 0x25, 0x64, 0x25, 0x69, 0x2a, 0x75, 0xa5, 0xac, + 0x7f, 0xf1, 0x7f, 0x2d, 0xef, 0x5c, 0x87, 0xf7, 0xae, 0xc1, 0xb2, 0xf3, 0xbf, 0x6a, 0xed, 0x0f, + 0x2f, 0xe1, 0xed, 0x9f, 0x33, 0x9b, 0x52, 0x2b, 0xc3, 0xd0, 0x33, 0x38, 0xca, 0xfa, 0x69, 0xaf, + 0xe6, 0x8f, 0xf1, 0xef, 0x8b, 0x60, 0xa7, 0x72, 0xdc, 0x7c, 0x05, 0x47, 0x4b, 0xc3, 0xd1, 0x5b, + 0x38, 0xfe, 0x65, 0x47, 0xf7, 0xe7, 0xca, 0xbf, 0x2c, 0x73, 0x1a, 0xfe, 0x5b, 0xd0, 0xfd, 0x39, + 0x5e, 0xec, 0x8f, 0x01, 0x38, 0x1c, 0x03, 0xf0, 0xfd, 0x18, 0x80, 0x2f, 0xa7, 0xc0, 0x3b, 0x9c, + 0x02, 0xef, 0xeb, 0x29, 0xf0, 0xd6, 0x84, 0x0b, 0xfb, 0xa1, 0xca, 0x70, 0xae, 0x25, 0x29, 0x19, + 0xe7, 0xbb, 0x8f, 0x35, 0x71, 0x0b, 0x62, 0x1b, 0xc1, 0xb6, 0xa4, 0x7e, 0x41, 0x9a, 0xe1, 0x89, + 0x10, 0xbb, 0x2b, 0x99, 0xc9, 0x2e, 0xdb, 0xc3, 0x3e, 0xff, 0x19, 0x00, 0x00, 0xff, 0xff, 0x1a, + 0x2b, 0x32, 0x4e, 0x66, 0x02, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -287,13 +278,6 @@ func (m *MsgSubmitBidRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l - if len(m.Signer) > 0 { - i -= len(m.Signer) - copy(dAtA[i:], m.Signer) - i = encodeVarintTx(dAtA, i, uint64(len(m.Signer))) - i-- - dAtA[i] = 0x2a - } { size, err := m.SaleTokenMinimumAmount.MarshalToSizedBuffer(dAtA[:i]) if err != nil { @@ -314,10 +298,10 @@ func (m *MsgSubmitBidRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { } i-- dAtA[i] = 0x1a - if len(m.Bidder) > 0 { - i -= len(m.Bidder) - copy(dAtA[i:], m.Bidder) - i = encodeVarintTx(dAtA, i, uint64(len(m.Bidder))) + if len(m.Signer) > 0 { + i -= len(m.Signer) + copy(dAtA[i:], m.Signer) + i = encodeVarintTx(dAtA, i, uint64(len(m.Signer))) i-- dAtA[i] = 0x12 } @@ -384,7 +368,7 @@ func (m *MsgSubmitBidRequest) Size() (n int) { if m.AuctionId != 0 { n += 1 + sovTx(uint64(m.AuctionId)) } - l = len(m.Bidder) + l = len(m.Signer) if l > 0 { n += 1 + l + sovTx(uint64(l)) } @@ -392,10 +376,6 @@ func (m *MsgSubmitBidRequest) Size() (n int) { n += 1 + l + sovTx(uint64(l)) l = m.SaleTokenMinimumAmount.Size() n += 1 + l + sovTx(uint64(l)) - l = len(m.Signer) - if l > 0 { - n += 1 + l + sovTx(uint64(l)) - } return n } @@ -468,7 +448,7 @@ func (m *MsgSubmitBidRequest) Unmarshal(dAtA []byte) error { } case 2: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Bidder", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Signer", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { @@ -496,7 +476,7 @@ func (m *MsgSubmitBidRequest) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.Bidder = string(dAtA[iNdEx:postIndex]) + m.Signer = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 3: if wireType != 2 { @@ -564,38 +544,6 @@ func (m *MsgSubmitBidRequest) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex - case 5: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Signer", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowTx - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthTx - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthTx - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Signer = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipTx(dAtA[iNdEx:]) diff --git a/x/cellarfees/keeper/genesis.go b/x/cellarfees/keeper/genesis.go index 000616a6..a03121ae 100644 --- a/x/cellarfees/keeper/genesis.go +++ b/x/cellarfees/keeper/genesis.go @@ -16,8 +16,6 @@ func InitGenesis(ctx sdk.Context, k Keeper, gs types.GenesisState) { if feesAccount == nil { panic(fmt.Sprintf("%s module account has not been set", types.ModuleName)) } - - k.accountKeeper.SetModuleAccount(ctx, feesAccount) } // ExportGenesis returns the module's exported genesis. diff --git a/x/cellarfees/types/expected_keepers.go b/x/cellarfees/types/expected_keepers.go index 4d952eb2..ab811b20 100644 --- a/x/cellarfees/types/expected_keepers.go +++ b/x/cellarfees/types/expected_keepers.go @@ -11,8 +11,6 @@ type AccountKeeper interface { GetModuleAddress(name string) sdk.AccAddress GetModuleAccount(ctx sdk.Context, name string) types.ModuleAccountI - - SetModuleAccount(sdk.Context, types.ModuleAccountI) } // BankKeeper defines the expected interface needed to retrieve account balances.