diff --git a/app/app.go b/app/app.go index 0a5a463d8..c9a4d859d 100644 --- a/app/app.go +++ b/app/app.go @@ -226,7 +226,9 @@ const ( ) var ( - Upgrades = []upgrades.Upgrade{v500.Upgrade} + Upgrades = []upgrades.Upgrade{ + v500.Upgrade, + } // DefaultNodeHome default home directories for the application daemon DefaultNodeHome string diff --git a/app/upgrades/v5.0.0/upgrades.go b/app/upgrades/v5.0.0/upgrades.go index 8a2f455ad..ab3b82543 100644 --- a/app/upgrades/v5.0.0/upgrades.go +++ b/app/upgrades/v5.0.0/upgrades.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "cosmossdk.io/math" upgradetypes "cosmossdk.io/x/upgrade/types" adminmoduletypes "github.com/cosmos/admin-module/v2/x/adminmodule/types" "github.com/cosmos/cosmos-sdk/codec" @@ -44,6 +45,11 @@ func CreateUpgradeHandler( } } + err = upgradePools(ctx, *keepers.DexKeeper) + if err != nil { + return nil, err + } + err = setMarketMapParams(ctx, keepers.MarketmapKeeper) if err != nil { return nil, err @@ -77,6 +83,48 @@ func upgradeDexPause(ctx sdk.Context, k dexkeeper.Keeper) error { return nil } +func upgradePools(ctx sdk.Context, k dexkeeper.Keeper) error { + // Due to an issue with autoswap logic any pools with multiple shareholders must be withdrawn to ensure correct accounting + ctx.Logger().Info("Migrating Pools...") + + allSharesholders := k.GetAllPoolShareholders(ctx) + + for poolID, shareholders := range allSharesholders { + if len(shareholders) > 1 { + pool, found := k.GetPoolByID(ctx, poolID) + if !found { + return fmt.Errorf("cannot find pool with ID %d", poolID) + } + for _, shareholder := range shareholders { + addr := sdk.MustAccAddressFromBech32(shareholder.Address) + pairID := pool.LowerTick0.Key.TradePairId.MustPairID() + tick := pool.CenterTickIndexToken1() + fee := pool.Fee() + nShares := shareholder.Shares + + reserve0Removed, reserve1Removed, sharesBurned, err := k.WithdrawCore(ctx, pairID, addr, addr, []math.Int{nShares}, []int64{tick}, []uint64{fee}) + if err != nil { + return fmt.Errorf("user %s failed to withdraw from pool %d", addr, poolID) + } + + ctx.Logger().Info( + "Withdrew user from pool", + "User", addr.String(), + "Pool", poolID, + "SharesBurned", sharesBurned.String(), + "Reserve0Withdrawn", reserve0Removed.String(), + "Reserve1Withdrawn", reserve1Removed.String(), + ) + + } + } + } + + ctx.Logger().Info("Finished migrating Pools...") + + return nil +} + func upgradeIbcRateLimitSetContract(ctx sdk.Context, k ibcratelimitkeeper.Keeper) error { // Set the dex to paused ctx.Logger().Info("Setting ibc rate limiting contract...") diff --git a/app/upgrades/v5.0.0/upgrades_test.go b/app/upgrades/v5.0.0/upgrades_test.go index a223707eb..f8403d210 100644 --- a/app/upgrades/v5.0.0/upgrades_test.go +++ b/app/upgrades/v5.0.0/upgrades_test.go @@ -6,6 +6,8 @@ import ( "cosmossdk.io/math" upgradetypes "cosmossdk.io/x/upgrade/types" + sdk "github.com/cosmos/cosmos-sdk/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -85,6 +87,112 @@ func (suite *UpgradeTestSuite) TestUpgradeDexPause() { suite.ErrorIs(err, dextypes.ErrDexPaused) } +func (suite *UpgradeTestSuite) TestPoolMigrationSingleShareHolder() { + var ( + app = suite.GetNeutronZoneApp(suite.ChainA) + ctx = suite.ChainA.GetContext().WithChainID("neutron-1") + alice = []byte("alice") + pairID = &dextypes.PairID{Token0: "TokenA", Token1: "TokenB"} + depositAmount = math.NewInt(10_000) + ) + + // create a pool with 1 shareholder + FundAccount(app.BankKeeper, ctx, alice, sdk.NewCoins(sdk.NewCoin("TokenA", depositAmount))) + shares, err := suite.makeDeposit(ctx, app.DexKeeper, alice, pairID, depositAmount, math.ZeroInt(), 0, 1) + suite.NoError(err) + + // run upgrade + upgrade := upgradetypes.Plan{ + Name: v500.UpgradeName, + Info: "some text here", + Height: 100, + } + suite.NoError(app.UpgradeKeeper.ApplyUpgrade(ctx, upgrade)) + + // assert pool and shareholder balance are unchanged + poolID, err := dextypes.ParsePoolIDFromDenom(shares[0].Denom) + suite.NoError(err) + + pool, _ := app.DexKeeper.GetPoolByID(ctx, poolID) + + suite.True(pool.LowerTick0.ReservesMakerDenom.Equal(depositAmount), "Pool value changed") + aliceBalance := app.BankKeeper.GetAllBalances(ctx, alice) + suite.True(aliceBalance.Equal(shares)) +} + +func (suite *UpgradeTestSuite) TestPoolMigrationMultiShareHolder() { + var ( + app = suite.GetNeutronZoneApp(suite.ChainA) + ctx = suite.ChainA.GetContext().WithChainID("neutron-1") + alice = []byte("alice") + bob = []byte("bob") + pairID = &dextypes.PairID{Token0: "TokenA", Token1: "TokenB"} + depositAmount = math.NewInt(10_000) + initialBalance = sdk.NewCoins(sdk.NewCoin("TokenA", depositAmount)) + ) + FundAccount(app.BankKeeper, ctx, alice, initialBalance) + FundAccount(app.BankKeeper, ctx, bob, initialBalance) + + // create a pool with 2 shareholders + shares, err := suite.makeDeposit(ctx, app.DexKeeper, alice, pairID, depositAmount, math.ZeroInt(), 0, 1) + suite.NoError(err) + aliceBalance := app.BankKeeper.GetAllBalances(ctx, alice) + suite.True(aliceBalance.Equal(shares)) + + shares, err = suite.makeDeposit(ctx, app.DexKeeper, bob, pairID, depositAmount, math.ZeroInt(), 0, 1) + suite.NoError(err) + bobBalance := app.BankKeeper.GetAllBalances(ctx, bob) + suite.True(bobBalance.Equal(shares)) + + // run upgrade + upgrade := upgradetypes.Plan{ + Name: v500.UpgradeName, + Info: "some text here", + Height: 100, + } + suite.NoError(app.UpgradeKeeper.ApplyUpgrade(ctx, upgrade)) + + // assert that all users have withdrawn from the pool + poolID, err := dextypes.ParsePoolIDFromDenom(shares[0].Denom) + suite.NoError(err) + + pool, _ := app.DexKeeper.GetPoolByID(ctx, poolID) + suite.True(pool.LowerTick0.ReservesMakerDenom.Equal(math.ZeroInt()), "Pool not withdrawn") + + // AND funds are returned to the users + aliceBalance = app.BankKeeper.GetAllBalances(ctx, alice) + suite.True(aliceBalance.Equal(initialBalance)) + + bobBalance = app.BankKeeper.GetAllBalances(ctx, bob) + suite.True(bobBalance.Equal(initialBalance)) +} + +func FundAccount(bankKeeper bankkeeper.Keeper, ctx sdk.Context, addr sdk.AccAddress, amounts sdk.Coins) { + if err := bankKeeper.MintCoins(ctx, dextypes.ModuleName, amounts); err != nil { + panic(err) + } + + if err := bankKeeper.SendCoinsFromModuleToAccount(ctx, dextypes.ModuleName, addr, amounts); err != nil { + panic(err) + } +} + +func (suite *UpgradeTestSuite) makeDeposit( + ctx sdk.Context, + k dexkeeper.Keeper, + addr sdk.AccAddress, + pairID *dextypes.PairID, + amount0, amount1 math.Int, + tick int64, + fee uint64, +) (sharesIssued sdk.Coins, err error) { + deposit0, deposit1, sharesIssued, _, err := k.DepositCore(ctx, pairID, addr, addr, []math.Int{amount0}, []math.Int{amount1}, []int64{tick}, []uint64{fee}, []*dextypes.DepositOptions{{}}) + suite.True(deposit0[0].Equal(amount0)) + suite.True(deposit1[0].Equal(amount1)) + + return sharesIssued, err +} + func (suite *UpgradeTestSuite) TestUpgradeSetRateLimitContractMainnet() { var ( app = suite.GetNeutronZoneApp(suite.ChainA) diff --git a/x/dex/keeper/core_helper_test.go b/x/dex/keeper/core_helper_test.go index 1c645006b..eeb01d698 100644 --- a/x/dex/keeper/core_helper_test.go +++ b/x/dex/keeper/core_helper_test.go @@ -10,6 +10,7 @@ import ( neutronapp "github.com/neutron-org/neutron/v5/app" "github.com/neutron-org/neutron/v5/testutil" + math_utils "github.com/neutron-org/neutron/v5/utils/math" "github.com/neutron-org/neutron/v5/x/dex/types" ) @@ -65,7 +66,8 @@ func (s *CoreHelpersTestSuite) setLPAtFee1Pool(tickIndex int64, amountA, amountB existingShares := s.app.BankKeeper.GetSupply(s.ctx, pool.GetPoolDenom()).Amount - totalShares := pool.CalcSharesMinted(amountAInt, amountBInt, existingShares) + depositAmountAsToken0 := types.CalcAmountAsToken0(amountAInt, amountBInt, pool.MustCalcPrice1To0Center()) + totalShares := pool.CalcSharesMinted(depositAmountAsToken0, existingShares, math_utils.ZeroPrecDec()) err = s.app.DexKeeper.MintShares(s.ctx, s.alice, sdk.NewCoins(totalShares)) s.Require().NoError(err) diff --git a/x/dex/keeper/grpc_query_pool_test.go b/x/dex/keeper/grpc_query_pool_test.go index f32dc6e98..9d96cd845 100644 --- a/x/dex/keeper/grpc_query_pool_test.go +++ b/x/dex/keeper/grpc_query_pool_test.go @@ -25,7 +25,7 @@ func TestPoolQuerySingle(t *testing.T) { desc: "First", request: &types.QueryPoolRequest{ PairId: "TokenA<>TokenB", - TickIndex: msgs[0].CenterTickIndex(), + TickIndex: msgs[0].CenterTickIndexToken1(), Fee: msgs[0].Fee(), }, response: &types.QueryPoolResponse{Pool: msgs[0]}, @@ -34,7 +34,7 @@ func TestPoolQuerySingle(t *testing.T) { desc: "Second", request: &types.QueryPoolRequest{ PairId: "TokenA<>TokenB", - TickIndex: msgs[1].CenterTickIndex(), + TickIndex: msgs[1].CenterTickIndexToken1(), Fee: msgs[1].Fee(), }, response: &types.QueryPoolResponse{Pool: msgs[1]}, diff --git a/x/dex/keeper/integration_deposit_autoswap_unit_test.go b/x/dex/keeper/integration_deposit_autoswap_unit_test.go index 1e6dc438f..8cf8ddb95 100644 --- a/x/dex/keeper/integration_deposit_autoswap_unit_test.go +++ b/x/dex/keeper/integration_deposit_autoswap_unit_test.go @@ -1,109 +1,111 @@ package keeper_test -import "github.com/neutron-org/neutron/v5/x/dex/types" +import ( + "cosmossdk.io/math" +) -func (s *DexTestSuite) TestAutoswapperWithdraws() { +func (s *DexTestSuite) TestAutoswapSingleSided0To1() { s.fundAliceBalances(50, 50) - s.fundBobBalances(50, 50) - - // GIVEN - // create spread around -1, 1 - bobDep0 := 10 - bobDep1 := 10 - tickIndex := 200 - fee := 5 - - bobSharesMinted := s.calcSharesMinted(int64(tickIndex), int64(bobDep0), int64(bobDep1)) - - s.bobDeposits(NewDeposit(bobDep0, bobDep1, tickIndex, fee)) - s.assertBobBalances(40, 40) - s.assertDexBalances(10, 10) - - // Alice deposits at a different balance ratio - s.aliceDeposits(NewDepositWithOptions(12, 5, tickIndex, fee, types.DepositOptions{DisableAutoswap: false})) - s.assertAliceBalances(38, 45) - s.assertDexBalances(22, 15) - - // Calculated expected amounts out - autoswapSharesMinted := s.calcAutoswapSharesMinted(int64(tickIndex), uint64(fee), 7, 0, 5, 5, bobSharesMinted.Int64(), bobSharesMinted.Int64()) - // totalShares := autoswapSharesMinted.Add(math.NewInt(20)) - - aliceExpectedBalance0, aliceExpectedBalance1, dexExpectedBalance0, dexExpectedBalance1 := s.calcExpectedBalancesAfterWithdrawOnePool(autoswapSharesMinted, s.alice, int64(tickIndex), uint64(fee)) - - s.aliceWithdraws(NewWithdrawalInt(autoswapSharesMinted, int64(tickIndex), uint64(fee))) - - s.assertAliceBalancesInt(aliceExpectedBalance0, aliceExpectedBalance1) - s.assertDexBalancesInt(dexExpectedBalance0, dexExpectedBalance1) + s.fundBobBalances(50, 0) + + // GIVEN a pool with double-sided liquidity + s.aliceDeposits(NewDeposit(50, 50, 2000, 2)) + s.assertAccountSharesInt(s.alice, 2000, 2, math.NewInt(111069527)) + + // WHEN bob deposits only TokenA + s.bobDeposits(NewDeposit(50, 0, 2000, 2)) + s.assertPoolLiquidity(100, 50, 2000, 2) + + // THEN his deposit is autoswapped + // He receives 49.985501 shares + // swapAmount = 27.491577 Token0 see pool.go for the math + // (50 - 27.491577) / ( 27.491577 / 1.0001^2000) = 1 ie. pool ratio is maintained + // depositValue = depositAmount - (autoswapedAmountAsToken0 * fee) + // = 50 - 27.491577 * (1 - 1.0001^-2) + // = 49.9945025092 + // SharesIssued = depositValue * existing shares / (existingValue + autoSwapFee) + // = 49.9945025092 * 111.069527 / (111.069527 + .005497490762642563860802206452577) + // = 49.992027 + + s.assertAccountSharesInt(s.bob, 2000, 2, math.NewInt(49992027)) } -func (s *DexTestSuite) TestAutoswapOtherDepositorWithdraws() { +func (s *DexTestSuite) TestAutoswapSingleSided1To0() { s.fundAliceBalances(50, 50) - s.fundBobBalances(50, 50) - - // GIVEN - // create spread around -1, 1 - bobDep0 := 10 - bobDep1 := 10 - tickIndex := 150 - fee := 10 - - bobSharesMinted := s.calcSharesMinted(int64(tickIndex), int64(bobDep0), int64(bobDep1)) - - s.bobDeposits(NewDeposit(bobDep0, bobDep1, tickIndex, fee)) - s.assertBobBalances(40, 40) - s.assertDexBalances(10, 10) - - // Alice deposits at a different balance ratio - s.aliceDeposits(NewDepositWithOptions(10, 7, tickIndex, fee, types.DepositOptions{DisableAutoswap: false})) - s.assertAliceBalances(40, 43) - s.assertDexBalances(20, 17) - - // Calculated expected amounts out - - bobExpectedBalance0, bobExpectedBalance1, dexExpectedBalance0, dexExpectedBalance1 := s.calcExpectedBalancesAfterWithdrawOnePool(bobSharesMinted, s.bob, int64(tickIndex), uint64(fee)) - - s.bobWithdraws(NewWithdrawalInt(bobSharesMinted, int64(tickIndex), uint64(fee))) - - s.assertBobBalancesInt(bobExpectedBalance0, bobExpectedBalance1) - s.assertDexBalancesInt(dexExpectedBalance0, dexExpectedBalance1) + s.fundBobBalances(0, 50) + + // GIVEN a pool with double-sided liquidity + s.aliceDeposits(NewDeposit(50, 50, 2000, 2)) + s.assertAccountSharesInt(s.alice, 2000, 2, math.NewInt(111069527)) + + // WHEN bob deposits only TokenB + s.bobDeposits(NewDeposit(0, 50, 2000, 2)) + s.assertPoolLiquidity(50, 100, 2000, 2) + + // THEN his deposit is autoswapped + // He receives 61.0 shares + // depositAmountAsToken0 = 50 * 1.0001^2000 = 61.06952725039 + // swapAmount = 22.508423 Token1 see pool.go for the math + // swapAmountAsToken0 = 27.4915750352 + // (22.508423 * 1.0001^2000) / (50 - 22.508423) = 1 ie. pool ratio is maintained + // depositValue = depositAmountAsToken0 - (autoswapedAmountAsToken0 * fee) + // = 61.06952725039 - 27.4915750352 * (1 - 1.0001^-2) + // = 61.06402976002 + // SharesIssued = depositValue * existing shares / (existingValue + autoSwapFee) + // = 61.06402976002 * 111.069527 / (111.069527 + 0.00549749037) + // = 61061007 + + s.assertAccountSharesInt(s.bob, 2000, 2, math.NewInt(61061007)) } -func (s *DexTestSuite) TestAutoswapBothWithdraws() { - s.fundAliceBalances(50, 50) +func (s *DexTestSuite) TestAutoswapDoubleSided0To1() { + s.fundAliceBalances(30, 50) s.fundBobBalances(50, 50) - // GIVEN - // create spread around -1, 1 - bobDep0 := 10 - bobDep1 := 10 - tickIndex := 10000 - fee := 5 - - s.bobDeposits(NewDeposit(bobDep0, bobDep1, tickIndex, fee)) - bobSharesMinted := s.getAccountShares(s.bob, "TokenA", "TokenB", int64(tickIndex), uint64(fee)) - s.assertBobBalances(40, 40) - s.assertDexBalances(10, 10) - - // Alice deposits at a different balance ratio - s.aliceDeposits(NewDepositWithOptions(10, 5, tickIndex, fee, types.DepositOptions{DisableAutoswap: false})) - s.assertAliceBalances(40, 45) - s.assertDexBalances(20, 15) - - // Calculated expected amounts out - autoswapSharesMinted := s.getAccountShares(s.alice, "TokenA", "TokenB", int64(tickIndex), uint64(fee)) - // totalShares := autoswapSharesMinted.Add(math.NewInt(20)) - - bobExpectedBalance0, bobExpectedBalance1, dexExpectedBalance0, dexExpectedBalance1 := s.calcExpectedBalancesAfterWithdrawOnePool(bobSharesMinted, s.bob, int64(tickIndex), uint64(fee)) - - s.bobWithdraws(NewWithdrawalInt(bobSharesMinted, int64(tickIndex), uint64(fee))) - - s.assertBobBalancesInt(bobExpectedBalance0, bobExpectedBalance1) - s.assertDexBalancesInt(dexExpectedBalance0, dexExpectedBalance1) - - aliceExpectedBalance0, aliceExpectedBalance1, dexExpectedBalance0, dexExpectedBalance1 := s.calcExpectedBalancesAfterWithdrawOnePool(autoswapSharesMinted, s.alice, int64(tickIndex), uint64(fee)) + // GIVEN a pool with double-sided liquidity at ratio 3:5 + s.aliceDeposits(NewDeposit(30, 50, -4000, 10)) + s.assertAccountSharesInt(s.alice, -4000, 10, math.NewInt(63516672)) + + // WHEN bob deposits a ratio of 1:1 tokenA and B + s.bobDeposits(NewDeposit(50, 50, -4000, 10)) + s.assertPoolLiquidity(80, 100, -4000, 10) + + // THEN his deposit is autoswapped + // He receives 83.5 shares + // swapAmount = 10.553662 Token0 see pool.go for the math + // (50 - 10.553662) / (50 + 10.553662 / 1.0001^-4000) = 3/5 ie. pool ratio is maintained + // depositValue = depositAmountAsToken0 - (autoswapedAmountAsToken0 * fee) + // = 83.5166725838 - 10.553662 * (1 - 1.0001^-10) + // = 83.506124724 + // SharesIssued = depositValue * existing shares / (existingValue + autoSwapFee) + // = 83.506124724 * 63.516672 / (63.516672 + .010547859) + // = 83.492258 + + s.assertAccountSharesInt(s.bob, -4000, 10, math.NewInt(83492258)) +} - s.aliceWithdraws(NewWithdrawalInt(autoswapSharesMinted, int64(tickIndex), uint64(fee))) +func (s *DexTestSuite) TestAutoswapDoubleSided1To0() { + s.fundAliceBalances(50, 30) + s.fundBobBalances(50, 50) - s.assertAliceBalancesInt(aliceExpectedBalance0, aliceExpectedBalance1) - s.assertDexBalancesInt(dexExpectedBalance0, dexExpectedBalance1) + // GIVEN a pool with double-sided liquidity + s.aliceDeposits(NewDeposit(50, 30, -4000, 10)) + s.assertAccountSharesInt(s.alice, -4000, 10, math.NewInt(70110003)) + + // WHEN bob deposits a ratio of 1:1 tokenA and B + s.bobDeposits(NewDeposit(50, 50, -4000, 10)) + s.assertPoolLiquidity(100, 80, -4000, 10) + + // THEN his deposit is autoswapped + // He receives 83.5 shares + // swapAmount = 14.263300 Token1 see pool.go for the math + // swapAmountAsToken0 = 9.5611671213 + // depositValue = depositAmountAsToken0 - (autoswapedAmountAsToken0 * fee) + // = 83.5166725838 - 9.5611671213 * (1 - 1.0001^-10) + // = 83.5071166732 + // SharesIssued = depositValue * existing shares / (existingValue + autoSwapFee) + // = 83.5071166732 * 70.110003 / (70.1100035503 + .009555910582) + // = 83.495735 + + s.assertAccountSharesInt(s.bob, -4000, 10, math.NewInt(83495735)) } diff --git a/x/dex/keeper/integration_deposit_multi_test.go b/x/dex/keeper/integration_deposit_multi_test.go index a33a6064d..591942343 100644 --- a/x/dex/keeper/integration_deposit_multi_test.go +++ b/x/dex/keeper/integration_deposit_multi_test.go @@ -23,7 +23,7 @@ func (s *DexTestSuite) TestDepositMultiCompleteFailure() { s.assertAliceDepositFails( err, NewDeposit(5, 0, 2, 1), - NewDeposit(0, 5, 0, 1), // fails + NewDepositWithOptions(0, 5, 0, 1, types.DepositOptions{DisableAutoswap: true}), // fails ) } diff --git a/x/dex/keeper/integration_deposit_singlesided_test.go b/x/dex/keeper/integration_deposit_singlesided_test.go index 71e3847c8..81a8e65e9 100644 --- a/x/dex/keeper/integration_deposit_singlesided_test.go +++ b/x/dex/keeper/integration_deposit_singlesided_test.go @@ -368,7 +368,7 @@ func (s *DexTestSuite) TestDepositSingleSidedZeroTrueAmountsFail() { // second deposit's ratio is different than pool after the first, so amounts will be rounded to 0,0 and tx will fail err := types.ErrZeroTrueDeposit - s.assertAliceDepositFails(err, NewDeposit(0, 5, 0, 1)) + s.assertAliceDepositFails(err, NewDepositWithOptions(0, 5, 0, 1, types.DepositOptions{DisableAutoswap: true})) } func (s *DexTestSuite) TestDepositNilOptions() { diff --git a/x/dex/keeper/msg_server_test.go b/x/dex/keeper/msg_server_test.go index 6f8272a0a..5fb64f8d6 100644 --- a/x/dex/keeper/msg_server_test.go +++ b/x/dex/keeper/msg_server_test.go @@ -1506,77 +1506,6 @@ func (s *DexTestSuite) assertNLimitOrderExpiration(expected int) { s.Assert().Equal(expected, len(exps)) } -func (s *DexTestSuite) calcAutoswapSharesMinted( - centerTick int64, - fee uint64, - residual0, residual1, balanced0, balanced1, totalShares, valuePool int64, -) sdkmath.Int { - residual0Int, residual1Int, balanced0Int, balanced1Int, totalSharesInt, valuePoolInt := sdkmath.NewInt(residual0), - sdkmath.NewInt(residual1), - sdkmath.NewInt(balanced0), - sdkmath.NewInt(balanced1), - sdkmath.NewInt(totalShares), - sdkmath.NewInt(valuePool) - - // residualValue = 1.0001^-f * residualAmount0 + 1.0001^{i-f} * residualAmount1 - // balancedValue = balancedAmount0 + 1.0001^{i} * balancedAmount1 - // value = residualValue + balancedValue - // shares minted = value * totalShares / valuePool - - centerPrice := types.MustCalcPrice(-1 * centerTick) - leftPrice := types.MustCalcPrice(-1 * (centerTick - int64(fee))) - discountPrice := types.MustCalcPrice(-1 * int64(fee)) - - balancedValue := math_utils.NewPrecDecFromInt(balanced0Int). - Add(centerPrice.MulInt(balanced1Int)). - TruncateInt() - residualValue := discountPrice.MulInt(residual0Int). - Add(leftPrice.Mul(math_utils.NewPrecDecFromInt(residual1Int))). - TruncateInt() - valueMint := balancedValue.Add(residualValue) - - return valueMint.Mul(totalSharesInt).Quo(valuePoolInt) -} - -func (s *DexTestSuite) calcSharesMinted(centerTick, amount0Int, amount1Int int64) sdkmath.Int { - amount0, amount1 := sdkmath.NewInt(amount0Int), sdkmath.NewInt(amount1Int) - centerPrice := types.MustCalcPrice(-1 * centerTick) - - return math_utils.NewPrecDecFromInt(amount0).Add(centerPrice.Mul(math_utils.NewPrecDecFromInt(amount1))).TruncateInt() -} - -func (s *DexTestSuite) calcExpectedBalancesAfterWithdrawOnePool( - sharesMinted sdkmath.Int, - account sdk.AccAddress, - tickIndex int64, - fee uint64, -) (sdkmath.Int, sdkmath.Int, sdkmath.Int, sdkmath.Int) { - dexCurrentBalance0 := s.App.BankKeeper.GetBalance( - s.Ctx, - s.App.AccountKeeper.GetModuleAddress("dex"), - "TokenA", - ).Amount - dexCurrentBalance1 := s.App.BankKeeper.GetBalance( - s.Ctx, - s.App.AccountKeeper.GetModuleAddress("dex"), - "TokenB", - ).Amount - currentBalance0 := s.App.BankKeeper.GetBalance(s.Ctx, account, "TokenA").Amount - currentBalance1 := s.App.BankKeeper.GetBalance(s.Ctx, account, "TokenB").Amount - amountPool0, amountPool1 := s.getLiquidityAtTick(tickIndex, fee) - poolShares := s.getPoolShares("TokenA", "TokenB", tickIndex, fee) - - amountOut0 := amountPool0.Mul(sharesMinted).Quo(poolShares) - amountOut1 := amountPool1.Mul(sharesMinted).Quo(poolShares) - - expectedBalance0 := currentBalance0.Add(amountOut0) - expectedBalance1 := currentBalance1.Add(amountOut1) - dexExpectedBalance0 := dexCurrentBalance0.Sub(amountOut0) - dexExpectedBalance1 := dexCurrentBalance1.Sub(amountOut1) - - return expectedBalance0, expectedBalance1, dexExpectedBalance0, dexExpectedBalance1 -} - func (s *DexTestSuite) nextBlockWithTime(blockTime time.Time) { newCtx := s.Ctx.WithBlockTime(blockTime) s.Ctx = newCtx diff --git a/x/dex/keeper/pool.go b/x/dex/keeper/pool.go index 5b95ec959..8da8a1850 100644 --- a/x/dex/keeper/pool.go +++ b/x/dex/keeper/pool.go @@ -165,3 +165,22 @@ func (k Keeper) SetPoolCount(ctx sdk.Context, count uint64) { binary.BigEndian.PutUint64(bz, count) store.Set(byteKey, bz) } + +func (k Keeper) GetAllPoolShareholders(ctx sdk.Context) map[uint64][]types.PoolShareholder { + result := make(map[uint64][]types.PoolShareholder) + balances := k.bankKeeper.GetAccountsBalances(ctx) + for _, balance := range balances { + for _, coin := range balance.Coins { + // Check if the Denom is a PoolShare denom + poolID, err := types.ParsePoolIDFromDenom(coin.Denom) + if err != nil { + // This is not a PoolShare denom + continue + } + shareholderInfo := types.PoolShareholder{Address: balance.Address, Shares: coin.Amount} + result[poolID] = append(result[poolID], shareholderInfo) + + } + } + return result +} diff --git a/x/dex/keeper/pool_test.go b/x/dex/keeper/pool_test.go index c628b12a3..73ca44619 100644 --- a/x/dex/keeper/pool_test.go +++ b/x/dex/keeper/pool_test.go @@ -74,7 +74,7 @@ func TestGetPoolIDByParams(t *testing.T) { id0, found := keeper.GetPoolIDByParams( ctx, items[0].LowerTick0.Key.TradePairId.MustPairID(), - items[0].CenterTickIndex(), + items[0].CenterTickIndexToken1(), items[0].Fee(), ) require.True(t, found) @@ -83,7 +83,7 @@ func TestGetPoolIDByParams(t *testing.T) { id1, found := keeper.GetPoolIDByParams( ctx, items[1].LowerTick0.Key.TradePairId.MustPairID(), - items[1].CenterTickIndex(), + items[1].CenterTickIndexToken1(), items[1].Fee(), ) require.True(t, found) diff --git a/x/dex/types/errors.go b/x/dex/types/errors.go index 4187226e3..ff0b765b2 100644 --- a/x/dex/types/errors.go +++ b/x/dex/types/errors.go @@ -57,7 +57,7 @@ var ( ErrZeroTrueDeposit = sdkerrors.Register( ModuleName, 1121, - "Cannot deposit double-sided liquidity in tick with prexisting single-sided liquidity.", + "Cannot deposit single-sided liquidity in tick with opposite liquidity while autoswap is disabled", ) ErrWithdrawEmptyLimitOrder = sdkerrors.Register( ModuleName, diff --git a/x/dex/types/expected_keepers.go b/x/dex/types/expected_keepers.go index 95c5c7a90..a925e867f 100644 --- a/x/dex/types/expected_keepers.go +++ b/x/dex/types/expected_keepers.go @@ -4,6 +4,7 @@ import ( "context" sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" ) // BankKeeper defines the expected interface needed to retrieve account balances. @@ -15,5 +16,6 @@ type BankKeeper interface { BurnCoins(ctx context.Context, moduleName string, amt sdk.Coins) error IterateAccountBalances(ctx context.Context, addr sdk.AccAddress, cb func(sdk.Coin) bool) GetSupply(ctx context.Context, denom string) sdk.Coin + GetAccountsBalances(ctx context.Context) []banktypes.Balance GetBalance(ctx context.Context, addr sdk.AccAddress, denom string) sdk.Coin } diff --git a/x/dex/types/pool.go b/x/dex/types/pool.go index 5a8cd9476..9d3093ce5 100644 --- a/x/dex/types/pool.go +++ b/x/dex/types/pool.go @@ -8,6 +8,11 @@ import ( "github.com/neutron-org/neutron/v5/x/dex/utils" ) +type PoolShareholder struct { + Address string + Shares math.Int +} + func NewPool( pairID *PairID, centerTickIndexNormalized int64, @@ -49,7 +54,7 @@ func MustNewPool( return pool } -func (p *Pool) CenterTickIndex() int64 { +func (p *Pool) CenterTickIndexToken1() int64 { feeInt64 := utils.MustSafeUint64ToInt64(p.Fee()) return p.UpperTick1.Key.TickIndexTakerToMaker - feeInt64 } @@ -115,34 +120,37 @@ func (p *Pool) Deposit( lowerReserve0 := &p.LowerTick0.ReservesMakerDenom upperReserve1 := &p.UpperTick1.ReservesMakerDenom - inAmount0, inAmount1 = CalcGreatestMatchingRatio( - *lowerReserve0, - *upperReserve1, - maxAmount0, - maxAmount1, - ) + centerPrice1To0 := p.MustCalcPrice1To0Center() + var depositValueAsToken0 math_utils.PrecDec + autoswapFee := math_utils.ZeroPrecDec() - if inAmount0.Equal(math.ZeroInt()) && inAmount1.Equal(math.ZeroInt()) { - return math.ZeroInt(), math.ZeroInt(), sdk.Coin{Denom: p.GetPoolDenom()} - } + if !autoswap { + inAmount0, inAmount1 = CalcGreatestMatchingRatio( + *lowerReserve0, + *upperReserve1, + maxAmount0, + maxAmount1, + ) + depositValueAsToken0 = CalcAmountAsToken0(inAmount0, inAmount1, centerPrice1To0) - outShares = p.CalcSharesMinted(inAmount0, inAmount1, existingShares) - - if autoswap { - residualAmount0 := maxAmount0.Sub(inAmount0) - residualAmount1 := maxAmount1.Sub(inAmount1) + } else { + residualAmount0, residualAmount1 := CalcAutoswapAmount( + *lowerReserve0, *upperReserve1, + maxAmount0, maxAmount1, + centerPrice1To0, + ) - // NOTE: Currently not doing anything with the error, - // but added error handling to all of the new functions for autoswap. - // Open to changing it however. - residualShares, _ := p.CalcResidualSharesMinted(residualAmount0, residualAmount1) + residualDepositValueAsToken0 := CalcAmountAsToken0(residualAmount0, residualAmount1, centerPrice1To0) + autoswapFee = p.CalcAutoswapFee(residualDepositValueAsToken0) - outShares = outShares.Add(residualShares) + fullDepositValueAsToken0 := CalcAmountAsToken0(maxAmount0, maxAmount1, centerPrice1To0) + depositValueAsToken0 = fullDepositValueAsToken0.Sub(autoswapFee) inAmount0 = maxAmount0 inAmount1 = maxAmount1 } + outShares = p.CalcSharesMinted(depositValueAsToken0, existingShares, autoswapFee) *lowerReserve0 = lowerReserve0.Add(inAmount0) *upperReserve1 = upperReserve1.Add(inAmount1) @@ -164,52 +172,35 @@ func (p *Pool) Price(tradePairID *TradePairID) math_utils.PrecDec { func (p *Pool) MustCalcPrice1To0Center() math_utils.PrecDec { // NOTE: We can safely call the error-less version of CalcPrice here because the pool object // has already been initialized with an upper and lower tick which satisfy a check for IsTickOutOfRange - return MustCalcPrice(-1 * p.CenterTickIndex()) + return MustCalcPrice(-1 * p.CenterTickIndexToken1()) } func (p *Pool) CalcSharesMinted( - amount0 math.Int, - amount1 math.Int, + depositValueAsToken0 math_utils.PrecDec, existingShares math.Int, + autoswapFee math_utils.PrecDec, ) (sharesMinted sdk.Coin) { price1To0Center := p.MustCalcPrice1To0Center() - valueMintedToken0 := CalcAmountAsToken0(amount0, amount1, price1To0Center) valueExistingToken0 := CalcAmountAsToken0( p.LowerTick0.ReservesMakerDenom, p.UpperTick1.ReservesMakerDenom, price1To0Center, ) + + totalValueWithAutoswapFeeToken0 := valueExistingToken0.Add(autoswapFee) var sharesMintedAmount math.Int if valueExistingToken0.GT(math_utils.ZeroPrecDec()) { - sharesMintedAmount = valueMintedToken0.MulInt(existingShares). - Quo(valueExistingToken0). + sharesMintedAmount = depositValueAsToken0.MulInt(existingShares). + Quo(totalValueWithAutoswapFeeToken0). TruncateInt() } else { - sharesMintedAmount = valueMintedToken0.TruncateInt() + sharesMintedAmount = depositValueAsToken0.TruncateInt() } return sdk.Coin{Denom: p.GetPoolDenom(), Amount: sharesMintedAmount} } -func (p *Pool) CalcResidualSharesMinted( - residualAmount0 math.Int, - residualAmount1 math.Int, -) (sharesMinted sdk.Coin, err error) { - fee := CalcFee(p.UpperTick1.Key.TickIndexTakerToMaker, p.LowerTick0.Key.TickIndexTakerToMaker) - valueMintedToken0, err := CalcResidualValue( - residualAmount0, - residualAmount1, - p.LowerTick0.MakerPrice, - fee, - ) - if err != nil { - return sdk.Coin{Denom: p.GetPoolDenom()}, err - } - - return sdk.Coin{Denom: p.GetPoolDenom(), Amount: valueMintedToken0.TruncateInt()}, nil -} - func (p *Pool) RedeemValue(sharesToRemove, totalShares math.Int) (outAmount0, outAmount1 math.Int) { reserves0 := &p.LowerTick0.ReservesMakerDenom reserves1 := &p.UpperTick1.ReservesMakerDenom @@ -235,7 +226,7 @@ func (p *Pool) Withdraw(sharesToRemove, totalShares math.Int) (outAmount0, outAm return outAmount0, outAmount1 } -// Balance trueAmount1 to the pool ratio +// Balance deposit amounts to match the existing ratio in the pool. If pool is empty allow any ratio. func CalcGreatestMatchingRatio( targetAmount0 math.Int, targetAmount1 math.Int, @@ -264,22 +255,48 @@ func CalcGreatestMatchingRatio( return resultAmount0, resultAmount1 } -func CalcResidualValue( - amount0, amount1 math.Int, - makerPriceToken0 math_utils.PrecDec, - fee int64, -) (math_utils.PrecDec, error) { - // ResidualValue = Amount0 * (Price1to0Center / Price1to0Upper) + Amount1 / MakerPriceToken0 - amount0Discount, err := CalcPrice(-fee) - if err != nil { - return math_utils.ZeroPrecDec(), err +// CalcAutoswapAmount calculates the smallest swap to match the current pool ratio. +// see: https://www.notion.so/Autoswap-Spec-ca5f35a4cd5b4dbf9ae27e0454ddd445?pvs=4#12032ea59b0e802c925efae10c3ca85f +func CalcAutoswapAmount( + reserves0, + reserves1, + depositAmount0, + depositAmount1 math.Int, + price1To0 math_utils.PrecDec, +) (resultAmount0, resultAmount1 math.Int) { + if reserves0.IsZero() && reserves1.IsZero() { + // The pool is empty, any deposit amount is allowed. Nothing to be swapped + return math.ZeroInt(), math.ZeroInt() } - return amount0Discount.MulInt(amount0).Add(math_utils.NewPrecDecFromInt(amount1).Quo(makerPriceToken0)), nil + reserves0Dec := math_utils.NewPrecDecFromInt(reserves0) + reserves1Dec := math_utils.NewPrecDecFromInt(reserves1) + // swapAmount = (reserves0*depositAmount1 - reserves1*depositAmount0) / (price * reserves1 + reserves0) + swapAmount := reserves0Dec.MulInt(depositAmount1).Sub(reserves1Dec.MulInt(depositAmount0)). + Quo(reserves0Dec.Add(reserves1Dec.Quo(price1To0))) + + switch { + case swapAmount.IsZero(): // nothing to be swapped + return math.ZeroInt(), math.ZeroInt() + + case swapAmount.IsPositive(): // Token1 needs to be swapped + return math.ZeroInt(), swapAmount.Ceil().TruncateInt() + + default: // Token0 needs to be swapped + amountSwappedAs1 := swapAmount.Neg() + + amountSwapped0 := amountSwappedAs1.Quo(price1To0) + return amountSwapped0.Ceil().TruncateInt(), math.ZeroInt() + } } -func CalcFee(upperTickIndex, lowerTickIndex int64) int64 { - return (upperTickIndex - lowerTickIndex) / 2 +func (p *Pool) CalcAutoswapFee(depositValueAsToken0 math_utils.PrecDec) math_utils.PrecDec { + feeInt64 := utils.MustSafeUint64ToInt64(p.Fee()) + feeAsPrice := MustCalcPrice(-feeInt64) + autoSwapFee := math_utils.OnePrecDec().Sub(feeAsPrice) + + // fee = depositValueAsToken0 * (1 - p(fee) ) + return autoSwapFee.Mul(depositValueAsToken0) } func CalcAmountAsToken0(amount0, amount1 math.Int, price1To0 math_utils.PrecDec) math_utils.PrecDec {