From 7bfb79d7929516820cf86fa9ad4a19fcc9d448bc Mon Sep 17 00:00:00 2001 From: Eric Warehime Date: Wed, 15 Jan 2025 23:15:31 -0800 Subject: [PATCH] Add tests, hooks --- app/keepers/keepers.go | 21 +- tests/e2e/e2e_lsm_test.go | 2 +- tests/e2e/e2e_setup_test.go | 2 +- tests/integration/lsm_test.go | 1301 ++++++++++++++++++++++++++++++ tests/integration/test_common.go | 176 ++++ x/lsm/keeper/hooks.go | 119 +++ x/lsm/keeper/liquid_stake.go | 18 +- x/lsm/keeper/msg_server.go | 16 +- x/lsm/types/liquid_validator.go | 15 +- 9 files changed, 1638 insertions(+), 32 deletions(-) create mode 100644 tests/integration/lsm_test.go create mode 100644 tests/integration/test_common.go create mode 100644 x/lsm/keeper/hooks.go diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index 030539d27b..76f5344da6 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -276,16 +276,6 @@ func NewAppKeeper( authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) - // register the staking hooks - // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks - appKeepers.StakingKeeper.SetHooks( - stakingtypes.NewMultiStakingHooks( - appKeepers.DistrKeeper.Hooks(), - appKeepers.SlashingKeeper.Hooks(), - appKeepers.ProviderKeeper.Hooks(), - ), - ) - appKeepers.LsmKeeper = lsmkeeper.NewKeeper( appCodec, runtime.NewKVStoreService(appKeepers.keys[lsmtypes.StoreKey]), @@ -296,6 +286,17 @@ func NewAppKeeper( authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) + // register the staking hooks + // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks + appKeepers.StakingKeeper.SetHooks( + stakingtypes.NewMultiStakingHooks( + appKeepers.DistrKeeper.Hooks(), + appKeepers.SlashingKeeper.Hooks(), + appKeepers.ProviderKeeper.Hooks(), + appKeepers.LsmKeeper.Hooks(), + ), + ) + appKeepers.FeeMarketKeeper = feemarketkeeper.NewKeeper( appCodec, appKeepers.keys[feemarkettypes.StoreKey], diff --git a/tests/e2e/e2e_lsm_test.go b/tests/e2e/e2e_lsm_test.go index 08db323e0f..ca511a6b5a 100644 --- a/tests/e2e/e2e_lsm_test.go +++ b/tests/e2e/e2e_lsm_test.go @@ -53,7 +53,7 @@ func (s *IntegrationTestSuite) testLSM() { s.Require().Equal(lsmParams.Params.GlobalLiquidStakingCap, math.LegacyNewDecWithPrec(25, 2)) s.Require().Equal(lsmParams.Params.ValidatorLiquidStakingCap, math.LegacyNewDecWithPrec(50, 2)) - s.Require().Equal(lsmParams.Params.ValidatorBondFactor, math.LegacyNewDec(250)) + s.Require().Equal(lsmParams.Params.ValidatorBondFactor, math.LegacyNewDec(-1)) return true }, diff --git a/tests/e2e/e2e_setup_test.go b/tests/e2e/e2e_setup_test.go index 114c191dc8..d0c0c4dc00 100644 --- a/tests/e2e/e2e_setup_test.go +++ b/tests/e2e/e2e_setup_test.go @@ -771,7 +771,7 @@ func (s *IntegrationTestSuite) writeLiquidStakingParamsUpdateProposal(c *chain) }` propMsgBody := fmt.Sprintf(template, govAuthority, - math.LegacyNewDec(250), // validator bond factor + math.LegacyNewDec(-1), // validator bond factor math.LegacyNewDecWithPrec(25, 2), // 25 global_liquid_staking_cap math.LegacyNewDecWithPrec(50, 2), // 50 validator_liquid_staking_cap ) diff --git a/tests/integration/lsm_test.go b/tests/integration/lsm_test.go new file mode 100644 index 0000000000..d0a311abd4 --- /dev/null +++ b/tests/integration/lsm_test.go @@ -0,0 +1,1301 @@ +package integration + +import ( + "errors" + "fmt" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "cosmossdk.io/math" + + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + vesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/exported" + vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + "github.com/cosmos/cosmos-sdk/x/staking/testutil" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + lsmkeeper "github.com/cosmos/gaia/v22/x/lsm/keeper" + lsmtypes "github.com/cosmos/gaia/v22/x/lsm/types" +) + +func TestTokenizeSharesAndRedeemTokens(t *testing.T) { + t.Parallel() + f := initFixture(t) + + ctx := f.sdkCtx + + stakingKeeper := f.stakingKeeper + + liquidStakingCapStrict := math.LegacyZeroDec() + liquidStakingCapConservative := math.LegacyMustNewDecFromStr("0.8") + liquidStakingCapDisabled := math.LegacyOneDec() + + validatorBondStrict := math.LegacyOneDec() + validatorBondConservative := math.LegacyNewDec(10) + validatorBondDisabled := math.LegacyNewDec(-1) + + testCases := []struct { + name string + vestingAmount math.Int + delegationAmount math.Int + tokenizeShareAmount math.Int + redeemAmount math.Int + targetVestingDelAfterShare math.Int + targetVestingDelAfterRedeem math.Int + globalLiquidStakingCap math.LegacyDec + slashFactor math.LegacyDec + validatorLiquidStakingCap math.LegacyDec + validatorBondFactor math.LegacyDec + validatorBondDelegation bool + validatorBondDelegatorIndex int + expTokenizeErr bool + expRedeemErr bool + prevAccountDelegationExists bool + recordAccountDelegationExists bool + }{ + { + name: "full amount tokenize and redeem", + vestingAmount: math.NewInt(0), + delegationAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + tokenizeShareAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + redeemAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + slashFactor: math.LegacyZeroDec(), + globalLiquidStakingCap: liquidStakingCapDisabled, + validatorLiquidStakingCap: liquidStakingCapDisabled, + validatorBondFactor: validatorBondDisabled, + validatorBondDelegation: false, + expTokenizeErr: false, + expRedeemErr: false, + prevAccountDelegationExists: false, + recordAccountDelegationExists: false, + }, + { + name: "full amount tokenize and partial redeem", + vestingAmount: math.NewInt(0), + delegationAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + tokenizeShareAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + redeemAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + slashFactor: math.LegacyZeroDec(), + globalLiquidStakingCap: liquidStakingCapDisabled, + validatorLiquidStakingCap: liquidStakingCapDisabled, + validatorBondFactor: validatorBondDisabled, + validatorBondDelegation: false, + expTokenizeErr: false, + expRedeemErr: false, + prevAccountDelegationExists: false, + recordAccountDelegationExists: true, + }, + { + name: "partial amount tokenize and full redeem", + vestingAmount: math.NewInt(0), + delegationAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + tokenizeShareAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + redeemAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + slashFactor: math.LegacyZeroDec(), + globalLiquidStakingCap: liquidStakingCapDisabled, + validatorLiquidStakingCap: liquidStakingCapDisabled, + validatorBondFactor: validatorBondDisabled, + validatorBondDelegation: false, + expTokenizeErr: false, + expRedeemErr: false, + prevAccountDelegationExists: true, + recordAccountDelegationExists: false, + }, + { + name: "tokenize and redeem with slash", + vestingAmount: math.NewInt(0), + delegationAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + tokenizeShareAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + redeemAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + slashFactor: math.LegacyMustNewDecFromStr("0.1"), + globalLiquidStakingCap: liquidStakingCapDisabled, + validatorLiquidStakingCap: liquidStakingCapDisabled, + validatorBondFactor: validatorBondDisabled, + validatorBondDelegation: false, + expTokenizeErr: false, + expRedeemErr: false, + prevAccountDelegationExists: false, + recordAccountDelegationExists: true, + }, + { + name: "over tokenize", + vestingAmount: math.NewInt(0), + delegationAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + tokenizeShareAmount: stakingKeeper.TokensFromConsensusPower(ctx, 30), + redeemAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + slashFactor: math.LegacyZeroDec(), + globalLiquidStakingCap: liquidStakingCapDisabled, + validatorLiquidStakingCap: liquidStakingCapDisabled, + validatorBondFactor: validatorBondDisabled, + validatorBondDelegation: false, + expTokenizeErr: true, + expRedeemErr: false, + }, + { + name: "over redeem", + vestingAmount: math.NewInt(0), + delegationAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + tokenizeShareAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + redeemAmount: stakingKeeper.TokensFromConsensusPower(ctx, 40), + slashFactor: math.LegacyZeroDec(), + globalLiquidStakingCap: liquidStakingCapDisabled, + validatorLiquidStakingCap: liquidStakingCapDisabled, + validatorBondFactor: validatorBondDisabled, + validatorBondDelegation: false, + expTokenizeErr: false, + expRedeemErr: true, + }, + { + name: "vesting account tokenize share failure", + vestingAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + delegationAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + tokenizeShareAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + redeemAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + slashFactor: math.LegacyZeroDec(), + globalLiquidStakingCap: liquidStakingCapDisabled, + validatorLiquidStakingCap: liquidStakingCapDisabled, + validatorBondFactor: validatorBondDisabled, + validatorBondDelegation: false, + expTokenizeErr: true, + expRedeemErr: false, + prevAccountDelegationExists: true, + }, + { + name: "vesting account tokenize share success", + vestingAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + delegationAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + tokenizeShareAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + redeemAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + targetVestingDelAfterShare: stakingKeeper.TokensFromConsensusPower(ctx, 10), + targetVestingDelAfterRedeem: stakingKeeper.TokensFromConsensusPower(ctx, 10), + slashFactor: math.LegacyZeroDec(), + globalLiquidStakingCap: liquidStakingCapDisabled, + validatorLiquidStakingCap: liquidStakingCapDisabled, + validatorBondFactor: validatorBondDisabled, + validatorBondDelegation: false, + expTokenizeErr: false, + expRedeemErr: false, + prevAccountDelegationExists: true, + }, + { + name: "try tokenize share for a validator-bond delegation", + vestingAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + delegationAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + tokenizeShareAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + redeemAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + targetVestingDelAfterShare: stakingKeeper.TokensFromConsensusPower(ctx, 10), + targetVestingDelAfterRedeem: stakingKeeper.TokensFromConsensusPower(ctx, 10), + slashFactor: math.LegacyZeroDec(), + globalLiquidStakingCap: liquidStakingCapDisabled, + validatorLiquidStakingCap: liquidStakingCapDisabled, + validatorBondFactor: validatorBondConservative, + validatorBondDelegation: true, + validatorBondDelegatorIndex: 1, + expTokenizeErr: true, + expRedeemErr: false, + prevAccountDelegationExists: true, + }, + { + name: "strict validator-bond - tokenization fails", + vestingAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + delegationAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + tokenizeShareAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + redeemAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + targetVestingDelAfterShare: stakingKeeper.TokensFromConsensusPower(ctx, 10), + targetVestingDelAfterRedeem: stakingKeeper.TokensFromConsensusPower(ctx, 10), + slashFactor: math.LegacyZeroDec(), + globalLiquidStakingCap: liquidStakingCapDisabled, + validatorLiquidStakingCap: liquidStakingCapDisabled, + validatorBondFactor: validatorBondStrict, + validatorBondDelegation: false, + expTokenizeErr: true, + expRedeemErr: false, + prevAccountDelegationExists: true, + }, + { + name: "conservative validator-bond - successful tokenization", + vestingAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + delegationAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + tokenizeShareAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + redeemAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + targetVestingDelAfterShare: stakingKeeper.TokensFromConsensusPower(ctx, 10), + targetVestingDelAfterRedeem: stakingKeeper.TokensFromConsensusPower(ctx, 10), + slashFactor: math.LegacyZeroDec(), + globalLiquidStakingCap: liquidStakingCapDisabled, + validatorLiquidStakingCap: liquidStakingCapDisabled, + validatorBondFactor: validatorBondConservative, + validatorBondDelegation: true, + validatorBondDelegatorIndex: 0, + expTokenizeErr: false, + expRedeemErr: false, + prevAccountDelegationExists: true, + }, + { + name: "strict global liquid staking cap - tokenization fails", + vestingAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + delegationAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + tokenizeShareAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + redeemAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + targetVestingDelAfterShare: stakingKeeper.TokensFromConsensusPower(ctx, 10), + targetVestingDelAfterRedeem: stakingKeeper.TokensFromConsensusPower(ctx, 10), + slashFactor: math.LegacyZeroDec(), + globalLiquidStakingCap: liquidStakingCapStrict, + validatorLiquidStakingCap: liquidStakingCapDisabled, + validatorBondFactor: validatorBondDisabled, + validatorBondDelegation: true, + validatorBondDelegatorIndex: 0, + expTokenizeErr: true, + expRedeemErr: false, + prevAccountDelegationExists: true, + }, + { + name: "conservative global liquid staking cap - successful tokenization", + vestingAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + delegationAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + tokenizeShareAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + redeemAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + targetVestingDelAfterShare: stakingKeeper.TokensFromConsensusPower(ctx, 10), + targetVestingDelAfterRedeem: stakingKeeper.TokensFromConsensusPower(ctx, 10), + slashFactor: math.LegacyZeroDec(), + globalLiquidStakingCap: liquidStakingCapConservative, + validatorLiquidStakingCap: liquidStakingCapDisabled, + validatorBondFactor: validatorBondDisabled, + validatorBondDelegation: true, + validatorBondDelegatorIndex: 0, + expTokenizeErr: false, + expRedeemErr: false, + prevAccountDelegationExists: true, + }, + { + name: "strict validator liquid staking cap - tokenization fails", + vestingAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + delegationAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + tokenizeShareAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + redeemAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + targetVestingDelAfterShare: stakingKeeper.TokensFromConsensusPower(ctx, 10), + targetVestingDelAfterRedeem: stakingKeeper.TokensFromConsensusPower(ctx, 10), + slashFactor: math.LegacyZeroDec(), + globalLiquidStakingCap: liquidStakingCapDisabled, + validatorLiquidStakingCap: liquidStakingCapStrict, + validatorBondFactor: validatorBondDisabled, + validatorBondDelegation: true, + validatorBondDelegatorIndex: 0, + expTokenizeErr: true, + expRedeemErr: false, + prevAccountDelegationExists: true, + }, + { + name: "conservative validator liquid staking cap - successful tokenization", + vestingAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + delegationAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + tokenizeShareAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + redeemAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + targetVestingDelAfterShare: stakingKeeper.TokensFromConsensusPower(ctx, 10), + targetVestingDelAfterRedeem: stakingKeeper.TokensFromConsensusPower(ctx, 10), + slashFactor: math.LegacyZeroDec(), + globalLiquidStakingCap: liquidStakingCapDisabled, + validatorLiquidStakingCap: liquidStakingCapConservative, + validatorBondFactor: validatorBondDisabled, + validatorBondDelegation: true, + validatorBondDelegatorIndex: 0, + expTokenizeErr: false, + expRedeemErr: false, + prevAccountDelegationExists: true, + }, + { + name: "all caps set conservatively - successful tokenize share", + vestingAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + delegationAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + tokenizeShareAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + redeemAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + targetVestingDelAfterShare: stakingKeeper.TokensFromConsensusPower(ctx, 10), + targetVestingDelAfterRedeem: stakingKeeper.TokensFromConsensusPower(ctx, 10), + slashFactor: math.LegacyZeroDec(), + globalLiquidStakingCap: liquidStakingCapConservative, + validatorLiquidStakingCap: liquidStakingCapConservative, + validatorBondFactor: validatorBondConservative, + validatorBondDelegation: true, + validatorBondDelegatorIndex: 0, + expTokenizeErr: false, + expRedeemErr: false, + prevAccountDelegationExists: true, + }, + { + name: "delegator is a liquid staking provider - accounting should not update", + vestingAmount: math.ZeroInt(), + delegationAmount: stakingKeeper.TokensFromConsensusPower(ctx, 20), + tokenizeShareAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + redeemAmount: stakingKeeper.TokensFromConsensusPower(ctx, 10), + targetVestingDelAfterShare: stakingKeeper.TokensFromConsensusPower(ctx, 10), + targetVestingDelAfterRedeem: stakingKeeper.TokensFromConsensusPower(ctx, 10), + slashFactor: math.LegacyZeroDec(), + globalLiquidStakingCap: liquidStakingCapConservative, + validatorLiquidStakingCap: liquidStakingCapConservative, + validatorBondFactor: validatorBondConservative, + validatorBondDelegation: true, + validatorBondDelegatorIndex: 0, + expTokenizeErr: false, + expRedeemErr: false, + prevAccountDelegationExists: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc := tc + t.Parallel() + f := initFixture(t) + + ctx := f.sdkCtx + var ( + bankKeeper = f.bankKeeper + accountKeeper = f.accountKeeper + stakingKeeper = f.stakingKeeper + lsmKeeper = f.lsmKeeper + ) + addrs := simtestutil.AddTestAddrs(bankKeeper, stakingKeeper, ctx, 2, stakingKeeper.TokensFromConsensusPower(ctx, 10000)) + addrAcc1, addrAcc2 := addrs[0], addrs[1] + addrVal1, addrVal2 := sdk.ValAddress(addrAcc1), sdk.ValAddress(addrAcc2) + + // Fund module account + bondDenom, err := stakingKeeper.BondDenom(ctx) + require.NoError(t, err) + delegationCoin := sdk.NewCoin(bondDenom, tc.delegationAmount) + err = bankKeeper.MintCoins(ctx, minttypes.ModuleName, sdk.NewCoins(delegationCoin)) + require.NoError(t, err) + + // set the delegator address depending on whether the delegator should be a liquid staking provider + delegatorAccount := addrAcc2 + + // set validator bond factor and global liquid staking cap + params, err := lsmKeeper.GetParams(ctx) + require.NoError(t, err) + params.ValidatorBondFactor = tc.validatorBondFactor + params.GlobalLiquidStakingCap = tc.globalLiquidStakingCap + params.ValidatorLiquidStakingCap = tc.validatorLiquidStakingCap + require.NoError(t, lsmKeeper.SetParams(ctx, params)) + + // set the total liquid staked tokens + lsmKeeper.SetTotalLiquidStakedTokens(ctx, math.ZeroInt()) + + if !tc.vestingAmount.IsZero() { + // create vesting account + acc2 := accountKeeper.GetAccount(ctx, addrAcc2).(*authtypes.BaseAccount) + initialVesting := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, tc.vestingAmount)) + baseVestingWithCoins, err := vestingtypes.NewBaseVestingAccount(acc2, initialVesting, time.Now().Unix()+86400*365) + require.NoError(t, err) + delayedVestingAccount := vestingtypes.NewDelayedVestingAccountRaw(baseVestingWithCoins) + accountKeeper.SetAccount(ctx, delayedVestingAccount) + } + + pubKeys := simtestutil.CreateTestPubKeys(2) + pk1, pk2 := pubKeys[0], pubKeys[1] + + // Create Validators and Delegation + val1 := testutil.NewValidator(t, addrVal1, pk1) + val1.Status = stakingtypes.Bonded + err = stakingKeeper.SetValidator(ctx, val1) + require.NoError(t, err) + err = stakingKeeper.SetValidatorByPowerIndex(ctx, val1) + require.NoError(t, err) + err = stakingKeeper.SetValidatorByConsAddr(ctx, val1) + require.NoError(t, err) + err = lsmKeeper.SetLiquidValidator(ctx, lsmtypes.NewLiquidValidator(val1.OperatorAddress)) + require.NoError(t, err) + + val2 := testutil.NewValidator(t, addrVal2, pk2) + val2.Status = stakingtypes.Bonded + err = stakingKeeper.SetValidator(ctx, val2) + require.NoError(t, err) + err = stakingKeeper.SetValidatorByPowerIndex(ctx, val2) + require.NoError(t, err) + err = stakingKeeper.SetValidatorByConsAddr(ctx, val2) + require.NoError(t, err) + err = lsmKeeper.SetLiquidValidator(ctx, lsmtypes.NewLiquidValidator(val2.OperatorAddress)) + require.NoError(t, err) + + // Delegate from both the main delegator as well as a random account so there is a + // non-zero delegation after redemption + err = delegateCoinsFromAccount(ctx, *stakingKeeper, delegatorAccount, tc.delegationAmount, val1) + require.NoError(t, err) + + // apply TM updates + applyValidatorSetUpdates(t, ctx, stakingKeeper, -1) + + _, err = stakingKeeper.GetDelegation(ctx, delegatorAccount, addrVal1) + require.NoError(t, err, "delegation not found after delegate") + + lastRecordID := lsmKeeper.GetLastTokenizeShareRecordID(ctx) + oldValidator, err := stakingKeeper.GetValidator(ctx, addrVal1) + require.NoError(t, err) + + // skServer := stakingkeeper.NewMsgServerImpl(stakingKeeper) + msgServer := lsmkeeper.NewMsgServerImpl(lsmKeeper) + if tc.validatorBondDelegation { + err := delegateCoinsFromAccount(ctx, *stakingKeeper, addrs[tc.validatorBondDelegatorIndex], tc.delegationAmount, val1) + require.NoError(t, err) + /* + _, err = skServer.Delegate(ctx, &stakingtypes.MsgDelegate{ + DelegatorAddress: addrs[tc.validatorBondDelegatorIndex].String(), + ValidatorAddress: addrVal1.String(), + }) + require.NoError(t, err) + */ + } + + resp, err := msgServer.TokenizeShares(ctx, &lsmtypes.MsgTokenizeShares{ + DelegatorAddress: delegatorAccount.String(), + ValidatorAddress: addrVal1.String(), + Amount: sdk.NewCoin(bondDenom, tc.tokenizeShareAmount), + TokenizedShareOwner: delegatorAccount.String(), + }) + if tc.expTokenizeErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + // check last record id increase + require.Equal(t, lastRecordID+1, lsmKeeper.GetLastTokenizeShareRecordID(ctx)) + + // ensure validator's total tokens is consistent + newValidator, err := stakingKeeper.GetValidator(ctx, addrVal1) + require.NoError(t, err) + require.Equal(t, oldValidator.Tokens, newValidator.Tokens) + newLiquidVal, err := lsmKeeper.GetLiquidValidator(ctx, addrVal1) + require.NoError(t, err) + + // if the delegator was not a provider, check that the total liquid staked and validator liquid shares increased + totalLiquidTokensAfterTokenization := lsmKeeper.GetTotalLiquidStakedTokens(ctx) + validatorLiquidSharesAfterTokenization := newLiquidVal.LiquidShares + require.Equal(t, tc.tokenizeShareAmount.String(), totalLiquidTokensAfterTokenization.String(), "total liquid tokens after tokenization") + require.Equal(t, tc.tokenizeShareAmount.String(), validatorLiquidSharesAfterTokenization.TruncateInt().String(), "validator liquid shares after tokenization") + + if tc.vestingAmount.IsPositive() { + acc := accountKeeper.GetAccount(ctx, addrAcc2) + vestingAcc := acc.(vesting.VestingAccount) + require.Equal(t, vestingAcc.GetDelegatedVesting().AmountOf(bondDenom).String(), tc.targetVestingDelAfterShare.String()) + } + + if tc.prevAccountDelegationExists { + _, err = stakingKeeper.GetDelegation(ctx, delegatorAccount, addrVal1) + require.NoError(t, err, "delegation not found after partial tokenize share") + } else { + _, err = stakingKeeper.GetDelegation(ctx, delegatorAccount, addrVal1) + require.ErrorIs(t, err, stakingtypes.ErrNoDelegation, "delegation found after full tokenize share") + } + + shareToken := bankKeeper.GetBalance(ctx, delegatorAccount, resp.Amount.Denom) + require.Equal(t, resp.Amount, shareToken) + _, err = stakingKeeper.GetValidator(ctx, addrVal1) + require.NoError(t, err, "validator not found") + + records := lsmKeeper.GetAllTokenizeShareRecords(ctx) + require.Len(t, records, 1) + delegation, err := stakingKeeper.GetDelegation(ctx, records[0].GetModuleAddress(), addrVal1) + require.NoError(t, err, "delegation not found from tokenize share module account after tokenize share") + + // slash before redeem + slashedTokens := math.ZeroInt() + redeemedShares := tc.redeemAmount + redeemedTokens := tc.redeemAmount + if tc.slashFactor.IsPositive() { + consAddr, err := val1.GetConsAddr() + require.NoError(t, err) + ctx = ctx.WithBlockHeight(100) + val1, err = stakingKeeper.GetValidator(ctx, addrVal1) + require.NoError(t, err) + power := stakingKeeper.TokensToConsensusPower(ctx, val1.Tokens) + _, err = stakingKeeper.Slash(ctx, consAddr, 10, power, tc.slashFactor) + require.NoError(t, err) + slashedTokens = math.LegacyNewDecFromInt(val1.Tokens).Mul(tc.slashFactor).TruncateInt() + + val1, _ := stakingKeeper.GetValidator(ctx, addrVal1) + redeemedTokens = val1.TokensFromShares(math.LegacyNewDecFromInt(redeemedShares)).TruncateInt() + } + + // get delegator balance and delegation + bondDenomAmountBefore := bankKeeper.GetBalance(ctx, delegatorAccount, bondDenom) + val1, err = stakingKeeper.GetValidator(ctx, addrVal1) + require.NoError(t, err) + delegation, err = stakingKeeper.GetDelegation(ctx, delegatorAccount, addrVal1) + if errors.Is(err, stakingtypes.ErrNoDelegation) { + delegation = stakingtypes.Delegation{Shares: math.LegacyZeroDec()} + } + delAmountBefore := val1.TokensFromShares(delegation.Shares) + oldValidator, err = stakingKeeper.GetValidator(ctx, addrVal1) + require.NoError(t, err) + + _, err = msgServer.RedeemTokensForShares(ctx, &lsmtypes.MsgRedeemTokensForShares{ + DelegatorAddress: delegatorAccount.String(), + Amount: sdk.NewCoin(resp.Amount.Denom, tc.redeemAmount), + }) + if tc.expRedeemErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + // ensure validator's total tokens is consistent + newLiquidVal, err = lsmKeeper.GetLiquidValidator(ctx, addrVal1) + require.NoError(t, err) + newValidator, err = stakingKeeper.GetValidator(ctx, addrVal1) + require.NoError(t, err) + require.Equal(t, oldValidator.Tokens, newValidator.Tokens) + + // if the delegator was not a liquid staking provider, check that the total liquid staked + // and liquid shares decreased + totalLiquidTokensAfterRedemption := lsmKeeper.GetTotalLiquidStakedTokens(ctx) + validatorLiquidSharesAfterRedemption := newLiquidVal.LiquidShares + expectedLiquidTokens := totalLiquidTokensAfterTokenization.Sub(redeemedTokens).Sub(slashedTokens) + expectedLiquidShares := validatorLiquidSharesAfterTokenization.Sub(math.LegacyNewDecFromInt(redeemedShares)) + require.Equal(t, expectedLiquidTokens.String(), totalLiquidTokensAfterRedemption.String(), "total liquid tokens after redemption") + require.Equal(t, expectedLiquidShares.String(), validatorLiquidSharesAfterRedemption.String(), "validator liquid shares after tokenization") + + if tc.vestingAmount.IsPositive() { + acc := accountKeeper.GetAccount(ctx, addrAcc2) + vestingAcc := acc.(vesting.VestingAccount) + require.Equal(t, vestingAcc.GetDelegatedVesting().AmountOf(bondDenom).String(), tc.targetVestingDelAfterRedeem.String()) + } + + expectedDelegatedShares := math.LegacyNewDecFromInt(tc.delegationAmount.Sub(tc.tokenizeShareAmount).Add(tc.redeemAmount)) + delegation, err = stakingKeeper.GetDelegation(ctx, delegatorAccount, addrVal1) + require.NoError(t, err, "delegation not found after redeem tokens") + require.Equal(t, delegatorAccount.String(), delegation.DelegatorAddress) + require.Equal(t, addrVal1.String(), delegation.ValidatorAddress) + require.Equal(t, expectedDelegatedShares, delegation.Shares, "delegation shares after redeem") + + // check delegator balance is not changed + bondDenomAmountAfter := bankKeeper.GetBalance(ctx, delegatorAccount, bondDenom) + require.Equal(t, bondDenomAmountAfter.Amount.String(), bondDenomAmountBefore.Amount.String()) + + // get delegation amount is changed correctly + val1, err = stakingKeeper.GetValidator(ctx, addrVal1) + require.NoError(t, err) + delegation, err = stakingKeeper.GetDelegation(ctx, delegatorAccount, addrVal1) + if errors.Is(err, stakingtypes.ErrNoDelegation) { + delegation = stakingtypes.Delegation{Shares: math.LegacyZeroDec()} + } + delAmountAfter := val1.TokensFromShares(delegation.Shares) + require.Equal(t, delAmountAfter.String(), delAmountBefore.Add(math.LegacyNewDecFromInt(tc.redeemAmount).Mul(math.LegacyOneDec().Sub(tc.slashFactor))).String()) + + shareToken = bankKeeper.GetBalance(ctx, delegatorAccount, resp.Amount.Denom) + require.Equal(t, shareToken.Amount.String(), tc.tokenizeShareAmount.Sub(tc.redeemAmount).String()) + _, err = stakingKeeper.GetValidator(ctx, addrVal1) + require.NoError(t, err, "validator not found") + + if tc.recordAccountDelegationExists { + _, err = stakingKeeper.GetDelegation(ctx, records[0].GetModuleAddress(), addrVal1) + require.NoError(t, err, "delegation not found from tokenize share module account after redeem partial amount") + + records = lsmKeeper.GetAllTokenizeShareRecords(ctx) + require.Len(t, records, 1) + } else { + _, err = stakingKeeper.GetDelegation(ctx, records[0].GetModuleAddress(), addrVal1) + require.True(t, errors.Is(err, stakingtypes.ErrNoDelegation), + "delegation found from tokenize share module account after redeem full amount") + + records = lsmKeeper.GetAllTokenizeShareRecords(ctx) + require.Len(t, records, 0) + } + }) + } +} + +func TestRedelegationTokenization(t *testing.T) { + // Test that a delegator with ongoing redelegation cannot + // tokenize any shares until the redelegation is complete. + f := initFixture(t) + + ctx := f.sdkCtx + var ( + stakingKeeper = f.stakingKeeper + bankKeeper = f.bankKeeper + lsmKeeper = f.lsmKeeper + ) + skServer := stakingkeeper.NewMsgServerImpl(stakingKeeper) + msgServer := lsmkeeper.NewMsgServerImpl(lsmKeeper) + pubKeys := simtestutil.CreateTestPubKeys(1) + pk1 := pubKeys[0] + + // Create Validators and Delegation + addrs := simtestutil.AddTestAddrs(bankKeeper, stakingKeeper, ctx, 2, stakingKeeper.TokensFromConsensusPower(ctx, 10000)) + alice := addrs[0] + + validatorAAddress := sdk.ValAddress(addrs[1]) + val1 := testutil.NewValidator(t, validatorAAddress, pk1) + val1.Status = stakingtypes.Bonded + require.NoError(t, stakingKeeper.SetValidator(ctx, val1)) + require.NoError(t, stakingKeeper.SetValidatorByPowerIndex(ctx, val1)) + err := stakingKeeper.SetValidatorByConsAddr(ctx, val1) + require.NoError(t, err) + + _, validatorBAddress := setupTestTokenizeAndRedeemConversion(t, *lsmKeeper, *stakingKeeper, bankKeeper, ctx) + + delegateAmount := sdk.TokensFromConsensusPower(10, sdk.DefaultPowerReduction) + bondedDenom, err := stakingKeeper.BondDenom(ctx) + require.NoError(t, err) + delegateCoin := sdk.NewCoin(bondedDenom, delegateAmount) + + // Alice delegates to validatorA + _, err = skServer.Delegate(ctx, &stakingtypes.MsgDelegate{ + DelegatorAddress: alice.String(), + ValidatorAddress: validatorAAddress.String(), + Amount: delegateCoin, + }) + require.NoError(t, err) + + // Alice redelegates to validatorB + redelegateAmount := sdk.TokensFromConsensusPower(5, sdk.DefaultPowerReduction) + redelegateCoin := sdk.NewCoin(bondedDenom, redelegateAmount) + _, err = skServer.BeginRedelegate(ctx, &stakingtypes.MsgBeginRedelegate{ + DelegatorAddress: alice.String(), + ValidatorSrcAddress: validatorAAddress.String(), + ValidatorDstAddress: validatorBAddress.String(), + Amount: redelegateCoin, + }) + require.NoError(t, err) + + redelegation, err := stakingKeeper.GetRedelegations(ctx, alice, uint16(10)) + require.NoError(t, err) + require.Len(t, redelegation, 1, "expect one redelegation") + require.Len(t, redelegation[0].Entries, 1, "expect one redelegation entry") + + // Alice attempts to tokenize the redelegation, but this fails because the redelegation is ongoing + tokenizedAmount := sdk.TokensFromConsensusPower(5, sdk.DefaultPowerReduction) + tokenizedCoin := sdk.NewCoin(bondedDenom, tokenizedAmount) + _, err = msgServer.TokenizeShares(ctx, &lsmtypes.MsgTokenizeShares{ + DelegatorAddress: alice.String(), + ValidatorAddress: validatorBAddress.String(), + Amount: tokenizedCoin, + TokenizedShareOwner: alice.String(), + }) + require.Error(t, err) + require.Equal(t, lsmtypes.ErrRedelegationInProgress, err) + + // Check that the redelegation is still present + redelegation, err = stakingKeeper.GetRedelegations(ctx, alice, uint16(10)) + require.NoError(t, err) + require.Len(t, redelegation, 1, "expect one redelegation") + require.Len(t, redelegation[0].Entries, 1, "expect one redelegation entry") + + // advance time until the redelegations should mature + // end block + _, err = f.stakingKeeper.EndBlocker(ctx) + require.NoError(t, err) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + // advance by 22 days + ctx = ctx.WithBlockTime(ctx.BlockTime().Add(22 * 24 * time.Hour)) + headerInfo := ctx.HeaderInfo() + headerInfo.Time = ctx.BlockHeader().Time + headerInfo.Height = ctx.BlockHeader().Height + ctx = ctx.WithHeaderInfo(headerInfo) + // begin block + require.NoError(t, f.stakingKeeper.BeginBlocker(ctx)) + // end block + _, err = f.stakingKeeper.EndBlocker(ctx) + require.NoError(t, err) + + // check that the redelegation is removed + redelegation, err = stakingKeeper.GetRedelegations(ctx, alice, uint16(10)) + require.NoError(t, err) + require.Len(t, redelegation, 0, "expect no redelegations") + + // Alice attempts to tokenize the redelegation again, and this time it should succeed + // because there is no ongoing redelegation + _, err = msgServer.TokenizeShares(ctx, &lsmtypes.MsgTokenizeShares{ + DelegatorAddress: alice.String(), + ValidatorAddress: validatorBAddress.String(), + Amount: tokenizedCoin, + TokenizedShareOwner: alice.String(), + }) + require.NoError(t, err) + + // Check that the tokenization was successful + shareRecord, err := lsmKeeper.GetTokenizeShareRecord(ctx, lsmKeeper.GetLastTokenizeShareRecordID(ctx)) + require.NoError(t, err, "expect to find token share record") + require.Equal(t, alice.String(), shareRecord.Owner) + require.Equal(t, validatorBAddress.String(), shareRecord.Validator) +} + +// Helper function to setup a delegator and validator for the Tokenize/Redeem conversion tests +func setupTestTokenizeAndRedeemConversion( + t *testing.T, + lk lsmkeeper.Keeper, + sk stakingkeeper.Keeper, + bk bankkeeper.Keeper, + ctx sdk.Context, +) (delAddress sdk.AccAddress, valAddress sdk.ValAddress) { + t.Helper() + addresses := simtestutil.AddTestAddrs(bk, sk, ctx, 2, math.NewInt(1_000_000)) + + pubKeys := simtestutil.CreateTestPubKeys(1) + + delegatorAddress := addresses[0] + validatorAddress := sdk.ValAddress(addresses[1]) + + validator, err := stakingtypes.NewValidator(validatorAddress.String(), pubKeys[0], stakingtypes.Description{}) + require.NoError(t, err) + liquidVal := lsmtypes.NewLiquidValidator(validatorAddress.String()) + validator.DelegatorShares = math.LegacyNewDec(1_000_000) + validator.Tokens = math.NewInt(1_000_000) + validator.Status = stakingtypes.Bonded + + _ = sk.SetValidator(ctx, validator) + _ = sk.SetValidatorByConsAddr(ctx, validator) + _ = lk.SetLiquidValidator(ctx, liquidVal) + + return delegatorAddress, validatorAddress +} + +// Simulate a slash by decrementing the validator's tokens +// We'll do this in a way such that the exchange rate is not an even integer +// and the shares associated with a delegation will have a long decimal +func simulateSlashWithImprecision(t *testing.T, sk stakingkeeper.Keeper, ctx sdk.Context, valAddress sdk.ValAddress) { + t.Helper() + validator, err := sk.GetValidator(ctx, valAddress) + require.NoError(t, err) + + slashMagnitude := math.LegacyMustNewDecFromStr("0.1111111111") + slashTokens := math.LegacyNewDecFromInt(validator.Tokens).Mul(slashMagnitude).TruncateInt() + validator.Tokens = validator.Tokens.Sub(slashTokens) + + require.NoError(t, sk.SetValidator(ctx, validator)) +} + +// Tests the conversion from tokenization and redemption from the following scenario: +// Slash -> Delegate -> Tokenize -> Redeem +// Note, in this example, there 2 tokens are lost during the decimal to int conversion +// during the unbonding step within tokenization and redemption +func TestTokenizeAndRedeemConversion_SlashBeforeDelegation(t *testing.T) { + t.Parallel() + f := initFixture(t) + + ctx := f.sdkCtx + var ( + stakingKeeper = f.stakingKeeper + bankKeeper = f.bankKeeper + lsmKepeer = f.lsmKeeper + ) + skServer := stakingkeeper.NewMsgServerImpl(stakingKeeper) + lsmServer := lsmkeeper.NewMsgServerImpl(lsmKepeer) + delegatorAddress, validatorAddress := setupTestTokenizeAndRedeemConversion(t, *lsmKepeer, *stakingKeeper, + bankKeeper, ctx) + + // slash the validator + simulateSlashWithImprecision(t, *stakingKeeper, ctx, validatorAddress) + validator, err := stakingKeeper.GetValidator(ctx, validatorAddress) + require.NoError(t, err) + + // Delegate and confirm the delegation record was created + delegateAmount := math.NewInt(1000) + bondDenom, err := stakingKeeper.BondDenom(ctx) + require.NoError(t, err) + delegateCoin := sdk.NewCoin(bondDenom, delegateAmount) + _, err = skServer.Delegate(ctx, &stakingtypes.MsgDelegate{ + DelegatorAddress: delegatorAddress.String(), + ValidatorAddress: validatorAddress.String(), + Amount: delegateCoin, + }) + require.NoError(t, err, "no error expected when delegating") + + delegation, err := stakingKeeper.GetDelegation(ctx, delegatorAddress, validatorAddress) + require.NoError(t, err, "delegation should have been found") + + // Tokenize the full delegation amount + _, err = lsmServer.TokenizeShares(ctx, &lsmtypes.MsgTokenizeShares{ + DelegatorAddress: delegatorAddress.String(), + ValidatorAddress: validatorAddress.String(), + Amount: delegateCoin, + TokenizedShareOwner: delegatorAddress.String(), + }) + require.NoError(t, err, "no error expected when tokenizing") + + // Confirm the number of shareTokens equals the number of shares truncated + // Note: 1 token is lost during unbonding due to rounding + shareDenom := validatorAddress.String() + "/1" + shareToken := bankKeeper.GetBalance(ctx, delegatorAddress, shareDenom) + expectedShareTokens := delegation.Shares.TruncateInt().Int64() - 1 // 1 token was lost during unbonding + require.Equal(t, expectedShareTokens, shareToken.Amount.Int64(), "share token amount") + + // Redeem the share tokens + _, err = lsmServer.RedeemTokensForShares(ctx, &lsmtypes.MsgRedeemTokensForShares{ + DelegatorAddress: delegatorAddress.String(), + Amount: shareToken, + }) + require.NoError(t, err, "no error expected when redeeming") + + // Confirm (almost) the full delegation was recovered - minus the 2 tokens from the precision error + // (1 occurs during tokenization, and 1 occurs during redemption) + newDelegation, err := stakingKeeper.GetDelegation(ctx, delegatorAddress, validatorAddress) + require.NoError(t, err) + + endDelegationTokens := validator.TokensFromShares(newDelegation.Shares).TruncateInt().Int64() + expectedDelegationTokens := delegateAmount.Int64() - 2 + require.Equal(t, expectedDelegationTokens, endDelegationTokens, "final delegation tokens") +} + +// Tests the conversion from tokenization and redemption from the following scenario: +// Delegate -> Slash -> Tokenize -> Redeem +// Note, in this example, there 1 token lost during the decimal to int conversion +// during the unbonding step within tokenization +func TestTokenizeAndRedeemConversion_SlashBeforeTokenization(t *testing.T) { + t.Parallel() + f := initFixture(t) + + ctx := f.sdkCtx + var ( + stakingKeeper = f.stakingKeeper + bankKeeper = f.bankKeeper + lsmKeeper = f.lsmKeeper + ) + msgServer := lsmkeeper.NewMsgServerImpl(lsmKeeper) + skServer := stakingkeeper.NewMsgServerImpl(stakingKeeper) + delegatorAddress, validatorAddress := setupTestTokenizeAndRedeemConversion(t, *lsmKeeper, *stakingKeeper, + bankKeeper, ctx) + + // Delegate and confirm the delegation record was created + delegateAmount := math.NewInt(1000) + bondDenom, err := stakingKeeper.BondDenom(ctx) + require.NoError(t, err) + delegateCoin := sdk.NewCoin(bondDenom, delegateAmount) + _, err = skServer.Delegate(ctx, &stakingtypes.MsgDelegate{ + DelegatorAddress: delegatorAddress.String(), + ValidatorAddress: validatorAddress.String(), + Amount: delegateCoin, + }) + require.NoError(t, err, "no error expected when delegating") + + _, err = stakingKeeper.GetDelegation(ctx, delegatorAddress, validatorAddress) + require.NoError(t, err, "delegation should have been found") + + // slash the validator + simulateSlashWithImprecision(t, *stakingKeeper, ctx, validatorAddress) + validator, err := stakingKeeper.GetValidator(ctx, validatorAddress) + require.NoError(t, err) + + // Tokenize the new amount after the slash + delegationAmountAfterSlash := validator.TokensFromShares(math.LegacyNewDecFromInt(delegateAmount)).TruncateInt() + tokenizationCoin := sdk.NewCoin(bondDenom, delegationAmountAfterSlash) + + _, err = msgServer.TokenizeShares(ctx, &lsmtypes.MsgTokenizeShares{ + DelegatorAddress: delegatorAddress.String(), + ValidatorAddress: validatorAddress.String(), + Amount: tokenizationCoin, + TokenizedShareOwner: delegatorAddress.String(), + }) + require.NoError(t, err, "no error expected when tokenizing") + + // The number of share tokens should line up with the **new** number of shares associated + // with the original delegated amount + // Note: 1 token is lost during unbonding due to rounding + shareDenom := validatorAddress.String() + "/1" + shareToken := bankKeeper.GetBalance(ctx, delegatorAddress, shareDenom) + expectedShareTokens, err := validator.SharesFromTokens(tokenizationCoin.Amount) + require.NoError(t, err) + require.Equal(t, expectedShareTokens.TruncateInt().Int64()-1, shareToken.Amount.Int64(), "share token amount") + + // // Redeem the share tokens + _, err = msgServer.RedeemTokensForShares(ctx, &lsmtypes.MsgRedeemTokensForShares{ + DelegatorAddress: delegatorAddress.String(), + Amount: shareToken, + }) + require.NoError(t, err, "no error expected when redeeming") + + // Confirm the full tokenization amount was recovered - minus the 1 token from the precision error + newDelegation, err := stakingKeeper.GetDelegation(ctx, delegatorAddress, validatorAddress) + require.NoError(t, err) + + endDelegationTokens := validator.TokensFromShares(newDelegation.Shares).TruncateInt().Int64() + expectedDelegationTokens := delegationAmountAfterSlash.Int64() - 1 + require.Equal(t, expectedDelegationTokens, endDelegationTokens, "final delegation tokens") +} + +// Tests the conversion from tokenization and redemption from the following scenario: +// Delegate -> Tokenize -> Slash -> Redeem +// Note, in this example, there 1 token lost during the decimal to int conversion +// during the unbonding step within redemption +func TestTokenizeAndRedeemConversion_SlashBeforeRedemption(t *testing.T) { + t.Parallel() + f := initFixture(t) + + ctx := f.sdkCtx + var ( + stakingKeeper = f.stakingKeeper + bankKeeper = f.bankKeeper + lsmKeeper = f.lsmKeeper + ) + msgServer := lsmkeeper.NewMsgServerImpl(lsmKeeper) + skServer := stakingkeeper.NewMsgServerImpl(stakingKeeper) + delegatorAddress, validatorAddress := setupTestTokenizeAndRedeemConversion(t, *lsmKeeper, *stakingKeeper, + bankKeeper, ctx) + + // Delegate and confirm the delegation record was created + delegateAmount := math.NewInt(1000) + bondDenom, err := stakingKeeper.BondDenom(ctx) + require.NoError(t, err) + delegateCoin := sdk.NewCoin(bondDenom, delegateAmount) + _, err = skServer.Delegate(ctx, &stakingtypes.MsgDelegate{ + DelegatorAddress: delegatorAddress.String(), + ValidatorAddress: validatorAddress.String(), + Amount: delegateCoin, + }) + require.NoError(t, err, "no error expected when delegating") + + _, err = stakingKeeper.GetDelegation(ctx, delegatorAddress, validatorAddress) + require.NoError(t, err, "delegation should have been found") + + // Tokenize the full delegation amount + _, err = msgServer.TokenizeShares(ctx, &lsmtypes.MsgTokenizeShares{ + DelegatorAddress: delegatorAddress.String(), + ValidatorAddress: validatorAddress.String(), + Amount: delegateCoin, + TokenizedShareOwner: delegatorAddress.String(), + }) + require.NoError(t, err, "no error expected when tokenizing") + + // The number of share tokens should line up 1:1 with the number of issued shares + // Since the validator has not been slashed, the shares also line up 1;1 + // with the original delegation amount + shareDenom := validatorAddress.String() + "/1" + shareToken := bankKeeper.GetBalance(ctx, delegatorAddress, shareDenom) + expectedShareTokens := delegateAmount + require.Equal(t, expectedShareTokens.Int64(), shareToken.Amount.Int64(), "share token amount") + + // slash the validator + simulateSlashWithImprecision(t, *stakingKeeper, ctx, validatorAddress) + validator, err := stakingKeeper.GetValidator(ctx, validatorAddress) + require.NoError(t, err) + + // Redeem the share tokens + _, err = msgServer.RedeemTokensForShares(ctx, &lsmtypes.MsgRedeemTokensForShares{ + DelegatorAddress: delegatorAddress.String(), + Amount: shareToken, + }) + require.NoError(t, err, "no error expected when redeeming") + + // Confirm the original delegation, minus the slash, was recovered + // There's an additional 1 token lost from precision error during unbonding + delegationAmountAfterSlash := validator.TokensFromShares(math.LegacyNewDecFromInt(delegateAmount)).TruncateInt().Int64() + newDelegation, err := stakingKeeper.GetDelegation(ctx, delegatorAddress, validatorAddress) + require.NoError(t, err) + + endDelegationTokens := validator.TokensFromShares(newDelegation.Shares).TruncateInt().Int64() + require.Equal(t, delegationAmountAfterSlash-1, endDelegationTokens, "final delegation tokens") +} + +func TestTransferTokenizeShareRecord(t *testing.T) { + t.Parallel() + f := initFixture(t) + + ctx := f.sdkCtx + var ( + stakingKeeper = f.stakingKeeper + bankKeeper = f.bankKeeper + lsmKeeper = f.lsmKeeper + ) + msgServer := lsmkeeper.NewMsgServerImpl(lsmKeeper) + + addrs := simtestutil.AddTestAddrs(bankKeeper, stakingKeeper, ctx, 3, stakingKeeper.TokensFromConsensusPower(ctx, 10000)) + addrAcc1, addrAcc2, valAcc := addrs[0], addrs[1], addrs[2] + addrVal := sdk.ValAddress(valAcc) + + pubKeys := simtestutil.CreateTestPubKeys(1) + pk := pubKeys[0] + + val, err := stakingtypes.NewValidator(addrVal.String(), pk, stakingtypes.Description{}) + require.NoError(t, err) + + require.NoError(t, stakingKeeper.SetValidator(ctx, val)) + require.NoError(t, stakingKeeper.SetValidatorByPowerIndex(ctx, val)) + + // apply TM updates + applyValidatorSetUpdates(t, ctx, stakingKeeper, -1) + + err = lsmKeeper.AddTokenizeShareRecord(ctx, lsmtypes.TokenizeShareRecord{ + Id: 1, + Owner: addrAcc1.String(), + ModuleAccount: "module_account", + Validator: val.String(), + }) + require.NoError(t, err) + + _, err = msgServer.TransferTokenizeShareRecord(ctx, &lsmtypes.MsgTransferTokenizeShareRecord{ + TokenizeShareRecordId: 1, + Sender: addrAcc1.String(), + NewOwner: addrAcc2.String(), + }) + require.NoError(t, err) + + record, err := lsmKeeper.GetTokenizeShareRecord(ctx, 1) + require.NoError(t, err) + require.Equal(t, record.Owner, addrAcc2.String()) + + records := lsmKeeper.GetTokenizeShareRecordsByOwner(ctx, addrAcc1) + require.Len(t, records, 0) + records = lsmKeeper.GetTokenizeShareRecordsByOwner(ctx, addrAcc2) + require.Len(t, records, 1) +} + +func TestEnableDisableTokenizeShares(t *testing.T) { + t.Parallel() + f := initFixture(t) + + ctx := f.sdkCtx + var ( + stakingKeeper = f.stakingKeeper + bankKeeper = f.bankKeeper + lsmKeeper = f.lsmKeeper + ) + // Create a delegator and validator + stakeAmount := math.NewInt(1000) + bondDenom, err := stakingKeeper.BondDenom(ctx) + require.NoError(t, err) + stakeToken := sdk.NewCoin(bondDenom, stakeAmount) + + addresses := simtestutil.AddTestAddrs(bankKeeper, stakingKeeper, ctx, 2, stakeAmount) + delegatorAddress := addresses[0] + + pubKeys := simtestutil.CreateTestPubKeys(1) + validatorAddress := sdk.ValAddress(addresses[1]) + validator, err := stakingtypes.NewValidator(validatorAddress.String(), pubKeys[0], stakingtypes.Description{}) + require.NoError(t, err) + + validator.DelegatorShares = math.LegacyNewDec(1_000_000) + validator.Tokens = math.NewInt(1_000_000) + validator.Status = stakingtypes.Bonded + require.NoError(t, stakingKeeper.SetValidator(ctx, validator)) + require.NoError(t, lsmKeeper.SetLiquidValidator(ctx, lsmtypes.NewLiquidValidator(validator.OperatorAddress))) + + // Fix block time and set unbonding period to 1 day + blockTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) + ctx = ctx.WithBlockTime(blockTime) + + unbondingPeriod := time.Hour * 24 + params, err := stakingKeeper.GetParams(ctx) + require.NoError(t, err) + params.UnbondingTime = unbondingPeriod + require.NoError(t, stakingKeeper.SetParams(ctx, params)) + unlockTime := blockTime.Add(unbondingPeriod) + + // Build test messages (some of which will be reused) + delegateMsg := stakingtypes.MsgDelegate{ + DelegatorAddress: delegatorAddress.String(), + ValidatorAddress: validatorAddress.String(), + Amount: stakeToken, + } + tokenizeMsg := lsmtypes.MsgTokenizeShares{ + DelegatorAddress: delegatorAddress.String(), + ValidatorAddress: validatorAddress.String(), + Amount: stakeToken, + TokenizedShareOwner: delegatorAddress.String(), + } + redeemMsg := lsmtypes.MsgRedeemTokensForShares{ + DelegatorAddress: delegatorAddress.String(), + } + disableMsg := lsmtypes.MsgDisableTokenizeShares{ + DelegatorAddress: delegatorAddress.String(), + } + enableMsg := lsmtypes.MsgEnableTokenizeShares{ + DelegatorAddress: delegatorAddress.String(), + } + + msgServer := lsmkeeper.NewMsgServerImpl(lsmKeeper) + skServer := stakingkeeper.NewMsgServerImpl(stakingKeeper) + // Delegate normally + _, err = skServer.Delegate(ctx, &delegateMsg) + require.NoError(t, err, "no error expected when delegating") + + // Tokenize shares - it should succeed + _, err = msgServer.TokenizeShares(ctx, &tokenizeMsg) + require.NoError(t, err, "no error expected when tokenizing shares for the first time") + + liquidToken := bankKeeper.GetBalance(ctx, delegatorAddress, validatorAddress.String()+"/1") + require.Equal(t, stakeAmount.Int64(), liquidToken.Amount.Int64(), "user received token after tokenizing share") + + // Redeem to remove all tokenized shares + redeemMsg.Amount = liquidToken + _, err = msgServer.RedeemTokensForShares(ctx, &redeemMsg) + require.NoError(t, err, "no error expected when redeeming") + + // Attempt to enable tokenizing shares when there is no lock in place, it should error + _, err = msgServer.EnableTokenizeShares(ctx, &enableMsg) + require.ErrorIs(t, err, lsmtypes.ErrTokenizeSharesAlreadyEnabledForAccount) + + // Attempt to disable when no lock is in place, it should succeed + _, err = msgServer.DisableTokenizeShares(ctx, &disableMsg) + require.NoError(t, err, "no error expected when disabling tokenization") + + // Disabling again while the lock is already in place, should error + _, err = msgServer.DisableTokenizeShares(ctx, &disableMsg) + require.ErrorIs(t, err, lsmtypes.ErrTokenizeSharesAlreadyDisabledForAccount) + + // Attempt to tokenize, it should fail since tokenization is disabled + _, err = msgServer.TokenizeShares(ctx, &tokenizeMsg) + require.ErrorIs(t, err, lsmtypes.ErrTokenizeSharesDisabledForAccount) + + // Now enable tokenization + _, err = msgServer.EnableTokenizeShares(ctx, &enableMsg) + require.NoError(t, err, "no error expected when enabling tokenization") + + // Attempt to tokenize again, it should still fail since the unbonding period has + // not passed and the lock is still active + _, err = msgServer.TokenizeShares(ctx, &tokenizeMsg) + require.ErrorIs(t, err, lsmtypes.ErrTokenizeSharesDisabledForAccount) + require.ErrorContains(t, err, fmt.Sprintf("tokenization will be allowed at %s", + blockTime.Add(unbondingPeriod))) + + // Confirm the unlock is queued + authorizations := lsmKeeper.GetPendingTokenizeShareAuthorizations(ctx, unlockTime) + require.Equal(t, []string{delegatorAddress.String()}, authorizations.Addresses, + "pending tokenize share authorizations") + + // Disable tokenization again - it should remove the pending record from the queue + _, err = msgServer.DisableTokenizeShares(ctx, &disableMsg) + require.NoError(t, err, "no error expected when re-enabling tokenization") + + authorizations = lsmKeeper.GetPendingTokenizeShareAuthorizations(ctx, unlockTime) + require.Empty(t, authorizations.Addresses, "there should be no pending authorizations in the queue") + + // Enable one more time + _, err = msgServer.EnableTokenizeShares(ctx, &enableMsg) + require.NoError(t, err, "no error expected when enabling tokenization again") + + // Increment the block time by the unbonding period and remove the expired locks + ctx = ctx.WithBlockTime(unlockTime) + _, err = lsmKeeper.RemoveExpiredTokenizeShareLocks(ctx, ctx.BlockTime()) + require.NoError(t, err) + + // Attempt to tokenize again, it should succeed this time since the lock has expired + _, err = msgServer.TokenizeShares(ctx, &tokenizeMsg) + require.NoError(t, err, "no error expected when tokenizing after lock has expired") +} + +func TestTokenizeAndRedeemVestedDelegation(t *testing.T) { + f := initFixture(t) + + ctx := f.sdkCtx + var ( + stakingKeeper = f.stakingKeeper + bankKeeper = f.bankKeeper + accountKeeper = f.accountKeeper + lsmKeeper = f.lsmKeeper + ) + + addrs := simtestutil.AddTestAddrs(bankKeeper, stakingKeeper, ctx, 1, stakingKeeper.TokensFromConsensusPower(ctx, 10000)) + addrAcc1 := addrs[0] + addrVal1 := sdk.ValAddress(addrAcc1) + + // Original vesting mount (OV) + originalVesting := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(100_000))) + startTime := time.Now() + endTime := time.Now().Add(24 * time.Hour) + + // Create vesting account + lastAccNum := uint64(1000) + baseAcc := authtypes.NewBaseAccountWithAddress(addrAcc1) + require.NoError(t, baseAcc.SetAccountNumber(atomic.AddUint64(&lastAccNum, 1))) + + continuousVestingAccount, err := vestingtypes.NewContinuousVestingAccount( + baseAcc, + originalVesting, + startTime.Unix(), + endTime.Unix(), + ) + require.NoError(t, err) + accountKeeper.SetAccount(ctx, continuousVestingAccount) + + pubKeys := simtestutil.CreateTestPubKeys(1) + pk1 := pubKeys[0] + + // Create Validators and Delegation + val1 := testutil.NewValidator(t, addrVal1, pk1) + val1.Status = stakingtypes.Bonded + require.NoError(t, stakingKeeper.SetValidator(ctx, val1)) + require.NoError(t, lsmKeeper.SetLiquidValidator(ctx, lsmtypes.NewLiquidValidator(val1.OperatorAddress))) + require.NoError(t, stakingKeeper.SetValidatorByPowerIndex(ctx, val1)) + err = stakingKeeper.SetValidatorByConsAddr(ctx, val1) + require.NoError(t, err) + + // Delegate all the vesting coins + originalVestingAmount := originalVesting.AmountOf(sdk.DefaultBondDenom) + err = delegateCoinsFromAccount(ctx, *stakingKeeper, addrAcc1, originalVestingAmount, val1) + require.NoError(t, err) + + // Apply TM updates + applyValidatorSetUpdates(t, ctx, stakingKeeper, -1) + + _, err = stakingKeeper.GetDelegation(ctx, addrAcc1, addrVal1) + require.NoError(t, err) + + // Check vesting account data + // V=100, V'=0, DV=100, DF=0 + acc := accountKeeper.GetAccount(ctx, addrAcc1).(*vestingtypes.ContinuousVestingAccount) + require.Equal(t, originalVesting, acc.GetVestingCoins(ctx.BlockTime())) + require.Empty(t, acc.GetVestedCoins(ctx.BlockTime())) + require.Equal(t, originalVesting, acc.GetDelegatedVesting()) + require.Empty(t, acc.GetDelegatedFree()) + + msgServer := lsmkeeper.NewMsgServerImpl(lsmKeeper) + + // Vest half the original vesting coins + vestHalfTime := startTime.Add(time.Duration(float64(endTime.Sub(startTime).Nanoseconds()) / float64(2))) + ctx = ctx.WithBlockTime(vestHalfTime) + + // expect that half of the orignal vesting coins are vested + expVestedCoins := originalVesting.QuoInt(math.NewInt(2)) + + // Check vesting account data + // V=50, V'=50, DV=100, DF=0 + acc = accountKeeper.GetAccount(ctx, addrAcc1).(*vestingtypes.ContinuousVestingAccount) + require.Equal(t, expVestedCoins, acc.GetVestingCoins(ctx.BlockTime())) + require.Equal(t, expVestedCoins, acc.GetVestedCoins(ctx.BlockTime())) + require.Equal(t, originalVesting, acc.GetDelegatedVesting()) + require.Empty(t, acc.GetDelegatedFree()) + + // Expect that tokenizing all the delegated coins fails + // since only the half are vested + _, err = msgServer.TokenizeShares(ctx, &lsmtypes.MsgTokenizeShares{ + DelegatorAddress: addrAcc1.String(), + ValidatorAddress: addrVal1.String(), + Amount: originalVesting[0], + TokenizedShareOwner: addrAcc1.String(), + }) + require.Error(t, err) + + // Tokenize the delegated vested coins + _, err = msgServer.TokenizeShares(ctx, &lsmtypes.MsgTokenizeShares{ + DelegatorAddress: addrAcc1.String(), + ValidatorAddress: addrVal1.String(), + Amount: sdk.Coin{Denom: sdk.DefaultBondDenom, Amount: originalVestingAmount.Quo(math.NewInt(2))}, + TokenizedShareOwner: addrAcc1.String(), + }) + require.NoError(t, err) + + shareDenom := addrVal1.String() + "/1" + + // Redeem the tokens + _, err = msgServer.RedeemTokensForShares(ctx, + &lsmtypes.MsgRedeemTokensForShares{ + DelegatorAddress: addrAcc1.String(), + Amount: sdk.Coin{Denom: shareDenom, Amount: originalVestingAmount.Quo(math.NewInt(2))}, + }, + ) + require.NoError(t, err) + + // After the redemption of the tokens, the vesting delegations should be evenly distributed + // V=50, V'=50, DV=100, DF=50 + acc = accountKeeper.GetAccount(ctx, addrAcc1).(*vestingtypes.ContinuousVestingAccount) + require.Equal(t, expVestedCoins, acc.GetVestingCoins(ctx.BlockTime())) + require.Equal(t, expVestedCoins, acc.GetVestedCoins(ctx.BlockTime())) + require.Equal(t, expVestedCoins, acc.GetDelegatedVesting()) + require.Equal(t, expVestedCoins, acc.GetDelegatedFree()) +} diff --git a/tests/integration/test_common.go b/tests/integration/test_common.go new file mode 100644 index 0000000000..fa639df22b --- /dev/null +++ b/tests/integration/test_common.go @@ -0,0 +1,176 @@ +package integration + +import ( + "testing" + + "github.com/stretchr/testify/require" + + abci "github.com/cometbft/cometbft/abci/types" + cmtprototypes "github.com/cometbft/cometbft/proto/tendermint/types" + + "cosmossdk.io/core/appmodule" + "cosmossdk.io/log" + "cosmossdk.io/math" + storetypes "cosmossdk.io/store/types" + + "github.com/cosmos/cosmos-sdk/codec" + addresscodec "github.com/cosmos/cosmos-sdk/codec/address" + "github.com/cosmos/cosmos-sdk/runtime" + "github.com/cosmos/cosmos-sdk/testutil/integration" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + "github.com/cosmos/cosmos-sdk/x/auth" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + authsims "github.com/cosmos/cosmos-sdk/x/auth/simulation" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/auth/vesting" + "github.com/cosmos/cosmos-sdk/x/bank" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/cosmos/cosmos-sdk/x/distribution" + distributionkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + "github.com/cosmos/cosmos-sdk/x/staking" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + "github.com/cosmos/gaia/v22/x/lsm" + lsmkeeper "github.com/cosmos/gaia/v22/x/lsm/keeper" + lsmtypes "github.com/cosmos/gaia/v22/x/lsm/types" +) + +type fixture struct { + app *integration.App + + sdkCtx sdk.Context + cdc codec.Codec + keys map[string]*storetypes.KVStoreKey + + accountKeeper authkeeper.AccountKeeper + bankKeeper bankkeeper.Keeper + distributionKeeper distributionkeeper.Keeper + stakingKeeper *stakingkeeper.Keeper + lsmKeeper *lsmkeeper.Keeper +} + +func initFixture(tb testing.TB) *fixture { + tb.Helper() + keys := storetypes.NewKVStoreKeys( + authtypes.StoreKey, banktypes.StoreKey, distributiontypes.StoreKey, stakingtypes.StoreKey, lsmtypes.StoreKey, + ) + cdc := moduletestutil.MakeTestEncodingConfig(auth.AppModuleBasic{}, staking.AppModuleBasic{}, vesting.AppModuleBasic{}).Codec + + logger := log.NewTestLogger(tb) + cms := integration.CreateMultiStore(keys, logger) + + newCtx := sdk.NewContext(cms, cmtprototypes.Header{}, true, logger) + + authority := authtypes.NewModuleAddress("gov") + + maccPerms := map[string][]string{ + distributiontypes.ModuleName: {authtypes.Minter}, + minttypes.ModuleName: {authtypes.Minter}, + stakingtypes.ModuleName: {authtypes.Minter}, + stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, + stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, + } + + accountKeeper := authkeeper.NewAccountKeeper( + cdc, + runtime.NewKVStoreService(keys[authtypes.StoreKey]), + authtypes.ProtoBaseAccount, + maccPerms, + addresscodec.NewBech32Codec(sdk.Bech32MainPrefix), + sdk.Bech32MainPrefix, + authority.String(), + ) + + blockedAddresses := map[string]bool{ + accountKeeper.GetAuthority(): false, + } + bankKeeper := bankkeeper.NewBaseKeeper( + cdc, + runtime.NewKVStoreService(keys[banktypes.StoreKey]), + accountKeeper, + blockedAddresses, + authority.String(), + log.NewNopLogger(), + ) + + stakingKeeper := stakingkeeper.NewKeeper(cdc, runtime.NewKVStoreService(keys[stakingtypes.StoreKey]), + accountKeeper, bankKeeper, authority.String(), addresscodec.NewBech32Codec(sdk.Bech32PrefixValAddr), addresscodec.NewBech32Codec(sdk.Bech32PrefixConsAddr)) + distributionKeeper := distributionkeeper.NewKeeper(cdc, runtime.NewKVStoreService(keys[distributiontypes. + StoreKey]), accountKeeper, bankKeeper, stakingKeeper, distributiontypes.ModuleName, authority.String()) + lsmKeeper := lsmkeeper.NewKeeper(cdc, runtime.NewKVStoreService(keys[lsmtypes.StoreKey]), accountKeeper, + bankKeeper, stakingKeeper, distributionKeeper, authority.String()) + + authModule := auth.NewAppModule(cdc, accountKeeper, authsims.RandomGenesisAccounts, nil) + bankModule := bank.NewAppModule(cdc, bankKeeper, accountKeeper, nil) + stakingModule := staking.NewAppModule(cdc, stakingKeeper, accountKeeper, bankKeeper, nil) + distributionModule := distribution.NewAppModule(cdc, distributionKeeper, accountKeeper, bankKeeper, + stakingKeeper, nil) + lsmModule := lsm.NewAppModule(cdc, lsmKeeper, accountKeeper, bankKeeper, stakingKeeper) + + integrationApp := integration.NewIntegrationApp(newCtx, logger, keys, cdc, map[string]appmodule.AppModule{ + authtypes.ModuleName: authModule, + banktypes.ModuleName: bankModule, + distributiontypes.ModuleName: distributionModule, + stakingtypes.ModuleName: stakingModule, + lsmtypes.ModuleName: lsmModule, + }) + + sdkCtx := sdk.UnwrapSDKContext(integrationApp.Context()) + + stakingKeeper.SetHooks(stakingtypes.NewMultiStakingHooks( + lsmKeeper.Hooks(), + )) + + // Register staking MsgServer and QueryServer + stakingtypes.RegisterMsgServer(integrationApp.MsgServiceRouter(), stakingkeeper.NewMsgServerImpl(stakingKeeper)) + stakingtypes.RegisterQueryServer(integrationApp.QueryHelper(), stakingkeeper.NewQuerier(stakingKeeper)) + + // set default staking params + require.NoError(tb, stakingKeeper.SetParams(sdkCtx, stakingtypes.DefaultParams())) + + // Register lsm MsgServer and QueryServer + lsmtypes.RegisterMsgServer(integrationApp.MsgServiceRouter(), lsmkeeper.NewMsgServerImpl(lsmKeeper)) + lsmtypes.RegisterQueryServer(integrationApp.QueryHelper(), lsmkeeper.NewQuerier(lsmKeeper)) + + // set default lsm params + require.NoError(tb, lsmKeeper.SetParams(sdkCtx, lsmtypes.DefaultParams())) + + f := fixture{ + app: integrationApp, + sdkCtx: sdkCtx, + cdc: cdc, + keys: keys, + accountKeeper: accountKeeper, + bankKeeper: bankKeeper, + distributionKeeper: distributionKeeper, + stakingKeeper: stakingKeeper, + lsmKeeper: lsmKeeper, + } + + return &f +} + +func delegateCoinsFromAccount(ctx sdk.Context, sk stakingkeeper.Keeper, addr sdk.AccAddress, amount math.Int, + val stakingtypes.ValidatorI, +) error { + _, err := sk.Delegate(ctx, addr, amount, stakingtypes.Unbonded, val.(stakingtypes.Validator), true) + + return err +} + +func applyValidatorSetUpdates(t *testing.T, ctx sdk.Context, k *stakingkeeper.Keeper, + expectedUpdatesLen int, +) []abci.ValidatorUpdate { + t.Helper() + updates, err := k.ApplyAndReturnValidatorSetUpdates(ctx) + require.NoError(t, err) + if expectedUpdatesLen >= 0 { + require.Equal(t, expectedUpdatesLen, len(updates), "%v", updates) + } + return updates +} diff --git a/x/lsm/keeper/hooks.go b/x/lsm/keeper/hooks.go new file mode 100644 index 0000000000..dd487d02ac --- /dev/null +++ b/x/lsm/keeper/hooks.go @@ -0,0 +1,119 @@ +package keeper + +import ( + "context" + "errors" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + "github.com/cosmos/gaia/v22/x/lsm/types" +) + +// Wrapper struct +type Hooks struct { + k Keeper +} + +var _ stakingtypes.StakingHooks = Hooks{} + +// Create new lsm hooks +func (k Keeper) Hooks() Hooks { + return Hooks{k} +} + +// initialize liquid validator record +func (h Hooks) AfterValidatorCreated(ctx context.Context, valAddr sdk.ValAddress) error { + val, err := h.k.stakingKeeper.Validator(ctx, valAddr) + if err != nil { + return err + } + lVal := types.NewLiquidValidator(val.GetOperator()) + del, err := h.k.stakingKeeper.GetDelegation(ctx, sdk.AccAddress(val.GetOperator()), valAddr) + if err != nil && !errors.Is(err, stakingtypes.ErrNoDelegation) { + return err + } else if err == nil { + lVal.ValidatorBondShares = del.Shares + } + return h.k.SetLiquidValidator(ctx, lVal) +} + +func (h Hooks) AfterValidatorRemoved(ctx context.Context, _ sdk.ConsAddress, valAddr sdk.ValAddress) error { + return h.k.RemoveLiquidValidator(ctx, valAddr) +} + +func (h Hooks) BeforeDelegationCreated(ctx context.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) error { + return nil +} + +func (h Hooks) BeforeDelegationSharesModified(ctx context.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) error { + return nil +} + +func (h Hooks) AfterDelegationModified(ctx context.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) error { + if delAddr.Equals(sdk.AccAddress(valAddr)) { + del, err := h.k.stakingKeeper.GetDelegation(ctx, sdk.AccAddress(valAddr), valAddr) + if err != nil { + return err + } + lVal, err := h.k.GetLiquidValidator(ctx, valAddr) + if err != nil { + return err + } + lVal.ValidatorBondShares = del.Shares + return h.k.SetLiquidValidator(ctx, lVal) + } + return nil +} + +func (h Hooks) BeforeValidatorSlashed(ctx context.Context, valAddr sdk.ValAddress, fraction sdkmath.LegacyDec) error { + validator, err := h.k.stakingKeeper.Validator(ctx, valAddr) + if err != nil { + return err + } + liquidVal, err := h.k.GetLiquidValidator(ctx, valAddr) + if err != nil { + return err + } + initialLiquidTokens := validator.TokensFromShares(liquidVal.LiquidShares).TruncateInt() + slashedLiquidTokens := fraction.Mul(sdkmath.LegacyNewDecFromInt(initialLiquidTokens)) + + decrease := slashedLiquidTokens.TruncateInt() + if err := h.k.DecreaseTotalLiquidStakedTokens(ctx, decrease); err != nil { + // This only error's if the total liquid staked tokens underflows + // which would indicate there's a corrupted state where the validator has + // liquid tokens that are not accounted for in the global total + panic(err) + } + return nil +} + +func (h Hooks) BeforeValidatorModified(_ context.Context, _ sdk.ValAddress) error { + return nil +} + +func (h Hooks) AfterValidatorBonded(_ context.Context, _ sdk.ConsAddress, _ sdk.ValAddress) error { + return nil +} + +func (h Hooks) AfterValidatorBeginUnbonding(_ context.Context, _ sdk.ConsAddress, _ sdk.ValAddress) error { + return nil +} + +func (h Hooks) BeforeDelegationRemoved(ctx context.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) error { + if delAddr.Equals(sdk.AccAddress(valAddr)) { + lVal, err := h.k.GetLiquidValidator(ctx, valAddr) + if err != nil { + return err + } + lVal.ValidatorBondShares = sdkmath.LegacyZeroDec() + return h.k.SetLiquidValidator(ctx, lVal) + } + return nil +} + +func (h Hooks) AfterUnbondingInitiated(_ context.Context, _ uint64) error { + return nil +} diff --git a/x/lsm/keeper/liquid_stake.go b/x/lsm/keeper/liquid_stake.go index f003311b00..261e0be55d 100644 --- a/x/lsm/keeper/liquid_stake.go +++ b/x/lsm/keeper/liquid_stake.go @@ -67,7 +67,7 @@ func (k Keeper) DelegatorIsLiquidStaker(delegatorAddress sdk.AccAddress) bool { // CheckExceedsGlobalLiquidStakingCap checks if a liquid delegation would cause the // global liquid staking cap to be exceeded -// A liquid delegation is defined as either tokenized shares, or a delegation from an ICA Account +// A liquid delegation is defined as tokenized shares // The total stake is determined by the balance of the bonded pool // If the delegation's shares are already bonded (e.g. in the event of a tokenized share) // the tokens are already included in the bonded pool @@ -104,7 +104,7 @@ func (k Keeper) CheckExceedsGlobalLiquidStakingCap(ctx context.Context, tokens m // CheckExceedsValidatorBondCap checks if a liquid delegation to a validator would cause // the liquid shares to exceed the validator bond factor -// A liquid delegation is defined as either tokenized shares, or a delegation from an ICA Account +// A liquid delegation is defined as tokenized shares // Returns true if the cap is exceeded func (k Keeper) CheckExceedsValidatorBondCap(ctx context.Context, validator types.LiquidValidator, shares math.LegacyDec, @@ -124,7 +124,7 @@ func (k Keeper) CheckExceedsValidatorBondCap(ctx context.Context, validator type // CheckExceedsValidatorLiquidStakingCap checks if a liquid delegation could cause the // total liquid shares to exceed the liquid staking cap -// A liquid delegation is defined as either tokenized shares, or a delegation from an ICA Account +// A liquid delegation is defined as tokenized shares // If the liquid delegation's shares are already bonded (e.g. in the event of a tokenized share) // the tokens are already included in the validator's delegator shares // If the liquid delegation's shares are not bonded (e.g. normal delegation), @@ -506,9 +506,7 @@ func (k Keeper) RemoveExpiredTokenizeShareLocks(ctx context.Context, blockTime t // Calculates and sets the global liquid staked tokens and liquid shares by validator // The totals are determined by looping each delegation record and summing the stake -// if the delegator has a 32-length address. Checking for a 32-length address will capture -// ICA accounts, as well as tokenized delegations which are owned by module accounts -// under the hood +// if the delegator is the lsm. // This function must be called in the upgrade handler which onboards LSM func (k Keeper) RefreshTotalLiquidStaked(ctx context.Context) error { validators, err := k.stakingKeeper.GetAllValidators(ctx) @@ -546,7 +544,7 @@ func (k Keeper) RefreshTotalLiquidStaked(ctx context.Context) error { return err } - // If the delegator is either an ICA account or a tokenize share module account, + // If the delegator is a tokenize share module account, // the delegation should be considered to be associated with liquid staking // Consequently, the global number of liquid staked tokens, and the total // liquid shares on the validator should be incremented @@ -636,3 +634,9 @@ func (k Keeper) GetLiquidValidator(ctx context.Context, addr sdk.ValAddress) (va return types.UnmarshalValidator(k.cdc, value) } + +// RemoveLiquidValidator delete the LiquidValidator record +func (k Keeper) RemoveLiquidValidator(ctx context.Context, addr sdk.ValAddress) error { + store := k.storeService.OpenKVStore(ctx) + return store.Delete(types.GetLiquidValidatorKey(addr)) +} diff --git a/x/lsm/keeper/msg_server.go b/x/lsm/keeper/msg_server.go index 0661b86381..028ad0e90e 100644 --- a/x/lsm/keeper/msg_server.go +++ b/x/lsm/keeper/msg_server.go @@ -1,6 +1,7 @@ package keeper import ( + "bytes" "context" "errors" "fmt" @@ -85,17 +86,10 @@ func (k msgServer) TokenizeShares(goCtx context.Context, msg *types.MsgTokenizeS return nil, types.ErrTokenizeSharesDisabledForAccount.Wrapf("tokenization will be allowed at %s", unlockTime) } - /* todo--replace validatorbond - delegation, err := k.stakingKeeper.GetDelegation(ctx, delegatorAddress, valAddr) - if err != nil { - return nil, err - } - // ValidatorBond delegation is not allowed for tokenize share - if delegation.ValidatorBond { + if bytes.Equal(delegatorAddress, sdk.AccAddress(validator.OperatorAddress)) { return nil, types.ErrValidatorBondNotAllowedForTokenizeShare } - */ bondDenom, err := k.stakingKeeper.BondDenom(ctx) if err != nil { @@ -334,7 +328,7 @@ func (k msgServer) RedeemTokensForShares(goCtx context.Context, msg *types.MsgRe // Note: since delegation object has been changed from unbond call, it gets latest delegation _, err = k.stakingKeeper.GetDelegation(ctx, record.GetModuleAddress(), valAddr) - if err != nil && !errors.Is(err, types.ErrNoDelegation) { + if err != nil && !errors.Is(err, stakingtypes.ErrNoDelegation) { return nil, err } @@ -385,7 +379,6 @@ func (k msgServer) RedeemTokensForShares(goCtx context.Context, msg *types.MsgRe return nil, err } - /* todo--replace validatorbond // tokenized shares can be transferred from a validator that does not have validator bond to a delegator with validator bond // in that case we need to increase the validator bond shares (same as during msgServer.Delegate) newDelegation, err := k.stakingKeeper.GetDelegation(ctx, delegatorAddress, valAddr) @@ -393,12 +386,11 @@ func (k msgServer) RedeemTokensForShares(goCtx context.Context, msg *types.MsgRe return nil, err } - if newDelegation.ValidatorBond { + if newDelegation.DelegatorAddress == validator.OperatorAddress { if err := k.IncreaseValidatorBondShares(ctx, valAddr, shares); err != nil { return nil, err } } - */ ctx.EventManager().EmitEvent( sdk.NewEvent( diff --git a/x/lsm/types/liquid_validator.go b/x/lsm/types/liquid_validator.go index b3329714a8..67cfca1d3e 100644 --- a/x/lsm/types/liquid_validator.go +++ b/x/lsm/types/liquid_validator.go @@ -1,6 +1,19 @@ package types -import "github.com/cosmos/cosmos-sdk/codec" +import ( + "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/codec" +) + +// NewLiquidValidator constructs a new LiquidValidator +func NewLiquidValidator(operator string) LiquidValidator { + return LiquidValidator{ + OperatorAddress: operator, + ValidatorBondShares: math.LegacyZeroDec(), + LiquidShares: math.LegacyZeroDec(), + } +} func MustMarshalValidator(cdc codec.BinaryCodec, validator *LiquidValidator) []byte { return cdc.MustMarshal(validator)