From 72f0cfc009fb62c452ead332b6c512ece7a6c1e8 Mon Sep 17 00:00:00 2001 From: Adam Tucker Date: Mon, 19 Jun 2023 08:48:18 -0500 Subject: [PATCH] CL: migration functional test (#5560) Closes: #XXX ## What is the purpose of the change The following PR introduces a functional test that: - Creates every type of balancer position that can be created - Bonded superfluid - Unbonded superfluid (locked) - Unbonded superfluid (unlocking) - Vanilla lock (locked) - Vanilla lock (unlocking) - No lock - It then migrates each position, asserting invariants after each position is migrated - Finally, an overall invariant is run after all positions have been migrated, asserting that all funds are accounted for in some way The next PR subsequent to this will be adding randomization to the inputs in order to make the test non deterministic. ## Testing and Verifying Functional test above added ## Documentation and Release Note - [ ] Does this pull request introduce a new feature or user-facing behavior changes? - [ ] Changelog entry added to `Unreleased` section of `CHANGELOG.md`? Where is the change documented? - [ ] Specification (`x/{module}/README.md`) - [ ] Osmosis documentation site - [ ] Code comments? - [ ] N/A --- app/apptesting/test_suite.go | 8 + x/superfluid/keeper/migrate_test.go | 387 +++++++++++++++++++++++----- 2 files changed, 336 insertions(+), 59 deletions(-) diff --git a/app/apptesting/test_suite.go b/app/apptesting/test_suite.go index 24cc4905971..77d4b091d45 100644 --- a/app/apptesting/test_suite.go +++ b/app/apptesting/test_suite.go @@ -376,6 +376,14 @@ func (s *KeeperTestHelper) LockTokens(addr sdk.AccAddress, coins sdk.Coins, dura return msgResponse.ID } +// LockTokensNoFund locks tokens and returns a lockID. +func (s *KeeperTestHelper) LockTokensNoFund(addr sdk.AccAddress, coins sdk.Coins, duration time.Duration) (lockID uint64) { + msgServer := lockupkeeper.NewMsgServerImpl(s.App.LockupKeeper) + msgResponse, err := msgServer.LockTokens(sdk.WrapSDKContext(s.Ctx), lockuptypes.NewMsgLockTokens(addr, duration, coins)) + s.Require().NoError(err) + return msgResponse.ID +} + // BuildTx builds a transaction. func (s *KeeperTestHelper) BuildTx( txBuilder client.TxBuilder, diff --git a/x/superfluid/keeper/migrate_test.go b/x/superfluid/keeper/migrate_test.go index e890662388f..c01df49ef55 100644 --- a/x/superfluid/keeper/migrate_test.go +++ b/x/superfluid/keeper/migrate_test.go @@ -11,6 +11,7 @@ import ( stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/osmosis-labs/osmosis/osmomath" + "github.com/osmosis-labs/osmosis/v16/app/apptesting" cltypes "github.com/osmosis-labs/osmosis/v16/x/concentrated-liquidity/types" "github.com/osmosis-labs/osmosis/v16/x/gamm/pool-models/balancer" gammtypes "github.com/osmosis-labs/osmosis/v16/x/gamm/types" @@ -19,6 +20,18 @@ import ( "github.com/osmosis-labs/osmosis/v16/x/superfluid/types" ) +var ( + STAKE = "stake" + DefaultAmt0 = sdk.NewInt(1000000) + DefaultCoin0 = sdk.NewCoin(STAKE, DefaultAmt0) + USDC = "usdc" + DefaultAmt1 = sdk.NewInt(5000000000) + DefaultCoin1 = sdk.NewCoin(USDC, DefaultAmt1) + DefaultCoins = sdk.NewCoins(DefaultCoin0, DefaultCoin1) + DefaultLowerTick = int64(30545000) + DefaultUpperTick = int64(31500000) +) + // We test migrating in the following circumstances: // 1. Migrating lock that is not superfluid delegated, not unlocking. // 2. Migrating lock that is not superfluid delegated, unlocking. @@ -148,13 +161,12 @@ func (s *KeeperTestSuite) TestRouteLockedBalancerToConcentratedMigration() { s.Run(name, func() { s.SetupTest() s.Ctx = s.Ctx.WithBlockTime(defaultJoinTime) - ctx := s.Ctx superfluidKeeper := s.App.SuperfluidKeeper lockupKeeper := s.App.LockupKeeper stakingKeeper := s.App.StakingKeeper // We bundle all migration setup into a single function to avoid repeating the same code for each test case. - joinPoolAmt, balancerIntermediaryAcc, balancerLock, _, poolJoinAcc, balancerPooId, clPoolId, balancerPoolShareOut, valAddr := s.SetupMigrationTest(ctx, tc.superfluidDelegated, tc.superfluidUndelegating, tc.unlocking, tc.noLock, tc.percentOfSharesToMigrate) + joinPoolAmt, balancerIntermediaryAcc, balancerLock, _, poolJoinAcc, balancerPooId, clPoolId, balancerPoolShareOut, valAddr := s.SetupMigrationTest(s.Ctx, tc.superfluidDelegated, tc.superfluidUndelegating, tc.unlocking, tc.noLock, tc.percentOfSharesToMigrate) originalGammLockId := balancerLock.GetID() // Depending on the test case, we attempt to migrate a subset of the balancer LP tokens we originally created. @@ -166,20 +178,19 @@ func (s *KeeperTestSuite) TestRouteLockedBalancerToConcentratedMigration() { originalGammLockId = originalGammLockId + 1 } - balancerDelegationPre, _ := stakingKeeper.GetDelegation(ctx, balancerIntermediaryAcc.GetAccAddress(), valAddr) + balancerDelegationPre, _ := stakingKeeper.GetDelegation(s.Ctx, balancerIntermediaryAcc.GetAccAddress(), valAddr) // Run the migration logic. - positionId, amount0, amount1, liquidityMigrated, poolIdLeaving, poolIdEntering, concentratedLockId, err := superfluidKeeper.RouteLockedBalancerToConcentratedMigration(ctx, poolJoinAcc, originalGammLockId, coinsToMigrate, tc.minExitCoins) + positionId, amount0, amount1, liquidityMigrated, poolIdLeaving, poolIdEntering, concentratedLockId, err := superfluidKeeper.RouteLockedBalancerToConcentratedMigration(s.Ctx, poolJoinAcc, originalGammLockId, coinsToMigrate, tc.minExitCoins) if tc.expectedError != nil { s.Require().Error(err) s.Require().ErrorIs(err, tc.expectedError) return } s.Require().NoError(err) - s.AssertEventEmitted(ctx, gammtypes.TypeEvtPoolExited, 1) + s.AssertEventEmitted(s.Ctx, gammtypes.TypeEvtPoolExited, 1) s.ValidateMigrateResult( - ctx, positionId, balancerPooId, poolIdLeaving, clPoolId, poolIdEntering, tc.percentOfSharesToMigrate, liquidityMigrated, *balancerLock, @@ -194,43 +205,43 @@ func (s *KeeperTestSuite) TestRouteLockedBalancerToConcentratedMigration() { // If we migrated all the shares: // The intermediary account connection to the old gamm lock should be deleted. - addr := superfluidKeeper.GetLockIdIntermediaryAccountConnection(ctx, originalGammLockId) + addr := superfluidKeeper.GetLockIdIntermediaryAccountConnection(s.Ctx, originalGammLockId) s.Require().Equal(addr.String(), "") // The synthetic lockup should be deleted. - _, err = lockupKeeper.GetSyntheticLockup(ctx, originalGammLockId, keeper.StakingSyntheticDenom(balancerLock.Coins[0].Denom, valAddr.String())) + _, err = lockupKeeper.GetSyntheticLockup(s.Ctx, originalGammLockId, keeper.StakingSyntheticDenom(balancerLock.Coins[0].Denom, valAddr.String())) s.Require().Error(err) // The delegation from the balancer intermediary account holder should not exist. - delegation, found := stakingKeeper.GetDelegation(ctx, balancerIntermediaryAcc.GetAccAddress(), valAddr) + delegation, found := stakingKeeper.GetDelegation(s.Ctx, balancerIntermediaryAcc.GetAccAddress(), valAddr) s.Require().False(found, "expected no delegation, found delegation w/ %d shares", delegation.Shares) // Check that the original gamm lockup is deleted. - _, err := s.App.LockupKeeper.GetLockByID(ctx, originalGammLockId) + _, err := s.App.LockupKeeper.GetLockByID(s.Ctx, originalGammLockId) s.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) + addr := superfluidKeeper.GetLockIdIntermediaryAccountConnection(s.Ctx, originalGammLockId) s.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())) + _, err = lockupKeeper.GetSyntheticLockup(s.Ctx, originalGammLockId, keeper.StakingSyntheticDenom(balancerLock.Coins[0].Denom, valAddr.String())) s.Require().NoError(err) // The delegation from the balancer intermediary account holder should still exist. - delegation, found := stakingKeeper.GetDelegation(ctx, balancerIntermediaryAcc.GetAccAddress(), valAddr) + delegation, found := stakingKeeper.GetDelegation(s.Ctx, balancerIntermediaryAcc.GetAccAddress(), valAddr) s.Require().True(found, "expected delegation, found delegation no delegation") s.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 := s.App.LockupKeeper.GetLockByID(ctx, originalGammLockId) + lock, err := s.App.LockupKeeper.GetLockByID(s.Ctx, originalGammLockId) s.Require().NoError(err) s.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) + clIntermediaryAcc := superfluidKeeper.GetLockIdIntermediaryAccountConnection(s.Ctx, concentratedLockId) + delegation, found := stakingKeeper.GetDelegation(s.Ctx, clIntermediaryAcc, valAddr) s.Require().True(found, "expected delegation, found delegation no delegation") s.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()) } @@ -240,22 +251,22 @@ func (s *KeeperTestSuite) TestRouteLockedBalancerToConcentratedMigration() { // Regardless oh how many shares we migrated: // The intermediary account connection to the old gamm lock should be deleted. - addr := superfluidKeeper.GetLockIdIntermediaryAccountConnection(ctx, originalGammLockId) + addr := superfluidKeeper.GetLockIdIntermediaryAccountConnection(s.Ctx, originalGammLockId) s.Require().Equal(addr.String(), "") // The synthetic lockup should be deleted. - _, err = lockupKeeper.GetSyntheticLockup(ctx, originalGammLockId, keeper.StakingSyntheticDenom(balancerLock.Coins[0].Denom, valAddr.String())) + _, err = lockupKeeper.GetSyntheticLockup(s.Ctx, originalGammLockId, keeper.StakingSyntheticDenom(balancerLock.Coins[0].Denom, valAddr.String())) s.Require().Error(err) // The delegation from the intermediary account holder does not exist. - delegation, found := stakingKeeper.GetDelegation(ctx, balancerIntermediaryAcc.GetAccAddress(), valAddr) + delegation, found := stakingKeeper.GetDelegation(s.Ctx, balancerIntermediaryAcc.GetAccAddress(), valAddr) s.Require().False(found, "expected no delegation, found delegation w/ %d shares", delegation.Shares) } // Run slashing logic if the test case involves locks and check if the new and old locks are slashed. if !tc.noLock { slashExpected := tc.superfluidDelegated || tc.superfluidUndelegating - s.SlashAndValidateResult(ctx, originalGammLockId, concentratedLockId, poolIdEntering, tc.percentOfSharesToMigrate, valAddr, *balancerLock, slashExpected) + s.SlashAndValidateResult(s.Ctx, originalGammLockId, concentratedLockId, poolIdEntering, tc.percentOfSharesToMigrate, valAddr, *balancerLock, slashExpected) } }) } @@ -302,13 +313,12 @@ func (s *KeeperTestSuite) TestMigrateSuperfluidBondedBalancerToConcentrated() { s.Run(name, func() { s.SetupTest() s.Ctx = s.Ctx.WithBlockTime(defaultJoinTime) - ctx := s.Ctx superfluidKeeper := s.App.SuperfluidKeeper lockupKeeper := s.App.LockupKeeper stakingKeeper := s.App.StakingKeeper // We bundle all migration setup into a single function to avoid repeating the same code for each test case. - joinPoolAmt, balancerIntermediaryAcc, balancerLock, _, poolJoinAcc, balancerPooId, clPoolId, balancerPoolShareOut, valAddr := s.SetupMigrationTest(ctx, true, false, false, false, tc.percentOfSharesToMigrate) + joinPoolAmt, balancerIntermediaryAcc, balancerLock, _, poolJoinAcc, balancerPooId, clPoolId, balancerPoolShareOut, valAddr := s.SetupMigrationTest(s.Ctx, true, false, false, false, tc.percentOfSharesToMigrate) originalGammLockId := balancerLock.GetID() // Depending on the test case, we attempt to migrate a subset of the balancer LP tokens we originally created. @@ -317,7 +327,7 @@ func (s *KeeperTestSuite) TestMigrateSuperfluidBondedBalancerToConcentrated() { // RouteMigration is called via the migration message router and is always run prior to the migration itself. // We use it here just to retrieve the synthetic lock before the migration. - synthLockBeforeMigration, migrationType, err := superfluidKeeper.RouteMigration(ctx, poolJoinAcc, originalGammLockId, coinsToMigrate) + synthLockBeforeMigration, migrationType, err := superfluidKeeper.RouteMigration(s.Ctx, poolJoinAcc, originalGammLockId, coinsToMigrate) s.Require().NoError(err) s.Require().Equal(migrationType, keeper.SuperfluidBonded) @@ -334,20 +344,19 @@ func (s *KeeperTestSuite) TestMigrateSuperfluidBondedBalancerToConcentrated() { originalGammLockId = originalGammLockId + 1 } - balancerDelegationPre, _ := stakingKeeper.GetDelegation(ctx, balancerIntermediaryAcc.GetAccAddress(), valAddr) + balancerDelegationPre, _ := stakingKeeper.GetDelegation(s.Ctx, balancerIntermediaryAcc.GetAccAddress(), valAddr) // System under test. - positionId, amount0, amount1, liquidityMigrated, concentratedLockId, poolIdLeaving, poolIdEntering, err := superfluidKeeper.MigrateSuperfluidBondedBalancerToConcentrated(ctx, poolJoinAcc, originalGammLockId, coinsToMigrate, synthLockBeforeMigration.SynthDenom, tc.tokenOutMins) + positionId, amount0, amount1, liquidityMigrated, concentratedLockId, poolIdLeaving, poolIdEntering, err := superfluidKeeper.MigrateSuperfluidBondedBalancerToConcentrated(s.Ctx, poolJoinAcc, originalGammLockId, coinsToMigrate, synthLockBeforeMigration.SynthDenom, tc.tokenOutMins) if tc.expectedError != nil { s.Require().Error(err) s.Require().ErrorContains(err, tc.expectedError.Error()) return } s.Require().NoError(err) - s.AssertEventEmitted(ctx, gammtypes.TypeEvtPoolExited, 1) + s.AssertEventEmitted(s.Ctx, gammtypes.TypeEvtPoolExited, 1) s.ValidateMigrateResult( - ctx, positionId, balancerPooId, poolIdLeaving, clPoolId, poolIdEntering, tc.percentOfSharesToMigrate, liquidityMigrated, *balancerLock, @@ -360,66 +369,66 @@ func (s *KeeperTestSuite) TestMigrateSuperfluidBondedBalancerToConcentrated() { // If we migrated all the shares: // The intermediary account connection to the old gamm lock should be deleted. - addr := superfluidKeeper.GetLockIdIntermediaryAccountConnection(ctx, originalGammLockId) + addr := superfluidKeeper.GetLockIdIntermediaryAccountConnection(s.Ctx, originalGammLockId) s.Require().Equal(addr.String(), "") // The synthetic lockup should be deleted. - _, err = lockupKeeper.GetSyntheticLockup(ctx, originalGammLockId, keeper.StakingSyntheticDenom(balancerLock.Coins[0].Denom, valAddr.String())) + _, err = lockupKeeper.GetSyntheticLockup(s.Ctx, originalGammLockId, keeper.StakingSyntheticDenom(balancerLock.Coins[0].Denom, valAddr.String())) s.Require().Error(err) // The delegation from the intermediary account holder should not exist. - delegation, found := stakingKeeper.GetDelegation(ctx, balancerIntermediaryAcc.GetAccAddress(), valAddr) + delegation, found := stakingKeeper.GetDelegation(s.Ctx, balancerIntermediaryAcc.GetAccAddress(), valAddr) s.Require().False(found, "expected no delegation, found delegation w/ %d shares", delegation.Shares) // Check that the original gamm lockup is deleted. - _, err := s.App.LockupKeeper.GetLockByID(ctx, originalGammLockId) + _, err := s.App.LockupKeeper.GetLockByID(s.Ctx, originalGammLockId) s.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) + addr := superfluidKeeper.GetLockIdIntermediaryAccountConnection(s.Ctx, originalGammLockId) s.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())) + gammSynthLock, err := lockupKeeper.GetSyntheticLockup(s.Ctx, originalGammLockId, keeper.StakingSyntheticDenom(balancerLock.Coins[0].Denom, valAddr.String())) s.Require().NoError(err) s.Require().Equal(originalGammLockId, gammSynthLock.UnderlyingLockId) // The delegation from the intermediary account holder should still exist. - _, found := stakingKeeper.GetDelegation(ctx, balancerIntermediaryAcc.GetAccAddress(), valAddr) + _, found := stakingKeeper.GetDelegation(s.Ctx, balancerIntermediaryAcc.GetAccAddress(), valAddr) s.Require().True(found, "expected delegation, found delegation no delegation") // Check what is remaining in the original gamm lock. - lock, err := s.App.LockupKeeper.GetLockByID(ctx, originalGammLockId) + lock, err := s.App.LockupKeeper.GetLockByID(s.Ctx, originalGammLockId) s.Require().NoError(err) s.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) + clIntermediaryAcc := superfluidKeeper.GetLockIdIntermediaryAccountConnection(s.Ctx, concentratedLockId) + delegation, found := stakingKeeper.GetDelegation(s.Ctx, clIntermediaryAcc, valAddr) s.Require().True(found, "expected delegation, found delegation no delegation") s.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) + newConcentratedIntermediaryAccount := superfluidKeeper.GetLockIdIntermediaryAccountConnection(s.Ctx, concentratedLockId) s.Require().NotEqual(newConcentratedIntermediaryAccount.String(), "") // Check newly created concentrated lock. - concentratedLock, err := lockupKeeper.GetLockByID(ctx, concentratedLockId) + concentratedLock, err := lockupKeeper.GetLockByID(s.Ctx, concentratedLockId) s.Require().NoError(err) s.Require().Equal(liquidityMigrated.TruncateInt().String(), concentratedLock.Coins[0].Amount.String(), "expected %s shares, found %s shares", coinsToMigrate.Amount.String(), concentratedLock.Coins[0].Amount.String()) s.Require().Equal(balancerLock.Duration, concentratedLock.Duration) s.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())) + clSynthLock, err := lockupKeeper.GetSyntheticLockup(s.Ctx, concentratedLockId, keeper.StakingSyntheticDenom(concentratedLock.Coins[0].Denom, valAddr.String())) s.Require().NoError(err) s.Require().Equal(concentratedLockId, clSynthLock.UnderlyingLockId) // Run slashing logic and check if the new and old locks are slashed. - s.SlashAndValidateResult(ctx, originalGammLockId, concentratedLockId, clPoolId, tc.percentOfSharesToMigrate, valAddr, *balancerLock, true) + s.SlashAndValidateResult(s.Ctx, originalGammLockId, concentratedLockId, clPoolId, tc.percentOfSharesToMigrate, valAddr, *balancerLock, true) }) } } @@ -502,7 +511,6 @@ func (s *KeeperTestSuite) TestMigrateSuperfluidUnbondingBalancerToConcentrated() s.AssertEventEmitted(s.Ctx, gammtypes.TypeEvtPoolExited, 1) s.ValidateMigrateResult( - s.Ctx, positionId, balancerPooId, poolIdLeaving, clPoolId, poolIdEntering, tc.percentOfSharesToMigrate, liquidityMigrated, *balancerLock, @@ -590,12 +598,11 @@ func (s *KeeperTestSuite) TestMigrateNonSuperfluidLockBalancerToConcentrated() { s.Run(name, func() { s.SetupTest() s.Ctx = s.Ctx.WithBlockTime(defaultJoinTime) - ctx := s.Ctx superfluidKeeper := s.App.SuperfluidKeeper lockupKeeper := s.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 := s.SetupMigrationTest(ctx, false, false, tc.unlocking, false, tc.percentOfSharesToMigrate) + joinPoolAmt, _, balancerLock, _, poolJoinAcc, balancerPooId, clPoolId, balancerPoolShareOut, valAddr := s.SetupMigrationTest(s.Ctx, false, false, tc.unlocking, false, tc.percentOfSharesToMigrate) originalGammLockId := balancerLock.GetID() // Depending on the test case, we attempt to migrate a subset of the balancer LP tokens we originally created. @@ -603,23 +610,22 @@ func (s *KeeperTestSuite) TestMigrateNonSuperfluidLockBalancerToConcentrated() { coinsToMigrate.Amount = coinsToMigrate.Amount.ToDec().Mul(tc.percentOfSharesToMigrate).RoundInt() // RouteMigration is called via the migration message router and is always run prior to the migration itself - synthLockBeforeMigration, migrationType, err := superfluidKeeper.RouteMigration(ctx, poolJoinAcc, originalGammLockId, coinsToMigrate) + synthLockBeforeMigration, migrationType, err := superfluidKeeper.RouteMigration(s.Ctx, poolJoinAcc, originalGammLockId, coinsToMigrate) s.Require().NoError(err) s.Require().Equal((lockuptypes.SyntheticLock{}), synthLockBeforeMigration) s.Require().Equal(migrationType, keeper.NonSuperfluid) // System under test. - positionId, amount0, amount1, liquidityMigrated, concentratedLockId, poolIdLeaving, poolIdEntering, err := superfluidKeeper.MigrateNonSuperfluidLockBalancerToConcentrated(ctx, poolJoinAcc, originalGammLockId, coinsToMigrate, tc.tokenOutMins) + positionId, amount0, amount1, liquidityMigrated, concentratedLockId, poolIdLeaving, poolIdEntering, err := superfluidKeeper.MigrateNonSuperfluidLockBalancerToConcentrated(s.Ctx, poolJoinAcc, originalGammLockId, coinsToMigrate, tc.tokenOutMins) if tc.expectedError != nil { s.Require().Error(err) s.Require().ErrorContains(err, tc.expectedError.Error()) return } s.Require().NoError(err) - s.AssertEventEmitted(ctx, gammtypes.TypeEvtPoolExited, 1) + s.AssertEventEmitted(s.Ctx, gammtypes.TypeEvtPoolExited, 1) s.ValidateMigrateResult( - ctx, positionId, balancerPooId, poolIdLeaving, clPoolId, poolIdEntering, tc.percentOfSharesToMigrate, liquidityMigrated, *balancerLock, @@ -629,14 +635,14 @@ func (s *KeeperTestSuite) TestMigrateNonSuperfluidLockBalancerToConcentrated() { ) // Check newly created concentrated lock. - concentratedLock, err := lockupKeeper.GetLockByID(ctx, concentratedLockId) + concentratedLock, err := lockupKeeper.GetLockByID(s.Ctx, concentratedLockId) s.Require().NoError(err) s.Require().Equal(liquidityMigrated.TruncateInt().String(), concentratedLock.Coins[0].Amount.String(), "expected %s shares, found %s shares", coinsToMigrate.Amount.String(), concentratedLock.Coins[0].Amount.String()) s.Require().Equal(balancerLock.Duration, concentratedLock.Duration) s.Require().Equal(s.Ctx.BlockTime().Add(balancerLock.Duration), concentratedLock.EndTime) // Run slashing logic and check if the new and old locks are not slashed. - s.SlashAndValidateResult(ctx, originalGammLockId, concentratedLockId, clPoolId, tc.percentOfSharesToMigrate, valAddr, *balancerLock, false) + s.SlashAndValidateResult(s.Ctx, originalGammLockId, concentratedLockId, clPoolId, tc.percentOfSharesToMigrate, valAddr, *balancerLock, false) }) } } @@ -675,12 +681,11 @@ func (s *KeeperTestSuite) TestMigrateUnlockedPositionFromBalancerToConcentrated( s.Run(name, func() { s.SetupTest() s.Ctx = s.Ctx.WithBlockTime(defaultJoinTime) - ctx := s.Ctx superfluidKeeper := s.App.SuperfluidKeeper gammKeeper := s.App.GAMMKeeper // 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, _ := s.SetupMigrationTest(ctx, false, false, false, true, tc.percentOfSharesToMigrate) + joinPoolAmt, _, balancerLock, _, poolJoinAcc, balancerPooId, clPoolId, balancerPoolShareOut, _ := s.SetupMigrationTest(s.Ctx, false, false, false, true, tc.percentOfSharesToMigrate) s.Require().Equal(uint64(0), balancerLock.GetID()) // Depending on the test case, we attempt to migrate a subset of the balancer LP tokens we originally created. @@ -688,22 +693,21 @@ func (s *KeeperTestSuite) TestMigrateUnlockedPositionFromBalancerToConcentrated( coinsToMigrate.Amount = coinsToMigrate.Amount.ToDec().Mul(tc.percentOfSharesToMigrate).RoundInt() // RouteMigration is called via the migration message router and is always run prior to the migration itself - synthLockBeforeMigration, migrationType, err := superfluidKeeper.RouteMigration(ctx, poolJoinAcc, 0, coinsToMigrate) + synthLockBeforeMigration, migrationType, err := superfluidKeeper.RouteMigration(s.Ctx, poolJoinAcc, 0, coinsToMigrate) s.Require().NoError(err) s.Require().Equal((lockuptypes.SyntheticLock{}), synthLockBeforeMigration) s.Require().Equal(migrationType, keeper.Unlocked) // System under test. - positionId, amount0, amount1, liquidityMigrated, poolIdLeaving, poolIdEntering, err := gammKeeper.MigrateUnlockedPositionFromBalancerToConcentrated(ctx, poolJoinAcc, coinsToMigrate, tc.tokenOutMins) + positionId, amount0, amount1, liquidityMigrated, poolIdLeaving, poolIdEntering, err := gammKeeper.MigrateUnlockedPositionFromBalancerToConcentrated(s.Ctx, poolJoinAcc, coinsToMigrate, tc.tokenOutMins) if tc.expectedError != nil { s.Require().ErrorContains(err, tc.expectedError.Error()) return } s.Require().NoError(err) - s.AssertEventEmitted(ctx, gammtypes.TypeEvtPoolExited, 1) + s.AssertEventEmitted(s.Ctx, gammtypes.TypeEvtPoolExited, 1) s.ValidateMigrateResult( - ctx, positionId, balancerPooId, poolIdLeaving, clPoolId, poolIdEntering, tc.percentOfSharesToMigrate, liquidityMigrated, *balancerLock, @@ -1177,8 +1181,9 @@ func (s *KeeperTestSuite) SlashAndValidateResult(ctx sdk.Context, gammLockId, co } } +// TODO add user balace pre swap and then add to result + func (s *KeeperTestSuite) ValidateMigrateResult( - ctx sdk.Context, positionId, balancerPooId, poolIdLeaving, clPoolId, poolIdEntering uint64, percentOfSharesToMigrate, liquidityMigrated sdk.Dec, balancerLock lockuptypes.PeriodLock, @@ -1187,10 +1192,10 @@ func (s *KeeperTestSuite) ValidateMigrateResult( amount0, amount1 sdk.Int, ) { // Check that the concentrated liquidity and join time match what we expect - position, err := s.App.ConcentratedLiquidityKeeper.GetPosition(ctx, positionId) + position, err := s.App.ConcentratedLiquidityKeeper.GetPosition(s.Ctx, positionId) s.Require().NoError(err) s.Require().Equal(liquidityMigrated, position.Liquidity) - s.Require().Equal(ctx.BlockTime(), position.JoinTime) + s.Require().Equal(s.Ctx.BlockTime(), position.JoinTime) // Expect the poolIdLeaving to be the balancer pool id // Expect the poolIdEntering to be the concentrated liquidity pool id @@ -1206,3 +1211,267 @@ func (s *KeeperTestSuite) ValidateMigrateResult( s.Require().Equal(0, defaultErrorTolerance.Compare(joinPoolAmt.AmountOf(defaultPoolAssets[0].Token.Denom).ToDec().Mul(percentOfSharesToMigrate).RoundInt(), amount0)) s.Require().Equal(0, defaultErrorTolerance.Compare(joinPoolAmt.AmountOf(defaultPoolAssets[1].Token.Denom).ToDec().Mul(percentOfSharesToMigrate).RoundInt(), amount1)) } + +type Positions struct { + numAccounts int + numBondedSuperfluid int + numUnbondingSuperfluidLocked int + numUnbondingSuperfluidUnlocking int + numVanillaLockLocked int + numVanillaLockUnlocking int + numNoLock int +} + +// Populate a Balancer pool with multiple positions +// Positions should be spread between: +// - Bonded superfluid +// - Unbonded superfluid (locked) +// - Unbonded superfluid (unlocking) +// - Vanilla lock (locked) +// - Vanilla lock (unlocking) +// - No lock +// +// After each position is created, we track the position info in a slice of slices. +// This allows us to easily iterate through all positions and test migration for each position later. +func (s *KeeperTestSuite) buildPositions(balancerPoolId uint64, positions Positions) [][]positionInfo { + balancerPoolShareDenom := fmt.Sprintf("gamm/pool/%d", balancerPoolId) + unbondingDuration := s.App.StakingKeeper.GetParams(s.Ctx).UnbondingTime + + positionInfos := make([][]positionInfo, 6) + + for i := 0; i < positions.numBondedSuperfluid; i++ { + posInfoInternal := s.createBalancerPosition(s.TestAccs[i+1], balancerPoolId, unbondingDuration, balancerPoolShareDenom, DefaultCoins, positions.numAccounts-i, true) + positionInfos[0] = append(positionInfos[0], posInfoInternal) + } + + for i := 0; i < positions.numUnbondingSuperfluidLocked; i++ { + posInfoInternal := s.createBalancerPosition(s.TestAccs[i+1], balancerPoolId, unbondingDuration+time.Nanosecond, balancerPoolShareDenom, DefaultCoins, positions.numAccounts-i, true) + err := s.App.SuperfluidKeeper.SuperfluidUndelegate(s.Ctx, s.TestAccs[i+1].String(), posInfoInternal.lockId) + s.Require().NoError(err) + positionInfos[1] = append(positionInfos[1], posInfoInternal) + } + + for i := 0; i < positions.numUnbondingSuperfluidUnlocking; i++ { + posInfoInternal := s.createBalancerPosition(s.TestAccs[i+1], balancerPoolId, unbondingDuration+time.Nanosecond*2, balancerPoolShareDenom, DefaultCoins, positions.numAccounts-i, true) + err := s.App.SuperfluidKeeper.SuperfluidUndelegate(s.Ctx, s.TestAccs[i+1].String(), posInfoInternal.lockId) + s.Require().NoError(err) + err = s.App.SuperfluidKeeper.SuperfluidUnbondLock(s.Ctx, posInfoInternal.lockId, s.TestAccs[i+1].String()) + s.Require().NoError(err) + positionInfos[2] = append(positionInfos[2], posInfoInternal) + } + + for i := 0; i < positions.numVanillaLockLocked; i++ { + posInfoInternal := s.createBalancerPosition(s.TestAccs[i+1], balancerPoolId, unbondingDuration+time.Nanosecond*3, balancerPoolShareDenom, DefaultCoins, positions.numAccounts-i, false) + positionInfos[3] = append(positionInfos[3], posInfoInternal) + } + + for i := 0; i < positions.numVanillaLockUnlocking; i++ { + posInfoInternal := s.createBalancerPosition(s.TestAccs[i+1], balancerPoolId, unbondingDuration+time.Nanosecond*4, balancerPoolShareDenom, DefaultCoins, positions.numAccounts-i, false) + lock, err := s.App.LockupKeeper.GetLockByID(s.Ctx, posInfoInternal.lockId) + s.Require().NoError(err) + _, err = s.App.LockupKeeper.BeginUnlock(s.Ctx, posInfoInternal.lockId, lock.Coins) + s.Require().NoError(err) + positionInfos[4] = append(positionInfos[4], posInfoInternal) + } + + for i := 0; i < positions.numNoLock; i++ { + posInfoInternal := s.createBalancerPosition(s.TestAccs[i+1], balancerPoolId, time.Duration(0), balancerPoolShareDenom, DefaultCoins, positions.numAccounts-i, false) + positionInfos[5] = append(positionInfos[5], posInfoInternal) + } + return positionInfos +} + +type positionInfo struct { + joinPoolCoins sdk.Coins + coin sdk.Coin + shares sdk.Int + lockId uint64 +} + +// Test that during a migration, all coins are either migrated or sent back to user. +// Repeats this test for many different balancer positions. +func (s *KeeperTestSuite) TestFunctional_VaryingPositions_Migrations() { + positions := Positions{ + numAccounts: 7, + numBondedSuperfluid: 6, + numUnbondingSuperfluidLocked: 5, + numUnbondingSuperfluidUnlocking: 4, + numVanillaLockLocked: 3, + numVanillaLockUnlocking: 2, + numNoLock: 1, + } + s.SetupTest() + s.TestAccs = apptesting.CreateRandomAccounts(positions.numAccounts) + + // Fund all accounts with default coins. Keeps track of total funds to be used for position creation (does not include first account in tracker, since this account is only used for pool creation). + // After all methods are tested, we should have total accountability of these funds. + totalFundsForPositionCreation := sdk.NewCoins() + for i := 0; i < positions.numAccounts; i++ { + s.FundAcc(s.TestAccs[i], DefaultCoins) + if i != 0 { + totalFundsForPositionCreation = totalFundsForPositionCreation.Add(DefaultCoins...) + } + } + + // Create a balancer pool (includes staking denom to be superfluid compatible). + balancerPoolId := s.PrepareBalancerPoolWithCoins(DefaultCoins...) + positionInfos := s.buildPositions(balancerPoolId, positions) + + // Some funds might not have been completely used when creating the above positions. + // We note them here and use them when tracking invariants at the very end. + unusedPositionCreationFunds := s.calculateUnusedPositionCreationFunds(positions.numAccounts, positions.numNoLock, DefaultCoin0.Denom, DefaultCoin1.Denom) + + // Create CL pool + clPool := s.PrepareConcentratedPoolWithCoins(DefaultCoin0.Denom, DefaultCoin1.Denom) + clPoolId := clPool.GetId() + + // Match the spot price of the CL pool to the balancer pool + balancerPool, err := s.App.GAMMKeeper.GetCFMMPool(s.Ctx, balancerPoolId) + s.Require().NoError(err) + balancerSpotPrice, err := balancerPool.SpotPrice(s.Ctx, DefaultCoin1.Denom, DefaultCoin0.Denom) + s.Require().NoError(err) + // TODO: investigate why we need a full range position to add as superfluid asset. + coin0 := sdk.NewCoin(DefaultCoin0.Denom, sdk.NewInt(100000000)) + coin1 := sdk.NewCoin(DefaultCoin1.Denom, sdk.NewDec(100000000).Mul(balancerSpotPrice).TruncateInt()) + s.CreateFullRangePosition(clPool, sdk.NewCoins(coin0, coin1)) + + // Add a gov sanctioned link between the balancer and concentrated liquidity pool. + migrationRecord := gammtypes.MigrationRecords{BalancerToConcentratedPoolLinks: []gammtypes.BalancerToConcentratedPoolLink{ + {BalancerPoolId: balancerPoolId, ClPoolId: clPoolId}, + }} + s.App.GAMMKeeper.OverwriteMigrationRecordsAndRedirectDistrRecords(s.Ctx, migrationRecord) + + // Register the CL denom as superfluid. + clPoolDenom := cltypes.GetConcentratedLockupDenomFromPoolId(clPoolId) + err = s.App.SuperfluidKeeper.AddNewSuperfluidAsset(s.Ctx, types.SuperfluidAsset{ + Denom: clPoolDenom, + AssetType: types.SuperfluidAssetTypeConcentratedShare, + }) + s.Require().NoError(err) + + // All the following values will be tracked as we migrate each position. We will check them against the invariants at the end. + totalAmount0Migrated := sdk.ZeroInt() + totalAmount1Migrated := sdk.ZeroInt() + totalSentBackToOwnersAmount0 := sdk.ZeroInt() + totalSentBackToOwnersAmount1 := sdk.ZeroInt() + totalBalancerPoolFundsLeftBehindAmount0 := sdk.ZeroInt() + totalBalancerPoolFundsLeftBehindAmount1 := sdk.ZeroInt() + amount0AccountFor := sdk.ZeroInt() + amount1AccountFor := sdk.ZeroInt() + + // Migrate all the positions. + // We will check certain invariants after each individual migration. + for _, positionInfo := range positionInfos { + for i, posInfo := range positionInfo { + balancerPool, err = s.App.GAMMKeeper.GetCFMMPool(s.Ctx, balancerPoolId) + s.Require().NoError(err) + + // Note owner and balancer pool balances before migration. + preClaimOwnerBalance := s.App.BankKeeper.GetAllBalances(s.Ctx, s.TestAccs[i+1]) + preClaimBalancerPoolBalance := balancerPool.GetTotalPoolLiquidity(s.Ctx) + + // Run the migration. + _, amount0, amount1, _, _, _, _, err := s.App.SuperfluidKeeper.RouteLockedBalancerToConcentratedMigration(s.Ctx, s.TestAccs[i+1], posInfo.lockId, posInfo.coin, sdk.Coins{}) + s.Require().NoError(err) + + // Note how much of amount0 and amount1 was actually created in the CL pool from the migration. + clJoinPoolAmt := sdk.NewCoins(sdk.NewCoin(clPool.GetToken0(), amount0), sdk.NewCoin(clPool.GetToken1(), amount1)) + + // Note owner and balancer pool balances after migration. + balancerPool, err = s.App.GAMMKeeper.GetCFMMPool(s.Ctx, balancerPoolId) + s.Require().NoError(err) + postClaimOwnerBalance := s.App.BankKeeper.GetAllBalances(s.Ctx, s.TestAccs[i+1]) + postClaimBalancerPoolBalance := balancerPool.GetTotalPoolLiquidity(s.Ctx) + + // Check that the diff between the initial balancer position and the new CL position is equal to the amount that was left behind in the balancer pool and the amount that was sent back to the owner. + for i := range posInfo.joinPoolCoins { + balancerToClBalanceDelta := posInfo.joinPoolCoins[i].Amount.Sub(clJoinPoolAmt.AmountOf(posInfo.joinPoolCoins[i].Denom)) + userBalanceDelta := postClaimOwnerBalance.AmountOf(posInfo.joinPoolCoins[i].Denom).Sub(preClaimOwnerBalance.AmountOf(posInfo.joinPoolCoins[i].Denom)) + balancerPoolDelta := posInfo.joinPoolCoins[i].Amount.Sub(preClaimBalancerPoolBalance.AmountOf(posInfo.joinPoolCoins[i].Denom).Sub(postClaimBalancerPoolBalance.AmountOf(posInfo.joinPoolCoins[i].Denom))) + if i == 0 { + totalBalancerPoolFundsLeftBehindAmount0 = totalBalancerPoolFundsLeftBehindAmount0.Add(balancerPoolDelta) + } else { + totalBalancerPoolFundsLeftBehindAmount1 = totalBalancerPoolFundsLeftBehindAmount1.Add(balancerPoolDelta) + } + s.Require().Equal(balancerToClBalanceDelta.String(), userBalanceDelta.Add(balancerPoolDelta).String()) + } + + // Add to the total amounts that were migrated and sent back to the owners. + totalAmount0Migrated = totalAmount0Migrated.Add(amount0) + totalAmount1Migrated = totalAmount1Migrated.Add(amount1) + totalSentBackToOwnersAmount0 = totalSentBackToOwnersAmount0.Add(postClaimOwnerBalance.AmountOf(DefaultCoin0.Denom).Sub(preClaimOwnerBalance.AmountOf(DefaultCoin0.Denom))) + totalSentBackToOwnersAmount1 = totalSentBackToOwnersAmount1.Add(postClaimOwnerBalance.AmountOf(DefaultCoin1.Denom).Sub(preClaimOwnerBalance.AmountOf(DefaultCoin1.Denom))) + } + } + + // Check that we have account for all the funds that were used to create the positions. + amount0AccountFor = totalAmount0Migrated.Add(totalSentBackToOwnersAmount0).Add(totalBalancerPoolFundsLeftBehindAmount0).Add(unusedPositionCreationFunds.AmountOf(DefaultCoin0.Denom)) + amount1AccountFor = totalAmount1Migrated.Add(totalSentBackToOwnersAmount1).Add(totalBalancerPoolFundsLeftBehindAmount1).Add(unusedPositionCreationFunds.AmountOf(DefaultCoin1.Denom)) + s.Require().Equal(totalFundsForPositionCreation.AmountOf(DefaultCoin0.Denom), amount0AccountFor) + s.Require().Equal(totalFundsForPositionCreation.AmountOf(DefaultCoin1.Denom), amount1AccountFor) +} + +// createBalancerPosition creates a position in a Balancer pool for a given account with optional superfluid delegation. +// +// The function joins the Balancer pool with the specified account, using coins with varying amounts based on the divisor `i`. +// It retrieves the number of shares received (`sharesOut`) from the `JoinSwapExactAmountIn` function. +// +// If `unbondingDuration` is non-zero, the function locks the obtained shares using `LockTokensNoFund` and assigns the lock ID to `lockId`. +// +// If `superfluidDelegate` is true, the function delegates the obtained shares to the default val module using `SuperfluidDelegateToDefaultVal`. +// +// The function returns a `positionInfo` struct with the created position's information. +func (s *KeeperTestSuite) createBalancerPosition(acc sdk.AccAddress, balancerPoolId uint64, unbondingDuration time.Duration, balancerPoolShareDenom string, coins sdk.Coins, i int, superfluidDelegate bool) positionInfo { + coin0, coin1 := DefaultCoin0, DefaultCoin1 + coin0.Amount, coin1.Amount = coin0.Amount.QuoRaw(int64(i)), coin1.Amount.QuoRaw(int64(i)) + joinPoolCoins := sdk.NewCoins(coin0, coin1) + + sharesOut, err := s.App.GAMMKeeper.JoinSwapExactAmountIn(s.Ctx, acc, balancerPoolId, joinPoolCoins, sdk.OneInt()) + s.Require().NoError(err) + shareCoins := sdk.NewCoins(sdk.NewCoin(balancerPoolShareDenom, sharesOut)) + + lockId := uint64(0) + // Lock tokens if a duration is specified. + if unbondingDuration != 0 { + lockId = s.LockTokensNoFund(acc, shareCoins, unbondingDuration) + } + + // Superfluid delegate if specified. + if superfluidDelegate { + err = s.SuperfluidDelegateToDefaultVal(acc, balancerPoolId, lockId) + s.Require().NoError(err) + } + + sharesOutCoin := sdk.NewCoin(balancerPoolShareDenom, sharesOut) + + posInfoInternal := positionInfo{ + joinPoolCoins: joinPoolCoins, + coin: sharesOutCoin, + shares: sharesOut, + lockId: lockId, + } + return posInfoInternal +} + +// calculateUnusedPositionCreationFunds calculates the unused position creation funds for a given number of accounts and positions without locks. +// +// The function iterates over the accounts starting from index 1 and retrieves their balances for the specified coin denominations. +// It aggregates the unused position creation funds by adding the coin amounts to the `unusedPositionCreationFunds` variable. +// +// Parameters: +// - numAccounts: The total number of accounts. +// - numNoLock: The number of positions without locks. +// - coin0Denom: The denomination of the first coin. +// - coin1Denom: The denomination of the second coin. +// +// Returns: +// - sdk.Coins: The total unused position creation funds as a `sdk.Coins` object. +func (s *KeeperTestSuite) calculateUnusedPositionCreationFunds(numAccounts, numNoLock int, coin0Denom, coin1Denom string) sdk.Coins { + unusedPositionCreationFunds := sdk.Coins{} + for i := 1; i < numAccounts; i++ { + balances := s.App.BankKeeper.GetAllBalances(s.Ctx, s.TestAccs[i]) + unusedPositionCreationFunds = unusedPositionCreationFunds.Add(sdk.NewCoin(coin0Denom, balances.AmountOf(coin0Denom))) + unusedPositionCreationFunds = unusedPositionCreationFunds.Add(sdk.NewCoin(coin1Denom, balances.AmountOf(coin1Denom))) + } + return unusedPositionCreationFunds +}