diff --git a/Makefile b/Makefile index 73363ee32..cab262baa 100644 --- a/Makefile +++ b/Makefile @@ -348,7 +348,7 @@ start-remote-sims: update-kvtool: git submodule init || true - git submodule update + git submodule update --remote cd tests/e2e/kvtool && make install .PHONY: all build-linux install build test test-cli test-all test-rest test-basic test-fuzz start-remote-sims diff --git a/app/upgrades.go b/app/upgrades.go index cef008252..3775d7468 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -1,3 +1,274 @@ package app -func (app App) RegisterUpgradeHandlers() {} +import ( + "fmt" + + sdkmath "cosmossdk.io/math" + + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + evmutilkeeper "github.com/kava-labs/kava/x/evmutil/keeper" + evmutiltypes "github.com/kava-labs/kava/x/evmutil/types" + precisebankkeeper "github.com/kava-labs/kava/x/precisebank/keeper" + precisebanktypes "github.com/kava-labs/kava/x/precisebank/types" +) + +const ( + UpgradeName_Mainnet = "v0.27.0" + UpgradeName_Testnet = "v0.27.0-alpha.0" +) + +// RegisterUpgradeHandlers registers the upgrade handlers for the app. +func (app App) RegisterUpgradeHandlers() { + app.upgradeKeeper.SetUpgradeHandler( + UpgradeName_Mainnet, + upgradeHandler(app, UpgradeName_Mainnet), + ) + app.upgradeKeeper.SetUpgradeHandler( + UpgradeName_Testnet, + upgradeHandler(app, UpgradeName_Testnet), + ) + + upgradeInfo, err := app.upgradeKeeper.ReadUpgradeInfoFromDisk() + if err != nil { + panic(err) + } + + doUpgrade := upgradeInfo.Name == UpgradeName_Mainnet || + upgradeInfo.Name == UpgradeName_Testnet + + if doUpgrade && !app.upgradeKeeper.IsSkipHeight(upgradeInfo.Height) { + storeUpgrades := storetypes.StoreUpgrades{ + Added: []string{ + precisebanktypes.ModuleName, + }, + } + + // configure store loader that checks if version == upgradeHeight and applies store upgrades + app.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, &storeUpgrades)) + } +} + +// upgradeHandler returns an UpgradeHandler for the given upgrade parameters. +func upgradeHandler( + app App, + name string, +) upgradetypes.UpgradeHandler { + return func( + ctx sdk.Context, + plan upgradetypes.Plan, + fromVM module.VersionMap, + ) (module.VersionMap, error) { + logger := app.Logger() + logger.Info(fmt.Sprintf("running %s upgrade handler", name)) + + // Run migrations for all modules and return new consensus version map. + versionMap, err := app.mm.RunMigrations(ctx, app.configurator, fromVM) + if err != nil { + return nil, err + } + + logger.Info("completed store migrations") + + // Migration of fractional balances from x/evmutil to x/precisebank + if err := MigrateEvmutilToPrecisebank( + ctx, + app.accountKeeper, + app.bankKeeper, + app.evmutilKeeper, + app.precisebankKeeper, + ); err != nil { + return nil, err + } + + logger.Info("completed x/evmutil to x/precisebank migration") + + return versionMap, nil + } +} + +// MigrateEvmutilToPrecisebank migrates all required state from x/evmutil to +// x/precisebank and ensures the resulting state is correct. +// This migrates the following state: +// - Fractional balances +// - Fractional balance reserve +// Initializes the following state in x/precisebank: +// - Remainder amount +func MigrateEvmutilToPrecisebank( + ctx sdk.Context, + accountKeeper evmutiltypes.AccountKeeper, + bankKeeper bankkeeper.Keeper, + evmutilKeeper evmutilkeeper.Keeper, + precisebankKeeper precisebankkeeper.Keeper, +) error { + logger := ctx.Logger() + + aggregateSum, err := TransferFractionalBalances( + ctx, + evmutilKeeper, + precisebankKeeper, + ) + if err != nil { + return fmt.Errorf("fractional balances transfer: %w", err) + } + logger.Info( + "fractional balances transferred from x/evmutil to x/precisebank", + "aggregate sum", aggregateSum, + ) + + remainder := InitializeRemainder(ctx, precisebankKeeper, aggregateSum) + logger.Info("remainder amount initialized in x/precisebank", "remainder", remainder) + + // Migrate fractional balances, reserve, and ensure reserve fully backs all + // fractional balances. + if err := TransferFractionalBalanceReserve( + ctx, + accountKeeper, + bankKeeper, + precisebankKeeper, + ); err != nil { + return fmt.Errorf("reserve transfer: %w", err) + } + + return nil +} + +// TransferFractionalBalances migrates fractional balances from x/evmutil to +// x/precisebank. It sets the fractional balance in x/precisebank and deletes +// the account from x/evmutil. Returns the aggregate sum of all fractional +// balances. +func TransferFractionalBalances( + ctx sdk.Context, + evmutilKeeper evmutilkeeper.Keeper, + precisebankKeeper precisebankkeeper.Keeper, +) (sdkmath.Int, error) { + aggregateSum := sdkmath.ZeroInt() + + var iterErr error + + evmutilKeeper.IterateAllAccounts(ctx, func(acc evmutiltypes.Account) bool { + // Set account balance in x/precisebank + precisebankKeeper.SetFractionalBalance(ctx, acc.Address, acc.Balance) + + // Delete account from x/evmutil + iterErr := evmutilKeeper.SetAccount(ctx, evmutiltypes.Account{ + Address: acc.Address, + // Set balance to 0 to delete it + Balance: sdkmath.ZeroInt(), + }) + + // Halt iteration if there was an error + if iterErr != nil { + return true + } + + // Aggregate sum of all fractional balances + aggregateSum = aggregateSum.Add(acc.Balance) + + // Continue iterating + return false + }) + + return aggregateSum, iterErr +} + +// InitializeRemainder initializes the remainder amount in x/precisebank. It +// calculates the remainder amount that is needed to ensure that the sum of all +// fractional balances is a multiple of the conversion factor. The remainder +// amount is stored in the store and returned. +func InitializeRemainder( + ctx sdk.Context, + precisebankKeeper precisebankkeeper.Keeper, + aggregateSum sdkmath.Int, +) sdkmath.Int { + // Extra fractional coins that exceed the conversion factor. + // This extra + remainder should equal the conversion factor to ensure + // (sum(fBalances) + remainder) % conversionFactor = 0 + extraFractionalAmount := aggregateSum.Mod(precisebanktypes.ConversionFactor()) + remainder := precisebanktypes.ConversionFactor(). + Sub(extraFractionalAmount). + // Mod conversion factor to ensure remainder is valid. + // If extraFractionalAmount is a multiple of conversion factor, the + // remainder is 0. + Mod(precisebanktypes.ConversionFactor()) + + // Panics if the remainder is invalid. In a correct chain state and only + // mint/burns due to transfers, this would be 0. + precisebankKeeper.SetRemainderAmount(ctx, remainder) + + return remainder +} + +// TransferFractionalBalanceReserve migrates the fractional balance reserve from +// x/evmutil to x/precisebank. It transfers the reserve balance from x/evmutil +// to x/precisebank and ensures that the reserve fully backs all fractional +// balances. It mints or burns coins to back the fractional balances exactly. +func TransferFractionalBalanceReserve( + ctx sdk.Context, + accountKeeper evmutiltypes.AccountKeeper, + bankKeeper bankkeeper.Keeper, + precisebankKeeper precisebankkeeper.Keeper, +) error { + logger := ctx.Logger() + + // Transfer x/evmutil reserve to x/precisebank. + evmutilAddr := accountKeeper.GetModuleAddress(evmutiltypes.ModuleName) + reserveBalance := bankKeeper.GetBalance(ctx, evmutilAddr, precisebanktypes.IntegerCoinDenom) + + if err := bankKeeper.SendCoinsFromModuleToModule( + ctx, + evmutiltypes.ModuleName, // from x/evmutil + precisebanktypes.ModuleName, // to x/precisebank + sdk.NewCoins(reserveBalance), + ); err != nil { + return fmt.Errorf("failed to transfer reserve from x/evmutil to x/precisebank: %w", err) + } + + logger.Info(fmt.Sprintf("transferred reserve balance: %s", reserveBalance)) + + // Ensure x/precisebank reserve fully backs all fractional balances. + totalFractionalBalances := precisebankKeeper.GetTotalSumFractionalBalances(ctx) + + // Does NOT ensure state is correct, total fractional balances should be a + // multiple of conversion factor but is not guaranteed due to the remainder. + // Remainder initialization is handled by InitializeRemainder. + + // Determine how much the reserve is off by, e.g. unbacked amount + expectedReserveBalance := totalFractionalBalances.Quo(precisebanktypes.ConversionFactor()) + + // If there is a remainder (totalFractionalBalances % conversionFactor != 0), + // then expectedReserveBalance is rounded up to the nearest integer. + if totalFractionalBalances.Mod(precisebanktypes.ConversionFactor()).IsPositive() { + expectedReserveBalance = expectedReserveBalance.Add(sdkmath.OneInt()) + } + + unbackedAmount := expectedReserveBalance.Sub(reserveBalance.Amount) + logger.Info(fmt.Sprintf("total account fractional balances: %s", totalFractionalBalances)) + + // Three possible cases: + // 1. Reserve is not enough, mint coins to back the fractional balances + // 2. Reserve is too much, burn coins to back the fractional balances exactly + // 3. Reserve is exactly enough, no action needed + if unbackedAmount.IsPositive() { + coins := sdk.NewCoins(sdk.NewCoin(precisebanktypes.IntegerCoinDenom, unbackedAmount)) + if err := bankKeeper.MintCoins(ctx, precisebanktypes.ModuleName, coins); err != nil { + return fmt.Errorf("failed to mint extra reserve coins: %w", err) + } + + logger.Info(fmt.Sprintf("unbacked amount minted to reserve: %s", unbackedAmount)) + } else if unbackedAmount.IsNegative() { + coins := sdk.NewCoins(sdk.NewCoin(precisebanktypes.IntegerCoinDenom, unbackedAmount.Neg())) + if err := bankKeeper.BurnCoins(ctx, precisebanktypes.ModuleName, coins); err != nil { + return fmt.Errorf("failed to burn extra reserve coins: %w", err) + } + + logger.Info(fmt.Sprintf("extra reserve amount burned: %s", unbackedAmount.Neg())) + } else { + logger.Info("reserve exactly backs fractional balances, no mint/burn needed") + } + + return nil +} diff --git a/app/upgrades_test.go b/app/upgrades_test.go new file mode 100644 index 000000000..56c3a26b7 --- /dev/null +++ b/app/upgrades_test.go @@ -0,0 +1,434 @@ +package app_test + +import ( + "strconv" + "testing" + "time" + + sdkmath "cosmossdk.io/math" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + evmutiltypes "github.com/kava-labs/kava/x/evmutil/types" + precisebankkeeper "github.com/kava-labs/kava/x/precisebank/keeper" + precisebanktypes "github.com/kava-labs/kava/x/precisebank/types" + "github.com/stretchr/testify/require" +) + +func TestMigrateEvmutilToPrecisebank(t *testing.T) { + // Full test case with all components together + tests := []struct { + name string + initialReserve sdkmath.Int + fractionalBalances []sdkmath.Int + }{ + { + "no fractional balances", + sdkmath.NewInt(0), + []sdkmath.Int{}, + }, + { + "sufficient reserve, 0 remainder", + // Accounts adding up to 2 int units, same as reserve + sdkmath.NewInt(2), + []sdkmath.Int{ + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + }, + }, + { + "insufficient reserve, 0 remainder", + // Accounts adding up to 2 int units, but only 1 int unit in reserve + sdkmath.NewInt(1), + []sdkmath.Int{ + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + }, + }, + { + "excess reserve, 0 remainder", + // Accounts adding up to 2 int units, but 3 int unit in reserve + sdkmath.NewInt(3), + []sdkmath.Int{ + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + }, + }, + { + "sufficient reserve, non-zero remainder", + // Accounts adding up to 1.5 int units, same as reserve + sdkmath.NewInt(2), + []sdkmath.Int{ + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + }, + }, + { + "insufficient reserve, non-zero remainder", + // Accounts adding up to 1.5 int units, less than reserve, + // Reserve should be 2 and remainder 0.5 + sdkmath.NewInt(1), + []sdkmath.Int{ + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + }, + }, + { + "excess reserve, non-zero remainder", + // Accounts adding up to 1.5 int units, 3 int units in reserve + sdkmath.NewInt(3), + []sdkmath.Int{ + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tApp := app.NewTestApp() + tApp.InitializeFromGenesisStates() + ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: time.Now()}) + + ak := tApp.GetAccountKeeper() + bk := tApp.GetBankKeeper() + evmuk := tApp.GetEvmutilKeeper() + pbk := tApp.GetPrecisebankKeeper() + + reserveCoin := sdk.NewCoin(precisebanktypes.IntegerCoinDenom, tt.initialReserve) + err := bk.MintCoins(ctx, evmutiltypes.ModuleName, sdk.NewCoins(reserveCoin)) + require.NoError(t, err) + + oldReserveAddr := tApp.GetAccountKeeper().GetModuleAddress(evmutiltypes.ModuleName) + newReserveAddr := tApp.GetAccountKeeper().GetModuleAddress(precisebanktypes.ModuleName) + + // Double check balances + oldReserveBalance := bk.GetBalance(ctx, oldReserveAddr, precisebanktypes.IntegerCoinDenom) + newReserveBalance := bk.GetBalance(ctx, newReserveAddr, precisebanktypes.IntegerCoinDenom) + + require.Equal(t, tt.initialReserve, oldReserveBalance.Amount, "initial x/evmutil reserve balance") + require.True(t, newReserveBalance.IsZero(), "empty initial new reserve") + + // Set accounts + for i, balance := range tt.fractionalBalances { + addr := sdk.AccAddress([]byte(strconv.Itoa(i))) + + err := evmuk.SetBalance(ctx, addr, balance) + require.NoError(t, err) + } + + // Run full x/evmutil -> x/precisebank migration + err = app.MigrateEvmutilToPrecisebank( + ctx, + ak, + bk, + evmuk, + pbk, + ) + require.NoError(t, err) + + // Check old reserve is empty + oldReserveBalanceAfter := bk.GetBalance(ctx, oldReserveAddr, precisebanktypes.IntegerCoinDenom) + require.True(t, oldReserveBalanceAfter.IsZero(), "old reserve should be empty") + + // Check new reserve fully backs fractional balances + newReserveBalanceAfter := bk.GetBalance(ctx, newReserveAddr, precisebanktypes.IntegerCoinDenom) + fractionalBalanceTotal := pbk.GetTotalSumFractionalBalances(ctx) + remainder := pbk.GetRemainderAmount(ctx) + + expectedReserveBal := fractionalBalanceTotal.Add(remainder) + require.Equal( + t, + expectedReserveBal, + newReserveBalanceAfter.Amount.Mul(precisebanktypes.ConversionFactor()), + "new reserve should equal total fractional balances", + ) + + // Check balances are deleted in evmutil and migrated to precisebank + for i := range tt.fractionalBalances { + addr := sdk.AccAddress([]byte(strconv.Itoa(i))) + acc := evmuk.GetAccount(ctx, addr) + require.Nil(t, acc, "account should be deleted") + + balance := pbk.GetFractionalBalance(ctx, addr) + require.Equal(t, tt.fractionalBalances[i], balance, "balance should be migrated") + } + + // Checks balances valid and remainder + res, stop := precisebankkeeper.AllInvariants(pbk)(ctx) + require.Falsef(t, stop, "invariants should pass: %s", res) + }) + } +} + +func TestTransferFractionalBalances(t *testing.T) { + tests := []struct { + name string + fractionalBalances []sdkmath.Int + }{ + { + "no fractional balances", + []sdkmath.Int{}, + }, + { + "balanced fractional balances", + []sdkmath.Int{ + // 4 accounts + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + }, + }, + { + "unbalanced balances", + []sdkmath.Int{ + // 3 accounts + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tApp := app.NewTestApp() + tApp.InitializeFromGenesisStates() + ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: time.Now()}) + + evmutilk := tApp.GetEvmutilKeeper() + pbk := tApp.GetPrecisebankKeeper() + + for i, balance := range tt.fractionalBalances { + addr := sdk.AccAddress([]byte(strconv.Itoa(i))) + + err := evmutilk.SetBalance(ctx, addr, balance) + require.NoError(t, err) + } + + // Run balance transfer + aggregateSum, err := app.TransferFractionalBalances( + ctx, + evmutilk, + pbk, + ) + require.NoError(t, err) + + // Check balances are deleted in evmutil and migrated to precisebank + sum := sdkmath.ZeroInt() + for i := range tt.fractionalBalances { + sum = sum.Add(tt.fractionalBalances[i]) + + addr := sdk.AccAddress([]byte(strconv.Itoa(i))) + acc := evmutilk.GetAccount(ctx, addr) + require.Nil(t, acc, "account should be deleted") + + balance := pbk.GetFractionalBalance(ctx, addr) + require.Equal(t, tt.fractionalBalances[i], balance, "balance should be migrated") + } + + require.Equal(t, sum, aggregateSum, "aggregate sum should be correct") + }) + } +} + +func TestInitializeRemainder(t *testing.T) { + tests := []struct { + name string + giveAggregateSum sdkmath.Int + wantRemainder sdkmath.Int + }{ + { + "0 remainder, 1ukava", + precisebanktypes.ConversionFactor(), + sdkmath.NewInt(0), + }, + { + "0 remainder, multiple ukava", + precisebanktypes.ConversionFactor().MulRaw(5), + sdkmath.NewInt(0), + }, + { + "non-zero remainder, min", + precisebanktypes.ConversionFactor().SubRaw(1), + sdkmath.NewInt(1), + }, + { + "non-zero remainder, max", + sdkmath.NewInt(1), + precisebanktypes.ConversionFactor().SubRaw(1), + }, + { + "non-zero remainder, half", + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tApp := app.NewTestApp() + tApp.InitializeFromGenesisStates() + + pbk := tApp.GetPrecisebankKeeper() + + ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: time.Now()}) + + remainder := app.InitializeRemainder( + ctx, + tApp.GetPrecisebankKeeper(), + tt.giveAggregateSum, + ) + require.Equal(t, tt.wantRemainder, remainder) + + // Check actual state + remainderAfter := pbk.GetRemainderAmount(ctx) + require.Equal(t, tt.wantRemainder, remainderAfter) + + // Not checking invariants here since it requires actual balance state + aggregateSumWithRemainder := tt.giveAggregateSum.Add(remainder) + require.True( + t, + aggregateSumWithRemainder. + Mod(precisebanktypes.ConversionFactor()). + IsZero(), + "remainder + aggregate sum should be a multiple of the conversion factor", + ) + }) + } +} + +func TestTransferFractionalBalanceReserve(t *testing.T) { + tests := []struct { + name string + initialReserve sdk.Coin + fractionalBalances []sdkmath.Int + }{ + { + "balanced reserve, no remainder", + sdk.NewCoin(precisebanktypes.IntegerCoinDenom, sdk.NewInt(1)), + []sdkmath.Int{ + // 2 accounts + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + }, + }, + { + "insufficient reserve", + sdk.NewCoin(precisebanktypes.IntegerCoinDenom, sdk.NewInt(1)), + []sdkmath.Int{ + // 4 accounts, total 2 int units + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + }, + }, + { + "extra reserve funds", + sdk.NewCoin(precisebanktypes.IntegerCoinDenom, sdk.NewInt(2)), + []sdkmath.Int{ + // 2 accounts, total 1 int units + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + }, + }, + { + "insufficient reserve, with remainder", + sdk.NewCoin(precisebanktypes.IntegerCoinDenom, sdk.NewInt(1)), + []sdkmath.Int{ + // 5 accounts, total 2.5 int units + // Expected 3 int units in reserve, 0.5 remainder + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + }, + }, + { + "extra reserve funds, with remainder", + sdk.NewCoin(precisebanktypes.IntegerCoinDenom, sdk.NewInt(3)), + []sdkmath.Int{ + // 3 accounts, total 1.5 int units. + // Expected 2 int units in reserve, 0.5 remainder + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + precisebanktypes.ConversionFactor().QuoRaw(2), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tApp := app.NewTestApp() + tApp.InitializeFromGenesisStates() + ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: time.Now()}) + + bk := tApp.GetBankKeeper() + pbk := tApp.GetPrecisebankKeeper() + err := bk.MintCoins(ctx, evmutiltypes.ModuleName, sdk.NewCoins(tt.initialReserve)) + require.NoError(t, err) + + oldReserveAddr := tApp.GetAccountKeeper().GetModuleAddress(evmutiltypes.ModuleName) + newReserveAddr := tApp.GetAccountKeeper().GetModuleAddress(precisebanktypes.ModuleName) + + // Double check balances + oldReserveBalance := bk.GetBalance(ctx, oldReserveAddr, precisebanktypes.IntegerCoinDenom) + newReserveBalance := bk.GetBalance(ctx, newReserveAddr, precisebanktypes.IntegerCoinDenom) + + require.Equal(t, tt.initialReserve, oldReserveBalance) + require.True(t, newReserveBalance.IsZero(), "empty initial new reserve") + + for i, balance := range tt.fractionalBalances { + addr := sdk.AccAddress([]byte{byte(i)}) + + require.NotPanics(t, func() { + pbk.SetFractionalBalance(ctx, addr, balance) + }, "given fractional balances should be valid") + } + + // Run reserve migration + err = app.TransferFractionalBalanceReserve( + ctx, + tApp.GetAccountKeeper(), + bk, + tApp.GetPrecisebankKeeper(), + ) + require.NoError(t, err) + + // Check old reserve is empty + oldReserveBalanceAfter := bk.GetBalance(ctx, oldReserveAddr, precisebanktypes.IntegerCoinDenom) + require.True(t, oldReserveBalanceAfter.IsZero(), "old reserve should be empty") + + // Check new reserve fully backs fractional balances + newReserveBalanceAfter := bk.GetBalance(ctx, newReserveAddr, precisebanktypes.IntegerCoinDenom) + fractionalBalanceTotal := pbk.GetTotalSumFractionalBalances(ctx) + + expectedReserveBal := fractionalBalanceTotal. + Quo(precisebanktypes.ConversionFactor()) + + // Check if theres a remainder + if fractionalBalanceTotal.Mod(precisebanktypes.ConversionFactor()).IsPositive() { + expectedReserveBal = expectedReserveBal.Add(sdkmath.OneInt()) + } + + require.Equal( + t, + expectedReserveBal, + newReserveBalanceAfter.Amount, + "new reserve should equal total fractional balances + remainder", + ) + }) + } +} diff --git a/tests/e2e/.env b/tests/e2e/.env index fbd211dde..795f0d056 100644 --- a/tests/e2e/.env +++ b/tests/e2e/.env @@ -19,14 +19,14 @@ E2E_SKIP_SHUTDOWN=false # The following variables should be defined to run an upgrade. # E2E_INCLUDE_AUTOMATED_UPGRADE when true enables the automated upgrade & corresponding tests in the suite. -E2E_INCLUDE_AUTOMATED_UPGRADE=false +E2E_INCLUDE_AUTOMATED_UPGRADE=true # E2E_KAVA_UPGRADE_NAME is the name of the upgrade that must be in the current local image. -E2E_KAVA_UPGRADE_NAME= +E2E_KAVA_UPGRADE_NAME=v0.27.0 # E2E_KAVA_UPGRADE_HEIGHT is the height at which the upgrade will be applied. # If IBC tests are enabled this should be >30. Otherwise, this should be >10. -E2E_KAVA_UPGRADE_HEIGHT= +E2E_KAVA_UPGRADE_HEIGHT=35 # E2E_KAVA_UPGRADE_BASE_IMAGE_TAG is the tag of the docker image the chain should upgrade from. -E2E_KAVA_UPGRADE_BASE_IMAGE_TAG= +E2E_KAVA_UPGRADE_BASE_IMAGE_TAG=v0.26.0-goleveldb # E2E_KAVA_ERC20_ADDRESS is the address of a pre-deployed ERC20 token with the following properties: # - the E2E_KAVA_FUNDED_ACCOUNT_MNEMONIC has nonzero balance diff --git a/tests/e2e/e2e_upgrade_handler_test.go b/tests/e2e/e2e_upgrade_handler_test.go index 2a3b44647..f7d77f4c4 100644 --- a/tests/e2e/e2e_upgrade_handler_test.go +++ b/tests/e2e/e2e_upgrade_handler_test.go @@ -1,18 +1,102 @@ package e2e_test import ( - "fmt" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + precisebanktypes "github.com/kava-labs/kava/x/precisebank/types" ) -// TestUpgradeHandler can be used to run tests post-upgrade. If an upgrade is enabled, all tests -// are run against the upgraded chain. However, this file is a good place to consolidate all -// acceptance tests for a given set of upgrade handlers. -func (suite *IntegrationTestSuite) TestUpgradeHandler() { +func (suite *IntegrationTestSuite) TestUpgrade_PreciseBankReserveTransfer() { suite.SkipIfUpgradeDisabled() - fmt.Println("An upgrade has run!") - suite.True(true) - // Uncomment & use these contexts to compare chain state before & after the upgrade occurs. - // beforeUpgradeCtx := suite.Kava.Grpc.CtxAtHeight(suite.UpgradeHeight - 1) - // afterUpgradeCtx := suite.Kava.Grpc.CtxAtHeight(suite.UpgradeHeight) + beforeUpgradeCtx := suite.Kava.Grpc.CtxAtHeight(suite.UpgradeHeight - 1) + afterUpgradeCtx := suite.Kava.Grpc.CtxAtHeight(suite.UpgradeHeight) + + grpcClient := suite.Kava.Grpc + + // ----------------------------- + // Get initial reserve balances + evmutilAddr := "kava1w9vxuke5dz6hyza2j932qgmxltnfxwl78u920k" + precisebankAddr := "kava12yfe2jaupmtjruwxsec7hg7er60fhaa4uz7ffl" + + previousEvmutilBalRes, err := grpcClient.Query.Bank.Balance(beforeUpgradeCtx, &banktypes.QueryBalanceRequest{ + Address: evmutilAddr, + Denom: precisebanktypes.IntegerCoinDenom, + }) + suite.Require().NoError(err) + suite.Require().NotNil(previousEvmutilBalRes.Balance) + suite.Require().True( + previousEvmutilBalRes.Balance.Amount.IsPositive(), + "should have reserve balance before upgrade", + ) + + previousPrecisebankBalRes, err := grpcClient.Query.Bank.Balance(beforeUpgradeCtx, &banktypes.QueryBalanceRequest{ + Address: precisebankAddr, + Denom: precisebanktypes.IntegerCoinDenom, + }) + suite.Require().NoError(err) + suite.Require().NotNil(previousPrecisebankBalRes.Balance) + suite.Require().True( + previousPrecisebankBalRes.Balance.Amount.IsZero(), + "should be empty before upgrade", + ) + + suite.T().Logf("x/evmutil balances before upgrade: %s", previousEvmutilBalRes.Balance) + suite.T().Logf("x/precisebank balances before upgrade: %s", previousPrecisebankBalRes.Balance) + + // ----------------------------- + // After upgrade + // - Check reserve balance transfer + // - Check reserve fully backs fractional amounts + afterEvmutilBalRes, err := grpcClient.Query.Bank.Balance(afterUpgradeCtx, &banktypes.QueryBalanceRequest{ + Address: evmutilAddr, + Denom: precisebanktypes.IntegerCoinDenom, + }) + suite.Require().NoError(err) + suite.Require().NotNil(afterEvmutilBalRes.Balance) + suite.Require().Truef( + afterEvmutilBalRes.Balance.Amount.IsZero(), + "should have transferred all reserve balance to precisebank, expected 0 but got %s", + afterEvmutilBalRes.Balance, + ) + + afterPrecisebankBalRes, err := grpcClient.Query.Bank.Balance(afterUpgradeCtx, &banktypes.QueryBalanceRequest{ + Address: precisebankAddr, + Denom: precisebanktypes.IntegerCoinDenom, + }) + suite.Require().NoError(err) + suite.Require().NotNil(afterPrecisebankBalRes.Balance) + // 2 total in reserve- genesis.json has 5 accounts with fractional balances + // totalling 2 integer coins + suite.Require().Equal(int64(2), afterPrecisebankBalRes.Balance.Amount.Int64()) + + suite.T().Logf("x/evmutil balances after upgrade: %s", afterEvmutilBalRes.Balance) + suite.T().Logf("x/precisebank balances after upgrade: %s", afterPrecisebankBalRes.Balance) + + sumFractional, err := grpcClient.Query.Precisebank.TotalFractionalBalances( + afterUpgradeCtx, + &precisebanktypes.QueryTotalFractionalBalancesRequest{}, + ) + suite.Require().NoError(err) + + suite.Require().Equal( + sumFractional.Total.Amount, + afterPrecisebankBalRes.Balance.Amount.Mul(precisebanktypes.ConversionFactor()), + "reserve should match exactly sum fractional balances", + ) + + // Check remainder + total fractional balances = reserve balance + remainderRes, err := grpcClient.Query.Precisebank.Remainder( + afterUpgradeCtx, + &precisebanktypes.QueryRemainderRequest{}, + ) + suite.Require().NoError(err) + + sumFractionalAndRemainder := sumFractional.Total.Add(remainderRes.Remainder) + reserveBalanceExtended := afterPrecisebankBalRes.Balance.Amount.Mul(precisebanktypes.ConversionFactor()) + + suite.Require().Equal( + sumFractionalAndRemainder.Amount, + reserveBalanceExtended, + "remainder + sum(fractional balances) should be = reserve balance", + ) }