Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: facilitate the tokenization of vested delegation in the LSM module #19614

Merged
merged 10 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0
google.golang.org/grpc v1.60.1
google.golang.org/protobuf v1.32.0
gopkg.in/yaml.v2 v2.4.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are there 2 imports for gopkg.in/yaml.v2 v2.4.0?

Copy link
Contributor Author

@sainoe sainoe Mar 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Was probably duplicated while resolving conflicts with feature/v0.47.x-ics-lsm.
Removed by 987a86d.

gotest.tools/v3 v3.5.1
pgregory.net/rapid v1.1.0
gopkg.in/yaml.v2 v2.4.0
sigs.k8s.io/yaml v1.4.0
)

Expand Down
114 changes: 114 additions & 0 deletions tests/integration/staking/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1583,6 +1583,120 @@ func TestICADelegateUndelegate(t *testing.T) {
require.Equal(t, sdk.ZeroDec(), validator.LiquidShares, "validator liquid shares after undelegation")
}

func TestTokenizeAndRedeemVestedDelegation(t *testing.T) {
_, app, ctx := createTestInput(t)
var (
bankKeeper = app.BankKeeper
accountKeeper = app.AccountKeeper
stakingKeeper = app.StakingKeeper
)

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
pubkey := secp256k1.GenPrivKey().PubKey()
baseAcc := authtypes.NewBaseAccount(addrAcc1, pubkey, 0, 0)
continuousVestingAccount := vestingtypes.NewContinuousVestingAccount(
baseAcc,
originalVesting,
startTime.Unix(),
endTime.Unix(),
)
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
app.StakingKeeper.SetValidator(ctx, val1)
app.StakingKeeper.SetValidatorByPowerIndex(ctx, val1)
err := app.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)

_, found := stakingKeeper.GetDelegation(ctx, addrAcc1, addrVal1)
require.True(t, found)

// 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 := keeper.NewMsgServerImpl(stakingKeeper)

// 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(sdk.WrapSDKContext(ctx), &types.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(sdk.WrapSDKContext(ctx), &types.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(sdk.WrapSDKContext(ctx),
&types.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())
}

// Helper function to create 32-length account
// Used to mock an liquid staking provider's ICA account
func createICAAccount(ctx sdk.Context, ak accountkeeper.AccountKeeper) sdk.AccAddress {
Expand Down
28 changes: 28 additions & 0 deletions x/staking/keeper/liquid_stake.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
vesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/exported"
"github.com/cosmos/cosmos-sdk/x/staking/types"
)

Expand Down Expand Up @@ -441,3 +442,30 @@ func (k Keeper) RefreshTotalLiquidStaked(ctx sdk.Context) error {

return nil
}

// CheckVestedDelegationInVestingAccount verifies whether the provided vesting account
// holds a vested delegation for an equal or greater amount of the specified coin
// at the given block time.
//
// Note that this function facilitates a specific use-case in the LSM module for tokenizing vested delegations.
// For more details, see https://github.com/cosmos/gaia/issues/2877.
func CheckVestedDelegationInVestingAccount(account vesting.VestingAccount, blockTime time.Time, coin sdk.Coin) bool {
// Get the vesting coins at the current block time
vestingAmount := account.GetVestingCoins(blockTime).AmountOf(coin.Denom)

// Note that the "DelegatedVesting" and "DelegatedFree" values
// were computed during the last delegation or undelegation operation
delVestingAmount := account.GetDelegatedVesting().AmountOf(coin.Denom)
delVested := account.GetDelegatedFree()

// Calculate the new vested delegated coins
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to perform any calc here? Shouldn't the vesting type give us the amount of total vested?

Copy link
Contributor Author

@sainoe sainoe Mar 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The LSM module needs the total delegations vested at the current block time to know how many can be tokenized. Since the DelegatedVesting and DelegatedFree are lazily computed and therefore not rebalanced according to the new vested coins over time, we need these extra calculations.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, right we don't have an API for that. Would be nice to have this on the vesting types, but since it's legacy now maybe not worth it.

x := sdk.MinInt(vestingAmount.Sub(delVestingAmount), math.ZeroInt())

// Add the newly vested delegated coins to the existing delegated vested amount
if !x.IsZero() {
delVested = delVested.Add(sdk.NewCoin(coin.Denom, x.Abs()))
}

// Check if the total delegated vested amount is greater than or equal to the specified coin amount
return delVested.AmountOf(coin.Denom).GTE(coin.Amount)
}
101 changes: 101 additions & 0 deletions x/staking/keeper/liquid_stake_test.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package keeper_test

import (
"testing"
"time"

"cosmossdk.io/math"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/address"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types"
"github.com/cosmos/cosmos-sdk/x/staking/keeper"
"github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/stretchr/testify/require"
)

// Tests Set/Get TotalLiquidStakedTokens
Expand Down Expand Up @@ -808,3 +813,99 @@ func (s *KeeperTestSuite) TestDelegatorIsLiquidStaker() {
require.False(keeper.DelegatorIsLiquidStaker(baseAccountAddress), "base account")
require.True(keeper.DelegatorIsLiquidStaker(icaAccountAddress), "ICA module account")
}

func TestCheckVestedDelegationInVestingAccount(t *testing.T) {

var (
vestingAcct *vestingtypes.ContinuousVestingAccount
startTime = time.Now()
endTime = startTime.Add(24 * time.Hour)
originalVesting = sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(100_000)))
)

testCases := []struct {
name string
setupAcct func()
blockTime time.Time
coinRequired sdk.Coin
expRes bool
}{
{
name: "vesting account has zero delegations",
setupAcct: func() {},
blockTime: endTime,
coinRequired: sdk.NewCoin(sdk.DefaultBondDenom, sdk.OneInt()),
expRes: false,
},
{
name: "vested delegations exist but for a different coin",
setupAcct: func() {
vestingAcct.DelegatedFree = sdk.NewCoins(sdk.NewCoin("uatom", math.NewInt(100_000)))
},
blockTime: endTime,
coinRequired: sdk.NewCoin(sdk.DefaultBondDenom, sdk.OneInt()),
expRes: false,
},
{
name: "all delegations are vesting",
setupAcct: func() {
vestingAcct.DelegatedVesting = vestingAcct.OriginalVesting
},
blockTime: startTime,
coinRequired: sdk.NewCoin(sdk.DefaultBondDenom, sdk.OneInt()),
expRes: false,
},
{
name: "not enough vested coin",
setupAcct: func() {
vestingAcct.DelegatedFree = sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(80_000)))
},
blockTime: endTime,
coinRequired: sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(100_000)),
expRes: false,
},
{
name: "account is vested and have vested delegations",
setupAcct: func() {
vestingAcct.DelegatedFree = vestingAcct.OriginalVesting
},
blockTime: endTime,
coinRequired: sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(100_000)),
expRes: true,
},
{
name: "vesting account partially vested and have vesting and vested delegations",
setupAcct: func() {
vestingAcct.DelegatedFree = sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(50_000)))
vestingAcct.DelegatedVesting = sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(50_000)))
},
blockTime: startTime.Add(18 * time.Hour), // vest 3/4 vesting period
coinRequired: sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(75_000)),

expRes: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
baseAcc := authtypes.NewBaseAccount(sdk.AccAddress([]byte("addr")), secp256k1.GenPrivKey().PubKey(), 0, 0)
vestingAcct = vestingtypes.NewContinuousVestingAccount(
baseAcc,
originalVesting,
startTime.Unix(),
endTime.Unix(),
)

tc.setupAcct()

require.Equal(
t,
tc.expRes, keeper.CheckVestedDelegationInVestingAccount(
vestingAcct,
tc.blockTime,
tc.coinRequired,
),
)
})
}
}
4 changes: 2 additions & 2 deletions x/staking/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -702,8 +702,8 @@ func (k msgServer) TokenizeShares(goCtx context.Context, msg *types.MsgTokenizeS
// the tokenize share amount and execute further tokenize share process
// tokenize share is reducing unlocked tokens delegation from the vesting account and further process
// is not causing issues
delFree := acc.GetDelegatedFree().AmountOf(msg.Amount.Denom)
if delFree.LT(msg.Amount.Amount) {

if !CheckVestedDelegationInVestingAccount(acc, ctx.BlockTime(), msg.Amount) {
return nil, types.ErrExceedingFreeVestingDelegations
}
}
Expand Down
Loading