Skip to content

Commit

Permalink
Add superfluid unbond partial amount (#4107)
Browse files Browse the repository at this point in the history
* implement superfluid unbond partial amount

* changelog for superfluild partial unbond

* remove whitespace

* add docs and tests

* update proto

* add comments to SuperfluidUndelegateAndUnbondLock

* shorten codec for SuperfluidUndelegateAndUnbondLock

* update proto

* test osmo total supply
  • Loading branch information
t4sk authored Feb 2, 2023
1 parent de81a5e commit 55cb894
Show file tree
Hide file tree
Showing 13 changed files with 973 additions and 70 deletions.
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

## Misc Improvements
* [#4131](https://github.com/osmosis-labs/osmosis/pull/4141) Add GatherValuesFromStorePrefixWithKeyParser function to osmoutils.
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)),
)
}

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
}

// 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) {
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
err = k.SuperfluidDelegate(ctx, sender, lockID, intermediaryAcc.ValAddr)
if err != nil {
return 0, err
}

// create synthetic unlocking lock for newLockID
err = k.createSyntheticLockup(ctx, newLockID, intermediaryAcc, unlockingStatus)
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) {
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

0 comments on commit 55cb894

Please sign in to comment.