From d3545ec0dbb5de56dceb33868169dc7d9fae98c3 Mon Sep 17 00:00:00 2001 From: Adam Tucker Date: Fri, 19 May 2023 21:31:29 -0500 Subject: [PATCH] feat: partial superfluid undelegate (#5162) * partial superfluid undelegate * further reduction of gas * Update x/superfluid/keeper/migrate.go Co-authored-by: Roman * reduce code duplication lock and lockNoSend * lockNoSend as default * add bug fix with expanded tests * unit test for CreateLockNoSend * Update x/superfluid/keeper/migrate.go Co-authored-by: Matt, Park <45252226+mattverse@users.noreply.github.com> * fix merge * partialSuperfluidUndelegate named return values * expand test checks for migrateDelegated * expanded bonded checks * check new lock and exact amount * assign vars directly * update merge * check newly created lock * add extra logic branch for fail case * split up partial superfluid undelegate func * expand partial undelegate test case * roman's review * Update x/lockup/keeper/lock_test.go * nit * fix test * expand CreateLockNoSend comment * rename lockNoSend back to lock * expand partial unlock comment --------- Co-authored-by: Roman Co-authored-by: Matt, Park <45252226+mattverse@users.noreply.github.com> --- tests/cl-go-client/go.mod | 2 +- x/concentrated-liquidity/position.go | 11 +- .../types/expected_keepers.go | 1 + x/lockup/keeper/lock.go | 40 ++- x/lockup/keeper/lock_test.go | 77 ++++- x/superfluid/keeper/export_test.go | 21 +- x/superfluid/keeper/migrate.go | 198 +++++------- x/superfluid/keeper/migrate_test.go | 287 ++++++++++++------ x/superfluid/keeper/msg_server.go | 3 +- x/superfluid/keeper/stake.go | 66 +++- x/superfluid/keeper/stake_test.go | 160 ++++++++++ x/superfluid/keeper/unpool.go | 4 +- x/superfluid/types/expected_keepers.go | 2 + 13 files changed, 627 insertions(+), 245 deletions(-) diff --git a/tests/cl-go-client/go.mod b/tests/cl-go-client/go.mod index 5d209c66e3f..75050111bb2 100644 --- a/tests/cl-go-client/go.mod +++ b/tests/cl-go-client/go.mod @@ -5,7 +5,7 @@ go 1.20 require ( github.com/cosmos/cosmos-sdk v0.47.2 github.com/ignite/cli v0.23.0 - github.com/osmosis-labs/osmosis/v15 v15.0.0-20230502194055-e465f0b40c14 + github.com/osmosis-labs/osmosis/v15 v15.0.0-20230511223858-61e374113afc ) diff --git a/x/concentrated-liquidity/position.go b/x/concentrated-liquidity/position.go index 420d14065d9..cfa0059988b 100644 --- a/x/concentrated-liquidity/position.go +++ b/x/concentrated-liquidity/position.go @@ -385,17 +385,12 @@ func (k Keeper) mintSharesAndLock(ctx sdk.Context, concentratedPoolId, positionI if err != nil { return 0, sdk.Coins{}, err } - err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, lockuptypes.ModuleName, owner, underlyingLiquidityTokenized) - if err != nil { - return 0, sdk.Coins{}, err - } // Lock the position for the specified duration. + // We don't need to send the coins from the owner to the lockup module account because the coins were minted directly to the module account above. // Note, the end blocker for the lockup module contains an exception for this CL denom. When a lock with a denom of cl/pool/{poolId} is mature, - // it does not send the coins to the owner account but instead burns them. - // This is implemented in such a way to use well-tested pre-existing methods rather than - // completely re-implementing concentrated liquidity superfluid infrastructure that has a risk of introducing bugs with new logic and methods. - concentratedLock, err := k.lockupKeeper.CreateLock(ctx, owner, underlyingLiquidityTokenized, remainingLockDuration) + // it does not send the coins to the owner account and instead burns them. This is strictly to use well tested pre-existing methods rather than potentially introducing bugs with new logic and methods. + concentratedLock, err := k.lockupKeeper.CreateLockNoSend(ctx, owner, underlyingLiquidityTokenized, remainingLockDuration) if err != nil { return 0, sdk.Coins{}, err } diff --git a/x/concentrated-liquidity/types/expected_keepers.go b/x/concentrated-liquidity/types/expected_keepers.go index 10191356a82..0b8fabb2307 100644 --- a/x/concentrated-liquidity/types/expected_keepers.go +++ b/x/concentrated-liquidity/types/expected_keepers.go @@ -57,6 +57,7 @@ type LockupKeeper interface { BeginForceUnlock(ctx sdk.Context, lockID uint64, coins sdk.Coins) (uint64, error) ForceUnlock(ctx sdk.Context, lock lockuptypes.PeriodLock) error CreateLock(ctx sdk.Context, owner sdk.AccAddress, coins sdk.Coins, duration time.Duration) (lockuptypes.PeriodLock, error) + CreateLockNoSend(ctx sdk.Context, owner sdk.AccAddress, coins sdk.Coins, duration time.Duration) (lockuptypes.PeriodLock, error) SlashTokensFromLockByID(ctx sdk.Context, lockID uint64, coins sdk.Coins) (*lockuptypes.PeriodLock, error) } diff --git a/x/lockup/keeper/lock.go b/x/lockup/keeper/lock.go index 076ed76537d..16ec390dcba 100644 --- a/x/lockup/keeper/lock.go +++ b/x/lockup/keeper/lock.go @@ -94,6 +94,12 @@ func (k Keeper) AddTokensToLockByID(ctx sdk.Context, lockID uint64, owner sdk.Ac } lock.Coins = lock.Coins.Add(tokensToAdd) + + // Send the tokens we are about to add to lock to the lockup module account. + if err := k.bk.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, sdk.NewCoins(tokensToAdd)); err != nil { + return nil, err + } + err = k.lock(ctx, *lock, sdk.NewCoins(tokensToAdd)) if err != nil { return nil, err @@ -116,10 +122,31 @@ func (k Keeper) AddTokensToLockByID(ctx sdk.Context, lockID uint64, owner sdk.Ac // Returns an error in the following conditions: // - account does not have enough balance func (k Keeper) CreateLock(ctx sdk.Context, owner sdk.AccAddress, coins sdk.Coins, duration time.Duration) (types.PeriodLock, error) { + // Send the coins we are about to lock to the lockup module account. + if err := k.bk.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, coins); err != nil { + return types.PeriodLock{}, err + } + + // Run the createLock logic without the send since we sent the coins above. + lock, err := k.CreateLockNoSend(ctx, owner, coins, duration) + if err != nil { + return types.PeriodLock{}, err + } + return lock, nil +} + +// CreateLockNoSend behaves the same as CreateLock, but does not send the coins to the lockup module account. +// This method is used in the concentrated liquidity module since we mint coins directly to the lockup module account. +// We do not want to mint the coins to send to the user just to send them back to the lockup module account for two reasons: +// - it is gas inefficient +// - users should not be able to have cl shares in their account, so this is an extra safety measure +func (k Keeper) CreateLockNoSend(ctx sdk.Context, owner sdk.AccAddress, coins sdk.Coins, duration time.Duration) (types.PeriodLock, error) { ID := k.GetLastLockID(ctx) + 1 // unlock time is initially set without a value, gets set as unlock start time + duration // when unlocking starts. lock := types.NewPeriodLock(ID, owner, duration, time.Time{}, coins) + + // lock the coins without sending them to the lockup module account err := k.lock(ctx, lock, lock.Coins) if err != nil { return lock, err @@ -139,14 +166,13 @@ func (k Keeper) CreateLock(ctx sdk.Context, owner sdk.AccAddress, coins sdk.Coin // This is only called by either of the two possible entry points to lock tokens. // 1. CreateLock // 2. AddTokensToLockByID +// WARNING: this method does not send the underlying coins to the lockup module account. +// This must be done by the caller. func (k Keeper) lock(ctx sdk.Context, lock types.PeriodLock, tokensToLock sdk.Coins) error { owner, err := sdk.AccAddressFromBech32(lock.Owner) if err != nil { return err } - if err := k.bk.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, tokensToLock); err != nil { - return err - } // store lock object into the store err = k.setLock(ctx, lock) @@ -218,7 +244,7 @@ func (k Keeper) beginUnlock(ctx sdk.Context, lock types.PeriodLock, coins sdk.Co // Otherwise, split the lock into two locks, and fully unlock the newly created lock. // (By virtue, the newly created lock we split into should have the unlock amount) if len(coins) != 0 && !coins.IsEqual(lock.Coins) { - splitLock, err := k.splitLock(ctx, lock, coins, false) + splitLock, err := k.SplitLock(ctx, lock, coins, false) if err != nil { return 0, err } @@ -336,7 +362,7 @@ func (k Keeper) PartialForceUnlock(ctx sdk.Context, lock types.PeriodLock, coins // split lock to support partial force unlock. // (By virtue, the newly created lock we split into should have the unlock amount) if len(coins) != 0 && !coins.IsEqual(lock.Coins) { - splitLock, err := k.splitLock(ctx, lock, coins, true) + splitLock, err := k.SplitLock(ctx, lock, coins, true) if err != nil { return err } @@ -750,9 +776,9 @@ func (k Keeper) deleteLock(ctx sdk.Context, id uint64) { store.Delete(lockStoreKey(id)) } -// splitLock splits a lock with the given amount, and stores split new lock to the state. +// SplitLock splits a lock with the given amount, and stores split new lock to the state. // Returns the new lock after modifying the state of the old lock. -func (k Keeper) splitLock(ctx sdk.Context, lock types.PeriodLock, coins sdk.Coins, forceUnlock bool) (types.PeriodLock, error) { +func (k Keeper) SplitLock(ctx sdk.Context, lock types.PeriodLock, coins sdk.Coins, forceUnlock bool) (types.PeriodLock, error) { if !forceUnlock && lock.IsUnlocking() { return types.PeriodLock{}, fmt.Errorf("cannot split unlocking lock") } diff --git a/x/lockup/keeper/lock_test.go b/x/lockup/keeper/lock_test.go index 63336318c58..a9a494de881 100644 --- a/x/lockup/keeper/lock_test.go +++ b/x/lockup/keeper/lock_test.go @@ -533,6 +533,61 @@ func (suite *KeeperTestSuite) TestCreateLock() { suite.Require().Equal(sdk.NewInt(30), balance.Amount) } +func (suite *KeeperTestSuite) TestCreateLockNoSend() { + suite.SetupTest() + + addr1 := sdk.AccAddress([]byte("addr1---------------")) + coins := sdk.Coins{sdk.NewInt64Coin("stake", 10)} + + // test locking without balance + lock, err := suite.App.LockupKeeper.CreateLockNoSend(suite.Ctx, addr1, coins, time.Second) + suite.Require().NoError(err) + + // check new lock + suite.Require().Equal(coins, lock.Coins) + suite.Require().Equal(time.Second, lock.Duration) + suite.Require().Equal(time.Time{}, lock.EndTime) + suite.Require().Equal(uint64(1), lock.ID) + + lockID := suite.App.LockupKeeper.GetLastLockID(suite.Ctx) + suite.Require().Equal(uint64(1), lockID) + + // check accumulation store + accum := suite.App.LockupKeeper.GetPeriodLocksAccumulation(suite.Ctx, types.QueryCondition{ + LockQueryType: types.ByDuration, + Denom: "stake", + Duration: time.Second, + }) + suite.Require().Equal(accum.String(), "10") + + // create new lock (this time with a balance) + originalLockBalance := int64(20) + coins = sdk.Coins{sdk.NewInt64Coin("stake", originalLockBalance)} + suite.FundAcc(addr1, coins) + + lock, err = suite.App.LockupKeeper.CreateLockNoSend(suite.Ctx, addr1, coins, time.Second) + suite.Require().NoError(err) + + lockID = suite.App.LockupKeeper.GetLastLockID(suite.Ctx) + suite.Require().Equal(uint64(2), lockID) + + // check accumulation store + accum = suite.App.LockupKeeper.GetPeriodLocksAccumulation(suite.Ctx, types.QueryCondition{ + LockQueryType: types.ByDuration, + Denom: "stake", + Duration: time.Second, + }) + suite.Require().Equal(accum.String(), "30") + + // check that send did not occur and balances are unchanged + balance := suite.App.BankKeeper.GetBalance(suite.Ctx, addr1, "stake") + suite.Require().Equal(sdk.NewInt(originalLockBalance).String(), balance.Amount.String()) + + acc := suite.App.AccountKeeper.GetModuleAccount(suite.Ctx, types.ModuleName) + balance = suite.App.BankKeeper.GetBalance(suite.Ctx, acc.GetAddress(), "stake") + suite.Require().Equal(sdk.ZeroInt().String(), balance.Amount.String()) +} + func (suite *KeeperTestSuite) TestAddTokensToLock() { initialLockCoin := sdk.NewInt64Coin("stake", 10) addr1 := sdk.AccAddress([]byte("addr1---------------")) @@ -703,7 +758,7 @@ func (suite *KeeperTestSuite) TestHasLock() { } } -func (suite *KeeperTestSuite) TestLock() { +func (suite *KeeperTestSuite) TestLockNoSend() { suite.SetupTest() addr1 := sdk.AccAddress([]byte("addr1---------------")) @@ -717,9 +772,9 @@ func (suite *KeeperTestSuite) TestLock() { Coins: coins, } - // test locking without balance + // test locking without balance (should work since we don't send the underlying balance) err := suite.App.LockupKeeper.Lock(suite.Ctx, lock, coins) - suite.Require().Error(err) + suite.Require().NoError(err) // check accumulation store accum := suite.App.LockupKeeper.GetPeriodLocksAccumulation(suite.Ctx, types.QueryCondition{ @@ -727,7 +782,7 @@ func (suite *KeeperTestSuite) TestLock() { Denom: "stake", Duration: time.Second, }) - suite.Require().Equal(accum.String(), "0") + suite.Require().Equal(accum.String(), "10") suite.FundAcc(addr1, coins) err = suite.App.LockupKeeper.Lock(suite.Ctx, lock, coins) @@ -739,14 +794,15 @@ func (suite *KeeperTestSuite) TestLock() { Denom: "stake", Duration: time.Second, }) - suite.Require().Equal(accum.String(), "10") + suite.Require().Equal(accum.String(), "20") + // Since lockNoSend does not send the underlying coins, the account balance should be unchanged balance := suite.App.BankKeeper.GetBalance(suite.Ctx, addr1, "stake") - suite.Require().Equal(sdk.ZeroInt(), balance.Amount) + suite.Require().Equal(sdk.NewInt(10).String(), balance.Amount.String()) acc := suite.App.AccountKeeper.GetModuleAccount(suite.Ctx, types.ModuleName) balance = suite.App.BankKeeper.GetBalance(suite.Ctx, acc.GetAddress(), "stake") - suite.Require().Equal(sdk.NewInt(10), balance.Amount) + suite.Require().Equal(sdk.NewInt(0).String(), balance.Amount.String()) } func (suite *KeeperTestSuite) AddTokensToLockForSynth() { @@ -1272,6 +1328,7 @@ func (suite *KeeperTestSuite) TestPartialForceUnlock() { defaultDenomToLock := "stake" defaultAmountToLock := sdk.NewInt(10000000) + coinsToLock := sdk.NewCoins(sdk.NewCoin("stake", defaultAmountToLock)) testCases := []struct { name string @@ -1280,7 +1337,7 @@ func (suite *KeeperTestSuite) TestPartialForceUnlock() { }{ { name: "unlock full amount", - coinsToForceUnlock: sdk.Coins{sdk.NewCoin(defaultDenomToLock, defaultAmountToLock)}, + coinsToForceUnlock: coinsToLock, expectedPass: true, }, { @@ -1302,9 +1359,9 @@ func (suite *KeeperTestSuite) TestPartialForceUnlock() { for _, tc := range testCases { // set up test and create default lock suite.SetupTest() - coinsToLock := sdk.NewCoins(sdk.NewCoin("stake", defaultAmountToLock)) + suite.FundAcc(addr1, sdk.NewCoins(coinsToLock...)) - // balanceBeforeLock := suite.App.BankKeeper.GetAllBalances(suite.Ctx, addr1) + lock, err := suite.App.LockupKeeper.CreateLock(suite.Ctx, addr1, coinsToLock, time.Minute) suite.Require().NoError(err) diff --git a/x/superfluid/keeper/export_test.go b/x/superfluid/keeper/export_test.go index c686a4cd6b6..1ca225ce161 100644 --- a/x/superfluid/keeper/export_test.go +++ b/x/superfluid/keeper/export_test.go @@ -6,6 +6,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" lockuptypes "github.com/osmosis-labs/osmosis/v15/x/lockup/types" + "github.com/osmosis-labs/osmosis/v15/x/superfluid/types" ) var ( @@ -21,19 +22,19 @@ func (k Keeper) PrepareConcentratedLockForSlash(ctx sdk.Context, lock *lockuptyp return k.prepareConcentratedLockForSlash(ctx, lock, slashAmt) } -func (k Keeper) MigrateSuperfluidBondedBalancerToConcentrated(ctx sdk.Context, sender sdk.AccAddress, lockId uint64, sharesToMigrate sdk.Coin, synthDenomBeforeMigration string, tokenOutMins sdk.Coins) (positionId uint64, amount0, amount1 sdk.Int, liquidity sdk.Dec, joinTime time.Time, gammLockId, concentratedLockId, poolIdLeaving, poolIdEntering uint64, err error) { +func (k Keeper) MigrateSuperfluidBondedBalancerToConcentrated(ctx sdk.Context, sender sdk.AccAddress, lockId uint64, sharesToMigrate sdk.Coin, synthDenomBeforeMigration string, tokenOutMins sdk.Coins) (positionId uint64, amount0, amount1 sdk.Int, liquidity sdk.Dec, joinTime time.Time, concentratedLockId, poolIdLeaving, poolIdEntering uint64, err error) { return k.migrateSuperfluidBondedBalancerToConcentrated(ctx, sender, lockId, sharesToMigrate, synthDenomBeforeMigration, tokenOutMins) } -func (k Keeper) MigrateSuperfluidUnbondingBalancerToConcentrated(ctx sdk.Context, sender sdk.AccAddress, lockId uint64, sharesToMigrate sdk.Coin, synthDenomBeforeMigration string, tokenOutMins sdk.Coins) (positionId uint64, amount0, amount1 sdk.Int, liquidity sdk.Dec, joinTime time.Time, gammLockId, concentratedLockId, poolIdLeaving, poolIdEntering uint64, err error) { +func (k Keeper) MigrateSuperfluidUnbondingBalancerToConcentrated(ctx sdk.Context, sender sdk.AccAddress, lockId uint64, sharesToMigrate sdk.Coin, synthDenomBeforeMigration string, tokenOutMins sdk.Coins) (positionId uint64, amount0, amount1 sdk.Int, liquidity sdk.Dec, joinTime time.Time, concentratedLockId, poolIdLeaving, poolIdEntering uint64, err error) { return k.migrateSuperfluidUnbondingBalancerToConcentrated(ctx, sender, lockId, sharesToMigrate, synthDenomBeforeMigration, tokenOutMins) } -func (k Keeper) MigrateNonSuperfluidLockBalancerToConcentrated(ctx sdk.Context, sender sdk.AccAddress, lockId uint64, sharesToMigrate sdk.Coin, tokenOutMins sdk.Coins) (positionId uint64, amount0, amount1 sdk.Int, liquidity sdk.Dec, joinTime time.Time, gammLockId, concentratedLockId, poolIdLeaving, poolIdEntering uint64, err error) { +func (k Keeper) MigrateNonSuperfluidLockBalancerToConcentrated(ctx sdk.Context, sender sdk.AccAddress, lockId uint64, sharesToMigrate sdk.Coin, tokenOutMins sdk.Coins) (positionId uint64, amount0, amount1 sdk.Int, liquidity sdk.Dec, joinTime time.Time, concentratedLockId, poolIdLeaving, poolIdEntering uint64, err error) { return k.migrateNonSuperfluidLockBalancerToConcentrated(ctx, sender, lockId, sharesToMigrate, tokenOutMins) } -func (k Keeper) ValidateSharesToMigrateUnlockAndExitBalancerPool(ctx sdk.Context, sender sdk.AccAddress, poolIdLeaving uint64, lock *lockuptypes.PeriodLock, sharesToMigrate sdk.Coin, tokenOutMins sdk.Coins, remainingLockTime time.Duration) (exitCoins sdk.Coins, remainingSharesLock lockuptypes.PeriodLock, err error) { +func (k Keeper) ValidateSharesToMigrateUnlockAndExitBalancerPool(ctx sdk.Context, sender sdk.AccAddress, poolIdLeaving uint64, lock *lockuptypes.PeriodLock, sharesToMigrate sdk.Coin, tokenOutMins sdk.Coins, remainingLockTime time.Duration) (exitCoins sdk.Coins, err error) { return k.validateSharesToMigrateUnlockAndExitBalancerPool(ctx, sender, poolIdLeaving, lock, sharesToMigrate, tokenOutMins, remainingLockTime) } @@ -48,3 +49,15 @@ func (k Keeper) ValidateMigration(ctx sdk.Context, sender sdk.AccAddress, lockId func (k Keeper) AddToConcentratedLiquiditySuperfluidPosition(ctx sdk.Context, owner sdk.AccAddress, positionId uint64, amount0Added, amount1Added sdk.Int) (uint64, sdk.Int, sdk.Int, sdk.Dec, uint64, error) { return k.addToConcentratedLiquiditySuperfluidPosition(ctx, owner, positionId, amount0Added, amount1Added) } + +func (k Keeper) ValidateGammLockForSuperfluidStaking(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, lockId uint64) (*lockuptypes.PeriodLock, error) { + return k.validateGammLockForSuperfluidStaking(ctx, sender, poolId, lockId) +} + +func (k Keeper) GetExistingLockRemainingDuration(ctx sdk.Context, lock *lockuptypes.PeriodLock) time.Duration { + return k.getExistingLockRemainingDuration(ctx, lock) +} + +func (k Keeper) PartialSuperfluidUndelegateToConcentratedPosition(ctx sdk.Context, sender string, lockID uint64, amountToUndelegate sdk.Coin) (intermediaryAcc types.SuperfluidIntermediaryAccount, newlock *lockuptypes.PeriodLock, err error) { + return k.partialSuperfluidUndelegateToConcentratedPosition(ctx, sender, lockID, amountToUndelegate) +} diff --git a/x/superfluid/keeper/migrate.go b/x/superfluid/keeper/migrate.go index 0a6543c8511..3093db5c636 100644 --- a/x/superfluid/keeper/migrate.go +++ b/x/superfluid/keeper/migrate.go @@ -27,85 +27,91 @@ const ( // If the lock is superfluid undelegating, it will instantly undelegate the superfluid position and redelegate it as a concentrated liquidity position, but continue to unlock where it left off. // If the lock is locked or unlocking but not superfluid delegated/undelegating, it will migrate the position and either start unlocking or continue unlocking where it left off. // Errors if the lock is not found, if the lock is not a balancer pool lock, or if the lock is not owned by the sender. -func (k Keeper) RouteLockedBalancerToConcentratedMigration(ctx sdk.Context, sender sdk.AccAddress, lockId uint64, sharesToMigrate sdk.Coin, tokenOutMins sdk.Coins) (positionId uint64, amount0, amount1 sdk.Int, liquidity sdk.Dec, joinTime time.Time, poolIdLeaving, poolIdEntering, gammLockId, concentratedLockId uint64, err error) { +func (k Keeper) RouteLockedBalancerToConcentratedMigration(ctx sdk.Context, sender sdk.AccAddress, lockId uint64, sharesToMigrate sdk.Coin, tokenOutMins sdk.Coins) (positionId uint64, amount0, amount1 sdk.Int, liquidity sdk.Dec, joinTime time.Time, poolIdLeaving, poolIdEntering, concentratedLockId uint64, err error) { synthLocksBeforeMigration, migrationType, err := k.routeMigration(ctx, sender, lockId, sharesToMigrate) if err != nil { - return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, 0, err + return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, err } switch migrationType { case SuperfluidBonded: - positionId, amount0, amount1, liquidity, joinTime, gammLockId, concentratedLockId, poolIdLeaving, poolIdEntering, err = k.migrateSuperfluidBondedBalancerToConcentrated(ctx, sender, lockId, sharesToMigrate, synthLocksBeforeMigration[0].SynthDenom, tokenOutMins) + positionId, amount0, amount1, liquidity, joinTime, concentratedLockId, poolIdLeaving, poolIdEntering, err = k.migrateSuperfluidBondedBalancerToConcentrated(ctx, sender, lockId, sharesToMigrate, synthLocksBeforeMigration[0].SynthDenom, tokenOutMins) case SuperfluidUnbonding: - positionId, amount0, amount1, liquidity, joinTime, gammLockId, concentratedLockId, poolIdLeaving, poolIdEntering, err = k.migrateSuperfluidUnbondingBalancerToConcentrated(ctx, sender, lockId, sharesToMigrate, synthLocksBeforeMigration[0].SynthDenom, tokenOutMins) + positionId, amount0, amount1, liquidity, joinTime, concentratedLockId, poolIdLeaving, poolIdEntering, err = k.migrateSuperfluidUnbondingBalancerToConcentrated(ctx, sender, lockId, sharesToMigrate, synthLocksBeforeMigration[0].SynthDenom, tokenOutMins) case NonSuperfluid: - positionId, amount0, amount1, liquidity, joinTime, gammLockId, concentratedLockId, poolIdLeaving, poolIdEntering, err = k.migrateNonSuperfluidLockBalancerToConcentrated(ctx, sender, lockId, sharesToMigrate, tokenOutMins) + positionId, amount0, amount1, liquidity, joinTime, concentratedLockId, poolIdLeaving, poolIdEntering, err = k.migrateNonSuperfluidLockBalancerToConcentrated(ctx, sender, lockId, sharesToMigrate, tokenOutMins) default: - return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, 0, fmt.Errorf("unsupported migration type") + return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, fmt.Errorf("unsupported migration type") } - return positionId, amount0, amount1, liquidity, joinTime, poolIdLeaving, poolIdEntering, gammLockId, concentratedLockId, err + return positionId, amount0, amount1, liquidity, joinTime, poolIdLeaving, poolIdEntering, concentratedLockId, err } // migrateSuperfluidBondedBalancerToConcentrated migrates a user's superfluid bonded balancer position to a superfluid bonded concentrated liquidity position. // The function first undelegates the superfluid delegated position, force unlocks and exits the balancer pool, creates a full range concentrated liquidity position, locks it, then superfluid delegates it. -// If there are any remaining gamm shares, they are re-locked back in the gamm pool and superfluid delegated as normal. The function returns the concentrated liquidity position ID, amounts of -// tokens in the position, the liquidity amount, join time, and IDs of the involved pools and locks. +// Any remaining gamm shares stay locked in the original gamm pool (utilizing the same lock and lockID that the shares originated from) and remain superfluid delegated / undelegating / vanilla locked as they +// were when the migration was initiated. The function returns the concentrated liquidity position ID, amounts of tokens in the position, the liquidity amount, join time, and IDs of the involved pools and locks. func (k Keeper) migrateSuperfluidBondedBalancerToConcentrated(ctx sdk.Context, sender sdk.AccAddress, - lockId uint64, + originalLockId uint64, sharesToMigrate sdk.Coin, synthDenomBeforeMigration string, tokenOutMins sdk.Coins, -) (positionId uint64, amount0, amount1 sdk.Int, liquidity sdk.Dec, joinTime time.Time, gammLockId, concentratedLockId, poolIdLeaving, poolIdEntering uint64, err error) { - poolIdLeaving, poolIdEntering, preMigrationLock, remainingLockTime, err := k.validateMigration(ctx, sender, lockId, sharesToMigrate) +) (positionId uint64, amount0, amount1 sdk.Int, liquidity sdk.Dec, joinTime time.Time, concentratedLockId, poolIdLeaving, poolIdEntering uint64, err error) { + poolIdLeaving, poolIdEntering, preMigrationLock, remainingLockTime, err := k.validateMigration(ctx, sender, originalLockId, sharesToMigrate) if err != nil { - return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, 0, err + return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, err } + isPartialMigration := sharesToMigrate.Amount.LT(preMigrationLock.Coins[0].Amount) + // Get the validator address from the synth denom and ensure it is a valid address. valAddr := strings.Split(synthDenomBeforeMigration, "/")[4] _, err = sdk.ValAddressFromBech32(valAddr) if err != nil { - return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, 0, err + return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, err } - // Superfluid undelegate the superfluid delegated position. - // This deletes the connection between the gamm lock and the intermediate account, deletes the synthetic lock, and burns the synthetic osmo. - intermediateAccount, err := k.SuperfluidUndelegateToConcentratedPosition(ctx, sender.String(), lockId) - if err != nil { - return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, 0, err + // Superfluid undelegate the portion of shares the user is migrating from the superfluid delegated position. + // If all shares are being migrated, this deletes the connection between the gamm lock and the intermediate account, deletes the synthetic lock, and burns the synthetic osmo. + intermediateAccount := types.SuperfluidIntermediaryAccount{} + var gammLockToMigrate *lockuptypes.PeriodLock + if isPartialMigration { + // Note that lock's id is different from the originalLockId sinec it was split. + // The original lock id stays in gamm. + intermediateAccount, gammLockToMigrate, err = k.partialSuperfluidUndelegateToConcentratedPosition(ctx, sender.String(), originalLockId, sharesToMigrate) + if err != nil { + return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, err + } + } else { + // Note that lock's id is the same as the originalLockId since all shares are being migrated + // and old lock is deleted + gammLockToMigrate = preMigrationLock + intermediateAccount, err = k.SuperfluidUndelegateToConcentratedPosition(ctx, sender.String(), originalLockId) + if err != nil { + return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, err + } } // Force unlock, validate the provided sharesToMigrate, and exit the balancer pool. // This will return the coins that will be used to create the concentrated liquidity position. // It also returns the lock object that contains the remaining shares that were not used in this migration. - exitCoins, remainingSharesLock, err := k.validateSharesToMigrateUnlockAndExitBalancerPool(ctx, sender, poolIdLeaving, preMigrationLock, sharesToMigrate, tokenOutMins, remainingLockTime) + exitCoins, err := k.validateSharesToMigrateUnlockAndExitBalancerPool(ctx, sender, poolIdLeaving, gammLockToMigrate, sharesToMigrate, tokenOutMins, remainingLockTime) if err != nil { - return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, 0, err + return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, err } // Create a full range (min to max tick) concentrated liquidity position, lock it, and superfluid delegate it. positionId, amount0, amount1, liquidity, joinTime, concentratedLockId, err = k.clk.CreateFullRangePositionLocked(ctx, poolIdEntering, sender, exitCoins, remainingLockTime) if err != nil { - return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, 0, err + return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, err } err = k.SuperfluidDelegate(ctx, sender.String(), concentratedLockId, intermediateAccount.ValAddr) if err != nil { - return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, 0, err + return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, err } - // If there are any remaining gamm shares after the migration, we must re-superfluid delegate them as they were previously in the gamm pool. - if remainingSharesLock.ID != 0 { - gammLockId = remainingSharesLock.ID - // Superfluid delegate the gamm lock. - err = k.SuperfluidDelegate(ctx, sender.String(), remainingSharesLock.ID, valAddr) - if err != nil { - return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, 0, err - } - } - - return positionId, amount0, amount1, liquidity, joinTime, gammLockId, concentratedLockId, poolIdLeaving, poolIdEntering, nil + return positionId, amount0, amount1, liquidity, joinTime, concentratedLockId, poolIdLeaving, poolIdEntering, nil } // migrateSuperfluidUnbondingBalancerToConcentrated migrates a user's superfluid unbonding balancer position to a superfluid unbonding concentrated liquidity position. @@ -119,35 +125,32 @@ func (k Keeper) migrateSuperfluidUnbondingBalancerToConcentrated(ctx sdk.Context sharesToMigrate sdk.Coin, synthDenomBeforeMigration string, tokenOutMins sdk.Coins, -) (positionId uint64, amount0, amount1 sdk.Int, liquidity sdk.Dec, joinTime time.Time, gammLockId, concentratedLockId, poolIdLeaving, poolIdEntering uint64, err error) { +) (positionId uint64, amount0, amount1 sdk.Int, liquidity sdk.Dec, joinTime time.Time, concentratedLockId, poolIdLeaving, poolIdEntering uint64, err error) { poolIdLeaving, poolIdEntering, preMigrationLock, remainingLockTime, err := k.validateMigration(ctx, sender, lockId, sharesToMigrate) if err != nil { - return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, 0, err + return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, err } // Get the validator address from the synth denom and ensure it is a valid address. valAddr := strings.Split(synthDenomBeforeMigration, "/")[4] _, err = sdk.ValAddressFromBech32(valAddr) if err != nil { - return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, 0, err + return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, err } - // Save unlocking state of lock before force unlocking - wasUnlocking := preMigrationLock.IsUnlocking() - // Force unlock, validate the provided sharesToMigrate, and exit the balancer pool. // This will return the coins that will be used to create the concentrated liquidity position. // It also returns the lock object that contains the remaining shares that were not used in this migration. - exitCoins, remainingSharesLock, err := k.validateSharesToMigrateUnlockAndExitBalancerPool(ctx, sender, poolIdLeaving, preMigrationLock, sharesToMigrate, tokenOutMins, remainingLockTime) + exitCoins, err := k.validateSharesToMigrateUnlockAndExitBalancerPool(ctx, sender, poolIdLeaving, preMigrationLock, sharesToMigrate, tokenOutMins, remainingLockTime) if err != nil { - return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, 0, err + return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, err } // Create a full range (min to max tick) concentrated liquidity position. // If the lock was unlocking, we create a new lock that is unlocking for the remaining time of the old lock. positionId, amount0, amount1, liquidity, joinTime, concentratedLockId, err = k.clk.CreateFullRangePositionUnlocking(ctx, poolIdEntering, sender, exitCoins, remainingLockTime) if err != nil { - return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, 0, err + return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, err } // The previous gamm intermediary account is now invalid for the new lock, since the underlying denom has changed and intermediary accounts are @@ -156,38 +159,16 @@ func (k Keeper) migrateSuperfluidUnbondingBalancerToConcentrated(ctx sdk.Context concentratedLockupDenom := cltypes.GetConcentratedLockupDenomFromPoolId(poolIdEntering) clIntermediateAccount, err := k.GetOrCreateIntermediaryAccount(ctx, concentratedLockupDenom, valAddr) if err != nil { - return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, 0, err + return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, err } // Create a new synthetic lockup for the new intermediary account in an unlocking status err = k.createSyntheticLockup(ctx, concentratedLockId, clIntermediateAccount, unlockingStatus) if err != nil { - return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, 0, err - } - - // If there are any remaining gamm shares after the migration, we must re-create the synthetic lock and begin unlocking it from where it left off. - if remainingSharesLock.ID != 0 { - gammLockId = remainingSharesLock.ID - // Get the previous gamm intermediary account, create a new gamm synthetic lockup, and set it to unlocking. - gammIntermediateAccount, err := k.GetOrCreateIntermediaryAccount(ctx, remainingSharesLock.Coins[0].Denom, valAddr) - if err != nil { - return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, 0, err - } - err = k.createSyntheticLockup(ctx, gammLockId, gammIntermediateAccount, unlockingStatus) - if err != nil { - return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, 0, err - } - - // If lock was previously unlocking, begin the unlock from where it left off. - if wasUnlocking { - _, err = k.lk.BeginForceUnlock(ctx, remainingSharesLock.ID, remainingSharesLock.Coins) - if err != nil { - return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, 0, err - } - } + return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, err } - return positionId, amount0, amount1, liquidity, joinTime, gammLockId, concentratedLockId, poolIdLeaving, poolIdEntering, nil + return positionId, amount0, amount1, liquidity, joinTime, concentratedLockId, poolIdLeaving, poolIdEntering, nil } // migrateNonSuperfluidLockBalancerToConcentrated migrates a user's non-superfluid locked or unlocking balancer position to an unlocking concentrated liquidity position. @@ -199,20 +180,18 @@ func (k Keeper) migrateNonSuperfluidLockBalancerToConcentrated(ctx sdk.Context, lockId uint64, sharesToMigrate sdk.Coin, tokenOutMins sdk.Coins, -) (positionId uint64, amount0, amount1 sdk.Int, liquidity sdk.Dec, joinTime time.Time, gammLockId, concentratedLockId, poolIdLeaving, poolIdEntering uint64, err error) { +) (positionId uint64, amount0, amount1 sdk.Int, liquidity sdk.Dec, joinTime time.Time, concentratedLockId, poolIdLeaving, poolIdEntering uint64, err error) { poolIdLeaving, poolIdEntering, preMigrationLock, remainingLockTime, err := k.validateMigration(ctx, sender, lockId, sharesToMigrate) if err != nil { - return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, 0, err + return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, err } - // Save unlocking state of lock before force unlocking - wasUnlocking := preMigrationLock.IsUnlocking() // Force unlock, validate the provided sharesToMigrate, and exit the balancer pool. // This will return the coins that will be used to create the concentrated liquidity position. // It also returns the lock object that contains the remaining shares that were not used in this migration. - exitCoins, remainingSharesLock, err := k.validateSharesToMigrateUnlockAndExitBalancerPool(ctx, sender, poolIdLeaving, preMigrationLock, sharesToMigrate, tokenOutMins, remainingLockTime) + exitCoins, err := k.validateSharesToMigrateUnlockAndExitBalancerPool(ctx, sender, poolIdLeaving, preMigrationLock, sharesToMigrate, tokenOutMins, remainingLockTime) if err != nil { - return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, 0, err + return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, err } // Create a new lock that is unlocking for the remaining time of the old lock. @@ -220,22 +199,10 @@ func (k Keeper) migrateNonSuperfluidLockBalancerToConcentrated(ctx sdk.Context, // This is because locking without superfluid is pointless in the context of concentrated liquidity. positionId, amount0, amount1, liquidity, joinTime, concentratedLockId, err = k.clk.CreateFullRangePositionUnlocking(ctx, poolIdEntering, sender, exitCoins, remainingLockTime) if err != nil { - return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, 0, err - } - - // If there are remaining gamm shares, we must re-lock them. - if remainingSharesLock.ID != 0 { - gammLockId = remainingSharesLock.ID - // If the gamm lock was unlocking, we begin the unlock from where it left off. - if wasUnlocking { - _, err := k.lk.BeginForceUnlock(ctx, remainingSharesLock.ID, remainingSharesLock.Coins) - if err != nil { - return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, 0, err - } - } + return 0, sdk.Int{}, sdk.Int{}, sdk.Dec{}, time.Time{}, 0, 0, 0, err } - return positionId, amount0, amount1, liquidity, joinTime, gammLockId, concentratedLockId, poolIdLeaving, poolIdEntering, nil + return positionId, amount0, amount1, liquidity, joinTime, concentratedLockId, poolIdLeaving, poolIdEntering, nil } // routeMigration determines the status of the provided lock which is used to determine the method for migration. @@ -246,12 +213,12 @@ func (k Keeper) routeMigration(ctx sdk.Context, sender sdk.AccAddress, lockId ui return nil, Unsupported, fmt.Errorf("lock %d contains more than one synthetic lock", lockId) } - if strings.Contains(synthLocksBeforeMigration[0].SynthDenom, "superbonding") { + if len(synthLocksBeforeMigration) == 0 { + migrationType = NonSuperfluid + } else if strings.Contains(synthLocksBeforeMigration[0].SynthDenom, "superbonding") { migrationType = SuperfluidBonded } else if strings.Contains(synthLocksBeforeMigration[0].SynthDenom, "superunbonding") { migrationType = SuperfluidUnbonding - } else if len(synthLocksBeforeMigration) == 0 { - migrationType = NonSuperfluid } else { return nil, Unsupported, fmt.Errorf("lock %d contains an unsupported synthetic lock", lockId) } @@ -291,14 +258,8 @@ func (k Keeper) validateMigration(ctx sdk.Context, sender sdk.AccAddress, lockId return 0, 0, &lockuptypes.PeriodLock{}, 0, err } - // Further defense in depth, ensuring that the pool ID we are entering can be type cased to a concentrated pool extension. - _, err = k.clk.GetConcentratedPoolById(ctx, poolIdEntering) - if err != nil { - return 0, 0, &lockuptypes.PeriodLock{}, 0, err - } - - // Check that lockID corresponds to sender, and contains correct denomination of LP shares. - preMigrationLock, err = k.validateLockForUnpool(ctx, sender, poolIdLeaving, lockId) + // Check that lockID corresponds to sender and that the denomination of LP shares corresponds to the poolId. + preMigrationLock, err = k.validateGammLockForSuperfluidStaking(ctx, sender, poolIdLeaving, lockId) if err != nil { return 0, 0, &lockuptypes.PeriodLock{}, 0, err } @@ -317,7 +278,7 @@ func (k Keeper) validateMigration(ctx sdk.Context, sender sdk.AccAddress, lockId // 4. Exits the position in the Balancer pool. // 5. Ensures that exactly two coins are returned. // 6. Any remaining shares that were not migrated are re-locked as a new lock for the remaining time on the lock. -func (k Keeper) validateSharesToMigrateUnlockAndExitBalancerPool(ctx sdk.Context, sender sdk.AccAddress, poolIdLeaving uint64, lock *lockuptypes.PeriodLock, sharesToMigrate sdk.Coin, tokenOutMins sdk.Coins, remainingLockTime time.Duration) (exitCoins sdk.Coins, remainingSharesLock lockuptypes.PeriodLock, err error) { +func (k Keeper) validateSharesToMigrateUnlockAndExitBalancerPool(ctx sdk.Context, sender sdk.AccAddress, poolIdLeaving uint64, lock *lockuptypes.PeriodLock, sharesToMigrate sdk.Coin, tokenOutMins sdk.Coins, remainingLockTime time.Duration) (exitCoins sdk.Coins, err error) { // validateMigration ensures that the preMigrationLock contains coins of length 1. gammSharesInLock := lock.Coins[0] @@ -328,37 +289,36 @@ func (k Keeper) validateSharesToMigrateUnlockAndExitBalancerPool(ctx sdk.Context // Otherwise, we must ensure that the shares to migrate is less than or equal to the shares in the lock. if sharesToMigrate.Amount.GT(gammSharesInLock.Amount) { - return sdk.Coins{}, lockuptypes.PeriodLock{}, types.MigrateMoreSharesThanLockHasError{SharesToMigrate: sharesToMigrate.Amount.String(), SharesInLock: gammSharesInLock.Amount.String()} + return sdk.Coins{}, types.MigrateMoreSharesThanLockHasError{SharesToMigrate: sharesToMigrate.Amount.String(), SharesInLock: gammSharesInLock.Amount.String()} } - // Determine if there will be any remaining gamm shares after migration. - remainingGammShares := gammSharesInLock.Sub(sharesToMigrate) - // Finish unlocking directly for locked or unlocking locks - // This also breaks and deletes associated synthetic locks. - err = k.lk.ForceUnlock(ctx, *lock) - if err != nil { - return sdk.Coins{}, lockuptypes.PeriodLock{}, err + if sharesToMigrate.Equal(gammSharesInLock) { + // If migrating the entire lock, force unlock. + // This breaks and deletes associated synthetic locks. + err = k.lk.ForceUnlock(ctx, *lock) + if err != nil { + return sdk.Coins{}, err + } + } else { + // Otherwise, we must split the lock and force unlock the partial shares to migrate. + // This breaks and deletes associated synthetic locks. + err = k.lk.PartialForceUnlock(ctx, *lock, sdk.NewCoins(sharesToMigrate)) + if err != nil { + return sdk.Coins{}, err + } } // Exit the balancer pool position. exitCoins, err = k.gk.ExitPool(ctx, sender, poolIdLeaving, sharesToMigrate.Amount, tokenOutMins) if err != nil { - return sdk.Coins{}, lockuptypes.PeriodLock{}, err + return sdk.Coins{}, err } // Defense in depth, ensuring we are returning exactly two coins. if len(exitCoins) != 2 { - return sdk.Coins{}, lockuptypes.PeriodLock{}, types.TwoTokenBalancerPoolError{NumberOfTokens: len(exitCoins)} - } - - // If there is a remainder of gamm shares, create a new lock with the remaining gamm shares for the remaining lock time. - if !remainingGammShares.IsZero() { - remainingSharesLock, err = k.lk.CreateLock(ctx, sender, sdk.NewCoins(remainingGammShares), remainingLockTime) - if err != nil { - return sdk.Coins{}, lockuptypes.PeriodLock{}, err - } + return sdk.Coins{}, types.TwoTokenBalancerPoolError{NumberOfTokens: len(exitCoins)} } - return exitCoins, remainingSharesLock, nil + return exitCoins, nil } diff --git a/x/superfluid/keeper/migrate_test.go b/x/superfluid/keeper/migrate_test.go index 42174516d32..03f2984fd9e 100644 --- a/x/superfluid/keeper/migrate_test.go +++ b/x/superfluid/keeper/migrate_test.go @@ -53,15 +53,28 @@ func (suite *KeeperTestSuite) TestRouteLockedBalancerToConcentratedMigration() { "lock that is superfluid delegated, not unlocking (partial shares)": { // migrateSuperfluidBondedBalancerToConcentrated superfluidDelegated: true, - percentOfSharesToMigrate: sdk.MustNewDecFromStr("0.5"), + percentOfSharesToMigrate: sdk.MustNewDecFromStr("0.4"), }, - "lock that is superfluid undelegating, not unlocking": { + "lock that is superfluid undelegating, not unlocking (full shares)": { // migrateSuperfluidUnbondingBalancerToConcentrated superfluidDelegated: true, superfluidUndelegating: true, - percentOfSharesToMigrate: sdk.MustNewDecFromStr("0.5"), + percentOfSharesToMigrate: sdk.MustNewDecFromStr("1"), }, - "lock that is superfluid undelegating, unlocking": { + "lock that is superfluid undelegating, not unlocking (partial shares)": { + // migrateSuperfluidUnbondingBalancerToConcentrated + superfluidDelegated: true, + superfluidUndelegating: true, + percentOfSharesToMigrate: sdk.MustNewDecFromStr("0.4"), + }, + "lock that is superfluid undelegating, unlocking (full shares)": { + // migrateSuperfluidUnbondingBalancerToConcentrated + superfluidDelegated: true, + superfluidUndelegating: true, + unlocking: true, + percentOfSharesToMigrate: sdk.MustNewDecFromStr("1"), + }, + "lock that is superfluid undelegating, unlocking (partial shares)": { // migrateSuperfluidUnbondingBalancerToConcentrated superfluidDelegated: true, superfluidUndelegating: true, @@ -141,8 +154,10 @@ func (suite *KeeperTestSuite) TestRouteLockedBalancerToConcentratedMigration() { originalGammLockId = originalGammLockId + 1 } + balancerDelegationPre, _ := stakingKeeper.GetDelegation(ctx, balancerIntermediaryAcc.GetAccAddress(), valAddr) + // Run the migration logic. - positionId, amount0, amount1, _, _, poolIdLeaving, poolIdEntering, newGammLockId, concentratedLockId, err := superfluidKeeper.RouteLockedBalancerToConcentratedMigration(ctx, poolJoinAcc, originalGammLockId, coinsToMigrate, tc.minExitCoins) + positionId, amount0, amount1, _, _, poolIdLeaving, poolIdEntering, concentratedLockId, err := superfluidKeeper.RouteLockedBalancerToConcentratedMigration(ctx, poolJoinAcc, originalGammLockId, coinsToMigrate, tc.minExitCoins) if tc.expectedError != nil { suite.Require().Error(err) suite.Require().ErrorIs(err, tc.expectedError) @@ -153,7 +168,7 @@ func (suite *KeeperTestSuite) TestRouteLockedBalancerToConcentratedMigration() { suite.ValidateMigrateResult( ctx, - positionId, balancerPooId, poolIdLeaving, clPoolId, poolIdEntering, originalGammLockId, newGammLockId, + positionId, balancerPooId, poolIdLeaving, clPoolId, poolIdEntering, tc.percentOfSharesToMigrate, *balancerLock, joinPoolAmt, @@ -161,26 +176,73 @@ func (suite *KeeperTestSuite) TestRouteLockedBalancerToConcentratedMigration() { amount0, amount1, ) - // Additional checks if the original gamm lock was superfluid staked. - if tc.superfluidDelegated { - // Check if migration deleted intermediary account connection. + // If the lock was superfluid delegated: + if tc.superfluidDelegated && !tc.superfluidUndelegating { + if tc.percentOfSharesToMigrate.Equal(sdk.OneDec()) { + // If we migrated all the shares: + + // The intermediary account connection to the old gamm lock should be deleted. + addr := superfluidKeeper.GetLockIdIntermediaryAccountConnection(ctx, originalGammLockId) + suite.Require().Equal(addr.String(), "") + + // The synthetic lockup should be deleted. + _, err = lockupKeeper.GetSyntheticLockup(ctx, originalGammLockId, keeper.StakingSyntheticDenom(balancerLock.Coins[0].Denom, valAddr.String())) + suite.Require().Error(err) + + // The delegation from the balancer intermediary account holder should not exist. + delegation, found := stakingKeeper.GetDelegation(ctx, balancerIntermediaryAcc.GetAccAddress(), valAddr) + suite.Require().False(found, "expected no delegation, found delegation w/ %d shares", delegation.Shares) + + // Check that the original gamm lockup is deleted. + _, err := suite.App.LockupKeeper.GetLockByID(ctx, originalGammLockId) + suite.Require().Error(err) + } else if tc.percentOfSharesToMigrate.LT(sdk.OneDec()) { + // If we migrated part of the shares: + // The intermediary account connection to the old gamm lock should still be present. + addr := superfluidKeeper.GetLockIdIntermediaryAccountConnection(ctx, originalGammLockId) + suite.Require().Equal(balancerIntermediaryAcc.GetAccAddress().String(), addr.String()) + + // Check if migration deleted synthetic lockup. + _, err = lockupKeeper.GetSyntheticLockup(ctx, originalGammLockId, keeper.StakingSyntheticDenom(balancerLock.Coins[0].Denom, valAddr.String())) + suite.Require().NoError(err) + + // The delegation from the balancer intermediary account holder should still exist. + delegation, found := stakingKeeper.GetDelegation(ctx, balancerIntermediaryAcc.GetAccAddress(), valAddr) + suite.Require().True(found, "expected delegation, found delegation no delegation") + suite.Require().Equal(balancerDelegationPre.Shares.Sub(balancerDelegationPre.Shares.Mul(tc.percentOfSharesToMigrate)).RoundInt().String(), delegation.Shares.RoundInt().String(), "expected %d shares, found %d shares", balancerDelegationPre.Shares.Mul(tc.percentOfSharesToMigrate).RoundInt().String(), delegation.Shares.String()) + + // Check what is remaining in the original gamm lock. + lock, err := suite.App.LockupKeeper.GetLockByID(ctx, originalGammLockId) + suite.Require().NoError(err) + suite.Require().Equal(balancerPoolShareOut.Amount.Sub(coinsToMigrate.Amount).String(), lock.Coins[0].Amount.String(), "expected %s shares, found %s shares", lock.Coins[0].Amount.String(), balancerPoolShareOut.Amount.Sub(coinsToMigrate.Amount).String()) + } + // Check the new superfluid staked amount. + clIntermediaryAcc := superfluidKeeper.GetLockIdIntermediaryAccountConnection(ctx, concentratedLockId) + delegation, found := stakingKeeper.GetDelegation(ctx, clIntermediaryAcc, valAddr) + suite.Require().True(found, "expected delegation, found delegation no delegation") + suite.Require().Equal(balancerDelegationPre.Shares.Mul(tc.percentOfSharesToMigrate).RoundInt().Sub(sdk.OneInt()).String(), delegation.Shares.RoundInt().String(), "expected %d shares, found %d shares", balancerDelegationPre.Shares.Mul(tc.percentOfSharesToMigrate).RoundInt().String(), delegation.Shares.String()) + } + + // If the lock was superfluid undelegating: + if tc.superfluidDelegated && tc.superfluidUndelegating { + // Regardless oh how many shares we migrated: + + // The intermediary account connection to the old gamm lock should be deleted. addr := superfluidKeeper.GetLockIdIntermediaryAccountConnection(ctx, originalGammLockId) suite.Require().Equal(addr.String(), "") - // Check if migration deleted synthetic lockup. + // The synthetic lockup should be deleted. _, err = lockupKeeper.GetSyntheticLockup(ctx, originalGammLockId, keeper.StakingSyntheticDenom(balancerLock.Coins[0].Denom, valAddr.String())) suite.Require().Error(err) - // If a new gamm position was not created and restaked, check if delegation has reduced from intermediary account. - if tc.percentOfSharesToMigrate.Equal(sdk.OneDec()) { - delegation, found := stakingKeeper.GetDelegation(ctx, balancerIntermediaryAcc.GetAccAddress(), valAddr) - suite.Require().False(found, "expected no delegation, found delegation w/ %d shares", delegation.Shares) - } + // The delegation from the intermediary account holder does not exist. + delegation, found := stakingKeeper.GetDelegation(ctx, balancerIntermediaryAcc.GetAccAddress(), valAddr) + suite.Require().False(found, "expected no delegation, found delegation w/ %d shares", delegation.Shares) } // Run slashing logic if the test case is superfluid staked or superfluid undelegating and check if the new and old locks are slashed. slashExpected := tc.superfluidDelegated || tc.superfluidUndelegating - suite.SlashAndValidateResult(ctx, newGammLockId, concentratedLockId, poolIdEntering, tc.percentOfSharesToMigrate, valAddr, *balancerLock, slashExpected) + suite.SlashAndValidateResult(ctx, originalGammLockId, concentratedLockId, poolIdEntering, tc.percentOfSharesToMigrate, valAddr, *balancerLock, slashExpected) }) } } @@ -258,8 +320,10 @@ func (suite *KeeperTestSuite) TestMigrateSuperfluidBondedBalancerToConcentrated( originalGammLockId = originalGammLockId + 1 } + balancerDelegationPre, _ := stakingKeeper.GetDelegation(ctx, balancerIntermediaryAcc.GetAccAddress(), valAddr) + // System under test. - positionId, amount0, amount1, _, _, newGammLockId, concentratedLockId, poolIdLeaving, poolIdEntering, err := superfluidKeeper.MigrateSuperfluidBondedBalancerToConcentrated(ctx, poolJoinAcc, originalGammLockId, coinsToMigrate, synthLockBeforeMigration[0].SynthDenom, tc.tokenOutMins) + positionId, amount0, amount1, liquidityMigrated, _, concentratedLockId, poolIdLeaving, poolIdEntering, err := superfluidKeeper.MigrateSuperfluidBondedBalancerToConcentrated(ctx, poolJoinAcc, originalGammLockId, coinsToMigrate, synthLockBeforeMigration[0].SynthDenom, tc.tokenOutMins) if tc.expectedError != nil { suite.Require().Error(err) suite.Require().ErrorContains(err, tc.expectedError.Error()) @@ -270,7 +334,7 @@ func (suite *KeeperTestSuite) TestMigrateSuperfluidBondedBalancerToConcentrated( suite.ValidateMigrateResult( ctx, - positionId, balancerPooId, poolIdLeaving, clPoolId, poolIdEntering, originalGammLockId, newGammLockId, + positionId, balancerPooId, poolIdLeaving, clPoolId, poolIdEntering, tc.percentOfSharesToMigrate, *balancerLock, joinPoolAmt, @@ -278,32 +342,70 @@ func (suite *KeeperTestSuite) TestMigrateSuperfluidBondedBalancerToConcentrated( amount0, amount1, ) - // Check if migration deleted intermediary account connection. - originalGammIntermediaryAccount := superfluidKeeper.GetLockIdIntermediaryAccountConnection(ctx, originalGammLockId) - suite.Require().Equal(originalGammIntermediaryAccount.String(), "") + if tc.percentOfSharesToMigrate.Equal(sdk.OneDec()) { + // If we migrated all the shares: + + // The intermediary account connection to the old gamm lock should be deleted. + addr := superfluidKeeper.GetLockIdIntermediaryAccountConnection(ctx, originalGammLockId) + suite.Require().Equal(addr.String(), "") + + // The synthetic lockup should be deleted. + _, err = lockupKeeper.GetSyntheticLockup(ctx, originalGammLockId, keeper.StakingSyntheticDenom(balancerLock.Coins[0].Denom, valAddr.String())) + suite.Require().Error(err) + + // The delegation from the intermediary account holder should not exist. + delegation, found := stakingKeeper.GetDelegation(ctx, balancerIntermediaryAcc.GetAccAddress(), valAddr) + suite.Require().False(found, "expected no delegation, found delegation w/ %d shares", delegation.Shares) + + // Check that the original gamm lockup is deleted. + _, err := suite.App.LockupKeeper.GetLockByID(ctx, originalGammLockId) + suite.Require().Error(err) + } else if tc.percentOfSharesToMigrate.LT(sdk.OneDec()) { + // If we migrated part of the shares: + // The intermediary account connection to the old gamm lock should still be present. + addr := superfluidKeeper.GetLockIdIntermediaryAccountConnection(ctx, originalGammLockId) + suite.Require().Equal(balancerIntermediaryAcc.GetAccAddress().String(), addr.String()) + + // Confirm that migration did not delete synthetic lockup. + gammSynthLock, err := lockupKeeper.GetSyntheticLockup(ctx, originalGammLockId, keeper.StakingSyntheticDenom(balancerLock.Coins[0].Denom, valAddr.String())) + suite.Require().NoError(err) + + suite.Require().Equal(originalGammLockId, gammSynthLock.UnderlyingLockId) - // Check if migration deleted synthetic lockup. - _, err = lockupKeeper.GetSyntheticLockup(ctx, originalGammLockId, keeper.StakingSyntheticDenom(balancerLock.Coins[0].Denom, valAddr.String())) - suite.Require().Error(err) + // The delegation from the intermediary account holder should still exist. + _, found := stakingKeeper.GetDelegation(ctx, balancerIntermediaryAcc.GetAccAddress(), valAddr) + suite.Require().True(found, "expected delegation, found delegation no delegation") + + // Check what is remaining in the original gamm lock. + lock, err := suite.App.LockupKeeper.GetLockByID(ctx, originalGammLockId) + suite.Require().NoError(err) + suite.Require().Equal(balancerPoolShareOut.Amount.Sub(coinsToMigrate.Amount).String(), lock.Coins[0].Amount.String(), "expected %s shares, found %s shares", lock.Coins[0].Amount.String(), balancerPoolShareOut.Amount.Sub(coinsToMigrate.Amount).String()) + } + // Check the new superfluid staked amount. + clIntermediaryAcc := superfluidKeeper.GetLockIdIntermediaryAccountConnection(ctx, concentratedLockId) + delegation, found := stakingKeeper.GetDelegation(ctx, clIntermediaryAcc, valAddr) + suite.Require().True(found, "expected delegation, found delegation no delegation") + suite.Require().Equal(balancerDelegationPre.Shares.Mul(tc.percentOfSharesToMigrate).RoundInt().Sub(sdk.OneInt()).String(), delegation.Shares.RoundInt().String(), "expected %d shares, found %d shares", balancerDelegationPre.Shares.Mul(tc.percentOfSharesToMigrate).RoundInt().String(), delegation.Shares.String()) // Check if the new intermediary account connection was created. newConcentratedIntermediaryAccount := superfluidKeeper.GetLockIdIntermediaryAccountConnection(ctx, concentratedLockId) suite.Require().NotEqual(newConcentratedIntermediaryAccount.String(), "") - // Check if the new synthetic bonded lockup was created. + // Check newly created concentrated lock. concentratedLock, err := lockupKeeper.GetLockByID(ctx, concentratedLockId) suite.Require().NoError(err) - _, err = lockupKeeper.GetSyntheticLockup(ctx, concentratedLockId, keeper.StakingSyntheticDenom(concentratedLock.Coins[0].Denom, valAddr.String())) + suite.Require().Equal(liquidityMigrated.TruncateInt().String(), concentratedLock.Coins[0].Amount.String(), "expected %s shares, found %s shares", coinsToMigrate.Amount.String(), concentratedLock.Coins[0].Amount.String()) + suite.Require().Equal(balancerLock.Duration, concentratedLock.Duration) + suite.Require().Equal(balancerLock.EndTime, concentratedLock.EndTime) + + // Check if the new synthetic bonded lockup was created. + clSynthLock, err := lockupKeeper.GetSyntheticLockup(ctx, concentratedLockId, keeper.StakingSyntheticDenom(concentratedLock.Coins[0].Denom, valAddr.String())) suite.Require().NoError(err) - // If a new gamm position was not created and restaked, check if delegation has reduced from intermediary account. - if tc.percentOfSharesToMigrate.Equal(sdk.OneDec()) { - delegation, found := stakingKeeper.GetDelegation(ctx, balancerIntermediaryAcc.GetAccAddress(), valAddr) - suite.Require().False(found, "expected no delegation, found delegation w/ %d shares", delegation.Shares) - } + suite.Require().Equal(concentratedLockId, clSynthLock.UnderlyingLockId) // Run slashing logic and check if the new and old locks are slashed. - suite.SlashAndValidateResult(ctx, newGammLockId, concentratedLockId, clPoolId, tc.percentOfSharesToMigrate, valAddr, *balancerLock, true) + suite.SlashAndValidateResult(ctx, originalGammLockId, concentratedLockId, clPoolId, tc.percentOfSharesToMigrate, valAddr, *balancerLock, true) }) } } @@ -375,7 +477,7 @@ func (suite *KeeperTestSuite) TestMigrateSuperfluidUnbondingBalancerToConcentrat } // System under test. - positionId, amount0, amount1, _, _, newGammLockId, concentratedLockId, poolIdLeaving, poolIdEntering, err := superfluidKeeper.MigrateSuperfluidUnbondingBalancerToConcentrated(ctx, poolJoinAcc, originalGammLockId, coinsToMigrate, synthLockBeforeMigration[0].SynthDenom, tc.tokenOutMins) + positionId, amount0, amount1, liquidityMigrated, _, concentratedLockId, poolIdLeaving, poolIdEntering, err := superfluidKeeper.MigrateSuperfluidUnbondingBalancerToConcentrated(ctx, poolJoinAcc, originalGammLockId, coinsToMigrate, synthLockBeforeMigration[0].SynthDenom, tc.tokenOutMins) if tc.expectedError != nil { suite.Require().Error(err) suite.Require().ErrorContains(err, tc.expectedError.Error()) @@ -386,7 +488,7 @@ func (suite *KeeperTestSuite) TestMigrateSuperfluidUnbondingBalancerToConcentrat suite.ValidateMigrateResult( ctx, - positionId, balancerPooId, poolIdLeaving, clPoolId, poolIdEntering, originalGammLockId, newGammLockId, + positionId, balancerPooId, poolIdLeaving, clPoolId, poolIdEntering, tc.percentOfSharesToMigrate, *balancerLock, joinPoolAmt, @@ -394,14 +496,37 @@ func (suite *KeeperTestSuite) TestMigrateSuperfluidUnbondingBalancerToConcentrat amount0, amount1, ) - // Check if the new synthetic unbonding lockup was created. + if tc.percentOfSharesToMigrate.Equal(sdk.OneDec()) { + // If we migrated all the shares: + + // The synthetic lockup should be deleted. + _, err = lockupKeeper.GetSyntheticLockup(ctx, originalGammLockId, keeper.UnstakingSyntheticDenom(balancerLock.Coins[0].Denom, valAddr.String())) + suite.Require().Error(err) + } else if tc.percentOfSharesToMigrate.LT(sdk.OneDec()) { + // If we migrated part of the shares: + + // The synthetic lockup should not be deleted. + gammSynthLock, err := lockupKeeper.GetSyntheticLockup(ctx, originalGammLockId, keeper.UnstakingSyntheticDenom(balancerLock.Coins[0].Denom, valAddr.String())) + suite.Require().NoError(err) + + suite.Require().Equal(originalGammLockId, gammSynthLock.UnderlyingLockId) + } + + // Check newly created concentrated lock. concentratedLock, err := lockupKeeper.GetLockByID(ctx, concentratedLockId) suite.Require().NoError(err) - _, err = lockupKeeper.GetSyntheticLockup(ctx, concentratedLockId, keeper.UnstakingSyntheticDenom(concentratedLock.Coins[0].Denom, valAddr.String())) + suite.Require().Equal(liquidityMigrated.TruncateInt().String(), concentratedLock.Coins[0].Amount.String(), "expected %s shares, found %s shares", coinsToMigrate.Amount.String(), concentratedLock.Coins[0].Amount.String()) + suite.Require().Equal(balancerLock.Duration, concentratedLock.Duration) + suite.Require().Equal(suite.Ctx.BlockTime().Add(balancerLock.Duration), concentratedLock.EndTime) + + // Check if the new synthetic unbonding lockup was created. + clSynthLock, err := lockupKeeper.GetSyntheticLockup(ctx, concentratedLockId, keeper.UnstakingSyntheticDenom(concentratedLock.Coins[0].Denom, valAddr.String())) suite.Require().NoError(err) + suite.Require().Equal(concentratedLockId, clSynthLock.UnderlyingLockId) + // Run slashing logic and check if the new and old locks are slashed. - suite.SlashAndValidateResult(ctx, newGammLockId, concentratedLockId, clPoolId, tc.percentOfSharesToMigrate, valAddr, *balancerLock, true) + suite.SlashAndValidateResult(ctx, originalGammLockId, concentratedLockId, clPoolId, tc.percentOfSharesToMigrate, valAddr, *balancerLock, true) }) } } @@ -442,6 +567,7 @@ func (suite *KeeperTestSuite) TestMigrateNonSuperfluidLockBalancerToConcentrated suite.Ctx = suite.Ctx.WithBlockTime(defaultJoinTime) ctx := suite.Ctx superfluidKeeper := suite.App.SuperfluidKeeper + lockupKeeper := suite.App.LockupKeeper // We bundle all migration setup into a single function to avoid repeating the same code for each test case. joinPoolAmt, _, balancerLock, _, poolJoinAcc, balancerPooId, clPoolId, balancerPoolShareOut, valAddr := suite.SetupMigrationTest(ctx, false, false, tc.unlocking, tc.percentOfSharesToMigrate) @@ -458,7 +584,7 @@ func (suite *KeeperTestSuite) TestMigrateNonSuperfluidLockBalancerToConcentrated suite.Require().Equal(migrationType, keeper.NonSuperfluid) // System under test. - positionId, amount0, amount1, _, _, newGammLockId, concentratedLockId, poolIdLeaving, poolIdEntering, err := superfluidKeeper.MigrateNonSuperfluidLockBalancerToConcentrated(ctx, poolJoinAcc, originalGammLockId, coinsToMigrate, tc.tokenOutMins) + positionId, amount0, amount1, liquidityMigrated, _, concentratedLockId, poolIdLeaving, poolIdEntering, err := superfluidKeeper.MigrateNonSuperfluidLockBalancerToConcentrated(ctx, poolJoinAcc, originalGammLockId, coinsToMigrate, tc.tokenOutMins) if tc.expectedError != nil { suite.Require().Error(err) suite.Require().ErrorContains(err, tc.expectedError.Error()) @@ -469,7 +595,7 @@ func (suite *KeeperTestSuite) TestMigrateNonSuperfluidLockBalancerToConcentrated suite.ValidateMigrateResult( ctx, - positionId, balancerPooId, poolIdLeaving, clPoolId, poolIdEntering, originalGammLockId, newGammLockId, + positionId, balancerPooId, poolIdLeaving, clPoolId, poolIdEntering, tc.percentOfSharesToMigrate, *balancerLock, joinPoolAmt, @@ -477,8 +603,15 @@ func (suite *KeeperTestSuite) TestMigrateNonSuperfluidLockBalancerToConcentrated amount0, amount1, ) + // Check newly created concentrated lock. + concentratedLock, err := lockupKeeper.GetLockByID(ctx, concentratedLockId) + suite.Require().NoError(err) + suite.Require().Equal(liquidityMigrated.TruncateInt().String(), concentratedLock.Coins[0].Amount.String(), "expected %s shares, found %s shares", coinsToMigrate.Amount.String(), concentratedLock.Coins[0].Amount.String()) + suite.Require().Equal(balancerLock.Duration, concentratedLock.Duration) + suite.Require().Equal(suite.Ctx.BlockTime().Add(balancerLock.Duration), concentratedLock.EndTime) + // Run slashing logic and check if the new and old locks are not slashed. - suite.SlashAndValidateResult(ctx, newGammLockId, concentratedLockId, clPoolId, tc.percentOfSharesToMigrate, valAddr, *balancerLock, false) + suite.SlashAndValidateResult(ctx, originalGammLockId, concentratedLockId, clPoolId, tc.percentOfSharesToMigrate, valAddr, *balancerLock, false) }) } } @@ -716,7 +849,6 @@ func (suite *KeeperTestSuite) TestValidateSharesToMigrateUnlockAndExitBalancerPo lock, err := lockupKeeper.GetLockByID(ctx, originalGammLockId) suite.Require().NoError(err) - preMigrationLock := *lock if tc.overwritePreMigrationLock { lock.ID = lock.ID + 1 @@ -739,7 +871,7 @@ func (suite *KeeperTestSuite) TestValidateSharesToMigrateUnlockAndExitBalancerPo } // System under test - exitCoins, remainingSharesLock, err := superfluidKeeper.ValidateSharesToMigrateUnlockAndExitBalancerPool(ctx, poolJoinAcc, balancerPooId, lock, coinsToMigrate, tc.tokenOutMins, lock.Duration) + exitCoins, err := superfluidKeeper.ValidateSharesToMigrateUnlockAndExitBalancerPool(ctx, poolJoinAcc, balancerPooId, lock, coinsToMigrate, tc.tokenOutMins, lock.Duration) if tc.expectedError != nil { suite.Require().Error(err) suite.Require().ErrorContains(err, tc.expectedError.Error()) @@ -747,19 +879,23 @@ func (suite *KeeperTestSuite) TestValidateSharesToMigrateUnlockAndExitBalancerPo } suite.Require().NoError(err) - if tc.percentOfSharesToMigrate.Equal(sdk.OneDec()) { - suite.Require().Equal(lockuptypes.PeriodLock{}, remainingSharesLock) - } else { - suite.Require().Equal(preMigrationLock.Coins[0].Amount.Sub(sharesToMigrate), remainingSharesLock.Coins[0].Amount) - suite.Require().Equal(preMigrationLock.Coins[0].Denom, remainingSharesLock.Coins[0].Denom) - suite.Require().Equal(preMigrationLock.Duration, remainingSharesLock.Duration) - } - defaultErrorTolerance := osmomath.ErrTolerance{ AdditiveTolerance: sdk.NewDec(1), RoundingDir: osmomath.RoundDown, } + if tc.percentOfSharesToMigrate.Equal(sdk.OneDec()) { + // If all of the shares were migrated, the original lock should be deleted + _, err := lockupKeeper.GetLockByID(ctx, originalGammLockId) + suite.Require().Error(err) + } else { + // If only a portion of the shares were migrated, the original lock should still exist (with the remaining shares) + lock, err := lockupKeeper.GetLockByID(ctx, originalGammLockId) + suite.Require().NoError(err) + expectedSharesStillInOldLock := balancerPoolShareOut.Amount.Sub(sharesToMigrate) + suite.Require().Equal(expectedSharesStillInOldLock.String(), lock.Coins[0].Amount.String()) + } + for _, coin := range exitCoins { // Check that the exit coin is the same amount that we joined with (with one unit rounding down) suite.Require().Equal(0, defaultErrorTolerance.Compare(tokensIn.AmountOf(coin.Denom).ToDec().Mul(tc.percentOfSharesToMigrate).RoundInt(), coin.Amount)) @@ -887,13 +1023,15 @@ func (suite *KeeperTestSuite) SetupMigrationTest(ctx sdk.Context, superfluidDele return joinPoolAmt, balancerIntermediaryAcc, balancerLock, poolCreateAcc, poolJoinAcc, balancerPooId, clPoolId, balancerPoolShareOut, valAddr } -func (suite *KeeperTestSuite) SlashAndValidateResult(ctx sdk.Context, newGammLockId, concentratedLockId, poolIdEntering uint64, percentOfSharesToMigrate sdk.Dec, valAddr sdk.ValAddress, balancerLock lockuptypes.PeriodLock, expectSlash bool) { +func (suite *KeeperTestSuite) SlashAndValidateResult(ctx sdk.Context, gammLockId, concentratedLockId, poolIdEntering uint64, percentOfSharesToMigrate sdk.Dec, valAddr sdk.ValAddress, balancerLock lockuptypes.PeriodLock, expectSlash bool) { // Retrieve the concentrated lock and gamm lock prior to slashing. concentratedLockPreSlash, err := suite.App.LockupKeeper.GetLockByID(suite.Ctx, concentratedLockId) suite.Require().NoError(err) - gammLockPreSlash, err := suite.App.LockupKeeper.GetLockByID(suite.Ctx, newGammLockId) - if err != nil && newGammLockId != 0 { + gammLockPreSlash, err := suite.App.LockupKeeper.GetLockByID(suite.Ctx, gammLockId) + if percentOfSharesToMigrate.LT(sdk.OneDec()) { suite.Require().NoError(err) + } else { + suite.Require().Error(err) } // Slash the validator. @@ -907,9 +1045,11 @@ func (suite *KeeperTestSuite) SlashAndValidateResult(ctx sdk.Context, newGammLoc // Retrieve the concentrated lock and gamm lock after slashing. concentratedLockPostSlash, err := suite.App.LockupKeeper.GetLockByID(suite.Ctx, concentratedLockId) suite.Require().NoError(err) - gammLockPostSlash, err := suite.App.LockupKeeper.GetLockByID(suite.Ctx, newGammLockId) - if err != nil && newGammLockId != 0 { + gammLockPostSlash, err := suite.App.LockupKeeper.GetLockByID(suite.Ctx, gammLockId) + if percentOfSharesToMigrate.LT(sdk.OneDec()) { suite.Require().NoError(err) + } else { + suite.Require().Error(err) } // Check if the concentrated lock was slashed. @@ -935,26 +1075,13 @@ func (suite *KeeperTestSuite) SlashAndValidateResult(ctx sdk.Context, newGammLoc func (suite *KeeperTestSuite) ValidateMigrateResult( ctx sdk.Context, - positionId, balancerPooId, poolIdLeaving, clPoolId, poolIdEntering, originalGammLockId, newGammLockId uint64, + positionId, balancerPooId, poolIdLeaving, clPoolId, poolIdEntering uint64, percentOfSharesToMigrate sdk.Dec, balancerLock lockuptypes.PeriodLock, joinPoolAmt sdk.Coins, balancerPoolShareOut, coinsToMigrate sdk.Coin, amount0, amount1 sdk.Int, ) { - lockupKeeper := suite.App.LockupKeeper - - newGammLock, err := lockupKeeper.GetLockByID(ctx, newGammLockId) - if percentOfSharesToMigrate.LT(sdk.OneDec()) { - // If we migrated a subset of the balancer LP tokens, we expect the new gamm lock to have a the same end time. - suite.Require().NoError(err) - suite.Require().Equal(balancerLock.EndTime, newGammLock.EndTime) - } else { - // If we migrated all of the balancer LP tokens, we expect no new gamm lock to be created. - suite.Require().Error(err) - suite.Require().Nil(newGammLock) - } - // Check that the concentrated liquidity position now exists position, err := suite.App.ConcentratedLiquidityKeeper.GetPositionLiquidity(ctx, positionId) suite.Require().NoError(err) @@ -973,26 +1100,4 @@ func (suite *KeeperTestSuite) ValidateMigrateResult( } suite.Require().Equal(0, defaultErrorTolerance.Compare(joinPoolAmt.AmountOf(defaultPoolAssets[0].Token.Denom).ToDec().Mul(percentOfSharesToMigrate).RoundInt(), amount0)) suite.Require().Equal(0, defaultErrorTolerance.Compare(joinPoolAmt.AmountOf(defaultPoolAssets[1].Token.Denom).ToDec().Mul(percentOfSharesToMigrate).RoundInt(), amount1)) - - // Check if the original gamm lock was deleted. - _, err = lockupKeeper.GetLockByID(ctx, originalGammLockId) - suite.Require().Error(err) - - // If we didn't migrate the entire gamm lock, we expect a new gamm lock to be created with the remaining lock time and coins associated with it. - if percentOfSharesToMigrate.LT(sdk.OneDec()) { - // Check if the new gamm lock was created. - newGammLock, err := lockupKeeper.GetLockByID(ctx, newGammLockId) - suite.Require().NoError(err) - // The new gamm lock should have the same owner and end time. - // The new gamm lock should have the difference in coins between the original lock and the coins migrated. - suite.Require().Equal(sdk.NewCoins(balancerPoolShareOut.Sub(coinsToMigrate)).String(), newGammLock.Coins.String()) - suite.Require().Equal(balancerLock.Owner, newGammLock.Owner) - suite.Require().Equal(balancerLock.EndTime.String(), newGammLock.EndTime.String()) - // If original gamm lock was unlocking, the new gamm lock should also be unlocking. - if balancerLock.IsUnlocking() { - suite.Require().True(newGammLock.IsUnlocking()) - } - } else { - suite.Require().Equal(uint64(0), newGammLockId) - } } diff --git a/x/superfluid/keeper/msg_server.go b/x/superfluid/keeper/msg_server.go index 7b9138b3ce5..01cfb6bfe3e 100644 --- a/x/superfluid/keeper/msg_server.go +++ b/x/superfluid/keeper/msg_server.go @@ -205,7 +205,7 @@ func (server msgServer) UnlockAndMigrateSharesToFullRangeConcentratedPosition(go return nil, err } - positionId, amount0, amount1, liquidity, joinTime, poolIdLeaving, poolIdEntering, gammLockId, clLockId, err := server.keeper.RouteLockedBalancerToConcentratedMigration(ctx, sender, msg.LockId, msg.SharesToMigrate, msg.TokenOutMins) + positionId, amount0, amount1, liquidity, joinTime, poolIdLeaving, poolIdEntering, clLockId, err := server.keeper.RouteLockedBalancerToConcentratedMigration(ctx, sender, msg.LockId, msg.SharesToMigrate, msg.TokenOutMins) if err != nil { return nil, err } @@ -215,7 +215,6 @@ func (server msgServer) UnlockAndMigrateSharesToFullRangeConcentratedPosition(go types.TypeEvtUnlockAndMigrateShares, sdk.NewAttribute(types.AttributeKeyPoolIdEntering, strconv.FormatUint(poolIdEntering, 10)), sdk.NewAttribute(types.AttributeKeyPoolIdLeaving, strconv.FormatUint(poolIdLeaving, 10)), - sdk.NewAttribute(types.AttributeGammLockId, strconv.FormatUint(gammLockId, 10)), sdk.NewAttribute(types.AttributeConcentratedLockId, strconv.FormatUint(clLockId, 10)), sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender), sdk.NewAttribute(types.AttributePositionId, strconv.FormatUint(positionId, 10)), diff --git a/x/superfluid/keeper/stake.go b/x/superfluid/keeper/stake.go index 8ead741735f..f5607d05f60 100644 --- a/x/superfluid/keeper/stake.go +++ b/x/superfluid/keeper/stake.go @@ -303,11 +303,75 @@ func (k Keeper) SuperfluidUndelegate(ctx sdk.Context, sender string, lockID uint // SuperfluidUndelegateToConcentratedPosition starts undelegating superfluid delegated position for the given lock. It behaves similarly to SuperfluidUndelegate, // however it does not create a new synthetic lockup representing the unstaking side. This is because after the time this function is called, we might // 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 thi function with caution to avoid accidentally missing synthetic lock creation. +// 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) } +// partialUndelegateCommon acts similarly to undelegateCommon, but undelegates a partial amount of the lock's delegation rather than the full amount. The amount +// that is undelegated is placed in a new lock. This function returns the intermediary account associated with the original lock ID as well as the new lock that was created. +// An error is returned if the amount to undelegate is greater than the locked amount. +func (k Keeper) partialUndelegateCommon(ctx sdk.Context, sender string, lockID uint64, amountToUndelegate sdk.Coin) (intermediaryAcc types.SuperfluidIntermediaryAccount, newlock *lockuptypes.PeriodLock, err error) { + lock, err := k.lk.GetLockByID(ctx, lockID) + if err != nil { + return types.SuperfluidIntermediaryAccount{}, &lockuptypes.PeriodLock{}, err + } + err = k.validateLockForSF(ctx, lock, sender) + if err != nil { + return types.SuperfluidIntermediaryAccount{}, &lockuptypes.PeriodLock{}, err + } + + if amountToUndelegate.Amount.GTE(lock.Coins[0].Amount) { + return types.SuperfluidIntermediaryAccount{}, &lockuptypes.PeriodLock{}, fmt.Errorf("partial undelegate amount must be less than the locked amount") + } + + // get the intermediate account associated with lock id, and delete the connection. + intermediaryAcc, found := k.GetIntermediaryAccountFromLockId(ctx, lockID) + if !found { + return types.SuperfluidIntermediaryAccount{}, &lockuptypes.PeriodLock{}, types.ErrNotSuperfluidUsedLockup + } + + // undelegate the desired lock amount, and burn the minted osmo. + amount, err := k.GetSuperfluidOSMOTokens(ctx, intermediaryAcc.Denom, amountToUndelegate.Amount) + if err != nil { + return types.SuperfluidIntermediaryAccount{}, &lockuptypes.PeriodLock{}, err + } + err = k.forceUndelegateAndBurnOsmoTokens(ctx, amount, intermediaryAcc) + if err != nil { + return types.SuperfluidIntermediaryAccount{}, &lockuptypes.PeriodLock{}, err + } + + // Move the funds from the old lock to a new lock with the remaining amount. + newLock, err := k.lk.SplitLock(ctx, *lock, sdk.NewCoins(amountToUndelegate), true) + if err != nil { + return types.SuperfluidIntermediaryAccount{}, &lockuptypes.PeriodLock{}, err + } + + return intermediaryAcc, &newLock, nil +} + +// partialSuperfluidUndelegate starts undelegating a portion of a superfluid delegated position for the given lock. +// Undelegation is done instantly and the equivalent amount is sent to the module account +// where it is burnt. Note that this method does not include unbonding the lock +// itself. +// nolint: unused +func (k Keeper) partialSuperfluidUndelegate(ctx sdk.Context, sender string, lockID uint64, amountToUndelegate sdk.Coin) error { + intermediaryAcc, newLock, err := k.partialUndelegateCommon(ctx, sender, lockID, amountToUndelegate) + if err != nil { + return err + } + // Create a new synthetic lockup representing the unstaking side. + return k.createSyntheticLockup(ctx, newLock.ID, intermediaryAcc, unlockingStatus) +} + +// partialSuperfluidUndelegateToConcentratedPosition starts undelegating a portion of a superfluid delegated position for the given lock. It behaves similarly to partialSuperfluidUndelegate, +// however it does not create a new synthetic lockup representing the unstaking side. This is because after the time this function is called, we might +// 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) partialSuperfluidUndelegateToConcentratedPosition(ctx sdk.Context, sender string, gammLockID uint64, amountToUndelegate sdk.Coin) (types.SuperfluidIntermediaryAccount, *lockuptypes.PeriodLock, error) { + return k.partialUndelegateCommon(ctx, sender, gammLockID, amountToUndelegate) +} + // 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 { diff --git a/x/superfluid/keeper/stake_test.go b/x/superfluid/keeper/stake_test.go index 7ab49d396c4..708554687ce 100644 --- a/x/superfluid/keeper/stake_test.go +++ b/x/superfluid/keeper/stake_test.go @@ -1028,6 +1028,166 @@ func (suite *KeeperTestSuite) TestRefreshIntermediaryDelegationAmounts() { } } +func (suite *KeeperTestSuite) TestPartialSuperfluidUndelegateToConcentratedPosition() { + testCases := []struct { + name string + validatorStats []stakingtypes.BondStatus + superDelegations []superfluidDelegation + undelegateAmounts []sdk.Coin + superUnbondingLockIds []uint64 + expSuperUnbondingErr []bool + // expected amount of delegation to intermediary account + expInterDelegation []sdk.Dec + }{ + { + "with single validator and single superfluid delegation and single undelegation", + []stakingtypes.BondStatus{stakingtypes.Bonded}, + []superfluidDelegation{{0, 0, 0, 1000000}}, + []sdk.Coin{sdk.NewCoin("gamm/pool/1", sdk.NewInt(400000))}, + []uint64{1}, + []bool{false}, + []sdk.Dec{sdk.NewDec(6000000)}, + }, + { + "with single validator and additional superfluid delegations and single undelegation", + []stakingtypes.BondStatus{stakingtypes.Bonded}, + []superfluidDelegation{{0, 0, 0, 1000000}, {0, 0, 0, 1000000}}, + []sdk.Coin{sdk.NewCoin("gamm/pool/1", sdk.NewInt(900000))}, + []uint64{1}, + []bool{false}, + []sdk.Dec{sdk.NewDec(11000000)}, + }, + { + "with multiple validators and multiple superfluid delegations and multiple undelegations", + []stakingtypes.BondStatus{stakingtypes.Bonded, stakingtypes.Bonded}, + []superfluidDelegation{{0, 0, 0, 1000000}, {1, 1, 0, 1000000}}, + []sdk.Coin{sdk.NewCoin("gamm/pool/1", sdk.NewInt(600000)), sdk.NewCoin("gamm/pool/1", sdk.NewInt(400000))}, + []uint64{1, 2}, + []bool{false, false}, + []sdk.Dec{sdk.NewDec(4000000), sdk.NewDec(6000000)}, + }, + { + "add unbonding validator", + []stakingtypes.BondStatus{stakingtypes.Bonded, stakingtypes.Unbonding}, + []superfluidDelegation{{0, 0, 0, 1000000}, {1, 1, 0, 1000000}}, + []sdk.Coin{sdk.NewCoin("gamm/pool/1", sdk.NewInt(600000)), sdk.NewCoin("gamm/pool/1", sdk.NewInt(400000))}, + []uint64{1, 2}, + []bool{false, false}, + []sdk.Dec{sdk.NewDec(4000000), sdk.NewDec(6000000)}, + }, + { + "add unbonded validator", + []stakingtypes.BondStatus{stakingtypes.Bonded, stakingtypes.Unbonded}, + []superfluidDelegation{{0, 0, 0, 1000000}, {1, 1, 0, 1000000}}, + []sdk.Coin{sdk.NewCoin("gamm/pool/1", sdk.NewInt(600000)), sdk.NewCoin("gamm/pool/1", sdk.NewInt(400000))}, + []uint64{1, 2}, + []bool{false, false}, + []sdk.Dec{sdk.NewDec(4000000), sdk.NewDec(6000000)}, + }, + { + "undelegating not available lock id", + []stakingtypes.BondStatus{stakingtypes.Bonded}, + []superfluidDelegation{{0, 0, 0, 1000000}}, + []sdk.Coin{sdk.NewCoin("gamm/pool/1", sdk.NewInt(600000))}, + []uint64{2}, + []bool{true}, + []sdk.Dec{}, + }, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + suite.SetupTest() + + bondDenom := suite.App.StakingKeeper.GetParams(suite.Ctx).BondDenom + + // setup validators + valAddrs := suite.SetupValidators(tc.validatorStats) + + denoms, _ := suite.SetupGammPoolsAndSuperfluidAssets([]sdk.Dec{sdk.NewDec(20), sdk.NewDec(20)}) + + // setup superfluid delegations + _, intermediaryAccs, _ := suite.setupSuperfluidDelegations(valAddrs, tc.superDelegations, denoms) + suite.checkIntermediaryAccountDelegations(intermediaryAccs) + + suite.Greater(len(tc.superUnbondingLockIds), 0) + for index, lockId := range tc.superUnbondingLockIds { + // get intermediary account + accAddr := suite.App.SuperfluidKeeper.GetLockIdIntermediaryAccountConnection(suite.Ctx, lockId) + intermediaryAcc := suite.App.SuperfluidKeeper.GetIntermediaryAccount(suite.Ctx, accAddr) + valAddr := intermediaryAcc.ValAddr + + lock, err := suite.App.LockupKeeper.GetLockByID(suite.Ctx, lockId) + if tc.expSuperUnbondingErr[index] { + // manually set the lock to nil if we expect an error so we don't fail early + suite.Require().Error(err) + lock = &lockuptypes.PeriodLock{} + } else { + suite.Require().NoError(err) + } + + // get pre-superfluid delgations osmo supply and supplyWithOffset + presupply := suite.App.BankKeeper.GetSupply(suite.Ctx, bondDenom) + presupplyWithOffset := suite.App.BankKeeper.GetSupplyWithOffset(suite.Ctx, bondDenom) + + // superfluid undelegate + intermediaryAcc, newLock, err := suite.App.SuperfluidKeeper.PartialSuperfluidUndelegateToConcentratedPosition(suite.Ctx, lock.Owner, lockId, tc.undelegateAmounts[index]) + if tc.expSuperUnbondingErr[index] { + suite.Require().Error(err) + continue + } + suite.Require().NoError(err) + + // the new lock should be equal to the amount we partially undelegated + suite.Require().Equal(tc.undelegateAmounts[index].Amount.String(), newLock.Coins[0].Amount.String()) + + // ensure post-superfluid delegations osmo supplywithoffset is the same while supply is not + postsupply := suite.App.BankKeeper.GetSupply(suite.Ctx, bondDenom) + postsupplyWithOffset := suite.App.BankKeeper.GetSupplyWithOffset(suite.Ctx, bondDenom) + + suite.Require().Equal(presupply.Amount.Sub(tc.undelegateAmounts[index].Amount.Mul(sdk.NewInt(10))).String(), postsupply.Amount.String()) + suite.Require().Equal(presupplyWithOffset, postsupplyWithOffset) + + // check lockId and intermediary account connection is not deleted + addr := suite.App.SuperfluidKeeper.GetLockIdIntermediaryAccountConnection(suite.Ctx, lockId) + suite.Require().Equal(intermediaryAcc.GetAccAddress().String(), addr.String()) + + // check bonding synthetic lockup is not deleted + _, err = suite.App.LockupKeeper.GetSyntheticLockup(suite.Ctx, lockId, keeper.StakingSyntheticDenom(lock.Coins[0].Denom, valAddr)) + suite.Require().NoError(err) + + // check unbonding synthetic lockup creation + // since this is the concentrated liquidity path, no new synthetic lockup should be created + synthLock, err := suite.App.LockupKeeper.GetSyntheticLockup(suite.Ctx, lockId, keeper.UnstakingSyntheticDenom(lock.Coins[0].Denom, valAddr)) + suite.Require().Error(err) + suite.Require().Nil(synthLock) + synthLock, err = suite.App.LockupKeeper.GetSyntheticLockup(suite.Ctx, newLock.ID, keeper.UnstakingSyntheticDenom(lock.Coins[0].Denom, valAddr)) + suite.Require().Error(err) + suite.Require().Nil(synthLock) + } + + // check invariant is fine + reason, broken := keeper.AllInvariants(*suite.App.SuperfluidKeeper)(suite.Ctx) + suite.Require().False(broken, reason) + + // check remaining intermediary account delegation + for index, expDelegation := range tc.expInterDelegation { + acc := intermediaryAccs[index] + valAddr, err := sdk.ValAddressFromBech32(acc.ValAddr) + suite.Require().NoError(err) + delegation, found := suite.App.StakingKeeper.GetDelegation(suite.Ctx, acc.GetAccAddress(), valAddr) + if expDelegation.IsZero() { + suite.Require().False(found, "expected no delegation, found delegation w/ %d shares", delegation.Shares) + } else { + suite.Require().True(found) + suite.Require().Equal(expDelegation, delegation.Shares) + } + } + }) + } +} + // type superfluidRedelegation struct { // lockId uint64 // oldValIndex int64 diff --git a/x/superfluid/keeper/unpool.go b/x/superfluid/keeper/unpool.go index 0f3dfadc4b5..8c254eb1b3b 100644 --- a/x/superfluid/keeper/unpool.go +++ b/x/superfluid/keeper/unpool.go @@ -33,7 +33,7 @@ func (k Keeper) UnpoolAllowedPools(ctx sdk.Context, sender sdk.AccAddress, poolI // 2) Consistency check that lockID corresponds to sender, and contains correct LP shares. // These are expected to be true by the caller, but good to double check // TODO: Try to minimize dependence on lock here - lock, err := k.validateLockForUnpool(ctx, sender, poolId, lockId) + lock, err := k.validateGammLockForSuperfluidStaking(ctx, sender, poolId, lockId) if err != nil { return []uint64{}, err } @@ -100,7 +100,7 @@ func (k Keeper) checkUnpoolWhitelisted(ctx sdk.Context, poolId uint64) error { } // check if pool is whitelisted for unpool -func (k Keeper) validateLockForUnpool(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, lockId uint64) (*lockuptypes.PeriodLock, error) { +func (k Keeper) validateGammLockForSuperfluidStaking(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, lockId uint64) (*lockuptypes.PeriodLock, error) { lock, err := k.lk.GetLockByID(ctx, lockId) if err != nil { return lock, err diff --git a/x/superfluid/types/expected_keepers.go b/x/superfluid/types/expected_keepers.go index 24bba8eb954..394728f3c0d 100644 --- a/x/superfluid/types/expected_keepers.go +++ b/x/superfluid/types/expected_keepers.go @@ -28,6 +28,8 @@ type LockupKeeper interface { // TODO: Fix this in future code update BeginForceUnlock(ctx sdk.Context, lockID uint64, coins sdk.Coins) (uint64, error) ForceUnlock(ctx sdk.Context, lock lockuptypes.PeriodLock) error + PartialForceUnlock(ctx sdk.Context, lock lockuptypes.PeriodLock, coins sdk.Coins) error + SplitLock(ctx sdk.Context, lock lockuptypes.PeriodLock, coins sdk.Coins, forceUnlock bool) (lockuptypes.PeriodLock, error) CreateLock(ctx sdk.Context, owner sdk.AccAddress, coins sdk.Coins, duration time.Duration) (lockuptypes.PeriodLock, error)