diff --git a/migrate/cmd_test.go b/migrate/cmd_test.go index a2f98529..22750cc8 100644 --- a/migrate/cmd_test.go +++ b/migrate/cmd_test.go @@ -24,7 +24,8 @@ func newCmdContext() context.Context { config := app.MakeEncodingConfig() clientCtx := client.Context{}. WithCodec(config.Marshaler). - WithLegacyAmino(config.Amino) + WithLegacyAmino(config.Amino). + WithInterfaceRegistry(config.InterfaceRegistry) ctx := context.Background() ctx = context.WithValue(ctx, client.ClientContextKey, &clientCtx) return ctx diff --git a/migrate/utils/periodic_vesting.go b/migrate/utils/periodic_vesting.go new file mode 100644 index 00000000..fca9b4df --- /dev/null +++ b/migrate/utils/periodic_vesting.go @@ -0,0 +1,59 @@ +package utils + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + v040vesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" +) + +// ResetPeriodicVestingAccount resets a periodic vesting account to a new start +// time. The account is modified in place, and vesting periods before the new +// start time are removed from the account. +func ResetPeriodicVestingAccount(vacc *v040vesting.PeriodicVestingAccount, startTime time.Time) { + currentPeriod := vacc.StartTime + + newOriginalVesting := sdk.Coins{} + newStartTime := startTime.Unix() + newPeriods := v040vesting.Periods{} + + for _, period := range vacc.VestingPeriods { + currentPeriod = currentPeriod + period.Length + + // Periods less than the newStartTime are still vesting, + // so adjust their length and add them to the newPeriods + if newStartTime < currentPeriod { + + // adjust the length of the first vesting period + // to be relative to the new start time + if len(newPeriods) == 0 { + period.Length = currentPeriod - newStartTime + } + + newOriginalVesting = newOriginalVesting.Add(period.Amount...) + newPeriods = append(newPeriods, period) + } + } + + // If the new original vesting amount is less than the delegated vesting amount, set delegated vesting + // to the new original vesting amount, and add the difference to the delegated free amount + for _, delegatedVestingCoin := range vacc.DelegatedVesting { + newDelegatedVestingCoin := sdk.NewCoin(delegatedVestingCoin.Denom, sdk.MinInt(delegatedVestingCoin.Amount, newOriginalVesting.AmountOf(delegatedVestingCoin.Denom))) + delegationAdjustment := delegatedVestingCoin.Sub(newDelegatedVestingCoin) + + if !delegationAdjustment.IsZero() { + vacc.DelegatedVesting = vacc.DelegatedVesting.Sub(sdk.NewCoins(delegationAdjustment)) + vacc.DelegatedFree = vacc.DelegatedFree.Add(delegationAdjustment) + } + } + + // update vesting account + vacc.StartTime = newStartTime + vacc.OriginalVesting = newOriginalVesting + vacc.VestingPeriods = newPeriods + + // ensure end time is >= start time + if vacc.StartTime >= vacc.EndTime { + vacc.EndTime = vacc.StartTime + } +} diff --git a/migrate/utils/periodic_vesting_reset_test.go b/migrate/utils/periodic_vesting_reset_test.go new file mode 100644 index 00000000..f1ca45a5 --- /dev/null +++ b/migrate/utils/periodic_vesting_reset_test.go @@ -0,0 +1,231 @@ +package utils + +import ( + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + "github.com/stretchr/testify/assert" +) + +func createVestingAccount(balance sdk.Coins, vestingStart time.Time, vestingPeriods vestingtypes.Periods) *vestingtypes.PeriodicVestingAccount { + key := secp256k1.GenPrivKey() + pub := key.PubKey() + addr := sdk.AccAddress(pub.Address()) + acc := authtypes.NewBaseAccount(addr, pub, 1, 1) + + originalVesting := sdk.NewCoins() + for _, vp := range vestingPeriods { + originalVesting = originalVesting.Add(vp.Amount...) + } + + return vestingtypes.NewPeriodicVestingAccount(acc, originalVesting, vestingStart.Unix(), vestingPeriods) +} + +func TestResetPeriodVestingAccount_NoVestingPeriods(t *testing.T) { + vestingStartTime := time.Now().Add(-1 * time.Hour) + vacc := createVestingAccount(sdk.Coins{}, vestingStartTime, vestingtypes.Periods{}) + + newVestingStartTime := vestingStartTime.Add(time.Hour) + + ResetPeriodicVestingAccount(vacc, newVestingStartTime) + + assert.Equal(t, sdk.Coins{}, vacc.OriginalVesting, "expected original vesting to be zero") + assert.Equal(t, newVestingStartTime.Unix(), vacc.StartTime, "expected vesting start time to be updated") + assert.Equal(t, newVestingStartTime.Unix(), vacc.EndTime, "expected vesting end time to be updated") + assert.Equal(t, []vestingtypes.Period{}, vacc.VestingPeriods, "expected vesting periods to be empty") +} + +func TestResetPeriodVestingAccount_SingleVestingPeriod_Vested(t *testing.T) { + balance := sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1e6))) + vestingStartTime := time.Now().Add(-30 * 24 * time.Hour) // 30 days in past + + periods := vestingtypes.Periods{ + vestingtypes.Period{ + Length: 15 * 24 * 60 * 60, // 15 days (-15 days in past) + Amount: balance, + }, + } + + vacc := createVestingAccount(balance, vestingStartTime, periods) + + newVestingStartTime := vestingStartTime.Add(30 * 24 * time.Hour) + + ResetPeriodicVestingAccount(vacc, newVestingStartTime) + + assert.Equal(t, sdk.Coins{}, vacc.OriginalVesting, "expected original vesting to be zero") + assert.Equal(t, newVestingStartTime.Unix(), vacc.StartTime, "expected vesting start time to be updated") + assert.Equal(t, newVestingStartTime.Unix(), vacc.EndTime, "expected vesting end time to be updated") + assert.Equal(t, []vestingtypes.Period{}, vacc.VestingPeriods, "expected vesting periods to be empty") +} + +func TestResetPeriodVestingAccount_SingleVestingPeriod_Vesting(t *testing.T) { + balance := sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1e6))) + vestingStartTime := time.Now().Add(-30 * 24 * time.Hour) // 30 days in past + + periods := vestingtypes.Periods{ + vestingtypes.Period{ + Length: 45 * 24 * 60 * 60, // 45 days + Amount: balance, + }, + } + + vacc := createVestingAccount(balance, vestingStartTime, periods) + + newVestingStartTime := vestingStartTime.Add(30 * 24 * time.Hour) + + ResetPeriodicVestingAccount(vacc, newVestingStartTime) + + // new period length 15 days + expectedEndtime := newVestingStartTime.Add(15 * 24 * time.Hour).Unix() + // new period length changed, amount unchanged + expectedPeriods := []vestingtypes.Period{ + vestingtypes.Period{ + Length: 15 * 24 * 60 * 60, // 15 days + Amount: balance, + }, + } + + assert.Equal(t, balance, vacc.OriginalVesting, "expected original vesting to be unchanged") + assert.Equal(t, newVestingStartTime.Unix(), vacc.StartTime, "expected vesting start time to be updated") + assert.Equal(t, expectedEndtime, vacc.EndTime, "expected vesting end time end at last period") + assert.Equal(t, expectedPeriods, vacc.VestingPeriods, "expected vesting periods to be updated") +} + +func TestResetPeriodVestingAccount_SingleVestingPeriod_ExactStartTime(t *testing.T) { + balance := sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1e6))) + vestingStartTime := time.Now().Add(-30 * 24 * time.Hour) // 30 days in past + + periods := vestingtypes.Periods{ + vestingtypes.Period{ + Length: 30 * 24 * 60 * 60, // 30 days - exact on the start time + Amount: balance, + }, + } + + vacc := createVestingAccount(balance, vestingStartTime, periods) + + newVestingStartTime := vestingStartTime.Add(30 * 24 * time.Hour) + + ResetPeriodicVestingAccount(vacc, newVestingStartTime) + + // new period length is 0 + expectedEndtime := newVestingStartTime.Unix() + // new period length changed, amount unchanged + expectedPeriods := []vestingtypes.Period{} + + assert.Equal(t, sdk.Coins{}, vacc.OriginalVesting, "expected original vesting to be unchanged") + assert.Equal(t, newVestingStartTime.Unix(), vacc.StartTime, "expected vesting start time to be updated") + assert.Equal(t, expectedEndtime, vacc.EndTime, "expected vesting end time end at last period") + assert.Equal(t, expectedPeriods, vacc.VestingPeriods, "expected vesting periods to be updated") +} + +func TestResetPeriodVestingAccount_MultiplePeriods(t *testing.T) { + balance := sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(4e6))) + vestingStartTime := time.Now().Add(-30 * 24 * time.Hour) // 30 days in past + + periods := vestingtypes.Periods{ + vestingtypes.Period{ + Length: 15 * 24 * 60 * 60, // -15 days - vested + Amount: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1e6))), + }, + vestingtypes.Period{ + Length: 15 * 24 * 60 * 60, // 0 days - exact on the start time + Amount: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1e6))), + }, + vestingtypes.Period{ + Length: 15 * 24 * 60 * 60, // +15 days - vesting + Amount: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1e6))), + }, + vestingtypes.Period{ + Length: 15 * 24 * 60 * 60, // +30 days - vesting + Amount: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1e6))), + }, + } + + vacc := createVestingAccount(balance, vestingStartTime, periods) + + newVestingStartTime := vestingStartTime.Add(30 * 24 * time.Hour) + + ResetPeriodicVestingAccount(vacc, newVestingStartTime) + + // new period length 15 days + expectedEndtime := newVestingStartTime.Add(30 * 24 * time.Hour).Unix() + // new period length changed, amount unchanged + expectedPeriods := []vestingtypes.Period{ + vestingtypes.Period{ + Length: 15 * 24 * 60 * 60, // 15 days + Amount: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1e6))), + }, + vestingtypes.Period{ + Length: 15 * 24 * 60 * 60, // 15 days + Amount: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1e6))), + }, + } + + assert.Equal(t, sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(2e6))), vacc.OriginalVesting, "expected original vesting to be updated") + assert.Equal(t, newVestingStartTime.Unix(), vacc.StartTime, "expected vesting start time to be updated") + assert.Equal(t, expectedEndtime, vacc.EndTime, "expected vesting end time end at last period") + assert.Equal(t, expectedPeriods, vacc.VestingPeriods, "expected vesting periods to be updated") +} + +func TestResetPeriodVestingAccount_DelegatedVesting_GreaterThanVesting(t *testing.T) { + balance := sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(3e6))) + vestingStartTime := time.Now().Add(-30 * 24 * time.Hour) // 30 days in past + + periods := vestingtypes.Periods{ + vestingtypes.Period{ + Length: 15 * 24 * 60 * 60, // -15 days - vested + Amount: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1e6))), + }, + vestingtypes.Period{ + Length: 15 * 24 * 60 * 60, // 0 days - exact on the start time + Amount: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1e6))), + }, + vestingtypes.Period{ + Length: 15 * 24 * 60 * 60, // +15 days - vesting + Amount: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1e6))), + }, + } + + vacc := createVestingAccount(balance, vestingStartTime, periods) + vacc.TrackDelegation(vestingStartTime, balance, balance) + + newVestingStartTime := vestingStartTime.Add(30 * 24 * time.Hour) + ResetPeriodicVestingAccount(vacc, newVestingStartTime) + + assert.Equal(t, sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(2e6))), vacc.DelegatedFree, "expected delegated free to be updated") + assert.Equal(t, sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1e6))), vacc.DelegatedVesting, "expected delegated vesting to be updated") +} + +func TestResetPeriodVestingAccount_DelegatedVesting_LessThanVested(t *testing.T) { + balance := sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(3e6))) + vestingStartTime := time.Now().Add(-30 * 24 * time.Hour) // 30 days in past + + periods := vestingtypes.Periods{ + vestingtypes.Period{ + Length: 15 * 24 * 60 * 60, // -15 days - vested + Amount: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1e6))), + }, + vestingtypes.Period{ + Length: 15 * 24 * 60 * 60, // 0 days - exact on the start time + Amount: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1e6))), + }, + vestingtypes.Period{ + Length: 15 * 24 * 60 * 60, // +15 days - vesting + Amount: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1e6))), + }, + } + + vacc := createVestingAccount(balance, vestingStartTime, periods) + vacc.TrackDelegation(vestingStartTime, balance, sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1e6)))) + + newVestingStartTime := vestingStartTime.Add(30 * 24 * time.Hour) + ResetPeriodicVestingAccount(vacc, newVestingStartTime) + + assert.Equal(t, sdk.Coins(nil), vacc.DelegatedFree, "expected delegrated free to be unmodified") + assert.Equal(t, sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1e6))), vacc.DelegatedVesting, "expected delegated vesting to be unmodified") +} diff --git a/migrate/v0_17/cosmos.go b/migrate/v0_17/cosmos.go new file mode 100644 index 00000000..2f41fc1b --- /dev/null +++ b/migrate/v0_17/cosmos.go @@ -0,0 +1,35 @@ +package v0_17 + +import ( + "time" + + "github.com/cosmos/cosmos-sdk/client" + + v040auth "github.com/cosmos/cosmos-sdk/x/auth/legacy/v040" + v040authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" +) + +func MigrateCosmosAppState(appState genutiltypes.AppMap, clientCtx client.Context, genesisTime time.Time) genutiltypes.AppMap { + appState = migrateV040(appState, clientCtx, genesisTime) + return appState +} + +// reset periodic vesting data for accounts +func migrateV040(appState genutiltypes.AppMap, clientCtx client.Context, genesisTime time.Time) genutiltypes.AppMap { + setConfigIfUnsealed() + + v040Codec := clientCtx.Codec + // reset periodic vesting data for accounts + if appState[v040auth.ModuleName] != nil { + // unmarshal relative source genesis application state + var authGenState v040authtypes.GenesisState + v040Codec.MustUnmarshalJSON(appState[v040auth.ModuleName], &authGenState) + + // reset periodic vesting data for accounts + appState[v040auth.ModuleName] = v040Codec.MustMarshalJSON(MigrateAuthV040(authGenState, genesisTime, clientCtx)) + } + + return appState +} diff --git a/migrate/v0_17/cosmos_auth.go b/migrate/v0_17/cosmos_auth.go new file mode 100644 index 00000000..e71591cb --- /dev/null +++ b/migrate/v0_17/cosmos_auth.go @@ -0,0 +1,67 @@ +package v0_17 + +import ( + "time" + + "github.com/cosmos/cosmos-sdk/client" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + v040auth "github.com/cosmos/cosmos-sdk/x/auth/types" + v040vesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + "github.com/kava-labs/kava/migrate/utils" +) + +// MigrateAuthV040 resets all periodic vesting accounts for a given +// v40 cosmos auth module genesis state, returning a copy of the original state where all +// periodic vesting accounts have been zeroed out. +func MigrateAuthV040(authGenState v040auth.GenesisState, genesisTime time.Time, ctx client.Context) *v040auth.GenesisState { + var anyAccounts = make([]*codectypes.Any, len(authGenState.Accounts)) + for i, anyAcc := range authGenState.Accounts { + // Only need to make modifications to vesting accounts + if anyAcc.TypeUrl != "/cosmos.vesting.v1beta1.PeriodicVestingAccount" { + anyAccounts[i] = anyAcc + continue + } + var acc v040auth.GenesisAccount + if err := ctx.InterfaceRegistry.UnpackAny(anyAcc, &acc); err != nil { + panic(err) + } + if vacc, ok := acc.(*v040vesting.PeriodicVestingAccount); ok { + vestingPeriods := make([]v040vesting.Period, len(vacc.VestingPeriods)) + for j, period := range vacc.VestingPeriods { + vestingPeriods[j] = v040vesting.Period{ + Length: period.Length, + Amount: period.Amount, + } + } + vacc := v040vesting.PeriodicVestingAccount{ + BaseVestingAccount: vacc.BaseVestingAccount, + StartTime: vacc.StartTime, + VestingPeriods: vestingPeriods, + } + + utils.ResetPeriodicVestingAccount(&vacc, genesisTime) + + // If periodic vesting account has zero periods, convert back + // to a base account + if genesisTime.Unix() >= vacc.EndTime { + any, err := codectypes.NewAnyWithValue(vacc.BaseVestingAccount.BaseAccount) + if err != nil { + panic(err) + } + anyAccounts[i] = any + continue + } + // Convert back to any + any, err := codectypes.NewAnyWithValue(&vacc) + if err != nil { + panic(err) + } + anyAccounts[i] = any + } + } + + return &v040auth.GenesisState{ + Params: authGenState.Params, + Accounts: anyAccounts, + } +} diff --git a/migrate/v0_17/migrate.go b/migrate/v0_17/migrate.go index 0feb53c9..c5f1b094 100644 --- a/migrate/v0_17/migrate.go +++ b/migrate/v0_17/migrate.go @@ -33,9 +33,10 @@ func Migrate(genDoc *tmtypes.GenesisDoc, ctx client.Context) (*tmtypes.GenesisDo var appState genutiltypes.AppMap var err error if err := json.Unmarshal(genDoc.AppState, &appState); err != nil { - return nil, fmt.Errorf("failed to marchal app state from genesis doc: %w", err) + return nil, fmt.Errorf("failed to unmarshal app state from genesis doc: %w", err) } + MigrateCosmosAppState(appState, ctx, GenesisTime) migrateAppState(appState, ctx) genDoc.AppState, err = json.Marshal(appState) diff --git a/migrate/v0_17/testdata/genesis-v17.json b/migrate/v0_17/testdata/genesis-v17.json index 785dc0b9..760df8b2 100644 --- a/migrate/v0_17/testdata/genesis-v17.json +++ b/migrate/v0_17/testdata/genesis-v17.json @@ -161,10 +161,10 @@ "delegated_vesting": [], "end_time": "1665767828" }, - "start_time": "1642608000", + "start_time": "1652202000", "vesting_periods": [ { - "length": "23159828", + "length": "13565828", "amount": [{ "denom": "swp", "amount": "14791312" }] } ]