From 1315ce58015a409c3c1dedc72799fdf78a3b82fd Mon Sep 17 00:00:00 2001 From: codchen Date: Wed, 14 Jun 2023 00:29:59 +0800 Subject: [PATCH] Add testing harness for e2e application logic tests (#850) * Add testing harness for e2e go tests * polish * add mint/epoch/distribution tests/helpers * dex test * refactor * rebase --- app/test_helpers.go | 7 +- tests/dex_test.go | 100 +++++++ tests/distribution_test.go | 35 +++ tests/epoch_test.go | 39 +++ tests/mars.wasm | Bin 0 -> 191011 bytes tests/mint_test.go | 40 +++ tests/template_test.go | 56 ++++ testutil/processblock/common.go | 167 ++++++++++++ testutil/processblock/genesisacc.go | 19 ++ testutil/processblock/genesisbank.go | 45 ++++ testutil/processblock/genesisepoch.go | 9 + testutil/processblock/genesismint.go | 19 ++ testutil/processblock/genesisstaking.go | 56 ++++ testutil/processblock/genesiswasm.go | 28 ++ testutil/processblock/msgs/bank.go | 14 + testutil/processblock/msgs/dex.go | 101 +++++++ testutil/processblock/presets.go | 91 +++++++ testutil/processblock/tx.go | 89 ++++++ testutil/processblock/verify/bank.go | 87 ++++++ testutil/processblock/verify/common.go | 23 ++ testutil/processblock/verify/dex.go | 269 +++++++++++++++++++ testutil/processblock/verify/distribution.go | 89 ++++++ testutil/processblock/verify/epoch.go | 21 ++ testutil/processblock/verify/mint.go | 40 +++ x/dex/contract/abci.go | 2 +- 25 files changed, 1442 insertions(+), 4 deletions(-) create mode 100644 tests/dex_test.go create mode 100644 tests/distribution_test.go create mode 100644 tests/epoch_test.go create mode 100644 tests/mars.wasm create mode 100644 tests/mint_test.go create mode 100644 tests/template_test.go create mode 100644 testutil/processblock/common.go create mode 100644 testutil/processblock/genesisacc.go create mode 100644 testutil/processblock/genesisbank.go create mode 100644 testutil/processblock/genesisepoch.go create mode 100644 testutil/processblock/genesismint.go create mode 100644 testutil/processblock/genesisstaking.go create mode 100644 testutil/processblock/genesiswasm.go create mode 100644 testutil/processblock/msgs/bank.go create mode 100644 testutil/processblock/msgs/dex.go create mode 100644 testutil/processblock/presets.go create mode 100644 testutil/processblock/tx.go create mode 100644 testutil/processblock/verify/bank.go create mode 100644 testutil/processblock/verify/common.go create mode 100644 testutil/processblock/verify/dex.go create mode 100644 testutil/processblock/verify/distribution.go create mode 100644 testutil/processblock/verify/epoch.go create mode 100644 testutil/processblock/verify/mint.go diff --git a/app/test_helpers.go b/app/test_helpers.go index 0169d03dc6..43e3aae1a1 100644 --- a/app/test_helpers.go +++ b/app/test_helpers.go @@ -19,6 +19,7 @@ import ( minttypes "github.com/sei-protocol/sei-chain/x/mint/types" "github.com/stretchr/testify/suite" abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/libs/log" tmproto "github.com/tendermint/tendermint/proto/tendermint/types" dbm "github.com/tendermint/tm-db" @@ -58,7 +59,7 @@ func NewTestWrapper(t *testing.T, tm time.Time, valPub crptotypes.PubKey) *TestW Ctx: ctx, } wrapper.SetT(t) - wrapper.setupValidator(stakingtypes.Bonded, valPub) + wrapper.setupValidator(stakingtypes.Unbonded, valPub) return wrapper } @@ -154,8 +155,8 @@ func Setup(isCheckTx bool) *App { true, map[int64]bool{}, DefaultNodeHome, - 5, - nil, + 1, + config.TestConfig(), encodingConfig, wasm.EnableAllProposals, &cosmostestutil.TestAppOpts{}, diff --git a/tests/dex_test.go b/tests/dex_test.go new file mode 100644 index 0000000000..dd5afeba37 --- /dev/null +++ b/tests/dex_test.go @@ -0,0 +1,100 @@ +package tests + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/sei-protocol/sei-chain/testutil/processblock" + "github.com/sei-protocol/sei-chain/testutil/processblock/verify" +) + +func TestPlaceOrders(t *testing.T) { + app := processblock.NewTestApp() + p := processblock.DexPreset(app, 3, 2) + p.DoRegisterMarkets(app) + + for _, testCase := range []TestCase{ + { + description: "send a single market buy without counterparty on orderbook", + input: []signing.Tx{ + app.Sign(p.SignableAccounts[0], 10000, p.AllDexMarkets[0].LongMarketOrder(p.SignableAccounts[0], "11", "2")), + }, + verifier: []verify.Verifier{ + verify.DexOrders, + }, + expectedCodes: []uint32{0}, + }, + { + description: "send a single market sell without counterparty on orderbook", + input: []signing.Tx{ + app.Sign(p.SignableAccounts[0], 10000, p.AllDexMarkets[0].ShortMarketOrder(p.SignableAccounts[0], "10.5", "4")), + }, + verifier: []verify.Verifier{ + verify.DexOrders, + }, + expectedCodes: []uint32{0}, + }, + { + description: "send a single buy limit order", + input: []signing.Tx{ + app.Sign(p.SignableAccounts[0], 10000, p.AllDexMarkets[0].LongLimitOrder(p.SignableAccounts[0], "10.5", "5")), + }, + verifier: []verify.Verifier{ + verify.DexOrders, + }, + expectedCodes: []uint32{0}, + }, + { + description: "send a single sell limit order", + input: []signing.Tx{ + app.Sign(p.SignableAccounts[1], 10000, p.AllDexMarkets[0].ShortLimitOrder(p.SignableAccounts[1], "11", "3")), + }, + verifier: []verify.Verifier{ + verify.DexOrders, + }, + expectedCodes: []uint32{0}, + }, + { + description: "send a single market buy without exhausting the orderbook", + input: []signing.Tx{ + app.Sign(p.SignableAccounts[2], 10000, p.AllDexMarkets[0].LongMarketOrder(p.SignableAccounts[2], "11", "2")), + }, + verifier: []verify.Verifier{ + verify.DexOrders, + }, + expectedCodes: []uint32{0}, + }, + { + description: "send a single market sell without exhausting the orderbook", + input: []signing.Tx{ + app.Sign(p.SignableAccounts[2], 10000, p.AllDexMarkets[0].ShortMarketOrder(p.SignableAccounts[2], "10.5", "4")), + }, + verifier: []verify.Verifier{ + verify.DexOrders, + }, + expectedCodes: []uint32{0}, + }, + { + description: "send a single market buy exhausting the orderbook", + input: []signing.Tx{ + app.Sign(p.SignableAccounts[2], 10000, p.AllDexMarkets[0].LongMarketOrder(p.SignableAccounts[2], "12", "2")), + }, + verifier: []verify.Verifier{ + verify.DexOrders, + }, + expectedCodes: []uint32{0}, + }, + { + description: "send a single market sell exhausting the orderbook", + input: []signing.Tx{ + app.Sign(p.SignableAccounts[2], 10000, p.AllDexMarkets[0].ShortMarketOrder(p.SignableAccounts[2], "10", "2")), + }, + verifier: []verify.Verifier{ + verify.DexOrders, + }, + expectedCodes: []uint32{0}, + }, + } { + testCase.run(t, app) + } +} diff --git a/tests/distribution_test.go b/tests/distribution_test.go new file mode 100644 index 0000000000..3eff30d7e0 --- /dev/null +++ b/tests/distribution_test.go @@ -0,0 +1,35 @@ +package tests + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/sei-protocol/sei-chain/testutil/processblock" + "github.com/sei-protocol/sei-chain/testutil/processblock/msgs" + "github.com/sei-protocol/sei-chain/testutil/processblock/verify" +) + +func TestDistribution(t *testing.T) { + app := processblock.NewTestApp() + p := processblock.CommonPreset(app) + for _, testCase := range []TestCase{ + { + description: "send to accrue fee for next block", + input: []signing.Tx{ + p.AdminSign(app, msgs.Send(p.Admin, p.AllAccounts[0], 1000)), + }, + verifier: []verify.Verifier{}, + expectedCodes: []uint32{0}, + }, + { + description: "check distribution", + input: []signing.Tx{}, + verifier: []verify.Verifier{ + verify.Allocation, + }, + expectedCodes: []uint32{}, + }, + } { + testCase.run(t, app) + } +} diff --git a/tests/epoch_test.go b/tests/epoch_test.go new file mode 100644 index 0000000000..4f779e6679 --- /dev/null +++ b/tests/epoch_test.go @@ -0,0 +1,39 @@ +package tests + +import ( + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/sei-protocol/sei-chain/testutil/processblock" + "github.com/sei-protocol/sei-chain/testutil/processblock/verify" +) + +func TestEpoch(t *testing.T) { + app := processblock.NewTestApp() + _ = processblock.CommonPreset(app) + app.FastEpoch() + for i, testCase := range []TestCase{ + { + description: "first epoch", + input: []signing.Tx{}, + verifier: []verify.Verifier{ + verify.Epoch, + }, + expectedCodes: []uint32{}, + }, + { + description: "second epoch", + input: []signing.Tx{}, + verifier: []verify.Verifier{ + verify.Epoch, + }, + expectedCodes: []uint32{}, + }, + } { + if i > 0 { + time.Sleep(6 * time.Second) + } + testCase.run(t, app) + } +} diff --git a/tests/mars.wasm b/tests/mars.wasm new file mode 100644 index 0000000000000000000000000000000000000000..8addcee304dbfe95128720b948d636864291403a GIT binary patch literal 191011 zcmeFa542}jRp)vC{QkY)pZ9+CFZEuk67KIuN?%Ep7LHOy$kKPKsU(O>Ovlw}o0(Kd ziXpE;NGb)5JsGM3X^0RoD6K(J3r(8{gpM}dlh%w@FhI0v32PWTJ&m)03L3Q^9$DqLsoL8PcdUxlN|M9vaF-lzojCC}{J#yoBe)o63^yO3UY>kDlyZ-nMZ{tc9fT&G}Noch> z*G}7c+HN+}@CKE51*npCXgW=s&D(>;xbo4$Ip{}@_ELyaV* zayQ?!Sv|M1R%1)DHLZ_Ipad?f0hL)*Fw!@lD5mG)sCn-E#Z!>u))JlSt>=8n?gs#y2H5_KzL8 z?aenGJ97JVw;#DFd1L4Ln{R&84XW^=w0q-`>h9;#O;trG_t^Ev-*hZ_7D7@ z(|?-2GksTjclu-LyVLii_oVMl7jC%W244HM^uy`DO#j#0jvc>c;UB-{tq-UF%c=BN z(|^K~hiLZ1!d>b6(jQOnRe!&n{#^F9g*&+V&Yw=-ys-b{>Az6%!q2C_kbW+G{laI{ zf0r(N=Wl1P|9utz`*cB7o=X2O9{tDki#+I{eRPE(mzlC zYx-=u@RQk3Wbe=ZJpJ+P-s}U}1KESwsqA#N@Qc~+WFO6bHT$*fce8()eIol;*}u)Y zAAYd=$?QMpY2!q;6#o^;VwTJ{(ngjiy&@_5%iNE%#*!X1Mro0i^EZ#Oe5pvwhMw^x z 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 F{|`2uo0k9p literal 0 HcmV?d00001 diff --git a/tests/mint_test.go b/tests/mint_test.go new file mode 100644 index 0000000000..e52b6f16e2 --- /dev/null +++ b/tests/mint_test.go @@ -0,0 +1,40 @@ +package tests + +import ( + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/sei-protocol/sei-chain/testutil/processblock" + "github.com/sei-protocol/sei-chain/testutil/processblock/verify" +) + +func TestMint(t *testing.T) { + app := processblock.NewTestApp() + _ = processblock.CommonPreset(app) + app.NewMinter(1000000) + app.FastEpoch() + for i, testCase := range []TestCase{ + { + description: "first epoch", + input: []signing.Tx{}, + verifier: []verify.Verifier{ + verify.MintRelease, + }, + expectedCodes: []uint32{}, + }, + { + description: "second epoch", + input: []signing.Tx{}, + verifier: []verify.Verifier{ + verify.MintRelease, + }, + expectedCodes: []uint32{}, + }, + } { + if i > 0 { + time.Sleep(6 * time.Second) + } + testCase.run(t, app) + } +} diff --git a/tests/template_test.go b/tests/template_test.go new file mode 100644 index 0000000000..ae07486ab7 --- /dev/null +++ b/tests/template_test.go @@ -0,0 +1,56 @@ +package tests + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/sei-protocol/sei-chain/testutil/processblock" + "github.com/sei-protocol/sei-chain/testutil/processblock/msgs" + "github.com/sei-protocol/sei-chain/testutil/processblock/verify" + "github.com/stretchr/testify/require" +) + +type TestCase struct { + description string + input []signing.Tx + verifier []verify.Verifier + expectedCodes []uint32 +} + +func (c *TestCase) run(t *testing.T, app *processblock.App) { + blockRunner := func() []uint32 { return app.RunBlock(c.input) } + for _, v := range c.verifier { + blockRunner = v(t, app, blockRunner, c.input) + } + require.Equal(t, c.expectedCodes, blockRunner(), c.description) +} + +func TestTemplate(t *testing.T) { + app := processblock.NewTestApp() + p := processblock.CommonPreset(app) // choose a preset + for _, testCase := range []TestCase{ + { + description: "simple send 1", + input: []signing.Tx{ + p.AdminSign(app, msgs.Send(p.Admin, p.AllAccounts[0], 1000)), + }, + verifier: []verify.Verifier{ + verify.Balance, + }, + expectedCodes: []uint32{0}, + }, + { + description: "simple send 2", + input: []signing.Tx{ + p.AdminSign(app, msgs.Send(p.Admin, p.AllAccounts[1], 2000)), + p.AdminSign(app, msgs.Send(p.Admin, p.AllAccounts[2], 3000)), + }, + verifier: []verify.Verifier{ + verify.Balance, + }, + expectedCodes: []uint32{0, 0}, + }, + } { + testCase.run(t, app) + } +} diff --git a/testutil/processblock/common.go b/testutil/processblock/common.go new file mode 100644 index 0000000000..a71c4998cd --- /dev/null +++ b/testutil/processblock/common.go @@ -0,0 +1,167 @@ +package processblock + +import ( + "context" + "crypto/rand" + "encoding/json" + "time" + + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/signing" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/cosmos/go-bip39" + "github.com/sei-protocol/sei-chain/app" + "github.com/sei-protocol/sei-chain/utils" + "github.com/tendermint/tendermint/abci/types" + tmtypes "github.com/tendermint/tendermint/types" +) + +type App struct { + *app.App + + height int64 + proposer int + accToMnemonic map[string]string + accToSeqDelta map[string]uint64 + lastCtx sdk.Context +} + +func NewTestApp() *App { + a := &App{ + App: app.Setup(false), + height: 1, + accToMnemonic: map[string]string{}, + accToSeqDelta: map[string]uint64{}, + } + interfaceRegistry := codectypes.NewInterfaceRegistry() + marshaler := codec.NewProtoCodec(interfaceRegistry) + cp := tmtypes.DefaultConsensusParams().ToProto() + gs := app.NewDefaultGenesisState(marshaler) + gbz, err := json.Marshal(gs) + if err != nil { + panic(err) + } + _, err = a.InitChain(context.Background(), &types.RequestInitChain{ + Time: time.Now(), + ChainId: "tendermint_test", + ConsensusParams: &cp, + Validators: []types.ValidatorUpdate{}, + InitialHeight: 1, + AppStateBytes: gbz, + }) + if err != nil { + panic(err) + } + a.lastCtx = a.GetContextForDeliverTx([]byte{}) + return a +} + +func (a *App) Ctx() sdk.Context { + return a.lastCtx +} + +// Processes and commits a block of transactions, and return a list of response codes. +// Assumes all validators voted with equal weight, and there are no byzantine validators. +// Proposer is rotated among all validators round-robin. +func (a *App) RunBlock(txs []signing.Tx) (resultCodes []uint32) { + defer func() { + a.lastCtx = a.GetContextForDeliverTx([]byte{}) // Commit will set deliver tx ctx to nil so we need to cache it here for testing queries before the next block is FinalizeBlock'ed (which will set deliver tx ctx) + _, err := a.Commit(context.Background()) + if err != nil { + panic(err) + } + a.accToSeqDelta = map[string]uint64{} + a.height++ + a.proposer = (a.proposer + 1) % len(a.GetAllValidators()) + }() + + res, err := a.FinalizeBlock(context.Background(), &types.RequestFinalizeBlock{ + Txs: utils.Map(txs, func(tx signing.Tx) []byte { + bz, err := TxConfig.TxEncoder()(tx) + if err != nil { + panic(err) + } + return bz + }), + DecidedLastCommit: types.CommitInfo{ + Round: 0, + Votes: a.GetVotes(), + }, + ByzantineValidators: []types.Misbehavior{}, + Hash: []byte("abc"), // no needed for application logic + Height: a.height, + ProposerAddress: getValAddress(a.GetProposer()), + Time: time.Now(), + }) + if err != nil { + panic(err) + } + return utils.Map(res.TxResults, func(r *types.ExecTxResult) uint32 { return r.Code }) +} + +func (a *App) GetVotes() []types.VoteInfo { + return utils.Map(a.GetAllValidators(), func(v stakingtypes.Validator) types.VoteInfo { + return types.VoteInfo{ + Validator: types.Validator{ + Address: getValAddress(v), + Power: 1, + }, + SignedLastBlock: true, + } + }) +} + +func (a *App) GetAllValidators() []stakingtypes.Validator { + return a.StakingKeeper.GetAllValidators(a.Ctx()) +} + +func (a *App) GetProposer() stakingtypes.Validator { + return a.GetAllValidators()[a.proposer] +} + +func (a *App) GenerateSignableKey(name string) (addr sdk.AccAddress) { + entropySeed, err := bip39.NewEntropy(256) + if err != nil { + panic(err) + } + mnemonic, err := bip39.NewMnemonic(entropySeed) + if err != nil { + panic(err) + } + hdPath := hd.CreateHDPath(sdk.GetConfig().GetCoinType(), 0, 0).String() + derivedPriv, _ := hd.Secp256k1.Derive()(mnemonic, "", hdPath) + privKey := hd.Secp256k1.Generate()(derivedPriv) + addr = sdk.AccAddress(privKey.PubKey().Address()) + a.accToMnemonic[addr.String()] = mnemonic + return +} + +func GenerateRandomPubKey() cryptotypes.PubKey { + pubBz := make([]byte, secp256k1.PubKeySize) + pub := &secp256k1.PubKey{Key: pubBz} + if _, err := rand.Read(pub.Key); err != nil { + panic(err) + } + return pub +} + +func generateRandomStringOfLength(len int) string { + bz := make([]byte, len) + if _, err := rand.Read(bz); err != nil { + panic(err) + } + return string(bz) +} + +func getValAddress(v stakingtypes.Validator) []byte { + pub := secp256k1.PubKey{} + if err := pub.Unmarshal(v.ConsensusPubkey.Value); err != nil { + panic(err) + } + return sdk.AccAddress(pub.Address()) +} diff --git a/testutil/processblock/genesisacc.go b/testutil/processblock/genesisacc.go new file mode 100644 index 0000000000..a51ae2bded --- /dev/null +++ b/testutil/processblock/genesisacc.go @@ -0,0 +1,19 @@ +package processblock + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func (a *App) NewAccount() sdk.AccAddress { + ctx := a.Ctx() + address := sdk.AccAddress(GenerateRandomPubKey().Address()) + a.AccountKeeper.SetAccount(ctx, a.AccountKeeper.NewAccountWithAddress(ctx, address)) + return address +} + +func (a *App) NewSignableAccount(name string) sdk.AccAddress { + ctx := a.Ctx() + address := a.GenerateSignableKey(name) + a.AccountKeeper.SetAccount(ctx, a.AccountKeeper.NewAccountWithAddress(ctx, address)) + return address +} diff --git a/testutil/processblock/genesisbank.go b/testutil/processblock/genesisbank.go new file mode 100644 index 0000000000..628648dc17 --- /dev/null +++ b/testutil/processblock/genesisbank.go @@ -0,0 +1,45 @@ +package processblock + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" +) + +func (a *App) FundAccount(acc sdk.AccAddress, amount int64) { + a.FundAccountWithDenom(acc, amount, "usei") +} + +func (a *App) FundModule(moduleName string, amount int64) { + a.FundModuleWithDenom(moduleName, amount, "usei") +} + +func (a *App) FundAccountWithDenom(acc sdk.AccAddress, amount int64, denom string) { + ctx := a.Ctx() + amounts := sdk.NewCoins(sdk.NewCoin(denom, sdk.NewInt(amount))) + if err := a.BankKeeper.MintCoins(ctx, minttypes.ModuleName, amounts); err != nil { + panic(err) + } + if err := a.BankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, acc, amounts); err != nil { + panic(err) + } + m, b := bankkeeper.TotalSupply(a.BankKeeper)(ctx) + if b { + panic(m) + } +} + +func (a *App) FundModuleWithDenom(moduleName string, amount int64, denom string) { + ctx := a.Ctx() + amounts := sdk.NewCoins(sdk.NewCoin(denom, sdk.NewInt(amount))) + if err := a.BankKeeper.MintCoins(ctx, minttypes.ModuleName, amounts); err != nil { + panic(err) + } + if err := a.BankKeeper.SendCoinsFromModuleToModule(ctx, minttypes.ModuleName, moduleName, amounts); err != nil { + panic(err) + } + m, b := bankkeeper.TotalSupply(a.BankKeeper)(ctx) + if b { + panic(m) + } +} diff --git a/testutil/processblock/genesisepoch.go b/testutil/processblock/genesisepoch.go new file mode 100644 index 0000000000..e67b18422b --- /dev/null +++ b/testutil/processblock/genesisepoch.go @@ -0,0 +1,9 @@ +package processblock + +import "time" + +func (a *App) FastEpoch() { + epoch := a.EpochKeeper.GetEpoch(a.Ctx()) + epoch.EpochDuration = 5 * time.Second + a.EpochKeeper.SetEpoch(a.Ctx(), epoch) +} diff --git a/testutil/processblock/genesismint.go b/testutil/processblock/genesismint.go new file mode 100644 index 0000000000..e4beda3827 --- /dev/null +++ b/testutil/processblock/genesismint.go @@ -0,0 +1,19 @@ +package processblock + +import ( + "time" + + minttypes "github.com/sei-protocol/sei-chain/x/mint/types" +) + +func (a *App) NewMinter(amount uint64) { + today := time.Now() + dayAfterTomorrow := today.Add(48 * time.Hour) + a.MintKeeper.SetMinter(a.Ctx(), minttypes.Minter{ + StartDate: today.Format(minttypes.TokenReleaseDateFormat), + EndDate: dayAfterTomorrow.Format(minttypes.TokenReleaseDateFormat), + Denom: "usei", + TotalMintAmount: amount, + RemainingMintAmount: amount, + }) +} diff --git a/testutil/processblock/genesisstaking.go b/testutil/processblock/genesisstaking.go new file mode 100644 index 0000000000..404e968ed0 --- /dev/null +++ b/testutil/processblock/genesisstaking.go @@ -0,0 +1,56 @@ +package processblock + +import ( + "fmt" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +func (a *App) NewValidator() sdk.ValAddress { + ctx := a.Ctx() + key := GenerateRandomPubKey() + address := key.Address() + a.AccountKeeper.SetAccount(ctx, a.AccountKeeper.NewAccountWithAddress(ctx, sdk.AccAddress(address))) + valAddress := sdk.ValAddress(address) + validator, err := stakingtypes.NewValidator(valAddress, key, stakingtypes.NewDescription( + generateRandomStringOfLength(4), + generateRandomStringOfLength(4), + generateRandomStringOfLength(8), + generateRandomStringOfLength(8), + generateRandomStringOfLength(16), + )) + if err != nil { + panic(err) + } + a.StakingKeeper.SetValidator(ctx, validator) + if err := a.StakingKeeper.SetValidatorByConsAddr(ctx, validator); err != nil { + panic(err) + } + a.StakingKeeper.SetNewValidatorByPowerIndex(ctx, validator) + a.StakingKeeper.AfterValidatorCreated(ctx, validator.GetOperator()) + signingInfo := slashingtypes.NewValidatorSigningInfo( + sdk.ConsAddress(address), + 0, + 0, + time.Unix(0, 0), + false, + 0, + ) + a.SlashingKeeper.SetValidatorSigningInfo(ctx, sdk.ConsAddress(address), signingInfo) + return valAddress +} + +func (a *App) NewDelegation(delegator sdk.AccAddress, validator sdk.ValAddress, amount int64) { + ctx := a.Ctx() + val, found := a.StakingKeeper.GetValidator(ctx, validator) + if !found { + panic(fmt.Sprintf("validator %s not found", validator)) + } + _, err := a.StakingKeeper.Delegate(ctx, delegator, sdk.NewInt(amount), stakingtypes.Unbonded, val, true) + if err != nil { + panic(err) + } +} diff --git a/testutil/processblock/genesiswasm.go b/testutil/processblock/genesiswasm.go new file mode 100644 index 0000000000..1d33da6f08 --- /dev/null +++ b/testutil/processblock/genesiswasm.go @@ -0,0 +1,28 @@ +package processblock + +import ( + "os" + + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func (a *App) NewContract(admin sdk.AccAddress, filePath string) sdk.AccAddress { + wasm, err := os.ReadFile(filePath) + if err != nil { + panic(err) + } + wasmKeeper := a.WasmKeeper + contractKeeper := wasmkeeper.NewDefaultPermissionKeeper(&wasmKeeper) + var perm *wasmtypes.AccessConfig + codeID, err := contractKeeper.Create(a.Ctx(), admin, wasm, perm) + if err != nil { + panic(err) + } + contractAddr, _, err := contractKeeper.Instantiate(a.Ctx(), codeID, admin, admin, []byte("{}"), "test", sdk.NewCoins()) + if err != nil { + panic(err) + } + return contractAddr +} diff --git a/testutil/processblock/msgs/bank.go b/testutil/processblock/msgs/bank.go new file mode 100644 index 0000000000..f7ac8c2b84 --- /dev/null +++ b/testutil/processblock/msgs/bank.go @@ -0,0 +1,14 @@ +package msgs + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" +) + +func Send(from sdk.AccAddress, to sdk.AccAddress, amount int64) *banktypes.MsgSend { + return &banktypes.MsgSend{ + FromAddress: from.String(), + ToAddress: to.String(), + Amount: sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(amount))), + } +} diff --git a/testutil/processblock/msgs/dex.go b/testutil/processblock/msgs/dex.go new file mode 100644 index 0000000000..6a9e79991b --- /dev/null +++ b/testutil/processblock/msgs/dex.go @@ -0,0 +1,101 @@ +package msgs + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/sei-protocol/sei-chain/utils" + dextypes "github.com/sei-protocol/sei-chain/x/dex/types" +) + +type Market struct { + contract string + priceDenom string + assetDenom string +} + +func NewMarket(contract string, priceDenom string, assetDenom string) *Market { + return &Market{ + contract: contract, + priceDenom: priceDenom, + assetDenom: assetDenom, + } +} + +func (m *Market) Register(admin sdk.AccAddress, deps []string, deposit uint64) []sdk.Msg { + pointOne := sdk.NewDecWithPrec(1, 1) + return []sdk.Msg{dextypes.NewMsgRegisterContract(admin.String(), 0, m.contract, true, utils.Map(deps, func(dep string) *dextypes.ContractDependencyInfo { + return &dextypes.ContractDependencyInfo{Dependency: dep} + }), deposit), dextypes.NewMsgRegisterPairs(admin.String(), []dextypes.BatchContractPair{{ + ContractAddr: m.contract, + Pairs: []*dextypes.Pair{{ + PriceDenom: m.priceDenom, + AssetDenom: m.assetDenom, + PriceTicksize: &pointOne, + QuantityTicksize: &pointOne, + }}, + }})} +} + +func (m *Market) LongLimitOrder(account sdk.AccAddress, price string, quantity string) *dextypes.MsgPlaceOrders { + o := m.commonOrder(account, price, quantity) + o.PositionDirection = dextypes.PositionDirection_LONG + o.OrderType = dextypes.OrderType_LIMIT + return dextypes.NewMsgPlaceOrders(account.String(), []*dextypes.Order{o}, m.contract, fundForOrder(o)) +} + +func (m *Market) ShortLimitOrder(account sdk.AccAddress, price string, quantity string) *dextypes.MsgPlaceOrders { + o := m.commonOrder(account, price, quantity) + o.PositionDirection = dextypes.PositionDirection_SHORT + o.OrderType = dextypes.OrderType_LIMIT + return dextypes.NewMsgPlaceOrders(account.String(), []*dextypes.Order{o}, m.contract, fundForOrder(o)) +} + +func (m *Market) LongMarketOrder(account sdk.AccAddress, price string, quantity string) *dextypes.MsgPlaceOrders { + o := m.commonOrder(account, price, quantity) + o.PositionDirection = dextypes.PositionDirection_LONG + o.OrderType = dextypes.OrderType_MARKET + return dextypes.NewMsgPlaceOrders(account.String(), []*dextypes.Order{o}, m.contract, fundForOrder(o)) +} + +func (m *Market) ShortMarketOrder(account sdk.AccAddress, price string, quantity string) *dextypes.MsgPlaceOrders { + o := m.commonOrder(account, price, quantity) + o.PositionDirection = dextypes.PositionDirection_SHORT + o.OrderType = dextypes.OrderType_MARKET + return dextypes.NewMsgPlaceOrders(account.String(), []*dextypes.Order{o}, m.contract, fundForOrder(o)) +} + +func (m *Market) CancelLongOrder(account sdk.AccAddress, price string, id uint64) *dextypes.MsgCancelOrders { + c := m.commonCancel(account, price, id) + c.PositionDirection = dextypes.PositionDirection_LONG + return dextypes.NewMsgCancelOrders(account.String(), []*dextypes.Cancellation{c}, m.contract) +} + +func (m *Market) CancelShortOrder(account sdk.AccAddress, price string, id uint64) *dextypes.MsgCancelOrders { + c := m.commonCancel(account, price, id) + c.PositionDirection = dextypes.PositionDirection_SHORT + return dextypes.NewMsgCancelOrders(account.String(), []*dextypes.Cancellation{c}, m.contract) +} + +func (m *Market) commonOrder(account sdk.AccAddress, price string, quantity string) *dextypes.Order { + return &dextypes.Order{ + Account: account.String(), + Price: sdk.MustNewDecFromStr(price), + Quantity: sdk.MustNewDecFromStr(quantity), + PriceDenom: m.priceDenom, + AssetDenom: m.assetDenom, + } +} + +func (m *Market) commonCancel(account sdk.AccAddress, price string, id uint64) *dextypes.Cancellation { + return &dextypes.Cancellation{ + Creator: account.String(), + Price: sdk.MustNewDecFromStr(price), + Id: id, + PriceDenom: m.priceDenom, + AssetDenom: m.assetDenom, + ContractAddr: m.contract, + } +} + +func fundForOrder(o *dextypes.Order) sdk.Coins { + return sdk.NewCoins(sdk.NewCoin("usei", o.Price.Mul(o.Quantity).RoundInt())) +} diff --git a/testutil/processblock/presets.go b/testutil/processblock/presets.go new file mode 100644 index 0000000000..785046f28d --- /dev/null +++ b/testutil/processblock/presets.go @@ -0,0 +1,91 @@ +package processblock + +import ( + "fmt" + + "github.com/CosmWasm/wasmd/x/wasm" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/signing" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/sei-protocol/sei-chain/testutil/processblock/msgs" + "github.com/sei-protocol/sei-chain/utils" + dextypes "github.com/sei-protocol/sei-chain/x/dex/types" + minttypes "github.com/sei-protocol/sei-chain/x/mint/types" +) + +type Preset struct { + Admin sdk.AccAddress // a signable account that's not supposed to run out of tokens + SignableAccounts []sdk.AccAddress + AllAccounts []sdk.AccAddress + AllValidators []sdk.ValAddress + AllContracts []sdk.AccAddress + AllDexMarkets []*msgs.Market +} + +// 3 unsignable accounts +// 3 bonded validators +func CommonPreset(app *App) *Preset { + fmt.Printf("Fee collector: %s\n", app.AccountKeeper.GetModuleAddress(authtypes.FeeCollectorName).String()) + fmt.Printf("Mint module: %s\n", app.AccountKeeper.GetModuleAddress(minttypes.ModuleName).String()) + fmt.Printf("Distribution module: %s\n", app.AccountKeeper.GetModuleAddress(distrtypes.ModuleName).String()) + fmt.Printf("Staking bonded pool: %s\n", app.AccountKeeper.GetModuleAddress(stakingtypes.BondedPoolName).String()) + fmt.Printf("Staking unbonded pool: %s\n", app.AccountKeeper.GetModuleAddress(stakingtypes.NotBondedPoolName).String()) + fmt.Printf("Dex module: %s\n", app.AccountKeeper.GetModuleAddress(dextypes.ModuleName).String()) + fmt.Printf("Wasm module: %s\n", app.AccountKeeper.GetModuleAddress(wasm.ModuleName).String()) + p := &Preset{ + Admin: app.NewSignableAccount("admin"), + } + fmt.Printf("Admin: %s\n", p.Admin.String()) + app.FundAccount(p.Admin, 100000000000) + for i := 0; i < 3; i++ { + acc := app.NewAccount() + p.AllAccounts = append(p.AllAccounts, acc) + fmt.Printf("CommonPreset account: %s\n", acc.String()) + } + for i := 0; i < 3; i++ { + val := app.NewValidator() + app.FundAccount(sdk.AccAddress(val), 10000000) + app.NewDelegation(sdk.AccAddress(val), val, 7000000) + p.AllAccounts = append(p.AllAccounts, sdk.AccAddress(val)) + p.AllValidators = append(p.AllValidators, val) + fmt.Printf("CommonPreset val: %s\n", sdk.AccAddress(val).String()) + } + return p +} + +func DexPreset(app *App, numAccounts int, numMarkets int) *Preset { + p := CommonPreset(app) + for i := 0; i < numAccounts; i++ { + acc := app.NewSignableAccount(fmt.Sprintf("DexPreset%d", i)) + app.FundAccount(acc, 10000000) + p.AllAccounts = append(p.AllAccounts, acc) + p.SignableAccounts = append(p.SignableAccounts, acc) + fmt.Printf("DexPreset account: %s\n", acc.String()) + } + for i := 0; i < numMarkets; i++ { + contract := app.NewContract(p.Admin, "./mars.wasm") + market := msgs.NewMarket(contract.String(), "SEI", fmt.Sprintf("ATOM%d", i)) + p.AllContracts = append(p.AllContracts, contract) + p.AllDexMarkets = append(p.AllDexMarkets, market) + fmt.Printf("DexPreset contract: %s\n", contract.String()) + } + return p +} + +// always with enough fee +func (p *Preset) AdminSign(app *App, msgs ...sdk.Msg) signing.Tx { + return app.Sign(p.Admin, 10000000, msgs...) +} + +func (p *Preset) DoRegisterMarkets(app *App) { + block := utils.Map(p.AllDexMarkets, func(m *msgs.Market) signing.Tx { + return p.AdminSign(app, m.Register(p.Admin, []string{}, 20000000)...) + }) + for i, code := range app.RunBlock(block) { + if code != 0 { + panic(fmt.Sprintf("error code %d when registering the %d-th market", code, i)) + } + } +} diff --git a/testutil/processblock/tx.go b/testutil/processblock/tx.go new file mode 100644 index 0000000000..095a990f21 --- /dev/null +++ b/testutil/processblock/tx.go @@ -0,0 +1,89 @@ +package processblock + +import ( + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/hd" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + xauthsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/cosmos/cosmos-sdk/x/auth/tx" +) + +var InterfaceReg = types.NewInterfaceRegistry() +var Marshaler = codec.NewProtoCodec(InterfaceReg) +var TxConfig = tx.NewTxConfig(Marshaler, tx.DefaultSignModes) + +func (a *App) Sign(account sdk.AccAddress, fee int64, msgs ...sdk.Msg) xauthsigning.Tx { + txBuilder := TxConfig.NewTxBuilder() + if err := txBuilder.SetMsgs(msgs...); err != nil { + panic(err) + } + txBuilder.SetGasLimit(1000000) + txBuilder.SetFeeAmount([]sdk.Coin{ + sdk.NewCoin("usei", sdk.NewInt(fee)), + }) + + acc := a.AccountKeeper.GetAccount(a.Ctx(), account) + seqNum := acc.GetSequence() + if delta, ok := a.accToSeqDelta[account.String()]; ok { + seqNum += delta + } + privKey := GetKey(a.accToMnemonic[account.String()]) + + signerData := xauthsigning.SignerData{ + ChainID: "tendermint_test", + AccountNumber: acc.GetAccountNumber(), + Sequence: seqNum, + } + sigData := signing.SingleSignatureData{ + SignMode: TxConfig.SignModeHandler().DefaultMode(), + Signature: nil, + } + sig := signing.SignatureV2{ + PubKey: privKey.PubKey(), + Data: &sigData, + Sequence: seqNum, + } + if err := txBuilder.SetSignatures(sig); err != nil { + panic(err) + } + bytesToSign, err := TxConfig.SignModeHandler().GetSignBytes(TxConfig.SignModeHandler().DefaultMode(), signerData, txBuilder.GetTx()) + if err != nil { + panic(err) + } + sigBytes, err := privKey.Sign(bytesToSign) + if err != nil { + panic(err) + } + sigData = signing.SingleSignatureData{ + SignMode: TxConfig.SignModeHandler().DefaultMode(), + Signature: sigBytes, + } + sig = signing.SignatureV2{ + PubKey: privKey.PubKey(), + Data: &sigData, + Sequence: seqNum, + } + + err = txBuilder.SetSignatures(sig) + if err != nil { + panic(err) + } + if _, ok := a.accToSeqDelta[account.String()]; ok { + a.accToSeqDelta[account.String()]++ + } else { + a.accToSeqDelta[account.String()] = 1 + } + return txBuilder.GetTx() +} + +func GetKey(mnemonic string) cryptotypes.PrivKey { + algo := hd.Secp256k1 + hdpath := hd.CreateHDPath(sdk.GetConfig().GetCoinType(), 0, 0).String() + derivedPriv, _ := algo.Derive()(mnemonic, "", hdpath) + privKey := algo.Generate()(derivedPriv) + + return privKey +} diff --git a/testutil/processblock/verify/bank.go b/testutil/processblock/verify/bank.go new file mode 100644 index 0000000000..580ee5b85d --- /dev/null +++ b/testutil/processblock/verify/bank.go @@ -0,0 +1,87 @@ +package verify + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/signing" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/sei-protocol/sei-chain/testutil/processblock" + dextypes "github.com/sei-protocol/sei-chain/x/dex/types" + "github.com/stretchr/testify/require" +) + +// Check balance changes as result of executing the provided transactions. +// Only works if all transactions are successful. +func Balance(t *testing.T, app *processblock.App, f BlockRunnable, txs []signing.Tx) BlockRunnable { + return func() []uint32 { + expectedChanges := map[string]map[string]int64{} // denom -> (account -> delta) + for _, tx := range txs { + for _, fee := range tx.GetFee() { + updateExpectedBalanceChange(expectedChanges, tx.FeePayer().String(), fee, false) + } + for _, msg := range tx.GetMsgs() { + switch m := msg.(type) { + case *banktypes.MsgSend: + updateMultipleExpectedBalanceChange(expectedChanges, m.FromAddress, m.Amount, false) + updateMultipleExpectedBalanceChange(expectedChanges, m.ToAddress, m.Amount, true) + case *banktypes.MsgMultiSend: + for _, input := range m.Inputs { + updateMultipleExpectedBalanceChange(expectedChanges, input.Address, input.Coins, false) + } + for _, output := range m.Outputs { + updateMultipleExpectedBalanceChange(expectedChanges, output.Address, output.Coins, true) + } + case *dextypes.MsgPlaceOrders: + updateMultipleExpectedBalanceChange(expectedChanges, m.Creator, m.Funds, false) + updateMultipleExpectedBalanceChange(expectedChanges, m.ContractAddr, m.Funds, true) + case *dextypes.MsgRegisterContract: + funds := sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(int64(m.Contract.RentBalance)))) + updateMultipleExpectedBalanceChange(expectedChanges, m.Creator, funds, false) + default: + // TODO: add coverage for other balance-affecting messages to enable testing for those message types + continue + } + } + } + expectedBalances := map[string]map[string]int64{} + for denom, changes := range expectedChanges { + expectedBalances[denom] = map[string]int64{} + for account, delta := range changes { + balance := app.BankKeeper.GetBalance(app.Ctx(), sdk.MustAccAddressFromBech32(account), denom) + expectedBalances[denom][account] = balance.Amount.Int64() + delta + } + } + + results := f() + + for denom, expectedBalances := range expectedBalances { + for account, expectedBalance := range expectedBalances { + actualBalance := app.BankKeeper.GetBalance(app.Ctx(), sdk.MustAccAddressFromBech32(account), denom) + require.Equal(t, expectedBalance, actualBalance.Amount.Int64()) + } + } + + return results + } +} + +func updateMultipleExpectedBalanceChange(changes map[string]map[string]int64, account string, coins sdk.Coins, positive bool) { + for _, coin := range coins { + updateExpectedBalanceChange(changes, account, coin, positive) + } +} + +func updateExpectedBalanceChange(changes map[string]map[string]int64, account string, coin sdk.Coin, positive bool) { + if _, ok := changes[coin.Denom]; !ok { + changes[coin.Denom] = map[string]int64{} + } + if _, ok := changes[coin.Denom][account]; !ok { + changes[coin.Denom][account] = 0 + } + if positive { + changes[coin.Denom][account] += coin.Amount.Int64() + } else { + changes[coin.Denom][account] -= coin.Amount.Int64() + } +} diff --git a/testutil/processblock/verify/common.go b/testutil/processblock/verify/common.go new file mode 100644 index 0000000000..6d7a45f7d0 --- /dev/null +++ b/testutil/processblock/verify/common.go @@ -0,0 +1,23 @@ +package verify + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/sei-protocol/sei-chain/testutil/processblock" +) + +type BlockRunnable func() (resultCodes []uint32) + +type Verifier func(*testing.T, *processblock.App, BlockRunnable, []signing.Tx) BlockRunnable + +// inefficient so only for test +func removeMatched[T any](l []T, matcher func(T) bool) []T { + newL := []T{} + for _, i := range l { + if !matcher(i) { + newL = append(newL, i) + } + } + return newL +} diff --git a/testutil/processblock/verify/dex.go b/testutil/processblock/verify/dex.go new file mode 100644 index 0000000000..6684959f06 --- /dev/null +++ b/testutil/processblock/verify/dex.go @@ -0,0 +1,269 @@ +package verify + +import ( + "sort" + "strings" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/sei-protocol/sei-chain/testutil/processblock" + "github.com/sei-protocol/sei-chain/utils" + dexkeeper "github.com/sei-protocol/sei-chain/x/dex/keeper" + dextypes "github.com/sei-protocol/sei-chain/x/dex/types" + "github.com/stretchr/testify/require" +) + +func DexOrders(t *testing.T, app *processblock.App, f BlockRunnable, txs []signing.Tx) BlockRunnable { + return func() []uint32 { + orderPlacementsByMarket := map[string][]*dextypes.Order{} + orderCancellationsByMarket := map[string][]*dextypes.Cancellation{} + markets := map[string]struct{}{} + for _, tx := range txs { + for _, msg := range tx.GetMsgs() { + switch m := msg.(type) { + case *dextypes.MsgPlaceOrders: + for _, o := range m.Orders { + id := strings.Join([]string{m.ContractAddr, o.PriceDenom, o.AssetDenom}, ",") + markets[id] = struct{}{} + if orders, ok := orderPlacementsByMarket[id]; ok { + o.Id = app.DexKeeper.GetNextOrderID(app.Ctx(), m.ContractAddr) + uint64(len(orders)) + orderPlacementsByMarket[id] = append(orders, o) + } else { + o.Id = app.DexKeeper.GetNextOrderID(app.Ctx(), m.ContractAddr) + orderPlacementsByMarket[id] = []*dextypes.Order{o} + } + } + case *dextypes.MsgCancelOrders: + for _, o := range m.Cancellations { + id := strings.Join([]string{m.ContractAddr, o.PriceDenom, o.AssetDenom}, ",") + markets[id] = struct{}{} + if cancels, ok := orderCancellationsByMarket[id]; ok { + orderCancellationsByMarket[id] = append(cancels, o) + } else { + orderCancellationsByMarket[id] = []*dextypes.Cancellation{o} + } + } + default: + continue + } + } + } + expectedLongBookByMarket := map[string]map[sdk.Dec]*dextypes.OrderEntry{} + expectedShortBookByMarket := map[string]map[sdk.Dec]*dextypes.OrderEntry{} + for market := range markets { + orderPlacements := []*dextypes.Order{} + if o, ok := orderPlacementsByMarket[market]; ok { + orderPlacements = o + } + orderCancellations := []*dextypes.Cancellation{} + if c, ok := orderCancellationsByMarket[market]; ok { + orderCancellations = c + } + parts := strings.Split(market, ",") + longBook, shortBook := expectedOrdersForMarket(app.Ctx(), &app.DexKeeper, orderPlacements, orderCancellations, parts[0], parts[1], parts[2]) + expectedLongBookByMarket[market] = longBook + expectedShortBookByMarket[market] = shortBook + } + + results := f() + + for market, longBook := range expectedLongBookByMarket { + parts := strings.Split(market, ",") + contract := parts[0] + priceDenom := parts[1] + assetDenom := parts[2] + require.Equal(t, len(longBook), len(app.DexKeeper.GetAllLongBookForPair(app.Ctx(), contract, priceDenom, assetDenom))) + for price, entry := range longBook { + actual, found := app.DexKeeper.GetLongOrderBookEntryByPrice(app.Ctx(), contract, price, priceDenom, assetDenom) + require.True(t, found) + require.Equal(t, *entry, *(actual.GetOrderEntry())) + } + } + + for market, shortBook := range expectedShortBookByMarket { + parts := strings.Split(market, ",") + contract := parts[0] + priceDenom := parts[1] + assetDenom := parts[2] + require.Equal(t, len(shortBook), len(app.DexKeeper.GetAllShortBookForPair(app.Ctx(), contract, priceDenom, assetDenom))) + for price, entry := range shortBook { + actual, found := app.DexKeeper.GetShortOrderBookEntryByPrice(app.Ctx(), contract, price, priceDenom, assetDenom) + require.True(t, found) + require.Equal(t, *entry, *(actual.GetOrderEntry())) + } + } + + return results + } +} + +// A slow but correct implementation of dex exchange logics that build the expected order book state based on the +// current state and the list of new orders/cancellations, based on dex exchange rules. +func expectedOrdersForMarket( + ctx sdk.Context, + keeper *dexkeeper.Keeper, + orderPlacements []*dextypes.Order, + orderCancellations []*dextypes.Cancellation, + contract string, + priceDenom string, + assetDenom string, +) (longEntries map[sdk.Dec]*dextypes.OrderEntry, shortEntries map[sdk.Dec]*dextypes.OrderEntry) { + longBook := toOrderBookMap(keeper.GetAllLongBookForPair(ctx, contract, priceDenom, assetDenom)) + shortBook := toOrderBookMap(keeper.GetAllShortBookForPair(ctx, contract, priceDenom, assetDenom)) + books := map[dextypes.PositionDirection]map[sdk.Dec]*dextypes.OrderEntry{ + dextypes.PositionDirection_LONG: longBook, + dextypes.PositionDirection_SHORT: shortBook, + } + // first, cancellation + cancelOrders(books, orderCancellations) + // then add new limit orders to book + addOrders(books, orderPlacements) + // then match market orders + matchOrders(getMarketOrderBookMap(orderPlacements, dextypes.PositionDirection_LONG), shortBook) + matchOrders(longBook, getMarketOrderBookMap(orderPlacements, dextypes.PositionDirection_SHORT)) + // finally match limit orders + matchOrders(longBook, shortBook) + return longBook, shortBook +} + +func cancelOrders( + books map[dextypes.PositionDirection]map[sdk.Dec]*dextypes.OrderEntry, + orderCancellations []*dextypes.Cancellation, +) { + for _, cancel := range orderCancellations { + book := books[cancel.PositionDirection] + if entry, ok := book[cancel.Price]; ok { + entry.Allocations = removeMatched(entry.Allocations, func(a *dextypes.Allocation) bool { return a.OrderId == cancel.Id }) + updateEntryQuantity(entry) + if entry.Quantity.IsZero() { + delete(book, cancel.Price) + } + } + } +} + +func addOrders( + books map[dextypes.PositionDirection]map[sdk.Dec]*dextypes.OrderEntry, + orderPlacements []*dextypes.Order, +) { + for _, o := range orderPlacements { + if o.OrderType != dextypes.OrderType_LIMIT { + continue + } + book := books[o.PositionDirection] + newAllocation := &dextypes.Allocation{ + Account: o.Account, + Quantity: o.Quantity, + OrderId: o.Id, + } + if entry, ok := book[o.Price]; ok { + entry.Allocations = append(entry.Allocations, newAllocation) + updateEntryQuantity(entry) + } else { + book[o.Price] = &dextypes.OrderEntry{ + Price: o.Price, + Quantity: o.Quantity, + PriceDenom: o.PriceDenom, + AssetDenom: o.AssetDenom, + Allocations: []*dextypes.Allocation{newAllocation}, + } + } + } +} + +func matchOrders( + longBook map[sdk.Dec]*dextypes.OrderEntry, + shortBook map[sdk.Dec]*dextypes.OrderEntry, +) { + buyPrices := sortedPrices(longBook, true) + sellPrices := sortedPrices(shortBook, false) + for i, j := 0, 0; i < len(buyPrices) && j < len(sellPrices) && buyPrices[i].GTE(sellPrices[j]); { + buyEntry := longBook[buyPrices[i]] + sellEntry := shortBook[sellPrices[j]] + if buyEntry.Quantity.GT(sellEntry.Quantity) { + takeLiquidity(longBook, buyPrices[i], sellEntry.Quantity) + takeLiquidity(shortBook, sellPrices[i], sellEntry.Quantity) + j++ + } else { + takeLiquidity(longBook, buyPrices[i], buyEntry.Quantity) + takeLiquidity(shortBook, sellPrices[i], buyEntry.Quantity) + i++ + } + } +} + +func toOrderBookMap(book []dextypes.OrderBookEntry) map[sdk.Dec]*dextypes.OrderEntry { + bookMap := map[sdk.Dec]*dextypes.OrderEntry{} + for _, e := range book { + bookMap[e.GetPrice()] = e.GetOrderEntry() + } + return bookMap +} + +func getMarketOrderBookMap(orderPlacements []*dextypes.Order, direction dextypes.PositionDirection) map[sdk.Dec]*dextypes.OrderEntry { + bookMap := map[sdk.Dec]*dextypes.OrderEntry{} + for _, o := range orderPlacements { + if o.OrderType != dextypes.OrderType_MARKET || o.PositionDirection != direction { + continue + } + bookMap[o.Price] = orderToOrderEntry(o) + } + return bookMap +} + +func updateEntryQuantity(entry *dextypes.OrderEntry) { + entry.Quantity = utils.Reduce( + entry.Allocations, + func(a *dextypes.Allocation, q sdk.Dec) sdk.Dec { return q.Add(a.Quantity) }, + sdk.ZeroDec(), + ) +} + +func takeLiquidity(book map[sdk.Dec]*dextypes.OrderEntry, price sdk.Dec, quantity sdk.Dec) { + entry := book[price] + if entry.Quantity.Equal(quantity) { + delete(book, price) + return + } + if quantity.GT(entry.Quantity) { + panic("insufficient liquidity") + } + allocated := sdk.ZeroDec() + newAllocations := []*dextypes.Allocation{} + for _, a := range entry.Allocations { + switch { + case allocated.Equal(quantity): + newAllocations = append(newAllocations, a) + case allocated.Add(a.Quantity).GT(quantity): + a.Quantity = a.Quantity.Sub(quantity.Sub(allocated)) + newAllocations = append(newAllocations, a) + allocated = quantity + default: + allocated = allocated.Add(a.Quantity) + } + } + entry.Allocations = newAllocations + entry.Quantity = entry.Quantity.Sub(quantity) +} + +func sortedPrices(book map[sdk.Dec]*dextypes.OrderEntry, descending bool) []sdk.Dec { + prices := []sdk.Dec{} + for p := range book { + prices = append(prices, p) + } + if descending { + sort.Slice(prices, func(i, j int) bool { return prices[i].GT(prices[j]) }) + } else { + sort.Slice(prices, func(i, j int) bool { return prices[i].LT(prices[j]) }) + } + return prices +} + +func orderToOrderEntry(order *dextypes.Order) *dextypes.OrderEntry { + return &dextypes.OrderEntry{ + Price: order.Price, + Quantity: order.Quantity, + Allocations: []*dextypes.Allocation{{Quantity: order.Quantity}}, + } +} diff --git a/testutil/processblock/verify/distribution.go b/testutil/processblock/verify/distribution.go new file mode 100644 index 0000000000..9d4506168d --- /dev/null +++ b/testutil/processblock/verify/distribution.go @@ -0,0 +1,89 @@ +package verify + +import ( + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/signing" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/distribution/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/sei-protocol/sei-chain/testutil/processblock" + "github.com/sei-protocol/sei-chain/utils" + "github.com/stretchr/testify/require" +) + +// assuming only `usei` will get distributed +func Allocation(t *testing.T, app *processblock.App, f BlockRunnable, _ []signing.Tx) BlockRunnable { + return func() []uint32 { + // fees collected in T-1 are allocated in T's BeginBlock, so we can simply + // query fee collector's balance since this function is called between T-1 + // and T. + feeCollector := app.AccountKeeper.GetModuleAccount(app.Ctx(), authtypes.FeeCollectorName) + feesCollectedInt := app.BankKeeper.GetBalance(app.Ctx(), feeCollector.GetAddress(), "usei") + feesCollected := sdk.NewDecCoinFromCoin(feesCollectedInt) + + prevProposer := sdk.ValAddress(app.DistrKeeper.GetPreviousProposerConsAddr(app.Ctx())).String() + votedValidators := utils.Map(app.GetAllValidators(), func(v stakingtypes.Validator) string { + return v.GetOperator().String() + }) + expectedOutstandingRewards := getOutstandingRewards(app) + + baseProposerReward := app.DistrKeeper.GetBaseProposerReward(app.Ctx()) + bonusProposerReward := app.DistrKeeper.GetBonusProposerReward(app.Ctx()) + proposerMultiplier := baseProposerReward.Add(bonusProposerReward.MulTruncate(sdk.OneDec())) // in test, every val always signs + proposerReward := sdk.DecCoin{ + Denom: "usei", + Amount: feesCollected.Amount.MulTruncate(proposerMultiplier), + } + expectedOutstandingRewards[prevProposer] = expectedOutstandingRewards[prevProposer].Add(proposerReward) + + communityTax := app.DistrKeeper.GetCommunityTax(app.Ctx()) + voteMultiplier := sdk.OneDec().Sub(proposerMultiplier).Sub(communityTax).QuoInt(sdk.NewInt(int64(len(votedValidators)))) + voterReward := sdk.DecCoin{ + Denom: "usei", + Amount: feesCollected.Amount.MulTruncate(voteMultiplier), + } + + for _, validator := range votedValidators { + expectedOutstandingRewards[validator] = expectedOutstandingRewards[validator].Add(voterReward) + } + + res := f() + + actualOutstandingRewards := getOutstandingRewards(app) + + require.Equal(t, len(expectedOutstandingRewards), len(actualOutstandingRewards)) + + for val, reward := range expectedOutstandingRewards { + require.True(t, reward.Equal(actualOutstandingRewards[val])) + } + + return res + } +} + +func getOutstandingRewards(app *processblock.App) map[string]sdk.DecCoin { + outstandingRewards := map[string]sdk.DecCoin{} + for _, val := range app.GetAllValidators() { + outstandingRewards[val.GetOperator().String()] = sdk.NewDecCoin("usei", sdk.NewInt(0)) + } + app.DistrKeeper.IterateValidatorOutstandingRewards( + app.Ctx(), + func(val sdk.ValAddress, rewards types.ValidatorOutstandingRewards) (stop bool) { + if len(rewards.Rewards) == 0 { + return false + } + if len(rewards.Rewards) > 1 { + panic("expecting only usei as validator reward denom but found multiple") + } + if rewards.Rewards[0].Denom != "usei" { + panic(fmt.Sprintf("expecting only usei as validator reward denom but found %s", rewards.Rewards[0].Denom)) + } + outstandingRewards[val.String()] = rewards.Rewards[0] + return false + }, + ) + return outstandingRewards +} diff --git a/testutil/processblock/verify/epoch.go b/testutil/processblock/verify/epoch.go new file mode 100644 index 0000000000..7c35411d4b --- /dev/null +++ b/testutil/processblock/verify/epoch.go @@ -0,0 +1,21 @@ +package verify + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/sei-protocol/sei-chain/testutil/processblock" + "github.com/stretchr/testify/require" +) + +func Epoch(t *testing.T, app *processblock.App, f BlockRunnable, _ []signing.Tx) BlockRunnable { + return func() []uint32 { + oldEpoch := app.EpochKeeper.GetEpoch(app.Ctx()) + res := f() + if app.Ctx().BlockTime().Sub(oldEpoch.CurrentEpochStartTime) > oldEpoch.EpochDuration { + newPoch := app.EpochKeeper.GetEpoch(app.Ctx()) + require.Equal(t, oldEpoch.CurrentEpoch+1, newPoch.CurrentEpoch) + } + return res + } +} diff --git a/testutil/processblock/verify/mint.go b/testutil/processblock/verify/mint.go new file mode 100644 index 0000000000..3517f55062 --- /dev/null +++ b/testutil/processblock/verify/mint.go @@ -0,0 +1,40 @@ +package verify + +import ( + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/sei-protocol/sei-chain/testutil/processblock" + minttypes "github.com/sei-protocol/sei-chain/x/mint/types" + "github.com/stretchr/testify/require" +) + +func MintRelease(t *testing.T, app *processblock.App, f BlockRunnable, _ []signing.Tx) BlockRunnable { + return func() []uint32 { + oldMinter := app.MintKeeper.GetMinter(app.Ctx()) + oldEpoch := app.EpochKeeper.GetEpoch(app.Ctx()) + oldSupply := app.BankKeeper.GetSupply(app.Ctx(), "usei") + res := f() + // if minter minted, it must be a new epoch, but not the other way around + newMinter := app.MintKeeper.GetMinter(app.Ctx()) + if newMinter.RemainingMintAmount == oldMinter.RemainingMintAmount { + return res + } + newPoch := app.EpochKeeper.GetEpoch(app.Ctx()) + require.Equal(t, oldEpoch.CurrentEpoch+1, newPoch.CurrentEpoch) + startDate, err := time.Parse(minttypes.TokenReleaseDateFormat, oldMinter.StartDate) + if err != nil { + panic(err) + } + endDate, err := time.Parse(minttypes.TokenReleaseDateFormat, oldMinter.EndDate) + if err != nil { + panic(err) + } + expectedMintedAmount := oldMinter.TotalMintAmount / uint64(endDate.Sub(startDate)/(24*time.Hour)) + require.Equal(t, expectedMintedAmount, oldMinter.RemainingMintAmount-newMinter.RemainingMintAmount) + newSupply := app.BankKeeper.GetSupply(app.Ctx(), "usei") + require.Equal(t, expectedMintedAmount, uint64(newSupply.Amount.Int64()-oldSupply.Amount.Int64())) + return res + } +} diff --git a/x/dex/contract/abci.go b/x/dex/contract/abci.go index e415cb8205..6243f4ce39 100644 --- a/x/dex/contract/abci.go +++ b/x/dex/contract/abci.go @@ -77,7 +77,7 @@ func EndBlockerAtomic(ctx sdk.Context, keeper *keeper.Keeper, validContractsInfo // No error is thrown for any contract. This should happen most of the time. if env.failedContractAddressesToErrors.Len() == 0 { postRunRents := keeper.GetRentsForContracts(cachedCtx, contractsToProcess) - TransferRentFromDexToCollector(ctx, keeper.BankKeeper, preRunRents, postRunRents) + TransferRentFromDexToCollector(cachedCtx, keeper.BankKeeper, preRunRents, postRunRents) msCached.Write() return env.validContractsInfo, []types.ContractInfoV2{}, map[string]string{}, ctx, true }