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

Add superfluid unbond partial amount #4107

Merged
merged 9 commits into from
Feb 2, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [#3843](https://github.com/osmosis-labs/osmosis/pull/3843) Cli support and tested SetValSet, DelValSet, UnDelValSet
* [#3810](https://github.com/osmosis-labs/osmosis/pull/3810) Allow migration of x/lockup uosmo to staking to a valset preference
* [#3966](https://github.com/osmosis-labs/osmosis/pull/3966) Add Redelegate, Withdraw cli commands and sim_msgs
* [#4107](https://github.com/osmosis-labs/osmosis/pull/4107) Add superfluid unbond partial amount

### API breaks

Expand Down
13 changes: 13 additions & 0 deletions proto/osmosis/superfluid/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ service Msg {
rpc SuperfluidUnbondLock(MsgSuperfluidUnbondLock)
returns (MsgSuperfluidUnbondLockResponse);

// Superfluid undelegate and unbond partial amount of the underlying lock.
rpc SuperfluidUndelegateAndUnbondLock(MsgSuperfluidUndelegateAndUnbondLock)
returns (MsgSuperfluidUndelegateAndUnbondLockResponse);

// Execute lockup lock and superfluid delegation in a single msg
rpc LockAndSuperfluidDelegate(MsgLockAndSuperfluidDelegate)
returns (MsgLockAndSuperfluidDelegateResponse);
Expand Down Expand Up @@ -54,6 +58,15 @@ message MsgSuperfluidUnbondLock {
}
message MsgSuperfluidUnbondLockResponse {}

message MsgSuperfluidUndelegateAndUnbondLock {
string sender = 1 [ (gogoproto.moretags) = "yaml:\"sender\"" ];
uint64 lock_id = 2;
// Amount of unlocking coin.
cosmos.base.v1beta1.Coin coin = 3
[ (gogoproto.moretags) = "yaml:\"coin\"", (gogoproto.nullable) = false ];
}
message MsgSuperfluidUndelegateAndUnbondLockResponse {}

// message MsgSuperfluidRedelegate {
// string sender = 1 [ (gogoproto.moretags) = "yaml:\"sender\"" ];
// uint64 lock_id = 2;
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 @@ -114,6 +114,23 @@ func newSuperfluidUnbondLockEvent(lockId uint64) sdk.Event {
)
}

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

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

func newSuperfluidUndelegateAndUnbondLockEvent(lockId uint64) sdk.Event {
return sdk.NewEvent(
types.TypeEvtSuperfluidUndelegateAndUnbondLock,
sdk.NewAttribute(types.AttributeLockId, fmt.Sprintf("%d", lockId)),
stackman27 marked this conversation as resolved.
Show resolved Hide resolved
)
}

func EmitUnpoolIdEvent(ctx sdk.Context, sender string, lpShareDenom string, allExitedLockIDsSerialized []byte) {
if ctx.EventManager() == nil {
return
Expand Down
41 changes: 41 additions & 0 deletions x/superfluid/keeper/internal/events/emit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,47 @@ func (suite *SuperfluidEventsTestSuite) TestEmitSuperfluidUnbondLockEvent() {
}
}

func (suite *SuperfluidEventsTestSuite) TestEmitSuperfluidUndelegateAndUnbondLockEvent() {
testcases := map[string]struct {
ctx sdk.Context
lockID uint64
}{
"basic valid": {
ctx: suite.CreateTestContext(),
lockID: 1,
},
"context with no event manager": {
ctx: sdk.Context{},
},
}

for name, tc := range testcases {
suite.Run(name, func() {
expectedEvents := sdk.Events{
sdk.NewEvent(
types.TypeEvtSuperfluidUndelegateAndUnbondLock,
sdk.NewAttribute(types.AttributeLockId, fmt.Sprintf("%d", tc.lockID)),
),
}

hasNoEventManager := tc.ctx.EventManager() == nil

// System under test.
events.EmitSuperfluidUndelegateAndUnbondLockEvent(tc.ctx, tc.lockID)

// Assertions
if hasNoEventManager {
// If there is no event manager on context, this is a no-op.
return
}

eventManager := tc.ctx.EventManager()
actualEvents := eventManager.Events()
suite.Equal(expectedEvents, actualEvents)
})
}
}

func (suite *SuperfluidEventsTestSuite) TestEmitUnpoolIdEvent() {
testAllExitedLockIDsSerialized, _ := json.Marshal([]uint64{1})

Expand Down
13 changes: 13 additions & 0 deletions x/superfluid/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,19 @@ func (server msgServer) SuperfluidUnbondLock(goCtx context.Context, msg *types.M
return &types.MsgSuperfluidUnbondLockResponse{}, err
}

// SuperfluidUndelegateAndUnbondLock undelegates and unbonds partial amount from a lock.
func (server msgServer) SuperfluidUndelegateAndUnbondLock(goCtx context.Context, msg *types.MsgSuperfluidUndelegateAndUnbondLock) (
*types.MsgSuperfluidUndelegateAndUnbondLockResponse, error,
) {
ctx := sdk.UnwrapSDKContext(goCtx)

_, err := server.keeper.SuperfluidUndelegateAndUnbondLock(ctx, msg.LockId, msg.Sender, msg.Coin.Amount)
if err == nil {
events.EmitSuperfluidUndelegateAndUnbondLockEvent(ctx, msg.LockId)
}
return &types.MsgSuperfluidUndelegateAndUnbondLockResponse{}, err
}

// LockAndSuperfluidDelegate locks and superfluid delegates given tokens in a single message.
// This method consists of multiple messages, `LockTokens` from the lockup module msg server, and
// `SuperfluidDelegate` from the superfluid module msg server.
Expand Down
47 changes: 47 additions & 0 deletions x/superfluid/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,53 @@ func (suite *KeeperTestSuite) TestMsgSuperfluidUnbondLock() {
}
}

func (suite *KeeperTestSuite) TestMsgSuperfluidUndelegateAndUnbondLock() {
type param struct {
coinsToLock sdk.Coins
amountToUnlock sdk.Coin
lockOwner sdk.AccAddress
duration time.Duration
coinsInOwnerAddress sdk.Coins
}

tests := []struct {
name string
param param
expectPass bool
}{
{
name: "superfluid unbond lock that is not superfluid lockup",
param: param{
coinsToLock: sdk.Coins{sdk.NewInt64Coin("stake", 10)},
amountToUnlock: sdk.NewInt64Coin("stake", 10),
lockOwner: sdk.AccAddress([]byte("addr1---------------")),
duration: time.Second,
coinsInOwnerAddress: sdk.Coins{sdk.NewInt64Coin("stake", 10)},
},
expectPass: false,
},
}

for _, test := range tests {
suite.SetupTest()

suite.FundAcc(test.param.lockOwner, test.param.coinsInOwnerAddress)

lockupMsgServer := lockupkeeper.NewMsgServerImpl(suite.App.LockupKeeper)
c := sdk.WrapSDKContext(suite.Ctx)
resp, err := lockupMsgServer.LockTokens(c, lockuptypes.NewMsgLockTokens(test.param.lockOwner, test.param.duration, test.param.coinsToLock))

msgServer := keeper.NewMsgServerImpl(suite.App.SuperfluidKeeper)
_, err = msgServer.SuperfluidUndelegateAndUnbondLock(c, types.NewMsgSuperfluidUndelegateAndUnbondLock(test.param.lockOwner, resp.ID, test.param.amountToUnlock))

if test.expectPass {
suite.Require().NoError(err)
} else {
suite.Require().Error(err)
}
}
}

func (suite *KeeperTestSuite) TestMsgLockAndSuperfluidDelegate() {
type param struct {
coinsToLock sdk.Coins
Expand Down
97 changes: 91 additions & 6 deletions x/superfluid/keeper/stake.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,23 +269,108 @@ func (k Keeper) SuperfluidUndelegate(ctx sdk.Context, sender string, lockID uint
// SuperfluidUnbondLock unbonds the lock that has been used for superfluid staking.
// This method would return an error if the underlying lock is not superfluid undelegating.
func (k Keeper) SuperfluidUnbondLock(ctx sdk.Context, underlyingLockId uint64, sender string) error {
_, err := k.unbondLock(ctx, underlyingLockId, sender, sdk.Coins{})
return err
stackman27 marked this conversation as resolved.
Show resolved Hide resolved
}

// SuperfluidUndelegateAndUnbondLock unbonds given amount from the
// underlying lock that has been used for superfluid staking.
// This method returns the lock id, same lock id if unlock amount is equal to the
// underlying lock amount. Otherwise it returns the newly created lock id.
// Note that we can either partially or fully undelegate and unbond lock using this method.
func (k Keeper) SuperfluidUndelegateAndUnbondLock(ctx sdk.Context, lockID uint64, sender string, amount sdk.Int) (uint64, error) {
mattverse marked this conversation as resolved.
Show resolved Hide resolved
lock, err := k.lk.GetLockByID(ctx, lockID)
if err != nil {
return 0, err
}

coins := sdk.Coins{sdk.NewCoin(lock.Coins[0].Denom, amount)}
if coins[0].IsZero() {
return 0, fmt.Errorf("amount to unlock must be greater than 0")
}
if lock.Coins[0].IsLT(coins[0]) {
return 0, fmt.Errorf("requested amount to unlock exceeds locked tokens")
}

// get intermediary account before connection is deleted in SuperfluidUndelegate
intermediaryAcc, found := k.GetIntermediaryAccountFromLockId(ctx, lockID)
if !found {
return 0, types.ErrNotSuperfluidUsedLockup
}

// undelegate all
err = k.SuperfluidUndelegate(ctx, sender, lockID)
if err != nil {
return 0, err
}

// unbond partial or full locked amount
newLockID, err := k.unbondLock(ctx, lockID, sender, coins)
if err != nil {
return 0, err
}

// check new lock id
// If unbond amount == locked amount, then the underlying lock was not split.
// So we double check that newLockID == lockID, and return.
// This has the same effect as calling SuperfluidUndelegate and then SuperfluidUnbondLock.
// Otherwise unbond amount < locked amount, and the underlying lock was split.
// lockID contains the amount still locked in the lockup module.
// newLockID contains the amount unlocked.
// We double check that newLockID != lockID and then proceed to re-delegate
// the remainder (locked amount - unbond amount).
if lock.Coins[0].IsEqual(coins[0]) {
if newLockID != lockID {
panic(fmt.Errorf("expected new lock id %v to = lock id %v", newLockID, lockID))
}
return lock.ID, nil
} else {
if newLockID == lockID {
panic(fmt.Errorf("expected new lock id %v to != lock id %v", newLockID, lockID))
}
}

// delete synthetic unlocking lock created in the last step of SuperfluidUndelegate
synthdenom := unstakingSyntheticDenom(lock.Coins[0].Denom, intermediaryAcc.ValAddr)
err = k.lk.DeleteSyntheticLockup(ctx, lockID, synthdenom)
if err != nil {
return 0, err
}

// re-delegate remainder
mattverse marked this conversation as resolved.
Show resolved Hide resolved
err = k.SuperfluidDelegate(ctx, sender, lockID, intermediaryAcc.ValAddr)
mattverse marked this conversation as resolved.
Show resolved Hide resolved
stackman27 marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return 0, err
}

// create synthetic unlocking lock for newLockID
err = k.createSyntheticLockup(ctx, newLockID, intermediaryAcc, unlockingStatus)
stackman27 marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return 0, err
}
return newLockID, nil
}

// unbondLock unlocks the underlying lock. Same lock id is returned if the amount to unlock
// is equal to the entire locked amount. Otherwise, the amount to unlock is less
// than the amount locked, it will return a new lock id which was created as an unlocking lock.
func (k Keeper) unbondLock(ctx sdk.Context, underlyingLockId uint64, sender string, coins sdk.Coins) (uint64, error) {
mattverse marked this conversation as resolved.
Show resolved Hide resolved
lock, err := k.lk.GetLockByID(ctx, underlyingLockId)
if err != nil {
return err
return 0, err
}
err = k.validateLockForSF(ctx, lock, sender)
if err != nil {
return err
return 0, err
}
synthLocks := k.lk.GetAllSyntheticLockupsByLockup(ctx, underlyingLockId)
if len(synthLocks) != 1 {
return types.ErrNotSuperfluidUsedLockup
return 0, types.ErrNotSuperfluidUsedLockup
}
if !synthLocks[0].IsUnlocking() {
return types.ErrBondingLockupNotSupported
return 0, types.ErrBondingLockupNotSupported
}
_, err = k.lk.BeginForceUnlock(ctx, underlyingLockId, sdk.Coins{})
return err
return k.lk.BeginForceUnlock(ctx, underlyingLockId, coins)
}

// alreadySuperfluidStaking returns true if underlying lock used in superfluid staking.
Expand Down
Loading