diff --git a/x/gamm/pool-models/internal/cfmm_common/lp.go b/x/gamm/pool-models/internal/cfmm_common/lp.go index 18ea63e782a..f3f0b4f2571 100644 --- a/x/gamm/pool-models/internal/cfmm_common/lp.go +++ b/x/gamm/pool-models/internal/cfmm_common/lp.go @@ -10,13 +10,13 @@ import ( "github.com/osmosis-labs/osmosis/v7/x/gamm/types" ) -const errMsgFormatSharesLargerThanMax = "%d resulted shares is larger than the max amount of %d" +const errMsgFormatSharesLargerThanMax = "%s resulted shares is larger than the max amount of %s" // CalcExitPool returns how many tokens should come out, when exiting k LP shares against a "standard" CFMM func CalcExitPool(ctx sdk.Context, pool types.PoolI, exitingShares sdk.Int, exitFee sdk.Dec) (sdk.Coins, error) { totalShares := pool.GetTotalShares() if exitingShares.GTE(totalShares) { - return sdk.Coins{}, sdkerrors.Wrapf(types.ErrLimitMaxAmount, errMsgFormatSharesLargerThanMax, exitingShares.Int64(), totalShares.Uint64()) + return sdk.Coins{}, sdkerrors.Wrapf(types.ErrLimitMaxAmount, errMsgFormatSharesLargerThanMax, exitingShares, totalShares) } // refundedShares = exitingShares * (1 - exit fee) diff --git a/x/gamm/pool-models/internal/cfmm_common/lp_test.go b/x/gamm/pool-models/internal/cfmm_common/lp_test.go new file mode 100644 index 00000000000..33f575af8b5 --- /dev/null +++ b/x/gamm/pool-models/internal/cfmm_common/lp_test.go @@ -0,0 +1,139 @@ +package cfmm_common_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/osmosis-labs/osmosis/v7/x/gamm/pool-models/balancer" + "github.com/osmosis-labs/osmosis/v7/x/gamm/pool-models/internal/cfmm_common" + "github.com/osmosis-labs/osmosis/v7/x/gamm/pool-models/stableswap" + gammtypes "github.com/osmosis-labs/osmosis/v7/x/gamm/types" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// a helper function used to multiply coins +func mulCoins(coins sdk.Coins, multiplier sdk.Dec) sdk.Coins { + outCoins := sdk.Coins{} + for _, coin := range coins { + outCoin := sdk.NewCoin(coin.Denom, multiplier.MulInt(coin.Amount).TruncateInt()) + if !outCoin.Amount.IsZero() { + outCoins = append(outCoins, outCoin) + } + } + return outCoins +} + +func TestCalcExitPool(t *testing.T) { + + emptyContext := sdk.Context{} + + twoStablePoolAssets := sdk.NewCoins( + sdk.NewInt64Coin("foo", 1000000000), + sdk.NewInt64Coin("bar", 1000000000), + ) + + threeBalancerPoolAssets := []balancer.PoolAsset{ + {Token: sdk.NewInt64Coin("foo", 2000000000), Weight: sdk.NewIntFromUint64(5)}, + {Token: sdk.NewInt64Coin("bar", 3000000000), Weight: sdk.NewIntFromUint64(5)}, + {Token: sdk.NewInt64Coin("baz", 4000000000), Weight: sdk.NewIntFromUint64(5)}, + } + + // create these pools used for testing + twoAssetPool, err := stableswap.NewStableswapPool( + 1, + stableswap.PoolParams{ExitFee: sdk.ZeroDec()}, + twoStablePoolAssets, + "", + time.Now(), + ) + require.NoError(t, err) + + threeAssetPool, err := balancer.NewBalancerPool( + 1, + balancer.PoolParams{SwapFee: sdk.ZeroDec(), ExitFee: sdk.ZeroDec()}, + threeBalancerPoolAssets, + "", + time.Now(), + ) + require.NoError(t, err) + + twoAssetPoolWithExitFee, err := stableswap.NewStableswapPool( + 1, + stableswap.PoolParams{ExitFee: sdk.MustNewDecFromStr("0.0001")}, + twoStablePoolAssets, + "", + time.Now(), + ) + require.NoError(t, err) + + threeAssetPoolWithExitFee, err := balancer.NewBalancerPool( + 1, + balancer.PoolParams{SwapFee: sdk.ZeroDec(), ExitFee: sdk.MustNewDecFromStr("0.0002")}, + threeBalancerPoolAssets, + "", + time.Now(), + ) + require.NoError(t, err) + + tests := []struct { + name string + pool gammtypes.PoolI + exitingShares sdk.Int + expError bool + }{ + { + name: "two-asset pool, exiting shares grater than total shares", + pool: &twoAssetPool, + exitingShares: twoAssetPool.GetTotalShares().AddRaw(1), + expError: true, + }, + { + name: "three-asset pool, exiting shares grater than total shares", + pool: &threeAssetPool, + exitingShares: threeAssetPool.GetTotalShares().AddRaw(1), + expError: true, + }, + { + name: "two-asset pool, valid exiting shares", + pool: &twoAssetPool, + exitingShares: twoAssetPool.GetTotalShares().QuoRaw(2), + expError: false, + }, + { + name: "three-asset pool, valid exiting shares", + pool: &threeAssetPool, + exitingShares: sdk.NewIntFromUint64(3000000000000), + expError: false, + }, + { + name: "two-asset pool with exit fee, valid exiting shares", + pool: &twoAssetPoolWithExitFee, + exitingShares: twoAssetPoolWithExitFee.GetTotalShares().QuoRaw(2), + expError: false, + }, + { + name: "three-asset pool with exit fee, valid exiting shares", + pool: &threeAssetPoolWithExitFee, + exitingShares: sdk.NewIntFromUint64(7000000000000), + expError: false, + }, + } + + for _, test := range tests { + // using empty context since, currently, the context is not used anyway. This might be changed in the future + exitFee := test.pool.GetExitFee(emptyContext) + exitCoins, err := cfmm_common.CalcExitPool(emptyContext, test.pool, test.exitingShares, exitFee) + if test.expError { + require.Error(t, err, "test: %v", test.name) + } else { + require.NoError(t, err, "test: %v", test.name) + + // exitCoins = ( (1 - exitFee) * exitingShares / poolTotalShares ) * poolTotalLiquidity + expExitCoins := mulCoins(test.pool.GetTotalPoolLiquidity(emptyContext), (sdk.OneDec().Sub(exitFee)).MulInt(test.exitingShares).QuoInt(test.pool.GetTotalShares())) + require.Equal(t, expExitCoins.Sort().String(), exitCoins.Sort().String(), "test: %v", test.name) + } + } +}