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

Force undelegate superfluid staking position by whitelisted address #5861

Closed
wants to merge 12 commits into from
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* [#5534](https://github.com/osmosis-labs/osmosis/pull/5534) fix: fix the account number of x/tokenfactory module account
* [#5750](https://github.com/osmosis-labs/osmosis/pull/5750) feat: add cli commmand for converting proto structs to proto marshalled bytes
* [#5861](https://github.com/osmosis-labs/osmosis/pull/5861) feat: force undelegate superfluid staking position by whitelisted address

## v16.0.0
Osmosis Labs is excited to announce the release of v16.0.0, a major upgrade that includes a number of new features and improvements like introduction of new modules, updates existing APIs, and dependency updates. This upgrade aims to enhance capital efficiency by introducing SuperCharged Liquidity, introduce custom liquidity pools backed by CosmWasm smart contracts, and improve overall functionality.
Expand Down
6 changes: 6 additions & 0 deletions proto/osmosis/superfluid/params.proto
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,10 @@ message Params {
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];

// force_superfluid_undelegate_allowed_addresses is a list of addresses that
// are allowed to force undelegate superfluid staking positions.
repeated string force_superfluid_undelegate_allowed_addresses = 2
[ (gogoproto.moretags) =
"yaml:\"force_superfluid_undelegate_allowed_addresses\"" ];
}
12 changes: 12 additions & 0 deletions proto/osmosis/superfluid/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ service Msg {
rpc SuperfluidUndelegate(MsgSuperfluidUndelegate)
returns (MsgSuperfluidUndelegateResponse);

rpc ForceSuperfluidUndelegate(MsgForceSuperfluidUndelegate)
returns (MsgForceSuperfluidUndelegateResponse);

// Execute superfluid redelegation for a lockup
// rpc SuperfluidRedelegate(MsgSuperfluidRedelegate) returns
// (MsgSuperfluidRedelegateResponse);
Expand Down Expand Up @@ -71,6 +74,15 @@ message MsgSuperfluidUndelegate {
}
message MsgSuperfluidUndelegateResponse {}

message MsgForceSuperfluidUndelegate {
option (amino.name) = "osmosis/force-superfluid-undelegate";

string sender = 1 [ (gogoproto.moretags) = "yaml:\"sender\"" ];
uint64 lock_id = 2;
}

message MsgForceSuperfluidUndelegateResponse {}

message MsgSuperfluidUnbondLock {
option (amino.name) = "osmosis/superfluid-unbond-lock";

Expand Down
20 changes: 20 additions & 0 deletions x/superfluid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,26 @@ type MsgSuperfluidUndelegate struct {
- Immediately burn undelegated `Osmo`
- Delete the connection between `lockID` and `IntermediaryAccount`

### Superfluid Force Undelegate

```{.go}
type MsgForceSuperfluidUndelegate struct {
Sender string
LockId uint64
}
```

**State Modifications:**

- Check if `Sender` is in the list of `ForceSuperfluidUndelegateAllowedAddresses` specified in params store
- Lookup `lock` by `LockID`
- Validate if the lock has single coin, but does not check if `Sender` is `lock.Owner`
- Get the `IntermediaryAccount` for this `lockID`
- Delete the connection between `lockID` and `IntermediaryAccount`
- Delete the `SyntheticLockup` associated to this `lockID` + `ValAddr`
pair
- Undelegates the superfluid staking position associated with the `lockID` and burns the underlying osmo tokens

### Lock and Superfluid Delegate

```{.go}
Expand Down
17 changes: 17 additions & 0 deletions x/superfluid/keeper/internal/events/emit.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,23 @@ func newSuperfluidUndelegateEvent(lockId uint64) sdk.Event {
)
}

func EmitForceSuperfluidUndelegateEvent(ctx sdk.Context, lockId uint64) {
if ctx.EventManager() == nil {
return
}

ctx.EventManager().EmitEvents(sdk.Events{
newForceSuperfluidUndelegateEvent(lockId),
})
}

func newForceSuperfluidUndelegateEvent(lockId uint64) sdk.Event {
return sdk.NewEvent(
types.TypeEvtForceSuperfluidUndelegate,
sdk.NewAttribute(types.AttributeLockId, fmt.Sprintf("%d", lockId)),
)
}

func EmitSuperfluidUnbondLockEvent(ctx sdk.Context, lockId uint64) {
if ctx.EventManager() == nil {
return
Expand Down
35 changes: 35 additions & 0 deletions x/superfluid/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package keeper
import (
"context"
"encoding/json"
"fmt"
"strconv"
"time"

Expand All @@ -13,6 +14,9 @@ import (

"github.com/osmosis-labs/osmosis/v16/x/superfluid/keeper/internal/events"
"github.com/osmosis-labs/osmosis/v16/x/superfluid/types"

errorsmod "cosmossdk.io/errors"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)

type msgServer struct {
Expand Down Expand Up @@ -66,6 +70,37 @@ func (server msgServer) SuperfluidUndelegate(goCtx context.Context, msg *types.M
return &types.MsgSuperfluidUndelegateResponse{}, err
}

// ForceSuperfluidUndelegate is a method to force undelegate superfluid staked asset.
// This method is only allowed to be called whitelisted addresses.
// With whitelisted addresses, we can force undelegate superfluid staked asset without being a lock owner.
func (server msgServer) ForceSuperfluidUndelegate(goCtx context.Context, msg *types.MsgForceSuperfluidUndelegate) (*types.MsgForceSuperfluidUndelegateResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

// check if the sender is allowed to force undelegate
forceUndelegateAllowedAddresses := server.keeper.GetParams(ctx).ForceSuperfluidUndelegateAllowedAddresses
allowed := false
for _, addr := range forceUndelegateAllowedAddresses {
if addr == msg.Sender {
allowed = true
break
}
}

// return error if the sender is not allowed to force undelegate
if !allowed {
return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, fmt.Sprintf("msg sender (%s) is not allowed to force undelegate superfluid staking position", msg.Sender))
}

// perform force undelegate
_, err := server.keeper.ForceSuperfluidUndelegate(ctx, msg.Sender, msg.LockId)
if err != nil {
return nil, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, err.Error())
}

events.EmitForceSuperfluidUndelegateEvent(ctx, msg.LockId)
return &types.MsgForceSuperfluidUndelegateResponse{}, nil
}

// SuperfluidRedelegate is a method to redelegate superfluid staked asset into a different validator.
// Currently this feature is not supported.
// func (server msgServer) SuperfluidRedelegate(goCtx context.Context, msg *types.MsgSuperfluidRedelegate) (*types.MsgSuperfluidRedelegateResponse, error) {
Expand Down
103 changes: 103 additions & 0 deletions x/superfluid/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package keeper_test
import (
"time"

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

Expand Down Expand Up @@ -135,6 +136,108 @@ func (s *KeeperTestSuite) TestMsgSuperfluidUndelegate() {
}
}

func (s *KeeperTestSuite) TestMsgForceSuperfluidUndelegate() {

forceSuperfluidUndelegateAllowedAddresses := []string{
sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()).String(),
sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()).String(),
sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()).String(),
}

type param struct {
lockOwner sdk.AccAddress
undelegator sdk.AccAddress
duration time.Duration
}

tests := []struct {
name string
param param
expectPass bool
}{
{
name: "unknown undelegator",
param: param{
lockOwner: sdk.AccAddress([]byte("addr1---------------")), // setup wallet
undelegator: sdk.AccAddress([]byte("addr2---------------")),
duration: time.Hour * 504,
},
expectPass: false,
},
{
name: "allowed undelegator 0",
param: param{
lockOwner: sdk.AccAddress([]byte("addr1---------------")), // setup wallet
undelegator: sdk.MustAccAddressFromBech32(forceSuperfluidUndelegateAllowedAddresses[0]),
duration: time.Hour * 504,
},
expectPass: true,
},
{
name: "allowed undelegator 1",
param: param{
lockOwner: sdk.AccAddress([]byte("addr1---------------")), // setup wallet
undelegator: sdk.MustAccAddressFromBech32(forceSuperfluidUndelegateAllowedAddresses[0]),
duration: time.Hour * 504,
},
expectPass: true,
},
{
name: "allowed undelegator 2",
param: param{
lockOwner: sdk.AccAddress([]byte("addr1---------------")), // setup wallet
undelegator: sdk.MustAccAddressFromBech32(forceSuperfluidUndelegateAllowedAddresses[0]),
duration: time.Hour * 504,
},
expectPass: true,
},
}

for _, test := range tests {
s.Run(test.name, func() {
s.SetupTest()

// set params with allowed addresses
params := s.App.SuperfluidKeeper.GetParams(s.Ctx)
s.App.SuperfluidKeeper.SetParams(s.Ctx, types.Params{
MinimumRiskFactor: params.MinimumRiskFactor,
ForceSuperfluidUndelegateAllowedAddresses: forceSuperfluidUndelegateAllowedAddresses,
})

params = s.App.SuperfluidKeeper.GetParams(s.Ctx)

lockupMsgServer := lockupkeeper.NewMsgServerImpl(s.App.LockupKeeper)
c := sdk.WrapSDKContext(s.Ctx)

denoms, _ := s.SetupGammPoolsAndSuperfluidAssets([]sdk.Dec{sdk.NewDec(20), sdk.NewDec(20)})

coinsToLock := sdk.NewCoins(sdk.NewCoin(denoms[0], sdk.NewInt(20)))

s.FundAcc(test.param.lockOwner, coinsToLock)
resp, err := lockupMsgServer.LockTokens(c, lockuptypes.NewMsgLockTokens(test.param.lockOwner, test.param.duration, coinsToLock))
s.Require().NoError(err)

valAddrs := s.SetupValidators([]stakingtypes.BondStatus{stakingtypes.Bonded})

msgServer := keeper.NewMsgServerImpl(s.App.SuperfluidKeeper)

// delegate
_, err = msgServer.SuperfluidDelegate(c, types.NewMsgSuperfluidDelegate(test.param.lockOwner, resp.ID, valAddrs[0]))
s.Require().NoError(err)

// force undelegate
_, err = msgServer.ForceSuperfluidUndelegate(c, types.NewMsgForceSuperfluidUndelegate(test.param.undelegator, resp.ID))

if test.expectPass {
s.Require().NoError(err)
s.AssertEventEmitted(s.Ctx, types.TypeEvtForceSuperfluidUndelegate, 1)
} else {
s.Require().Error(err)
}
})
}
}

func (s *KeeperTestSuite) TestMsgCreateFullRangePositionAndSuperfluidDelegate() {
defaultSender := s.TestAccs[0]
type param struct {
Expand Down
27 changes: 22 additions & 5 deletions x/superfluid/keeper/stake.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ func (k Keeper) validateLockForSF(ctx sdk.Context, lock *lockuptypes.PeriodLock,
if lock.Owner != sender {
return lockuptypes.ErrNotLockOwner
}

err := k.validateLockForSFWithoutCheckingOwner(ctx, lock)
if err != nil {
return err
}
return nil
}

// basic validation for locks to be eligible for superfluid delegation. This includes checking
func (k Keeper) validateLockForSFWithoutCheckingOwner(ctx sdk.Context, lock *lockuptypes.PeriodLock) error {
if lock.Coins.Len() != 1 {
return types.ErrMultipleCoinsLockupNotSupported
}
Expand Down Expand Up @@ -242,20 +252,27 @@ func (k Keeper) SuperfluidDelegate(ctx sdk.Context, sender string, lockID uint64
return k.mintOsmoTokensAndDelegate(ctx, amount, acc)
}

func (k Keeper) ForceSuperfluidUndelegate(ctx sdk.Context, sender string, lockID uint64) (types.SuperfluidIntermediaryAccount, error) {
validateHasSingleCoin := func(ctx sdk.Context, lock *lockuptypes.PeriodLock, _ string) error {
return k.validateLockForSFWithoutCheckingOwner(ctx, lock)
}
return k.undelegateCommon(ctx, sender, lockID, validateHasSingleCoin)
}

// undelegateCommon is a helper function for SuperfluidUndelegate and superfluidUndelegateToConcentratedPosition.
// It performs the following tasks:
// - checks that the lock is valid for superfluid staking
// - checks that the lock is valid with provided validation function
// - gets the intermediary account associated with the lock id
// - deletes the connection between the lock id and the intermediary account
// - deletes the synthetic lockup associated with the lock id
// - undelegates the superfluid staking position associated with the lock id and burns the underlying osmo tokens
// - returns the intermediary account
func (k Keeper) undelegateCommon(ctx sdk.Context, sender string, lockID uint64) (types.SuperfluidIntermediaryAccount, error) {
func (k Keeper) undelegateCommon(ctx sdk.Context, sender string, lockID uint64, validate func(sdk.Context, *lockuptypes.PeriodLock, string) error) (types.SuperfluidIntermediaryAccount, error) {
lock, err := k.lk.GetLockByID(ctx, lockID)
if err != nil {
return types.SuperfluidIntermediaryAccount{}, err
}
err = k.validateLockForSF(ctx, lock, sender)
err = validate(ctx, lock, sender)
if err != nil {
return types.SuperfluidIntermediaryAccount{}, err
}
Expand Down Expand Up @@ -292,7 +309,7 @@ func (k Keeper) undelegateCommon(ctx sdk.Context, sender string, lockID uint64)
// where it is burnt. Note that this method does not include unbonding the lock
// itself.
func (k Keeper) SuperfluidUndelegate(ctx sdk.Context, sender string, lockID uint64) error {
intermediaryAcc, err := k.undelegateCommon(ctx, sender, lockID)
intermediaryAcc, err := k.undelegateCommon(ctx, sender, lockID, k.validateLockForSF)
if err != nil {
return err
}
Expand All @@ -305,7 +322,7 @@ func (k Keeper) SuperfluidUndelegate(ctx sdk.Context, sender string, lockID uint
// want to perform more operations prior to creating a lock. Once the actual lock is created, the synthetic lockup representing the unstaking side
// should eventually be created as well. Use this function with caution to avoid accidentally missing synthetic lock creation.
func (k Keeper) SuperfluidUndelegateToConcentratedPosition(ctx sdk.Context, sender string, gammLockID uint64) (types.SuperfluidIntermediaryAccount, error) {
return k.undelegateCommon(ctx, sender, gammLockID)
return k.undelegateCommon(ctx, sender, gammLockID, k.validateLockForSF)
}

// partialUndelegateCommon acts similarly to undelegateCommon, but undelegates a partial amount of the lock's delegation rather than the full amount. The amount
Expand Down
Loading