Skip to content

Commit

Permalink
[ValSet-Pref] Allow migration of x/lockup uosmo to staking to a valse…
Browse files Browse the repository at this point in the history
…t preference (#3810)

* added core logic

* rebased

* fixed proto

* matts feedback

* matts feedback
  • Loading branch information
stackman27 authored Jan 19, 2023
1 parent 531b8c6 commit 0730d2c
Show file tree
Hide file tree
Showing 12 changed files with 838 additions and 41 deletions.
1 change: 1 addition & 0 deletions app/keepers/keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@ func (appKeepers *AppKeepers) InitNormalKeepers(
appKeepers.GetSubspace(valsetpreftypes.ModuleName),
appKeepers.StakingKeeper,
appKeepers.DistrKeeper,
appKeepers.LockupKeeper,
)

appKeepers.ValidatorSetPreferenceKeeper = &validatorSetPreferenceKeeper
Expand Down
17 changes: 17 additions & 0 deletions proto/osmosis/valset-pref/v1beta1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ service Msg {
// validator-set.
rpc WithdrawDelegationRewards(MsgWithdrawDelegationRewards)
returns (MsgWithdrawDelegationRewardsResponse);

// DelegateBondedTokens allows users to break the lockup bond and delegate
// osmo tokens to a predefined validator-set.
rpc DelegateBondedTokens(MsgDelegateBondedTokens)
returns (MsgDelegateBondedTokensResponse);
}

// MsgCreateValidatorSetPreference is a list that holds validator-set.
Expand Down Expand Up @@ -107,3 +112,15 @@ message MsgWithdrawDelegationRewards {
}

message MsgWithdrawDelegationRewardsResponse {}

// MsgDelegateBondedTokens breaks bonded lockup (by ID) of osmo, of
// length <= 2 weeks and takes all that osmo and delegates according to
// delegator's current validator set preference.
message MsgDelegateBondedTokens {
// delegator is the user who is trying to force unbond osmo and delegate.
string delegator = 1 [ (gogoproto.moretags) = "yaml:\"delegator\"" ];
// lockup id of osmo in the pool
uint64 lockID = 2;
}

message MsgDelegateBondedTokensResponse {}
11 changes: 11 additions & 0 deletions x/valset-pref/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package keeper

import (
sdk "github.com/cosmos/cosmos-sdk/types"

lockuptypes "github.com/osmosis-labs/osmosis/v14/x/lockup/types"
)

func (k Keeper) ValidateLockForForceUnlock(ctx sdk.Context, lockID uint64, delegatorAddr string) (*lockuptypes.PeriodLock, sdk.Int, error) {
return k.validateLockForForceUnlock(ctx, lockID, delegatorAddr)
}
3 changes: 3 additions & 0 deletions x/valset-pref/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,21 @@ type Keeper struct {
paramSpace paramtypes.Subspace
stakingKeeper types.StakingInterface
distirbutionKeeper types.DistributionKeeper
lockupKeeper types.LockupKeeper
}

func NewKeeper(storeKey sdk.StoreKey,
paramSpace paramtypes.Subspace,
stakingKeeper types.StakingInterface,
distirbutionKeeper types.DistributionKeeper,
lockupKeeper types.LockupKeeper,
) Keeper {
return Keeper{
storeKey: storeKey,
paramSpace: paramSpace,
stakingKeeper: stakingKeeper,
distirbutionKeeper: distirbutionKeeper,
lockupKeeper: lockupKeeper,
}
}

Expand Down
71 changes: 69 additions & 2 deletions x/valset-pref/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ package keeper_test
import (
"fmt"
"testing"
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"

"github.com/stretchr/testify/suite"

"github.com/osmosis-labs/osmosis/v14/app/apptesting"
appParams "github.com/osmosis-labs/osmosis/v14/app/params"
lockuptypes "github.com/osmosis-labs/osmosis/v14/x/lockup/types"
"github.com/osmosis-labs/osmosis/v14/x/valset-pref/types"

"github.com/stretchr/testify/suite"

valPref "github.com/osmosis-labs/osmosis/v14/x/valset-pref"
)

Expand Down Expand Up @@ -199,7 +202,71 @@ func (suite *KeeperTestSuite) SetupValidatorsAndDelegations() ([]string, []types
amountToFund := sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 100_000_000)} // 100 osmo

return valAddrs, preferences, amountToFund
}

func (suite *KeeperTestSuite) SetupLocks(delegator sdk.AccAddress) []lockuptypes.PeriodLock {
// create a pool with uosmo
locks := []lockuptypes.PeriodLock{}
// Setup lock
coinsToLock := sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 10_000_000)}
osmoToLock := sdk.Coins{sdk.NewInt64Coin(appParams.BaseCoinUnit, 10_000_000)}
multipleCoinsToLock := sdk.Coins{coinsToLock[0], osmoToLock[0]}
suite.FundAcc(delegator, sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 100_000_000), sdk.NewInt64Coin(appParams.BaseCoinUnit, 100_000_000)})

// lock with osmo
twoWeekDuration, err := time.ParseDuration("336h")
suite.Require().NoError(err)
workingLock, err := suite.App.LockupKeeper.CreateLock(suite.Ctx, delegator, osmoToLock, twoWeekDuration)
suite.Require().NoError(err)

locks = append(locks, workingLock)

// locking with stake denom instead of osmo denom
stakeDenomLock, err := suite.App.LockupKeeper.CreateLock(suite.Ctx, delegator, coinsToLock, twoWeekDuration)
suite.Require().NoError(err)

locks = append(locks, stakeDenomLock)

// lock case where lock owner != delegator
suite.FundAcc(sdk.AccAddress([]byte("addr5---------------")), osmoToLock)
lockWithDifferentOwner, err := suite.App.LockupKeeper.CreateLock(suite.Ctx, sdk.AccAddress([]byte("addr5---------------")), osmoToLock, twoWeekDuration)
suite.Require().NoError(err)

locks = append(locks, lockWithDifferentOwner)

// lock case where the duration != <= 2 weeks
morethanTwoWeekDuration, err := time.ParseDuration("337h")
suite.Require().NoError(err)
maxDurationLock, err := suite.App.LockupKeeper.CreateLock(suite.Ctx, delegator, osmoToLock, morethanTwoWeekDuration)
suite.Require().NoError(err)

locks = append(locks, maxDurationLock)

// unbonding locks
unbondingLocks, err := suite.App.LockupKeeper.CreateLock(suite.Ctx, delegator, osmoToLock, twoWeekDuration)
suite.Require().NoError(err)

err = suite.App.LockupKeeper.BeginUnlock(suite.Ctx, unbondingLocks.ID, nil)
suite.Require().NoError(err)

locks = append(locks, unbondingLocks)

// synthetic locks
syntheticLocks, err := suite.App.LockupKeeper.CreateLock(suite.Ctx, delegator, osmoToLock, twoWeekDuration)
suite.Require().NoError(err)

err = suite.App.LockupKeeper.CreateSyntheticLockup(suite.Ctx, syntheticLocks.ID, "uosmo", time.Minute, true)
suite.Require().NoError(err)

locks = append(locks, syntheticLocks)

// multiple asset lock
multiassetLock, err := suite.App.LockupKeeper.CreateLock(suite.Ctx, delegator, multipleCoinsToLock, twoWeekDuration)
suite.Require().NoError(err)

locks = append(locks, multiassetLock)

return locks
}

func TestKeeperTestSuite(t *testing.T) {
Expand Down
35 changes: 35 additions & 0 deletions x/valset-pref/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"

sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/osmosis-labs/osmosis/v14/x/valset-pref/types"
)

Expand Down Expand Up @@ -34,6 +35,7 @@ func (server msgServer) SetValidatorSetPreference(goCtx context.Context, msg *ty
return &types.MsgSetValidatorSetPreferenceResponse{}, nil
}

// DelegateToValidatorSet delegates to a delegators existing validator-set.
func (server msgServer) DelegateToValidatorSet(goCtx context.Context, msg *types.MsgDelegateToValidatorSet) (*types.MsgDelegateToValidatorSetResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

Expand All @@ -45,6 +47,7 @@ func (server msgServer) DelegateToValidatorSet(goCtx context.Context, msg *types
return &types.MsgDelegateToValidatorSetResponse{}, nil
}

// UndelegateFromValidatorSet undelegates {coin} amount from the validator set.
func (server msgServer) UndelegateFromValidatorSet(goCtx context.Context, msg *types.MsgUndelegateFromValidatorSet) (*types.MsgUndelegateFromValidatorSetResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

Expand All @@ -56,6 +59,7 @@ func (server msgServer) UndelegateFromValidatorSet(goCtx context.Context, msg *t
return &types.MsgUndelegateFromValidatorSetResponse{}, nil
}

// RedelegateValidatorSet allows delegators to set a new validator set and switch validators.
func (server msgServer) RedelegateValidatorSet(goCtx context.Context, msg *types.MsgRedelegateValidatorSet) (*types.MsgRedelegateValidatorSetResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

Expand Down Expand Up @@ -87,6 +91,7 @@ func (server msgServer) RedelegateValidatorSet(goCtx context.Context, msg *types
return &types.MsgRedelegateValidatorSetResponse{}, nil
}

// WithdrawDelegationRewards withdraws all the delegation rewards from the validator in the val-set.
func (server msgServer) WithdrawDelegationRewards(goCtx context.Context, msg *types.MsgWithdrawDelegationRewards) (*types.MsgWithdrawDelegationRewardsResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

Expand All @@ -97,3 +102,33 @@ func (server msgServer) WithdrawDelegationRewards(goCtx context.Context, msg *ty

return &types.MsgWithdrawDelegationRewardsResponse{}, nil
}

// DelegateBondedTokens force unlocks bonded uosmo and stakes according to your current validator set preference.
func (server msgServer) DelegateBondedTokens(goCtx context.Context, msg *types.MsgDelegateBondedTokens) (*types.MsgDelegateBondedTokensResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

// get the existingValSet if it exists, if not check existingStakingPosition and return it
_, err := server.keeper.GetDelegationPreferences(ctx, msg.Delegator)
if err != nil {
return nil, fmt.Errorf("user %s doesn't have validator set", msg.Delegator)
}

// Message 1: force unlock bonded osmo tokens.
unlockedOsmoToken, err := server.keeper.ForceUnlockBondedOsmo(ctx, msg.LockID, msg.Delegator)
if err != nil {
return nil, err
}

delegator, err := sdk.AccAddressFromBech32(msg.Delegator)
if err != nil {
return nil, err
}

// Message 2: Perform osmo token delegation.
_, err = server.DelegateToValidatorSet(goCtx, types.NewMsgDelegateToValidatorSet(delegator, unlockedOsmoToken))
if err != nil {
return nil, err
}

return &types.MsgDelegateBondedTokensResponse{}, nil
}
112 changes: 112 additions & 0 deletions x/valset-pref/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package keeper_test
import (
sdk "github.com/cosmos/cosmos-sdk/types"

appParams "github.com/osmosis-labs/osmosis/v14/app/params"
valPref "github.com/osmosis-labs/osmosis/v14/x/valset-pref"
"github.com/osmosis-labs/osmosis/v14/x/valset-pref/types"
)
Expand Down Expand Up @@ -642,7 +643,118 @@ func (suite *KeeperTestSuite) TestWithdrawDelegationRewards() {

} else {
suite.Require().Error(err)
}
})
}
}

func (suite *KeeperTestSuite) TestDelegateBondedTokens() {
suite.SetupTest()

testLock := suite.SetupLocks(sdk.AccAddress([]byte("addr1---------------")))

tests := []struct {
name string
delegator sdk.AccAddress
lockId uint64
expectedUnlockedOsmo sdk.Coin
expectedDelegations []sdk.Dec
setValSet bool
expectPass bool
}{
{
name: "DelegateBondedTokens with existing osmo denom lockId, bonded and <= 2 weeks bond duration",
delegator: sdk.AccAddress([]byte("addr1---------------")),
lockId: testLock[0].ID,
expectedUnlockedOsmo: sdk.NewCoin(appParams.BaseCoinUnit, sdk.NewInt(60_000_000)), // delegator has 100osmo and creates 5 locks 10osmo each, forceUnlock only 1 lock
expectedDelegations: []sdk.Dec{sdk.NewDec(2_000_000), sdk.NewDec(3_300_000), sdk.NewDec(1_200_000), sdk.NewDec(3_500_000)},
setValSet: true,
expectPass: true,
},
{
name: "DelegateBondedTokens with existing stake denom lockId, bonded and <= 2 weeks bond duration",
delegator: sdk.AccAddress([]byte("addr1---------------")),
lockId: testLock[1].ID,
expectPass: false,
},
{
name: "DelegateBondedTokens with non existing lockId",
delegator: sdk.AccAddress([]byte("addr1---------------")),
lockId: 10,
expectPass: false,
},
{
name: "DelegateBondedTokens with lockOwner != delegatorOwner",
delegator: sdk.AccAddress([]byte("addr1---------------")),
lockId: testLock[2].ID,
expectPass: false,
},
{
name: "DelegateBondedTokens with lock duration > 2 weeks",
delegator: sdk.AccAddress([]byte("addr1---------------")),
lockId: testLock[3].ID,
expectPass: false,
},
{
name: "DelegateBondedTokens with non bonded lockId",
delegator: sdk.AccAddress([]byte("addr1---------------")),
lockId: testLock[4].ID,
expectPass: false,
},
{
name: "DelegateBondedTokens with synthetic locks",
delegator: sdk.AccAddress([]byte("addr1---------------")),
lockId: testLock[5].ID,
expectPass: false,
},
{
name: "DelegateBondedTokens with multiple asset lock",
delegator: sdk.AccAddress([]byte("addr1---------------")),
lockId: testLock[6].ID,
expectPass: false,
},
}

for _, test := range tests {
suite.Run(test.name, func() {
// setup message server
msgServer := valPref.NewMsgServerImpl(suite.App.ValidatorSetPreferenceKeeper)
c := sdk.WrapSDKContext(suite.Ctx)

// creates a validator preference list to delegate to
preferences := suite.PrepareDelegateToValidatorSet()

if test.setValSet {
// SetValidatorSetPreference sets a new list of val-set
_, err := msgServer.SetValidatorSetPreference(c, types.NewMsgSetValidatorSetPreference(test.delegator, preferences))
suite.Require().NoError(err)
}

_, err := msgServer.DelegateBondedTokens(c, types.NewMsgDelegateBondedTokens(test.delegator, test.lockId))
if test.expectPass {
suite.Require().NoError(err)

// check that the lock has been successfully unlocked
// existingLocks should not contain the current lock
existingLocks, err := suite.App.LockupKeeper.GetPeriodLocks(suite.Ctx)

suite.Require().NoError(err)
suite.Require().Equal(len(existingLocks), len(testLock)-1)

balance := suite.App.BankKeeper.GetBalance(suite.Ctx, test.delegator, appParams.BaseCoinUnit)
suite.Require().Equal(balance, test.expectedUnlockedOsmo)

// check if delegation has been done by checking if expectedDelegations matches after delegation
for i, val := range preferences {
valAddr, err := sdk.ValAddressFromBech32(val.ValOperAddress)
suite.Require().NoError(err)

// guarantees that the delegator exists because we check it in DelegateToValidatorSet
del, _ := suite.App.StakingKeeper.GetDelegation(suite.Ctx, test.delegator, valAddr)
suite.Require().Equal(del.Shares, test.expectedDelegations[i])
}
} else {
suite.Require().Error(err)
}
})
}
Expand Down
10 changes: 9 additions & 1 deletion x/valset-pref/types/expected_interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (

sdk "github.com/cosmos/cosmos-sdk/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"

lockuptypes "github.com/osmosis-labs/osmosis/v14/x/lockup/types"
)

// StakingInterface expected staking keeper.
Expand All @@ -23,10 +25,16 @@ type BankKeeper interface {
GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin
}

// For testing only
type DistributionKeeper interface {
WithdrawDelegationRewards(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) (sdk.Coins, error)
IncrementValidatorPeriod(ctx sdk.Context, val stakingtypes.ValidatorI) uint64
CalculateDelegationRewards(ctx sdk.Context, val stakingtypes.ValidatorI, del stakingtypes.DelegationI, endingPeriod uint64) (rewards sdk.DecCoins)
AllocateTokensToValidator(ctx sdk.Context, val stakingtypes.ValidatorI, tokens sdk.DecCoins)
}
type LockupKeeper interface {
GetLockByID(ctx sdk.Context, lockID uint64) (*lockuptypes.PeriodLock, error)
GetAllSyntheticLockupsByLockup(ctx sdk.Context, lockID uint64) []lockuptypes.SyntheticLock
ForceUnlock(ctx sdk.Context, lock lockuptypes.PeriodLock) error
BeginUnlock(ctx sdk.Context, lockID uint64, coins sdk.Coins) error
GetPeriodLocks(ctx sdk.Context) ([]lockuptypes.PeriodLock, error)
}
Loading

0 comments on commit 0730d2c

Please sign in to comment.